From b1f143c238fcfdc40b122028278d27dd8e80d9dc Mon Sep 17 00:00:00 2001 From: suyx Date: Wed, 5 Aug 2026 13:14:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(POSTV1-02):=20=E5=A2=9E=E5=8A=A0AI?= =?UTF-8?q?=E7=9C=9F=E5=AE=9E=E7=94=9F=E6=88=90=E6=8E=A2=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/worker/src/ai-runtime-probe.ts | 47 ++++++++++++ apps/worker/src/worker.ts | 18 ++++- supervisor/Dada.Supervisor.Tests/Program.cs | 2 + supervisor/Dada.Supervisor/AiGatewayProbe.cs | 74 +++++++++++++++++++ .../Dada.Supervisor/OfflineCommandRouter.cs | 6 +- tests/worker/postv1-oneapi-runtime.test.ts | 19 +++++ 6 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 apps/worker/src/ai-runtime-probe.ts create mode 100644 supervisor/Dada.Supervisor/AiGatewayProbe.cs diff --git a/apps/worker/src/ai-runtime-probe.ts b/apps/worker/src/ai-runtime-probe.ts new file mode 100644 index 0000000..dc6fb7a --- /dev/null +++ b/apps/worker/src/ai-runtime-probe.ts @@ -0,0 +1,47 @@ +import type { GenerationAdapter } from "./ai-adapter-contract.js"; + +export type AiRuntimeProbeResult = + | { + code: "ai_probe_passed"; + mime_type: "image/jpeg" | "image/png" | "image/webp"; + pixel_height: number; + pixel_width: number; + real_calls: 1; + success: true; + } + | { + code: "ai_probe_failed"; + error_category: string; + real_calls: 1; + success: false; + }; + +export async function runAiRuntimeProbe(adapter: GenerationAdapter): Promise { + const result = await adapter.start({ + configSnapshot: { probe: true }, + generationId: "00000000-0000-4000-8000-000000000002", + modelId: "gemini-3.1-flash-image-preview", + prompt: "生成一张简洁的红蓝几何色块测试图,不含文字。", + ratio: "1:1", + referenceAssetIds: [], + }); + if (result.status === "failed") { + return { code: "ai_probe_failed", error_category: result.category, real_calls: 1, success: false }; + } + if (result.status !== "completed" || result.outputs.length !== 1) { + return { code: "ai_probe_failed", error_category: "gateway_contract_invalid", real_calls: 1, success: false }; + } + const output = result.outputs[0]!; + try { + return { + code: "ai_probe_passed", + mime_type: output.mimeType, + pixel_height: output.pixelHeight, + pixel_width: output.pixelWidth, + real_calls: 1, + success: true, + }; + } finally { + output.bytes.fill(0); + } +} diff --git a/apps/worker/src/worker.ts b/apps/worker/src/worker.ts index 91171cd..194cb0e 100644 --- a/apps/worker/src/worker.ts +++ b/apps/worker/src/worker.ts @@ -2,6 +2,7 @@ import { parentPort } from "node:worker_threads"; import { join } from "node:path"; import { WorkerAiCallGate } from "./ai-call-gate.js"; +import { runAiRuntimeProbe } from "./ai-runtime-probe.js"; import { GenerationProcessor } from "./generation-processor.js"; import { OneApiGenerationAdapter } from "./oneapi-generation-adapter.js"; import { readConfiguredLocalDataRoot } from "./runtime-config.js"; @@ -23,7 +24,22 @@ if (workerPort) { }); } -if (!workerPort && process.argv.includes("--dada-credential-stdin")) { +if (!workerPort && process.argv.includes("--dada-ai-probe")) { + const credentialClient = initializeWorkerCredentialClient(await receiveWorkerCredentials()); + let adapter: OneApiGenerationAdapter | undefined; + try { + adapter = new OneApiGenerationAdapter({ credential: credentialClient.aiGatewayCredential }); + const result = await runAiRuntimeProbe(adapter); + process.stdout.write(JSON.stringify(result)); + if (!result.success) process.exitCode = 2; + } catch { + process.stdout.write(JSON.stringify({ code: "ai_probe_failed", error_category: "upstream_failed", real_calls: 0, success: false })); + process.exitCode = 2; + } finally { + credentialClient.aiGatewayCredential.fill(0); + adapter?.dispose(); + } +} else if (!workerPort && process.argv.includes("--dada-credential-stdin")) { const credentialClient = initializeWorkerCredentialClient(await receiveWorkerCredentials()); const controlPipeIndex = process.argv.indexOf("--dada-control-pipe"); const controlPipe = process.argv[controlPipeIndex + 1]; diff --git a/supervisor/Dada.Supervisor.Tests/Program.cs b/supervisor/Dada.Supervisor.Tests/Program.cs index 24a0a28..b3bd3b8 100644 --- a/supervisor/Dada.Supervisor.Tests/Program.cs +++ b/supervisor/Dada.Supervisor.Tests/Program.cs @@ -170,6 +170,8 @@ internal static class Program True(leakProbe.SensitiveOutputDetected, "credential echo must be detected"); Equal(string.Empty, leakProbe.StandardOutput, "credential echo output discarded"); Equal(string.Empty, leakProbe.StandardError, "credential echo error discarded"); + True(AiGatewayProbe.TryValidateOutput("{\"code\":\"ai_probe_passed\",\"mime_type\":\"image/png\",\"pixel_height\":1080,\"pixel_width\":1080,\"real_calls\":1,\"success\":true}", out _), "AI probe success output accepted"); + False(AiGatewayProbe.TryValidateOutput("{\"code\":\"ai_probe_passed\",\"raw_body\":\"private\",\"real_calls\":1,\"success\":true}", out _), "AI probe private output rejected"); var externalArguments = new[] { diff --git a/supervisor/Dada.Supervisor/AiGatewayProbe.cs b/supervisor/Dada.Supervisor/AiGatewayProbe.cs new file mode 100644 index 0000000..aa40b2a --- /dev/null +++ b/supervisor/Dada.Supervisor/AiGatewayProbe.cs @@ -0,0 +1,74 @@ +using System.Diagnostics; +using System.Text.Json; + +namespace Dada.Supervisor; + +internal static class AiGatewayProbe +{ + internal static async Task RunAsync(ICredentialStore credentials, CancellationToken cancellationToken = default) + { + var node = Path.Combine(AppContext.BaseDirectory, "runtime", "node.exe"); + var worker = Path.Combine(AppContext.BaseDirectory, "server", "worker.mjs"); + if (!File.Exists(node) || !File.Exists(worker)) return WriteFailure("ai_probe_runtime_missing", 0); + var startInfo = new ProcessStartInfo(node) { WorkingDirectory = AppContext.BaseDirectory }; + startInfo.Environment["DADA_SQLITE_NATIVE_BINDING"] = Path.Combine(AppContext.BaseDirectory, "server", "native", "better_sqlite3.node"); + startInfo.ArgumentList.Add(worker); + startInfo.ArgumentList.Add("--dada-ai-probe"); + startInfo.ArgumentList.Add("--dada-credential-stdin"); + var result = await CredentialProcessLauncher.RunToCompletionAsync(startInfo, ChildRole.Worker, credentials, cancellationToken); + if (result.SensitiveOutputDetected || result.StandardError.Length > 0 || !TryValidateOutput(result.StandardOutput, out var sanitized)) + { + return WriteFailure("ai_probe_runtime_failed", 0); + } + Console.WriteLine(sanitized); + return result.ExitCode; + } + + internal static bool TryValidateOutput(string output, out string sanitized) + { + sanitized = string.Empty; + try + { + using var document = JsonDocument.Parse(output); + var root = document.RootElement; + if (root.ValueKind != JsonValueKind.Object) return false; + var allowed = new HashSet(StringComparer.Ordinal) + { + "code", "error_category", "mime_type", "pixel_height", "pixel_width", "real_calls", "success", + }; + if (root.EnumerateObject().Any(property => !allowed.Contains(property.Name))) return false; + if (!root.TryGetProperty("success", out var success) || success.ValueKind is not (JsonValueKind.True or JsonValueKind.False)) return false; + if (!root.TryGetProperty("real_calls", out var realCalls) || realCalls.ValueKind != JsonValueKind.Number || !realCalls.TryGetInt32(out var count) || count is < 0 or > 1) return false; + var passed = success.GetBoolean(); + var code = root.GetProperty("code").GetString(); + if (passed) + { + if (code != "ai_probe_passed" || count != 1) return false; + var mime = root.GetProperty("mime_type").GetString(); + if (mime is not ("image/jpeg" or "image/png" or "image/webp")) return false; + if (!PositiveDimension(root, "pixel_width") || !PositiveDimension(root, "pixel_height")) return false; + } + else + { + if (code != "ai_probe_failed" || !root.TryGetProperty("error_category", out var category) + || category.ValueKind != JsonValueKind.String || (category.GetString()?.Length ?? 0) is < 1 or > 64) return false; + } + sanitized = JsonSerializer.Serialize(root); + return true; + } + catch (Exception exception) when (exception is JsonException or InvalidOperationException or KeyNotFoundException) + { + return false; + } + } + + private static bool PositiveDimension(JsonElement root, string name) => + root.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.Number + && value.TryGetInt32(out var dimension) && dimension is > 0 and <= 4096; + + private static int WriteFailure(string code, int realCalls) + { + Console.WriteLine(JsonSerializer.Serialize(new { code, real_calls = realCalls, success = false })); + return 2; + } +} diff --git a/supervisor/Dada.Supervisor/OfflineCommandRouter.cs b/supervisor/Dada.Supervisor/OfflineCommandRouter.cs index 03f62cd..7260dd2 100644 --- a/supervisor/Dada.Supervisor/OfflineCommandRouter.cs +++ b/supervisor/Dada.Supervisor/OfflineCommandRouter.cs @@ -26,7 +26,7 @@ internal static class OfflineCommandRouter return args[0] switch { "configure" => RunConfigure(args.Skip(1).ToArray()), - "secrets" => RunSecrets(args.Skip(1).ToArray(), credentials), + "secrets" => await RunSecretsAsync(args.Skip(1).ToArray(), credentials), "admin-allowlist" => RunAdminAllowlist(args.Skip(1).ToArray(), credentials), "doctor" when args.Length == 1 => RunDoctor(credentials), "validate-external" => await ControlledExternalValidationLauncher.RunAsync(args.Skip(1).ToArray(), credentials), @@ -66,7 +66,7 @@ internal static class OfflineCommandRouter return 0; } - private static int RunSecrets(string[] args, ICredentialStore store) + private static async Task RunSecretsAsync(string[] args, ICredentialStore store) { if (args.Length != 2 || !TryResolveCredential(args[1], out var target)) return Usage(); switch (args[0]) @@ -86,6 +86,8 @@ internal static class OfflineCommandRouter return 0; case "probe" when target == CredentialCatalog.ApiAmap: return AmapProbe.Run(store.Read(target)); + case "probe" when target == CredentialCatalog.WorkerAiGateway: + return await AiGatewayProbe.RunAsync(store); default: return Usage(); } diff --git a/tests/worker/postv1-oneapi-runtime.test.ts b/tests/worker/postv1-oneapi-runtime.test.ts index d9ce1d9..8feed89 100644 --- a/tests/worker/postv1-oneapi-runtime.test.ts +++ b/tests/worker/postv1-oneapi-runtime.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import type { GenerationAdapterRequest } from "../../apps/worker/src/ai-adapter-contract.js"; +import { runAiRuntimeProbe } from "../../apps/worker/src/ai-runtime-probe.js"; import { OneApiGenerationAdapter } from "../../apps/worker/src/oneapi-generation-adapter.js"; function request(overrides: Partial = {}): GenerationAdapterRequest { @@ -63,4 +64,22 @@ describe("POSTV1-02 OneAPI runtime adapter", () => { status: "failed", }); }); + + it("returns only a bounded probe summary and wipes generated bytes", async () => { + const bytes = Buffer.from("probe-output"); + const result = await runAiRuntimeProbe({ + async start() { + return { outputs: [{ bytes, mimeType: "image/png", pixelHeight: 1080, pixelWidth: 1080 }], status: "completed" }; + }, + }); + expect(result).toEqual({ + code: "ai_probe_passed", + mime_type: "image/png", + pixel_height: 1080, + pixel_width: 1080, + real_calls: 1, + success: true, + }); + expect(bytes.every((value) => value === 0)).toBe(true); + }); });