import { execFile } from "node:child_process"; import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; import { extname, join, relative } from "node:path"; import { promisify } from "node:util"; import { resolveWorkspacePath, workspacePathBlockReason } from "../platform/path.ts"; const execFileAsync = promisify(execFile); export type SdkSignatureLanguage = "java" | "csharp"; export interface SdkSignatureParams { language: SdkSignatureLanguage; query?: string; className?: string; method?: string; path?: string; limit?: number; } export interface SdkSignatureResult { language: SdkSignatureLanguage; query: string; exitCode: number; stdout: string; stderr: string; sources: string[]; } const SKIP_DIRS = new Set([ ".git", ".gradle", ".idea", ".pi", ".tmp", "dist", "node_modules", "out", "target", ]); export async function inspectSdkSignature(cwd: string, params: SdkSignatureParams): Promise { const query = (params.className || params.query || "").trim(); if (!query) { return { language: params.language, query, exitCode: 2, stdout: "", stderr: "kd_sdk_signature 参数要求:提供 query 或 className。method 仅在已匹配类/类型内部过滤。", sources: [], }; } if (params.path) { const blockReason = workspacePathBlockReason(cwd, params.path, "读取 SDK 签名来源"); if (blockReason) { return { language: params.language, query, exitCode: 2, stdout: "", stderr: blockReason, sources: [], }; } } if (params.language === "java") return inspectJavaSignature(cwd, params, query); return inspectCsharpSignature(cwd, params, query); } export function formatSdkSignatureResult(result: SdkSignatureResult): string { return [ `语言:${result.language}`, `查询:${result.query}`, `退出码:${result.exitCode}`, result.sources.length ? `来源:\n${result.sources.map((source) => `- ${source}`).join("\n")}` : "来源:无", result.stdout.trim() ? `\nSTDOUT:\n${result.stdout.trim()}` : undefined, result.stderr.trim() ? `\nSTDERR:\n${result.stderr.trim()}` : undefined, "", "证据规则:只有退出码为 0 时,该结果才能作为本地 SDK 证据。失败时必须使用构建输出或项目 SDK 配置查证;禁止用随包知识库替代。", ] .filter(Boolean) .join("\n"); } async function inspectJavaSignature(cwd: string, params: SdkSignatureParams, query: string): Promise { const roots = scanRoots(cwd, params.path); const jars = roots.flatMap((root) => findFiles(root, ".jar", params.limit ?? 200)); if (jars.length === 0) { return { language: "java", query, exitCode: 2, stdout: "", stderr: "当前项目未找到 jar 文件。执行构建/复制依赖,或传入 path=。", sources: [], }; } const classNames = params.className ? [params.className] : await findJavaClasses(jars, query, params.limit ?? 20); if (classNames.length === 0) { return { language: "java", query, exitCode: 1, stdout: "", stderr: `项目 jar 中未找到匹配 "${query}" 的类。`, sources: jars.map((jar) => relativeOrSelf(cwd, jar)).slice(0, 20), }; } const classpath = jars.join(process.platform === "win32" ? ";" : ":"); const outputs: string[] = []; const errors: string[] = []; const inspectedSources = new Set(); for (const className of classNames.slice(0, params.limit ?? 10)) { try { const result = await execFileAsync("javap", ["-classpath", classpath, "-public", className], { cwd, timeout: 60_000, maxBuffer: 1024 * 1024 * 4, }); const text = filterMethodLines(result.stdout || "", params.method); if (text.trim()) outputs.push(`## ${className}\n${text.trim()}`); for (const source of jarsContainingClass(jars, className)) inspectedSources.add(relativeOrSelf(cwd, source)); } catch (error) { const err = error as { message?: string; stderr?: string }; errors.push(`${className}: ${err.stderr || err.message || String(error)}`); } } return { language: "java", query, exitCode: outputs.length ? 0 : 1, stdout: outputs.join("\n\n"), stderr: errors.join("\n").trim(), sources: [...inspectedSources].sort(), }; } async function inspectCsharpSignature(cwd: string, params: SdkSignatureParams, query: string): Promise { const roots = scanRoots(cwd, params.path); const dlls = roots.flatMap((root) => findFiles(root, ".dll", params.limit ?? 200)); if (dlls.length === 0) { return { language: "csharp", query, exitCode: 2, stdout: "", stderr: "当前项目未找到 dll 文件。执行构建/还原引用,或传入 path=。", sources: [], }; } const script = [ "$ErrorActionPreference = 'SilentlyContinue'", "$query = $args[0]", "$method = $args[1]", "$dlls = $args[2..($args.Length-1)]", "$hits = New-Object System.Collections.Generic.List[string]", "foreach ($dll in $dlls) {", " try { $asm = [System.Reflection.Assembly]::LoadFrom($dll) } catch { continue }", " try { $types = $asm.GetExportedTypes() } catch { continue }", " foreach ($type in $types) {", " if ($type.FullName -notlike \"*$query*\" -and $type.Name -notlike \"*$query*\") { continue }", " $hits.Add(\"## \" + $type.FullName + \"`nAssembly: \" + $dll)", " foreach ($m in $type.GetMethods([System.Reflection.BindingFlags]'Public,Instance,Static,DeclaredOnly')) {", " if ($method -and $m.Name -notlike \"*$method*\") { continue }", " $params = ($m.GetParameters() | ForEach-Object { $_.ParameterType.Name + ' ' + $_.Name }) -join ', '", " $hits.Add(' ' + $m.ReturnType.Name + ' ' + $m.Name + '(' + $params + ')')", " }", " foreach ($p in $type.GetProperties([System.Reflection.BindingFlags]'Public,Instance,Static,DeclaredOnly')) {", " if ($method -and $p.Name -notlike \"*$method*\") { continue }", " $hits.Add(' property ' + $p.PropertyType.Name + ' ' + $p.Name)", " }", " }", "}", "$hits | Select-Object -First 200", ].join("; "); try { const executable = process.platform === "win32" ? "powershell" : "pwsh"; const result = await execFileAsync(executable, ["-NoProfile", "-Command", script, query, params.method ?? "", ...dlls], { cwd, timeout: 60_000, maxBuffer: 1024 * 1024 * 4, }); const stdout = result.stdout || ""; return { language: "csharp", query, exitCode: stdout.trim() ? 0 : 1, stdout, stderr: stdout.trim() ? "" : `项目 dll 中未找到匹配 "${query}" 的公开类型。`, sources: dlls.map((dll) => relativeOrSelf(cwd, dll)).slice(0, 20), }; } catch (error) { const err = error as { message?: string; stderr?: string }; return { language: "csharp", query, exitCode: 1, stdout: "", stderr: err.stderr || err.message || String(error), sources: dlls.map((dll) => relativeOrSelf(cwd, dll)).slice(0, 20), }; } } async function findJavaClasses(jars: string[], query: string, limit: number): Promise { const basenameQuery = query.includes(".") ? query.split(".").at(-1) || query : query; const classPattern = `${basenameQuery}.class`; const results = new Set(); for (const jar of jars) { try { const output = await execFileAsync("jar", ["tf", jar], { timeout: 30_000, maxBuffer: 1024 * 1024 * 4 }); for (const line of (output.stdout || "").split(/\r?\n/)) { if (!line.endsWith(".class") || line.includes("$")) continue; if (!line.toLowerCase().includes(classPattern.toLowerCase())) continue; results.add(line.replace(/\.class$/, "").replace(/\//g, ".")); if (results.size >= limit) return [...results]; } } catch { const matched = findClassNamesInJarBytes(jar, basenameQuery, limit - results.size); for (const className of matched) results.add(className); if (results.size >= limit) return [...results]; } } return [...results]; } function findClassNamesInJarBytes(jar: string, query: string, limit: number): string[] { const data = readFileSync(jar).toString("latin1"); const pattern = `${query}.class`; const results = new Set(); let index = data.indexOf(pattern); while (index >= 0 && results.size < limit) { let start = index; while (start > 0 && /[A-Za-z0-9_/$-]/.test(data[start - 1])) start--; const entry = data.slice(start, index + pattern.length); if (entry.includes("/") && !entry.includes("$")) { results.add(entry.replace(/\.class$/, "").replace(/\//g, ".")); } index = data.indexOf(pattern, index + pattern.length); } return [...results]; } function jarsContainingClass(jars: string[], className: string): string[] { const entry = `${className.replace(/\./g, "/")}.class`; return jars.filter((jar) => { try { const data = readFileSync(jar).toString("latin1"); return data.includes(entry); } catch { return false; } }); } function filterMethodLines(output: string, method?: string): string { if (!method) return output; const lines = output.split(/\r?\n/); const kept = lines.filter((line) => line.includes(`${method}(`) || /^(Compiled from|public class|public interface)/.test(line.trim())); return kept.join("\n"); } function scanRoots(cwd: string, path?: string): string[] { if (path) { const blockReason = workspacePathBlockReason(cwd, path, "读取 SDK 签名来源"); if (blockReason) return []; const resolved = resolveWorkspacePath(cwd, path); return existsSync(resolved) ? [resolved] : []; } return [cwd]; } function findFiles(root: string, extension: string, limit: number): string[] { if (existsSync(root)) { try { const stat = statSync(root); if (stat.isFile()) return extname(root).toLowerCase() === extension ? [root] : []; } catch { return []; } } const results: string[] = []; walk(root, extension, results, limit, 0); return results; } function walk(dir: string, extension: string, results: string[], limit: number, depth: number): void { if (results.length >= limit || depth > 8 || !existsSync(dir)) return; let entries: string[]; try { entries = readdirSync(dir); } catch { return; } for (const entry of entries) { if (results.length >= limit) return; const full = join(dir, entry); let stat; try { stat = statSync(full); } catch { continue; } if (stat.isDirectory()) { if (!SKIP_DIRS.has(entry)) walk(full, extension, results, limit, depth + 1); continue; } if (stat.isFile() && extname(entry).toLowerCase() === extension) results.push(full); } } function relativeOrSelf(cwd: string, path: string): string { const rel = relative(cwd, path); return rel && !rel.startsWith("..") ? rel : path; }