{"repo": "alexliaopoly/cmhk-competitive-intelligence", "files": {"agent.py": {"8f9023fc06f9ad2f908206cd8e330bf470e0bded": {"t": 1786068465.5341725, "n": 87814, "h": "be2f2935bf3a0b5a", "c": "import csv\nimport collections\nimport hashlib\nimport json\nimport math\nimport os\nimport re\nimport statistics\nimport subprocess\nimport sys\nimport threading\nfrom contextvars import ContextVar\nfrom io import BytesIO\nfrom pathlib import Path\nfrom typing import Any, Generator\nfrom urllib.parse import parse_qs, unquote, urlencode, urlparse\nfrom urllib.request import Request\n\nfrom langchain_core.messages import AIMessageChunk, ToolMessage\nfrom langchain_core.tools import tool\nfrom langchain_deepseek import ChatDeepSeek\nfrom langgraph.prebuilt import create_react_agent\n\nfrom ai_config import load_ai_config\nfrom agent_memory import add_memory, auto_capture_user_memory, load_memories, memory_context, search_memories\nfrom agent_production import (\n AgentRunRecorder,\n action_id,\n confirmation_event,\n confirmation_metadata,\n dataset_lineage,\n retrieval_quality,\n rolling_backtest,\n)\nfrom crawl_run_registry import latest_crawl_run_summary\nfrom network_utils import urlopen_with_local_proxy_fallback\nfrom rag_llm import build_context_package, list_knowledge_datasets, retrieve_context\nfrom chart_renderer import render_chart\n\nROOT = Path(__file__).resolve().parent\nAGENT_SKILLS_DIR = ROOT / \"Codex\" / \"agent\" / \"skills\"\nFRONTEND_SKILL_ORDER = [\n \"executive-briefing\",\n \"quarterly-competitor-metrics\",\n \"cloud-vendor-metrics\",\n \"macro-policy-context\",\n \"trend-forecasting\",\n]\nWEB_SEARCH_INDEX_LOCK = threading.Lock()\nWEB_SEARCH_NEXT_INDEX = 6\nSELECTED_DATASET_IDS: ContextVar[set[str] | None] = ContextVar(\"SELECTED_DATASET_IDS\", default=None)\nSELECTED_SKILL_IDS: ContextVar[set[str] | None] = ContextVar(\"SELECTED_SKILL_IDS\", default=None)\nAPPROVED_ACTION_IDS: ContextVar[set[str]] = ContextVar(\"APPROVED_ACTION_IDS\", default=set())\nCHART_RUN_STATE: ContextVar[dict[str, Any] | None] = ContextVar(\"CHART_RUN_STATE\", default=None)\nTOOL_RUN_STATE: ContextVar[dict[str, Any] | None] = ContextVar(\"TOOL_RUN_STATE\", default=None)\n\n\ndef _reset_web_search_indexes() -> None:\n global WEB_SEARCH_NEXT_INDEX\n with WEB_SEARCH_INDEX_LOCK:\n WEB_SEARCH_NEXT_INDEX = 6\n\n\ndef _allocate_web_search_indexes(count: int) -> int:\n global WEB_SEARCH_NEXT_INDEX\n with WEB_SEARCH_INDEX_LOCK:\n start = WEB_SEARCH_NEXT_INDEX\n WEB_SEARCH_NEXT_INDEX += max(0, count)\n return start\n\n\ndef _clean_search_text(value: Any, limit: int = 500) -> str:\n text = \" \".join(str(value or \"\").split())\n return text[:limit]\n\n\ndef _register_tool_invocation(tool_name: str, limit: int) -> str | None:\n state = TOOL_RUN_STATE.get()\n if state is None:\n return None\n counts = state.setdefault(\"counts\", {})\n count = int(counts.get(tool_name) or 0) + 1\n counts[tool_name] = count\n if count > limit:\n return (\n f\"{tool_name} 已达到本轮调用上限({limit} 次),请停止继续调用该工具,\"\n \"直接基于已经返回的资料给出结论、表格或图表。\"\n )\n return None\n\n\nclass MarkdownTableLimiter:\n \"\"\"Limit streamed Markdown tables before they reach the browser.\"\"\"\n\n def __init__(self, max_rows: int = 30, max_columns: int = 8) -> None:\n self.max_rows = max_rows\n self.max_columns = max_columns\n self.buffer = \"\"\n self.in_table = False\n self.row_count = 0\n self.notice_sent = False\n\n @staticmethod\n def _is_table_line(line: str) -> bool:\n stripped = line.strip()\n return stripped.startswith(\"|\") and stripped.endswith(\"|\") and stripped.count(\"|\") >= 2\n\n @staticmethod\n def _is_separator_line(line: str) -> bool:\n cells = line.strip().split(\"|\")[1:-1]\n return bool(cells) and all(re.fullmatch(r\"\\s*:?-{3,}:?\\s*\", cell or \"\") for cell in cells)\n\n def _cap_columns(self, line: str) -> str:\n newline = \"\\n\" if line.endswith(\"\\n\") else \"\"\n cells = line.strip().split(\"|\")[1:-1]\n if len(cells) <= self.max_columns:\n return line\n capped = cells[: self.max_columns]\n return \"|\" + \"|\".join(capped) + \"|\" + newline\n\n def _process_line(self, line: str) -> str:\n if not self._is_table_line(line):\n self.in_table = False\n self.row_count = 0\n self.notice_sent = False\n return line\n if not self.in_table:\n self.in_table = True\n self.row_count = 0\n self.notice_sent = False\n if self._is_separator_line(line):\n return self._cap_columns(line)\n self.row_count += 1\n if self.row_count > self.max_rows:\n if self.notice_sent:\n return \"\"\n self.notice_sent = True\n return \"\\n表格已由后端截断:最多显示 30 行、8 列;请缩小范围或导出 CSV 查看完整数据。\\n\"\n return self._cap_columns(line)\n\n def feed(self, text: str) -> str:\n if not text:\n return \"\"\n self.buffer += text\n output: list[str] = []\n while True:\n newline_index = self.buffer.find(\"\\n\")\n if newline_index < 0:\n break\n line = self.buffer[: newline_index + 1]\n self.buffer = self.buffer[newline_index + 1 :]\n output.append(self._process_line(line))\n return \"\".join(output)\n\n def flush(self) -> str:\n if not self.buffer:\n return \"\"\n line = self.buffer\n self.buffer = \"\"\n stripped = line.strip()\n if stripped.startswith(\"|\") and not stripped.endswith(\"|\"):\n return \"\"\n return self._process_line(line)\n\n\ndef _parse_skill_frontmatter(text: str) -> dict[str, Any]:\n meta: dict[str, Any] = {\"description\": \"\", \"tags\": [], \"data\": \"\"}\n match = re.search(r\"^##\\s+前端展示\\s*\\n(?P
.*?)(?=^##\\s+|\\Z)\", text, re.MULTILINE | re.DOTALL)\n if not match:\n return meta\n for raw_line in match.group(\"body\").splitlines():\n line = raw_line.strip()\n if not line.startswith(\"-\"):\n continue\n item = line.lstrip(\"-\").strip()\n if \":\" not in item:\n continue\n key, value = item.split(\":\", 1)\n key = key.strip()\n value = value.strip()\n if key == \"简介\":\n meta[\"description\"] = value\n elif key == \"标签\":\n meta[\"tags\"] = [part.strip() for part in re.split(r\"[、,,]\", value) if part.strip()]\n elif key == \"数据\":\n meta[\"data\"] = value\n return meta\n\n\ndef available_agent_skills() -> list[dict[str, Any]]:\n by_id: dict[str, dict[str, Any]] = {}\n if not AGENT_SKILLS_DIR.exists():\n return []\n for skill_file in sorted(AGENT_SKILLS_DIR.glob(\"*/SKILL.md\")):\n skill_id = skill_file.parent.name\n try:\n text = skill_file.read_text(encoding=\"utf-8\", errors=\"ignore\")\n except Exception:\n continue\n display_meta = _parse_skill_frontmatter(text)\n title = skill_id\n summary = \"\"\n for line in text.splitlines():\n clean = line.strip()\n if not clean:\n continue\n if clean.startswith(\"#\") and title == skill_id:\n title = clean.lstrip(\"#\").strip() or title\n continue\n if not summary and not clean.startswith((\"-\", \"`\", \"#\")):\n summary = clean[:180]\n break\n by_id[skill_id] = {\n \"id\": skill_id,\n \"title\": title,\n \"summary\": display_meta.get(\"description\") or summary or \"项目内 Agent skill\",\n \"description\": display_meta.get(\"description\") or summary or \"项目内 Agent skill\",\n \"tags\": display_meta.get(\"tags\") or [],\n \"data\": display_meta.get(\"data\") or \"\",\n \"path\": skill_file.relative_to(ROOT).as_posix(),\n }\n visible = [by_id[skill_id] for skill_id in FRONTEND_SKILL_ORDER if skill_id in by_id]\n return visible\n\n\ndef _selected_skill_context(skill_ids: list[str] | None) -> str:\n if not skill_ids:\n return \"\"\n allowed = {item[\"id\"] for item in available_agent_skills()}\n blocks: list[str] = [\n \"以下是本轮前端勾选的 Agent Skill 发现信息。\"\n \"这不是完整 Skill 指令;当你准备使用某个 Skill 时,必须先调用 `read_agent_skill(skill_id)` 读取完整 SKILL.md。\"\n ]\n for skill_id in skill_ids[:5]:\n clean_id = re.sub(r\"[^A-Za-z0-9_.-]\", \"\", str(skill_id or \"\"))\n if clean_id not in allowed:\n continue\n item = next((row for row in available_agent_skills() if row.get(\"id\") == clean_id), None)\n if item:\n blocks.append(\n \"\\n\".join(\n [\n f\"## Skill: {clean_id}\",\n f\"- 标题: {item.get('title') or clean_id}\",\n f\"- 简介: {item.get('description') or item.get('summary') or ''}\",\n f\"- 数据: {item.get('data') or ''}\",\n f\"- 路径: {item.get('path') or ''}\",\n ]\n )\n )\n return \"\\n\\n\".join(blocks)\n\n\ndef _looks_like_blocked_or_encoded_text(text: str) -> bool:\n sample = \" \".join((text or \"\").split())[:2400]\n if not sample:\n return False\n if re.search(r\"_waf_|waf|captcha|verify|访问验证|安全验证|人机验证\", sample, re.IGNORECASE):\n return True\n compact = re.sub(r\"\\s+\", \"\", sample)\n if len(compact) < 300:\n return False\n encoded_chars = sum(1 for ch in compact if ch.isalnum() or ch in \"+/=_-\")\n cjk_chars = len(re.findall(r\"[\\u4e00-\\u9fff]\", compact))\n return encoded_chars / max(len(compact), 1) > 0.92 and cjk_chars < 20\n\n\ndef _search_query_from_instruction(value: str) -> str:\n text = _clean_search_text(value, 200)\n text = re.sub(r\"^(请|帮我|麻烦)?(上网|联网|网上)?(搜一下|搜索一下|搜搜|搜索|查一下|查询)\", \"\", text).strip()\n text = re.sub(r\"(给我|帮我)?(?:找|列|提供)?[一二两三四五六七八九十\\d]+个?来源.*$\", \"\", text).strip(\" ,,。;;\")\n return text or _clean_search_text(value, 200)\n\n\ndef _normalize_search_results(items: list[dict[str, Any]], limit: int) -> list[dict[str, str]]:\n results: list[dict[str, str]] = []\n seen: set[str] = set()\n for item in items:\n title = _clean_search_text(item.get(\"title\") or item.get(\"heading\") or item.get(\"name\"), 120)\n url = _clean_search_text(item.get(\"url\") or item.get(\"href\") or item.get(\"link\"), 500)\n snippet = _clean_search_text(\n item.get(\"content\") or item.get(\"body\") or item.get(\"snippet\") or item.get(\"description\"),\n 320,\n )\n if not title or not url or not url.startswith((\"http://\", \"https://\")) or url in seen:\n continue\n seen.add(url)\n results.append({\"title\": title, \"url\": url, \"snippet\": snippet})\n if len(results) >= limit:\n break\n return results\n\n\ndef _unwrap_search_redirect(url: str) -> str:\n if url.startswith(\"//\"):\n url = \"https:\" + url\n parsed = urlparse(url)\n if \"search.yahoo.com\" in parsed.netloc and \"/RU=\" in parsed.path:\n encoded = parsed.path.split(\"/RU=\", 1)[1].split(\"/RK=\", 1)[0]\n return unquote(encoded)\n if \"duckduckgo.com\" in parsed.netloc and parsed.path.startswith(\"/l/\"):\n query = parse_qs(parsed.query)\n if query.get(\"uddg\"):\n return query[\"uddg\"][0]\n query = parse_qs(parsed.query)\n for key in (\"url\", \"u\"):\n if query.get(key):\n return query[key][0]\n return url\n\n\ndef _search_with_searxng(query: str, limit: int) -> list[dict[str, str]]:\n base_url = (os.environ.get(\"SEARXNG_URL\") or os.environ.get(\"CMHK_SEARXNG_URL\") or \"\").strip().rstrip(\"/\")\n if not base_url:\n return []\n params = urlencode({\"q\": query, \"format\": \"json\", \"language\": \"zh-CN\"})\n req = Request(\n f\"{base_url}/search?{params}\",\n headers={\"User-Agent\": \"CMHK-Research-Agent/1.0\"},\n )\n with urlopen_with_local_proxy_fallback(req, timeout=20) as response:\n payload = json.loads(response.read().decode(\"utf-8\", errors=\"ignore\"))\n raw_results = payload.get(\"results\") if isinstance(payload, dict) else []\n return _normalize_search_results(raw_results or [], limit)\n\n\ndef _search_with_duckduckgo(query: str, limit: int) -> list[dict[str, str]]:\n try:\n from ddgs import DDGS # type: ignore\n except Exception:\n return []\n with DDGS() as ddgs:\n raw_results = list(ddgs.text(query, max_results=limit))\n return _normalize_search_results(raw_results, limit)\n\n\ndef _search_with_duckduckgo_html(query: str, limit: int) -> list[dict[str, str]]:\n from bs4 import BeautifulSoup\n\n req = Request(\n \"https://html.duckduckgo.com/html/?\" + urlencode({\"q\": query}),\n headers={\n \"User-Agent\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/125 Safari/537.36\",\n \"Accept-Language\": \"zh-CN,zh;q=0.9,en;q=0.8\",\n },\n )\n with urlopen_with_local_proxy_fallback(req, timeout=20) as response:\n html = response.read().decode(\"utf-8\", errors=\"ignore\")\n soup = BeautifulSoup(html, \"html.parser\")\n raw_results: list[dict[str, Any]] = []\n for block in soup.select(\".result\"):\n link = block.select_one(\".result__a[href]\") or block.select_one('a[href]')\n if not link:\n continue\n snippet_node = block.select_one(\".result__snippet\")\n raw_results.append(\n {\n \"title\": link.get_text(\" \", strip=True),\n \"url\": _unwrap_search_redirect(link.get(\"href\") or \"\"),\n \"snippet\": snippet_node.get_text(\" \", strip=True) if snippet_node else \"\",\n }\n )\n return _normalize_search_results(raw_results, limit)\n\n\ndef _search_with_brave_html(query: str, limit: int) -> list[dict[str, str]]:\n from bs4 import BeautifulSoup\n\n req = Request(\n \"https://search.brave.com/search?\" + urlencode({\"q\": query, \"source\": \"web\"}),\n headers={\"User-Agent\": \"Mozilla/5.0\"},\n )\n with urlopen_with_local_proxy_fallback(req, timeout=20) as response:\n html = response.read().decode(\"utf-8\", errors=\"ignore\")\n soup = BeautifulSoup(html, \"html.parser\")\n raw_results: list[dict[str, Any]] = []\n for block in soup.select(\".snippet\"):\n link = block.select_one('a[href^=\"http\"]')\n if not link:\n continue\n title = link.get_text(\" \", strip=True)\n url = _unwrap_search_redirect(link.get(\"href\") or \"\")\n text = block.get_text(\" \", strip=True)\n snippet = text.replace(title, \"\", 1).strip(\" -\")\n raw_results.append({\"title\": title, \"url\": url, \"snippet\": snippet})\n return _normalize_search_results(raw_results, limit)\n\n\ndef _search_with_yahoo_html(query: str, limit: int) -> list[dict[str, str]]:\n from bs4 import BeautifulSoup\n\n req = Request(\n \"https://search.yahoo.com/search?\" + urlencode({\"p\": query}),\n headers={\"User-Agent\": \"Mozilla/5.0\"},\n )\n with urlopen_with_local_proxy_fallback(req, timeout=20) as response:\n html = response.read().decode(\"utf-8\", errors=\"ignore\")\n soup = BeautifulSoup(html, \"html.parser\")\n raw_results: list[dict[str, Any]] = []\n for block in soup.select(\".dd.algo\"):\n link = block.select_one(\".compTitle a[href]\") or block.select_one('a[href^=\"http\"]')\n title_node = block.select_one(\"h3.title\") or link\n snippet_node = block.select_one(\".compText p\") or block.select_one(\"p\")\n if not link or not title_node:\n continue\n raw_results.append(\n {\n \"title\": title_node.get_text(\" \", strip=True),\n \"url\": _unwrap_search_redirect(link.get(\"href\") or \"\"),\n \"snippet\": snippet_node.get_text(\" \", strip=True) if snippet_node else \"\",\n }\n )\n return _normalize_search_results(raw_results, limit)\n\n\ndef _display_tool_result(tool_name: str, content: str, meta_event: dict[str, Any] | None) -> str:\n if tool_name == \"list_local_datasets\":\n return content[:4000].rstrip()\n if tool_name == \"list_crawl_runs\":\n return content[:4000].rstrip()\n if tool_name == \"web_search\":\n refs = (meta_event or {}).get(\"references\") or []\n if not refs:\n preview = content.strip()\n return preview[:1200].rstrip() if preview else \"联网搜索完成,但没有返回可展示来源。\"\n lines = [f\"联网搜索完成,找到 {len(refs)} 个来源:\"]\n for ref in refs[:6]:\n links = ref.get(\"links\") or []\n link = links[0] if links else {}\n label = _clean_search_text(ref.get(\"source\") or link.get(\"label\") or \"来源\", 120)\n url = _clean_search_text(link.get(\"url\") or \"\", 300)\n lines.append(f\"[{ref.get('index')}] {label}\" + (f\"\\n{url}\" if url else \"\"))\n preview = content.strip()\n if preview:\n lines.append(\"\\n具体返回内容:\")\n lines.append(preview[:4000].rstrip())\n return \"\\n\".join(lines)\n if tool_name == \"search_local_reports\":\n refs = (meta_event or {}).get(\"references\") or []\n preview = content.strip()\n return (\n f\"已读取已选数据库摘要,找到 {len(refs)} 个相关片段。\\n\\n\"\n f\"具体返回内容:\\n{preview[:4000].rstrip()}\"\n ).rstrip()\n if tool_name == \"read_local_reference\":\n if content.startswith(\"本地引用读取失败\"):\n return content\n return f\"已读取数据库原文。\\n\\n具体返回内容:\\n{content[:4000].rstrip()}\"\n if tool_name == \"read_agent_skill\":\n if content.startswith(\"Agent Skill 读取失败\"):\n return content\n return f\"Agent Skill 已读取。\\n\\n具体返回内容:\\n{content[:6000].rstrip()}\"\n if tool_name == \"read_webpage\":\n if content.startswith((\"网页读取失败\", \"网页读取跳过\", \"读取网页\")):\n return \"网页读取失败,已改用搜索摘要和本地资料。\"\n if content.startswith(\"PDF读取失败\"):\n return \"PDF 读取失败,已改用搜索摘要和本地资料。\"\n return f\"网页读取完成。\\n\\n具体返回内容:\\n{content[:4000].rstrip()}\"\n if tool_name == \"render_python_chart\":\n return content[:4000].rstrip()\n if len(content) > 1200:\n return content[:1200].rstrip() + \"\\n...\"\n return content\n\n\ndef _selected_dataset_ids() -> set[str] | None:\n return SELECTED_DATASET_IDS.get()\n\n\ndef _dataset_id_for_agent_knowledge_source(source: str) -> str:\n clean = str(source or \"\").strip().removeprefix(\"/references/\")\n parts = clean.split(\"/\")\n if len(parts) >= 2 and parts[0] == \"agent_knowledge\":\n folder = ROOT / \"agent_knowledge\" / parts[1]\n if folder.exists():\n for dataset in list_knowledge_datasets():\n if dataset.get(\"folder\") == f\"agent_knowledge/{parts[1]}\":\n return str(dataset.get(\"id\") or parts[1])\n return parts[1]\n return \"\"\n\n\ndef _dataset_is_selected(source: str) -> bool:\n selected = _selected_dataset_ids()\n if selected is None:\n return True\n dataset_id = _dataset_id_for_agent_knowledge_source(source)\n return not dataset_id or dataset_id in selected\n\n\ndef _is_action_approved(name: str, payload: Any = None) -> bool:\n approved = APPROVED_ACTION_IDS.get()\n return action_id(name, payload) in approved\n\n\ndef _require_action_confirmation(name: str, payload: Any = None, description: str = \"\") -> str | None:\n if _is_action_approved(name, payload):\n return None\n event = confirmation_event(name, payload, description)\n return (\n f\"需要用户确认后才能执行:{event['label']}。\\n\"\n f\"- 原因:{event['risk']}\\n\"\n f\"- 操作说明:{event['description']}\\n\"\n \"请点击前端确认按钮后再执行。\"\n f\"\\n{confirmation_metadata(event)}\"\n )\n\n\n@tool\ndef read_agent_skill(skill_id: str) -> str:\n \"\"\"读取本轮已选择的 Agent Skill 完整 SKILL.md 指令。\n 当你准备使用某个前端已选 Agent Skill 时,必须先调用此工具读取完整指令;\n `load_agent_skills` 只表示前端选择了哪些 Skill,不等于已经阅读和执行了 Skill。\n \"\"\"\n clean_id = re.sub(r\"[^A-Za-z0-9_.-]\", \"\", str(skill_id or \"\"))\n if not clean_id:\n return \"Agent Skill 读取失败:skill_id 为空。\"\n selected_ids = {\n re.sub(r\"[^A-Za-z0-9_.-]\", \"\", str(item or \"\"))\n for item in (SELECTED_SKILL_IDS.get() or set())\n if str(item or \"\").strip()\n }\n allowed = {item[\"id\"] for item in available_agent_skills()}\n if clean_id not in allowed:\n return f\"Agent Skill 读取失败:{clean_id} 不是前端允许的主要 Skill。\"\n if selected_ids and clean_id not in selected_ids:\n return f\"Agent Skill 读取失败:{clean_id} 本轮未在前端 Skill 按钮中选择。\"\n skill_file = AGENT_SKILLS_DIR / clean_id / \"SKILL.md\"\n if not skill_file.exists():\n return f\"Agent Skill 读取失败:未找到 {clean_id}/SKILL.md。\"\n try:\n text = skill_file.read_text(encoding=\"utf-8\", errors=\"ignore\").strip()\n except Exception as exc:\n return f\"Agent Skill 读取失败:{clean_id} 无法读取,原因:{_clean_search_text(exc, 160)}。\"\n if not text:\n return f\"Agent Skill 读取失败:{clean_id}/SKILL.md 内容为空。\"\n referenced_files = sorted(\n {\n match\n for match in re.findall(r\"`([^`]+?\\.(?:md|csv|json|py|txt))`\", text, flags=re.IGNORECASE)\n if not match.startswith((\"/\", \"http://\", \"https://\")) and \"..\" not in match\n }\n )\n refs_text = \"\\n\".join(f\"- {item}\" for item in referenced_files[:12]) or \"- 未发现显式引用文件\"\n return (\n f\"[Agent Skill: {clean_id}]\\n\"\n f\"路径:{skill_file.relative_to(ROOT).as_posix()}\\n\"\n \"已读取完整 SKILL.md。若下列引用文件与本轮任务相关,应继续用本地检索/读取工具追溯,而不是只停留在 Skill 标题。\\n\"\n f\"引用文件线索:\\n{refs_text}\\n\\n\"\n f\"{text[:20000]}\"\n )\n\n\n@tool\ndef list_local_datasets() -> str:\n \"\"\"列出小竞 AI 后端当前可检索和读取的本地数据集。\n 当用户问“你能访问哪些数据”“数据放哪里”“有哪些内部/外部数据”,或你准备做趋势分析、问数、核验前需要了解可用数据时,先使用此工具。\n \"\"\"\n selected_ids = _selected_dataset_ids()\n datasets = list_knowledge_datasets(dataset_ids=selected_ids)\n if not datasets:\n return \"当前未选择任何本地数据库;本轮 AI 不会读取、列举或猜测未选择的数据库内容。请在前端数据库按钮中选择需要发送给 AI 的数据库。\"\n lines = [\"本轮已选择并可发送给 AI 的本地数据库:\"]\n references = []\n links = []\n for index, dataset in enumerate(datasets, 1):\n entrypoints = dataset.get(\"entrypoints\") or []\n files = dataset.get(\"files\") or []\n primary = next((item for item in files if item.get(\"name\") in entrypoints), files[0] if files else None)\n primary_link = {\"label\": dataset.get(\"title\") or dataset.get(\"id\"), \"url\": primary.get(\"url\")} if primary else None\n lines.append(\n \"\\n\".join(\n [\n f\"[数据集 {index}] {dataset.get('title') or dataset.get('id')}\",\n f\"- id: {dataset.get('id')}\",\n f\"- 类型: {dataset.get('source_type') or 'local'}\",\n f\"- 范围: {dataset.get('scope') or '未说明'}\",\n f\"- 简介: {dataset.get('summary') or '未说明'}\",\n f\"- 标签: {'、'.join(dataset.get('tags') or []) or '无'}\",\n f\"- 入口文件: {', '.join(entrypoints) or '未指定'}\",\n f\"- 文件夹: {dataset.get('folder')}\",\n ]\n )\n )\n if primary_link:\n links.append(primary_link)\n references.append({\"index\": index, \"source\": dataset.get(\"title\") or dataset.get(\"id\"), \"links\": [primary_link]})\n meta_data = {\"type\": \"meta\", \"sources\": [d.get(\"title\") for d in datasets], \"links\": links, \"references\": references}\n return \"\\n\\n\".join(lines) + f\"\\n