BaiFu
Committed by GitHub

Feat: 新增安思派AI搜索支持 (#457) (#461)

* :sparkling: 安思派搜索框架定义

* feat: 安思派搜索接入后端实现

* feat: MediaEngine中接入AnspireSearchAgent

* feat: 修改前端以适配新搜索agent

Co-authored-by: Zhang Yuxiang <51037789+NTFago@users.noreply.github.com>
... ... @@ -66,6 +66,13 @@ KEYWORD_OPTIMIZER_MODEL_NAME=
# Tavily API密钥,用于Tavily网络搜索,申请地址:https://www.tavily.com/
TAVILY_API_KEY=
# 网络搜索工具类型,支持BochaAPI或AnspireAPI两种,默认为AnspireAPI
SEARCH_TOOL_TYPE=AnspireAPI
# Bocha AI Search BASEURL,用于Bocha多模态搜索,这里密钥名称虽然是Web Search,但其实是要AI Search的,申请地址:https://open.bochaai.com/
BOCHA_BASE_URL=https://api.bocha.cn/v1/ai-search
BOCHA_WEB_SEARCH_API_KEY=
\ No newline at end of file
BOCHA_WEB_SEARCH_API_KEY=
# Anspire AI Search API(申请地址:https://open.anspire.cn/)
ANSPIRE_BASE_URL=https://plugin.anspire.cn/api/ntsearch/search
ANSPIRE_API_KEY=
\ No newline at end of file
... ...
... ... @@ -3,10 +3,10 @@ Deep Search Agent
一个无框架的深度搜索AI代理实现
"""
from .agent import DeepSearchAgent, create_agent
from .agent import DeepSearchAgent, AnspireSearchAgent, create_agent
from .utils.config import Settings
__version__ = "1.0.0"
__author__ = "Deep Search Agent Team"
__all__ = ["DeepSearchAgent", "create_agent", "Settings"]
__all__ = ["DeepSearchAgent", "AnspireSearchAgent", "create_agent", "Settings"]
... ...
... ... @@ -19,7 +19,7 @@ from .nodes import (
ReportFormattingNode
)
from .state import State
from .tools import BochaMultimodalSearch, BochaResponse
from .tools import BochaMultimodalSearch, BochaResponse, AnspireAISearch, AnspireResponse
from .utils import settings, Settings, format_search_results_for_prompt
... ... @@ -50,7 +50,7 @@ class DeepSearchAgent:
# 确保输出目录存在
os.makedirs(self.config.OUTPUT_DIR, exist_ok=True)
logger.info(f"Meida Agent已初始化")
logger.info(f"Media Agent已初始化")
logger.info(f"使用LLM: {self.llm_client.get_model_info()}")
logger.info(f"搜索工具集: BochaMultimodalSearch (支持5种多模态搜索工具)")
... ... @@ -436,6 +436,60 @@ class DeepSearchAgent:
self.state.save_to_file(filepath)
logger.info(f"状态已保存到 {filepath}")
class AnspireSearchAgent(DeepSearchAgent):
"""调用Anspire搜索引擎的Deep Search Agent"""
def __init__(self, config: Settings | None = None):
self.config = config or settings
# 初始化LLM客户端
self.llm_client = self._initialize_llm()
# 初始化搜索工具集
self.search_agency = AnspireAISearch(api_key=self.config.ANSPIRE_API_KEY)
# 初始化节点
self._initialize_nodes()
# 状态
self.state = State()
# 确保输出目录存在
os.makedirs(self.config.OUTPUT_DIR, exist_ok=True)
logger.info(f"Media Agent已初始化")
logger.info(f"使用LLM: {self.llm_client.get_model_info()}")
logger.info(f"搜索工具集: AnspireSearch")
def execute_search_tool(self, tool_name: str, query: str, **kwargs) -> AnspireResponse:
# TODO: 使用Anspire搜索工具执行搜索
"""
执行指定的搜索工具
Args:
tool_name: 工具名称,可选值:
- "comprehensive_search": 全面综合搜索(默认)
- "search_last_24_hours": 24小时内最新信息
- "search_last_week": 本周信息
query: 搜索查询
**kwargs: 额外参数(如max_results)
Returns:
AnspireResponse对象
"""
logger.info(f" → 执行搜索工具: {tool_name}")
if tool_name == "comprehensive_search":
max_results = kwargs.get("max_results", 10)
return self.search_agency.comprehensive_search(query, max_results)
elif tool_name == "search_last_24_hours":
return self.search_agency.search_last_24_hours(query)
elif tool_name == "search_last_week":
return self.search_agency.search_last_week(query)
else:
logger.info(f" ⚠️ 未知的搜索工具: {tool_name},使用默认综合搜索")
return self.search_agency.comprehensive_search(query)
def create_agent(config_file: Optional[str] = None) -> DeepSearchAgent:
"""
... ... @@ -448,4 +502,6 @@ def create_agent(config_file: Optional[str] = None) -> DeepSearchAgent:
DeepSearchAgent实例
"""
settings = Settings()
if settings.SEARCH_TOOL_TYPE == "AnspireAPI":
return AnspireSearchAgent(settings)
return DeepSearchAgent(settings)
... ...
... ... @@ -5,18 +5,22 @@
from .search import (
BochaMultimodalSearch,
AnspireAISearch,
WebpageResult,
ImageResult,
ModalCardResult,
BochaResponse,
AnspireResponse,
print_response_summary
)
__all__ = [
"BochaMultimodalSearch",
"AnspireAISearch",
"WebpageResult",
"ImageResult",
"ModalCardResult",
"BochaResponse",
"AnspireResponse",
"print_response_summary"
]
... ...
... ... @@ -23,6 +23,7 @@
import os
import json
import sys
import datetime
from typing import List, Dict, Any, Optional, Literal
from loguru import logger
... ... @@ -85,6 +86,14 @@ class BochaResponse:
images: List[ImageResult] = field(default_factory=list)
modal_cards: List[ModalCardResult] = field(default_factory=list)
@dataclass
class AnspireResponse:
"""封装 Anspire API 的完整返回结果,以便在工具间传递"""
query: str
conversation_id: Optional[str] = None
score: Optional[float] = None
webpages: List[WebpageResult] = field(default_factory=list)
# --- 2. 核心客户端与专用工具集 ---
... ... @@ -94,7 +103,7 @@ class BochaMultimodalSearch:
每个公共方法都设计为供 AI Agent 独立调用的工具。
"""
BOCHA_BASE_URL = settings.BOCHA_BASE_URL or "https://api.bochaai.com/v1/ai-search"
BOCHA_BASE_URL = settings.BOCHA_BASE_URL or "https://api.bocha.com/v1/ai-search"
def __init__(self, api_key: Optional[str] = None):
"""
... ... @@ -181,6 +190,7 @@ class BochaMultimodalSearch:
payload.update(kwargs)
try:
response = requests.post(self.BOCHA_BASE_URL, headers=self._headers, json=payload, timeout=30)
response.raise_for_status() # 如果HTTP状态码是4xx或5xx,则抛出异常
... ... @@ -255,22 +265,138 @@ class BochaMultimodalSearch:
logger.info(f"--- TOOL: 搜索本周信息 (query: {query}) ---")
return self._search_internal(query=query, freshness='oneWeek', answer=True)
class AnspireAISearch:
"""
Anspire AI Search 客户端
"""
ANSPIRE_BASE_URL = settings.ANSPIRE_BASE_URL or "https://plugin.anspire.cn/api/ntsearch/search"
# --- 3. 测试与使用示例 ---
def __init__(self, api_key: Optional[str] = None):
"""
初始化客户端。
Args:
api_key: Anspire API密钥,若不提供则从环境变量 ANSPIRE_API_KEY 读取。
"""
if api_key is None:
api_key = settings.ANSPIRE_API_KEY
if not api_key:
raise ValueError("Anspire API Key未找到!请设置 ANSPIRE_API_KEY 环境变量或在初始化时提供")
self._headers = {
'Authorization': f'Bearer {api_key}',
'Content-Type': 'application/json',
'Connection': 'keep-alive',
'Accept': '*/*'
}
def _parse_search_response(self, response_dict: Dict[str, Any], query: str) -> AnspireResponse:
final_response = AnspireResponse(query=query)
final_response.conversation_id = response_dict.get('Uuid')
messages = response_dict.get("results", [])
for msg in messages:
final_response.score = msg.get("score")
final_response.webpages.append(WebpageResult(
name = msg.get("title", ""),
url = msg.get("url", ""),
snippet = msg.get("content", ""),
date_last_crawled = msg.get("date", None)
))
return final_response
@with_graceful_retry(SEARCH_API_RETRY_CONFIG, default_return=AnspireResponse(query="搜索失败"))
def _search_internal(self, **kwargs) -> AnspireResponse:
"""内部通用的搜索执行器,所有工具最终都调用此方法"""
query = kwargs.get("query", "Unknown Query")
payload = {
"query": query,
"top_k": kwargs.get("top_k", 10),
"Insite": kwargs.get("Insite", ""),
"FromTime": kwargs.get("FromTime", ""),
"ToTime": kwargs.get("ToTime", "")
}
try:
response = requests.get(self.ANSPIRE_BASE_URL, headers=self._headers, params=payload, timeout=30)
response.raise_for_status() # 如果HTTP状态码是4xx或5xx,则抛出异常
response_dict = response.json()
return self._parse_search_response(response_dict, query)
except requests.exceptions.RequestException as e:
logger.exception(f"搜索时发生网络错误: {str(e)}")
raise e # 让重试机制捕获并处理
except Exception as e:
logger.exception(f"处理响应时发生未知错误: {str(e)}")
raise e # 让重试机制捕获并处理
def comprehensive_search(self, query: str, max_results: int = 10) -> AnspireResponse:
"""
【工具】综合搜索: 获取关于某个主题的全面信息,包括网页。
适用于需要多种信息来源的场景。
"""
logger.info(f"--- TOOL: 综合搜索 (query: {query}) ---")
return self._search_internal(
query=query,
top_k=max_results
)
def print_response_summary(response: BochaResponse):
def search_last_24_hours(self, query: str, max_results: int = 10) -> AnspireResponse:
"""
【工具】搜索24小时内信息: 获取关于某个主题的最新动态。
此工具专门查找过去24小时内发布的内容。适用于追踪突发事件或最新进展。
"""
logger.info(f"--- TOOL: 搜索24小时内信息 (query: {query}) ---")
to_time = datetime.datetime.now()
from_time = to_time - datetime.timedelta(days=1)
return self._search_internal(query=query,
top_k=max_results,
FromTime=from_time.strftime("%Y-%m-%d %H:%M:%S"),
ToTime=to_time.strftime("%Y-%m-%d %H:%M:%S"))
def search_last_week(self, query: str, max_results: int = 10) -> AnspireResponse:
"""
【工具】搜索本周信息: 获取关于某个主题过去一周内的主要报道。
适用于进行周度舆情总结或回顾。
"""
logger.info(f"--- TOOL: 搜索本周信息 (query: {query}) ---")
to_time = datetime.datetime.now()
from_time = to_time - datetime.timedelta(weeks=1)
return self._search_internal(query=query,
top_k=max_results,
FromTime=from_time.strftime("%Y-%m-%d %H:%M:%S"),
ToTime=to_time.strftime("%Y-%m-%d %H:%M:%S"))
# --- 3. 测试与使用示例 ---
def load_agent_from_config():
"""根据配置文件选择并加载搜索Agent"""
if settings.BOCHA_WEB_SEARCH_API_KEY:
logger.info("加载 BochaMultimodalSearch Agent")
return BochaMultimodalSearch()
elif settings.ANSPIRE_API_KEY:
logger.info("加载 AnspireAISearch Agent")
return AnspireAISearch()
else:
raise ValueError("未配置有效的搜索Agent")
def print_response_summary(response):
"""简化的打印函数,用于展示测试结果"""
if not response or not response.query:
logger.error("未能获取有效响应。")
return
logger.info(f"\n查询: '{response.query}' | 会话ID: {response.conversation_id}")
if response.answer:
if hasattr(response, 'answer') and response.answer:
logger.info(f"AI摘要: {response.answer[:150]}...")
logger.info(f"找到 {len(response.webpages)} 个网页, {len(response.images)} 张图片, {len(response.modal_cards)} 个模态卡。")
logger.info(f"找到 {len(response.webpages)} 个网页")
if hasattr(response, 'images'):
logger.info(f"找到 {len(response.images)} 张图片")
if hasattr(response, 'modal_cards'):
logger.info(f"找到 {len(response.modal_cards)} 个模态卡")
if response.modal_cards:
if hasattr(response, 'modal_cards') and response.modal_cards:
first_card = response.modal_cards[0]
logger.info(f"第一个模态卡类型: {first_card.card_type}")
... ... @@ -278,7 +404,7 @@ def print_response_summary(response: BochaResponse):
first_result = response.webpages[0]
logger.info(f"第一条网页结果: {first_result.name}")
if response.follow_ups:
if hasattr(response, 'follow_ups') and response.follow_ups:
logger.info(f"建议追问: {response.follow_ups}")
logger.info("-" * 60)
... ... @@ -289,31 +415,34 @@ if __name__ == "__main__":
try:
# 初始化多模态搜索客户端,它内部包含了所有工具
search_client = BochaMultimodalSearch()
search_client = load_agent_from_config()
# 场景1: Agent进行一次常规的、需要AI总结的综合搜索
response1 = search_client.comprehensive_search(query="人工智能对未来教育的影响")
print_response_summary(response1)
# 场景2: Agent需要查询特定结构化信息 - 天气
response2 = search_client.search_for_structured_data(query="上海明天天气怎么样")
print_response_summary(response2)
# 深度解析第一个模态卡
if response2.modal_cards and response2.modal_cards[0].card_type == 'weather_china':
logger.info("天气模态卡详情:", json.dumps(response2.modal_cards[0].content, indent=2, ensure_ascii=False))
if isinstance(search_client, BochaMultimodalSearch):
response2 = search_client.search_for_structured_data(query="上海明天天气怎么样")
print_response_summary(response2)
# 深度解析第一个模态卡
if response2.modal_cards and response2.modal_cards[0].card_type == 'weather_china':
logger.info("天气模态卡详情:", json.dumps(response2.modal_cards[0].content, indent=2, ensure_ascii=False))
# 场景3: Agent需要查询特定结构化信息 - 股票
response3 = search_client.search_for_structured_data(query="东方财富股票")
print_response_summary(response3)
if isinstance(search_client, BochaMultimodalSearch):
response3 = search_client.search_for_structured_data(query="东方财富股票")
print_response_summary(response3)
# 场景4: Agent需要追踪某个事件的最新进展
response4 = search_client.search_last_24_hours(query="C929大飞机最新消息")
print_response_summary(response4)
# 场景5: Agent只需要快速获取网页信息,不需要AI总结
response5 = search_client.web_search_only(query="Python dataclasses用法")
print_response_summary(response5)
if isinstance(search_client, BochaMultimodalSearch):
response5 = search_client.web_search_only(query="Python dataclasses用法")
print_response_summary(response5)
# 场景6: Agent需要回顾一周内关于某项技术的新闻
response6 = search_client.search_last_week(query="量子计算商业化")
... ...
... ... @@ -5,7 +5,7 @@ Configuration management module for the Media Engine (pydantic_settings style).
from pathlib import Path
from pydantic_settings import BaseSettings
from pydantic import Field
from typing import Optional
from typing import Optional, Literal
# 计算 .env 优先级:优先当前工作目录,其次项目根目录
... ... @@ -70,8 +70,13 @@ class Settings(BaseSettings):
# ================== 网络工具配置 ====================
TAVILY_API_KEY: str = Field(None, description="Tavily API(申请地址:https://www.tavily.com/)API密钥,用于Tavily网络搜索")
SEARCH_TOOL_TYPE: Literal["AnspireAPI", "BochaAPI"] = Field("AnspireAPI", description="网络搜索工具类型,支持BochaAPI或AnspireAPI两种,默认为AnspireAPI")
BOCHA_BASE_URL: Optional[str] = Field("https://api.bochaai.com/v1/ai-search", description="Bocha AI 搜索BaseUrl或博查网页搜索BaseUrl")
BOCHA_WEB_SEARCH_API_KEY: str = Field(None, description="Bocha API(申请地址:https://open.bochaai.com/)API密钥,用于Bocha搜索")
BOCHA_WEB_SEARCH_API_KEY: Optional[str] = Field(None, description="Bocha API(申请地址:https://open.bochaai.com/)API密钥,用于Bocha搜索")
# Anspire AI Search API(申请地址:https://open.anspire.cn/)
ANSPIRE_BASE_URL: Optional[str] = Field("https://plugin.anspire.cn/api/ntsearch/search", description="Anspire AI 搜索BaseUrl")
ANSPIRE_API_KEY: Optional[str] = Field(None, description="Anspire AI Search API(申请地址:https://open.anspire.cn/)API密钥,用于Anspire搜索")
class Config:
env_file = ENV_FILE
... ...
... ... @@ -27,7 +27,7 @@ except locale.Error:
# 添加src目录到Python路径
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from MediaEngine import DeepSearchAgent, Settings
from MediaEngine import DeepSearchAgent, AnspireSearchAgent, Settings
from config import settings
from utils.github_issues import error_with_issue_link
... ... @@ -101,25 +101,39 @@ def main():
st.error("请在您的环境变量中设置MEDIA_ENGINE_API_KEY")
logger.error("请在您的环境变量中设置MEDIA_ENGINE_API_KEY")
return
if not settings.BOCHA_WEB_SEARCH_API_KEY:
st.error("请在您的环境变量中设置BOCHA_WEB_SEARCH_API_KEY")
logger.error("请在您的环境变量中设置BOCHA_WEB_SEARCH_API_KEY")
if (not settings.BOCHA_WEB_SEARCH_API_KEY) and (not settings.ANSPIRE_API_KEY):
st.error("请在您的环境变量中设置BOCHA_WEB_SEARCH_API_KEY或ANSPIRE_API_KEY")
logger.error("请在您的环境变量中设置BOCHA_WEB_SEARCH_API_KEY或ANSPIRE_API_KEY")
return
# 自动使用配置文件中的API密钥
engine_key = settings.MEDIA_ENGINE_API_KEY
bocha_key = settings.BOCHA_WEB_SEARCH_API_KEY
ansire_key = settings.ANSPIRE_API_KEY
# 构建 Settings(pydantic_settings风格,优先大写环境变量)
config = Settings(
MEDIA_ENGINE_API_KEY=engine_key,
MEDIA_ENGINE_BASE_URL=settings.MEDIA_ENGINE_BASE_URL,
MEDIA_ENGINE_MODEL_NAME=model_name,
BOCHA_WEB_SEARCH_API_KEY=bocha_key,
MAX_REFLECTIONS=max_reflections,
SEARCH_CONTENT_MAX_LENGTH=max_content_length,
OUTPUT_DIR="media_engine_streamlit_reports",
)
if bocha_key:
logger.info("使用Bocha搜索API密钥")
config = Settings(
MEDIA_ENGINE_API_KEY=engine_key,
MEDIA_ENGINE_BASE_URL=settings.MEDIA_ENGINE_BASE_URL,
MEDIA_ENGINE_MODEL_NAME=model_name,
BOCHA_WEB_SEARCH_API_KEY=bocha_key,
MAX_REFLECTIONS=max_reflections,
SEARCH_CONTENT_MAX_LENGTH=max_content_length,
OUTPUT_DIR="media_engine_streamlit_reports",
)
elif ansire_key:
logger.info("使用Anspire搜索API密钥")
config = Settings(
MEDIA_ENGINE_API_KEY=engine_key,
MEDIA_ENGINE_BASE_URL=settings.MEDIA_ENGINE_BASE_URL,
MEDIA_ENGINE_MODEL_NAME=model_name,
ANSPIRE_API_KEY=ansire_key,
MAX_REFLECTIONS=max_reflections,
SEARCH_CONTENT_MAX_LENGTH=max_content_length,
OUTPUT_DIR="media_engine_streamlit_reports",
)
# 执行研究
execute_research(query, config)
... ... @@ -134,7 +148,10 @@ def execute_research(query: str, config: Settings):
# 初始化Agent
status_text.text("正在初始化Agent...")
agent = DeepSearchAgent(config)
if config.SEARCH_TOOL_TYPE == "BochaAPI":
agent = DeepSearchAgent(config)
else:
agent = AnspireSearchAgent(config)
st.session_state.agent = agent
progress_bar.progress(10)
... ...
... ... @@ -111,7 +111,9 @@ CONFIG_KEYS = [
'KEYWORD_OPTIMIZER_BASE_URL',
'KEYWORD_OPTIMIZER_MODEL_NAME',
'TAVILY_API_KEY',
'BOCHA_WEB_SEARCH_API_KEY'
'SEARCH_TOOL_TYPE',
'BOCHA_WEB_SEARCH_API_KEY',
'ANSPIRE_API_KEY'
]
... ...
... ... @@ -10,7 +10,7 @@
from pathlib import Path
from pydantic_settings import BaseSettings
from pydantic import Field, ConfigDict
from typing import Optional
from typing import Optional, Literal
from loguru import logger
... ... @@ -79,10 +79,16 @@ class Settings(BaseSettings):
# ================== 网络工具配置 ====================
# Tavily API(申请地址:https://www.tavily.com/)
TAVILY_API_KEY: Optional[str] = Field(None, description="Tavily API(申请地址:https://www.tavily.com/)API密钥,用于Tavily网络搜索")
SEARCH_TOOL_TYPE: Literal["AnspireAPI", "BochaAPI"] = Field("AnspireAPI", description="网络搜索工具类型,支持BochaAPI或AnspireAPI两种,默认为AnspireAPI")
# Bocha API(申请地址:https://open.bochaai.com/)
BOCHA_BASE_URL: Optional[str] = Field("https://api.bocha.cn/v1/ai-search", description="Bocha AI 搜索BaseUrl或博查网页搜索BaseUrl")
BOCHA_WEB_SEARCH_API_KEY: Optional[str] = Field(None, description="Bocha API(申请地址:https://open.bochaai.com/)API密钥,用于Bocha搜索")
# Anspire AI Search API(申请地址:https://open.anspire.cn/)
ANSPIRE_BASE_URL: Optional[str] = Field("https://plugin.anspire.cn/api/ntsearch/search", description="Anspire AI 搜索BaseUrl")
ANSPIRE_API_KEY: Optional[str] = Field(None, description="Anspire AI Search API(申请地址:https://open.anspire.cn/)API密钥,用于Anspire搜索")
# ================== Insight Engine 搜索配置 ====================
DEFAULT_SEARCH_HOT_CONTENT_LIMIT: int = Field(100, description="热榜内容默认最大数")
... ...
... ... @@ -605,6 +605,15 @@
display: flex;
flex-direction: column;
margin-bottom: 12px;
transition: all 0.3s ease;
}
.config-field.hidden {
display: none;
opacity: 0;
height: 0;
margin-bottom: 0;
overflow: hidden;
}
.config-field-label {
... ... @@ -1821,8 +1830,15 @@
title: '外部检索工具',
subtitle: '联动搜索引擎、网站抓取等在线服务,两个都需配置',
fields: [
{
key: 'SEARCH_TOOL_TYPE',
label: '选择检索工具',
type: 'select',
options: ['BochaAPI', 'AnspireAPI']
},
{ key: 'TAVILY_API_KEY', label: 'Tavily API Key', type: 'password' },
{ key: 'BOCHA_WEB_SEARCH_API_KEY', label: 'Bocha API Key', type: 'password' }
{ key: 'BOCHA_WEB_SEARCH_API_KEY', label: 'Bocha API Key', type: 'password', condition: { key: 'SEARCH_TOOL_TYPE', value: 'BochaAPI' } },
{ key: 'ANSPIRE_API_KEY', label: 'Anspire API Key', type: 'password', condition: { key: 'SEARCH_TOOL_TYPE', value: 'AnspireAPI' } }
]
}
];
... ... @@ -2117,6 +2133,17 @@
const value = values[field.key] !== undefined ? values[field.key] : '';
const safeValue = escapeHtml(String(value || ''));
// 检查条件是否满足
let isVisible = true;
let hiddenClass = '';
if (field.condition) {
const conditionKey = field.condition.key;
const conditionValue = field.condition.value;
const currentValue = values[conditionKey];
isVisible = currentValue === conditionValue;
hiddenClass = isVisible ? '' : 'hidden';
}
let control;
if (field.type === 'select' && field.options) {
... ... @@ -2180,7 +2207,7 @@
}
return `
<label class="config-field">
<label class="config-field ${hiddenClass}" data-condition-key="${field.condition ? field.condition.key : ''}" data-condition-value="${field.condition ? field.condition.value : ''}">
<span class="config-field-label">${field.label}</span>
${control}
</label>
... ... @@ -2201,6 +2228,9 @@
container.innerHTML = sections;
// 不再需要每次调用 attachConfigPasswordToggles
// 事件委托已在页面初始化时设置
// 为所有 select 下拉框绑定事件,监听值变化并动态显示/隐藏条件字段
attachConfigConditionalLogic();
}
function attachConfigPasswordToggles() {
... ... @@ -2252,6 +2282,51 @@
container.dataset.passwordToggleAttached = 'true';
}
// 【新增】条件字段动态显示逻辑
function attachConfigConditionalLogic() {
const container = document.getElementById('configFormContainer');
if (!container) {
return;
}
// 防止重复绑定
if (container.dataset.conditionalLogicAttached === 'true') {
return;
}
// 监听所有 select 下拉框的变化
container.addEventListener('change', (event) => {
const select = event.target.closest('select.config-field-input');
if (!select) {
return;
}
const triggerKey = select.dataset.configKey;
const triggerValue = select.value;
// 更新所有依赖于这个字段的条件字段的显示状态
const conditionalFields = container.querySelectorAll('.config-field[data-condition-key]');
conditionalFields.forEach(field => {
const conditionKey = field.dataset.conditionKey;
const conditionValue = field.dataset.conditionValue;
// 检查这个条件字段是否依赖于当前改变的字段
if (conditionKey === triggerKey) {
if (triggerValue === conditionValue) {
// 显示字段
field.classList.remove('hidden');
} else {
// 隐藏字段
field.classList.add('hidden');
}
}
});
});
// 标记已绑定,防止重复
container.dataset.conditionalLogicAttached = 'true';
}
function collectConfigUpdates() {
const inputs = document.querySelectorAll('#configFormContainer [data-config-key]');
const updates = {};
... ...