@shirlytaylor73/smart-search 0.2.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +188 -0
- package/README.zh-CN.md +180 -0
- package/npm/bin/smart-search.js +68 -0
- package/npm/scripts/postinstall.js +87 -0
- package/npm/scripts/resolve-prerelease-version.js +108 -0
- package/npm/scripts/set-package-version.js +35 -0
- package/npm/scripts/sync-python-version.js +22 -0
- package/npm/scripts/test-wrapper-repair.js +137 -0
- package/npm/scripts/test.js +76 -0
- package/package.json +42 -0
- package/pyproject.toml +36 -0
- package/skills/smart-search-cli/SKILL.md +41 -0
- package/skills/smart-search-cli/agents/openai.yaml +3 -0
- package/skills/smart-search-cli/references/cli-contract.md +7 -0
- package/skills/smart-search-cli/references/cli-core.md +5 -0
- package/skills/smart-search-cli/references/command-patterns.md +12 -0
- package/skills/smart-search-cli/references/provider-routing.md +3 -0
- package/skills/smart-search-cli/references/regression-release.md +28 -0
- package/skills/smart-search-cli/references/setup-config.md +3 -0
- package/src/smart_search/__init__.py +1 -0
- package/src/smart_search/assets/skills/smart-search-cli/SKILL.md +41 -0
- package/src/smart_search/assets/skills/smart-search-cli/agents/openai.yaml +3 -0
- package/src/smart_search/assets/skills/smart-search-cli/references/cli-contract.md +7 -0
- package/src/smart_search/assets/skills/smart-search-cli/references/cli-core.md +5 -0
- package/src/smart_search/assets/skills/smart-search-cli/references/command-patterns.md +12 -0
- package/src/smart_search/assets/skills/smart-search-cli/references/provider-routing.md +3 -0
- package/src/smart_search/assets/skills/smart-search-cli/references/regression-release.md +28 -0
- package/src/smart_search/assets/skills/smart-search-cli/references/setup-config.md +3 -0
- package/src/smart_search/cli.py +3118 -0
- package/src/smart_search/config.py +809 -0
- package/src/smart_search/embedding_presets.py +40 -0
- package/src/smart_search/intent_router.py +757 -0
- package/src/smart_search/logger.py +43 -0
- package/src/smart_search/providers/__init__.py +20 -0
- package/src/smart_search/providers/base.py +41 -0
- package/src/smart_search/providers/context7.py +141 -0
- package/src/smart_search/providers/exa.py +206 -0
- package/src/smart_search/providers/jina.py +136 -0
- package/src/smart_search/providers/openai_compatible.py +541 -0
- package/src/smart_search/providers/xai_responses.py +117 -0
- package/src/smart_search/providers/zhipu.py +143 -0
- package/src/smart_search/providers/zhipu_mcp.py +230 -0
- package/src/smart_search/service.py +3445 -0
- package/src/smart_search/skill_installer.py +342 -0
- package/src/smart_search/sources.py +429 -0
- package/src/smart_search/utils.py +220 -0
|
@@ -0,0 +1,809 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
class Config:
|
|
7
|
+
_instance = None
|
|
8
|
+
_SETUP_COMMAND = (
|
|
9
|
+
"Run `smart-search setup`, or configure XAI_API_KEY and/or "
|
|
10
|
+
"OPENAI_COMPATIBLE_API_URL plus OPENAI_COMPATIBLE_API_KEY, then run "
|
|
11
|
+
"`smart-search doctor --format json`."
|
|
12
|
+
)
|
|
13
|
+
_DEFAULT_MODEL = "grok-4-fast"
|
|
14
|
+
_DEFAULT_XAI_TOOLS = "web_search,x_search"
|
|
15
|
+
_DEFAULT_VALIDATION_LEVEL = "balanced"
|
|
16
|
+
_DEFAULT_FALLBACK_MODE = "auto"
|
|
17
|
+
_DEFAULT_MINIMUM_PROFILE = "standard"
|
|
18
|
+
_DEFAULT_INTENT_ROUTER_MODE = "hybrid"
|
|
19
|
+
_DEFAULT_INTENT_ROUTER_TIMEOUT_SECONDS = "8"
|
|
20
|
+
_DEFAULT_INTENT_EMBEDDING_THRESHOLD = "0.74"
|
|
21
|
+
_DEFAULT_INTENT_EMBEDDING_MARGIN = "0.05"
|
|
22
|
+
_ALLOWED_XAI_TOOLS = {"web_search", "x_search"}
|
|
23
|
+
_ALLOWED_VALIDATION_LEVELS = {"fast", "balanced", "strict"}
|
|
24
|
+
_ALLOWED_FALLBACK_MODES = {"auto", "off"}
|
|
25
|
+
_ALLOWED_MINIMUM_PROFILES = {"standard", "off"}
|
|
26
|
+
_ALLOWED_INTENT_ROUTER_MODES = {"hybrid", "rules", "off"}
|
|
27
|
+
_CONFIG_KEYS = {
|
|
28
|
+
"XAI_API_URL",
|
|
29
|
+
"XAI_API_KEY",
|
|
30
|
+
"XAI_MODEL",
|
|
31
|
+
"XAI_TOOLS",
|
|
32
|
+
"OPENAI_COMPATIBLE_API_URL",
|
|
33
|
+
"OPENAI_COMPATIBLE_API_KEY",
|
|
34
|
+
"OPENAI_COMPATIBLE_MODEL",
|
|
35
|
+
"OPENAI_COMPATIBLE_FALLBACK_MODELS",
|
|
36
|
+
"OPENAI_COMPATIBLE_STREAM",
|
|
37
|
+
"SMART_SEARCH_VALIDATION_LEVEL",
|
|
38
|
+
"SMART_SEARCH_FALLBACK_MODE",
|
|
39
|
+
"SMART_SEARCH_MINIMUM_PROFILE",
|
|
40
|
+
"SMART_SEARCH_OPERATION_CONFIG",
|
|
41
|
+
"SMART_SEARCH_INTENT_ROUTER",
|
|
42
|
+
"INTENT_EMBEDDING_API_URL",
|
|
43
|
+
"INTENT_EMBEDDING_API_KEY",
|
|
44
|
+
"INTENT_EMBEDDING_MODEL",
|
|
45
|
+
"INTENT_EMBEDDING_THRESHOLD",
|
|
46
|
+
"INTENT_EMBEDDING_MARGIN",
|
|
47
|
+
"INTENT_CLASSIFIER_API_URL",
|
|
48
|
+
"INTENT_CLASSIFIER_API_KEY",
|
|
49
|
+
"INTENT_CLASSIFIER_MODEL",
|
|
50
|
+
"INTENT_ROUTER_TIMEOUT_SECONDS",
|
|
51
|
+
"EXA_API_KEY",
|
|
52
|
+
"EXA_BASE_URL",
|
|
53
|
+
"EXA_TIMEOUT_SECONDS",
|
|
54
|
+
"CONTEXT7_API_KEY",
|
|
55
|
+
"CONTEXT7_BASE_URL",
|
|
56
|
+
"CONTEXT7_TIMEOUT_SECONDS",
|
|
57
|
+
"ZHIPU_API_KEY",
|
|
58
|
+
"ZHIPU_API_URL",
|
|
59
|
+
"ZHIPU_SEARCH_ENGINE",
|
|
60
|
+
"ZHIPU_TIMEOUT_SECONDS",
|
|
61
|
+
"ZHIPU_MCP_API_KEY",
|
|
62
|
+
"ZHIPU_MCP_SEARCH_API_URL",
|
|
63
|
+
"ZHIPU_MCP_READER_API_URL",
|
|
64
|
+
"ZHIPU_MCP_ZREAD_API_URL",
|
|
65
|
+
"ZHIPU_MCP_TIMEOUT_SECONDS",
|
|
66
|
+
"JINA_API_KEY",
|
|
67
|
+
"JINA_READER_API_URL",
|
|
68
|
+
"JINA_RESPOND_WITH",
|
|
69
|
+
"JINA_TIMEOUT_SECONDS",
|
|
70
|
+
"TAVILY_API_KEY",
|
|
71
|
+
"TAVILY_API_URL",
|
|
72
|
+
"TAVILY_ENABLED",
|
|
73
|
+
"TAVILY_TIMEOUT_SECONDS",
|
|
74
|
+
"FIRECRAWL_API_KEY",
|
|
75
|
+
"FIRECRAWL_API_URL",
|
|
76
|
+
"SMART_SEARCH_DEBUG",
|
|
77
|
+
"SMART_SEARCH_LOG_LEVEL",
|
|
78
|
+
"SMART_SEARCH_LOG_DIR",
|
|
79
|
+
"SMART_SEARCH_RETRY_MAX_ATTEMPTS",
|
|
80
|
+
"SMART_SEARCH_RETRY_MULTIPLIER",
|
|
81
|
+
"SMART_SEARCH_RETRY_MAX_WAIT",
|
|
82
|
+
"SMART_SEARCH_OUTPUT_CLEANUP",
|
|
83
|
+
"SMART_SEARCH_LOG_TO_FILE",
|
|
84
|
+
"SSL_VERIFY",
|
|
85
|
+
}
|
|
86
|
+
_LEGACY_CONFIG_KEYS: dict[str, str] = {}
|
|
87
|
+
|
|
88
|
+
def __new__(cls):
|
|
89
|
+
if cls._instance is None:
|
|
90
|
+
cls._instance = super().__new__(cls)
|
|
91
|
+
cls._instance._config_file = None
|
|
92
|
+
cls._instance._config_dir_source = None
|
|
93
|
+
cls._instance._cached_model = None
|
|
94
|
+
return cls._instance
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def _default_config_dir() -> Path:
|
|
98
|
+
if sys.platform.startswith("win"):
|
|
99
|
+
local_appdata = os.getenv("LOCALAPPDATA")
|
|
100
|
+
if local_appdata:
|
|
101
|
+
return Path(local_appdata).expanduser() / "smart-search"
|
|
102
|
+
return Path.home() / ".config" / "smart-search"
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def _legacy_windows_config_dir() -> Path:
|
|
106
|
+
return Path.home() / ".config" / "smart-search"
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def _config_dir_override_value() -> str:
|
|
110
|
+
return os.getenv("SMART_SEARCH_CONFIG_DIR") or ""
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def _same_config_dir(left: Path, right: Path) -> bool:
|
|
114
|
+
left_text = os.path.abspath(os.path.expanduser(str(left)))
|
|
115
|
+
right_text = os.path.abspath(os.path.expanduser(str(right)))
|
|
116
|
+
if sys.platform.startswith("win"):
|
|
117
|
+
left_text = left_text.replace("/", "\\").rstrip("\\").lower()
|
|
118
|
+
right_text = right_text.replace("/", "\\").rstrip("\\").lower()
|
|
119
|
+
else:
|
|
120
|
+
left_text = left_text.rstrip("/")
|
|
121
|
+
right_text = right_text.rstrip("/")
|
|
122
|
+
return left_text == right_text
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def _config_dir_override_matches_default(cls) -> bool:
|
|
126
|
+
env_dir = cls._config_dir_override_value()
|
|
127
|
+
if not env_dir:
|
|
128
|
+
return False
|
|
129
|
+
return cls._same_config_dir(Path(env_dir).expanduser(), cls._default_config_dir())
|
|
130
|
+
|
|
131
|
+
@staticmethod
|
|
132
|
+
def _resolve_config_dir() -> tuple[Path, str]:
|
|
133
|
+
env_dir = os.getenv("SMART_SEARCH_CONFIG_DIR")
|
|
134
|
+
if env_dir:
|
|
135
|
+
return Path(env_dir).expanduser(), "environment"
|
|
136
|
+
default_dir = Config._default_config_dir()
|
|
137
|
+
if sys.platform.startswith("win"):
|
|
138
|
+
legacy_dir = Config._legacy_windows_config_dir()
|
|
139
|
+
if legacy_dir != default_dir and not (default_dir / "config.json").exists() and (legacy_dir / "config.json").exists():
|
|
140
|
+
return legacy_dir, "legacy_windows_home"
|
|
141
|
+
return default_dir, "default"
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def _safe_mkdir(p: Path) -> bool:
|
|
145
|
+
try:
|
|
146
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
return True
|
|
148
|
+
except (PermissionError, OSError):
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def config_file(self) -> Path:
|
|
153
|
+
if self._config_file is None:
|
|
154
|
+
config_dir, config_dir_source = self._resolve_config_dir()
|
|
155
|
+
ok = self._safe_mkdir(config_dir)
|
|
156
|
+
if config_dir_source == "default" and not ok:
|
|
157
|
+
cwd_dir = Path.cwd() / ".smart-search"
|
|
158
|
+
if self._safe_mkdir(cwd_dir):
|
|
159
|
+
config_dir = cwd_dir
|
|
160
|
+
config_dir_source = "cwd_fallback"
|
|
161
|
+
self._config_file = config_dir / "config.json"
|
|
162
|
+
self._config_dir_source = config_dir_source
|
|
163
|
+
return self._config_file
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def config_dir_source(self) -> str:
|
|
167
|
+
if self._config_file is None:
|
|
168
|
+
_ = self.config_file
|
|
169
|
+
return self._config_dir_source or "override"
|
|
170
|
+
|
|
171
|
+
def _load_config_file(self) -> dict:
|
|
172
|
+
try:
|
|
173
|
+
with open(self.config_file, 'r', encoding='utf-8') as f:
|
|
174
|
+
data = json.load(f)
|
|
175
|
+
return data if isinstance(data, dict) else {}
|
|
176
|
+
except (FileNotFoundError, PermissionError, OSError, json.JSONDecodeError):
|
|
177
|
+
return {}
|
|
178
|
+
|
|
179
|
+
def _save_config_file(self, config_data: dict) -> None:
|
|
180
|
+
try:
|
|
181
|
+
with open(self.config_file, 'w', encoding='utf-8') as f:
|
|
182
|
+
json.dump(config_data, f, ensure_ascii=False, indent=2)
|
|
183
|
+
except (IOError, PermissionError, OSError) as e:
|
|
184
|
+
hint = " (sandbox/CI 下可设 SMART_SEARCH_CONFIG_DIR 指向可写目录)" if isinstance(e, PermissionError) else ""
|
|
185
|
+
raise ValueError(f"无法保存配置文件: {str(e)}{hint}")
|
|
186
|
+
|
|
187
|
+
def _get_config_value(self, key: str, default: str | None = None) -> str | None:
|
|
188
|
+
env_value = os.getenv(key)
|
|
189
|
+
if env_value is not None:
|
|
190
|
+
return env_value
|
|
191
|
+
|
|
192
|
+
data = self._load_config_file()
|
|
193
|
+
value = data.get(key)
|
|
194
|
+
if value is None:
|
|
195
|
+
legacy_key = next((old for old, new in self._LEGACY_CONFIG_KEYS.items() if new == key), None)
|
|
196
|
+
if legacy_key:
|
|
197
|
+
value = data.get(legacy_key)
|
|
198
|
+
if value is None:
|
|
199
|
+
return default
|
|
200
|
+
return str(value)
|
|
201
|
+
|
|
202
|
+
def get_saved_config(self, masked: bool = True) -> dict:
|
|
203
|
+
data = self._load_config_file()
|
|
204
|
+
normalized: dict[str, str] = {}
|
|
205
|
+
for old_key, new_key in self._LEGACY_CONFIG_KEYS.items():
|
|
206
|
+
if old_key in data and new_key not in data:
|
|
207
|
+
normalized[new_key] = str(data[old_key])
|
|
208
|
+
for key, value in data.items():
|
|
209
|
+
if key in self._CONFIG_KEYS and value is not None:
|
|
210
|
+
normalized[key] = str(value)
|
|
211
|
+
if not masked:
|
|
212
|
+
return normalized
|
|
213
|
+
return {key: self._mask_if_secret(key, value) for key, value in normalized.items()}
|
|
214
|
+
|
|
215
|
+
def get_config_source(self, key: str) -> str:
|
|
216
|
+
if os.getenv(key) is not None:
|
|
217
|
+
return "environment"
|
|
218
|
+
data = self._load_config_file()
|
|
219
|
+
if key in data:
|
|
220
|
+
return "config_file"
|
|
221
|
+
legacy_key = next((old for old, new in self._LEGACY_CONFIG_KEYS.items() if new == key), None)
|
|
222
|
+
if legacy_key and legacy_key in data:
|
|
223
|
+
return "config_file"
|
|
224
|
+
return "default"
|
|
225
|
+
|
|
226
|
+
def get_config_sources(self) -> dict[str, str]:
|
|
227
|
+
return {key: self.get_config_source(key) for key in sorted(self._CONFIG_KEYS)}
|
|
228
|
+
|
|
229
|
+
def set_config_value(self, key: str, value: str) -> None:
|
|
230
|
+
key = key.strip().upper()
|
|
231
|
+
if key not in self._CONFIG_KEYS:
|
|
232
|
+
raise ValueError(f"Unsupported config key: {key}")
|
|
233
|
+
config_data = self._load_config_file()
|
|
234
|
+
config_data[key] = value
|
|
235
|
+
self._save_config_file(config_data)
|
|
236
|
+
if key in {
|
|
237
|
+
"XAI_API_URL",
|
|
238
|
+
"XAI_API_KEY",
|
|
239
|
+
"XAI_MODEL",
|
|
240
|
+
"XAI_TOOLS",
|
|
241
|
+
"OPENAI_COMPATIBLE_API_URL",
|
|
242
|
+
"OPENAI_COMPATIBLE_API_KEY",
|
|
243
|
+
"OPENAI_COMPATIBLE_MODEL",
|
|
244
|
+
"OPENAI_COMPATIBLE_FALLBACK_MODELS",
|
|
245
|
+
"OPENAI_COMPATIBLE_STREAM",
|
|
246
|
+
"SMART_SEARCH_VALIDATION_LEVEL",
|
|
247
|
+
"SMART_SEARCH_FALLBACK_MODE",
|
|
248
|
+
"SMART_SEARCH_MINIMUM_PROFILE",
|
|
249
|
+
"SMART_SEARCH_INTENT_ROUTER",
|
|
250
|
+
}:
|
|
251
|
+
self._cached_model = None
|
|
252
|
+
|
|
253
|
+
def unset_config_value(self, key: str) -> None:
|
|
254
|
+
key = key.strip().upper()
|
|
255
|
+
if key not in self._CONFIG_KEYS:
|
|
256
|
+
raise ValueError(f"Unsupported config key: {key}")
|
|
257
|
+
config_data = self._load_config_file()
|
|
258
|
+
config_data.pop(key, None)
|
|
259
|
+
for old_key, new_key in self._LEGACY_CONFIG_KEYS.items():
|
|
260
|
+
if new_key == key:
|
|
261
|
+
config_data.pop(old_key, None)
|
|
262
|
+
self._save_config_file(config_data)
|
|
263
|
+
if key in {
|
|
264
|
+
"XAI_API_URL",
|
|
265
|
+
"XAI_API_KEY",
|
|
266
|
+
"XAI_MODEL",
|
|
267
|
+
"XAI_TOOLS",
|
|
268
|
+
"OPENAI_COMPATIBLE_API_URL",
|
|
269
|
+
"OPENAI_COMPATIBLE_API_KEY",
|
|
270
|
+
"OPENAI_COMPATIBLE_MODEL",
|
|
271
|
+
"OPENAI_COMPATIBLE_FALLBACK_MODELS",
|
|
272
|
+
"OPENAI_COMPATIBLE_STREAM",
|
|
273
|
+
"SMART_SEARCH_VALIDATION_LEVEL",
|
|
274
|
+
"SMART_SEARCH_FALLBACK_MODE",
|
|
275
|
+
"SMART_SEARCH_MINIMUM_PROFILE",
|
|
276
|
+
"SMART_SEARCH_INTENT_ROUTER",
|
|
277
|
+
}:
|
|
278
|
+
self._cached_model = None
|
|
279
|
+
|
|
280
|
+
def config_path_info(self) -> dict:
|
|
281
|
+
return {
|
|
282
|
+
"ok": True,
|
|
283
|
+
"config_file": str(self.config_file),
|
|
284
|
+
"config_dir": str(self.config_file.parent),
|
|
285
|
+
"config_dir_source": self.config_dir_source,
|
|
286
|
+
"default_config_file": str(self._default_config_dir() / "config.json"),
|
|
287
|
+
"legacy_windows_config_file": str(self._legacy_windows_config_dir() / "config.json") if sys.platform.startswith("win") else "",
|
|
288
|
+
"legacy_windows_config_exists": (self._legacy_windows_config_dir() / "config.json").exists() if sys.platform.startswith("win") else False,
|
|
289
|
+
"config_dir_override_value": self._config_dir_override_value(),
|
|
290
|
+
"config_dir_override_matches_default": self._config_dir_override_matches_default(),
|
|
291
|
+
"exists": self.config_file.exists(),
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
@property
|
|
295
|
+
def debug_enabled(self) -> bool:
|
|
296
|
+
return (self._get_config_value("SMART_SEARCH_DEBUG", "false") or "false").lower() in ("true", "1", "yes")
|
|
297
|
+
|
|
298
|
+
@property
|
|
299
|
+
def retry_max_attempts(self) -> int:
|
|
300
|
+
return int(self._get_config_value("SMART_SEARCH_RETRY_MAX_ATTEMPTS", "3") or "3")
|
|
301
|
+
|
|
302
|
+
@property
|
|
303
|
+
def retry_multiplier(self) -> float:
|
|
304
|
+
return float(self._get_config_value("SMART_SEARCH_RETRY_MULTIPLIER", "1") or "1")
|
|
305
|
+
|
|
306
|
+
@property
|
|
307
|
+
def retry_max_wait(self) -> int:
|
|
308
|
+
return int(self._get_config_value("SMART_SEARCH_RETRY_MAX_WAIT", "10") or "10")
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def xai_api_url(self) -> str:
|
|
312
|
+
return self._get_config_value("XAI_API_URL", "https://api.x.ai/v1") or "https://api.x.ai/v1"
|
|
313
|
+
|
|
314
|
+
@property
|
|
315
|
+
def xai_api_key(self) -> str | None:
|
|
316
|
+
return self._get_config_value("XAI_API_KEY")
|
|
317
|
+
|
|
318
|
+
@property
|
|
319
|
+
def xai_model(self) -> str:
|
|
320
|
+
return self._get_config_value("XAI_MODEL") or self._base_model_value()
|
|
321
|
+
|
|
322
|
+
@property
|
|
323
|
+
def xai_tools_raw(self) -> str:
|
|
324
|
+
return self._get_config_value("XAI_TOOLS", self._DEFAULT_XAI_TOOLS) or self._DEFAULT_XAI_TOOLS
|
|
325
|
+
|
|
326
|
+
@property
|
|
327
|
+
def openai_compatible_api_url(self) -> str | None:
|
|
328
|
+
return self._get_config_value("OPENAI_COMPATIBLE_API_URL")
|
|
329
|
+
|
|
330
|
+
@property
|
|
331
|
+
def openai_compatible_api_key(self) -> str | None:
|
|
332
|
+
return self._get_config_value("OPENAI_COMPATIBLE_API_KEY")
|
|
333
|
+
|
|
334
|
+
@property
|
|
335
|
+
def openai_compatible_model(self) -> str:
|
|
336
|
+
model = self._get_config_value("OPENAI_COMPATIBLE_MODEL") or self._base_model_value()
|
|
337
|
+
return self.apply_model_suffix_for_url(model, self.openai_compatible_api_url or "")
|
|
338
|
+
|
|
339
|
+
@property
|
|
340
|
+
def openai_compatible_fallback_models(self) -> list[str]:
|
|
341
|
+
raw = self._get_config_value("OPENAI_COMPATIBLE_FALLBACK_MODELS", "") or ""
|
|
342
|
+
models: list[str] = []
|
|
343
|
+
seen: set[str] = set()
|
|
344
|
+
api_url = self.openai_compatible_api_url or ""
|
|
345
|
+
primary = self.openai_compatible_model
|
|
346
|
+
for item in raw.split(","):
|
|
347
|
+
model = item.strip()
|
|
348
|
+
if not model:
|
|
349
|
+
continue
|
|
350
|
+
model = self.apply_model_suffix_for_url(model, api_url)
|
|
351
|
+
if model == primary or model in seen:
|
|
352
|
+
continue
|
|
353
|
+
seen.add(model)
|
|
354
|
+
models.append(model)
|
|
355
|
+
return models
|
|
356
|
+
|
|
357
|
+
@property
|
|
358
|
+
def openai_compatible_stream(self) -> bool:
|
|
359
|
+
return (self._get_config_value("OPENAI_COMPATIBLE_STREAM", "false") or "false").lower() in ("true", "1", "yes")
|
|
360
|
+
|
|
361
|
+
def parse_xai_tools(self, raw: str | None = None) -> list[str]:
|
|
362
|
+
raw = raw or self.xai_tools_raw
|
|
363
|
+
tools: list[str] = []
|
|
364
|
+
invalid: list[str] = []
|
|
365
|
+
seen: set[str] = set()
|
|
366
|
+
for item in raw.split(","):
|
|
367
|
+
tool = item.strip().lower()
|
|
368
|
+
if not tool:
|
|
369
|
+
continue
|
|
370
|
+
if tool not in self._ALLOWED_XAI_TOOLS:
|
|
371
|
+
invalid.append(tool)
|
|
372
|
+
continue
|
|
373
|
+
if tool not in seen:
|
|
374
|
+
seen.add(tool)
|
|
375
|
+
tools.append(tool)
|
|
376
|
+
if invalid:
|
|
377
|
+
allowed = ", ".join(sorted(self._ALLOWED_XAI_TOOLS))
|
|
378
|
+
invalid_text = ", ".join(invalid)
|
|
379
|
+
raise ValueError(f"Invalid XAI_TOOLS: {invalid_text}. Supported values: {allowed}")
|
|
380
|
+
return tools
|
|
381
|
+
|
|
382
|
+
def _validated_enum(self, key: str, default: str, allowed: set[str]) -> str:
|
|
383
|
+
value = (self._get_config_value(key, default) or default).strip().lower()
|
|
384
|
+
if value not in allowed:
|
|
385
|
+
allowed_text = ", ".join(sorted(allowed))
|
|
386
|
+
raise ValueError(f"Invalid {key}: {value}. Supported values: {allowed_text}")
|
|
387
|
+
return value
|
|
388
|
+
|
|
389
|
+
def _enum_info(self, key: str, default: str, allowed: set[str]) -> tuple[str, str]:
|
|
390
|
+
value = (self._get_config_value(key, default) or default).strip().lower()
|
|
391
|
+
if value not in allowed:
|
|
392
|
+
allowed_text = ", ".join(sorted(allowed))
|
|
393
|
+
return value, f"Invalid {key}: {value}. Supported values: {allowed_text}"
|
|
394
|
+
return value, ""
|
|
395
|
+
|
|
396
|
+
def _float_value(self, key: str, default: str) -> float:
|
|
397
|
+
value = self._get_config_value(key, default) or default
|
|
398
|
+
try:
|
|
399
|
+
return float(value)
|
|
400
|
+
except (TypeError, ValueError):
|
|
401
|
+
raise ValueError(f"Invalid {key}: {value}. Expected a number.")
|
|
402
|
+
|
|
403
|
+
def _float_info(self, key: str, default: str) -> tuple[float, str]:
|
|
404
|
+
try:
|
|
405
|
+
return self._float_value(key, default), ""
|
|
406
|
+
except ValueError as e:
|
|
407
|
+
return float(default), str(e)
|
|
408
|
+
|
|
409
|
+
def _bounded_float_value(self, key: str, default: str, minimum: float, maximum: float) -> float:
|
|
410
|
+
value = self._float_value(key, default)
|
|
411
|
+
if value < minimum or value > maximum:
|
|
412
|
+
raise ValueError(f"Invalid {key}: {value}. Expected a number between {minimum:g} and {maximum:g}.")
|
|
413
|
+
return value
|
|
414
|
+
|
|
415
|
+
def _bounded_float_info(self, key: str, default: str, minimum: float, maximum: float) -> tuple[float, str]:
|
|
416
|
+
try:
|
|
417
|
+
return self._bounded_float_value(key, default, minimum, maximum), ""
|
|
418
|
+
except ValueError as e:
|
|
419
|
+
return float(default), str(e)
|
|
420
|
+
|
|
421
|
+
@property
|
|
422
|
+
def validation_level(self) -> str:
|
|
423
|
+
return self._validated_enum(
|
|
424
|
+
"SMART_SEARCH_VALIDATION_LEVEL",
|
|
425
|
+
self._DEFAULT_VALIDATION_LEVEL,
|
|
426
|
+
self._ALLOWED_VALIDATION_LEVELS,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
@property
|
|
430
|
+
def fallback_mode(self) -> str:
|
|
431
|
+
return self._validated_enum(
|
|
432
|
+
"SMART_SEARCH_FALLBACK_MODE",
|
|
433
|
+
self._DEFAULT_FALLBACK_MODE,
|
|
434
|
+
self._ALLOWED_FALLBACK_MODES,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
@property
|
|
438
|
+
def minimum_profile(self) -> str:
|
|
439
|
+
return self._validated_enum(
|
|
440
|
+
"SMART_SEARCH_MINIMUM_PROFILE",
|
|
441
|
+
self._DEFAULT_MINIMUM_PROFILE,
|
|
442
|
+
self._ALLOWED_MINIMUM_PROFILES,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
@property
|
|
446
|
+
def intent_router_mode(self) -> str:
|
|
447
|
+
return self._validated_enum(
|
|
448
|
+
"SMART_SEARCH_INTENT_ROUTER",
|
|
449
|
+
self._DEFAULT_INTENT_ROUTER_MODE,
|
|
450
|
+
self._ALLOWED_INTENT_ROUTER_MODES,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
@property
|
|
454
|
+
def intent_embedding_api_url(self) -> str:
|
|
455
|
+
return self._get_config_value("INTENT_EMBEDDING_API_URL", "") or ""
|
|
456
|
+
|
|
457
|
+
@property
|
|
458
|
+
def intent_embedding_api_key(self) -> str | None:
|
|
459
|
+
return self._get_config_value("INTENT_EMBEDDING_API_KEY")
|
|
460
|
+
|
|
461
|
+
@property
|
|
462
|
+
def intent_embedding_model(self) -> str:
|
|
463
|
+
return self._get_config_value("INTENT_EMBEDDING_MODEL", "") or ""
|
|
464
|
+
|
|
465
|
+
@property
|
|
466
|
+
def intent_embedding_threshold(self) -> float:
|
|
467
|
+
return self._bounded_float_value("INTENT_EMBEDDING_THRESHOLD", self._DEFAULT_INTENT_EMBEDDING_THRESHOLD, 0.0, 1.0)
|
|
468
|
+
|
|
469
|
+
@property
|
|
470
|
+
def intent_embedding_margin(self) -> float:
|
|
471
|
+
return self._bounded_float_value("INTENT_EMBEDDING_MARGIN", self._DEFAULT_INTENT_EMBEDDING_MARGIN, 0.0, 1.0)
|
|
472
|
+
|
|
473
|
+
@property
|
|
474
|
+
def intent_classifier_api_url(self) -> str:
|
|
475
|
+
return self._get_config_value("INTENT_CLASSIFIER_API_URL", "") or ""
|
|
476
|
+
|
|
477
|
+
@property
|
|
478
|
+
def intent_classifier_api_key(self) -> str | None:
|
|
479
|
+
return self._get_config_value("INTENT_CLASSIFIER_API_KEY")
|
|
480
|
+
|
|
481
|
+
@property
|
|
482
|
+
def intent_classifier_model(self) -> str:
|
|
483
|
+
return self._get_config_value("INTENT_CLASSIFIER_MODEL", "") or ""
|
|
484
|
+
|
|
485
|
+
@property
|
|
486
|
+
def intent_router_timeout(self) -> float:
|
|
487
|
+
return self._float_value("INTENT_ROUTER_TIMEOUT_SECONDS", self._DEFAULT_INTENT_ROUTER_TIMEOUT_SECONDS)
|
|
488
|
+
|
|
489
|
+
def _csv_values(self, key: str) -> list[str]:
|
|
490
|
+
raw = self._get_config_value(key, "") or ""
|
|
491
|
+
values: list[str] = []
|
|
492
|
+
seen: set[str] = set()
|
|
493
|
+
for item in raw.split(","):
|
|
494
|
+
value = item.strip().lower()
|
|
495
|
+
if value and value not in seen:
|
|
496
|
+
seen.add(value)
|
|
497
|
+
values.append(value)
|
|
498
|
+
return values
|
|
499
|
+
|
|
500
|
+
@property
|
|
501
|
+
def operation_config(self) -> dict[str, dict]:
|
|
502
|
+
raw = self._get_config_value("SMART_SEARCH_OPERATION_CONFIG", "{}") or "{}"
|
|
503
|
+
try:
|
|
504
|
+
value = json.loads(raw)
|
|
505
|
+
except json.JSONDecodeError as exc:
|
|
506
|
+
raise ValueError(f"Invalid SMART_SEARCH_OPERATION_CONFIG: {exc}") from exc
|
|
507
|
+
if not isinstance(value, dict):
|
|
508
|
+
raise ValueError("Invalid SMART_SEARCH_OPERATION_CONFIG: expected a JSON object")
|
|
509
|
+
return {str(key): item for key, item in value.items() if isinstance(item, dict)}
|
|
510
|
+
|
|
511
|
+
@property
|
|
512
|
+
def tavily_enabled(self) -> bool:
|
|
513
|
+
return (self._get_config_value("TAVILY_ENABLED", "true") or "true").lower() in ("true", "1", "yes")
|
|
514
|
+
|
|
515
|
+
@property
|
|
516
|
+
def tavily_api_url(self) -> str:
|
|
517
|
+
return self._get_config_value("TAVILY_API_URL", "https://api.tavily.com") or "https://api.tavily.com"
|
|
518
|
+
|
|
519
|
+
@property
|
|
520
|
+
def tavily_api_key(self) -> str | None:
|
|
521
|
+
return self._get_config_value("TAVILY_API_KEY")
|
|
522
|
+
|
|
523
|
+
@property
|
|
524
|
+
def tavily_timeout(self) -> float:
|
|
525
|
+
return float(self._get_config_value("TAVILY_TIMEOUT_SECONDS", "30") or "30")
|
|
526
|
+
|
|
527
|
+
@property
|
|
528
|
+
def firecrawl_api_url(self) -> str:
|
|
529
|
+
return self._get_config_value("FIRECRAWL_API_URL", "https://api.firecrawl.dev/v2") or "https://api.firecrawl.dev/v2"
|
|
530
|
+
|
|
531
|
+
@property
|
|
532
|
+
def firecrawl_api_key(self) -> str | None:
|
|
533
|
+
return self._get_config_value("FIRECRAWL_API_KEY")
|
|
534
|
+
|
|
535
|
+
@property
|
|
536
|
+
def log_level(self) -> str:
|
|
537
|
+
return (self._get_config_value("SMART_SEARCH_LOG_LEVEL", "INFO") or "INFO").upper()
|
|
538
|
+
|
|
539
|
+
@property
|
|
540
|
+
def log_dir(self) -> Path:
|
|
541
|
+
log_dir_str = self.log_dir_config_value
|
|
542
|
+
log_dir = Path(log_dir_str)
|
|
543
|
+
if log_dir.is_absolute():
|
|
544
|
+
return log_dir
|
|
545
|
+
|
|
546
|
+
return self.config_file.parent / log_dir
|
|
547
|
+
|
|
548
|
+
@property
|
|
549
|
+
def log_dir_config_value(self) -> str:
|
|
550
|
+
return self._get_config_value("SMART_SEARCH_LOG_DIR", "logs") or "logs"
|
|
551
|
+
|
|
552
|
+
@staticmethod
|
|
553
|
+
def apply_model_suffix_for_url(model: str, api_url: str) -> str:
|
|
554
|
+
if "openrouter" in api_url and ":online" not in model:
|
|
555
|
+
return f"{model}:online"
|
|
556
|
+
return model
|
|
557
|
+
|
|
558
|
+
def _base_model_value(self) -> str:
|
|
559
|
+
return self._DEFAULT_MODEL
|
|
560
|
+
|
|
561
|
+
@staticmethod
|
|
562
|
+
def _mask_api_key(key: str) -> str:
|
|
563
|
+
if not key or len(key) <= 8:
|
|
564
|
+
return "***"
|
|
565
|
+
return f"{key[:4]}{'*' * (len(key) - 8)}{key[-4:]}"
|
|
566
|
+
|
|
567
|
+
@classmethod
|
|
568
|
+
def _mask_if_secret(cls, key: str, value: str) -> str:
|
|
569
|
+
if "KEY" in key or "TOKEN" in key or "SECRET" in key:
|
|
570
|
+
return cls._mask_api_key(value)
|
|
571
|
+
return value
|
|
572
|
+
|
|
573
|
+
@property
|
|
574
|
+
def output_cleanup_enabled(self) -> bool:
|
|
575
|
+
return (self._get_config_value("SMART_SEARCH_OUTPUT_CLEANUP", "true") or "true").lower() in ("true", "1", "yes")
|
|
576
|
+
|
|
577
|
+
@property
|
|
578
|
+
def log_to_file_enabled(self) -> bool:
|
|
579
|
+
return (self._get_config_value("SMART_SEARCH_LOG_TO_FILE", "false") or "false").lower() in ("true", "1", "yes")
|
|
580
|
+
|
|
581
|
+
@property
|
|
582
|
+
def ssl_verify_enabled(self) -> bool:
|
|
583
|
+
return (self._get_config_value("SSL_VERIFY", "true") or "true").lower() not in ("false", "0", "no")
|
|
584
|
+
|
|
585
|
+
@property
|
|
586
|
+
def exa_api_key(self) -> str | None:
|
|
587
|
+
return self._get_config_value("EXA_API_KEY")
|
|
588
|
+
|
|
589
|
+
@property
|
|
590
|
+
def exa_base_url(self) -> str:
|
|
591
|
+
return self._get_config_value("EXA_BASE_URL", "https://api.exa.ai") or "https://api.exa.ai"
|
|
592
|
+
|
|
593
|
+
@property
|
|
594
|
+
def exa_timeout(self) -> float:
|
|
595
|
+
return float(self._get_config_value("EXA_TIMEOUT_SECONDS", "30") or "30")
|
|
596
|
+
|
|
597
|
+
@property
|
|
598
|
+
def context7_api_key(self) -> str | None:
|
|
599
|
+
return self._get_config_value("CONTEXT7_API_KEY")
|
|
600
|
+
|
|
601
|
+
@property
|
|
602
|
+
def context7_base_url(self) -> str:
|
|
603
|
+
return self._get_config_value("CONTEXT7_BASE_URL", "https://context7.com") or "https://context7.com"
|
|
604
|
+
|
|
605
|
+
@property
|
|
606
|
+
def context7_timeout(self) -> float:
|
|
607
|
+
return float(self._get_config_value("CONTEXT7_TIMEOUT_SECONDS", "30") or "30")
|
|
608
|
+
|
|
609
|
+
@property
|
|
610
|
+
def zhipu_api_key(self) -> str | None:
|
|
611
|
+
return self._get_config_value("ZHIPU_API_KEY")
|
|
612
|
+
|
|
613
|
+
@property
|
|
614
|
+
def zhipu_api_url(self) -> str:
|
|
615
|
+
return self._get_config_value("ZHIPU_API_URL", "https://open.bigmodel.cn/api") or "https://open.bigmodel.cn/api"
|
|
616
|
+
|
|
617
|
+
@property
|
|
618
|
+
def zhipu_search_engine(self) -> str:
|
|
619
|
+
return self._get_config_value("ZHIPU_SEARCH_ENGINE", "search_std") or "search_std"
|
|
620
|
+
|
|
621
|
+
@property
|
|
622
|
+
def zhipu_timeout(self) -> float:
|
|
623
|
+
return float(self._get_config_value("ZHIPU_TIMEOUT_SECONDS", "30") or "30")
|
|
624
|
+
|
|
625
|
+
@property
|
|
626
|
+
def zhipu_mcp_api_key(self) -> str | None:
|
|
627
|
+
return self._get_config_value("ZHIPU_MCP_API_KEY")
|
|
628
|
+
|
|
629
|
+
@property
|
|
630
|
+
def zhipu_mcp_search_api_url(self) -> str:
|
|
631
|
+
return self._get_config_value(
|
|
632
|
+
"ZHIPU_MCP_SEARCH_API_URL",
|
|
633
|
+
"https://open.bigmodel.cn/api/mcp/web_search_prime/mcp",
|
|
634
|
+
) or "https://open.bigmodel.cn/api/mcp/web_search_prime/mcp"
|
|
635
|
+
|
|
636
|
+
@property
|
|
637
|
+
def zhipu_mcp_reader_api_url(self) -> str:
|
|
638
|
+
return self._get_config_value(
|
|
639
|
+
"ZHIPU_MCP_READER_API_URL",
|
|
640
|
+
"https://open.bigmodel.cn/api/mcp/web_reader/mcp",
|
|
641
|
+
) or "https://open.bigmodel.cn/api/mcp/web_reader/mcp"
|
|
642
|
+
|
|
643
|
+
@property
|
|
644
|
+
def zhipu_mcp_zread_api_url(self) -> str:
|
|
645
|
+
return self._get_config_value(
|
|
646
|
+
"ZHIPU_MCP_ZREAD_API_URL",
|
|
647
|
+
"https://open.bigmodel.cn/api/mcp/zread/mcp",
|
|
648
|
+
) or "https://open.bigmodel.cn/api/mcp/zread/mcp"
|
|
649
|
+
|
|
650
|
+
@property
|
|
651
|
+
def zhipu_mcp_timeout(self) -> float:
|
|
652
|
+
return float(self._get_config_value("ZHIPU_MCP_TIMEOUT_SECONDS", "30") or "30")
|
|
653
|
+
|
|
654
|
+
@property
|
|
655
|
+
def jina_api_key(self) -> str | None:
|
|
656
|
+
return self._get_config_value("JINA_API_KEY")
|
|
657
|
+
|
|
658
|
+
@property
|
|
659
|
+
def jina_reader_api_url(self) -> str:
|
|
660
|
+
return self._get_config_value("JINA_READER_API_URL", "https://r.jina.ai") or "https://r.jina.ai"
|
|
661
|
+
|
|
662
|
+
@property
|
|
663
|
+
def jina_respond_with(self) -> str:
|
|
664
|
+
return self._get_config_value("JINA_RESPOND_WITH", "") or ""
|
|
665
|
+
|
|
666
|
+
@property
|
|
667
|
+
def jina_timeout(self) -> float:
|
|
668
|
+
return float(self._get_config_value("JINA_TIMEOUT_SECONDS", "30") or "30")
|
|
669
|
+
|
|
670
|
+
def get_config_info(self) -> dict:
|
|
671
|
+
config_parameter_errors: list[str] = []
|
|
672
|
+
explicit_main_configured = bool(
|
|
673
|
+
self.xai_api_key
|
|
674
|
+
or (self.openai_compatible_api_url and self.openai_compatible_api_key)
|
|
675
|
+
)
|
|
676
|
+
if explicit_main_configured:
|
|
677
|
+
config_status = "ok: 配置完整"
|
|
678
|
+
else:
|
|
679
|
+
config_status = f"config_error: {self._SETUP_COMMAND}"
|
|
680
|
+
|
|
681
|
+
validation_level, validation_error = self._enum_info(
|
|
682
|
+
"SMART_SEARCH_VALIDATION_LEVEL",
|
|
683
|
+
self._DEFAULT_VALIDATION_LEVEL,
|
|
684
|
+
self._ALLOWED_VALIDATION_LEVELS,
|
|
685
|
+
)
|
|
686
|
+
fallback_mode, fallback_error = self._enum_info(
|
|
687
|
+
"SMART_SEARCH_FALLBACK_MODE",
|
|
688
|
+
self._DEFAULT_FALLBACK_MODE,
|
|
689
|
+
self._ALLOWED_FALLBACK_MODES,
|
|
690
|
+
)
|
|
691
|
+
minimum_profile, minimum_error = self._enum_info(
|
|
692
|
+
"SMART_SEARCH_MINIMUM_PROFILE",
|
|
693
|
+
self._DEFAULT_MINIMUM_PROFILE,
|
|
694
|
+
self._ALLOWED_MINIMUM_PROFILES,
|
|
695
|
+
)
|
|
696
|
+
intent_router_mode, intent_router_error = self._enum_info(
|
|
697
|
+
"SMART_SEARCH_INTENT_ROUTER",
|
|
698
|
+
self._DEFAULT_INTENT_ROUTER_MODE,
|
|
699
|
+
self._ALLOWED_INTENT_ROUTER_MODES,
|
|
700
|
+
)
|
|
701
|
+
intent_router_timeout, intent_router_timeout_error = self._float_info(
|
|
702
|
+
"INTENT_ROUTER_TIMEOUT_SECONDS",
|
|
703
|
+
self._DEFAULT_INTENT_ROUTER_TIMEOUT_SECONDS,
|
|
704
|
+
)
|
|
705
|
+
intent_embedding_threshold, intent_embedding_threshold_error = self._bounded_float_info(
|
|
706
|
+
"INTENT_EMBEDDING_THRESHOLD",
|
|
707
|
+
self._DEFAULT_INTENT_EMBEDDING_THRESHOLD,
|
|
708
|
+
0.0,
|
|
709
|
+
1.0,
|
|
710
|
+
)
|
|
711
|
+
intent_embedding_margin, intent_embedding_margin_error = self._bounded_float_info(
|
|
712
|
+
"INTENT_EMBEDDING_MARGIN",
|
|
713
|
+
self._DEFAULT_INTENT_EMBEDDING_MARGIN,
|
|
714
|
+
0.0,
|
|
715
|
+
1.0,
|
|
716
|
+
)
|
|
717
|
+
config_parameter_errors.extend(
|
|
718
|
+
error
|
|
719
|
+
for error in (
|
|
720
|
+
validation_error,
|
|
721
|
+
fallback_error,
|
|
722
|
+
minimum_error,
|
|
723
|
+
intent_router_error,
|
|
724
|
+
intent_router_timeout_error,
|
|
725
|
+
intent_embedding_threshold_error,
|
|
726
|
+
intent_embedding_margin_error,
|
|
727
|
+
)
|
|
728
|
+
if error
|
|
729
|
+
)
|
|
730
|
+
if config_parameter_errors and config_status.startswith("ok:"):
|
|
731
|
+
config_status = f"config_error: {'; '.join(config_parameter_errors)}"
|
|
732
|
+
|
|
733
|
+
return {
|
|
734
|
+
"XAI_API_URL": self.xai_api_url,
|
|
735
|
+
"XAI_API_KEY": self._mask_api_key(self.xai_api_key) if self.xai_api_key else "未配置",
|
|
736
|
+
"XAI_MODEL": self.xai_model,
|
|
737
|
+
"XAI_TOOLS": self.xai_tools_raw,
|
|
738
|
+
"OPENAI_COMPATIBLE_API_URL": self.openai_compatible_api_url or "未配置",
|
|
739
|
+
"OPENAI_COMPATIBLE_API_KEY": self._mask_api_key(self.openai_compatible_api_key) if self.openai_compatible_api_key else "未配置",
|
|
740
|
+
"OPENAI_COMPATIBLE_MODEL": self.openai_compatible_model,
|
|
741
|
+
"OPENAI_COMPATIBLE_FALLBACK_MODELS": ",".join(self.openai_compatible_fallback_models),
|
|
742
|
+
"OPENAI_COMPATIBLE_STREAM": self.openai_compatible_stream,
|
|
743
|
+
"SMART_SEARCH_VALIDATION_LEVEL": validation_level,
|
|
744
|
+
"SMART_SEARCH_FALLBACK_MODE": fallback_mode,
|
|
745
|
+
"SMART_SEARCH_MINIMUM_PROFILE": minimum_profile,
|
|
746
|
+
"SMART_SEARCH_OPERATION_CONFIG": self.operation_config,
|
|
747
|
+
"SMART_SEARCH_INTENT_ROUTER": intent_router_mode,
|
|
748
|
+
"INTENT_EMBEDDING_API_URL": self.intent_embedding_api_url or "未配置",
|
|
749
|
+
"INTENT_EMBEDDING_API_KEY": self._mask_api_key(self.intent_embedding_api_key) if self.intent_embedding_api_key else "未配置",
|
|
750
|
+
"INTENT_EMBEDDING_MODEL": self.intent_embedding_model or "未配置",
|
|
751
|
+
"INTENT_EMBEDDING_THRESHOLD": intent_embedding_threshold,
|
|
752
|
+
"INTENT_EMBEDDING_MARGIN": intent_embedding_margin,
|
|
753
|
+
"INTENT_CLASSIFIER_API_URL": self.intent_classifier_api_url or "未配置",
|
|
754
|
+
"INTENT_CLASSIFIER_API_KEY": self._mask_api_key(self.intent_classifier_api_key) if self.intent_classifier_api_key else "未配置",
|
|
755
|
+
"INTENT_CLASSIFIER_MODEL": self.intent_classifier_model or "未配置",
|
|
756
|
+
"INTENT_ROUTER_TIMEOUT_SECONDS": intent_router_timeout,
|
|
757
|
+
"SMART_SEARCH_DEBUG": self.debug_enabled,
|
|
758
|
+
"SMART_SEARCH_LOG_LEVEL": self.log_level,
|
|
759
|
+
"SMART_SEARCH_LOG_DIR": self.log_dir_config_value,
|
|
760
|
+
"SMART_SEARCH_RETRY_MAX_ATTEMPTS": self.retry_max_attempts,
|
|
761
|
+
"SMART_SEARCH_RETRY_MULTIPLIER": self.retry_multiplier,
|
|
762
|
+
"SMART_SEARCH_RETRY_MAX_WAIT": self.retry_max_wait,
|
|
763
|
+
"TAVILY_API_URL": self.tavily_api_url,
|
|
764
|
+
"TAVILY_ENABLED": self.tavily_enabled,
|
|
765
|
+
"TAVILY_API_KEY": self._mask_api_key(self.tavily_api_key) if self.tavily_api_key else "未配置",
|
|
766
|
+
"TAVILY_TIMEOUT_SECONDS": self.tavily_timeout,
|
|
767
|
+
"FIRECRAWL_API_URL": self.firecrawl_api_url,
|
|
768
|
+
"FIRECRAWL_API_KEY": self._mask_api_key(self.firecrawl_api_key) if self.firecrawl_api_key else "未配置",
|
|
769
|
+
"SMART_SEARCH_OUTPUT_CLEANUP": self.output_cleanup_enabled,
|
|
770
|
+
"SMART_SEARCH_LOG_TO_FILE": self.log_to_file_enabled,
|
|
771
|
+
"SSL_VERIFY": self.ssl_verify_enabled,
|
|
772
|
+
"EXA_API_KEY": self._mask_api_key(self.exa_api_key) if self.exa_api_key else "未配置",
|
|
773
|
+
"EXA_BASE_URL": self.exa_base_url,
|
|
774
|
+
"EXA_TIMEOUT_SECONDS": self.exa_timeout,
|
|
775
|
+
"CONTEXT7_API_KEY": self._mask_api_key(self.context7_api_key) if self.context7_api_key else "未配置",
|
|
776
|
+
"CONTEXT7_BASE_URL": self.context7_base_url,
|
|
777
|
+
"CONTEXT7_TIMEOUT_SECONDS": self.context7_timeout,
|
|
778
|
+
"ZHIPU_API_KEY": self._mask_api_key(self.zhipu_api_key) if self.zhipu_api_key else "未配置",
|
|
779
|
+
"ZHIPU_API_URL": self.zhipu_api_url,
|
|
780
|
+
"ZHIPU_SEARCH_ENGINE": self.zhipu_search_engine,
|
|
781
|
+
"ZHIPU_TIMEOUT_SECONDS": self.zhipu_timeout,
|
|
782
|
+
"ZHIPU_MCP_API_KEY": self._mask_api_key(self.zhipu_mcp_api_key) if self.zhipu_mcp_api_key else "未配置",
|
|
783
|
+
"ZHIPU_MCP_SEARCH_API_URL": self.zhipu_mcp_search_api_url,
|
|
784
|
+
"ZHIPU_MCP_READER_API_URL": self.zhipu_mcp_reader_api_url,
|
|
785
|
+
"ZHIPU_MCP_ZREAD_API_URL": self.zhipu_mcp_zread_api_url,
|
|
786
|
+
"ZHIPU_MCP_TIMEOUT_SECONDS": self.zhipu_mcp_timeout,
|
|
787
|
+
"JINA_API_KEY": self._mask_api_key(self.jina_api_key) if self.jina_api_key else "未配置",
|
|
788
|
+
"JINA_READER_API_URL": self.jina_reader_api_url,
|
|
789
|
+
"JINA_RESPOND_WITH": self.jina_respond_with,
|
|
790
|
+
"JINA_TIMEOUT_SECONDS": self.jina_timeout,
|
|
791
|
+
"primary_api_mode": "xai-responses" if self.xai_api_key else ("chat-completions" if self.openai_compatible_api_url and self.openai_compatible_api_key else "未配置"),
|
|
792
|
+
"primary_api_mode_source": "config_file" if explicit_main_configured else "default",
|
|
793
|
+
"config_file": str(self.config_file),
|
|
794
|
+
"config_dir": str(self.config_file.parent),
|
|
795
|
+
"config_dir_source": self.config_dir_source,
|
|
796
|
+
"default_config_file": str(self._default_config_dir() / "config.json"),
|
|
797
|
+
"legacy_windows_config_file": str(self._legacy_windows_config_dir() / "config.json") if sys.platform.startswith("win") else "",
|
|
798
|
+
"legacy_windows_config_exists": (self._legacy_windows_config_dir() / "config.json").exists() if sys.platform.startswith("win") else False,
|
|
799
|
+
"config_dir_override_value": self._config_dir_override_value(),
|
|
800
|
+
"config_dir_override_matches_default": self._config_dir_override_matches_default(),
|
|
801
|
+
"log_dir_config_value": self.log_dir_config_value,
|
|
802
|
+
"resolved_log_dir": str(self.log_dir),
|
|
803
|
+
"file_logging_enabled": self.debug_enabled or self.log_to_file_enabled,
|
|
804
|
+
"config_sources": self.get_config_sources(),
|
|
805
|
+
"config_parameter_errors": config_parameter_errors,
|
|
806
|
+
"config_status": config_status
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
config = Config()
|