feat: 修复 H5 底部导航覆盖 + 更新项目进度文档

## H5 底部导航修复 (Bug #10)
- 精简 App.vue,移除重复 tabbar,仅保留全局样式
- uni-page 设置 height: calc(100% - 50px) + overflow-y: auto
- 内容区域精确停在底部导航上方,独立滚动不再叠加
- 恢复 custom-tab-bar 组件

## 项目进度文档
- PROGRESS.md 更新至 10 个 Bug 修复
- 新增 H5 底部导航修复记录
- 新增历史变更条目
This commit is contained in:
TradeMate Dev
2026-05-12 20:24:42 +08:00
parent 69e164dcae
commit 7b62c2f8b4
125 changed files with 19725 additions and 728 deletions
+2 -1
View File
@@ -13,7 +13,7 @@ class AIProvider(ABC):
@abstractmethod
async def reply(
self, inquiry: str, context: Optional[Dict[str, Any]] = None,
tone: str = "professional",
tone: str = "professional", preference_context: Optional[str] = None,
) -> Dict[str, Any]:
pass
@@ -21,6 +21,7 @@ class AIProvider(ABC):
async def generate_marketing(
self, product_info: Dict[str, Any], target: str,
style: str = "professional", language: str = "en",
preference_context: Optional[str] = None,
) -> Dict[str, Any]:
pass
+3 -1
View File
@@ -2,5 +2,7 @@ from .openai import OpenAIProvider
from .claude import ClaudeProvider
from .deepl import DeepLProvider
from .local import LocalProvider
from .spark import SparkProvider
from .sensenova import SensenovaProvider
__all__ = ["OpenAIProvider", "ClaudeProvider", "DeepLProvider", "LocalProvider"]
__all__ = ["OpenAIProvider", "ClaudeProvider", "DeepLProvider", "LocalProvider", "SparkProvider", "SensenovaProvider"]
+6 -2
View File
@@ -32,8 +32,10 @@ class ClaudeProvider(AIProvider):
content = await self._call(system, prompt)
return {"translated_text": content, "provider": self.name}
async def reply(self, inquiry: str, context: Optional[Dict[str, Any]] = None, tone: str = "professional") -> Dict[str, Any]:
async def reply(self, inquiry: str, context: Optional[Dict[str, Any]] = None, tone: str = "professional", preference_context: Optional[str] = None) -> Dict[str, Any]:
system = SYSTEM_PROMPTS["reply"]
if preference_context:
system += f"\nUser writing preference: {preference_context}"
context_str = ""
if context:
for k, v in context.items():
@@ -43,8 +45,10 @@ class ClaudeProvider(AIProvider):
content = await self._call(system, prompt)
return {"reply": content, "provider": self.name}
async def generate_marketing(self, product_info: Dict[str, Any], target: str, style: str = "professional", language: str = "en") -> Dict[str, Any]:
async def generate_marketing(self, product_info: Dict[str, Any], target: str, style: str = "professional", language: str = "en", preference_context: Optional[str] = None) -> Dict[str, Any]:
system = SYSTEM_PROMPTS["marketing"]
if preference_context:
system += f"\nUser preference: {preference_context}"
info = json.dumps(product_info, ensure_ascii=False, indent=2)
prompt = f"Product:\n{info}\n\nTarget: {target}\nStyle: {style}\nLanguage: {language}\n\nWrite marketing copy:"
content = await self._call(system, prompt, max_tokens=1500)
+11 -6
View File
@@ -14,17 +14,22 @@ class LocalProvider(AIProvider):
result = await self._generate(prompt)
return {"translated_text": result, "provider": self.name, "cost": 0.0}
async def reply(self, inquiry: str, context: Optional[Dict[str, Any]] = None, tone: str = "professional") -> Dict[str, Any]:
ctx = ""
async def reply(self, inquiry: str, context: Optional[Dict[str, Any]] = None, tone: str = "professional", preference_context: Optional[str] = None) -> Dict[str, Any]:
prompt = ""
if preference_context:
prompt += f"[User prefers: {preference_context}]\n"
if context:
ctx = "\n".join(f"{k}: {v}" for k, v in context.items() if v)
prompt = f"{ctx}\nCustomer: {inquiry}\n\nWrite a {tone} reply:"
prompt += "\n".join(f"{k}: {v}" for k, v in context.items() if v) + "\n"
prompt += f"Customer: {inquiry}\n\nWrite a {tone} reply:"
result = await self._generate(prompt)
return {"reply": result, "provider": self.name, "cost": 0.0}
async def generate_marketing(self, product_info: Dict[str, Any], target: str, style: str = "professional", language: str = "en") -> Dict[str, Any]:
async def generate_marketing(self, product_info: Dict[str, Any], target: str, style: str = "professional", language: str = "en", preference_context: Optional[str] = None) -> Dict[str, Any]:
info = json.dumps(product_info, ensure_ascii=False)
prompt = f"Product: {info}\nTarget: {target}\nStyle: {style}\nLanguage: {language}\n\nMarketing copy:"
prompt = ""
if preference_context:
prompt += f"[User prefers: {preference_context}]\n"
prompt += f"Product: {info}\nTarget: {target}\nStyle: {style}\nLanguage: {language}\n\nMarketing copy:"
result = await self._generate(prompt, max_tokens=800)
return {"content": result, "provider": self.name, "cost": 0.0}
+52 -6
View File
@@ -19,8 +19,11 @@ SYSTEM_PROMPTS = {
class OpenAIProvider(AIProvider):
def __init__(self, api_key: str, model: str = "gpt-4o"):
self.client = AsyncOpenAI(api_key=api_key)
def __init__(self, api_key: str, model: str = "gpt-4o", base_url: Optional[str] = None):
kwargs = {"api_key": api_key}
if base_url:
kwargs["base_url"] = base_url
self.client = AsyncOpenAI(**kwargs)
self.model = model
self._name = f"openai-{model}"
self._pricing = {
@@ -39,8 +42,10 @@ class OpenAIProvider(AIProvider):
content = await self._call(system, f"Translate to {target_lang}:\n\n{text}", model=self._cheap_model)
return {"translated_text": content, "provider": self.name, "model": self.model}
async def reply(self, inquiry: str, context: Optional[Dict[str, Any]] = None, tone: str = "professional") -> Dict[str, Any]:
async def reply(self, inquiry: str, context: Optional[Dict[str, Any]] = None, tone: str = "professional", preference_context: Optional[str] = None) -> Dict[str, Any]:
system = SYSTEM_PROMPTS["reply"] + f"\nTone: {tone}"
if preference_context:
system += f"\nUser preference: {preference_context}"
context_str = ""
if context:
@@ -57,8 +62,10 @@ class OpenAIProvider(AIProvider):
content = await self._call(system, prompt)
return {"reply": content, "provider": self.name, "model": self.model}
async def generate_marketing(self, product_info: Dict[str, Any], target: str, style: str = "professional", language: str = "en") -> Dict[str, Any]:
async def generate_marketing(self, product_info: Dict[str, Any], target: str, style: str = "professional", language: str = "en", preference_context: Optional[str] = None) -> Dict[str, Any]:
system = SYSTEM_PROMPTS["marketing"] + f"\nStyle: {style}\nTarget audience: {target}\nLanguage: {language}"
if preference_context:
system += f"\nUser preference: {preference_context}"
product_str = json.dumps(product_info, ensure_ascii=False, indent=2)
prompt = f"Product information:\n{product_str}\n\nGenerate marketing copy:"
@@ -76,7 +83,7 @@ class OpenAIProvider(AIProvider):
except json.JSONDecodeError:
return {"data": {}, "confidence": 0.0, "provider": self.name, "error": "parse_failed"}
async def _call(self, system: str, prompt: str, max_tokens: int = 1000, response_format: Optional[Dict] = None, model: Optional[str] = None) -> str:
async def _call(self, system: str, prompt: str, max_tokens: int = 3000, response_format: Optional[Dict] = None, model: Optional[str] = None) -> str:
kwargs = {
"model": model or self.model,
"messages": [
@@ -90,7 +97,46 @@ class OpenAIProvider(AIProvider):
kwargs["response_format"] = response_format
resp = await self.client.chat.completions.create(**kwargs)
return resp.choices[0].message.content
content = resp.choices[0].message.content
if content is None and hasattr(resp.choices[0].message, 'reasoning'):
reasoning = resp.choices[0].message.reasoning
if reasoning:
import re
final_output_patterns = [
r'Final Output Generation[:]\s*(.+?)(?:\n\n|$)',
r'Final Output[:]\s*(.+?)(?:\n\n|$)',
r'7\.\s*Final Output Generation[:]\s*(.+?)(?:\n\n|$)',
r'翻译结果[:]\s*(.+?)(?:\n\n|$)',
r'最终输出[:]\s*(.+?)(?:\n\n|$)',
]
for pattern in final_output_patterns:
match = re.search(pattern, reasoning, re.DOTALL)
if match:
content = match.group(1).strip()
break
if content is None:
paragraphs = re.split(r'\n\n+', reasoning.strip())
if paragraphs:
for p in reversed(paragraphs):
p = p.strip()
if p and len(p) > 10:
if not p.startswith('步骤') and not p.startswith('Step'):
content = p
break
if content is None and hasattr(resp.choices[0].message, 'reasoning'):
reasoning = resp.choices[0].message.reasoning
if reasoning:
import re
cleaned = re.sub(r'^步骤\d+[:].*$', '', reasoning, flags=re.MULTILINE)
cleaned = re.sub(r'^Step \d+[:].*$', '', cleaned, flags=re.MULTILINE)
cleaned = re.sub(r'\n+', '\n', cleaned).strip()
if cleaned:
content = cleaned
return content
@property
def name(self) -> str:
+7
View File
@@ -0,0 +1,7 @@
from app.ai.providers.openai import OpenAIProvider
class SensenovaProvider(OpenAIProvider):
def __init__(self, api_key: str, model: str = "sensenova-6.7-flash-lite", base_url: str = "https://token.sensenova.cn/v1"):
super().__init__(api_key=api_key, model=model, base_url=base_url)
self._name = f"sensenova-{model}"
+87
View File
@@ -0,0 +1,87 @@
from typing import Dict, Any, Optional
import json
from openai import AsyncOpenAI
from app.ai.base import AIProvider
SYSTEM_PROMPTS = {
"translate": "You are a professional translator specialized in foreign trade. "
"Translate business terms accurately. Return ONLY the translated text.",
"reply": "You are an experienced foreign trade sales expert. Write professional, "
"clear business replies. Return ONLY the reply text.",
"marketing": "You are a creative copywriter for international trade. "
"Return ONLY the marketing copy, no explanations.",
"extract": "Extract structured data from text. Return ONLY valid JSON.",
}
class SparkProvider(AIProvider):
def __init__(self, api_key: str, model: str = "astron-code-latest", base_url: str = None):
from app.config import settings
self.client = AsyncOpenAI(
api_key=api_key,
base_url=base_url or settings.IFLYTEK_API_BASE,
)
self.model = model
self._name = f"spark-{model}"
async def translate(self, text: str, source_lang: Optional[str], target_lang: str, context: Optional[str] = None) -> Dict[str, Any]:
system = SYSTEM_PROMPTS["translate"]
if context:
system += f"\nContext: {context}"
prompt = f"Translate {f'from {source_lang} ' if source_lang and source_lang != 'auto' else ''}to {target_lang}:\n\n{text}"
content = await self._call(system, prompt)
return {"translated_text": content, "provider": self.name}
async def reply(self, inquiry: str, context: Optional[Dict[str, Any]] = None, tone: str = "professional", preference_context: Optional[str] = None) -> Dict[str, Any]:
system = SYSTEM_PROMPTS["reply"] + f"\nTone: {tone}"
if preference_context:
system += f"\nUser preference: {preference_context}"
ctx = ""
if context:
ctx = "\n".join(f"{k}: {v}" for k, v in context.items() if v)
prompt = f"{ctx}\nCustomer inquiry:\n{inquiry}\n\nWrite a reply:"
content = await self._call(system, prompt)
return {"reply": content, "provider": self.name}
async def generate_marketing(self, product_info: Dict[str, Any], target: str, style: str = "professional", language: str = "en", preference_context: Optional[str] = None) -> Dict[str, Any]:
system = SYSTEM_PROMPTS["marketing"] + f"\nStyle: {style}\nAudience: {target}\nLanguage: {language}"
if preference_context:
system += f"\nUser preference: {preference_context}"
info = json.dumps(product_info, ensure_ascii=False)
prompt = f"Product:\n{info}\n\nGenerate marketing copy:"
content = await self._call(system, prompt, max_tokens=1500)
return {"content": content, "provider": self.name}
async def extract_info(self, text: str, schema: Dict[str, Any]) -> Dict[str, Any]:
system = SYSTEM_PROMPTS["extract"]
prompt = f"Schema:\n{json.dumps(schema, indent=2)}\n\nText:\n{text}\n\nJSON:"
content = await self._call(system, prompt, response_format={"type": "json_object"})
try:
data = json.loads(content)
return {"data": data, "confidence": 0.9, "provider": self.name}
except json.JSONDecodeError:
return {"data": {}, "confidence": 0.0, "provider": self.name}
async def _call(self, system: str, prompt: str, max_tokens: int = 1000, response_format: Optional[Dict] = None) -> str:
kwargs = {
"model": self.model,
"messages": [
{"role": "system", "content": system},
{"role": "user", "content": prompt},
],
"max_tokens": max_tokens,
"temperature": 0.7,
}
if response_format:
kwargs["response_format"] = response_format
resp = await self.client.chat.completions.create(**kwargs)
return resp.choices[0].message.content
@property
def name(self) -> str:
return self._name
@property
def cost_per_1k_tokens(self) -> float:
return 0.0
+27 -5
View File
@@ -1,6 +1,6 @@
from typing import Dict, Any, Optional, List
from app.ai.base import AIProvider
from app.ai.providers import OpenAIProvider, ClaudeProvider, DeepLProvider, LocalProvider
from app.ai.providers import OpenAIProvider, ClaudeProvider, DeepLProvider, LocalProvider, SparkProvider, SensenovaProvider
from app.config import settings
from app.ai.trade_corpus import TradeCorpus
import logging
@@ -23,6 +23,17 @@ class AIRouter:
except Exception as e:
logger.warning(f"OpenAI init failed: {e}")
if settings.SENSENOVA_API_KEY:
try:
self.providers["sensenova"] = SensenovaProvider(
api_key=settings.SENSENOVA_API_KEY,
model=settings.SENSENOVA_MODEL,
base_url=settings.SENSENOVA_BASE_URL,
)
logger.info("Sensenova provider ready")
except Exception as e:
logger.warning(f"Sensenova init failed: {e}")
if settings.ANTHROPIC_API_KEY:
try:
self.providers["anthropic"] = ClaudeProvider(api_key=settings.ANTHROPIC_API_KEY)
@@ -37,6 +48,17 @@ class AIRouter:
except Exception as e:
logger.warning(f"DeepL init failed: {e}")
if settings.IFLYTEK_API_KEY:
try:
self.providers["spark"] = SparkProvider(
api_key=settings.IFLYTEK_API_KEY,
model=settings.IFLYTEK_MODEL,
base_url=settings.IFLYTEK_API_BASE,
)
logger.info("Spark provider ready")
except Exception as e:
logger.warning(f"Spark init failed: {e}")
if settings.LOCAL_MODEL_ENABLED:
try:
self.providers["local"] = LocalProvider(model_url=settings.LOCAL_MODEL_URL)
@@ -90,11 +112,11 @@ class AIRouter:
async def translate(self, text: str, target_lang: str, source_lang: Optional[str] = None, context: Optional[str] = None) -> Dict[str, Any]:
return await self.execute("translate", "translate", text, source_lang, target_lang, context)
async def reply(self, inquiry: str, context: Optional[Dict[str, Any]] = None, tone: str = "professional") -> Dict[str, Any]:
return await self.execute("reply", "reply", inquiry, context, tone)
async def reply(self, inquiry: str, context: Optional[Dict[str, Any]] = None, tone: str = "professional", preference_context: Optional[str] = None) -> Dict[str, Any]:
return await self.execute("reply", "reply", inquiry, context, tone, preference_context)
async def marketing(self, product_info: Dict[str, Any], target: str, style: str = "professional", language: str = "en") -> Dict[str, Any]:
return await self.execute("marketing", "generate_marketing", product_info, target, style, language)
async def marketing(self, product_info: Dict[str, Any], target: str, style: str = "professional", language: str = "en", preference_context: Optional[str] = None) -> Dict[str, Any]:
return await self.execute("marketing", "generate_marketing", product_info, target, style, language, preference_context)
async def extract(self, text: str, schema: Dict[str, Any]) -> Dict[str, Any]:
return await self.execute("extract", "extract_info", text, schema)