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

27 KiB
Raw Blame History

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 控制台:

  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
浏览器(两层正交) ① 浏览器进程层 IBrowserProviderS1 主实现 PlainChromiumProviderplaywright-cli 直接启 Chromium+ 占位 BitBrowser AntBrowser AdsPower。② 自动化层 BrowserAutomationService:统一用 playwright-cli -s={session} 做 cookie/state/click/type不关心底层是哪种浏览器
绑定关系 account ↔ proxy_ip ↔ browser_profile 严格 1:1:1IP/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 / rebindIpgeo:proxy:add / update / delete / healthCheck / batchImportgeo: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 → 重写代理 JSONopen → exec playwright-cli -s={session} open {url} --persistent --profile={profileDir} --headed --config={configPath}close → exec playwright-cli -s={session} closedelete → playwright-cli -s={session} delete-data + 删除配置文件
BitBrowserStub 占位。未来实现:调 BitBrowser Local API 启动 profile 拿到 CDP endpointplaywright-cli 通过 attach 模式连进去做自动化
AntBrowserStub 占位。等用户提供源码后实现
AdsPowerStub 占位。仅声明 name + 抛 NotImplemented

6.4 BrowserAutomationService统一自动化层所有 Provider 共用)

// 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 等自动化方法 */
}

关键设计BrowserAutomationServiceIBrowserProvider 完全解耦。无论 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 };
前端轮询/手动触发 → 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 softRemoveBaseEntity 没有 @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.tsGeoEncryptService
Provider provider/proxy/{interface,local,tianqi}.ts provider/browser/{interface,plain_chromium,bitbrowser.stub,ant_browser.stub,adspower.stub}.ts
配置 modules/geo/config.tsentities.tscool entity 自动生成,无需手动改)
菜单 mcp__mysql__execute 注入 base_sys_menu4 条 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 主实现重命名为 PlainChromiumProviderplaywright-cli + ChromiumBitBrowser/AntBrowser/AdsPower 全部 stub等具体浏览器需求到位时只增加 provider 不改自动化层