GPU_GUARD_MONOREPO/docs/superpowers/specs/2026-05-03-geo-s1-infrastructure-design.md
2026-05-20 21:39:12 +08:00

592 lines
27 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# S1 基础设施层 设计文档
> Geo 模块第一个子项目:账号矩阵 / IP 池 / 指纹浏览器 / 菜单。
> 上位文档:[`2026-05-03-geo-master-roadmap.md`](./2026-05-03-geo-master-roadmap.md)
| 元数据 | 值 |
|---|---|
| 子项目 | S1 基础设施层 |
| 创建日期 | 2026-05-03 |
| 估期 | ~9 天 |
| 状态 | 设计完成,待 user 复核 → writing-plans |
---
## 1. 目标
为 Geo 模块后续所有子项目S2/S3/S4提供**账号—IP—指纹浏览器**三位一体的基础设施。S1 完成后,用户能在 Neta 控制台:
1. 在 GEO 一级菜单下看到三个子页面
2. 创建一个 IP本地或第三方
3. 创建一个指纹浏览器 profile
4. 创建一个社媒账号,自动绑定 IP + Profile强 1:1:1
5. 点击「启动登录」打开真实浏览器,扫码/输账密/输短信完成登录
6. Cookie 自动抱回并加密存储
7. 删除账号时安全释放 IP 和 Profile
S1 **不涉及**任何平台特定行为(发布、评论、监测)。
---
## 2. 关键决策brainstorming 阶段已确认)
| 决策点 | 决策 |
|---|---|
| 子项目拆分 | 4 个子项目S1/S2/S3/S4先做 S1 |
| IP 池 | 抽象 `IProxyProvider` + 内置 `LocalProvider` + 占位 `TianqiProvider` |
| 浏览器(两层正交) | ① 浏览器进程层 `IBrowserProvider`S1 主实现 `PlainChromiumProvider`playwright-cli 直接启 Chromium+ 占位 `BitBrowser` `AntBrowser` `AdsPower`。② 自动化层 `BrowserAutomationService`:统一用 `playwright-cli -s={session}` 做 cookie/state/click/type不关心底层是哪种浏览器 |
| 绑定关系 | account ↔ proxy_ip ↔ browser_profile **严格 1:1:1**IP/Profile 上 `bindAccountId` 唯一索引 |
| 登录方式 | `playwright-cli -s={session} open {url} --persistent --headed` 启动有头浏览器 + 用户自己登录 + `BrowserAutomationService.getCookies()` 自动抱回 |
| 实现风格 | 分层 Provider 抽象 + account service 单点编排(与 NetaClaw `llm_providers/` 对齐) |
| 数据库 | 不写 SQL 文件Entity + TypeORM `synchronize` 自动建表;菜单 seed 用 `mcp__mysql__execute` |
| 加密 | geo 模块自建 `GeoEncryptService`(复用同一 AES-256-GCM 算法和 `SKILL_SECRET_KEY`/`APP_SECRET` 密钥派生逻辑,但接口签名为 `encrypt(plain: string): string` / `decrypt(cipher: string): string`,适配 cookie/密码等字符串场景。不直接调用 `SkillSecretService`——后者签名为 `encrypt(obj: Record<string,string>)` 且语义是 skill scoped secrets |
---
## 3. 与 Neta 现有架构契合度审计
| Neta 约定 | S1 实施 |
|---|---|
| 模块根目录 `src/modules/{name}/` | ✅ `src/modules/geo/` |
| `entity/` 文件下划线 + BaseEntity + 字段驼峰 | ✅ |
| `controller/admin/` + `@CoolController` 自动 CRUD | ✅ |
| `service/` 业务编排 | ✅ |
| `entities.ts` 注册 | ✅ 自动生成(`cool entity` 命令扫描 `modules/*/entity` 通配符,无需手动修改) |
| 前端模块 `config.ts` 导出 ModuleConfig | ✅ |
| 加密复用 AES-256-GCM 算法和密钥派生 | ✅ 自建 `GeoEncryptService`(复用算法+密钥,接口适配字符串场景) |
| 菜单走 base_sys_menu | ✅ |
| TypeORM `synchronize: true` 自动建表 | ✅dev 环境prod 禁用) |
| 多租户 tenantId 自动注入 | ✅ |
| 不在 S1gateway/skill/runtime属过度设计 | ✅ |
---
## 4. 架构与目录
### 4.1 后端目录
```
packages/backend/src/
└── modules/geo/ # entities.ts 由 `cool entity` 自动生成,无需手动修改
├── config.ts
├── entity/
│ ├── account.ts # geo_account
│ ├── proxy_ip.ts # geo_proxy_ip
│ └── browser_profile.ts # geo_browser_profile
├── controller/admin/
│ ├── account.ts # @CoolController + 自定义 launch / captureCookies
│ ├── proxy_ip.ts # @CoolController + 自定义 healthCheck / batchImport
│ └── browser_profile.ts # @CoolController + 自定义 open / close
├── service/
│ ├── account.ts # ★ 单点编排bindResources / launch / captureCookies / rebindIp
│ ├── proxy_ip.ts # CRUD 包装 + provider 调度
│ ├── browser_profile.ts # CRUD 包装 + provider 调度
│ ├── browser_automation.ts # ★ 统一自动化层playwright-cli 包装cookie/state/click/type
│ └── encrypt.ts # GeoEncryptService
└── provider/ # geo 模块内部约定(非 NetaClaw plugins/
# 原因geo provider 不需要动态加载,且 plugins/ 在 Neta 语义里
# 特指 LLM 适配层netaclaw/plugins/llm_providers/
├── proxy/
│ ├── interface.ts # IProxyProvider
│ ├── local.ts # LocalProxyProvider
│ └── tianqi.ts # 占位 TianqiProxyProvider等文档
└── browser/
├── interface.ts # IBrowserProvider仅进程生命周期
├── plain_chromium.ts # ★ S1 主实现playwright-cli + Chromium
├── bitbrowser.stub.ts # 占位(启动 BitBrowserplaywright-cli attach
├── ant_browser.stub.ts # 占位(等用户提供源码)
└── adspower.stub.ts # 占位
```
### 4.2 前端目录
```
packages/frontend/src/modules/geo/
├── config.ts # ★ 必须 export ModuleConfig
└── views/ # service 代理由 Cool Admin 根据后端 controller 文件名自动生成
# 如 service.geo.account.page() / service.geo.proxy_ip.healthCheck()
# 无需手动创建 service/ 目录
├── dashboard.vue # 占位S4 才正式做内容)
├── accounts.vue # 账号矩阵主页
├── proxies.vue # IP 池
└── browser-profiles.vue # 指纹浏览器
```
### 4.3 菜单 seed运行时通过 MCP mysql 执行 INSERT
```
🌍 GEO一级目录icon=icon-trendorder=50
├── 账号矩阵 viewPath=modules/geo/views/accounts.vue perms=geo:account:list
├── IP 池 viewPath=modules/geo/views/proxies.vue perms=geo:proxy:list
└── 指纹浏览器 viewPath=modules/geo/views/browser-profiles.vue perms=geo:profile:list
```
按钮权限:`geo:account:add` / `update` / `delete` / `launch` / `captureCookies` / `rebindIp``geo:proxy:add` / `update` / `delete` / `healthCheck` / `batchImport``geo:profile:add` / `update` / `delete` / `open` / `close`
---
## 5. 数据模型
### 5.1 geo_account
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | bigint PK | BaseEntity |
| `tenantId` | bigint Index | BaseEntity 自动注入 |
| `name` | varchar(64) | 备注名/昵称 |
| `platform` | varchar(32) Index | xiaohongshu / douyin / weibo / zhihu / wechat |
| `loginAccount` | varchar(128) | 登录手机号/账号 |
| `cookies` | text | **加密** JSON |
| `cookieCapturedAt` | datetime NULL | |
| `loginStatus` | varchar(16) | never / logged_in / expired |
| `status` | varchar(16) | draft / active / risky / banned / deleted |
| `proxyId` | bigint UQ Index | → geo_proxy_ip.id |
| `browserProfileId` | bigint UQ Index | → geo_browser_profile.id |
| `personaId` | bigint NULL | S2 用S1 占位 |
| `agentConfigId` | bigint NULL | S3 用 |
| `lastActiveAt` | datetime NULL | |
| `extra` | json | 平台特定字段 |
| `createTime` `updateTime` | datetime | BaseEntity |
**约束**(`platform`, `loginAccount`) 联合唯一;`proxyId` 唯一;`browserProfileId` 唯一。
### 5.2 geo_proxy_ip
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | bigint PK | |
| `tenantId` | bigint Index | |
| `name` | varchar(64) | |
| `provider` | varchar(32) | local / tianqi |
| `mode` | varchar(16) | local / third_party |
| `host` | varchar(128) NULL | 第三方才有 |
| `port` | int NULL | |
| `protocol` | varchar(8) | http / socks5 |
| `username` | varchar(128) NULL | **加密** |
| `password` | varchar(255) NULL | **加密** |
| `region` | varchar(64) | 城市/省份 |
| `isp` | varchar(32) | 联通/电信/移动 |
| `externalId` | varchar(128) NULL | provider 侧 ID |
| `bindAccountId` | bigint UQ NULL Index | 反向唯一 |
| `status` | varchar(16) | active / expired / error / unbound |
| `latencyMs` | int NULL | |
| `lastCheckAt` | datetime NULL | |
| `expiresAt` | datetime NULL | |
| `extra` | json | |
### 5.3 geo_browser_profile
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | bigint PK | |
| `tenantId` | bigint Index | |
| `name` | varchar(64) | |
| `provider` | varchar(32) | plain_chromium / bitbrowser / ant_browser / adspower |
| `sessionName` | varchar(128) Index | playwright-cli 的 `-s=xxx` 命名 session唯一标识 |
| `accountId` | bigint UQ NULL Index | 反向唯一 |
| `profileDir` | varchar(512) NULL | `--profile=/path` 持久化目录 |
| `configPath` | varchar(512) NULL | `--config=xxx.json` 代理配置文件路径 |
| `userAgent` | varchar(512) | |
| `osPlatform` | varchar(32) | windows / mac / linux |
| `timezone` | varchar(32) | |
| `language` | varchar(16) | |
| `screenW` | int | |
| `screenH` | int | |
| `fingerprint` | json | 深度指纹配置S1 不填,等 ant-browser |
| `lastOpenAt` | datetime NULL | |
| `status` | varchar(16) | created / running / closed / error / deleted |
| `extra` | json | |
---
## 6. Provider 接口契约
### 6.1 IProxyProvider
```ts
// provider/proxy/interface.ts
export interface AcquireOpts {
region?: string;
isp?: string;
duration?: 'fixed' | 'rotating';
// 第三方扩展字段
extra?: Record<string, any>;
}
export interface ProxyInfo {
externalId: string;
mode: 'local' | 'third_party';
host?: string;
port?: number;
protocol: 'http' | 'socks5';
username?: string;
password?: string;
region?: string;
isp?: string;
expiresAt?: Date;
}
export interface IProxyProvider {
readonly name: string;
acquire(opts: AcquireOpts): Promise<ProxyInfo>;
release(externalId: string): Promise<void>;
healthCheck(p: ProxyInfo): Promise<{ ok: boolean; latencyMs: number }>;
list?(): Promise<ProxyInfo[]>;
}
```
### 6.2 IBrowserProvider仅浏览器进程生命周期不含自动化
```ts
// provider/browser/interface.ts
import type { ProxyInfo } from '../proxy/interface';
export interface CreateOpts {
name: string;
sessionName: string;
userAgent?: string;
osPlatform?: 'windows' | 'mac' | 'linux';
timezone?: string;
language?: string;
screenW?: number;
screenH?: number;
fingerprint?: Record<string, any>;
}
export interface ProfileInfo {
sessionName: string;
profileDir?: string;
configPath?: string;
userAgent: string;
osPlatform: string;
timezone: string;
language: string;
screenW: number;
screenH: number;
}
export interface IBrowserProvider {
readonly name: string;
/** 创建 profile写配置文件、注册指纹环境不启动进程 */
create(opts: CreateOpts): Promise<ProfileInfo>;
/** 删除 profile 配置 */
delete(profile: ProfileInfo): Promise<void>;
/** 把代理写入 profile 配置attachProxy 与 open 解耦) */
attachProxy(profile: ProfileInfo, proxy: ProxyInfo): Promise<void>;
/** 启动浏览器进程并打开 URL最终 BrowserAutomationService 通过 sessionName 操作) */
open(profile: ProfileInfo, url?: string): Promise<void>;
/** 关闭浏览器进程 */
close(profile: ProfileInfo): Promise<void>;
}
```
### 6.3 Provider 实现职责
| 实现 | 关键行为 |
|---|---|
| `LocalProxyProvider` | acquire 直接返回 `mode='local'` 标记的 ProxyInfo不连出站代理healthCheck 走宿主机 https://www.baidu.com |
| `TianqiProxyProvider`(占位) | acquire/release 抛 `NotImplemented` 但不阻塞模块加载healthCheck 同上;文件头部注释列出对接 TODO |
| **`PlainChromiumProvider`**(★ S1 主实现) | create → 在 `dataDir/geo/profiles/` 下生成 `geo-{accountId}` profile 目录 + 写一个 `geo-proxy-{accountId}.json` 代理配置attachProxy → 重写代理 JSONopen → exec `playwright-cli -s={session} open {url} --persistent --profile={profileDir} --headed --config={configPath}`close → exec `playwright-cli -s={session} close`delete → `playwright-cli -s={session} delete-data` + 删除配置文件 |
| `BitBrowserStub` | 占位。未来实现:调 BitBrowser Local API 启动 profile 拿到 CDP endpointplaywright-cli 通过 attach 模式连进去做自动化 |
| `AntBrowserStub` | 占位。等用户提供源码后实现 |
| `AdsPowerStub` | 占位。仅声明 name + 抛 NotImplemented |
### 6.4 BrowserAutomationService统一自动化层所有 Provider 共用)
```ts
// service/browser_automation.ts
@Provide()
export class BrowserAutomationService {
/** 列出指定域名的 cookie。底层 execplaywright-cli -s={session} cookie-list --domain={domain} --raw */
async getCookies(sessionName: string, domain?: string): Promise<Cookie[]>;
/** 保存完整登录态到文件。底层 execplaywright-cli -s={session} state-save {path} */
async saveState(sessionName: string, filePath: string): Promise<void>;
/** 恢复登录态。底层 execplaywright-cli -s={session} state-load {path} */
async loadState(sessionName: string, filePath: string): Promise<void>;
/** S3 后续会增加 click / type / snapshot 等自动化方法 */
}
```
**关键设计**`BrowserAutomationService``IBrowserProvider` 完全解耦。无论 provider 启动的是普通 Chromium 还是 BitBrowser/ant-browser只要进程存在且 `playwright-cli -s={session}` 能命中(或 attach 进去),自动化层无差别工作。
---
## 7. 核心数据流
### 7.1 创建账号(同时分配 IP + Profile
```
前端 accounts.vue → POST /admin/geo/account/add
service/account.ts.add(dto):
await dataSource.transaction(async manager => {
// 1. 申请 IP
const proxyDto = dto.ipMode === 'local'
? { provider: 'local', mode: 'local' }
: { provider: 'tianqi', mode: 'third_party', region: dto.region };
const ipInfo = await proxyService.acquire(proxyDto);
const ipEntity = await proxyService.persist(manager, ipInfo);
// 2. 申请 Profile
const profileInfo = await browserService.create({
provider: dto.browserProvider ?? 'bitbrowser',
...dto.fingerprint
});
const profileEntity = await browserService.persist(manager, profileInfo);
// 3. Provider 侧挂代理
await browserService.attachProxy(profileEntity, ipEntity);
// 4. 创建 account
const account = manager.create(GeoAccount, {
...dto, proxyId: ipEntity.id, browserProfileId: profileEntity.id
});
return manager.save(account);
});
// 失败补偿transaction 回滚 + Provider 端
catch (e) {
if (ipExternal) await proxyProvider.release(ipExternal);
if (profileExternal) await browserProvider.delete(profileExternal);
throw e;
}
```
### 7.2 启动浏览器环境
```
前端 [启动登录] → POST /admin/geo/account/launch { id, url? }
service/account.ts.launch(id, url?):
const account = repo.findOneOrFail(id);
const profile = profileRepo.findOneOrFail(account.browserProfileId);
const targetUrl = url || PLATFORM_URLS[account.platform]; // 如 https://www.xiaohongshu.com
await browserProvider.open(profile.sessionName, targetUrl);
// playwright-cli 会弹出有头浏览器窗口,用户在里面登录
await profileRepo.update(profile.id, { status: 'running', lastOpenAt: new Date() });
return { sessionName: profile.sessionName };
```
### 7.3 抓 Cookie
```
前端轮询/手动触发 → POST /admin/geo/account/captureCookies { id, domains? }
service/account.ts.captureCookies(id, domains?):
const account = ...findOneOrFail(id);
const profile = ...findOneOrFail(account.browserProfileId);
if (profile.status !== 'running') throw new Error('Browser not running, call launch first');
// 自动化层走 BrowserAutomationService与底层是哪种浏览器无关
const domain = domains?.[0];
const cookies = await browserAutomationService.getCookies(profile.sessionName, domain);
if (cookies.length === 0) {
return { captured: 0 };
}
const encrypted = geoEncryptService.encrypt(JSON.stringify(cookies));
await accountRepo.update(id, {
cookies: encrypted,
cookieCapturedAt: new Date(),
loginStatus: 'logged_in',
});
// 同时保存完整登录态cookie + localStorage到文件方便后续 loadState 复活会话
const statePath = `${dataDir}/geo/states/geo-state-${id}.json`;
await browserAutomationService.saveState(profile.sessionName, statePath);
return { captured: cookies.length };
```
### 7.4 IP 健康检查Neta task 模块 cron
Neta 的 task 模块通过 `task_info` 表 + `TaskLocalService`(基于 `cron` npm 包)调度。注册方式:在 `task_info` 表中 INSERT 一条记录,`service` 字段指向要调用的 service 方法路径。
```
注册(通过 mcp__mysql__execute INSERT 到 task_info 表):
name: 'GEO IP 健康检查'
service: 'geo.proxy_ip.healthCheckAll'
type: 0 # 0=cron 表达式
cron: '0 0 */6 * * *' # 每 6 小时
status: 1 # 启用
service/proxy_ip.ts:
async healthCheckAll() {
const ips = await this.proxyIpEntity.find({ where: { status: 'active' } });
for (const ip of ips) {
const provider = this.getProvider(ip.provider);
const result = await provider.healthCheck(this.toProxyInfo(ip));
await this.proxyIpEntity.update(ip.id, {
latencyMs: result.latencyMs,
lastCheckAt: new Date(),
status: result.ok ? 'active' : 'error',
});
if (!result.ok) {
this.logger.warn(`[GEO] IP ${ip.id} (${ip.host}) health check failed`);
}
}
// S1 仅 logger.warn不入告警表S4 才有 geo_alert
}
```
### 7.5 解绑/换 IP
```
service/account.ts.rebindIp(accountId, newIpId):
await dataSource.transaction(async manager => {
const account = await manager.findOneOrFail(GeoAccount, accountId);
const oldIp = await manager.findOne(GeoProxyIp, account.proxyId);
const newIp = await manager.findOneOrFail(GeoProxyIp, newIpId);
if (newIp.bindAccountId) throw new Error('IP 已被其他账号绑定');
if (oldIp) {
await manager.update(GeoProxyIp, oldIp.id, { bindAccountId: null, status: 'unbound' });
}
await manager.update(GeoProxyIp, newIp.id, { bindAccountId: accountId, status: 'active' });
await manager.update(GeoAccount, accountId, { proxyId: newIp.id });
const profile = await manager.findOneOrFail(GeoBrowserProfile, account.browserProfileId);
await browserProvider.attachProxy(profile.externalProfileId, toProxyInfo(newIp));
});
```
### 7.6 删除账号
Neta 现有模块不使用 TypeORM `softRemove`BaseEntity 没有 `@DeleteDateColumn`。geo 模块采用**逻辑删除**account 和 profile 设 `status='deleted'`,与 Neta 其他模块风格一致。
```
service/account.ts.deleteAccount(id):
await dataSource.transaction(async manager => {
const account = await manager.findOneOrFail(GeoAccount, { where: { id } });
const ip = account.proxyId
? await manager.findOne(GeoProxyIp, { where: { id: account.proxyId } })
: null;
const profile = account.browserProfileId
? await manager.findOne(GeoBrowserProfile, { where: { id: account.browserProfileId } })
: null;
if (profile) {
await browserProvider.delete(profile.externalProfileId).catch(() => {/* 容忍外部失败 */});
await manager.update(GeoBrowserProfile, profile.id, { status: 'deleted', accountId: null });
}
if (ip) {
await manager.update(GeoProxyIp, ip.id, { bindAccountId: null, status: 'unbound' });
}
await manager.update(GeoAccount, id, { status: 'deleted', proxyId: null, browserProfileId: null });
});
```
---
## 8. 错误处理
| 故障 | 处理 |
|---|---|
| playwright-cli 未安装 | `PlaywrightCliProvider` 启动时检测 `playwright-cli --version`,失败抛 `BrowserProviderUnavailable`,前端弹窗提示安装 |
| 天启 API 超时 | 重试 3 次(指数退避),仍失败 → `ProxyAcquireFailed`,前端提示切 local 或重试 |
| Cookie 抓取空 | 不写库,返回 `captured: 0`,前端提示用户先在打开的浏览器里完成登录 |
| 事务中途失败 | DB 自动回滚 + Provider 端补偿 release/delete |
| 并发竞争 IP | UQ 索引兜底service 层先 `lockMode: 'pessimistic_write'` 锁住 IP |
| 浏览器进程残留 | `close` 失败时 logger.error 但不阻塞业务,运维侧定期清理 |
| 加密密钥丢失 | `GeoEncryptService` 复用 `SKILL_SECRET_KEY`/`APP_SECRET` 环境变量派生密钥,丢失时抛 `EncryptionKeyMissing` |
---
## 9. 测试策略
| 层 | 方法 |
|---|---|
| **Provider 单元** | LocalProxyProvider / BitBrowserProvider 走 mock fetchTianqiProvider 验证抛 NotImplemented |
| **Service 单元** | jest + sqlite验证编排逻辑、事务回滚、补偿动作触发 |
| **Controller 集成** | jest + supertest跑 add → launch → captureCookies 一遍 |
| **手工冒烟** | 真机 BitBrowser + 真小红书账号:创建 → 启动 → 登录 → 抓 cookie → loginStatus 验证 |
测试不在 S1 范围放真账号 token所有真实凭据只在手工冒烟阶段使用。
---
## 10. 验收标准
1. ✅ 后端 `pnpm dev` 启动TypeORM 自动建好 `geo_account` `geo_proxy_ip` `geo_browser_profile` 三张表(可用 `mcp__mysql__list_tables` 验证)
2. ✅ Cool Admin 自动 CRUD 接口可用:`/admin/geo/{account,proxy_ip,browser_profile}/{add,delete,update,info,list,page}` 全部返回 200
3. ✅ 自定义接口可用:`/admin/geo/account/launch` `/captureCookies` `/rebindIp``/admin/geo/proxy_ip/healthCheck` `/batchImport``/admin/geo/browser_profile/open` `/close`
4.`mcp__mysql__query` 能查到 `base_sys_menu``🌍 GEO` 顶级菜单 + 3 个子菜单 + 全部按钮权限
5. ✅ 前端登录后菜单出现 `🌍 GEO` + 3 个子项;点击进入对应页面
6. ✅ 在前端能完整跑通:
- 创建一个 Local IP能跑 healthCheck 看到延迟数字
- 创建一个 playwright-cli profile能 open 弹出有头浏览器窗口
- 创建一个账号platform=xiaohongshu自动绑定 IP+Profile
- 点击「启动登录」弹出有头浏览器,登录小红书后点「抓取 Cookie」loginStatus 变 logged_incookies 字段非空且加密
7. ✅ 删除账号DB 中 IP 置 unbound 但记录保留Profile 状态置 deletedaccount 状态置 deleted逻辑删除非 softRemove
8. ✅ 强绑定约束:尝试给已绑账号的 IP 重新绑给别人DB 报唯一约束错(前端友好提示)
9. ✅ playwright-cli 未安装时,前端 open 操作返回 `BrowserProviderUnavailable` 错误并弹窗
---
## 11. 交付物清单
| 类别 | 文件 |
|---|---|
| **后端 entity** | `entity/account.ts` `entity/proxy_ip.ts` `entity/browser_profile.ts` |
| **后端 controller** | `controller/admin/account.ts` `proxy_ip.ts` `browser_profile.ts` |
| **后端 service** | `service/account.ts` `service/proxy_ip.ts` `service/browser_profile.ts` `service/browser_automation.ts`(统一自动化层) `service/encrypt.ts`GeoEncryptService |
| **Provider** | `provider/proxy/{interface,local,tianqi}.ts` `provider/browser/{interface,plain_chromium,bitbrowser.stub,ant_browser.stub,adspower.stub}.ts` |
| **配置** | `modules/geo/config.ts``entities.ts``cool entity` 自动生成,无需手动改) |
| **菜单** | `mcp__mysql__execute` 注入 `base_sys_menu`4 条 menu + 12+ 条 perms |
| **前端** | `modules/geo/{config.ts, views/{accounts,proxies,browser-profiles,dashboard}.vue, components/}` |
| **测试** | `test/modules/geo/**` 单元+集成 |
| **文档** | 本 spec + 后续 plan |
---
## 12. 风险与依赖
| 风险/依赖 | 缓解 |
|---|---|
| playwright-cli 未安装 | Provider 启动时检测 + 前端友好提示 + 文档说明安装步骤(`npm i -g @playwright/cli@latest` |
| 天启 HTTP 文档迟到 | TianqiProvider 占位实现可走通整体流程,文档到位后只改一文件 |
| `synchronize: true` 在 prod 危险 | Neta 现有约定 prod 禁用S1 不改前提 |
| Cookie 抓取目标域不全 | captureCookies 接受 `domains?: string[]` 参数 |
| Playwright 与代理兼容性 | playwright-cli `--config` 支持 proxy 配置S1 验证 local + http proxy 两种场景 |
| 测试环境没有 playwright-cli | Provider 抽象允许 mock child_process.execCI 用 mock |
| 深度指纹伪装 | S1 不做playwright-cli 不支持 canvas/webgl 伪装);等 ant-browser 源码到位后新增 AntBrowserProvider |
---
## 13. 范围红线(不在 S1
- ❌ 平台特定登录流程QR 轮询/账密表单/短信) → S3
- ❌ 浏览/发布/评论/指标采集 → S3
- ❌ 关键词、内容、Schema → S2
- ❌ AI 引用监测 → S4
- ❌ 多模态/数字人/爆款拆解 → S5暂不规划
- ❌ Skill 包对外暴露给 NetaClaw Agent → S3 起做
- ❌ WebSocket 实时启动日志 → S3 评估是否需要
---
## 14. 工作流
完成本 spec 后:
1. user 复核本文档
2. 调用 `superpowers:writing-plans` 生成 `docs/superpowers/plans/2026-05-03-geo-s1-infrastructure-plan.md`
3. user 复核 plan
4. `superpowers:executing-plans` + TDD 实施
5. `superpowers:verification-before-completion` 逐条核对验收标准
6. `superpowers:requesting-code-review`
7. 更新主路线图S1 状态 → 已完成;启动 S2 brainstorming
---
## 15. 变更日志
| 日期 | 变更 |
|---|---|
| 2026-05-03 | 初稿 |
| 2026-05-03 | 架构审查修复 8 项①entities.ts 自动生成不手动改 ②自建 GeoEncryptService 替代直接调用 SkillSecretService ③定时任务改用 task_info 表注册 ④provider/ 目录加说明 ⑤删除前端 service/ 目录 ⑥逻辑删除替代 softRemove ⑦BitBrowser 端口从 config/env 读取 ⑧account/profile status 枚举加 deleted |
| 2026-05-03 | 浏览器层重构:主实现从 BitBrowserProvider 改为 PlaywrightCliProvider复用 Neta 已有 playwright-cli skillBitBrowser 降为 stubEntity 字段调整externalProfileId→sessionNamecdpEndpoint→profileDir/configPath接口增加 getCookies/saveState/loadState不再依赖 playwright Node.js 库 |
| 2026-05-03 | 浏览器层二次重构:澄清"自动化工具"与"浏览器进程"是两个正交层。新增 `BrowserAutomationService`(统一 playwright-cli 包装层)。`IBrowserProvider` 缩减为只管浏览器进程生命周期(去掉 getCookies/saveState/loadState。S1 主实现重命名为 `PlainChromiumProvider`playwright-cli + ChromiumBitBrowser/AntBrowser/AdsPower 全部 stub等具体浏览器需求到位时只增加 provider 不改自动化层 |