argo-search 1.0.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.
@@ -0,0 +1,381 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ evidence.py — 来源可信度评估工具(wigolo evidence_score 理念移植)
4
+
5
+ 核心能力:
6
+ 1. 权威性评分:基于域名白名单/黑名单 + 来源类型分级
7
+ 2. 时效性评分:基于内容时间戳 + 搜索时间
8
+ 3. 交叉验证:多个来源是否佐证同一结论
9
+ 4. 综合可信度:加权计算 + 透明分解
10
+
11
+ 用法:
12
+ python3 evidence.py --urls "https://example.com" "https://example2.com"
13
+ python3 evidence.py --search-result '{"results": [...]}' --query "茅台股价"
14
+ echo '{"results": [...]}' | python3 evidence.py --stdin --query "查询词"
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import json
21
+ import os
22
+ import re
23
+ import sys
24
+ import time
25
+ from typing import Any
26
+ from urllib.parse import urlparse
27
+
28
+ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
29
+ sys.path.insert(0, SCRIPT_DIR)
30
+
31
+
32
+ # ── 权威性评分 ────────────────────────────────────────────────────────────────
33
+
34
+ # 域名权威性分级(越高越好)
35
+ AUTHORITY_TIERS = {
36
+ # Tier 1: 官方/权威(0.95-1.0)
37
+ "gov.cn": 1.0, "gov": 0.95, "edu.cn": 0.95, "edu": 0.9,
38
+ "ac.cn": 0.95, # 中国科学院
39
+ "nature.com": 0.95, "science.org": 0.95, "ieee.org": 0.9,
40
+ "acm.org": 0.9, "springer.com": 0.9, "elsevier.com": 0.9,
41
+ "arxiv.org": 0.9, "pubmed.ncbi.nlm.nih.gov": 0.95,
42
+ "scholar.google.com": 0.85, "ncbi.nlm.nih.gov": 0.95,
43
+ "nvd.nist.gov": 0.95, # 国家漏洞数据库
44
+ "cve.mitre.org": 0.95,
45
+
46
+ # Tier 2: 专业媒体/平台(0.75-0.9)
47
+ "zhihu.com": 0.85, "github.com": 0.85, "stackoverflow.com": 0.85,
48
+ "medium.com": 0.75, "dev.to": 0.75,
49
+ "reuters.com": 0.9, "bloomberg.com": 0.9, "wsj.com": 0.9,
50
+ "财新": 0.9, "caixin.com": 0.9,
51
+ "36kr.com": 0.8, "infoq.cn": 0.8, "juejin.cn": 0.75,
52
+ "eastmoney.com": 0.85, "xueqiu.com": 0.8,
53
+ "docs.python.org": 0.9, "react.dev": 0.9, "nextjs.org": 0.9,
54
+
55
+ # Tier 3: 通用可信(0.6-0.75)
56
+ "wikipedia.org": 0.75, "baike.baidu.com": 0.7,
57
+ "维基百科": 0.75, "百度百科": 0.7,
58
+ "linkedin.com": 0.65, "twitter.com": 0.55, "x.com": 0.55,
59
+ "reddit.com": 0.65, "hackernews": 0.7,
60
+
61
+ # Tier 4: 内容农场/低质(0.2-0.4)
62
+ "sohu.com": 0.4, "163.com": 0.45, "sina.com.cn": 0.5,
63
+ "baijiahao.baidu.com": 0.35, "zhuanlan.zhihu.com": 0.7,
64
+ "toutiao.com": 0.4, "weixin.qq.com": 0.5,
65
+
66
+ # Tier 5: 已知低质(0.1-0.2)
67
+ "content-farm": 0.1, "seo-spam": 0.1,
68
+ }
69
+
70
+ # 来源类型映射
71
+ SOURCE_TYPE_MAP = {
72
+ "eastmoney": ("金融数据", 0.9),
73
+ "zhihu": ("社区观点", 0.8),
74
+ "arxiv": ("学术预印本", 0.85),
75
+ "semantic_scholar": ("学术索引", 0.9),
76
+ "openalex": ("学术索引", 0.85),
77
+ "crossref": ("学术元数据", 0.9),
78
+ "github": ("代码仓库", 0.85),
79
+ "byted": ("中文搜索", 0.7),
80
+ "bocha": ("中文搜索", 0.7),
81
+ "tavily": ("AI搜索", 0.75),
82
+ "felo": ("AI搜索", 0.7),
83
+ "duckduckgo": ("通用搜索", 0.65),
84
+ "wikipedia": ("百科", 0.75),
85
+ "anysearch": ("垂直搜索", 0.75),
86
+ "wigolo": ("本地搜索", 0.7),
87
+ "metaso": ("AI搜索", 0.75),
88
+ }
89
+
90
+
91
+ def score_authority(url: str, source: str = "") -> dict[str, Any]:
92
+ """评估 URL 的权威性。"""
93
+ if not url:
94
+ return {"score": 0.3, "reason": "无 URL", "tier": "unknown"}
95
+
96
+ parsed = urlparse(url)
97
+ domain = parsed.netloc.lower().strip("www.")
98
+ path = parsed.path.lower()
99
+
100
+ # 精确匹配域名
101
+ best_score = 0.5
102
+ best_reason = "通用域名"
103
+
104
+ for pattern, score in AUTHORITY_TIERS.items():
105
+ if domain == pattern or domain.endswith("." + pattern):
106
+ if score > best_score:
107
+ best_score = score
108
+ best_reason = f"域名匹配:{pattern}"
109
+
110
+ # 路径特征加分
111
+ if "/docs/" in path or "/documentation/" in path:
112
+ best_score = min(best_score + 0.05, 1.0)
113
+ best_reason += "(文档路径)"
114
+ if "/paper/" in path or "/arxiv/" in path or "/abs/" in path:
115
+ best_score = min(best_score + 0.05, 1.0)
116
+ best_reason += "(论文路径)"
117
+ if "/issues/" in path or "/pull/" in path:
118
+ best_score = min(best_score + 0.03, 1.0)
119
+ best_reason += "(Issue/PR路径)"
120
+
121
+ # 来源类型加分
122
+ if source and source in SOURCE_TYPE_MAP:
123
+ type_name, type_score = SOURCE_TYPE_MAP[source]
124
+ if type_score > best_score:
125
+ best_score = type_score
126
+ best_reason = f"来源类型:{type_name}"
127
+
128
+ tier = "high" if best_score >= 0.8 else "medium" if best_score >= 0.6 else "low" if best_score >= 0.4 else "very_low"
129
+
130
+ return {
131
+ "score": round(best_score, 2),
132
+ "reason": best_reason,
133
+ "tier": tier,
134
+ "domain": domain,
135
+ }
136
+
137
+
138
+ # ── 时效性评分 ────────────────────────────────────────────────────────────────
139
+
140
+ def score_freshness(result: dict[str, Any], query_time: float = None) -> dict[str, Any]:
141
+ """评估结果的时效性。"""
142
+
143
+ def _extract_year_from_url(url: str):
144
+ """从 URL 路径提取年份。"""
145
+ patterns = [
146
+ r"/(20\d{2})[/-](\d{1,2})[/-](\d{1,2})",
147
+ r"/(20\d{2})[/-](\d{1,2})",
148
+ r"/(20\d{2})/",
149
+ ]
150
+ for p in patterns:
151
+ m = re.search(p, url)
152
+ if m:
153
+ return int(m.group(1))
154
+ return None
155
+
156
+ if query_time is None:
157
+ query_time = time.time()
158
+
159
+ snippet = result.get("snippet", "") or ""
160
+ title = result.get("title", "") or ""
161
+ url = result.get("url", "") or ""
162
+ combined = f"{title} {snippet}"
163
+
164
+ # 提取时间信息
165
+ # 年份
166
+ year_match = re.search(r"20(\d{2})", combined)
167
+ if year_match:
168
+ year = 2000 + int(year_match.group(1))
169
+ from datetime import datetime
170
+ age_years = datetime.now().year - year
171
+ if age_years < 0:
172
+ score = 0.4
173
+ reason = f"{year}年(未来年份)"
174
+ elif age_years == 0:
175
+ score = 0.8
176
+ reason = f"{year}年(今年)"
177
+ elif age_years == 1:
178
+ score = 0.9
179
+ reason = f"{year}年(去年)"
180
+ elif age_years <= 2:
181
+ score = 0.7
182
+ reason = f"{year}年({age_years}年前)"
183
+ elif age_years <= 5:
184
+ score = 0.5
185
+ reason = f"{year}年({age_years}年前)"
186
+ else:
187
+ score = 0.3
188
+ reason = f"{year}年({age_years}年前,较旧)"
189
+ else:
190
+ # 无时间信息,尝试从 URL 提取
191
+ url_year = _extract_year_from_url(url)
192
+ if url_year:
193
+ from datetime import datetime
194
+ age_years = datetime.now().year - url_year
195
+ if age_years <= 1:
196
+ score = 0.8
197
+ reason = f"URL含{url_year}年"
198
+ elif age_years <= 3:
199
+ score = 0.6
200
+ reason = f"URL含{url_year}年({age_years}年前)"
201
+ else:
202
+ score = 0.4
203
+ reason = f"URL含{url_year}年(较旧)"
204
+ else:
205
+ score = 0.5
206
+ reason = "无明确时间标记"
207
+
208
+ # 时效性关键词加分
209
+ if re.search(r"(最新|latest|recent|breaking|just|刚刚|今日|today)", combined, re.I):
210
+ score = min(score + 0.1, 1.0)
211
+ reason += "(含时效关键词)"
212
+
213
+ return {
214
+ "score": round(score, 2),
215
+ "reason": reason,
216
+ }
217
+
218
+
219
+ # ── 交叉验证 ──────────────────────────────────────────────────────────────────
220
+
221
+ def cross_validate(results: list[dict[str, Any]], query: str) -> dict[str, Any]:
222
+ """多来源交叉验证。"""
223
+ if len(results) < 2:
224
+ return {
225
+ "corroboration_level": "insufficient",
226
+ "score": 0.3,
227
+ "detail": f"仅 {len(results)} 个结果,无法交叉验证",
228
+ "agreement_count": 0,
229
+ "total_sources": len(results),
230
+ }
231
+
232
+ # 检查 URL 重叠(不同来源指向同一页面 = 强佐证)
233
+ urls = [r.get("url", "") for r in results if r.get("url")]
234
+ unique_urls = set(urls)
235
+ url_overlap = 1 - (len(unique_urls) / max(len(urls), 1))
236
+
237
+ # 检查域名多样性
238
+ domains = set()
239
+ for r in results:
240
+ url = r.get("url", "")
241
+ if url:
242
+ domain = urlparse(url).netloc.lower().strip("www.")
243
+ domains.add(domain)
244
+
245
+ # 检查标题/内容相似度(简单关键词匹配)
246
+ query_words = set(query.lower().split())
247
+ content_matches = 0
248
+ for r in results:
249
+ text = f"{r.get('title', '')} {r.get('snippet', '')}".lower()
250
+ if any(w in text for w in query_words if len(w) > 1):
251
+ content_matches += 1
252
+
253
+ match_ratio = content_matches / max(len(results), 1)
254
+
255
+ # 计算佐证等级
256
+ if match_ratio >= 0.8 and len(domains) >= 3:
257
+ level = "strong"
258
+ score = 0.9
259
+ elif match_ratio >= 0.6 and len(domains) >= 2:
260
+ level = "moderate"
261
+ score = 0.7
262
+ elif match_ratio >= 0.4:
263
+ level = "weak"
264
+ score = 0.5
265
+ else:
266
+ level = "minimal"
267
+ score = 0.3
268
+
269
+ return {
270
+ "corroboration_level": level,
271
+ "score": round(score, 2),
272
+ "detail": f"{content_matches}/{len(results)} 个结果与查询相关,{len(domains)} 个独立域名",
273
+ "agreement_count": content_matches,
274
+ "total_sources": len(results),
275
+ "unique_domains": len(domains),
276
+ "url_overlap": round(url_overlap, 2),
277
+ }
278
+
279
+
280
+ # ── 综合可信度 ────────────────────────────────────────────────────────────────
281
+
282
+ def compute_credibility(results: list[dict[str, Any]], query: str) -> dict[str, Any]:
283
+ """计算每个结果的综合可信度评分。"""
284
+ query_time = time.time()
285
+ scored_results = []
286
+
287
+ for r in results:
288
+ url = r.get("url", "")
289
+ source = r.get("source", "")
290
+
291
+ auth = score_authority(url, source)
292
+ fresh = score_freshness(r, query_time)
293
+
294
+ # 综合公式:权威性 0.5 + 时效性 0.3 + 原始分数 0.2
295
+ original_score = r.get("score", 0.5) or 0.5
296
+ credibility = (
297
+ auth["score"] * 0.5 +
298
+ fresh["score"] * 0.3 +
299
+ original_score * 0.2
300
+ )
301
+
302
+ scored_results.append({
303
+ "title": r.get("title", ""),
304
+ "url": url,
305
+ "source": source,
306
+ "snippet": (r.get("snippet", "") or "")[:150],
307
+ "credibility": {
308
+ "final": round(credibility, 3),
309
+ "authority": auth,
310
+ "freshness": fresh,
311
+ "original_score": original_score,
312
+ },
313
+ })
314
+
315
+ # 交叉验证
316
+ cross = cross_validate(results, query)
317
+
318
+ # 排序
319
+ scored_results.sort(key=lambda x: x["credibility"]["final"], reverse=True)
320
+
321
+ return {
322
+ "query": query,
323
+ "results": scored_results,
324
+ "cross_validation": cross,
325
+ "summary": {
326
+ "total": len(scored_results),
327
+ "high_credibility": sum(1 for r in scored_results if r["credibility"]["final"] >= 0.7),
328
+ "medium_credibility": sum(1 for r in scored_results if 0.5 <= r["credibility"]["final"] < 0.7),
329
+ "low_credibility": sum(1 for r in scored_results if r["credibility"]["final"] < 0.5),
330
+ "best_source": scored_results[0] if scored_results else None,
331
+ },
332
+ }
333
+
334
+
335
+ # ── CLI ───────────────────────────────────────────────────────────────────────
336
+
337
+ def main():
338
+ parser = argparse.ArgumentParser(description="来源可信度评估工具")
339
+ parser.add_argument("query", nargs="?", default="", help="搜索查询词")
340
+ parser.add_argument("--stdin", action="store_true", help="从 stdin 读取 JSON 搜索结果")
341
+ parser.add_argument("--json", action="store_true", help="JSON 输出")
342
+ args = parser.parse_args()
343
+
344
+ if args.stdin:
345
+ data = json.load(sys.stdin)
346
+ results = data.get("results", [])
347
+ else:
348
+ # 从搜索结果文件读取
349
+ results = []
350
+
351
+ if not results and args.query:
352
+ # 无输入结果时,输出提示
353
+ print("需要提供搜索结果进行评估。用法:")
354
+ print(' echo \'{"results": [...]}\' | python3 evidence.py --stdin --query "查询词"')
355
+ sys.exit(1)
356
+
357
+ report = compute_credibility(results, args.query)
358
+
359
+ if args.json:
360
+ print(json.dumps(report, ensure_ascii=False, indent=2))
361
+ else:
362
+ print(f"\n来源可信度评估:{report['query']}")
363
+ print(f"{'='*50}")
364
+ for r in report["results"]:
365
+ c = r["credibility"]
366
+ level = "🟢" if c["final"] >= 0.7 else "🟡" if c["final"] >= 0.5 else "🔴"
367
+ print(f"{level} [{c['final']:.2f}] {r['title'][:50]}")
368
+ print(f" 权威:{c['authority']['score']:.2f} ({c['authority']['reason']})")
369
+ print(f" 时效:{c['freshness']['score']:.2f} ({c['freshness']['reason']})")
370
+ print()
371
+
372
+ cv = report["cross_validation"]
373
+ print(f"交叉验证:{cv['corroboration_level']} (score={cv['score']:.2f})")
374
+ print(f" {cv['detail']}")
375
+
376
+ s = report["summary"]
377
+ print(f"\n总结:🟢高={s['high_credibility']} 🟡中={s['medium_credibility']} 🔴低={s['low_credibility']}")
378
+
379
+
380
+ if __name__ == "__main__":
381
+ main()
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env python3
2
+ """extract.py — 结构化数据提取"""
3
+ import json, re, sys
4
+ from html.parser import HTMLParser
5
+ from fetch import fetch_page
6
+
7
+ class TableExtractor(HTMLParser):
8
+ def __init__(self):
9
+ super().__init__()
10
+ self.tables = []
11
+ self._in_table = False
12
+ self._rows = []
13
+ self._row = []
14
+ self._cell = []
15
+ self._in_cell = False
16
+ def handle_starttag(self, tag, attrs):
17
+ if tag == 'table': self._in_table = True; self._rows = []
18
+ elif tag in ('td','th') and self._in_table: self._in_cell = True; self._cell = []
19
+ elif tag == 'tr' and self._in_table: self._row = []
20
+ def handle_endtag(self, tag):
21
+ if tag in ('td','th') and self._in_cell:
22
+ self._in_cell = False; self._row.append(''.join(self._cell).strip())
23
+ elif tag == 'tr' and self._in_table and self._row:
24
+ self._rows.append(self._row)
25
+ elif tag == 'table' and self._in_table:
26
+ self._in_table = False
27
+ if self._rows:
28
+ headers = self._rows[0]
29
+ table = [{headers[i]: row[i] if i < len(row) else '' for i in range(len(headers))} for row in self._rows[1:]]
30
+ self.tables.append(table)
31
+ def handle_data(self, data):
32
+ if self._in_cell: self._cell.append(data)
33
+
34
+ def extract_tables(html):
35
+ ext = TableExtractor(); ext.feed(html); return ext.tables
36
+
37
+ def extract_metadata(html):
38
+ meta = {}
39
+ title_m = re.search(r'<title>(.*?)</title>', html, re.I|re.S)
40
+ if title_m: meta['title'] = title_m.group(1).strip()
41
+ for pattern in [r'<meta\s+name=["\']description["\']\s+content=["\'](.*?)["\']', r'<meta\s+content=["\'](.*?)["\']\s+name=["\']description["\']']:
42
+ m = re.search(pattern, html, re.I)
43
+ if m: meta['description'] = m.group(1).strip(); break
44
+ for prop in ['og:title','og:description','og:image']:
45
+ m = re.search(rf'property=["\']{prop}["\']\s+content=["\'](.*?)["\']', html, re.I)
46
+ if m: meta[prop] = m.group(1).strip()
47
+ return meta
48
+
49
+ def extract_jsonld(html):
50
+ results = []
51
+ for m in re.finditer(r'<script[^>]*type=["\']application/ld\+json["\'][^>]*>(.*?)</script>', html, re.S|re.I):
52
+ try: results.append(json.loads(m.group(1)))
53
+ except: pass
54
+ return results
55
+
56
+ if __name__ == '__main__':
57
+ import argparse
58
+ p = argparse.ArgumentParser()
59
+ p.add_argument('--url', required=True)
60
+ p.add_argument('--mode', default='all', choices=['tables','metadata','jsonld','all'])
61
+ args = p.parse_args()
62
+ result = fetch_page(args.url, 50000)
63
+ if not result['success']: print(json.dumps({'error': result.get('error')})); sys.exit(1)
64
+ html = result['content']
65
+ output = {}
66
+ if args.mode in ('tables','all'): output['tables'] = extract_tables(html)
67
+ if args.mode in ('metadata','all'): output['metadata'] = extract_metadata(html)
68
+ if args.mode in ('jsonld','all'): output['jsonld'] = extract_jsonld(html)
69
+ print(json.dumps(output, ensure_ascii=False, indent=2)[:3000])
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ fetch.py — 轻量页面抓取(纯标准库)
4
+
5
+ 从 URL 抓取页面并提取正文文本,用于 research 深度研究。
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ import urllib.request
12
+ from html.parser import HTMLParser
13
+ from typing import Any
14
+ from urllib.parse import urlparse
15
+
16
+
17
+ class ContentExtractor(HTMLParser):
18
+ """从 HTML 中提取正文文本。"""
19
+
20
+ _skip_tags = {"script", "style", "nav", "header", "footer", "aside", "noscript"}
21
+
22
+ def __init__(self):
23
+ super().__init__()
24
+ self._in_skip = 0
25
+ self._blocks: list[tuple[float, str]] = []
26
+ self._current_text: list[str] = []
27
+
28
+ def handle_starttag(self, tag, attrs):
29
+ if tag in self._skip_tags:
30
+ self._in_skip += 1
31
+
32
+ def handle_endtag(self, tag):
33
+ if tag in self._skip_tags:
34
+ self._in_skip = max(0, self._in_skip - 1)
35
+ if tag in ("p", "div", "article", "section", "li", "h1", "h2", "h3", "td"):
36
+ text = "".join(self._current_text).strip()
37
+ if len(text) > 20:
38
+ # 文本密度 = 非空字符 / 总字符
39
+ density = len(text.replace(" ", "")) / max(len(text), 1)
40
+ self._blocks.append((density, text))
41
+ self._current_text = []
42
+
43
+ def handle_data(self, data):
44
+ if self._in_skip == 0:
45
+ self._current_text.append(data)
46
+
47
+
48
+ def fetch_page(url: str, max_chars: int = 3000, timeout: int = 8,
49
+ raw: bool = False) -> dict[str, Any]:
50
+ """抓取页面并提取正文。
51
+
52
+ Args:
53
+ url: 目标 URL
54
+ max_chars: 最大返回字符数
55
+ timeout: 超时秒数
56
+ raw: True 时返回原始 HTML(用于 extract/crawl_sitemap 等需要 HTML 的场景)
57
+ """
58
+ try:
59
+ req = urllib.request.Request(url, headers={
60
+ "User-Agent": "unified-search/2.5 (+local-research)",
61
+ "Accept": "text/html,application/xhtml+xml,application/xml",
62
+ })
63
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
64
+ content_type = resp.headers.get("Content-Type", "")
65
+ if "pdf" in content_type:
66
+ return {"url": url, "content": "", "html": "", "length": 0, "success": False, "error": "PDF not supported"}
67
+ raw_bytes = resp.read(max(max_chars * 5, 500000))
68
+ # 编码检测
69
+ charset = "utf-8"
70
+ if "charset=" in content_type:
71
+ charset = content_type.split("charset=")[-1].strip().split(";")[0]
72
+ html = raw_bytes.decode(charset, errors="replace")
73
+
74
+ # 正文提取
75
+ extractor = ContentExtractor()
76
+ extractor.feed(html)
77
+ extractor._blocks.sort(key=lambda x: x[0], reverse=True)
78
+ content = "\n\n".join(text for _, text in extractor._blocks[:8])
79
+
80
+ result = {
81
+ "url": url,
82
+ "content": content[:max_chars],
83
+ "length": len(content),
84
+ "success": True,
85
+ }
86
+ if raw:
87
+ result["html"] = html[:max(max_chars * 2, 100000)]
88
+ return result
89
+ except Exception as e:
90
+ return {"url": url, "content": "", "html": "", "length": 0, "success": False, "error": str(e)[:100]}
91
+
92
+
93
+ def fetch_pages_parallel(urls: list[str], max_chars: int = 3000,
94
+ timeout: int = 8, max_workers: int = 3) -> list[dict[str, Any]]:
95
+ """并行抓取多个页面。"""
96
+ from concurrent.futures import ThreadPoolExecutor, as_completed
97
+
98
+ results = []
99
+ with ThreadPoolExecutor(max_workers=min(len(urls), max_workers)) as ex:
100
+ futures = {ex.submit(fetch_page, url, max_chars, timeout): url for url in urls}
101
+ for fut in as_completed(futures, timeout=timeout + 3):
102
+ try:
103
+ results.append(fut.result())
104
+ except Exception:
105
+ results.append({"url": futures[fut], "content": "", "length": 0, "success": False})
106
+ return results
107
+
108
+
109
+ # CLI 测试
110
+ if __name__ == "__main__":
111
+ import sys
112
+ url = sys.argv[1] if len(sys.argv) > 1 else "https://docs.python.org/3/"
113
+ result = fetch_page(url, max_chars=500)
114
+ print(f"成功: {result['success']}, 长度: {result['length']}")
115
+ if result["success"]:
116
+ print(result["content"][:300])
117
+ else:
118
+ print(f"错误: {result.get('error', 'unknown')}")