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:
@@ -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
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}"
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user