import { existsSync, readdirSync, statSync } from "node:fs"; import { join } from "node:path"; import type { ActiveRun } from "./types.ts"; import { runRoot } from "./paths.ts"; export type MetadataDiscoveryKind = "evidence" | "project-file" | "mcp-config" | "official-config"; export interface MetadataDiscoveryItem { kind: MetadataDiscoveryKind; path: string; action: string; } export interface MetadataDiscoveryResult { items: MetadataDiscoveryItem[]; recommendedAction: string; } const METADATA_FILE_PATTERN = /(fkernelxml|fdata|metadata|entitydesign|meta)[^\\/]*\.(xml|json|txt)$/i; const IGNORED_DIRS = new Set([".git", ".pi", ".codex", ".agents", "node_modules", "dist", "vendor", ".tmp"]); export function discoverMetadataSources(cwd: string, run: ActiveRun): MetadataDiscoveryResult { const items: MetadataDiscoveryItem[] = []; const root = runRoot(cwd, run); for (const evidence of ["evidence/cosmic-metadata.json", "evidence/data-source.md"]) { const path = join(root, evidence); if (existsSync(path)) { items.push({ kind: "evidence", path: evidence, action: `复用 ${evidence},必要时读取并抽取 FormId、字段、实体、表名和插件事件。`, }); } } for (const path of findMetadataFiles(cwd, 80)) { items.push({ kind: "project-file", path, action: `优先使用 kd_metadata_collect path=${path} 或 kcode meta ${path} 自动写入当前产品所需 evidence;仅需预览时再用 kd_metadata_parse。`, }); } if (existsSync(join(cwd, ".pi", "kd", "mcp-db-connect.yml"))) { items.push({ kind: "mcp-config", path: ".pi/kd/mcp-db-connect.yml", action: "使用已配置数据库 MCP 只读查询目标业务库元数据;查询结果必须用 kd_metadata_parse 或 kcode metadata --evidence 解析后才能编码。", }); } if (existsSync(join(cwd, ".mcp.json")) || existsSync(join(cwd, ".pi", "mcp.json"))) { items.push({ kind: "mcp-config", path: existsSync(join(cwd, ".mcp.json")) ? ".mcp.json" : ".pi/mcp.json", action: "检查项目 MCP 配置中是否已有只读数据库 server;禁止使用跨库或超级用户连接。", }); } if (existsSync(join(cwd, "ok-cosmic.json"))) { items.push({ kind: "official-config", path: "ok-cosmic.json", action: "优先使用 kd_cosmic_config 和 kd_cosmic_metadata 基于官方 Cosmic 配置查证表单/字段元数据。", }); } return { items, recommendedAction: recommendedMetadataAction(items, run), }; } export function formatMetadataDiscovery(result: MetadataDiscoveryResult): string { return [ `recommendedAction:${result.recommendedAction}`, "items:", result.items.length > 0 ? result.items.map((item) => `- [${item.kind}] ${item.path}:${item.action}`).join("\n") : "- 未发现本地 metadata evidence、项目元数据文件、MCP 配置或 ok-cosmic.json。", ].join("\n"); } function recommendedMetadataAction(items: MetadataDiscoveryItem[], run: ActiveRun): string { if (items.some((item) => item.kind === "evidence")) return "先读取已有 evidence,不足时再查项目元数据或 MCP。"; if (items.some((item) => item.kind === "official-config") && run.profile?.platform === "cosmic") { return "先运行 kd_cosmic_metadata 查询目标 FormId/字段;缺项目定制元数据时再查 MCP 或本地 fdata。"; } if (items.some((item) => item.kind === "project-file")) return "先用 kd_metadata_collect/kcode meta 自动采集本地元数据文件并写入 evidence。"; if (items.some((item) => item.kind === "mcp-config")) return "先使用已配置只读 MCP 查询元数据,再解析为 evidence。"; return "先搜索项目中的 FKERNELXML/fdata/MCP 导出文件;找不到再用 kd_ask_user 确认 FormId 和字段线索。"; } function findMetadataFiles(cwd: string, limit: number): string[] { const found: string[] = []; walk(cwd, ".", found, limit, 0); return found.sort((left, right) => left.localeCompare(right)); } function walk(cwd: string, relativeDir: string, found: string[], limit: number, depth: number): void { if (found.length >= limit || depth > 4) return; const absoluteDir = join(cwd, relativeDir); let entries; try { entries = readdirSync(absoluteDir, { withFileTypes: true }); } catch { return; } for (const entry of entries) { if (found.length >= limit) return; const relativePath = relativeDir === "." ? entry.name : join(relativeDir, entry.name); if (entry.isDirectory()) { if (IGNORED_DIRS.has(entry.name)) continue; walk(cwd, relativePath, found, limit, depth + 1); continue; } if (!entry.isFile()) continue; if (!METADATA_FILE_PATTERN.test(relativePath)) continue; try { if (statSync(join(cwd, relativePath)).size > 5 * 1024 * 1024) continue; } catch { continue; } found.push(relativePath.replace(/\\/g, "/")); } }