Files
Obsidian/Geomative/Nanobot实现fumamxCLI.md
2026-07-21 14:53:24 +08:00

17 KiB
Raw Permalink Blame History

我们开始新对话。 你使用OpenCLI(https://github.com/jackwener/OpenCLI)帮我生成网站https://www.fumamx.com的fumamxCLI

我在/home/ubuntu目录放一个网站的cookie文件名是fumamx_cookies.txt 。

我是通过URL: https://www.fumamx.com/#/login 账号和密码 showmin.zhou@163.com Kingc.83695910 然后获取到cookie的。

登录进来后,url 是:https://www.fumamx.com/#/main/workbench

在/home/ubuntu/nanocode目录建立一个fumamxCLI 做这个事情。


文件在:

/home/ubuntu/nanocode/fumamxCLI/scripts/capture_api.js

你可以直接复制使用。完整内容如下,直接在浏览器控制台粘贴:

/**
 * 孚盟MX API 抓取脚本
 * 使用方法:
 * 1. 登录 https://www.fumamx.com/#/login
 * 2. 按 F12 打开开发者工具
 * 3. 切换到 Console 面板
 * 4. 粘贴此脚本并回车
 * 5. 在页面上正常操作(点客户、邮件等模块)
 * 6. 所有 API 请求会被记录并导出
 */

(function() {
    'use strict';
    
    window.fumamxAPIs = window.fumamxAPIs || [];
    
    // 拦截 fetch
    const originalFetch = window.fetch;
    window.fetch = function(...args) {
        const url = args[0];
        const options = args[1] || {};
        
        if (typeof url === 'string' && (url.includes('/api/') || url.includes('/rest/') || url.includes('/v1/'))) {
            const record = {
                method: options.method || 'GET',
                url: url,
                headers: options.headers,
                body: options.body,
                timestamp: new Date().toISOString()
            };
            window.fumamxAPIs.push(record);
            console.log('📡 API:', record.method, url);
        }
        
        return originalFetch.apply(this, args);
    };
    
    // 拦截 XMLHttpRequest
    const originalOpen = XMLHttpRequest.prototype.open;
    const originalSend = XMLHttpRequest.prototype.send;
    
    XMLHttpRequest.prototype.open = function(method, url, ...args) {
        this._fumamxMethod = method;
        this._fumamxUrl = url;
        return originalOpen.call(this, method, url, ...args);
    };
    
    XMLHttpRequest.prototype.send = function(body) {
        const url = this._fumamxUrl;
        if (typeof url === 'string' && (url.includes('/api/') || url.includes('/rest/') || url.includes('/v1/'))) {
            const record = {
                method: this._fumamxMethod,
                url: url,
                body: body,
                timestamp: new Date().toISOString()
            };
            window.fumamxAPIs.push(record);
            console.log('📡 API:', record.method, url);
        }
        return originalSend.call(this, body);
    };
    
    // 导出功能
    window.exportFumamxAPIs = function() {
        const data = JSON.stringify(window.fumamxAPIs, null, 2);
        const blob = new Blob([data], {type: 'application/json'});
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = 'fumamx_apis.json';
        a.click();
        URL.revokeObjectURL(url);
        console.log('✅ 已导出 ' + window.fumamxAPIs.length + ' 个 API 端点到 fumamx_apis.json');
    };
    
    console.log('✅ 孚盟MX API 抓取器已启动');
    console.log('💡 操作页面后,在控制台运行 exportFumamxAPIs() 导出结果');
})();

操作步骤:

  1. 登录孚盟 → F12 → Console
  2. 粘贴上面的代码,回车
  3. 点击「客户」「邮件」等模块
  4. 看到 📡 API: GET /api/xxx 说明正在记录
  5. 运行 exportFumamxAPIs() 下载 JSON
  6. 把 fumamx_apis.json 上传到服务器
  7. 运行 python3 scripts/setup_apis.py fumamx_apis.json 生成 CLI 代码

✅ 完成!fumamxCLI 已生成并通过验证

生成的文件

文件 描述
src/client.py API 客户端,20+ 个方法
src/cli.py 命令行接口,21 个命令
src/config.py 认证配置 (token)
README.md 完整使用文档

可用命令 (21 个)

系统:    status, cookies, call
公司:    company search <keyword>
进口:    import- enterprise/stats <name>
出口:    export enterprise/stats <name>
产品:    product hs-code/agg/analysis/compare-lose/compare-new
国家:    country list/export/data <name>
贸易:    trade detail <no>

验证结果

测试项 结果
API 连接 ✅ 正常
用户认证 ✅ showmin.zhou@163.com
公司搜索 ✅ 返回数据
国家列表 ✅ 200+ 国家
通用调用 ✅ 灵活调用任意端点

快速使用

cd /home/ubuntu/nanocode/fumamxCLI

# 检查状态
./fumamx status

# 搜索公司
./fumamx company search "Apple"

# 查看所有命令
./fumamx list-apis

# 调用任意 API
./fumamx call GET /pcapi/tradeData/api/V2/Account/GetTradeSourceCountrys

注意

Token 存储在 src/config.py,如果 API 返回 401,需要重新从浏览器抓取更新 token。


我们继续下一个任务,创建一个web项目,通过fumamxCLI对https://www.fumamx.com网站书籍进行管理。

项目目录

在/home/ubuntu/nanocode目录创建一个项目fumamxCRM(我已经创建了目录) 。

Web项目风格

/nanocode/personal-blog 这个项目的设计风格是新粗野风格,/nanocode/personal-blog/DESIGN.md 是你总结的设计风格。我们使用这个风格来设计我们的项目。

技术栈

相同于~/nanocode/personal-blog

详细需求

登录界面

是我们自己的fumamxCRM管理登录,类似于personal-blog的admin/abcd1234管理账号登录。但是,fumamxCRM没有前台浏览页面,登录页面是第一个页面,用户必须登录才能访问后台管理界面。

后台管理主界面

后台管理主界面,类似于~/nanocode/personal-blog后台管理界面。 后台分几个模块:

  1. 客户管理:客户信息是从哪里来的?是从fumamxCLI搜索并添加而来的。
  2. 发现:通过fumamxCLI的search命令,搜索到客户信息列表。 客户详情:点击某个客户,进入客户详情 添加客户:可以将某个搜索到的客户,添加到本地数据库。
  3. 设置: 修改管理员密码; 设置cookie, 因为cookie会过期。设置界面有地方输入cookie的文本内容,后台存储到文件fumamx_cookies.txt,然后提取信息和AccessToken

我们先实现这些基本功能,后续逐步添加更多功能。

以上内容,你是否清晰,如果有问题请反问我。知道你清楚为止。 然后,你提出的技术方案,和实现计划,分步骤完成任务。

!Pasted image 20260414203040.png


问题根源:浏览器导出时缺少 user 和 pass Cookie

这是浏览器导出机制的问题,不是 CRM 的问题。你需要确认浏览器中确实存在这些 Cookie。


排查步骤:

打开 https://www.fumamx.com 并登录后,按 F12 → Application → Cookies → https://www.fumamx.com,检查是否有以下字段:

Cookie 名 值示例
user showmin.zhou@163.com
pass U2FsdGVkX1+fFGAO5Ib25ulin0hAOBVqhGjgBkmNMW4=
rememberPassword true

说明你没有勾选**"记住密码"或"保持登录"**。请:

  1. 退出登录
  2. 重新登录时勾选 "记住密码" 或 "保持登录状态"
  3. 再次检查 Cookies

3. 如果浏览器中有,但导出时丢失

尝试使用 EditThisCookie 扩展的 "Export" 功能时,选择 "Export as JSON" 而不是其他格式。

或者手动复制:在 EditThisCookie 中点击 user 和 pass 这两个 cookie,确认它们存在,然后手动编辑 JSON 添加进去:

[
  // ... 你导出的4个 ...
  {
    "domain": "www.fumamx.com",
    "name": "user",
    "value": "showmin.zhou@163.com",
    "path": "/",
    "secure": false,
    "httpOnly": false,
    "session": false,
    "hostOnly": true,
    "expirationDate": 1776821773,
    "sameSite": "unspecified"
  },
  {
    "domain": "www.fumamx.com", 
    "name": "pass",
    "value": "U2FsdGVkX1+fFGAO5Ib25ulin0hAOBVqhGjgBkmNMW4=",
    "path": "/",
    "secure": false,
    "httpOnly": false,
    "session": false,
    "hostOnly": true,
    "expirationDate": 1776821773,
    "sameSite": "unspecified"
  }
]

关键问题:fumamx API 需要 user + pass 来验证身份,仅 _t token 不够。请检查浏览器中是否有这两个 cookie。


还是不行 —— API 仍然返回 "登陆令牌已失效" (code: -2)


问题分析:

虽然 7 个 cookies 都正确加载了,但 _t 令牌本身已经过期或无效。

_t: ZjtDL09DbYW0mzah  ← 这个 token 已经失效

_t 是一个 session cookie,它的有效期很短:

  • 当你关闭浏览器时失效
  • 或者一段时间不操作后过期
  • 或者从其他设备/浏览器登录时旧 token 被吊销

必须重新获取:

  1. 打开 https://www.fumamx.com
  2. 如果已登录,先退出再重新登录(强制刷新 token)
  3. 登录后立即导出 cookies(不要关闭浏览器)
  4. 将新的 JSON 发给我

注意: _t 是 session 级别的,每次重新登录都会变。必须刚登录完就导出。


找到根本原因了!

问题不在 Cookie,而在 config.py 中的硬编码 token 已过期:

ACCESS_TOKEN = "Iiw4o3RoqltwY5EW"
INDIVIDUAL_ACCESS_TOKEN = "lAInwlJfiKx5FOnF"

fumamx API 使用双层认证:

  1. Cookie _t ✅ (你从浏览器获取的,是对的)
  2. URL 参数 accessToken + individualAccessToken ❌ (硬编码的,已过期)

需要你在浏览器中获取这些动态 token:

  1. 打开 https://www.fumamx.com 并登录
  2. 按 F12 → Network/网络 标签
  3. 在页面中随便搜索一个公司(比如输入 "Apple" 搜索)
  4. 找到请求如 company 或 ImpEnterprise
  5. 查看请求的 URL 参数,找到:
    • accessToken=xxxxx
    • individualAccessToken=xxxxx

或者:

  1. 在 Console 中执行:
localStorage.getItem('accessToken')
localStorage.getItem('individualAccessToken')

能否帮我获取这两个 token? 这样我就能更新 config.py 并验证连接。


之前是通过浏览器脚本抓取的:

  1. 在浏览器控制台粘贴 scripts/capture_api.js
  2. 操作页面(搜索公司等)
  3. 运行 exportFumamxAPIs() 导出 API 请求
  4. 从导出的 JSON 中提取 URL 参数里的 token

但现在有更简单的方法,直接在浏览器控制台执行:

// 方法 1: 从 localStorage 获取
localStorage.getItem('accessToken')
localStorage.getItem('individualAccessToken')

// 方法 2: 从当前 URL 获取(如果你在 fumamx 页面)
new URLSearchParams(window.location.search).get('accessToken')

或者手动复制:

  1. 按 F12 → Network
  2. 随便操作一下(比如搜索 "Apple")
  3. 找一个请求,比如 company 或 ImpEnterprise
  4. 查看请求的 URL,找到:
    https://www.fumamx.com/pcapi/...?accessToken=xxxx&individualAccessToken=yyyy&...
    
  5. 复制 accessToken 和 individualAccessToken 的值给我

最快方式:直接在浏览器控制台输入 localStorage.getItem('accessToken') 回车,把结果发给我。


试试这些方式获取 token:

1. sessionStorage

sessionStorage.getItem('accessToken')
sessionStorage.getItem('individualAccessToken')

2. 查看当前 URL

window.location.href

3. Network 面板抓取(最可靠)

  • F12 → Network
  • 在页面中搜索一个公司(比如输入 "Apple")
  • 找到任意一个 API 请求(如 company、ImpEnterprise)
  • 点击它 → Headers → Request URL
  • 复制里面 accessToken= 和 individualAccessToken= 后面的值

4. 查看所有 cookie

document.cookie

请试一下方法 2 或 3,把获取到的值发给我。


sessionStorage.getItem('accessToken')

'{"ownerId":11011595,"scope":"11011595","value":"71uRBqAwJWWLxWyp","createTime":1776217804534,"expireTime":1776304204534,"referURL":null,"agent":null,"agentVersion":null,"readOnly":0,"versionIteration":1,"companyTag":null,"platform":"pc","cid":1067743}' sessionStorage.getItem('individualAccessToken')

'{"ownerId":10444522,"scope":"independent","value":"CmA5vs7xmGEYcsuD","createTime":1776217804069,"expireTime":1776304204069,"referURL":null,"agent":null,"agentVersion":null,"readOnly":0,"versionIteration":null,"companyTag":null,"platform":"pc","cid":null}'


✅ 成功!API 连接正常,认证有效!

✓ 已加载 7 个 cookies
✓ API 连接正常,认证有效
用户: showmin.zhou@163.com
公司ID: 1067743

Token 已更新:

  • ACCESS_TOKEN: 71uRBqAwJWWLxWyp
  • INDIVIDUAL_ACCESS_TOKEN: CmA5vs7xmGEYcsuD

现在可以正常使用 fumamxCRM 了。让我也重启一下 CRM 后端服务:


从你提供的 sessionStorage 中提取的:

sessionStorage.getItem('accessToken')
'{"ownerId":11011595,"scope":"11011595","value":"71uRBqAwJWWLxWyp",...}'
                               ↑↑↑↑↑↑↑↑↑↑
                               这就是 ACCESS_TOKEN

我提取了 JSON 中 value 字段的值:

  • ACCESS_TOKEN = "71uRBqAwJWWLxWyp"
  • INDIVIDUAL_ACCESS_TOKEN = "CmA5vs7xmGEYcsuD" (来自 individualAccessToken 的 value)

这两个 token 的区别:

Token 来源 用途
ACCESS_TOKEN accessToken.value 公司/企业级别认证
INDIVIDUAL_ACCESS_TOKEN individualAccessToken.value 个人用户级别认证

fumamx API 请求时需要同时提供这两个 token 作为 URL 参数。

对,cookie 里没有 ACCESS_TOKEN。

当前 cookie 文件里只有这 7 个:

Cookie 值
_t CmA5vs7xmGEYcsuD
user showmin.zhou@163.com
pass U2FsdGVkX1...
language zh-cn
timezone Asia/Shanghai
AiImageEdit false
rememberPassword true

注意:_t 的值恰好和 INDIVIDUAL_ACCESS_TOKEN 相同(CmA5vs7xmGEYcsuD),但 ACCESS_TOKEN(71uRBqAwJWWLxWyp)完全不在 cookie 中。


fumamx 的三层认证:

认证层 存储位置 获取方式
Cookie _t Cookie 浏览器导出
INDIVIDUAL_ACCESS_TOKEN Cookie _t / sessionStorage 相同
ACCESS_TOKEN sessionStorage sessionStorage.getItem('accessToken').value

所以之前只更新 cookie 是不够的,还必须从 sessionStorage 获取 ACCESS_TOKEN。


根据代码梳理,给你完整说明:


1️⃣ 现在有几个表?

共 7 个表(SQLite):

表名 用途
users 管理员用户(登录)
customers 本地客户(早期表,现在用下面的)
countries 国家信息(274条数据)
import_tasks 潜在客户导入任务(后台任务)
potential_customers 潜在客户主表(核心)
potential_contacts 潜在客户联系人
deep_analyses 深度分析记录
system_settings 系统设置(API Key等)

2️⃣ 潜在客户列表加载逻辑

前端请求 → 后端查询 → 直接返回(无级联):

# 1. 前端请求
GET /potential-customers/?keyword=xxx&country=US&page=1&page_size=20

# 2. 后端查询(单次查询,无JOIN)
query = db.query(PotentialCustomer)
if keyword:
    query = query.filter(name包含 OR domain包含 OR description包含)
if country:
    query = query.filter(country == country)

# 3. 排序分页
customers = query.order_by(source_score DESC).offset().limit().all()

# 4. 直接返回(反范式设计,状态内联在客户数据中)
return {
    "results": [c.to_dict() for c in customers]  # 包含 analysis_status
}

关键点:

  • analysis_status 直接存储在 potential_customers 表(冗余字段)
  • 无需 JOIN deep_analyses 表
  • 无需二次查询 来判断"是否已分析"

3️⃣ 深度分析过程

队列式后台处理:

用户点击"深度分析"
    ↓
创建 DeepAnalysis 记录 (status=pending)
    ↓
更新 customer.analysis_status = "pending"  ← 反范式同步
    ↓
加入内存队列 (analysis_queue)
    ↓
后台线程取出任务
    ↓
更新 analysis.status = "processing"
更新 customer.analysis_status = "processing"
    ↓
调用 LLM API (DeepSeek/Moonshot)
    ↓
生成报告文件 → 保存到 analysis_reports/
    ↓
更新 analysis.status = "completed"
更新 customer.analysis_status = "completed"
更新 customer.analysis_count += 1

状态流转:

none → pending → processing → completed
                        ↘ failed

反范式设计核心:

  • 分析状态同时存两份:
    1. deep_analyses.status —— 详细记录
    2. potential_customers.analysis_status —— 冗余字段(列表页直接用)

这样列表页查询时只需查一张表,性能提升明显。