S1 基础设施层 设计文档
Geo 模块第一个子项目:账号矩阵 / IP 池 / 指纹浏览器 / 菜单。
上位文档:2026-05-03-geo-master-roadmap.md
| 元数据 |
值 |
| 子项目 |
S1 基础设施层 |
| 创建日期 |
2026-05-03 |
| 估期 |
~9 天 |
| 状态 |
设计完成,待 user 复核 → writing-plans |
1. 目标
为 Geo 模块后续所有子项目(S2/S3/S4)提供账号—IP—指纹浏览器三位一体的基础设施。S1 完成后,用户能在 Neta 控制台:
- 在 GEO 一级菜单下看到三个子页面
- 创建一个 IP(本地或第三方)
- 创建一个指纹浏览器 profile
- 创建一个社媒账号,自动绑定 IP + Profile(强 1:1:1)
- 点击「启动登录」打开真实浏览器,扫码/输账密/输短信完成登录
- Cookie 自动抱回并加密存储
- 删除账号时安全释放 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 自动注入 |
✅ |
| 不在 S1:gateway/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 # 占位(启动 BitBrowser,playwright-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-trend,order=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
// 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(仅浏览器进程生命周期,不含自动化)
// 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 → 重写代理 JSON;open → 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 endpoint,playwright-cli 通过 attach 模式连进去做自动化 |
AntBrowserStub |
占位。等用户提供源码后实现 |
AdsPowerStub |
占位。仅声明 name + 抛 NotImplemented |
6.4 BrowserAutomationService(统一自动化层,所有 Provider 共用)
// service/browser_automation.ts
@Provide()
export class BrowserAutomationService {
/** 列出指定域名的 cookie。底层 exec:playwright-cli -s={session} cookie-list --domain={domain} --raw */
async getCookies(sessionName: string, domain?: string): Promise<Cookie[]>;
/** 保存完整登录态到文件。底层 exec:playwright-cli -s={session} state-save {path} */
async saveState(sessionName: string, filePath: string): Promise<void>;
/** 恢复登录态。底层 exec:playwright-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 fetch;TianqiProvider 验证抛 NotImplemented |
| Service 单元 |
jest + sqlite,验证编排逻辑、事务回滚、补偿动作触发 |
| Controller 集成 |
jest + supertest,跑 add → launch → captureCookies 一遍 |
| 手工冒烟 |
真机 BitBrowser + 真小红书账号:创建 → 启动 → 登录 → 抓 cookie → loginStatus 验证 |
测试不在 S1 范围放真账号 token,所有真实凭据只在手工冒烟阶段使用。
10. 验收标准
- ✅ 后端
pnpm dev 启动,TypeORM 自动建好 geo_account geo_proxy_ip geo_browser_profile 三张表(可用 mcp__mysql__list_tables 验证)
- ✅ Cool Admin 自动 CRUD 接口可用:
/admin/geo/{account,proxy_ip,browser_profile}/{add,delete,update,info,list,page} 全部返回 200
- ✅ 自定义接口可用:
/admin/geo/account/launch /captureCookies /rebindIp、/admin/geo/proxy_ip/healthCheck /batchImport、/admin/geo/browser_profile/open /close
- ✅
mcp__mysql__query 能查到 base_sys_menu 中 🌍 GEO 顶级菜单 + 3 个子菜单 + 全部按钮权限
- ✅ 前端登录后菜单出现
🌍 GEO + 3 个子项;点击进入对应页面
- ✅ 在前端能完整跑通:
- 创建一个 Local IP,能跑 healthCheck 看到延迟数字
- 创建一个 playwright-cli profile,能 open 弹出有头浏览器窗口
- 创建一个账号(platform=xiaohongshu),自动绑定 IP+Profile
- 点击「启动登录」弹出有头浏览器,登录小红书后点「抓取 Cookie」,loginStatus 变 logged_in,cookies 字段非空且加密
- ✅ 删除账号:DB 中 IP 置 unbound 但记录保留;Profile 状态置 deleted;account 状态置 deleted(逻辑删除,非 softRemove)
- ✅ 强绑定约束:尝试给已绑账号的 IP 重新绑给别人,DB 报唯一约束错(前端友好提示)
- ✅ 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.exec;CI 用 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 后:
- user 复核本文档
- 调用
superpowers:writing-plans 生成 docs/superpowers/plans/2026-05-03-geo-s1-infrastructure-plan.md
- user 复核 plan
superpowers:executing-plans + TDD 实施
superpowers:verification-before-completion 逐条核对验收标准
superpowers:requesting-code-review
- 更新主路线图: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 skill);BitBrowser 降为 stub;Entity 字段调整(externalProfileId→sessionName,cdpEndpoint→profileDir/configPath);接口增加 getCookies/saveState/loadState;不再依赖 playwright Node.js 库 |
| 2026-05-03 |
浏览器层二次重构:澄清"自动化工具"与"浏览器进程"是两个正交层。新增 BrowserAutomationService(统一 playwright-cli 包装层)。IBrowserProvider 缩减为只管浏览器进程生命周期(去掉 getCookies/saveState/loadState)。S1 主实现重命名为 PlainChromiumProvider(playwright-cli + Chromium),BitBrowser/AntBrowser/AdsPower 全部 stub,等具体浏览器需求到位时只增加 provider 不改自动化层 |