import { existsSync, readFileSync, readdirSync } from "node:fs"; import { extname, join } from "node:path"; import type { ProductProfile } from "../product/profile.ts"; import { type CommandSpec, formatCommandResult, runCommand } from "../official/kingdee-skills.ts"; import { resolveWorkspacePath, workspacePathBlockReason } from "../platform/path.ts"; export interface BuildPlan { profile: string; command: CommandSpec; reason: string; } export interface DebugFinding { severity: "error" | "warning" | "info"; rule: string; message: string; evidence: string; nextStep: string; } export function planBuild(cwd: string, profile: ProductProfile, target?: string): BuildPlan { if (profile.product === "unknown") { throw new Error("产品画像未知。运行 kd_build 前必须设置 product。"); } if (profile.platform === "cosmic") return planJavaBuild(cwd, profile, target); if (profile.platform === "enterprise-csharp") return planCsharpBuild(cwd, profile, target); if (profile.platform === "enterprise-python") { throw new Error( "企业版 Python 插件通常没有本地构建步骤。LLM 不能替代 BOS 注册和功能测试;必须要求用户提供验证结果,并在 VERIFY.md 记录脚本路径、插件类型、FormId、字段/实体标识、测试数据和用户侧验证证据。", ); } throw new Error(`未找到 ${profile.product}/${profile.platform}/${profile.techStack} 的构建策略。`); } export async function runBuild(plan: BuildPlan, dryRun?: boolean) { if (dryRun) { return { content: [{ type: "text" as const, text: formatBuildPlan(plan) }], details: { dryRun: true, command: plan.command.display, reason: plan.reason }, }; } const result = await runCommand(plan.command, 10 * 60 * 1000); return { content: [{ type: "text" as const, text: `${formatBuildPlan(plan)}\n\n${formatCommandResult(result)}` }], details: { dryRun: false, command: result.command, exitCode: result.exitCode, reason: plan.reason }, }; } export function analyzeDebugText(text: string): DebugFinding[] { const findings: DebugFinding[] = []; const lines = text.split(/\r?\n/); pushIfMatch(findings, lines, /NullPointerException|Object reference not set/i, { severity: "error", rule: "null-pointer", message: "检测到空指针/空引用异常。", nextStep: "检查堆栈附近的字段元数据、可空字段、DynamicObject 取值和 DataSet 行读取。", }); pushIfMatch(findings, lines, /ClassNotFoundException|NoClassDefFoundError|FileNotFoundException.*\.jar/i, { severity: "error", rule: "classpath", message: "检测到类或 jar 依赖缺失。", nextStep: "检查模块依赖、biz/cuslib 部署,以及是否使用了正确产品/版本的 SDK。", }); pushIfMatch(findings, lines, /DataSet.*closed|ResultSet.*closed|connection.*closed/i, { severity: "error", rule: "dataset-scope", message: "检测到已关闭的 DataSet/connection 被继续使用。", nextStep: "确保 DataSet 处理位于 try-with-resources 内,并处于创建它的事务/资源作用域内。", }); pushIfMatch(findings, lines, /SQLSyntaxErrorException|PSQLException|syntax error|invalid column|column .* does not exist/i, { severity: "error", rule: "sql-metadata", message: "检测到 SQL 或元数据不匹配。", nextStep: "使用 kd_cosmic_metadata 并设置 sql=true 查证表名、数据库字段、枚举值和 dbName/dbKey。", }); pushIfMatch(findings, lines, /deadlock|lock wait timeout|could not serialize access|并发|死锁/i, { severity: "error", rule: "transaction-lock", message: "检测到事务锁或并发问题。", nextStep: "检查事务范围、跨库写入、更新顺序,以及外部调用是否放在事务内。", }); pushIfMatch(findings, lines, /OutOfMemoryError|GC overhead|Java heap space|内存溢出/i, { severity: "error", rule: "memory", message: "检测到内存压力或 OOM。", nextStep: "检查全表查询、无限集合、过深字段路径,以及大 DataSet/物化加载。", }); pushIfMatch(findings, lines, /StackOverflowError|propertyChanged/i, { severity: "warning", rule: "event-recursion", message: "可能存在事件递归触发。", nextStep: "检查 propertyChanged/setValue 循环,写字段前比较新旧值。", }); pushIfMatch(findings, lines, /Gradle|Compilation failed|javac|CS\d{4}|MSB\d{4}|BUILD FAILED|BUILD FAILURE/i, { severity: "error", rule: "build-error", message: "检测到构建或编译错误。", nextStep: "使用产品对应的构建输出。Cosmic Java 必须查证 SDK 签名;企业版 C# 必须查证命名空间和引用。", }); if (findings.length === 0) { findings.push({ severity: "info", rule: "no-known-pattern", message: "未匹配到已知金蝶调试模式。", evidence: "", nextStep: "补充完整日志、堆栈、产品/版本、目标单据/表单和最近代码改动。", }); } return findings; } export function formatDebugFindings(findings: DebugFinding[]): string { return findings .map((finding) => [ `[${finding.severity}] ${finding.rule}`, finding.message, finding.evidence ? `证据:${finding.evidence}` : undefined, `下一步:${finding.nextStep}`, ] .filter(Boolean) .join("\n"), ) .join("\n\n"); } export function readDebugInput(cwd: string, text?: string, path?: string): { source: string; text: string } { if (text) return { source: "inline", text }; if (!path) throw new Error("kd_debug 参数要求:提供 text 或 path。"); const blockReason = workspacePathBlockReason(cwd, path, "读取调试输入"); if (blockReason) throw new Error(blockReason); const fullPath = resolveWorkspacePath(cwd, path); return { source: path, text: readFileSync(fullPath, "utf8") }; } function planJavaBuild(cwd: string, profile: ProductProfile, target?: string): BuildPlan { const task = target ?? "build"; const gradlewBat = join(cwd, "gradlew.bat"); const gradlew = join(cwd, "gradlew"); if (existsSync(gradlewBat)) { return buildPlan(profile, gradlewBat, [task], cwd, "检测到 Cosmic 家族 Java 项目的 Gradle wrapper。"); } if (existsSync(gradlew)) { return buildPlan(profile, gradlew, [task], cwd, "检测到 Cosmic 家族 Java 项目的 Gradle wrapper。"); } if (existsSync(join(cwd, "build.gradle")) || existsSync(join(cwd, "build.gradle.kts"))) { return buildPlan(profile, "gradle", [task], cwd, "检测到 Gradle 构建文件,但未找到 wrapper。"); } throw new Error("未找到 Java Gradle 构建入口。期望在工作区根目录看到 gradlew、gradlew.bat、build.gradle 或 build.gradle.kts。"); } function planCsharpBuild(cwd: string, profile: ProductProfile, target?: string): BuildPlan { if (target) { const blockReason = workspacePathBlockReason(cwd, target, "构建"); if (blockReason) throw new Error(blockReason); } const explicit = target ? resolveWorkspacePath(cwd, target) : undefined; if (explicit && existsSync(explicit)) { return buildPlan(profile, "dotnet", ["build", explicit], cwd, "使用显式指定的 C# 项目或解决方案。"); } const candidates = readdirSync(cwd).filter((entry) => [".sln", ".csproj"].includes(extname(entry).toLowerCase())); if (candidates.length > 0) { return buildPlan(profile, "dotnet", ["build", join(cwd, candidates[0])], cwd, `检测到企业版 C# 项目文件 ${candidates[0]}。`); } throw new Error("未找到 C# 构建入口。期望在工作区根目录看到 .sln 或 .csproj,或通过 target 指定路径。"); } function buildPlan(profile: ProductProfile, executable: string, args: string[], cwd: string, reason: string): BuildPlan { return { profile: `${profile.product}/${profile.platform}/${profile.techStack}`, command: { executable, args, cwd, display: [executable, ...args].map(formatArg).join(" "), }, reason, }; } function formatBuildPlan(plan: BuildPlan): string { return [`产品:${plan.profile}`, `原因:${plan.reason}`, `命令:${plan.command.display}`].join("\n"); } function formatArg(value: string): string { return /[\s"'&|<>]/.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value; } function pushIfMatch( findings: DebugFinding[], lines: string[], pattern: RegExp, template: Omit, ): void { const line = lines.find((candidate) => pattern.test(candidate)); if (!line) return; findings.push({ ...template, evidence: line.trim().slice(0, 500) }); }