Fix API errors and improve customer discovery with real web results

- Fix usage/stats 500: use Date() not datetime.date() for SQL cast
- Fix customers 422: raise size limit to 1000
- Replace unreliable MCP client with direct Bing batch search for discovery
- Batch all search queries in one browser session (faster)
- Show real company names/URLs from Bing, not generic templates
- Smart filter for non-business results (news, blogs, forums)
- Fallback suggestions when search results are insufficient
- Frontend: clickable contact URLs, provider indicator, better layout
This commit is contained in:
TradeMate Dev
2026-05-27 10:29:23 +08:00
parent bed5c7abef
commit ab06990e73
7 changed files with 223 additions and 163 deletions
+2 -1
View File
@@ -28,10 +28,11 @@ class NvidiaProvider(OpenAIProvider):
messages.append({"role": "user", "content": message}) messages.append({"role": "user", "content": message})
t1 = time.time() t1 = time.time()
max_tokens = 800 if "JSON" in (system or "").upper() else 300
kwargs = { kwargs = {
"model": self.model, "model": self.model,
"messages": messages, "messages": messages,
"max_tokens": 300, "max_tokens": max_tokens,
"temperature": 0.3, "temperature": 0.3,
} }
resp = await self.client.chat.completions.create(**kwargs) resp = await self.client.chat.completions.create(**kwargs)
+1 -1
View File
@@ -17,7 +17,7 @@ router = APIRouter()
async def list_customers( async def list_customers(
status: Optional[str] = None, status: Optional[str] = None,
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100), size: int = Query(20, ge=1, le=1000),
user_id: str = Depends(get_current_user_id), user_id: str = Depends(get_current_user_id),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
+1
View File
@@ -0,0 +1 @@
_batch_search.js
+129 -105
View File
@@ -1,11 +1,10 @@
import asyncio
import json import json
import logging import logging
from typing import Dict, Any, Optional from typing import Dict, Any, Optional, Union
from app.ai.router import get_ai_router from app.ai.router import get_ai_router
from app.services.search_web import search_companies, fetch_page_text from app.services.search_web import search_companies, fetch_page_text
from app.services.mcp_search_client import mcp_search from app.services.mcp_search_server import search_bing_batch
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -35,27 +34,32 @@ class DiscoveryService:
async def search(self, product_description: str, target_market: str) -> Dict[str, Any]: async def search(self, product_description: str, target_market: str) -> Dict[str, Any]:
queries = self._build_queries(product_description, target_market) queries = self._build_queries(product_description, target_market)
all_results = await self._mcp_search_all(queries)
all_results = await self._web_search_all(queries)
companies = []
provider = "template"
if all_results: if all_results:
raw = all_results.get("results", [])
companies = [self._to_company(r) for r in raw[:12]]
provider = all_results.get("provider", "web_search")
good_enough = [c for c in companies if self._looks_like_business(c)]
if len(good_enough) < 3:
logger.info(f"Web search returned only {len(good_enough)} good results, supplementing with suggestions")
extras = self._suggest_companies(product_description, target_market)
seen_names = set(c.get("name", "") for c in good_enough)
for c in extras:
if c.get("name") and c["name"] not in seen_names:
seen_names.add(c["name"])
good_enough.append(c)
return { return {
"companies": all_results[:15], "companies": good_enough[:15],
"query": product_description, "query": product_description,
"market": target_market, "market": target_market,
"provider": "mcp_search", "provider": provider,
} }
all_results = await self._google_search_all(queries)
if all_results:
return {
"companies": all_results[:15],
"query": product_description,
"market": target_market,
"provider": "web_search",
}
logger.info("No real search results, using AI strategy")
return await self._ai_strategy(product_description, target_market)
async def analyze(self, company_url: str, product_description: str) -> Dict[str, Any]: async def analyze(self, company_url: str, product_description: str) -> Dict[str, Any]:
page_text = await fetch_page_text(company_url) page_text = await fetch_page_text(company_url)
company_info = {"url": company_url} company_info = {"url": company_url}
@@ -117,83 +121,115 @@ URL: {company_url}
logger.warning(f"Outreach AI parse failed: {e}") logger.warning(f"Outreach AI parse failed: {e}")
return self._template_outreach(company_info, product_info) return self._template_outreach(company_info, product_info)
async def _mcp_search_all(self, queries: list) -> list: async def _web_search_all(self, queries: list) -> dict:
seen_urls = set()
tasks = [asyncio.create_task(mcp_search(q, max_results=6)) for q in queries[:2]]
all_results = []
try: try:
for coro in asyncio.as_completed(tasks, timeout=8): results = await search_bing_batch(queries[:4], max_per_query=5)
try: if results:
results = await coro return {"results": self._dedup_and_filter(results)[:15], "provider": "bing"}
for r in results: except Exception as e:
url = r.get("url", "").rstrip("/") logger.warning(f"Bing batch search failed: {e}")
if url and url not in seen_urls:
seen_urls.add(url) results = await search_companies(queries[0], max_results=10)
all_results.append(r) if results:
except (asyncio.TimeoutError, Exception) as e: return {"results": results[:15], "provider": "google_cse"}
logger.debug(f"MCP search query failed: {e}")
except asyncio.TimeoutError: return {}
logger.warning("MCP search overall timeout")
finally:
for t in tasks:
if not t.done():
t.cancel()
await asyncio.gather(*tasks, return_exceptions=True)
if all_results:
return self._dedup_and_filter(all_results)[:15]
return []
def _dedup_and_filter(self, results: list) -> list: def _dedup_and_filter(self, results: list) -> list:
seen = set() seen = set()
filtered = [] filtered = []
junk = ["sciencedirect", "mdpi", "springer", "wiley", "acm.org",
"ieee.org", "researchgate", "nature.com", "oup.com",
"sagepub", "tandfonline", "ncbi", "semanticscholar",
"britannica", "dictionary", "cambridge", "iciba", "wikipedia",
"w3.org", "whatsapp.com", "wechat.com", "qq.com",
"zhihu.com", "sogou.com", "163.com", "sohu.com", "sina.com",
"taobao.com", "tmall.com", "alipay.com", "alibaba.com",
"csdn.net", "blog.csdn", "jianshu.com", "36kr.com",
"huxiu.com", "geekpark.net", "leiphone.com",
"medium.com", "wordpress.com", "blogspot.com",
"youtube.com", "facebook.com", "twitter.com", "instagram.com",
"reddit.com", "quora.com"]
for r in results: for r in results:
url = r.get("url", "").rstrip("/") url = r.get("url", "").rstrip("/")
title = r.get("title", "")
if not url or url in seen: if not url or url in seen:
continue continue
seen.add(url) seen.add(url)
s = url.split("/")[2] if "://" in url else url s = url.split("/")[2] if "://" in url else url
hostname = s.split(":")[0].lower() if ":" in s else s.lower() hostname = s.split(":")[0].lower() if ":" in s else s.lower()
if any(tld in hostname for tld in [".cn", ".com.cn", ".edu", ".ac.", ".gov"]): if any(tld in hostname for tld in [".edu", ".ac.", ".gov", ".edu.cn"]):
continue continue
if any(domain in hostname for domain in if any(domain in hostname for domain in junk):
["sciencedirect", "mdpi", "springer", "wiley", "acm.org",
"ieee.org", "researchgate", "nature.com", "oup.com",
"sagepub", "tandfonline", "ncbi", "semanticscholar",
"britannica", "dictionary", "cambridge", "iciba", "wikipedia"]):
continue continue
filtered.append(r) filtered.append(r)
return filtered return filtered
async def _google_search_all(self, queries: list) -> list: def _to_company(self, r: dict) -> dict:
all_results = [] url = r.get("url", "")
seen_urls = set() title = r.get("title", url)[:60]
for q in queries[:3]: snippet = r.get("snippet", "")[:200]
results = await search_companies(q, max_results=8) return {
for r in results: "name": title,
url = r["url"].rstrip("/") "description": snippet,
if url not in seen_urls: "country": "",
seen_urls.add(url) "match_score": 60,
all_results.append(r) "contact": url[:100] if url else "暂无",
if len(all_results) >= 15: "source": "web",
break }
return self._dedup_and_filter(all_results)[:15]
def _looks_like_business(self, c: dict) -> bool:
name = c.get("name", "")
snippet = c.get("description", "")
junk_words = ["news", "review", "blog", "dictionary", "translate",
"wikipedia", "百科", "词典", "新闻", "评测",
"price", "shop", "buy online", "forum", "subscribe",
"专业媒体", "行业媒体", "新媒体", "门户"]
name_lower = name.lower()
if any(w in name_lower for w in junk_words):
return False
snippet_lower = snippet.lower()
newsy = ["news", "review", "blog post", "article", "dictionary",
"专业媒体", "行业媒体", "新媒体"]
if any(w in snippet_lower for w in newsy):
biz_words = ["company", "inc", "ltd", "corp", "gmbh", "llc",
"manufacturer", "supplier", "exporter",
"wholesale", "distributor", "trading",
"import", "enterprise", "co.", "factory",
"industry", "electric", "solar", "energy",
"automotive", "vehicle", "官网", "有限公司",
"集团", "股份", "实业"]
if not any(w in snippet_lower for w in biz_words):
return False
return True
def _build_queries(self, product: str, market: str) -> list: def _build_queries(self, product: str, market: str) -> list:
return [ import re
has_cjk = bool(re.search(r'[\u4e00-\u9fff]', product))
queries = [
f"{product} importer {market}", f"{product} importer {market}",
f"{product} distributor {market}", f"{product} distributor {market}",
f"{product} wholesale buyer {market}", f"{product} wholesale buyer {market}",
f"{product} procurement {market}", f"{product} procurement {market}",
f"{product} company {market}", f"{product} trading company {market}",
f"buy {product} from {market}", f"buy {product} from {market}",
f"{product} supply chain {market}", f"{product} supply chain {market}",
f"top {product} manufacturers {market}", f"top {product} manufacturers {market}",
f"{product} import export {market}",
f"{product} trading company {market}",
] ]
if has_cjk:
queries += [
f"{product} {market} 进口商",
f"{product} {market} 经销商",
f"{product} {market} 采购",
f"{product} {market} 批发",
]
b2b_queries = [
f"{product} buyer {market} alibaba",
f"{product} supplier {market}",
f"{product} wholesale price {market}",
]
return queries + b2b_queries
def _extract_json(self, text: str) -> Optional[dict]: def _extract_json(self, text: str) -> Optional[Union[dict, list]]:
text = text.strip() text = text.strip()
for prefix in ["```json", "```", "```JSON"]: for prefix in ["```json", "```", "```JSON"]:
if text.startswith(prefix): if text.startswith(prefix):
@@ -215,49 +251,37 @@ URL: {company_url}
pass pass
return None return None
async def _ai_strategy(self, product: str, market: str) -> Dict[str, Any]: def _suggest_companies(self, product: str, market: str) -> list:
if not self._ai_available: return [
return self._template_strategy(product, market) {"name": f"{product} Importers in {market}", "description": f"{market} 从事 {product} 进口和批发的贸易商和专业进口商", "country": market, "match_score": 80, "contact": f"在 LinkedIn 搜索 '{product} importer {market}'", "source": "建议"},
system = """你是外贸客户发现专家。根据用户的产品和目标市场,列出15家有可能采购该产品的潜在公司。 {"name": f"{product} Distributors in {market}", "description": f"{market} 分销 {product} 的分销渠道商和批发商", "country": market, "match_score": 75, "contact": f"在 Google/Bing 搜索 '{product} distributor {market}'", "source": "建议"},
{"name": f"{market} Trade Association", "description": f"联系 {market} 的相关行业协会获取会员企业名录", "country": market, "match_score": 70, "contact": f"搜索 '{market} {product} association'", "source": "建议"},
请以 JSON 格式返回(不要用 markdown 代码块标记): {"name": f"Alibaba {market} Buyers", "description": f"在 Alibaba.com 搜索 '{product}' 并筛选 {market} 买家", "country": market, "match_score": 75, "contact": "https://www.alibaba.com", "source": "建议"},
{ {"name": f"LinkedIn {market} Decision Makers", "description": f"在 LinkedIn 搜索 '{market} {product} procurement/sourcing manager' 找决策人", "country": market, "match_score": 65, "contact": "LinkedIn Premium", "source": "建议"},
"companies": [ {"name": f"{market} Import-Export Records", "description": f"在 importgenius.com 搜索 {product}{market} 的进口记录,找到真实买家", "country": market, "match_score": 70, "contact": "https://www.importgenius.com", "source": "建议"},
{"name": "公司名称", "description": "公司业务简介", "country": "所在国家", "match_score": 匹配度0-100, "contact": "联系方式(有就写,没有写'需进一步查找'", "source": "推荐来源说明"} ]
],
"strategy": "整体获取策略建议",
"tips": ["搜索建议1", "搜索建议2"]
}
要求:
- 公司名称要真实感,不要编造知名大公司
- 公司业务要与产品相关
- 匹配度要有区分度,60-95之间
- 至少返回10家
- 只返回 JSON,不要其他内容"""
prompt = f"产品:{product}\n目标市场:{market}\n请列出在该市场可能采购该产品的公司。"
try:
result = await self.ai.chat(prompt, system_prompt=system)
content = result.get("reply", "")
parsed = self._extract_json(content)
if parsed and "companies" in parsed:
parsed["provider"] = result.get("provider_used", "unknown")
parsed["ai_generated"] = True
return parsed
return self._template_strategy(product, market)
except Exception as e:
logger.warning(f"AI strategy failed: {e}")
return self._template_strategy(product, market)
def _template_strategy(self, product: str, market: str) -> Dict[str, Any]: def _template_strategy(self, product: str, market: str) -> Dict[str, Any]:
search_terms = [
f"{product} importer {market}",
f"{product} distributor {market}",
f"{product} wholesale {market}",
f"{product} buyers {market}",
]
b2b_sites = ["Alibaba.com", "TradeIndia.com", "GlobalSources.com", "Made-in-China.com"]
return { return {
"companies": [ "companies": [
{"name": f"{product} Importers in {market} (示例)", "description": f"{market}从事{product}进口和批发的贸易商,建议在LinkedIn上搜索相关关键词", "country": market, "match_score": 75, "contact": "需进一步查找", "source": "AI推荐"}, {"name": f"{product} Importers in {market}", "description": f"使用Google/Bing搜索 '{product} importer {market}' 可找到正在采购该产品的进口商", "country": market, "match_score": 80, "contact": "通过搜索结果获取", "source": "搜索建议"},
{"name": f"{product} Distributors in {market} (示例)", "description": f"{market}分销{product}的渠道商,建议通过Google搜索关键词", "country": market, "match_score": 70, "contact": "需进一步查找", "source": "AI推荐"}, {"name": f"{product} Distributors in {market}", "description": f"使用Google/Bing搜索 '{product} distributor {market}' 可找到分销渠道商", "country": market, "match_score": 75, "contact": "通过搜索结果获取", "source": "搜索建议"},
{"name": f"{product} Wholesale Buyers in {market}", "description": f"使用Google/Bing搜索 '{product} wholesale {market}' 可找到批发采购商", "country": market, "match_score": 70, "contact": "通过搜索结果获取", "source": "搜索建议"},
{"name": f"{market} {product} Trade Partners", "description": f"在B2B平台({', '.join(b2b_sites[:3])})搜索 {market} 买家发布的采购需求", "country": market, "match_score": 65, "contact": "B2B平台站内信", "source": "B2B平台"},
], ],
"strategy": f"建议在 LinkedIn 和 Google 搜索 {market} {product} 相关公司,使用导入商、批发商、经销商等关键词组合", "strategy": f"推荐搜索计划:\n1. 搜索 '{' '.join(search_terms[:2])}' 直接找客户\n2. 在 LinkedIn 搜索 '{market} {product} manager' 找决策人\n3. 在 {b2b_sites[0]}{b2b_sites[1]} 查找 {market} 买家询盘\n4. 参加 {market} 相关行业展会获取名录",
"tips": ["使用多个搜索词组合", "找到公司后在 LinkedIn 找决策人", "查看公司网站了解其业务范围"], "tips": [
f"把搜索词 '{product} importer {market}' 改成当地语言效果更好",
"找到公司后访问官网,在 About/Team 页面找决策人LinkedIn",
"用 SimilarWeb 查看目标公司网站流量和来源",
"在行业协会网站查找会员名录"],
"provider": "template", "provider": "template",
"ai_generated": True, "ai_generated": True,
} }
+47 -30
View File
@@ -4,6 +4,7 @@ import logging
import os import os
import subprocess import subprocess
from typing import List, Dict from typing import List, Dict
import functools
from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp import FastMCP
@@ -11,22 +12,29 @@ logger = logging.getLogger(__name__)
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
NODE_BIN = "/usr/bin/node" NODE_BIN = "/usr/bin/node"
BING_SCRIPT = r""" BATCH_SCRIPT = r"""
const p = require('puppeteer'); const p = require('puppeteer');
(async () => { (async () => {
const b = await p.launch({headless:true,args:['--no-sandbox','--disable-setuid-sandbox','--disable-blink-features=AutomationControlled']}); const queries = JSON.parse(process.argv[process.argv.length - 2]);
const max = parseInt(process.argv[process.argv.length - 1] || '6', 10);
const sk = ['bing.com','google.com','facebook.com','twitter.com','instagram.com','youtube.com','reddit.com','amazon.com','walmart.com','w3.org','whatsapp.com','wechat.com','qq.com','taobao.com','tmall.com','alipay.com','zhihu.com','baike.baidu.com','sogou.com','163.com','sohu.com','sina.com','iciba.com','cambridge','britannica','sciencedirect','mdpi.com','springer','wiley.com','acm.org','ieee.org','researchgate','semanticscholar','ncbi.nlm.nih','nature.com','oup.com','sagepub','tandfonline','pinterest','ebay','dictionary','translate'];
try {
const b = await p.launch({headless:true,args:['--no-sandbox','--disable-setuid-sandbox','--disable-blink-features=AutomationControlled'],timeout:10000});
const allResults = [];
const seenUrls = new Set();
for (const q of queries) {
try {
const page = await b.newPage(); const page = await b.newPage();
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'); await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
await page.setExtraHTTPHeaders({'Accept-Language':'en-US,en;q=0.9'}); await page.setExtraHTTPHeaders({'Accept-Language':'en-US,en;q=0.9'});
await page.evaluateOnNewDocument(() => { Object.defineProperty(navigator, 'webdriver', {get:()=>undefined}); });
const q = process.argv[process.argv.length - 2]; const url = 'https://www.bing.com/search?q=' + encodeURIComponent(q) + '&setlang=en-US&cc=US';
const max = parseInt(process.argv[process.argv.length - 1] || '10', 10); await page.goto(url, {waitUntil:'domcontentloaded',timeout:8000});
const sk = ['bing.com','google.com','facebook.com','twitter.com','instagram.com','youtube.com','reddit.com','amazon.com','wikipedia.org','baidu.com','linkedin.com','pinterest.com','ebay.com','walmart.com','w3.org','whatsapp.com','wechat.com','qq.com','taobao.com','tmall.com','alibaba.com','alipay.com','dict','dictionary','translate','zhihu.com','baike.baidu.com','sogou.com','163.com','sohu.com','sina.com','iciba.com','cambridge','britannica','sciencedirect','mdpi.com','springer','wiley.com','acm.org','ieee.org','researchgate','semanticscholar','ncbi.nlm.nih','nature.com','oup.com','sagepub','tandfonline']; await page.waitForSelector('.b_algo', {timeout:4000}).catch(()=>{});
try {
await page.goto('https://cn.bing.com/search?q=' + encodeURIComponent(q) + '&setlang=en-US&cc=US', {waitUntil:'domcontentloaded',timeout:10000});
await page.waitForSelector('.b_algo', {timeout:5000}).catch(()=>{});
const results = await page.evaluate((m, sk) => { const results = await page.evaluate((m, sk) => {
const reCJK = /[\u4e00-\u9fff\u3400-\u4dbf]/;
const found = []; const seen = new Set(); const found = []; const seen = new Set();
document.querySelectorAll('li.b_algo').forEach(li => { document.querySelectorAll('li.b_algo').forEach(li => {
const a = li.querySelector('h2 a'); if (!a) return; const a = li.querySelector('h2 a'); if (!a) return;
@@ -35,55 +43,64 @@ const p = require('puppeteer');
seen.add(url); seen.add(url);
if (sk.some(d => url.includes(d))) return; if (sk.some(d => url.includes(d))) return;
const hostname = url.replace(/^https?:\/\//,'').split('/')[0]; const hostname = url.replace(/^https?:\/\//,'').split('/')[0];
if (hostname.endsWith('.cn') || hostname.endsWith('.com.cn') || hostname.endsWith('.edu') || hostname.endsWith('.ac')) return; if (hostname.endsWith('.edu') || hostname.endsWith('.ac') || hostname.endsWith('.gov')) return;
const title = (a.textContent||'').trim().substring(0,100); const title = (a.textContent||'').trim().substring(0,100);
if (reCJK.test(title)) return;
const s = li.querySelector('.b_caption p, .b_lineclamp2'); const s = li.querySelector('.b_caption p, .b_lineclamp2');
found.push({title, url, snippet:s?s.textContent.trim().substring(0,200):''}); found.push({title, url, snippet:s?s.textContent.trim().substring(0,200):''});
}); });
return found.slice(0,m); return found.slice(0,m);
}, max, sk); }, max, sk);
console.log(JSON.stringify(results));
} catch(e) { console.log('[]'); } for (const r of results) {
if (!seenUrls.has(r.url)) {
seenUrls.add(r.url);
allResults.push(r);
}
}
await page.close();
} catch(e) { /* skip failed query */ }
}
console.log(JSON.stringify(allResults.slice(0, max * queries.length)));
await b.close(); await b.close();
} catch(e) { console.log('[]'); }
})(); })();
""" """
BING_SCRIPT_FILE = os.path.join(os.path.dirname(__file__), "_bing_search.js") BATCH_SCRIPT_FILE = os.path.join(os.path.dirname(__file__), "_batch_search.js")
NODE_MODULES = os.path.join(PROJECT_ROOT, "node_modules") NODE_MODULES = os.path.join(PROJECT_ROOT, "node_modules")
async def search_bing(query: str, max_results: int = 10) -> List[Dict[str, str]]: async def search_bing_batch(queries: List[str], max_per_query: int = 6) -> List[Dict[str, str]]:
loop = asyncio.get_running_loop()
try: try:
with open(BING_SCRIPT_FILE, "w") as f: with open(BATCH_SCRIPT_FILE, "w") as f:
f.write(BING_SCRIPT) f.write(BATCH_SCRIPT)
env = os.environ.copy() env = os.environ.copy()
env["NODE_PATH"] = NODE_MODULES env["NODE_PATH"] = NODE_MODULES
result = subprocess.run( fn = functools.partial(
[NODE_BIN, BING_SCRIPT_FILE, query, str(max_results)], subprocess.run,
capture_output=True, [NODE_BIN, BATCH_SCRIPT_FILE, json.dumps(queries), str(max_per_query)],
text=True, capture_output=True, text=True, timeout=120, cwd=PROJECT_ROOT, env=env,
timeout=15,
cwd=PROJECT_ROOT,
env=env,
) )
if result.returncode != 0: result = await loop.run_in_executor(None, fn)
logger.warning(f"Bing search failed: {result.stderr[:300]}")
return []
for line in result.stdout.strip().split("\n"): for line in result.stdout.strip().split("\n"):
line = line.strip() line = line.strip()
if line.startswith("["): if line.startswith("["):
return json.loads(line) return json.loads(line)
return [] return []
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
logger.warning("Bing search timed out") logger.warning("Bing batch search timed out")
return [] return []
except (json.JSONDecodeError, Exception) as e: except (json.JSONDecodeError, Exception) as e:
logger.warning(f"Bing search error: {e}") logger.warning(f"Bing batch search error: {e}")
return [] return []
async def search_bing(query: str, max_results: int = 10) -> List[Dict[str, str]]:
return await search_bing_batch([query], max_per_query=max_results)
mcp = FastMCP("trade-search", log_level="WARNING") mcp = FastMCP("trade-search", log_level="WARNING")
+3 -2
View File
@@ -2,6 +2,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func from sqlalchemy import select, func
from fastapi import HTTPException, Depends from fastapi import HTTPException, Depends
from datetime import datetime, date from datetime import datetime, date
from sqlalchemy import Date
import logging import logging
from app.models import UsageLog, SystemConfig, User, Customer, Product from app.models import UsageLog, SystemConfig, User, Customer, Product
@@ -52,7 +53,7 @@ class UsageService:
stmt = select(func.count()).where( stmt = select(func.count()).where(
UsageLog.user_id == user_id, UsageLog.user_id == user_id,
UsageLog.action == action, UsageLog.action == action,
func.cast(UsageLog.created_at, date) == today, func.cast(UsageLog.created_at, Date) == today,
) )
result = await self.db.execute(stmt) result = await self.db.execute(stmt)
return result.scalar() or 0 return result.scalar() or 0
@@ -64,7 +65,7 @@ class UsageService:
), 0)).where( ), 0)).where(
UsageLog.user_id == user_id, UsageLog.user_id == user_id,
UsageLog.action == "translate", UsageLog.action == "translate",
func.cast(UsageLog.created_at, date) == today, func.cast(UsageLog.created_at, Date) == today,
) )
result = await self.db.execute(stmt) result = await self.db.execute(stmt)
return result.scalar() or 0 return result.scalar() or 0
+21 -5
View File
@@ -11,12 +11,24 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<div v-if="results.length"> <div v-if="results.length">
<el-card v-for="r in results" :key="r.id || r.name" shadow="hover" style="margin-top:12px"> <el-alert v-if="provider === 'template' || provider === '建议'" title="以下为搜索策略建议,可用于手动开发客户" type="warning" show-icon :closable="false" style="margin-bottom:12px" />
<h4>{{ r.name }} <el-tag v-if="r.match_score" size="small" :type="scoreType(r.match_score)">{{ r.match_score }}%</el-tag></h4> <el-alert v-if="provider === 'bing'" title="以下为搜索到的相关公司,点击'访问网站'了解更多,也可添加为客户" type="info" show-icon :closable="false" style="margin-bottom:12px" />
<p v-if="r.description" style="color:#666;font-size:13px">{{ r.description }}</p> <el-card v-for="(r, idx) in results" :key="r.id || r.name || idx" shadow="hover" style="margin-top:12px">
<p v-if="r.contact" style="font-size:12px;color:#999">联系方式{{ r.contact }}</p> <div style="display:flex;justify-content:space-between;align-items:center">
<div style="margin-top:8px"> <h4 style="margin:0">{{ r.name }} <el-tag v-if="r.match_score" size="small" :type="scoreType(r.match_score)">{{ r.match_score }}%</el-tag></h4>
<el-tag v-if="r.source && provider !== 'template'" size="small" type="info">{{ r.source }}</el-tag>
</div>
<p v-if="r.description" style="color:#666;font-size:13px;margin:8px 0">{{ r.description }}</p>
<p v-if="r.contact" style="font-size:12px;color:#999;margin:4px 0">
联系方式
<template v-if="r.contact.startsWith('http')">
<a :href="r.contact" target="_blank" rel="noopener">{{ r.contact.substring(0, 50) }}{{ r.contact.length > 50 ? '…' : '' }}</a>
</template>
<template v-else>{{ r.contact }}</template>
</p>
<div style="margin-top:8px;display:flex;gap:8px">
<el-button size="small" type="primary" @click="addCustomer(r)">添加为客户</el-button> <el-button size="small" type="primary" @click="addCustomer(r)">添加为客户</el-button>
<el-button v-if="r.contact && r.contact.startsWith('http')" size="small" @click="openUrl(r.contact)">访问网站</el-button>
</div> </div>
</el-card> </el-card>
</div> </div>
@@ -55,6 +67,9 @@ const tab = ref('search')
const loading = ref(false) const loading = ref(false)
const searched = ref(false) const searched = ref(false)
const results = ref([]) const results = ref([])
const provider = ref('')
function openUrl(url) { window.open(url, '_blank') }
const form = ref({ product: '', market: '' }) const form = ref({ product: '', market: '' })
const outForm = ref({ company: '', product: '', channel: 'email' }) const outForm = ref({ company: '', product: '', channel: 'email' })
const outLoading = ref(false) const outLoading = ref(false)
@@ -73,6 +88,7 @@ async function search() {
}) })
const d = res.data || res const d = res.data || res
results.value = d.companies || d.items || d.results || d || [] results.value = d.companies || d.items || d.results || d || []
provider.value = d.provider || ''
} catch { ElMessage.error('挖掘失败') } } catch { ElMessage.error('挖掘失败') }
finally { loading.value = false } finally { loading.value = false }
} }