feat: add product strategy, agent skills (3 SKILL.md), and Chrome browser extension

This commit is contained in:
wlt
2026-06-24 10:37:58 +08:00
parent 2ccaafe470
commit 7b03d803b5
19 changed files with 2279 additions and 0 deletions
@@ -0,0 +1,10 @@
{
"sessionID": "ses_130abdc28ffekjJkBUigMX2Aq2",
"updatedAt": "2026-06-22T04:52:32.490Z",
"sources": {
"background-task": {
"state": "idle",
"updatedAt": "2026-06-22T04:52:32.490Z"
}
}
}
+85
View File
@@ -0,0 +1,85 @@
# TradeMate Customer Discovery Skill
Search and discover potential foreign trade customers from the web.
## Description
Automatically searches Google for potential buyers in your target market, extracts company information and contact details (email, phone, WhatsApp), and scores leads by relevance. Helps foreign trade professionals find new customers without manual browsing.
## Triggers
- "find customers" / "找客户"
- "discover leads" / "发现潜在客户"
- "search buyers" / "搜索买家"
- "customer discovery" / "客户发现"
## Workflow
### 1. Search for Potential Customers
```
POST /api/v1/discovery/search
Content-Type: application/json
Authorization: Bearer <token>
{
"keyword": "<product or industry keyword>",
"market": "US|UK|DE|FR|AU|...", // target country code
"max_results": 10
}
```
Response:
```json
{
"results": [
{
"title": "Company Name",
"url": "https://company-website.com",
"description": "...",
"emails": ["info@company.com"],
"phones": ["+1-xxx-xxx-xxxx"],
"social": {
"whatsapp": "...",
"wechat": "..."
},
"relevance_score": 85
}
],
"total": 10
}
```
### 2. Save High-Scoring Leads
High-scoring leads (score >= 70) are automatically saved as Customer records in the TradeMate CRM via the Agent pipeline:
```
POST /api/v1/agent/start
Content-Type: application/json
Authorization: Bearer <token>
{
"product_name": "<your product>",
"product_description": "<product description>",
"target_market": "<target country>"
}
```
## Configuration
Requires a running TradeMate backend instance with:
- Google Custom Search API configured
- Valid API token with discovery credits
```bash
export TRADEMATE_API_URL=http://localhost:8000
export TRADEMATE_API_KEY=<your-api-token>
```
## Notes
- Each search consumes discovery credits (check balance via `GET /api/v1/credits/balance`)
- Free tier: limited searches per day
- Pro tier: expanded daily quota
- Enterprise tier: unlimited searches + auto-save to CRM
+106
View File
@@ -0,0 +1,106 @@
# TradeMate Marketing Content Skill
Generate marketing copy and keyword suggestions for foreign trade products.
## Description
AI-powered marketing content generation for export products. Creates professional marketing copy in multiple languages and styles, generates SEO keywords, and analyzes competitor positioning. Designed for foreign trade professionals who need compelling product descriptions for international buyers.
## Triggers
- "generate marketing" / "生成营销文案"
- "write product description" / "写产品描述"
- "marketing keywords" / "营销关键词"
- "product copy" / "产品文案"
## Workflow
### 1. Generate Marketing Copy
```
POST /api/v1/marketing/generate
Content-Type: application/json
Authorization: Bearer <token>
{
"product_name": "<product name>",
"description": "<product description>",
"category": "<category>",
"target": "<target market, e.g. US importers>",
"style": "professional|friendly|luxury",
"count": 3,
"language": "en|zh"
}
```
Response:
```json
{
"results": [
{
"content": "Professional marketing copy...",
"style": "professional",
"provider": "sensenova"
}
],
"product": "...",
"target": "...",
"count": 3,
"credits_remaining": 45
}
```
### 2. Generate Keywords
```
POST /api/v1/marketing/keywords
Content-Type: application/json
Authorization: Bearer <token>
{
"product_name": "<product name>",
"description": "<product description>",
"category": "<category>",
"language": "en",
"count": 10
}
```
Response:
```json
{
"keywords": ["keyword1", "keyword2", ...],
"product": "...",
"credits_remaining": 45
}
```
### 3. Competitor Analysis
```
POST /api/v1/marketing/competitor-analysis
Content-Type: application/json
Authorization: Bearer <token>
{
"product_name": "<product name>",
"description": "<product description>",
"competitors": ["competitor1", "competitor2"]
}
```
## Configuration
Requires a running TradeMate backend instance:
```bash
export TRADEMATE_API_URL=http://localhost:8000
export TRADEMATE_API_KEY=<your-api-token>
```
## Notes
- Each generation consumes 5 marketing credits
- Free tier: limited daily generations
- Multiple AI providers (Sensenova / NVIDIA) with automatic fallback
- Supports English and Chinese output
+97
View File
@@ -0,0 +1,97 @@
# TradeMate Translate & Reply Skill
Translate foreign trade inquiries and generate professional replies.
## Description
AI-powered translation and smart reply generation for foreign trade professionals. Supports Chinese-English bidirectional translation with trade-specific context, and generates reply suggestions in multiple tones (professional/friendly).
## Triggers
- "translate this" / "翻译"
- "reply to inquiry" / "回复询盘"
- "generate reply" / "生成回复"
- "translate for trade" / "外贸翻译"
## Workflow
### 1. Translate Text
```
POST /api/v1/translate
Content-Type: application/json
Authorization: Bearer <token>
{
"text": "<text to translate>",
"target_lang": "zh|en",
"context": "trade" // optional, adds trade-specific context
}
```
Response:
```json
{
"translated_text": "...",
"source_lang": "en",
"provider_used": "sensenova",
"from_cache": false
}
```
### 2. Generate Reply
```
POST /api/v1/translate/reply
Content-Type: application/json
Authorization: Bearer <token>
{
"inquiry": "<customer inquiry text>",
"tone": "professional|friendly",
"count": 2,
"context": {
"product": "<optional product name>",
"price": "<optional price info>"
}
}
```
Response:
```json
{
"suggestions": [
{"reply": "...", "tone": "professional", "provider": "sensenova"},
{"reply": "...", "tone": "friendly", "provider": "sensenova"}
],
"count": 2
}
```
### 3. Extract Info from Inquiry
```
POST /api/v1/translate/extract
Content-Type: application/json
Authorization: Bearer <token>
{
"text": "<inquiry text>",
"extract_type": "inquiry"
}
```
## Configuration
Requires a running TradeMate backend instance. Set environment variables:
```bash
export TRADEMATE_API_URL=http://localhost:8000
export TRADEMATE_API_KEY=<your-api-token>
```
## Notes
- Free tier: limited daily translations
- Pro tier: unlimited usage
- Uses Sensenova (商汤) as default AI provider, falls back to NVIDIA
+14
View File
@@ -22,6 +22,20 @@ TradeMate 是一个 AI 驱动的外贸业务助手,帮助外贸 SOHO 和小型
--- ---
## ✨ 功能特性 ## ✨ 功能特性
## 🏗️ 产品生态
TradeMate 提供三种使用方式,共享同一后端和账号体系:
| 产品 | 描述 | 入口 |
|------|------|------|
| 🌐 **网页工作台** | 全功能外贸 SaaS — 翻译、CRM、营销、报价、AI Agent | 浏览器访问 `/workspace` |
| 🧩 **Chrome 浏览器插件** | 划词翻译、快捷客户搜索、营销生成 | 见 `browser-extension/` [安装指南](browser-extension/INSTALL.html) |
| 🤖 **AI 技能包** | SKILL.md 技能包,用于 Cursor/Claude Code/OpenCode | `.opencode/skills/` |
> 三者独立运营、互相补充。订阅一个入口,全平台可用。
---
### 🔐 认证系统 ### 🔐 认证系统
- JWT 双 Token 认证(access_token + refresh_token - JWT 双 Token 认证(access_token + refresh_token
+75
View File
@@ -0,0 +1,75 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>TradeMate 浏览器插件安装指南</title>
<style>
body { font-family: -apple-system, 'PingFang SC', sans-serif; max-width: 720px; margin: 40px auto; padding: 0 20px; color: #333; line-height: 1.6; }
h1 { color: #2563eb; }
.step { display: flex; gap: 16px; margin-bottom: 24px; padding: 16px; background: #f8fafc; border-radius: 12px; }
.num { width: 32px; height: 32px; background: #2563eb; color: #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 700; flex-shrink: 0; }
code { background: #e8edf5; padding: 2px 8px; border-radius: 4px; font-size: 13px; color: #2563eb; }
img { max-width: 100%; border: 1px solid #e0e0e0; border-radius: 8px; margin: 8px 0; }
.tip { background: #fef3c7; border-left: 4px solid #f59e0b; padding: 12px 16px; border-radius: 8px; font-size: 14px; }
</style>
</head>
<body>
<h1>TradeMate 浏览器插件</h1>
<p>Chrome 扩展 — 划词翻译、客户发现、营销生成,与网页工作台数据互通</p>
<h2>安装步骤</h2>
<div class="step">
<div class="num">1</div>
<div>
<strong>下载项目</strong>
<p>克隆或下载 <code>trade-assistant</code> 项目,或直接下载 <code>browser-extension/</code> 目录</p>
</div>
</div>
<div class="step">
<div class="num">2</div>
<div>
<strong>打开扩展管理页面</strong>
<p>Chrome 地址栏输入 <code>chrome://extensions/</code> 并回车</p>
</div>
</div>
<div class="step">
<div class="num">3</div>
<div>
<strong>开启开发者模式</strong>
<p>页面右上角"开发者模式"开关 → 打开</p>
</div>
</div>
<div class="step">
<div class="num">4</div>
<div>
<strong>加载扩展</strong>
<p>点击"加载已解压的扩展程序" → 选择项目的 <code>browser-extension/</code> 目录</p>
</div>
</div>
<div class="step">
<div class="num">5</div>
<div>
<strong>配置使用</strong>
<p>点击浏览器工具栏的 TradeMate 图标 → 输入你的 API 地址和登录凭据</p>
</div>
</div>
<div class="tip">
<strong>提示:</strong>登录后自动保存 Token,所有功能使用你的 TradeMate 账号积分。
一个账号在网页端、插件端通用。
</div>
<h2>功能一览</h2>
<ul>
<li><strong>翻译</strong> — 输入翻译 + 选中文本右键翻译</li>
<li><strong>智能回复</strong> — 根据客户询盘生成回复建议</li>
<li><strong>客户发现</strong> — Google 搜索潜在客户</li>
<li><strong>营销文案</strong> — AI 生成产品营销内容</li>
</ul>
</body>
</html>
+64
View File
@@ -0,0 +1,64 @@
# TradeMate 浏览器插件
AI 外贸助手 Chrome 扩展 — 翻译、客户发现、营销文案生成。
## 功能
| 功能 | 说明 |
|------|------|
| 🌐 翻译 | 中英互译 + 多语种,支持划词右键翻译 |
| 💬 智能回复 | 根据客户询盘生成专业/友好回复建议 |
| 🔍 客户发现 | Google 搜索潜在客户,提取联系方式 |
| 📝 营销文案 | 产品描述营销文案生成 |
## 安装
### 开发者模式
1. 打开 Chrome 浏览器,进入 `chrome://extensions/`
2. 开启"开发者模式"
3. 点击"加载已解压的扩展程序",选择 `browser-extension/` 目录
### Chrome Web Store(待上架)
*Coming soon*
## 使用
1. 点击浏览器工具栏的 TradeMate 图标
2. 首次使用需在设置页面输入 API 地址和登录凭据
3. 登录后自动保存 Token,下次直接使用
### 划词翻译
选中网页上的文字 → 右键 → "TradeMate 翻译选中文本"
## 配置要求
需要一个正在运行的 TradeMate 后端实例:
- API 地址:`https://your-domain.com`
- 登录凭据:TradeMate 平台的用户名和密码
## 技术栈
- Chrome Extension Manifest V3
- Vanilla JS (无构建依赖)
- 调用 TradeMate 后端 API
## 目录结构
```
browser-extension/
├── manifest.json # 扩展配置
├── popup/ # 弹出窗口
│ ├── index.html # UI 主界面
│ ├── popup.js # 交互逻辑
│ └── popup.css # 样式
├── background/ # Service Worker
│ └── background.js # 后台处理(右键菜单)
├── content/ # 内容脚本
│ └── content.js # 页面内浮动提示框
├── api/ # API 客户端
│ └── client.js # 后端 API 封装
└── icons/ # 图标
```
+138
View File
@@ -0,0 +1,138 @@
/**
* TradeMate API Client
* Communicates with TradeMate backend from the browser extension.
* All functions are async, return parsed JSON or throw on error.
* Errors from 402/403 carry structured data for upgrade prompts.
*/
const STORAGE_KEYS = {
API_URL: 'trademate_api_url',
TOKEN: 'trademate_token',
};
export class ApiError extends Error {
constructor(message, status, data = {}) {
super(message);
this.name = 'ApiError';
this.status = status;
this.data = data; // parsed response body (may contain credits_remaining)
this.isCreditError = status === 402;
}
}
async function getConfig() {
const { apiUrl, token } = await chrome.storage.local.get([STORAGE_KEYS.API_URL, STORAGE_KEYS.TOKEN]);
if (!apiUrl) throw new ApiError('请先设置 API 地址', 0);
if (!token) throw new ApiError('请先设置 API Token', 0);
return { apiUrl: apiUrl.replace(/\/+$/, ''), token };
}
async function request(path, options = {}) {
const { apiUrl, token } = await getConfig();
const url = `${apiUrl}${path}`;
const res = await fetch(url, {
method: options.method || 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
...options.headers,
},
body: options.body ? JSON.stringify(options.body) : undefined,
});
if (!res.ok) {
let detail = `HTTP ${res.status}`;
let body = {};
try {
body = await res.json();
detail = body.detail || detail;
} catch {}
throw new ApiError(detail, res.status, body);
}
return res.json();
}
/** Login: get JWT token from username + password */
export async function login(username, password, apiUrl) {
const url = `${apiUrl.replace(/\/+$/, '')}/api/v1/auth/login`;
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
if (!res.ok) {
let detail = '登录失败';
try { const e = await res.json(); detail = e.detail || detail; } catch {}
throw new ApiError(detail, res.status);
}
const data = await res.json();
return data.access_token;
}
/** Get current credit balance */
export async function getBalance() {
const data = await request('/api/v1/credits/balance', { method: 'GET' });
return {
balance: data.balance ?? 0,
totalPurchased: data.total_purchased ?? 0,
totalUsed: data.total_used ?? 0,
subscription: data.subscription ?? null,
freeTrialUsed: data.free_trial_used ?? false,
dailyFreeTranslateCharsLeft: data.daily_free_translate_chars_left ?? 0,
rates: data.rates ?? {},
};
}
/** Get available subscription plans */
export async function getSubscriptionPlans() {
return request('/api/v1/credits/subscription-plans', { method: 'GET' });
}
/** Get available credit packages */
export async function getCreditPackages() {
return request('/api/v1/credits/packages', { method: 'GET' });
}
/** Translate text */
export async function translate(text, targetLang = 'zh', sourceLang = 'auto') {
return request('/api/v1/translate', {
body: { text, target_lang: targetLang, source_lang: sourceLang, context: 'trade' },
});
}
/** Generate reply suggestions */
export async function generateReply(inquiry, tone = 'professional', count = 3) {
return request('/api/v1/translate/reply', {
body: { inquiry, tone, count },
});
}
/** Search for potential customers */
export async function searchLeads(productDescription, targetMarket = 'US') {
return request('/api/v1/discovery/search', {
body: { product_description: productDescription, target_market: targetMarket },
});
}
/** Generate marketing copy */
export async function generateMarketing(productName, description, style = 'professional', count = 3) {
return request('/api/v1/marketing/generate', {
body: {
product_name: productName,
description,
style,
count,
target: 'US importers',
language: 'en',
},
});
}
/** Generate keywords */
export async function generateKeywords(productName, description, count = 10) {
return request('/api/v1/marketing/keywords', {
body: { product_name: productName, description, count, language: 'en' },
});
}
@@ -0,0 +1,61 @@
/**
* TradeMate Background Service Worker
* Handles context menus and clipboard operations.
*/
import { translate } from '../api/client.js';
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
id: 'translate-selection',
title: 'TradeMate 翻译选中文本',
contexts: ['selection'],
});
});
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
if (info.menuItemId === 'translate-selection') {
const selectedText = info.selectionText.trim();
if (!selectedText) return;
try {
const result = await translate(selectedText, 'zh');
const translated = result.translated_text || '翻译失败';
// Try to send to content script for display
if (tab?.id) {
chrome.tabs.sendMessage(tab.id, {
action: 'showTranslation',
original: selectedText,
translated,
}).catch(() => {
// Fallback: show in notification
notifyResult(translated);
});
} else {
notifyResult(translated);
}
} catch (err) {
notifyResult(`错误: ${err.message}`);
}
}
});
function notifyResult(text) {
chrome.notifications.create({
type: 'basic',
iconUrl: 'icons/icon48.png',
title: 'TradeMate 翻译',
message: text.slice(0, 200),
});
}
// Listen for popup API calls that need background processing
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'translateContext') {
translate(request.text, request.targetLang)
.then(result => sendResponse({ ok: true, data: result }))
.catch(err => sendResponse({ ok: false, error: err.message }));
return true; // Keep channel open for async response
}
});
+72
View File
@@ -0,0 +1,72 @@
/**
* TradeMate Content Script
* Displays translation results on the page via a floating tooltip.
*/
chrome.runtime.onMessage.addListener((request) => {
if (request.action === 'showTranslation') {
showTooltip(request.original, request.translated);
}
});
function showTooltip(original, translated) {
const existing = document.getElementById('trademate-tooltip');
if (existing) existing.remove();
const tooltip = document.createElement('div');
tooltip.id = 'trademate-tooltip';
tooltip.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 999999;
max-width: 400px;
padding: 16px;
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 14px;
line-height: 1.6;
color: #333;
animation: slideIn 0.3s ease;
`;
tooltip.innerHTML = `
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<strong style="color:#2563eb;">TradeMate 翻译</strong>
<button id="trademate-close" style="
background:none;border:none;font-size:18px;cursor:pointer;color:#999;padding:0 4px;
">&times;</button>
</div>
<div style="margin-bottom:8px;color:#666;font-size:13px;">${escapeHtml(original)}</div>
<div style="color:#333;font-size:15px;font-weight:500;">${escapeHtml(translated)}</div>
`;
document.body.appendChild(tooltip);
document.getElementById('trademate-close').onclick = () => tooltip.remove();
// Auto-remove after 15 seconds
setTimeout(() => { if (tooltip.parentNode) tooltip.remove(); }, 15000);
// Add animation style
if (!document.getElementById('trademate-style')) {
const style = document.createElement('style');
style.id = 'trademate-style';
style.textContent = `
@keyframes slideIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
`;
document.head.appendChild(style);
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 360 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B

+39
View File
@@ -0,0 +1,39 @@
{
"manifest_version": 3,
"name": "TradeMate - AI 外贸助手",
"version": "1.0.0",
"description": "AI-powered foreign trade assistant: translate inquiries, discover customers, generate marketing content",
"permissions": [
"storage",
"contextMenus",
"activeTab"
],
"host_permissions": [
"<all_urls>"
],
"action": {
"default_popup": "popup/index.html",
"default_title": "TradeMate",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"background": {
"service_worker": "background/background.js",
"type": "module"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content/content.js"],
"run_at": "document_end"
}
],
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
}
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
# Package TradeMate browser extension for Chrome Web Store or manual install
# Usage: bash package.sh
set -e
DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$DIR"
OUTPUT="trademate-extension.zip"
# Clean previous
rm -f "$OUTPUT"
# Zip required files
zip -r "$OUTPUT" \
manifest.json \
popup/index.html popup/popup.js popup/popup.css \
background/background.js \
content/content.js \
api/client.js \
icons/icon16.png icons/icon48.png icons/icon128.png \
-x "*.DS_Store" -x "*/.git/*"
echo "✅ Packaged: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))"
echo ""
echo "To install:"
echo " 1. Open chrome://extensions/"
echo " 2. Enable Developer mode"
echo " 3. Drag $OUTPUT onto the page"
echo " OR: Load unpacked → select browser-extension/ directory"
+194
View File
@@ -0,0 +1,194 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TradeMate</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div id="app">
<!-- Header -->
<div class="header">
<h1 class="logo">TradeMate</h1>
<span class="badge">外贸助手</span>
</div>
<!-- Settings Screen -->
<div id="screen-settings" class="screen">
<h2>设置</h2>
<div class="form-group">
<label>API 地址</label>
<input type="text" id="settings-url" placeholder="https://your-domain.com" />
</div>
<div class="form-group">
<label>用户名</label>
<input type="text" id="settings-username" placeholder="登录邮箱/用户名" />
</div>
<div class="form-group">
<label>密码</label>
<input type="password" id="settings-password" placeholder="登录密码" />
</div>
<button id="settings-login" class="btn btn-primary">登录并保存</button>
<p id="settings-status" class="status"></p>
<p class="hint">登录后将自动获取 Token 并保存在本地</p>
</div>
<!-- Main Screen -->
<div id="screen-main" class="screen" style="display:none;">
<!-- Tab Navigation -->
<div class="tabs">
<button class="tab active" data-tab="translate">翻译</button>
<button class="tab" data-tab="reply">回复</button>
<button class="tab" data-tab="discovery">发现</button>
<button class="tab" data-tab="marketing">营销</button>
</div>
<!-- Tab: Translate -->
<div id="tab-translate" class="tab-content active">
<textarea id="translate-input" rows="4" placeholder="输入要翻译的文字..."></textarea>
<div class="row">
<select id="translate-target">
<option value="zh">中文</option>
<option value="en">英文</option>
<option value="ja">日语</option>
<option value="ko">韩语</option>
<option value="fr">法语</option>
<option value="de">德语</option>
<option value="es">西班牙语</option>
</select>
<button id="translate-btn" class="btn btn-primary">翻译</button>
</div>
<div id="translate-result" class="result-box"></div>
</div>
<!-- Tab: Reply -->
<div id="tab-reply" class="tab-content">
<textarea id="reply-input" rows="4" placeholder="粘贴客户询盘内容..."></textarea>
<div class="row">
<select id="reply-tone">
<option value="professional">专业</option>
<option value="friendly">友好</option>
</select>
<button id="reply-btn" class="btn btn-primary">生成回复</button>
</div>
<div id="reply-result" class="result-box"></div>
</div>
<!-- Tab: Discovery -->
<div id="tab-discovery" class="tab-content">
<input type="text" id="discovery-product" placeholder="产品名称/描述" />
<div class="row">
<input type="text" id="discovery-market" placeholder="目标市场 (US)" value="US" />
<button id="discovery-btn" class="btn btn-primary">搜索</button>
</div>
<div id="discovery-result" class="result-box"></div>
</div>
<!-- Tab: Marketing -->
<div id="tab-marketing" class="tab-content">
<input type="text" id="marketing-name" placeholder="产品名称" />
<textarea id="marketing-desc" rows="3" placeholder="产品描述..."></textarea>
<div class="row">
<select id="marketing-style">
<option value="professional">专业</option>
<option value="friendly">友好</option>
<option value="luxury">高端</option>
</select>
<button id="marketing-btn" class="btn btn-primary">生成文案</button>
</div>
<div id="marketing-result" class="result-box"></div>
</div>
</div>
<!-- Footer -->
<div class="footer">
<button id="btn-settings" class="btn-link">设置</button>
<span id="credits-display" class="credits"></span>
</div>
<!-- Upgrade Banner (shown on 402 or low credits) -->
<div id="upgrade-banner" class="upgrade-banner" style="display:none;">
<span id="upgrade-banner-text" class="upgrade-banner-text">次数不足</span>
<button id="upgrade-banner-btn" class="upgrade-btn">升级</button>
<button id="upgrade-banner-close" class="upgrade-close">&times;</button>
</div>
<!-- Upgrade Modal -->
<div id="upgrade-modal-overlay" class="modal-overlay" style="display:none;">
<div class="modal">
<div class="modal-header">
<h3>升级套餐</h3>
<button id="modal-close" class="modal-close-btn">&times;</button>
</div>
<div class="modal-body">
<div class="plan-card plan-starter">
<div class="plan-name">Starter</div>
<div class="plan-price">¥9.9<small>/月</small></div>
<ul class="plan-features">
<li>200 积分/月</li>
<li>无每日限制</li>
<li>翻译 + 客户发现 + 营销</li>
</ul>
<button class="btn btn-primary plan-btn" data-plan="starter">订阅</button>
</div>
<div class="plan-card plan-pro featured">
<div class="plan-badge">推荐</div>
<div class="plan-name">Professional</div>
<div class="plan-price">¥49<small>/月</small></div>
<ul class="plan-features">
<li>1000 积分/月</li>
<li>AI 数字员工</li>
<li>团队协作 (3人)</li>
</ul>
<button class="btn btn-primary plan-btn" data-plan="pro">订阅</button>
</div>
<div class="plan-card plan-enterprise">
<div class="plan-name">Enterprise</div>
<div class="plan-price">¥99<small>/月</small></div>
<ul class="plan-features">
<li>2500 积分/月</li>
<li>不限团队</li>
<li>API 调用 + SLA</li>
</ul>
<button class="btn btn-primary plan-btn" data-plan="enterprise">订阅</button>
</div>
</div>
<div class="modal-footer">
<span class="hint">订阅后所有产品线通用(网页/插件/Skills)</span>
</div>
</div>
</div>
<!-- Credit Package Modal (低门槛) -->
<div id="package-modal-overlay" class="modal-overlay" style="display:none;">
<div class="modal modal-sm">
<div class="modal-header">
<h3>购买积分包</h3>
<button id="pkg-modal-close" class="modal-close-btn">&times;</button>
</div>
<div class="modal-body">
<div class="plan-card plan-starter">
<div class="plan-name">50 积分</div>
<div class="plan-price">¥2.9</div>
<button class="btn btn-primary plan-btn" data-pkg="50">购买</button>
</div>
<div class="plan-card plan-pro featured">
<div class="plan-badge">超值</div>
<div class="plan-name">200 积分</div>
<div class="plan-price">¥9.9</div>
<button class="btn btn-primary plan-btn" data-pkg="200">购买</button>
</div>
<div class="plan-card plan-enterprise">
<div class="plan-name">600 积分</div>
<div class="plan-price">¥24.9</div>
<button class="btn btn-primary plan-btn" data-pkg="600">购买</button>
</div>
</div>
</div>
</div>
</div>
<script src="popup.js" type="module"></script>
</body>
</html>
+504
View File
@@ -0,0 +1,504 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 400px;
min-height: 480px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-size: 14px;
color: #333;
background: #f8fafc;
}
#app {
display: flex;
flex-direction: column;
height: 100%;
min-height: 480px;
}
/* Header */
.header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: linear-gradient(135deg, #2563eb, #1d4ed8);
color: #fff;
}
.logo {
font-size: 18px;
font-weight: 700;
letter-spacing: 0.5px;
}
.badge {
font-size: 11px;
background: rgba(255,255,255,0.2);
padding: 2px 8px;
border-radius: 10px;
}
/* Screens */
.screen {
flex: 1;
padding: 16px;
overflow-y: auto;
}
/* Tabs */
.tabs {
display: flex;
gap: 4px;
margin-bottom: 12px;
background: #e8edf5;
border-radius: 8px;
padding: 4px;
}
.tab {
flex: 1;
padding: 8px 4px;
border: none;
background: transparent;
border-radius: 6px;
font-size: 13px;
cursor: pointer;
color: #64748b;
transition: all 0.2s;
font-weight: 500;
}
.tab:hover {
color: #2563eb;
background: rgba(37, 99, 235, 0.08);
}
.tab.active {
background: #fff;
color: #2563eb;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* Form Elements */
input, textarea, select {
width: 100%;
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 13px;
font-family: inherit;
outline: none;
transition: border-color 0.2s;
background: #fff;
}
input:focus, textarea:focus, select:focus {
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37,99,235,0.1);
}
textarea {
resize: vertical;
min-height: 60px;
margin-bottom: 8px;
}
input {
margin-bottom: 8px;
}
select {
margin-bottom: 0;
cursor: pointer;
}
.row {
display: flex;
gap: 8px;
align-items: flex-start;
}
.row select {
flex: 1;
}
.row input {
flex: 1;
margin-bottom: 0;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.btn-primary {
background: #2563eb;
color: #fff;
}
.btn-primary:hover {
background: #1d4ed8;
}
.btn-primary:active {
transform: scale(0.97);
}
.btn-link {
background: none;
border: none;
color: #64748b;
font-size: 13px;
cursor: pointer;
padding: 4px 8px;
}
.btn-link:hover {
color: #2563eb;
}
/* Result Box */
.result-box {
margin-top: 8px;
padding: 12px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
min-height: 40px;
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
.result-box:empty {
display: none;
}
.result-box .loading {
color: #64748b;
display: flex;
align-items: center;
gap: 8px;
}
.result-box .error {
color: #dc2626;
}
.result-box .item {
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.result-box .item:last-child {
border-bottom: none;
}
.result-box .item-label {
font-weight: 600;
color: #2563eb;
margin-bottom: 4px;
}
.spinner {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid #e0e0e0;
border-top-color: #2563eb;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Settings */
.form-group {
margin-bottom: 12px;
}
.form-group label {
display: block;
font-size: 12px;
font-weight: 600;
color: #64748b;
margin-bottom: 4px;
}
.status {
margin-top: 8px;
font-size: 13px;
}
.status.success { color: #16a34a; }
.status.error { color: #dc2626; }
.hint {
font-size: 12px;
color: #94a3b8;
margin-top: 12px;
line-height: 1.5;
}
/* Footer */
.footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
border-top: 1px solid #e5e7eb;
background: #fff;
}
.credits {
font-size: 12px;
color: #94a3b8;
}
/* Inline Copy Button */
.copy-btn {
float: right;
padding: 2px 8px;
font-size: 11px;
border: 1px solid #d1d5db;
border-radius: 4px;
background: #fff;
cursor: pointer;
color: #64748b;
}
.copy-btn:hover {
background: #f0f4ff;
color: #2563eb;
}
/* ─── Upgrade Banner ─────────────────────────────── */
.upgrade-banner {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background: linear-gradient(135deg, #fef3c7, #fde68a);
border-top: 1px solid #f59e0b;
font-size: 13px;
animation: slideUp 0.3s ease;
}
.upgrade-banner-text {
flex: 1;
color: #92400e;
font-weight: 500;
}
.upgrade-btn {
padding: 6px 16px;
border: none;
border-radius: 6px;
background: #f59e0b;
color: #fff;
font-size: 13px;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
transition: background 0.2s;
}
.upgrade-btn:hover {
background: #d97706;
}
.upgrade-close {
background: none;
border: none;
font-size: 18px;
color: #92400e;
cursor: pointer;
padding: 0 4px;
opacity: 0.6;
}
.upgrade-close:hover {
opacity: 1;
}
@keyframes slideUp {
from { transform: translateY(100%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
/* ─── Credit Error ───────────────────────────────── */
.credit-error {
padding: 12px;
background: #fffbeb;
border: 1px solid #fde68a;
border-radius: 8px;
text-align: center;
font-weight: 500;
}
.credit-error-hint {
display: block;
margin-top: 4px;
font-size: 12px;
font-weight: 400;
color: #92400e;
}
/* ─── Modal ───────────────────────────────────────── */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: fadeIn 0.2s ease;
}
.modal {
background: #fff;
border-radius: 16px;
width: 360px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 16px 48px rgba(0,0,0,0.2);
animation: scaleIn 0.25s ease;
}
.modal-sm {
width: 300px;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px 0;
}
.modal-header h3 {
font-size: 16px;
color: #1e293b;
}
.modal-close-btn {
background: none;
border: none;
font-size: 22px;
color: #94a3b8;
cursor: pointer;
padding: 0 4px;
}
.modal-close-btn:hover {
color: #64748b;
}
.modal-body {
padding: 16px 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
.modal-footer {
padding: 8px 20px 16px;
text-align: center;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes scaleIn {
from { transform: scale(0.9); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
/* ─── Plan Cards ──────────────────────────────────── */
.plan-card {
padding: 14px 16px;
border: 1px solid #e5e7eb;
border-radius: 12px;
text-align: center;
position: relative;
transition: border-color 0.2s, box-shadow 0.2s;
}
.plan-card:hover {
border-color: #2563eb;
box-shadow: 0 2px 8px rgba(37,99,235,0.1);
}
.plan-card.featured {
border-color: #2563eb;
background: #f8faff;
}
.plan-badge {
position: absolute;
top: -10px;
right: 16px;
background: #2563eb;
color: #fff;
font-size: 11px;
padding: 2px 10px;
border-radius: 10px;
font-weight: 600;
}
.plan-name {
font-size: 14px;
font-weight: 700;
color: #1e293b;
margin-bottom: 4px;
}
.plan-price {
font-size: 24px;
font-weight: 800;
color: #2563eb;
margin-bottom: 8px;
}
.plan-price small {
font-size: 13px;
font-weight: 400;
color: #64748b;
}
.plan-features {
list-style: none;
font-size: 12px;
color: #64748b;
line-height: 1.8;
margin-bottom: 10px;
}
.plan-btn {
width: 100%;
}
+471
View File
@@ -0,0 +1,471 @@
/**
* TradeMate Popup - Main UI Logic
*/
import {
login,
translate,
generateReply,
searchLeads,
generateMarketing,
getBalance,
getSubscriptionPlans,
getCreditPackages,
ApiError,
} from '../api/client.js';
// ─── State ───────────────────────────────────────────
let creditsState = {
balance: 0,
dailyFreeCharsLeft: 0,
planName: 'Free',
};
// ─── DOM refs ────────────────────────────────────────
const $ = (id) => document.getElementById(id);
const screenSettings = $('screen-settings');
const screenMain = $('screen-main');
const settingsUrl = $('settings-url');
const settingsUser = $('settings-username');
const settingsPass = $('settings-password');
const settingsLogin = $('settings-login');
const settingsStatus = $('settings-status');
const translateInput = $('translate-input');
const translateTarget = $('translate-target');
const translateBtn = $('translate-btn');
const translateResult = $('translate-result');
const replyInput = $('reply-input');
const replyTone = $('reply-tone');
const replyBtn = $('reply-btn');
const replyResult = $('reply-result');
const discoveryProduct = $('discovery-product');
const discoveryMarket = $('discovery-market');
const discoveryBtn = $('discovery-btn');
const discoveryResult = $('discovery-result');
const marketingName = $('marketing-name');
const marketingDesc = $('marketing-desc');
const marketingStyle = $('marketing-style');
const marketingBtn = $('marketing-btn');
const marketingResult = $('marketing-result');
const creditsDisplay = $('credits-display');
const btnSettings = $('btn-settings');
const upgradeBanner = $('upgrade-banner');
const upgradeBannerText = $('upgrade-banner-text');
const upgradeBannerBtn = $('upgrade-banner-btn');
const upgradeBannerClose = $('upgrade-banner-close');
const upgradeModalOverlay = $('upgrade-modal-overlay');
const modalClose = $('modal-close');
const packageModalOverlay = $('package-modal-overlay');
const pkgModalClose = $('pkg-modal-close');
// ─── CONSTANTS ───────────────────────────────────────
const CONSUMPTION_LABELS = {
lead_search: { name: '客户搜索', cost: 10 },
marketing_content: { name: '营销生成', cost: 5 },
reply_suggest: { name: '智能回复', cost: 2 },
translate_per_1000chars: { name: '翻译', cost: 1 },
quotation: { name: '报价单', cost: 2 },
followup_scan: { name: '跟进扫描', cost: 2 },
};
// ─── Init ────────────────────────────────────────────
async function init() {
const { apiUrl, token } = await chrome.storage.local.get(['trademate_api_url', 'trademate_token']);
if (apiUrl) settingsUrl.value = apiUrl;
if (apiUrl && token) {
showScreen('main');
await refreshCredits();
} else {
showScreen('settings');
}
setupTabs();
setupActions();
setupUpgradeUI();
}
function showScreen(screen) {
screenSettings.style.display = screen === 'settings' ? 'block' : 'none';
screenMain.style.display = screen === 'main' ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', init);
// ─── Credits ─────────────────────────────────────────
async function refreshCredits() {
try {
const bal = await getBalance();
creditsState.balance = bal.balance;
creditsState.dailyFreeCharsLeft = bal.dailyFreeTranslateCharsLeft;
// Determine plan name from subscription data
if (bal.subscription?.plan_id) {
creditsState.planName = 'Starter';
// Could fetch plan name from subscription-plans endpoint for accuracy
} else {
creditsState.planName = 'Free';
}
updateCreditsDisplay();
hideUpgradeBanner(); // clear any stale credit error
return bal;
} catch (err) {
console.warn('Failed to fetch credits:', err.message);
updateCreditsDisplay(); // show fallback
return null;
}
}
function updateCreditsDisplay() {
const { balance, planName, dailyFreeCharsLeft } = creditsState;
let parts = [];
if (planName !== 'Free') {
parts.push(`${planName}`);
}
if (balance > 0) {
parts.push(`${balance} 积分`);
}
parts.push(`剩余`);
creditsDisplay.textContent = parts.join(' · ');
// Make credits clickable to upgrade
creditsDisplay.style.cursor = 'pointer';
creditsDisplay.title = '点击查看套餐';
creditsDisplay.onclick = () => showUpgradeModal();
}
function showCreditErrorBanner(detail, actionLabel = '升级') {
upgradeBannerText.textContent = detail || '次数不足';
upgradeBannerBtn.textContent = actionLabel;
upgradeBanner.style.display = 'flex';
}
function hideUpgradeBanner() {
upgradeBanner.style.display = 'none';
}
// ─── Low Credit Warning ──────────────────────────────
function checkLowCredits(consumed) {
const cost = consumed || 0;
const remaining = creditsState.balance - cost;
if (remaining <= 5 && creditsState.planName === 'Free') {
// Almost out - show subtle hint in footer
creditsDisplay.textContent = `⚠ 仅剩 ${Math.max(0, remaining)} 积分 · 升级 ↑`;
creditsDisplay.style.cursor = 'pointer';
creditsDisplay.onclick = () => showLowCreditsPrompt();
}
}
function showLowCreditsPrompt() {
showCreditErrorBanner('体验积分即将用完,升级后继续使用', '查看套餐');
}
// ─── Upgrade Banner ──────────────────────────────────
upgradeBannerBtn.addEventListener('click', () => {
showUpgradeModal();
});
upgradeBannerClose.addEventListener('click', () => {
hideUpgradeBanner();
});
// ─── Upgrade Modal ───────────────────────────────────
function showUpgradeModal() {
upgradeModalOverlay.style.display = 'flex';
}
function hideUpgradeModal() {
upgradeModalOverlay.style.display = 'none';
}
modalClose.addEventListener('click', hideUpgradeModal);
upgradeModalOverlay.addEventListener('click', (e) => {
if (e.target === upgradeModalOverlay) hideUpgradeModal();
});
// ─── Package Modal (low-commitment) ──────────────────
function showPackageModal() {
packageModalOverlay.style.display = 'flex';
}
function hidePackageModal() {
packageModalOverlay.style.display = 'none';
}
pkgModalClose.addEventListener('click', hidePackageModal);
packageModalOverlay.addEventListener('click', (e) => {
if (e.target === packageModalOverlay) hidePackageModal();
});
// ─── Plan/Pkg button handlers ────────────────────────
function setupUpgradeUI() {
document.querySelectorAll('.plan-btn[data-plan]').forEach(btn => {
btn.addEventListener('click', () => {
const plan = btn.dataset.plan;
// Open backend payment page or settings page
// For now, open the workspace credits page
chrome.tabs.create({
url: `${settingsUrl.value?.replace(/\/+$/, '') || 'https://trade.yuzhiran.com'}/workspace/credits`,
});
hideUpgradeModal();
});
});
document.querySelectorAll('.plan-btn[data-pkg]').forEach(btn => {
btn.addEventListener('click', () => {
const pkg = btn.dataset.pkg;
chrome.tabs.create({
url: `${settingsUrl.value?.replace(/\/+$/, '') || 'https://trade.yuzhiran.com'}/workspace/credits`,
});
hidePackageModal();
});
});
// Package link in upgrade modal footer
const pkgLink = document.createElement('a');
pkgLink.href = '#';
pkgLink.textContent = '购买积分包(低至 ¥2.9';
pkgLink.style.cssText = 'font-size:12px;color:#2563eb;cursor:pointer;';
pkgLink.onclick = (e) => {
e.preventDefault();
hideUpgradeModal();
showPackageModal();
};
document.querySelector('.modal-footer .hint')?.after(pkgLink);
}
// ─── Loading / Result helpers ────────────────────────
function setLoading(btn, loading) {
btn.disabled = loading;
btn.textContent = loading ? '处理中...' : btn.dataset.originalText || btn.textContent;
if (!btn.dataset.originalText) btn.dataset.originalText = btn.textContent;
}
function showResult(el, html) {
el.innerHTML = html;
}
function showError(el, msg) {
el.innerHTML = `<div class="error">${escapeHtml(msg)}</div>`;
}
function handleApiError(el, err) {
if (err instanceof ApiError && err.isCreditError) {
// 402 - out of credits: show result + upgrade banner
showResult(el, `<div class="error credit-error">
⚡ 积分不足<br>
<span class="credit-error-hint">${escapeHtml(err.message)}</span>
</div>`);
showCreditErrorBanner('积分不足,升级后继续使用');
} else {
showError(el, err.message);
}
}
function escapeHtml(str) {
const d = document.createElement('div');
d.textContent = str;
return d.innerHTML;
}
// ─── Tabs ────────────────────────────────────────────
function setupTabs() {
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
tab.classList.add('active');
const target = document.getElementById(`tab-${tab.dataset.tab}`);
if (target) target.classList.add('active');
});
});
}
// ─── Settings ────────────────────────────────────────
settingsLogin.addEventListener('click', async () => {
const url = settingsUrl.value.trim();
const username = settingsUser.value.trim();
const password = settingsPass.value.trim();
if (!url) { settingsStatus.textContent = '请输入 API 地址'; settingsStatus.className = 'status error'; return; }
if (!username || !password) { settingsStatus.textContent = '请输入用户名和密码'; settingsStatus.className = 'status error'; return; }
setLoading(settingsLogin, true);
settingsStatus.textContent = '';
try {
const token = await login(username, password, url);
await chrome.storage.local.set({
trademate_api_url: url,
trademate_token: token,
});
settingsStatus.textContent = '登录成功!';
settingsStatus.className = 'status success';
setTimeout(() => {
showScreen('main');
refreshCredits();
}, 800);
} catch (err) {
settingsStatus.textContent = err.message;
settingsStatus.className = 'status error';
} finally {
setLoading(settingsLogin, false);
}
});
btnSettings.addEventListener('click', () => {
settingsPass.value = '';
showScreen('settings');
});
// ─── Action Handlers ─────────────────────────────────
function setupActions() {
// Translate
translateBtn.addEventListener('click', async () => {
const text = translateInput.value.trim();
if (!text) return;
setLoading(translateBtn, true);
showResult(translateResult, '<div class="loading"><span class="spinner"></span>翻译中...</div>');
try {
const result = await translate(text, translateTarget.value);
const translated = result.translated_text || '(无结果)';
showResult(translateResult, `
<button class="copy-btn" onclick="navigator.clipboard.writeText(this.parentElement.querySelector('.t-text').textContent)">复制</button>
<div class="t-text">${escapeHtml(translated)}</div>
${result.provider_used ? `<div style="color:#94a3b8;font-size:11px;margin-top:4px;">provider: ${result.provider_used}</div>` : ''}
`);
// Refresh credits (translation may consume daily free or credits)
refreshCredits();
} catch (err) {
handleApiError(translateResult, err);
} finally {
setLoading(translateBtn, false);
}
});
// Reply
replyBtn.addEventListener('click', async () => {
const inquiry = replyInput.value.trim();
if (!inquiry) return;
setLoading(replyBtn, true);
showResult(replyResult, '<div class="loading"><span class="spinner"></span>生成中...</div>');
try {
const result = await generateReply(inquiry, replyTone.value);
const suggestions = result.suggestions || result.results || [];
let html = suggestions.map((s, i) => `
<div class="item">
<div class="item-label">回复 ${i + 1}${s.tone ? ` (${s.tone})` : ''}</div>
<button class="copy-btn" onclick="navigator.clipboard.writeText(this.parentElement.querySelector('.r-text').textContent)">复制</button>
<div class="r-text">${escapeHtml(s.reply || s.content || s)}</div>
</div>
`).join('');
if (!html) html = '(无回复建议)';
showResult(replyResult, html);
refreshCredits();
} catch (err) {
handleApiError(replyResult, err);
} finally {
setLoading(replyBtn, false);
}
});
// Discovery
discoveryBtn.addEventListener('click', async () => {
const product = discoveryProduct.value.trim();
if (!product) return;
setLoading(discoveryBtn, true);
showResult(discoveryResult, '<div class="loading"><span class="spinner"></span>搜索中...</div>');
try {
const result = await searchLeads(product, discoveryMarket.value.trim() || 'US');
const leads = result.data || result.results || [];
// Response may include credits_remaining
if (result.credits_remaining !== undefined) {
creditsState.balance = result.credits_remaining;
updateCreditsDisplay();
checkLowCredits(0);
}
if (!Array.isArray(leads)) {
showResult(discoveryResult, escapeHtml(JSON.stringify(leads, null, 2)));
return;
}
let html = leads.map((l, i) => `
<div class="item">
<div class="item-label">${escapeHtml(l.title || l.company_name || `结果 ${i+1}`)}</div>
<div>${escapeHtml(l.description || l.snippet || '')}</div>
${l.url ? `<div><a href="${escapeHtml(l.url)}" target="_blank" style="color:#2563eb;font-size:12px;">${escapeHtml(l.url)}</a></div>` : ''}
${l.emails?.length ? `<div style="font-size:12px;color:#64748b;">📧 ${l.emails.join(', ')}</div>` : ''}
${l.relevance_score ? `<div style="font-size:11px;color:#94a3b8;">评分: ${l.relevance_score}</div>` : ''}
</div>
`).join('');
if (!html) html = '未找到匹配客户';
showResult(discoveryResult, html);
refreshCredits();
} catch (err) {
handleApiError(discoveryResult, err);
} finally {
setLoading(discoveryBtn, false);
}
});
// Marketing
marketingBtn.addEventListener('click', async () => {
const name = marketingName.value.trim();
const desc = marketingDesc.value.trim();
if (!name || !desc) return;
setLoading(marketingBtn, true);
showResult(marketingResult, '<div class="loading"><span class="spinner"></span>生成中...</div>');
try {
const result = await generateMarketing(name, desc, marketingStyle.value);
const contents = result.results || [];
if (result.credits_remaining !== undefined) {
creditsState.balance = result.credits_remaining;
updateCreditsDisplay();
checkLowCredits(5);
}
let html = contents.map((c, i) => `
<div class="item">
<div class="item-label">文案 ${i + 1}</div>
<button class="copy-btn" onclick="navigator.clipboard.writeText(this.parentElement.querySelector('.m-text').textContent)">复制</button>
<div class="m-text">${escapeHtml(c.content || c)}</div>
</div>
`).join('');
if (!html) html = '(无结果)';
showResult(marketingResult, html);
refreshCredits();
} catch (err) {
handleApiError(marketingResult, err);
} finally {
setLoading(marketingBtn, false);
}
});
// Enter key support
[translateInput, replyInput, discoveryProduct, marketingName].forEach(el => {
el.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
const btn = el.closest('.tab-content')?.querySelector('.btn-primary');
if (btn) btn.click();
}
});
});
}
// Auto-init on load
init();
+319
View File
@@ -0,0 +1,319 @@
# TradeMate 产品策略:Agent Skills + 浏览器插件
## 一、市场背景
### 1.1 AI Agent Skills 生态(2026
SKILL.md 开放标准已被 20+ 工具原生支持,包括 Cursor、Claude Code、OpenAI Codex CLI、OpenCode、Gemini CLI、GitHub Copilot、VS Code 等。这意味着**一份 SKILL.md 文件可以分发到几乎所有主流 AI 编程工具**。
商业化现状:
- 头部 skill 月收入 $500-$3,000,中位数 < $50/月
- 定价 $5-$25 一次性购买为主,订阅制尚在试验
- 平台抽成:Agensi 等第三方市场抽 20%
### 1.2 外贸工具浏览器插件生态
Chrome Web Store 上已有数十款外贸工具插件:
- **Topease** — AI 客户开发、邮箱提取、LinkedIn 线索
- **瞬悉** — 商品采集、店铺分析、AI 生成标题/开发信
- **生意助手** — 国际站选品/搬品
- **外贸侠** — 询盘分析、访客分析、聊天记录分析
- **信风AI** — 海关数据、社媒拓客、AI 电话
共同特征:
- 全部为 Chrome 插件
- 核心卖点:获客 + 运营提效
- 定价:几十到几百人民币/月
- 模式:免费版限制用量 → 付费订阅
## 二、TradeMate 的定位
TradeMate 的核心优势:
- **已有完整后端 API**:翻译、客户管理、营销生成、客户发现、报价单等
- **多 AI 提供商**:商汤、NVIDIA、阿里翻译,不依赖单一厂商
- **轻量级**:无需部署,已有 Vue 3 前端工作台
定位差异化:**不做阿里国际站垂直插件,做通用的外贸 AI 工作台**,覆盖从获客到成交的全链路。
## 三、三产品线架构
```
┌─────────────────────────────────────────────────────────────────┐
│ TradeMate 生态架构 │
├─────────────────┬──────────────────┬────────────────────────────┤
│ 项目本身 │ Agent Skills │ 浏览器插件 │
│ (trade-assistant)│ (SKILL.md) │ (Chrome Extension) │
├─────────────────┼──────────────────┼────────────────────────────┤
│ Vue 3 前端 │ OpenCode 标准 │ Chrome Web Store │
│ FastAPI 后端 │ 跨平台兼容 │ 轻量级 popup 交互 │
│ PostgreSQL │ 零部署分发 │ 调用后端 API │
├─────────────────┼──────────────────┼────────────────────────────┤
│ 独立运营 │ 独立运营 │ 独立运营 │
│ SOHO/小团队 │ 开发者/高级用户 │ 传统外贸人员 │
└─────────────────┴──────────────────┴────────────────────────────┘
```
三者关系:
- **共享同一后端 API**,不重复开发逻辑
- **项目本身**是全功能工作台
- **Agent Skills** 是给 AI 编程助手用的轻量能力包
- **浏览器插件**是非技术用户最顺手的入口
- 用户可以从任一入口开始使用,随需求升级自然流向其他产品
## 四、Agent Skills 规划
### 4.1 Skill 列表
| Skill 名称 | 功能 | 调用后端 API |
|-----------|------|-------------|
| `translate-reply` | AI 翻译 + 智能回复生成 | `POST /api/v1/translate`, `POST /api/v1/translate/reply` |
| `customer-discovery` | Google 搜索客户 + 信息提取 | `POST /api/v1/discovery/search` |
| `marketing-content` | 营销文案/关键词生成 | `POST /api/v1/marketing/generate`, `POST /api/v1/marketing/keywords` |
| `quotation-gen` | 从询盘生成报价单 | `POST /api/v1/quotations` |
| `followup-scan` | 沉默客户扫描 + 提醒 | `POST /api/v1/followup/scan` |
### 4.2 分发策略
1. **免费发布**到 Agensi.io、cursor.directory、skills.sh
2. 内置在项目 `.opencode/skills/` 中,项目自身用 OpenCode 开发时可直接使用
3. 每个 SKILL.md 描述清晰的目标、输入、输出,独立可用
### 4.3 格式标准
遵循 SKILL.md 标准格式:
- `name` — 技能名称
- `description` — 一句话说明
- `trigger` — 触发关键词
- `steps` — 执行步骤
- `api_endpoints` — 调用的后端接口(需用户自行配置 BASE_URL 和 API Key
## 五、浏览器插件规划
### 5.1 功能模块
| 模块 | 功能 | 对应后端 |
|------|------|---------|
| AI 翻译 | 输入文本自动翻译、划词翻译 | `POST /api/v1/translate` |
| 智能回复 | 根据客户询盘生成回复建议 | `POST /api/v1/translate/reply` |
| 客户发现 | 输入关键词搜索目标客户 | `POST /api/v1/discovery/search` |
| 营销文案 | 生成营销内容/关键词 | `POST /api/v1/marketing/generate` |
| 快速报价 | 从询盘内容生成报价单 | `POST /api/v1/quotations` |
### 5.2 技术架构
```
browser-extension/
├── manifest.json # Chrome Extension v3 manifest
├── popup/ # 弹出窗口 UI
│ ├── index.html
│ ├── popup.js
│ └── popup.css
├── background/ # Service Worker
│ └── background.js
├── content/ # 内容脚本(划词翻译等)
│ └── content.js
├── api/ # 后端 API 客户端
│ └── client.js
└── icons/ # 插件图标
```
### 5.3 统一积分体系
三产品线共享同一套积分系统(后端已实现):
```
┌─────────────────────────┐
│ TradeMate 后端 │
│ CreditService │
│ ┌──────────────────┐ │
│ │ 积分池 │ │
│ │ Free: 30 一次性 │ │
│ │ Starter: 200/月 │ │
│ │ Pro: 1000/月 │ │
│ │ Enterprise: 2500/月│ │
│ └──────────────────┘ │
└─────────────────────────┘
┌─────────────────────┼─────────────────────┐
│ │ │
网页工作台 浏览器插件 Agent Skills
(消耗积分) (消耗积分) (需要有效 Token)
```
**关键设计**
- 积分(credits)是唯一货币,跨三产品线通用
- 用户在一个地方订阅,所有入口自动升级
- 后端已有 `CreditService` + `SubscriptionPlan` + `CreditPackage` 完整体系
- 浏览器插件通过 API 自动获取当前用户积分余额和套餐信息
**消耗标准**(后端已定义 `DEFAULT_CONSUMPTION_RATES`):
| 功能 | 消耗积分 | 说明 |
|------|---------|------|
| 翻译 | 1/1000 字符 | 每日前 1000 字符免费 |
| 客户搜索 (lead_search) | 10 | 单次 Google 搜索 + 结果提取 |
| 客户分析 (company_analysis) | 5 | 单个网站信息提取 |
| 营销生成 (marketing_content) | 5 | 单次 AI 文案生成 |
| 智能回复 (reply_suggest) | 2 | 单次回复建议生成 |
| 竞品分析 | 10 | 单次竞品对比 |
| 报价单 | 2 | 从询盘生成报价单 |
| AI 对话 | 1/10 条 | AI 数字员工对话 |
### 5.4 认证方案
- 使用 JWT Token 认证,与项目本身共享同一套用户体系
- 首次使用输入 API 地址和 Token(或扫码登录)
- Token 存储在 `chrome.storage.local`
### 5.5 定价模型
#### 5.5.1 月度订阅
| 层级 | 月付 | 年付 (折合/月) | 月积分 | 核心权益 |
|------|------|---------------|--------|---------|
| **Free** | ¥0 | — | 30 一次性 + 1000字/天免费翻译 | 体验客户搜索 x3 + 基本翻译 |
| **Starter** | ¥9.9 | ¥99 (¥8.25) | 200 | 解除日限,含翻译/回复/发现/营销/报价 |
| **Professional** | ¥49 | ¥499 (¥41.6) | 1000 | AI 数字员工 + 团队 (3人) + 优先支持 |
| **Enterprise** | ¥99 | ¥999 (¥83.3) | 2500 | 不限团队 + API 调用 + SLA 保障 |
**Free → Starter 转化逻辑**
- 新用户一次性赠送 30 积分 ≈ 3 次客户搜索 或 6 次营销生成
- 用完即触发升级提示,不走"每日限制"——集中的体验窗口比分散的每日限额更能驱动转化
- 每日 1000 字免费翻译作为持续留存钩子,不让用户完全流失
**Starter ¥9.9/月**
- 200 积分/月 ≈ 20 次客户搜索 或 40 次营销生成
- 足够轻度 SOHO 用户日常使用 20 天
- 不是"无限量"(后端 AI 调用有成本),但大部分用户用不完 200 积分
- 积分的**滚动/过期策略**:未用完的积分最多累积到月配额的 2x,鼓励持续付费
**Professional ¥49/月** — 利润引擎:
- AI 数字员工(AgentOrchestrator)是差异化的核心功能
- 1000 积分 ≈ 100 次客户搜索,覆盖重度用户
- 团队协作(3 人)增加粘性和替换成本
**Enterprise ¥99/月** — 高价值客户:
- 不限团队成员数
- 开放 API 调用权限(用于自定义集成)
- SLA 保障
#### 5.5.2 一次性积分包(低门槛入门付费)
为"偶尔用用"的用户设计,降低首次付费心理门槛:
| 积分包 | 价格 | 折合单价 | 有效期 |
|-------|------|---------|-------|
| 50 积分 | ¥2.9 | 0.058/积分 | 30 天 |
| 200 积分 | ¥9.9 | 0.050/积分 | 90 天 |
| 600 积分 | ¥24.9 | 0.042/积分 | 180 天 |
积分包的作用:
- 用完 Free 额度的用户只需花 **¥2.9** 就能继续用,而不是直接跳到 ¥9.9/月
- ¥2.9 的支付摩擦远低于 ¥9.9,付费转化率会显著提升
- 多次购买积分包后,用户自然过渡到订阅("我每个月都花 ¥9.9 买积分包,不如直接订阅")
#### 5.5.3 三产品线统一定价映射
```
Free Starter (¥9.9) Professional (¥49) Enterprise (¥99)
网页工作台 ✓ ✓ ✓ ✓
浏览器插件 ✓ ✓ ✓ ✓
Agent Skills (需自备Key) (需自备Key) 含 Token 含 Token
```
用户只需订阅一次,所有入口通用。后端判断权限的依据是**当前用户的积分余额和订阅计划**,而不是产品入口。
### 5.6 付费转化设计(升级触发器)
定价只是桌子,付费转化才是把客人请进来坐下。以下是每个触点的设计:
#### 5.6.1 Free 用完积分
```
触发时机:用户发起请求,后端返回 402 Payment Required
{ "detail": "次数不足 (剩余 0.0, 需要 10)" }
插件显示:
┌──────────────────────────────────────────────┐
│ ⚡ 体验积分已用完 │
│ 30 次体验积分已用完,本次搜索需要 10 积分。 │
│ │
│ [买 50 积分 ¥2.9] [订阅 Starter ¥9.9/月] │
│ 或者下次再说 │
└──────────────────────────────────────────────┘
```
#### 5.6.2 功能灰化预览
```
AI 数字员工 Tab 始终可见,但 Professional 以下层级显示:
┌──────────────────────────────────────────────┐
│ 🔒 AI 数字员工 │
│ 自动搜索客户 → 分析公司 → 生成开发信 → 保存 CRM │
│ 全流程自动化,每天可处理 10+ 潜在客户 │
│ │
│ [升级 Professional ¥49/月 解锁] │
└──────────────────────────────────────────────┘
```
#### 5.6.3 活跃度推送
首次使用后第 3 天:
```
通知标题:TradeMate 还够用吗?
内容:你已使用了 12 次翻译、2 次客户搜索
升级 Starter 可解除所有限制,仅 ¥9.9/月
```
#### 5.6.4 积分仪表盘
Footer 始终显示:
```
剩余 15 积分 · 可用 1 次搜索 · 升级 ↑
```
点击"升级"弹出价格对比卡片——**即时可见可用性**是转化核心驱动力。
#### 5.6.5 限时 Pro 试用
在新用户注册后,自动赠送 3 天 Pro 体验(300 积分 + 完整功能):
- 体验结束后恢复到 Free 层
- 体验期间展示:"还剩 2 天 Pro 体验 · 到期自动降级"
- 到期前 24 小时推送转化:"Pro 体验即将结束,订阅仅 ¥9.9/月"
## 六、实施路线图
### Phase 1:文档 + Agent Skills(已完成)
- [x] 产品策略文档(本文档)
- [x] 创建 `.opencode/skills/` 目录
- [x] 完成 `translate-reply` SKILL.md
- [x] 完成 `customer-discovery` SKILL.md
- [x] 完成 `marketing-content` SKILL.md
### Phase 2:浏览器插件骨架(已完成)
- [x] 创建 `browser-extension/` 项目
- [x] 完成 manifest.json 配置
- [x] 完成 popup 基础 UI
- [x] 对接翻译 API
- [x] 对接客户发现 API
- [x] 对接营销生成 API
- [x] 积分余额展示 + 配额不足升级引导
### Phase 3:上架与运营(持续)
- [ ] 定价模型数据库化:配置 SubscriptionPlan + CreditPackage 种子数据
- [ ] 插件端积分展示 + 升级引导 UI 绑定实际 API 响应
- [ ] 发布 SKILL.md 到 Agensi / cursor.directory
- [ ] 发布 Chrome 插件到 Chrome Web Store
- [ ] 配置 Stripe/PayPal 订阅支付(复用后端现有支付系统)
## 七、兼容性保证
1. **后端无侵入** — Skills 和插件调用已有 API 端点,不改动现有路由
2. **前端无侵入** — 浏览器插件是独立项目,不修改 user-frontend/admin-frontend
3. **数据共享** — 同一用户在不同入口产生的数据互通(共用同一数据库)
4. **可单独部署** — 后端可独立运行,Skills 和插件只是客户端
5. **积分互通** — 所有入口共享 `CreditService`,消费记录统一记录在 `credit_consumptions`
6. **套餐配置化**`SubscriptionPlan``CreditPackage` 存储在 DB 中,不硬编码在前端,管理员可随时调整价格和配额