mx_search.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. #!/usr/bin/env python3
  2. # mx_search - 妙想资讯搜索 skill
  3. # 基于东方财富妙想搜索API提供金融资讯搜索能力
  4. # 默认输出目录: /root/.openclaw/workspace/mx_data/output/
  5. import os
  6. import sys
  7. import json
  8. import re
  9. import requests
  10. from pathlib import Path
  11. from typing import Dict, List, Optional, Any
  12. def safe_filename(text: str, max_len: int = 80) -> str:
  13. """Convert query string to safe filenameh"""
  14. cleaned = re.sub(r'[<>:"/\\|?*]', "_", text).strip().replace(" ", "_")
  15. return (cleaned[:max_len] or "query").strip("._")
  16. class MXSearch:
  17. """妙想资讯搜索客户端"""
  18. BASE_URL = "https://mkapi2.dfcfs.com/finskillshub/api/claw/news-search"
  19. def __init__(self, api_key: Optional[str] = None):
  20. """
  21. 初始化客户端
  22. :param api_key: MX API Key,如果不提供则从环境变量 MX_APIKEY 读取
  23. """
  24. self.api_key = api_key or os.getenv("MX_APIKEY")
  25. if not self.api_key:
  26. raise ValueError(
  27. "MX_APIKEY 环境变量未设置,请先设置环境变量:\n"
  28. "export MX_APIKEY=your_api_key_here\n"
  29. "或者在初始化时传入 api_key 参数"
  30. )
  31. def search(self, query: str) -> Dict[str, Any]:
  32. """
  33. 搜索金融资讯
  34. :param query: 搜索问句
  35. :return: API 响应结果
  36. """
  37. headers = {
  38. "Content-Type": "application/json",
  39. "apikey": self.api_key
  40. }
  41. data = {
  42. "query": query
  43. }
  44. response = requests.post(self.BASE_URL, headers=headers, json=data, timeout=30)
  45. response.raise_for_status()
  46. return response.json()
  47. @staticmethod
  48. def extract_content(result: Dict[str, Any]) -> str:
  49. """
  50. 提取纯文本内容
  51. :param result: API 响应结果
  52. :return: 提取后的纯文本
  53. """
  54. def _extract(raw: Any) -> str:
  55. if not isinstance(raw, dict):
  56. if isinstance(raw, str):
  57. return raw.strip()
  58. return ""
  59. # Common envelope format
  60. for wrapper_key in ("data", "result"):
  61. wrapped = raw.get(wrapper_key)
  62. if isinstance(wrapped, dict):
  63. nested = _extract(wrapped)
  64. if nested:
  65. return nested
  66. for key in ("llmSearchResponse", "searchResponse", "content", "answer", "summary"):
  67. value = raw.get(key)
  68. if isinstance(value, str) and value.strip():
  69. return value.strip()
  70. if isinstance(value, (list, dict)):
  71. return json.dumps(value, ensure_ascii=False, indent=2)
  72. return json.dumps(raw, ensure_ascii=False, indent=2)
  73. return _extract(result)
  74. @staticmethod
  75. def format_pretty(result: Dict[str, Any]) -> str:
  76. """
  77. 格式化结果用于终端显示
  78. :param result: API 响应结果
  79. :return: 格式化文本
  80. """
  81. output = []
  82. status = result.get("status")
  83. message = result.get("message", "")
  84. if status != 0:
  85. output.append(f"错误: 状态码 {status} - {message}")
  86. return "\n".join(output)
  87. data = result.get("data", {})
  88. inner_data = data.get("data", {})
  89. search_response = inner_data.get("llmSearchResponse", {})
  90. items = search_response.get("data", [])
  91. if not items:
  92. return "未找到相关资讯"
  93. output.append(f"搜索结果: 共找到 {len(items)} 条相关资讯:\n")
  94. for i, item in enumerate(items, 1):
  95. title = item.get("title", "无标题")
  96. content = item.get("content", "无内容")
  97. date = item.get("date", "")
  98. ins_name = item.get("insName", "")
  99. info_type = item.get("informationType", "")
  100. rating = item.get("rating", "")
  101. entity_name = item.get("entityFullName", "")
  102. type_map = {
  103. "REPORT": "研报",
  104. "NEWS": "新闻",
  105. "ANNOUNCEMENT": "公告"
  106. }
  107. type_cn = type_map.get(info_type, info_type)
  108. output.append(f"--- {i}. {title} ---")
  109. meta = []
  110. if entity_name:
  111. meta.append(f"证券: {entity_name}")
  112. if ins_name:
  113. meta.append(f"机构: {ins_name}")
  114. if date:
  115. meta.append(f"日期: {date.split()[0]}")
  116. if type_cn:
  117. meta.append(f"类型: {type_cn}")
  118. if rating:
  119. meta.append(f"评级: {rating}")
  120. if meta:
  121. output.append(" | ".join(meta))
  122. if content:
  123. output.append("")
  124. output.append(content)
  125. output.append("")
  126. return "\n".join(output)
  127. def main():
  128. """命令行入口"""
  129. # 解析参数
  130. if len(sys.argv) < 2:
  131. print(f"用法: {sys.argv[0]} \"搜索问句\" [输出目录]")
  132. print(f"默认输出目录: /root/.openclaw/workspace/mx_data/output/")
  133. print("示例: python mx_search.py \"格力电器最新研报\"")
  134. sys.exit(1)
  135. # 拼接查询
  136. if len(sys.argv) >= 3:
  137. query = " ".join(sys.argv[1:-1])
  138. output_dir = Path(sys.argv[-1])
  139. else:
  140. query = " ".join(sys.argv[1:])
  141. # 默认输出到固定目录
  142. output_dir = Path("/root/.openclaw/workspace/mx_data/output")
  143. # 确保输出目录存在
  144. output_dir.mkdir(parents=True, exist_ok=True)
  145. try:
  146. mx = MXSearch()
  147. result = mx.search(query)
  148. # 终端显示格式化结果
  149. print(mx.format_pretty(result))
  150. # 提取纯文本保存为 .txt 文件
  151. content = mx.extract_content(result)
  152. if content.strip():
  153. filename = output_dir / f"mx_search_{safe_filename(query)}.txt"
  154. with open(filename, "w", encoding="utf-8") as f:
  155. f.write(content)
  156. print(f"\n✅ 纯文本结果已保存到: {filename}")
  157. # 同时保存原始 JSON 结果
  158. json_filename = output_dir / f"mx_search_{safe_filename(query)}.json"
  159. with open(json_filename, "w", encoding="utf-8") as f:
  160. json.dump(result, f, ensure_ascii=False, indent=2)
  161. print(f"📄 原始结果已保存到: {json_filename}")
  162. except Exception as e:
  163. print(f"错误: {str(e)}", file=sys.stderr)
  164. sys.exit(1)
  165. if __name__ == "__main__":
  166. main()