a
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# fumamxCLI - CLI for fumamx.com
|
||||
__version__ = "0.1.0"
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
API 端点发现和测试工具
|
||||
用于找出 fumamx.com 实际使用的 API 端点
|
||||
"""
|
||||
import json
|
||||
import requests
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
def test_endpoints(base_url: str = "https://www.fumamx.com", cookie_file: str = "/home/ubuntu/fumamx_cookies.txt"):
|
||||
"""测试常见的企业系统 API 端点"""
|
||||
|
||||
# 加载 cookies
|
||||
with open(cookie_file, 'r') as f:
|
||||
cookies = {c['name']: c['value'] for c in json.load(f)}
|
||||
|
||||
session = requests.Session()
|
||||
for name, value in cookies.items():
|
||||
session.cookies.set(name, value)
|
||||
|
||||
headers = {
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
}
|
||||
|
||||
# 常见的企业 CRM API 端点模式
|
||||
test_paths = [
|
||||
# 用户相关
|
||||
"/api/v1/user/info",
|
||||
"/api/user/info",
|
||||
"/api/current-user",
|
||||
"/api/account/info",
|
||||
"/rest/account/info",
|
||||
"/v1/api/user",
|
||||
"/sys/user/info",
|
||||
|
||||
# 工作台
|
||||
"/api/workbench/info",
|
||||
"/api/v1/workbench",
|
||||
"/api/dashboard/data",
|
||||
"/rest/workbench",
|
||||
|
||||
# 客户
|
||||
"/api/customer/list",
|
||||
"/api/v1/customers",
|
||||
"/api/crm/customers",
|
||||
"/rest/customers",
|
||||
"/crm/api/customers",
|
||||
|
||||
# 邮件
|
||||
"/api/email/list",
|
||||
"/api/v1/emails",
|
||||
"/rest/emails",
|
||||
|
||||
# 订单
|
||||
"/api/order/list",
|
||||
"/api/v1/orders",
|
||||
"/rest/orders",
|
||||
"/api/trade/orders",
|
||||
|
||||
# 通知
|
||||
"/api/notice/list",
|
||||
"/api/notifications",
|
||||
"/api/message/list",
|
||||
|
||||
# 统计
|
||||
"/api/statistics/overview",
|
||||
"/api/stats/summary",
|
||||
"/api/report/dashboard",
|
||||
|
||||
# 通用
|
||||
"/api/common/config",
|
||||
"/api/system/config",
|
||||
"/api/setting",
|
||||
]
|
||||
|
||||
results = []
|
||||
for path in test_paths:
|
||||
url = base_url + path
|
||||
try:
|
||||
response = session.get(url, headers=headers, timeout=10)
|
||||
status = response.status_code
|
||||
content_type = response.headers.get('Content-Type', '')
|
||||
|
||||
if status == 200:
|
||||
try:
|
||||
data = response.json()
|
||||
preview = str(data)[:100]
|
||||
except:
|
||||
preview = response.text[:100]
|
||||
results.append({
|
||||
"path": path,
|
||||
"status": status,
|
||||
"content_type": content_type,
|
||||
"preview": preview,
|
||||
"valid": True
|
||||
})
|
||||
print(f"✓ {path} - 200 OK")
|
||||
elif status in [401, 403]:
|
||||
results.append({
|
||||
"path": path,
|
||||
"status": status,
|
||||
"valid": "auth_required"
|
||||
})
|
||||
print(f"⚠ {path} - {status} (可能需要认证)")
|
||||
else:
|
||||
print(f"✗ {path} - {status}")
|
||||
except Exception as e:
|
||||
print(f"✗ {path} - Error: {str(e)[:50]}")
|
||||
|
||||
# 保存结果
|
||||
with open("/home/ubuntu/nanocode/fumamxCLI/api_endpoints.json", "w") as f:
|
||||
json.dump(results, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"\n发现 {len([r for r in results if r.get('valid') == True])} 个可用端点")
|
||||
print("结果已保存到 api_endpoints.json")
|
||||
return results
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_endpoints()
|
||||
@@ -0,0 +1,282 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
fumamx 认证模块 - 自动登录获取 Token
|
||||
使用 Playwright 浏览器引擎完成 RSA 加密登录
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
# 登录凭据配置
|
||||
USERNAME = 'showmin.zhou@163.com'
|
||||
PASSWORD = 'Kingc.83695910'
|
||||
|
||||
# Token 缓存文件
|
||||
TOKEN_CACHE_FILE = Path.home() / '.fumamx' / 'tokens.json'
|
||||
|
||||
|
||||
class AuthError(Exception):
|
||||
"""认证错误"""
|
||||
pass
|
||||
|
||||
|
||||
async def login_with_playwright(username: str = USERNAME, password: str = PASSWORD) -> Dict:
|
||||
"""
|
||||
使用 Playwright 浏览器自动登录 fumamx
|
||||
|
||||
Returns:
|
||||
{
|
||||
'success': bool,
|
||||
'accessToken': str or None,
|
||||
'individualAccessToken': str or None,
|
||||
'cookies': dict,
|
||||
'error': str or None
|
||||
}
|
||||
"""
|
||||
try:
|
||||
from playwright.async_api import async_playwright
|
||||
except ImportError:
|
||||
raise AuthError("Playwright 未安装,请运行: pip install playwright && playwright install chromium")
|
||||
|
||||
result = {
|
||||
'success': False,
|
||||
'accessToken': None,
|
||||
'individualAccessToken': None,
|
||||
'cookies': {},
|
||||
'error': None,
|
||||
}
|
||||
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch(
|
||||
headless=True,
|
||||
args=['--no-sandbox', '--disable-dev-shm-usage']
|
||||
)
|
||||
context = await browser.new_context(
|
||||
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
)
|
||||
|
||||
# 注入请求拦截脚本
|
||||
await context.add_init_script("""
|
||||
window._fumamx_data = {
|
||||
payloads: [],
|
||||
responses: {},
|
||||
cookies: {}
|
||||
};
|
||||
|
||||
// 拦截 XHR
|
||||
const origXHRSend = XMLHttpRequest.prototype.send;
|
||||
const origXHROpen = XMLHttpRequest.prototype.open;
|
||||
|
||||
XMLHttpRequest.prototype.open = function(method, url) {
|
||||
this._url = url;
|
||||
this._method = method;
|
||||
return origXHROpen.apply(this, arguments);
|
||||
};
|
||||
|
||||
XMLHttpRequest.prototype.send = function(body) {
|
||||
if (body) {
|
||||
try {
|
||||
const data = JSON.parse(body);
|
||||
if (data && data.pass && data.user) {
|
||||
window._fumamx_data.payloads.push({url: this._url, payload: data});
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
return origXHRSend.apply(this, arguments);
|
||||
};
|
||||
""")
|
||||
|
||||
page = await context.new_page()
|
||||
|
||||
# 监听响应捕获 token
|
||||
login_response = {}
|
||||
|
||||
async def on_response(resp):
|
||||
url = resp.url
|
||||
if 'login/auth' in url or 'login' in url:
|
||||
try:
|
||||
body = await resp.json()
|
||||
login_response['auth'] = body
|
||||
except:
|
||||
pass
|
||||
elif 'codeForToken' in url:
|
||||
try:
|
||||
body = await resp.json()
|
||||
login_response['token'] = body
|
||||
except:
|
||||
pass
|
||||
|
||||
page.on('response', on_response)
|
||||
|
||||
# 打开登录页
|
||||
try:
|
||||
await page.goto('https://fumamx.com', timeout=60000, wait_until='domcontentloaded')
|
||||
except Exception as e:
|
||||
result['error'] = f"页面加载失败: {e}"
|
||||
await browser.close()
|
||||
return result
|
||||
|
||||
await asyncio.sleep(3)
|
||||
|
||||
# 填写表单
|
||||
try:
|
||||
await page.fill('input[type="text"]', username)
|
||||
await page.fill('input[type="password"]', password)
|
||||
except Exception as e:
|
||||
result['error'] = f"表单填写失败: {e}"
|
||||
await browser.close()
|
||||
return result
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# 提交登录
|
||||
try:
|
||||
await page.click('button:has-text("登录")')
|
||||
except:
|
||||
try:
|
||||
await page.press('input[type="password"]', 'Enter')
|
||||
except Exception as e:
|
||||
result['error'] = f"登录提交失败: {e}"
|
||||
await browser.close()
|
||||
return result
|
||||
|
||||
# 等待登录完成
|
||||
await asyncio.sleep(5)
|
||||
|
||||
# 检查是否登录成功
|
||||
current_url = page.url
|
||||
if 'login' in current_url or 'error' in current_url.lower():
|
||||
result['error'] = "登录失败,请检查用户名和密码"
|
||||
await browser.close()
|
||||
return result
|
||||
|
||||
# 从响应中提取 token (备用方法)
|
||||
if 'auth' in login_response:
|
||||
auth_data = login_response['auth']
|
||||
if auth_data.get('code') == '0' or auth_data.get('code') == 0:
|
||||
result['success'] = True
|
||||
data = auth_data.get('data', {})
|
||||
access_token = data.get('accessToken')
|
||||
if isinstance(access_token, dict):
|
||||
result['accessToken'] = access_token.get('value')
|
||||
elif isinstance(access_token, str):
|
||||
result['accessToken'] = access_token
|
||||
else:
|
||||
result['error'] = f"登录失败: {auth_data.get('msg', '未知错误')}"
|
||||
|
||||
if 'token' in login_response:
|
||||
token_data = login_response['token']
|
||||
if token_data.get('code') == '0':
|
||||
result['individualAccessToken'] = token_data.get('data', {}).get('value')
|
||||
|
||||
# 方法2: 直接从 sessionStorage 获取 (更可靠)
|
||||
try:
|
||||
session_data = await page.evaluate("""() => {
|
||||
return {
|
||||
accessToken: sessionStorage.getItem('accessToken'),
|
||||
individualAccessToken: sessionStorage.getItem('individualAccessToken')
|
||||
};
|
||||
}""")
|
||||
|
||||
if session_data.get('accessToken'):
|
||||
try:
|
||||
at_data = json.loads(session_data['accessToken'])
|
||||
if isinstance(at_data, dict):
|
||||
result['accessToken'] = at_data.get('value')
|
||||
else:
|
||||
result['accessToken'] = str(at_data)
|
||||
except:
|
||||
result['accessToken'] = session_data['accessToken']
|
||||
|
||||
if session_data.get('individualAccessToken'):
|
||||
try:
|
||||
iat_data = json.loads(session_data['individualAccessToken'])
|
||||
if isinstance(iat_data, dict):
|
||||
result['individualAccessToken'] = iat_data.get('value')
|
||||
else:
|
||||
result['individualAccessToken'] = str(iat_data)
|
||||
except:
|
||||
result['individualAccessToken'] = session_data['individualAccessToken']
|
||||
|
||||
if session_data.get('accessToken') or session_data.get('individualAccessToken'):
|
||||
result['success'] = True
|
||||
|
||||
except Exception as e:
|
||||
print(f"[debug] sessionStorage 获取失败: {e}")
|
||||
|
||||
# 获取 cookies
|
||||
cookies = await context.cookies()
|
||||
for cookie in cookies:
|
||||
result['cookies'][cookie['name']] = cookie['value']
|
||||
|
||||
await browser.close()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def save_tokens(tokens: Dict) -> bool:
|
||||
"""保存 token 到缓存文件"""
|
||||
try:
|
||||
TOKEN_CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(TOKEN_CACHE_FILE, 'w') as f:
|
||||
json.dump({
|
||||
'accessToken': tokens.get('accessToken'),
|
||||
'individualAccessToken': tokens.get('individualAccessToken'),
|
||||
'cookies': tokens.get('cookies', {})
|
||||
}, f, indent=2)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"保存 token 失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def load_tokens() -> Optional[Dict]:
|
||||
"""从缓存文件加载 token"""
|
||||
try:
|
||||
if not TOKEN_CACHE_FILE.exists():
|
||||
return None
|
||||
with open(TOKEN_CACHE_FILE, 'r') as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def refresh_auth() -> Dict:
|
||||
"""
|
||||
刷新认证 - 使用 Playwright 重新登录
|
||||
|
||||
Returns:
|
||||
包含新 token 的字典
|
||||
"""
|
||||
print("[auth] 正在使用 Playwright 自动登录...")
|
||||
result = asyncio.run(login_with_playwright())
|
||||
|
||||
if result['success']:
|
||||
save_tokens(result)
|
||||
print(f"[auth] ✓ 登录成功")
|
||||
print(f"[auth] accessToken: {result['accessToken'][:20]}...")
|
||||
print(f"[auth] individualAccessToken: {result['individualAccessToken'][:20]}...")
|
||||
else:
|
||||
print(f"[auth] ✗ 登录失败: {result['error']}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_auth_headers() -> Dict[str, str]:
|
||||
"""获取认证请求头"""
|
||||
tokens = load_tokens()
|
||||
if not tokens:
|
||||
return {}
|
||||
|
||||
return {
|
||||
'accessToken': tokens.get('accessToken', ''),
|
||||
'individualAccessToken': tokens.get('individualAccessToken', ''),
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试登录
|
||||
result = asyncio.run(login_with_playwright())
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
@@ -0,0 +1,623 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
fumamxCLI - 孚盟MX 贸易数据命令行工具
|
||||
基于抓取的实际 API 生成
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich.panel import Panel
|
||||
from rich.json import JSON
|
||||
from rich.tree import Tree
|
||||
|
||||
from .client import FumamxClient
|
||||
from . import config
|
||||
from .auth import refresh_auth, load_tokens
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
def get_client():
|
||||
"""获取 API 客户端实例"""
|
||||
cookie_file = "/home/ubuntu/fumamx_cookies.txt"
|
||||
if not Path(cookie_file).exists():
|
||||
console.print(f"[red]✗ Cookie 文件不存在: {cookie_file}[/red]")
|
||||
sys.exit(1)
|
||||
return FumamxClient(cookie_file)
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.version_option(version="0.2.0")
|
||||
def cli():
|
||||
"""fumamxCLI - 孚盟MX 贸易数据命令行工具
|
||||
|
||||
使用已保存的 Cookie 访问 fumamx.com 贸易数据 API
|
||||
|
||||
主要功能:
|
||||
- 公司搜索与详情查询
|
||||
- 进口/出口企业数据分析
|
||||
- 产品 HS 编码分析
|
||||
- 贸易记录查询
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# ============ 系统命令 ============
|
||||
|
||||
@cli.command("status")
|
||||
def status():
|
||||
"""检查登录状态和 API 连通性"""
|
||||
try:
|
||||
client = get_client()
|
||||
# 测试公司搜索端点
|
||||
result = client.search_company("test")
|
||||
if result.get("code") == "0":
|
||||
console.print("[green]✓ API 连接正常,认证有效[/green]")
|
||||
console.print(f"[dim]用户: {client.cookies.get('user', 'N/A')}[/dim]")
|
||||
console.print(f"[dim]公司ID: {config.COMPANY_ID}[/dim]")
|
||||
else:
|
||||
console.print(f"[yellow]⚠ API 响应异常: {result}[/yellow]")
|
||||
except Exception as e:
|
||||
console.print(f"[red]✗ 连接失败: {e}[/red]")
|
||||
console.print("[yellow]提示: 尝试运行 'fumamx login' 重新获取认证[/yellow]")
|
||||
|
||||
|
||||
@cli.command("login")
|
||||
def login_cmd():
|
||||
"""使用 Playwright 自动登录获取 Token
|
||||
|
||||
当 Cookie 文件或缓存的 Token 失效时,使用此命令重新登录。
|
||||
会自动完成 RSA 加密并获取新的 accessToken 和 individualAccessToken。
|
||||
"""
|
||||
from .auth import USERNAME, PASSWORD, refresh_auth
|
||||
|
||||
console.print("[cyan]正在使用 Playwright 自动登录...[/cyan]")
|
||||
console.print(f"[dim]账号: {USERNAME}[/dim]")
|
||||
|
||||
try:
|
||||
# refresh_auth 已经内部处理了 asyncio.run
|
||||
result = refresh_auth()
|
||||
|
||||
if result['success']:
|
||||
console.print("[green]✓ 登录成功[/green]")
|
||||
access_token = result.get('accessToken', '')
|
||||
individual_token = result.get('individualAccessToken', '')
|
||||
console.print(f"[dim]accessToken: {access_token[:20] if access_token else 'N/A'}...[/dim]")
|
||||
console.print(f"[dim]individualAccessToken: {individual_token[:20] if individual_token else 'N/A'}...[/dim]")
|
||||
|
||||
# 验证认证
|
||||
console.print("[cyan]正在验证 API 连接...[/cyan]")
|
||||
client = FumamxClient(auto_login=False)
|
||||
if client.check_auth():
|
||||
console.print("[green]✓ API 连接正常,认证有效[/green]")
|
||||
else:
|
||||
console.print("[yellow]⚠ 登录成功但 API 验证失败[/yellow]")
|
||||
else:
|
||||
console.print(f"[red]✗ 登录失败: {result.get('error', '未知错误')}[/red]")
|
||||
except Exception as e:
|
||||
console.print(f"[red]✗ 登录出错: {e}[/red]")
|
||||
console.print("[yellow]提示: 确保已安装 Playwright[/yellow]")
|
||||
console.print(" pip install playwright && playwright install chromium")
|
||||
|
||||
|
||||
@cli.command("cookies")
|
||||
def show_cookies():
|
||||
"""显示已加载的 cookies"""
|
||||
cookie_file = "/home/ubuntu/fumamx_cookies.txt"
|
||||
|
||||
# 先显示缓存的 tokens
|
||||
tokens = load_tokens()
|
||||
if tokens:
|
||||
console.print("[cyan]缓存的 Tokens:[/cyan]")
|
||||
console.print(f" accessToken: {tokens.get('accessToken', 'N/A')[:30]}...")
|
||||
console.print(f" individualAccessToken: {tokens.get('individualAccessToken', 'N/A')[:30]}...")
|
||||
console.print()
|
||||
|
||||
if not Path(cookie_file).exists():
|
||||
console.print(f"[yellow]Cookie 文件不存在: {cookie_file}[/yellow]")
|
||||
return
|
||||
|
||||
with open(cookie_file, 'r') as f:
|
||||
cookies = json.load(f)
|
||||
|
||||
table = Table(title="已加载的 Cookies")
|
||||
table.add_column("名称", style="cyan")
|
||||
table.add_column("值", style="green")
|
||||
table.add_column("域名", style="yellow")
|
||||
|
||||
for cookie in cookies:
|
||||
value = cookie['value'][:40] + "..." if len(cookie['value']) > 40 else cookie['value']
|
||||
table.add_row(cookie['name'], value, cookie.get('domain', 'N/A'))
|
||||
|
||||
console.print(table)
|
||||
|
||||
|
||||
# ============ 公司搜索命令 ============
|
||||
|
||||
@cli.group()
|
||||
def company():
|
||||
"""公司搜索与查询"""
|
||||
pass
|
||||
|
||||
|
||||
@company.command("search")
|
||||
@click.argument("keyword")
|
||||
@click.option("--page", "-p", default=1, help="页码")
|
||||
@click.option("--page-size", "-s", default=10, help="每页数量")
|
||||
@click.option("--raw", is_flag=True, help="直接输出原始 JSON")
|
||||
def company_search(keyword, page, page_size, raw):
|
||||
"""搜索公司/企业
|
||||
|
||||
示例: fumamx company search "Geomatrix"
|
||||
"""
|
||||
client = get_client()
|
||||
try:
|
||||
data = client.search_company(keyword, page, page_size)
|
||||
if raw:
|
||||
# 直接输出 JSON,供程序解析
|
||||
click.echo(json.dumps(data, ensure_ascii=False))
|
||||
else:
|
||||
console.print(Panel.fit(
|
||||
JSON(json.dumps(data, ensure_ascii=False, indent=2)),
|
||||
title=f"公司搜索: {keyword} (第{page}页)"
|
||||
))
|
||||
except Exception as e:
|
||||
console.print(f"[red]✗ 搜索失败: {e}[/red]")
|
||||
|
||||
|
||||
@company.command("info")
|
||||
@click.argument("company_id")
|
||||
@click.argument("domain")
|
||||
@click.option("--raw", is_flag=True, help="直接输出原始 JSON")
|
||||
def company_info(company_id, domain, raw):
|
||||
"""获取公司详细信息
|
||||
|
||||
示例: fumamx company info "f35246ed..." "vectorgeo.co"
|
||||
|
||||
包含: 地址、电话、员工数、行业、LinkedIn 等信息
|
||||
"""
|
||||
client = get_client()
|
||||
try:
|
||||
data = client.get_company_info(company_id, domain)
|
||||
if raw:
|
||||
click.echo(json.dumps(data, ensure_ascii=False))
|
||||
else:
|
||||
console.print(Panel.fit(
|
||||
JSON(json.dumps(data, ensure_ascii=False, indent=2)),
|
||||
title=f"公司详情: {domain}"
|
||||
))
|
||||
except Exception as e:
|
||||
console.print(f"[red]✗ 获取失败: {e}[/red]")
|
||||
|
||||
|
||||
@company.command("contacts")
|
||||
@click.argument("company_id")
|
||||
@click.argument("domain")
|
||||
@click.option("--raw", is_flag=True, help="直接输出原始 JSON")
|
||||
@click.option("--limit", "-l", default=10, help="显示联系人数量限制")
|
||||
def company_contacts(company_id, domain, raw, limit):
|
||||
"""获取公司联系人列表
|
||||
|
||||
示例: fumamx company contacts "f35246ed..." "vectorgeo.co"
|
||||
|
||||
包含: 姓名、邮箱、职位、LinkedIn、电话等
|
||||
"""
|
||||
client = get_client()
|
||||
try:
|
||||
data = client.get_company_contacts(company_id, domain)
|
||||
if raw:
|
||||
click.echo(json.dumps(data, ensure_ascii=False))
|
||||
else:
|
||||
# 格式化输出联系人列表
|
||||
if data.get("code") == 0 and data.get("data", {}).get("list"):
|
||||
contacts = data["data"]["list"][:limit]
|
||||
table = Table(title=f"联系人列表: {domain}")
|
||||
table.add_column("姓名", style="cyan")
|
||||
table.add_column("职位", style="green")
|
||||
table.add_column("邮箱", style="yellow")
|
||||
table.add_column("电话", style="red")
|
||||
table.add_column("LinkedIn", style="blue")
|
||||
|
||||
for c in contacts:
|
||||
table.add_row(
|
||||
c.get("name") or "-",
|
||||
c.get("position") or "-",
|
||||
c.get("email") or "-",
|
||||
c.get("phone") or c.get("realWhatsApp") or "-",
|
||||
"✓" if c.get("linkedin") else "-"
|
||||
)
|
||||
console.print(table)
|
||||
console.print(f"\n[dim]总计: {data['data'].get('workerNumber', 0)} 人, "
|
||||
f"高管: {data['data'].get('executiveCount', 0)} 人, "
|
||||
f"有电话: {data['data'].get('phoneCount', 0)} 人[/dim]")
|
||||
else:
|
||||
console.print(Panel.fit(
|
||||
JSON(json.dumps(data, ensure_ascii=False, indent=2)),
|
||||
title=f"联系人: {domain}"
|
||||
))
|
||||
except Exception as e:
|
||||
console.print(f"[red]✗ 获取失败: {e}[/red]")
|
||||
|
||||
|
||||
@company.command("phones")
|
||||
@click.argument("domain")
|
||||
@click.option("--raw", is_flag=True, help="直接输出原始 JSON")
|
||||
def company_phones(domain, raw):
|
||||
"""获取公司电话/图片信息
|
||||
|
||||
示例: fumamx company phones "vectorgeo.co"
|
||||
|
||||
通过爬虫抓取的电话、图片等信息
|
||||
"""
|
||||
client = get_client()
|
||||
try:
|
||||
data = client.get_company_phones(domain)
|
||||
if raw:
|
||||
click.echo(json.dumps(data, ensure_ascii=False))
|
||||
else:
|
||||
console.print(Panel.fit(
|
||||
JSON(json.dumps(data, ensure_ascii=False, indent=2)),
|
||||
title=f"电话/图片信息: {domain}"
|
||||
))
|
||||
except Exception as e:
|
||||
console.print(f"[red]✗ 获取失败: {e}[/red]")
|
||||
|
||||
|
||||
# ============ 进口数据命令 ============
|
||||
|
||||
@cli.group()
|
||||
def import_():
|
||||
"""进口企业数据分析"""
|
||||
pass
|
||||
|
||||
|
||||
@import_.command("enterprise")
|
||||
@click.argument("company_name")
|
||||
@click.option("--country", "-c", default="", help="国家筛选")
|
||||
@click.option("--page", "-p", default=1, help="页码")
|
||||
@click.option("--page-size", "-s", default=20, help="每页数量")
|
||||
def import_enterprise(company_name, country, page, page_size):
|
||||
"""获取进口企业数据
|
||||
|
||||
示例: fumamx import enterprise "Apple Inc" --country "US"
|
||||
"""
|
||||
client = get_client()
|
||||
try:
|
||||
data = client.import_enterprise(company_name, country)
|
||||
console.print(Panel.fit(
|
||||
JSON(json.dumps(data, ensure_ascii=False, indent=2)),
|
||||
title=f"进口企业: {company_name}"
|
||||
))
|
||||
except Exception as e:
|
||||
console.print(f"[red]✗ 获取失败: {e}[/red]")
|
||||
|
||||
|
||||
@import_.command("stats")
|
||||
@click.argument("company_name")
|
||||
def import_stats(company_name):
|
||||
"""获取进口企业统计数据
|
||||
|
||||
示例: fumamx import stats "Apple Inc"
|
||||
"""
|
||||
client = get_client()
|
||||
try:
|
||||
data = client.import_enterprise_stats(company_name)
|
||||
console.print(Panel.fit(
|
||||
JSON(json.dumps(data, ensure_ascii=False, indent=2)),
|
||||
title=f"进口统计: {company_name}"
|
||||
))
|
||||
except Exception as e:
|
||||
console.print(f"[red]✗ 获取失败: {e}[/red]")
|
||||
|
||||
|
||||
# ============ 出口数据命令 ============
|
||||
|
||||
@cli.group()
|
||||
def export():
|
||||
"""出口企业数据分析"""
|
||||
pass
|
||||
|
||||
|
||||
@export.command("enterprise")
|
||||
@click.argument("company_name")
|
||||
@click.option("--country", "-c", default="", help="国家筛选")
|
||||
@click.option("--page", "-p", default=1, help="页码")
|
||||
@click.option("--page-size", "-s", default=20, help="每页数量")
|
||||
def export_enterprise(company_name, country, page, page_size):
|
||||
"""获取出口企业数据
|
||||
|
||||
示例: fumamx export enterprise "Foxconn" --country "CN"
|
||||
"""
|
||||
client = get_client()
|
||||
try:
|
||||
data = client.export_enterprise(company_name, country)
|
||||
console.print(Panel.fit(
|
||||
JSON(json.dumps(data, ensure_ascii=False, indent=2)),
|
||||
title=f"出口企业: {company_name}"
|
||||
))
|
||||
except Exception as e:
|
||||
console.print(f"[red]✗ 获取失败: {e}[/red]")
|
||||
|
||||
|
||||
@export.command("stats")
|
||||
@click.argument("company_name")
|
||||
def export_stats(company_name):
|
||||
"""获取出口企业统计数据
|
||||
|
||||
示例: fumamx export stats "Foxconn"
|
||||
"""
|
||||
client = get_client()
|
||||
try:
|
||||
data = client.export_enterprise_stats(company_name)
|
||||
console.print(Panel.fit(
|
||||
JSON(json.dumps(data, ensure_ascii=False, indent=2)),
|
||||
title=f"出口统计: {company_name}"
|
||||
))
|
||||
except Exception as e:
|
||||
console.print(f"[red]✗ 获取失败: {e}[/red]")
|
||||
|
||||
|
||||
# ============ 产品分析命令 ============
|
||||
|
||||
@cli.group()
|
||||
def product():
|
||||
"""产品数据分析"""
|
||||
pass
|
||||
|
||||
|
||||
@product.command("hs-code")
|
||||
@click.argument("hs_code")
|
||||
def product_hs_code(hs_code):
|
||||
"""HS 编码分析
|
||||
|
||||
示例: fumamx product hs-code "8517.12.00"
|
||||
"""
|
||||
client = get_client()
|
||||
try:
|
||||
data = client.hs_code_analysis(hs_code)
|
||||
console.print(Panel.fit(
|
||||
JSON(json.dumps(data, ensure_ascii=False, indent=2)),
|
||||
title=f"HS 编码分析: {hs_code}"
|
||||
))
|
||||
except Exception as e:
|
||||
console.print(f"[red]✗ 分析失败: {e}[/red]")
|
||||
|
||||
|
||||
@product.command("agg")
|
||||
@click.argument("company_name")
|
||||
def product_agg(company_name):
|
||||
"""获取公司产品聚合数据
|
||||
|
||||
示例: fumamx product agg "Apple Inc"
|
||||
"""
|
||||
client = get_client()
|
||||
try:
|
||||
data = client.company_agg_data(company_name)
|
||||
console.print(Panel.fit(
|
||||
JSON(json.dumps(data, ensure_ascii=False, indent=2)),
|
||||
title=f"产品聚合: {company_name}"
|
||||
))
|
||||
except Exception as e:
|
||||
console.print(f"[red]✗ 获取失败: {e}[/red]")
|
||||
|
||||
|
||||
@product.command("analysis")
|
||||
@click.argument("company_name")
|
||||
def product_analysis(company_name):
|
||||
"""出口产品分析
|
||||
|
||||
示例: fumamx product analysis "Foxconn"
|
||||
"""
|
||||
client = get_client()
|
||||
try:
|
||||
data = client.export_enterprise_analysis(company_name)
|
||||
console.print(Panel.fit(
|
||||
JSON(json.dumps(data, ensure_ascii=False, indent=2)),
|
||||
title=f"产品分析: {company_name}"
|
||||
))
|
||||
except Exception as e:
|
||||
console.print(f"[red]✗ 分析失败: {e}[/red]")
|
||||
|
||||
|
||||
@product.command("compare-lose")
|
||||
@click.argument("company_name")
|
||||
@click.argument("compare_company")
|
||||
def product_compare_lose(company_name, compare_company):
|
||||
"""出口对比流失分析
|
||||
|
||||
示例: fumamx product compare-lose "Company A" "Company B"
|
||||
"""
|
||||
client = get_client()
|
||||
try:
|
||||
data = client.export_compare_lose_analysis(company_name, compare_company)
|
||||
console.print(Panel.fit(
|
||||
JSON(json.dumps(data, ensure_ascii=False, indent=2)),
|
||||
title=f"流失分析: {company_name} vs {compare_company}"
|
||||
))
|
||||
except Exception as e:
|
||||
console.print(f"[red]✗ 分析失败: {e}[/red]")
|
||||
|
||||
|
||||
@product.command("compare-new")
|
||||
@click.argument("company_name")
|
||||
@click.argument("compare_company")
|
||||
def product_compare_new(company_name, compare_company):
|
||||
"""出口对比新增分析
|
||||
|
||||
示例: fumamx product compare-new "Company A" "Company B"
|
||||
"""
|
||||
client = get_client()
|
||||
try:
|
||||
data = client.export_compare_new_analysis(company_name, compare_company)
|
||||
console.print(Panel.fit(
|
||||
JSON(json.dumps(data, ensure_ascii=False, indent=2)),
|
||||
title=f"新增分析: {company_name} vs {compare_company}"
|
||||
))
|
||||
except Exception as e:
|
||||
console.print(f"[red]✗ 分析失败: {e}[/red]")
|
||||
|
||||
|
||||
# ============ 国家分析命令 ============
|
||||
|
||||
@cli.group()
|
||||
def country():
|
||||
"""国家贸易数据分析"""
|
||||
pass
|
||||
|
||||
|
||||
@country.command("export")
|
||||
@click.argument("country_name")
|
||||
def country_export(country_name):
|
||||
"""出口国家分析
|
||||
|
||||
示例: fumamx country export "United States"
|
||||
"""
|
||||
client = get_client()
|
||||
try:
|
||||
data = client.export_country_analysis(country_name)
|
||||
console.print(Panel.fit(
|
||||
JSON(json.dumps(data, ensure_ascii=False, indent=2)),
|
||||
title=f"国家出口分析: {country_name}"
|
||||
))
|
||||
except Exception as e:
|
||||
console.print(f"[red]✗ 分析失败: {e}[/red]")
|
||||
|
||||
|
||||
@country.command("data")
|
||||
@click.argument("country_name")
|
||||
def country_data(country_name):
|
||||
"""国家贸易数据详情
|
||||
|
||||
示例: fumamx country data "China"
|
||||
"""
|
||||
client = get_client()
|
||||
try:
|
||||
data = client.country_analysis_data(country_name)
|
||||
console.print(Panel.fit(
|
||||
JSON(json.dumps(data, ensure_ascii=False, indent=2)),
|
||||
title=f"国家数据: {country_name}"
|
||||
))
|
||||
except Exception as e:
|
||||
console.print(f"[red]✗ 获取失败: {e}[/red]")
|
||||
|
||||
|
||||
@country.command("list")
|
||||
def country_list():
|
||||
"""获取贸易来源国家列表"""
|
||||
client = get_client()
|
||||
try:
|
||||
data = client.get_trade_source_countries()
|
||||
console.print(Panel.fit(
|
||||
JSON(json.dumps(data, ensure_ascii=False, indent=2)),
|
||||
title="贸易来源国家"
|
||||
))
|
||||
except Exception as e:
|
||||
console.print(f"[red]✗ 获取失败: {e}[/red]")
|
||||
|
||||
|
||||
# ============ 贸易记录命令 ============
|
||||
|
||||
@cli.group()
|
||||
def trade():
|
||||
"""贸易记录查询"""
|
||||
pass
|
||||
|
||||
|
||||
@trade.command("detail")
|
||||
@click.argument("trade_no")
|
||||
@click.option("--country", "-c", default="", help="来源国家")
|
||||
@click.option("--date", "-d", default="", help="注册日期")
|
||||
@click.option("--type", "-t", default="", help="进出口类型")
|
||||
def trade_detail(trade_no, country, date, type):
|
||||
"""获取完整贸易详情
|
||||
|
||||
示例: fumamx trade detail "TRADE2024001" --country "US"
|
||||
"""
|
||||
client = get_client()
|
||||
try:
|
||||
data = client.trade_detail_full(trade_no, country, date, type)
|
||||
console.print(Panel.fit(
|
||||
JSON(json.dumps(data, ensure_ascii=False, indent=2)),
|
||||
title=f"贸易详情: {trade_no}"
|
||||
))
|
||||
except Exception as e:
|
||||
console.print(f"[red]✗ 获取失败: {e}[/red]")
|
||||
|
||||
|
||||
# ============ 通用 API 调用命令 ============
|
||||
|
||||
@cli.command("call")
|
||||
@click.argument("method")
|
||||
@click.argument("path")
|
||||
@click.option("--data", "-d", help="POST/PUT 数据 (JSON 格式)")
|
||||
@click.option("--params", "-p", help="查询参数 (JSON 格式)")
|
||||
def api_call(method, path, data, params):
|
||||
"""调用任意 API 端点
|
||||
|
||||
示例:
|
||||
fumamx call GET /pcapi/tradeData/api/V2/Account/GetTradeSourceCountrys
|
||||
fumamx call POST /pcapi/tradeData/api/V2/TradeCompany/ImpEnterprise -d '{"companyName":"Apple"}'
|
||||
"""
|
||||
client = get_client()
|
||||
|
||||
request_data = json.loads(data) if data else None
|
||||
request_params = json.loads(params) if params else None
|
||||
|
||||
try:
|
||||
result = client.call_api(method, path, request_data, request_params)
|
||||
console.print(Panel.fit(
|
||||
JSON(json.dumps(result, ensure_ascii=False, indent=2)),
|
||||
title=f"{method} {path}"
|
||||
))
|
||||
except Exception as e:
|
||||
console.print(f"[red]✗ 调用失败: {e}[/red]")
|
||||
|
||||
|
||||
@cli.command("list-apis")
|
||||
def list_apis():
|
||||
"""列出所有可用的 API 命令"""
|
||||
tree = Tree("[bold cyan]fumamxCLI 可用命令[/bold cyan]")
|
||||
|
||||
sys_node = tree.add("[yellow]系统命令[/yellow]")
|
||||
sys_node.add("status - 检查登录状态")
|
||||
sys_node.add("cookies - 显示已加载的 cookies")
|
||||
sys_node.add("call - 调用任意 API 端点")
|
||||
|
||||
company_node = tree.add("[green]公司查询 (company)[/green]")
|
||||
company_node.add("search <keyword> - 搜索公司")
|
||||
company_node.add("info <id> <domain> - 公司详细信息")
|
||||
company_node.add("contacts <id> <domain> - 联系人列表")
|
||||
company_node.add("phones <domain> - 电话/图片信息")
|
||||
|
||||
import_node = tree.add("[blue]进口数据 (import)[/blue]")
|
||||
import_node.add("enterprise <name> - 进口企业数据")
|
||||
import_node.add("stats <name> - 进口统计")
|
||||
|
||||
export_node = tree.add("[magenta]出口数据 (export)[/magenta]")
|
||||
export_node.add("enterprise <name> - 出口企业数据")
|
||||
export_node.add("stats <name> - 出口统计")
|
||||
|
||||
product_node = tree.add("[cyan]产品分析 (product)[/cyan]")
|
||||
product_node.add("hs-code <code> - HS 编码分析")
|
||||
product_node.add("agg <name> - 公司产品聚合")
|
||||
product_node.add("analysis <name> - 产品分析")
|
||||
product_node.add("compare-lose <a> <b> - 流失对比")
|
||||
product_node.add("compare-new <a> <b> - 新增对比")
|
||||
|
||||
country_node = tree.add("[yellow]国家分析 (country)[/yellow]")
|
||||
country_node.add("export <name> - 出口国家分析")
|
||||
country_node.add("data <name> - 国家贸易数据")
|
||||
country_node.add("list - 贸易来源国家列表")
|
||||
|
||||
trade_node = tree.add("[red]贸易记录 (trade)[/red]")
|
||||
trade_node.add("detail <no> - 贸易详情")
|
||||
|
||||
console.print(tree)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
@@ -0,0 +1,493 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Fumamx API Client - 基于抓取的实际 API 生成
|
||||
支持自动登录刷新 Token
|
||||
"""
|
||||
import json
|
||||
import requests
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, List, Any
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from . import config
|
||||
from .auth import refresh_auth, load_tokens, AuthError
|
||||
|
||||
|
||||
class FumamxClient:
|
||||
"""孚盟MX API 客户端 - 贸易数据模块
|
||||
|
||||
认证优先级:
|
||||
1. 使用传入的 cookie_file 中的 cookies
|
||||
2. 使用缓存的 tokens (通过 Playwright 登录获得)
|
||||
3. 自动触发 Playwright 登录获取新 tokens
|
||||
"""
|
||||
|
||||
BASE_URL = "https://www.fumamx.com"
|
||||
|
||||
def __init__(self, cookie_file: str = "/home/ubuntu/fumamx_cookies.txt", auto_login: bool = True):
|
||||
self.cookie_file = cookie_file
|
||||
self.session = requests.Session()
|
||||
self.cookies = {}
|
||||
self.common_params = {}
|
||||
self.auto_login = auto_login
|
||||
self._auth_refreshed = False
|
||||
|
||||
# 尝试加载认证信息
|
||||
if not self._load_cookies() and auto_login:
|
||||
# Cookie 加载失败,尝试使用缓存的 token
|
||||
if not self._load_cached_tokens():
|
||||
# 缓存也没有,触发自动登录
|
||||
self._do_auto_login()
|
||||
|
||||
self._init_common_params()
|
||||
|
||||
def _load_cookies(self) -> bool:
|
||||
"""从 JSON 文件加载 cookies
|
||||
|
||||
Returns:
|
||||
bool: 是否成功加载
|
||||
"""
|
||||
try:
|
||||
if not Path(self.cookie_file).exists():
|
||||
return False
|
||||
|
||||
with open(self.cookie_file, 'r') as f:
|
||||
cookie_list = json.load(f)
|
||||
|
||||
for cookie in cookie_list:
|
||||
name = cookie.get('name')
|
||||
value = cookie.get('value')
|
||||
if name and value:
|
||||
self.cookies[name] = value
|
||||
self.session.cookies.set(name, value)
|
||||
|
||||
print(f"✓ 已加载 {len(self.cookies)} 个 cookies")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"⚠ 加载 cookies 失败: {e}")
|
||||
return False
|
||||
|
||||
def _load_cached_tokens(self) -> bool:
|
||||
"""从缓存加载 tokens 和 cookies
|
||||
|
||||
Returns:
|
||||
bool: 是否成功加载
|
||||
"""
|
||||
tokens = load_tokens()
|
||||
if not tokens:
|
||||
return False
|
||||
|
||||
has_tokens = tokens.get('accessToken') and tokens.get('individualAccessToken')
|
||||
|
||||
if has_tokens:
|
||||
print("✓ 使用缓存的 tokens")
|
||||
|
||||
# 同时加载缓存的 cookies (如 _t)
|
||||
cached_cookies = tokens.get('cookies', {})
|
||||
for name, value in cached_cookies.items():
|
||||
self.cookies[name] = value
|
||||
self.session.cookies.set(name, value)
|
||||
|
||||
if cached_cookies:
|
||||
print(f"✓ 已加载 {len(cached_cookies)} 个缓存 cookies")
|
||||
|
||||
return has_tokens
|
||||
|
||||
def _do_auto_login(self) -> bool:
|
||||
"""触发自动登录获取新 tokens 和 cookies
|
||||
|
||||
Returns:
|
||||
bool: 是否登录成功
|
||||
"""
|
||||
if self._auth_refreshed:
|
||||
return False # 避免循环刷新
|
||||
|
||||
try:
|
||||
print("⚠ Cookie/Token 无效,正在自动登录...")
|
||||
result = refresh_auth()
|
||||
if result['success']:
|
||||
self._auth_refreshed = True
|
||||
# 加载登录获得的 cookies 到 session
|
||||
login_cookies = result.get('cookies', {})
|
||||
for name, value in login_cookies.items():
|
||||
self.cookies[name] = value
|
||||
self.session.cookies.set(name, value)
|
||||
if login_cookies:
|
||||
print(f"✓ 已加载 {len(login_cookies)} 个登录 cookies")
|
||||
# 重新加载 tokens
|
||||
self._load_cached_tokens()
|
||||
return True
|
||||
else:
|
||||
print(f"✗ 自动登录失败: {result.get('error')}")
|
||||
return False
|
||||
except AuthError as e:
|
||||
print(f"✗ 自动登录需要 Playwright: {e}")
|
||||
print(" 请运行: pip install playwright && playwright install chromium")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"✗ 自动登录出错: {e}")
|
||||
return False
|
||||
|
||||
def _init_common_params(self):
|
||||
"""初始化公共参数(优先使用缓存/自动登录获取的 token)"""
|
||||
# 优先使用缓存的 tokens
|
||||
tokens = load_tokens()
|
||||
access_token = tokens.get('accessToken') if tokens else None
|
||||
individual_token = tokens.get('individualAccessToken') if tokens else None
|
||||
|
||||
# 如果没有缓存的,使用配置文件中的
|
||||
if not access_token:
|
||||
access_token = config.ACCESS_TOKEN
|
||||
if not individual_token:
|
||||
individual_token = config.INDIVIDUAL_ACCESS_TOKEN
|
||||
|
||||
self.common_params = {
|
||||
'accessToken': access_token,
|
||||
'individualAccessToken': individual_token,
|
||||
'_filterCompanyId': config.COMPANY_ID,
|
||||
'isV5': 'true',
|
||||
'isV5AndNewMail': 'true',
|
||||
'isNewArchCompany': 'true',
|
||||
'languageForMX5': 'zh-cn',
|
||||
'operating_terminal': 'pc_网页端',
|
||||
'event_source': 'MX',
|
||||
}
|
||||
|
||||
def _headers(self) -> Dict[str, str]:
|
||||
"""构造请求头"""
|
||||
return {
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Content-Type': 'application/json;charset=UTF-8',
|
||||
'Origin': self.BASE_URL,
|
||||
'Referer': f"{self.BASE_URL}/",
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
}
|
||||
|
||||
def _build_url(self, path: str) -> str:
|
||||
"""构建完整 URL"""
|
||||
if path.startswith('http'):
|
||||
return path
|
||||
return f"{self.BASE_URL}{path}"
|
||||
|
||||
def _merge_params(self, extra_params: Optional[Dict] = None) -> Dict:
|
||||
"""合并公共参数和额外参数"""
|
||||
params = self.common_params.copy()
|
||||
params['_@#time'] = int(time.time() * 1000)
|
||||
if extra_params:
|
||||
params.update(extra_params)
|
||||
# 过滤 None 值
|
||||
return {k: v for k, v in params.items() if v is not None}
|
||||
|
||||
def get(self, path: str, params: Optional[Dict] = None) -> Dict:
|
||||
"""GET 请求"""
|
||||
url = self._build_url(path)
|
||||
merged_params = self._merge_params(params)
|
||||
|
||||
response = self.session.get(
|
||||
url,
|
||||
headers=self._headers(),
|
||||
params=merged_params,
|
||||
timeout=30
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def post(self, path: str, data: Optional[Dict] = None) -> Dict:
|
||||
"""POST 请求"""
|
||||
url = self._build_url(path)
|
||||
merged_params = self._merge_params()
|
||||
|
||||
response = self.session.post(
|
||||
url,
|
||||
headers=self._headers(),
|
||||
params=merged_params,
|
||||
json=data,
|
||||
timeout=30
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
# ============ 贸易数据 - 公司搜索 ============
|
||||
|
||||
def search_company(self, keyword: str, page: int = 1, page_size: int = 10) -> Dict:
|
||||
"""搜索公司/企业
|
||||
|
||||
对应端点: POST /pcapi/findServer/standard/company
|
||||
"""
|
||||
data = {
|
||||
"selectedKey": [],
|
||||
"searchType": "all",
|
||||
"activeType": "company",
|
||||
"inputValue": keyword,
|
||||
"keyword": [keyword],
|
||||
"FyStr": keyword,
|
||||
"productFy": [keyword],
|
||||
"currentValue": "input",
|
||||
"language": ["en", "ru", "pt", "spa"],
|
||||
"selectCountry": [],
|
||||
"searchMode": 0,
|
||||
"country": [],
|
||||
"industry": [],
|
||||
"employeesLevel": [],
|
||||
"founded": [],
|
||||
"priceRange": [],
|
||||
"filterCust": 0,
|
||||
"filterView": 0,
|
||||
"excludeGovernment": False,
|
||||
"excludeSchool": False,
|
||||
"excludeForwarder": False,
|
||||
"excludeBank": False,
|
||||
"excludeCn": True,
|
||||
"page": page,
|
||||
"size": page_size
|
||||
}
|
||||
return self.post("/pcapi/findServer/standard/company", data)
|
||||
|
||||
# ============ 贸易数据 - 进口企业 ============
|
||||
|
||||
def import_enterprise(self, company_name: str, country: str = "") -> Dict:
|
||||
"""获取进口企业数据
|
||||
|
||||
对应端点: POST /pcapi/tradeData/api/V2/TradeCompany/ImpEnterprise
|
||||
"""
|
||||
data = {
|
||||
"companyName": company_name,
|
||||
"country": country,
|
||||
"page": 1,
|
||||
"pageSize": 20
|
||||
}
|
||||
return self.post("/pcapi/tradeData/api/V2/TradeCompany/ImpEnterprise", data)
|
||||
|
||||
def import_enterprise_stats(self, company_name: str) -> Dict:
|
||||
"""获取进口企业统计
|
||||
|
||||
对应端点: POST /pcapi/tradeData/api/V1/TradeCompany/ImpEnterpriseStatistics
|
||||
"""
|
||||
data = {"companyName": company_name}
|
||||
return self.post("/pcapi/tradeData/api/V1/TradeCompany/ImpEnterpriseStatistics", data)
|
||||
|
||||
# ============ 贸易数据 - 出口企业 ============
|
||||
|
||||
def export_enterprise(self, company_name: str, country: str = "") -> Dict:
|
||||
"""获取出口企业数据
|
||||
|
||||
对应端点: POST /pcapi/tradeData/api/V2/TradeCompany/ExpEnterprise
|
||||
"""
|
||||
data = {
|
||||
"companyName": company_name,
|
||||
"country": country,
|
||||
"page": 1,
|
||||
"pageSize": 20
|
||||
}
|
||||
return self.post("/pcapi/tradeData/api/V2/TradeCompany/ExpEnterprise", data)
|
||||
|
||||
def export_enterprise_stats(self, company_name: str) -> Dict:
|
||||
"""获取出口企业统计
|
||||
|
||||
对应端点: POST /pcapi/tradeData/api/V1/TradeCompany/ExpEnterpriseStatistics
|
||||
"""
|
||||
data = {"companyName": company_name}
|
||||
return self.post("/pcapi/tradeData/api/V1/TradeCompany/ExpEnterpriseStatistics", data)
|
||||
|
||||
# ============ 贸易数据 - 产品分析 ============
|
||||
|
||||
def company_agg_data(self, company_name: str) -> Dict:
|
||||
"""获取公司聚合数据
|
||||
|
||||
对应端点: POST /pcapi/tradeData/api/V2/TradeProduct/getCompanyAggData
|
||||
"""
|
||||
data = {"companyName": company_name}
|
||||
return self.post("/pcapi/tradeData/api/V2/TradeProduct/getCompanyAggData", data)
|
||||
|
||||
def hs_code_analysis(self, hs_code: str) -> Dict:
|
||||
"""HS 编码分析
|
||||
|
||||
对应端点: POST /pcapi/tradeData/api/V2/TradeProduct/HsCodeAnalysis
|
||||
"""
|
||||
data = {"hsCode": hs_code}
|
||||
return self.post("/pcapi/tradeData/api/V2/TradeProduct/HsCodeAnalysis", data)
|
||||
|
||||
def export_enterprise_analysis(self, company_name: str) -> Dict:
|
||||
"""出口企业分析
|
||||
|
||||
对应端点: POST /pcapi/tradeData/api/V2/TradeProduct/ExpEnterpriseAnalysis
|
||||
"""
|
||||
data = {"companyName": company_name}
|
||||
return self.post("/pcapi/tradeData/api/V2/TradeProduct/ExpEnterpriseAnalysis", data)
|
||||
|
||||
def company_analysis_agg(self, company_name: str) -> Dict:
|
||||
"""公司分析聚合数据
|
||||
|
||||
对应端点: POST /pcapi/tradeData/api/V2/TradeProduct/getCompanyAnalysisAggData
|
||||
"""
|
||||
data = {"companyName": company_name}
|
||||
return self.post("/pcapi/tradeData/api/V2/TradeProduct/getCompanyAnalysisAggData", data)
|
||||
|
||||
def export_compare_lose_analysis(self, company_name: str, compare_company: str) -> Dict:
|
||||
"""出口对比流失分析
|
||||
|
||||
对应端点: POST /pcapi/tradeData//api/V2/TradeProduct/ExpEnterpriseCompareLoseAnalysis
|
||||
"""
|
||||
data = {
|
||||
"companyName": company_name,
|
||||
"compareCompany": compare_company
|
||||
}
|
||||
return self.post("/pcapi/tradeData//api/V2/TradeProduct/ExpEnterpriseCompareLoseAnalysis", data)
|
||||
|
||||
def export_compare_new_analysis(self, company_name: str, compare_company: str) -> Dict:
|
||||
"""出口对比新增分析
|
||||
|
||||
对应端点: POST /pcapi/tradeData//api/V2/TradeProduct/ExpEnterpriseCompareNewAnalysis
|
||||
"""
|
||||
data = {
|
||||
"companyName": company_name,
|
||||
"compareCompany": compare_company
|
||||
}
|
||||
return self.post("/pcapi/tradeData//api/V2/TradeProduct/ExpEnterpriseCompareNewAnalysis", data)
|
||||
|
||||
def export_country_analysis(self, country: str) -> Dict:
|
||||
"""出口国家分析
|
||||
|
||||
对应端点: POST /pcapi/tradeData/api/V2/TradeProduct/ExpCountryAnalysis
|
||||
"""
|
||||
data = {"country": country}
|
||||
return self.post("/pcapi/tradeData/api/V2/TradeProduct/ExpCountryAnalysis", data)
|
||||
|
||||
def country_analysis_data(self, country: str) -> Dict:
|
||||
"""国家分析数据
|
||||
|
||||
对应端点: POST /pcapi/tradeData/api/V2/TradeProduct/getCountryAnalysisData
|
||||
"""
|
||||
data = {"country": country}
|
||||
return self.post("/pcapi/tradeData/api/V2/TradeProduct/getCountryAnalysisData", data)
|
||||
|
||||
# ============ 贸易详情 ============
|
||||
|
||||
def trade_detail_full(self, trade_no: str, source_country: str = "", reg_date: str = "", in_out: str = "") -> Dict:
|
||||
"""获取完整贸易详情
|
||||
|
||||
对应端点: GET /pcapi/tradeData/api/V2/Trade/TradeDetailFull
|
||||
"""
|
||||
return self.get("/pcapi/tradeData/api/V2/Trade/TradeDetailFull", {
|
||||
"tradeNo": trade_no,
|
||||
"sourceCountry": source_country,
|
||||
"regDate": reg_date,
|
||||
"inOut": in_out,
|
||||
"sourceType": "1"
|
||||
})
|
||||
|
||||
# ============ 用户/账户 ============
|
||||
|
||||
def get_trade_source_countries(self) -> Dict:
|
||||
"""获取贸易来源国家列表
|
||||
|
||||
对应端点: GET /pcapi/tradeData/api/V2/Account/GetTradeSourceCountrys
|
||||
"""
|
||||
return self.get("/pcapi/tradeData/api/V2/Account/GetTradeSourceCountrys")
|
||||
|
||||
# ============ 企业详情 API ============
|
||||
|
||||
def get_company_info(self, company_id: str, domain: str) -> Dict:
|
||||
"""获取公司详细信息
|
||||
|
||||
对应端点: GET /pcapi/findServer/find/getMxCompanyInfo
|
||||
包含: 地址、电话、员工数、行业等信息
|
||||
"""
|
||||
params = {
|
||||
"id": company_id,
|
||||
"domain": domain,
|
||||
}
|
||||
return self.get("/pcapi/findServer/find/getMxCompanyInfo", params)
|
||||
|
||||
def get_company_contacts(self, company_id: str, domain: str) -> Dict:
|
||||
"""获取公司联系人列表
|
||||
|
||||
对应端点: GET /pcapi/findServer/find/mxStaffList
|
||||
包含: 姓名、邮箱、职位、LinkedIn、电话等
|
||||
"""
|
||||
params = {
|
||||
"id": company_id,
|
||||
"domain": domain,
|
||||
}
|
||||
return self.get("/pcapi/findServer/find/mxStaffList", params)
|
||||
|
||||
def get_company_phones(self, domain: str) -> Dict:
|
||||
"""获取公司电话/图片信息
|
||||
|
||||
对应端点: GET /pcapi/findServer/find/spiderDomainImage
|
||||
包含: 电话、图片等爬虫抓取的信息
|
||||
"""
|
||||
params = {
|
||||
"domain": domain,
|
||||
}
|
||||
return self.get("/pcapi/findServer/find/spiderDomainImage", params)
|
||||
|
||||
# ============ 其他 ============
|
||||
|
||||
def wap_label_list(self, wid: str = "330204727296") -> Dict:
|
||||
"""获取标签列表
|
||||
|
||||
对应端点: GET /pcapi/v1/waplabellist
|
||||
"""
|
||||
return self.get("/pcapi/v1/waplabellist", {
|
||||
"wid": wid,
|
||||
"format": "mx"
|
||||
})
|
||||
|
||||
def check_sse_exist(self) -> Dict:
|
||||
"""检查 SSE 是否存在
|
||||
|
||||
对应端点: GET /v1/isSseExist
|
||||
"""
|
||||
return self.get("/v1/isSseExist")
|
||||
|
||||
# ============ 通用 API 探测 ============
|
||||
|
||||
def probe_api(self, endpoint: str) -> Dict:
|
||||
"""探测任意 API 端点"""
|
||||
try:
|
||||
return self.get(endpoint)
|
||||
except Exception as e:
|
||||
return {"error": str(e), "endpoint": endpoint}
|
||||
|
||||
def call_api(self, method: str, path: str, data: Optional[Dict] = None, params: Optional[Dict] = None) -> Dict:
|
||||
"""通用 API 调用"""
|
||||
if method.upper() == "GET":
|
||||
return self.get(path, params)
|
||||
elif method.upper() == "POST":
|
||||
return self.post(path, data)
|
||||
else:
|
||||
raise ValueError(f"不支持的 HTTP 方法: {method}")
|
||||
|
||||
def check_auth(self) -> bool:
|
||||
"""检查当前认证是否有效
|
||||
|
||||
Returns:
|
||||
bool: 认证是否有效
|
||||
"""
|
||||
try:
|
||||
result = self.search_company("test", page=1, page_size=1)
|
||||
return result.get("code") == "0"
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def refresh_if_needed(self) -> bool:
|
||||
"""如果需要,刷新认证
|
||||
|
||||
Returns:
|
||||
bool: 刷新后认证是否有效
|
||||
"""
|
||||
if self.check_auth():
|
||||
return True
|
||||
|
||||
if not self.auto_login:
|
||||
return False
|
||||
|
||||
print("⚠ 认证失效,尝试自动刷新...")
|
||||
if self._do_auto_login():
|
||||
self._init_common_params() # 重新初始化参数
|
||||
return self.check_auth()
|
||||
return False
|
||||
@@ -0,0 +1,12 @@
|
||||
"""
|
||||
fumamxCLI 配置文件
|
||||
从抓取的 API 中提取的认证信息
|
||||
"""
|
||||
|
||||
# API 认证令牌 (从浏览器抓取获得)
|
||||
ACCESS_TOKEN = "wxUqXNKY4gacVh0c"
|
||||
INDIVIDUAL_ACCESS_TOKEN = "0p4I8gwiwrwpW906"
|
||||
COMPANY_ID = "1067743"
|
||||
|
||||
# 用户 ID (从 cookie 中获得)
|
||||
USER_EMAIL = "showmin.zhou@163.com"
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
动态 API 调用工具 - 支持自定义端点
|
||||
"""
|
||||
import json
|
||||
import requests
|
||||
from typing import Dict, Optional
|
||||
|
||||
|
||||
class DynamicAPI:
|
||||
"""动态 API 调用器"""
|
||||
|
||||
def __init__(self, cookie_file: str = "/home/ubuntu/fumamx_cookies.txt"):
|
||||
self.cookie_file = cookie_file
|
||||
self.session = requests.Session()
|
||||
self.base_url = "https://www.fumamx.com"
|
||||
self._load_cookies()
|
||||
|
||||
def _load_cookies(self):
|
||||
"""加载 cookies"""
|
||||
with open(self.cookie_file, 'r') as f:
|
||||
cookies = json.load(f)
|
||||
for c in cookies:
|
||||
self.session.cookies.set(c['name'], c['value'])
|
||||
|
||||
def call(self, method: str, path: str, **kwargs) -> Dict:
|
||||
"""调用任意 API"""
|
||||
url = self.base_url + path if not path.startswith('http') else path
|
||||
|
||||
headers = {
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
}
|
||||
|
||||
if 'headers' in kwargs:
|
||||
headers.update(kwargs.pop('headers'))
|
||||
|
||||
try:
|
||||
response = self.session.request(
|
||||
method.upper(),
|
||||
url,
|
||||
headers=headers,
|
||||
timeout=30,
|
||||
**kwargs
|
||||
)
|
||||
return {
|
||||
"status": response.status_code,
|
||||
"url": response.url,
|
||||
"headers": dict(response.headers),
|
||||
"content": response.text[:1000] if not response.headers.get('Content-Type', '').startswith('application/json') else response.json()
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def interactive_mode():
|
||||
"""交互式 API 测试模式"""
|
||||
api = DynamicAPI()
|
||||
|
||||
print("fumamx 动态 API 测试工具")
|
||||
print("示例: GET /api/user/info")
|
||||
print(" POST /api/login data={}")
|
||||
print("输入 'quit' 退出\n")
|
||||
|
||||
while True:
|
||||
try:
|
||||
cmd = input("> ").strip()
|
||||
if cmd.lower() in ['quit', 'exit', 'q']:
|
||||
break
|
||||
|
||||
parts = cmd.split()
|
||||
if len(parts) < 2:
|
||||
print("格式: METHOD /path [data={}]")
|
||||
continue
|
||||
|
||||
method = parts[0].upper()
|
||||
path = parts[1]
|
||||
|
||||
kwargs = {}
|
||||
for part in parts[2:]:
|
||||
if part.startswith('data='):
|
||||
kwargs['json'] = json.loads(part[5:])
|
||||
elif part.startswith('params='):
|
||||
kwargs['params'] = json.loads(part[7:])
|
||||
|
||||
result = api.call(method, path, **kwargs)
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"错误: {e}")
|
||||
|
||||
print("\n再见!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
interactive_mode()
|
||||
Reference in New Issue
Block a user