feat(POSTV1-02): 增加AI真实生成探测
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
Dada P0-A isolated Windows CI / validate-and-package (push) Waiting to run
This commit is contained in:
@@ -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<AiRuntimeProbeResult> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { parentPort } from "node:worker_threads";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
import { WorkerAiCallGate } from "./ai-call-gate.js";
|
import { WorkerAiCallGate } from "./ai-call-gate.js";
|
||||||
|
import { runAiRuntimeProbe } from "./ai-runtime-probe.js";
|
||||||
import { GenerationProcessor } from "./generation-processor.js";
|
import { GenerationProcessor } from "./generation-processor.js";
|
||||||
import { OneApiGenerationAdapter } from "./oneapi-generation-adapter.js";
|
import { OneApiGenerationAdapter } from "./oneapi-generation-adapter.js";
|
||||||
import { readConfiguredLocalDataRoot } from "./runtime-config.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 credentialClient = initializeWorkerCredentialClient(await receiveWorkerCredentials());
|
||||||
const controlPipeIndex = process.argv.indexOf("--dada-control-pipe");
|
const controlPipeIndex = process.argv.indexOf("--dada-control-pipe");
|
||||||
const controlPipe = process.argv[controlPipeIndex + 1];
|
const controlPipe = process.argv[controlPipeIndex + 1];
|
||||||
|
|||||||
@@ -170,6 +170,8 @@ internal static class Program
|
|||||||
True(leakProbe.SensitiveOutputDetected, "credential echo must be detected");
|
True(leakProbe.SensitiveOutputDetected, "credential echo must be detected");
|
||||||
Equal(string.Empty, leakProbe.StandardOutput, "credential echo output discarded");
|
Equal(string.Empty, leakProbe.StandardOutput, "credential echo output discarded");
|
||||||
Equal(string.Empty, leakProbe.StandardError, "credential echo error 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[]
|
var externalArguments = new[]
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Dada.Supervisor;
|
||||||
|
|
||||||
|
internal static class AiGatewayProbe
|
||||||
|
{
|
||||||
|
internal static async Task<int> 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<string>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ internal static class OfflineCommandRouter
|
|||||||
return args[0] switch
|
return args[0] switch
|
||||||
{
|
{
|
||||||
"configure" => RunConfigure(args.Skip(1).ToArray()),
|
"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),
|
"admin-allowlist" => RunAdminAllowlist(args.Skip(1).ToArray(), credentials),
|
||||||
"doctor" when args.Length == 1 => RunDoctor(credentials),
|
"doctor" when args.Length == 1 => RunDoctor(credentials),
|
||||||
"validate-external" => await ControlledExternalValidationLauncher.RunAsync(args.Skip(1).ToArray(), credentials),
|
"validate-external" => await ControlledExternalValidationLauncher.RunAsync(args.Skip(1).ToArray(), credentials),
|
||||||
@@ -66,7 +66,7 @@ internal static class OfflineCommandRouter
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int RunSecrets(string[] args, ICredentialStore store)
|
private static async Task<int> RunSecretsAsync(string[] args, ICredentialStore store)
|
||||||
{
|
{
|
||||||
if (args.Length != 2 || !TryResolveCredential(args[1], out var target)) return Usage();
|
if (args.Length != 2 || !TryResolveCredential(args[1], out var target)) return Usage();
|
||||||
switch (args[0])
|
switch (args[0])
|
||||||
@@ -86,6 +86,8 @@ internal static class OfflineCommandRouter
|
|||||||
return 0;
|
return 0;
|
||||||
case "probe" when target == CredentialCatalog.ApiAmap:
|
case "probe" when target == CredentialCatalog.ApiAmap:
|
||||||
return AmapProbe.Run(store.Read(target));
|
return AmapProbe.Run(store.Read(target));
|
||||||
|
case "probe" when target == CredentialCatalog.WorkerAiGateway:
|
||||||
|
return await AiGatewayProbe.RunAsync(store);
|
||||||
default:
|
default:
|
||||||
return Usage();
|
return Usage();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import type { GenerationAdapterRequest } from "../../apps/worker/src/ai-adapter-contract.js";
|
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";
|
import { OneApiGenerationAdapter } from "../../apps/worker/src/oneapi-generation-adapter.js";
|
||||||
|
|
||||||
function request(overrides: Partial<GenerationAdapterRequest> = {}): GenerationAdapterRequest {
|
function request(overrides: Partial<GenerationAdapterRequest> = {}): GenerationAdapterRequest {
|
||||||
@@ -63,4 +64,22 @@ describe("POSTV1-02 OneAPI runtime adapter", () => {
|
|||||||
status: "failed",
|
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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user