17 KiB
我们开始新对话。 你使用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() 导出结果');
})();
操作步骤:
- 登录孚盟 → F12 → Console
- 粘贴上面的代码,回车
- 点击「客户」「邮件」等模块
- 看到
📡 API: GET /api/xxx说明正在记录 - 运行
exportFumamxAPIs()下载 JSON - 把
fumamx_apis.json上传到服务器 - 运行
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后台管理界面。 后台分几个模块:
- 客户管理:客户信息是从哪里来的?是从fumamxCLI搜索并添加而来的。
- 发现:通过fumamxCLI的search命令,搜索到客户信息列表。 客户详情:点击某个客户,进入客户详情 添加客户:可以将某个搜索到的客户,添加到本地数据库。
- 设置: 修改管理员密码; 设置cookie, 因为cookie会过期。设置界面有地方输入cookie的文本内容,后台存储到文件fumamx_cookies.txt,然后提取信息和AccessToken
我们先实现这些基本功能,后续逐步添加更多功能。
以上内容,你是否清晰,如果有问题请反问我。知道你清楚为止。 然后,你提出的技术方案,和实现计划,分步骤完成任务。
问题根源:浏览器导出时缺少 user 和 pass Cookie
这是浏览器导出机制的问题,不是 CRM 的问题。你需要确认浏览器中确实存在这些 Cookie。
排查步骤:
1. 浏览器中验证 Cookie 是否存在
打开 https://www.fumamx.com 并登录后,按 F12 → Application → Cookies → https://www.fumamx.com,检查是否有以下字段:
| Cookie 名 | 值示例 |
|---|---|
user |
showmin.zhou@163.com |
pass |
U2FsdGVkX1+fFGAO5Ib25ulin0hAOBVqhGjgBkmNMW4= |
rememberPassword |
true |
2. 如果浏览器中没有这些 Cookie
说明你没有勾选**"记住密码"或"保持登录"**。请:
- 退出登录
- 重新登录时勾选 "记住密码" 或 "保持登录状态"
- 再次检查 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 被吊销
必须重新获取:
- 打开 https://www.fumamx.com
- 如果已登录,先退出再重新登录(强制刷新 token)
- 登录后立即导出 cookies(不要关闭浏览器)
- 将新的 JSON 发给我
注意: _t 是 session 级别的,每次重新登录都会变。必须刚登录完就导出。
找到根本原因了!
问题不在 Cookie,而在 config.py 中的硬编码 token 已过期:
ACCESS_TOKEN = "Iiw4o3RoqltwY5EW"
INDIVIDUAL_ACCESS_TOKEN = "lAInwlJfiKx5FOnF"
fumamx API 使用双层认证:
- Cookie
_t✅ (你从浏览器获取的,是对的) - URL 参数
accessToken+individualAccessToken❌ (硬编码的,已过期)
需要你在浏览器中获取这些动态 token:
- 打开 https://www.fumamx.com 并登录
- 按 F12 → Network/网络 标签
- 在页面中随便搜索一个公司(比如输入 "Apple" 搜索)
- 找到请求如
company或ImpEnterprise - 查看请求的 URL 参数,找到:
accessToken=xxxxxindividualAccessToken=xxxxx
或者:
- 在 Console 中执行:
localStorage.getItem('accessToken')
localStorage.getItem('individualAccessToken')
能否帮我获取这两个 token? 这样我就能更新 config.py 并验证连接。
之前是通过浏览器脚本抓取的:
- 在浏览器控制台粘贴
scripts/capture_api.js - 操作页面(搜索公司等)
- 运行
exportFumamxAPIs()导出 API 请求 - 从导出的 JSON 中提取 URL 参数里的 token
但现在有更简单的方法,直接在浏览器控制台执行:
// 方法 1: 从 localStorage 获取
localStorage.getItem('accessToken')
localStorage.getItem('individualAccessToken')
// 方法 2: 从当前 URL 获取(如果你在 fumamx 页面)
new URLSearchParams(window.location.search).get('accessToken')
或者手动复制:
- 按 F12 → Network
- 随便操作一下(比如搜索 "Apple")
- 找一个请求,比如
company或ImpEnterprise - 查看请求的 URL,找到:
https://www.fumamx.com/pcapi/...?accessToken=xxxx&individualAccessToken=yyyy&... - 复制
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:71uRBqAwJWWLxWypINDIVIDUAL_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
反范式设计核心:
- 分析状态同时存两份:
deep_analyses.status—— 详细记录potential_customers.analysis_status—— 冗余字段(列表页直接用)
这样列表页查询时只需查一张表,性能提升明显。
