GPU_GUARD_MONOREPO/docs/superpowers/specs/2026-05-03-geo-s1-infrastructure-design.md

592 lines
27 KiB
Markdown
Raw Normal View History

2026-05-20 21:39:12 +08:00
# 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 不改自动化层 |