From 4ec8327f5e7420c21401afaa2b75dc01f0ab7ff0 Mon Sep 17 00:00:00 2001 From: suyx Date: Tue, 4 Aug 2026 14:52:23 +0800 Subject: [PATCH] feat: add controlled Amap credential probe --- scripts/run-wp7-04-validation.mjs | 29 ++++++---- supervisor/Dada.Supervisor/AmapProbe.cs | 57 +++++++++++++++++++ .../Dada.Supervisor/OfflineCommandRouter.cs | 4 +- 3 files changed, 77 insertions(+), 13 deletions(-) create mode 100644 supervisor/Dada.Supervisor/AmapProbe.cs diff --git a/scripts/run-wp7-04-validation.mjs b/scripts/run-wp7-04-validation.mjs index f8c14dd..6cede54 100644 --- a/scripts/run-wp7-04-validation.mjs +++ b/scripts/run-wp7-04-validation.mjs @@ -28,35 +28,40 @@ const build = run("pnpm", ["build:workspace-packages"]); const contract = build.exit_code === 0 ? run("pnpm", ["exec", "vitest", "run", "tests/api/wp4-04-location.test.ts", "tests/api/wp6-03-services.test.ts"]) : { command: "pnpm exec vitest run tests/api/wp4-04-location.test.ts tests/api/wp6-03-services.test.ts", exit_code: 1, finished_at: new Date().toISOString(), started_at: new Date().toISOString() }; -const external = run("pnpm", ["validate:external", "--", "--service", "amap", "--run-id", runId]); +const supervisorBuild = run("dotnet", ["build", "supervisor/Dada.Supervisor/Dada.Supervisor.csproj", "--configuration", "Release"]); +const supervisorExe = resolve("supervisor", "Dada.Supervisor", "bin", "Release", "net8.0-windows", "Dada.Supervisor.exe"); +const external = supervisorBuild.exit_code === 0 && existsSync(supervisorExe) + ? run(supervisorExe, ["secrets", "probe", "api-amap"]) + : { command: `${supervisorExe} secrets probe api-amap`, exit_code: 3, finished_at: new Date().toISOString(), started_at: new Date().toISOString(), stderr: "supervisor_probe_unavailable", stdout: "" }; const trace = run("pnpm", ["validate:tdd-trace"]); const commands = [build, contract, external, trace].map(({ command, exit_code, finished_at, started_at }) => ({ command, exit_code, finished_at, started_at })); const parsedExternal = (() => { - try { return JSON.parse(external.stdout.trim()); } catch { return { status: "invalid_output" }; } + try { return JSON.parse(external.stdout.trim().split(/\r?\n/).at(-1) ?? ""); } catch { return { status: "invalid_output" }; } })(); -const blocker = "real_amap_credentials_absent"; +const probePassed = external.exit_code === 0 && parsedExternal.code === "amap_probe_passed" && parsedExternal.real_calls === 2; +const blocker = probePassed ? "manual_acceptance_required" : (parsedExternal.code ?? "real_amap_validation_failed"); const contractPassed = contract.exit_code === 0; const tracePassed = trace.exit_code === 0; writeFileSync(resolve(caseDirectory, "amap-contract.json"), `${JSON.stringify({ checks: { - location_and_reverse_geocode: "mock_contract_passed", + location_and_reverse_geocode: probePassed ? "real_probe_passed" : "not_verified", qps_and_allowlist: "not_verified", security_binding: "not_verified", - dyn004_hard_stop: "mock_contract_passed", + dyn004_hard_stop: "contract_passed", monthly_hard_limit_1000: "not_verified", }, - mode: "controlled_real", - real_calls: 0, + mode: probePassed ? "controlled_real" : "blocked", + real_calls: parsedExternal.real_calls ?? 0, status: "externally_blocked", blocker, schema_version: "1.0", }, null, 2)}\n`); writeFileSync(resolve(caseDirectory, "external-calls.json"), `${JSON.stringify({ blocker, - mode: parsedExternal.mode ?? "mock", - real_calls: 0, + mode: probePassed ? "controlled_real" : "blocked", + real_calls: parsedExternal.real_calls ?? 0, service: "amap", - source_command_status: parsedExternal.status ?? "unknown", + source_command_status: parsedExternal.code ?? "unknown", status: "externally_blocked", schema_version: "1.0", }, null, 2)}\n`); @@ -69,7 +74,7 @@ writeFileSync(resolve(caseDirectory, "redaction.json"), `${JSON.stringify({ }, null, 2)}\n`); writeFileSync(resolve(caseDirectory, "manual-review.json"), `${JSON.stringify({ blocker, - required_before_release: ["controlled_amap_key", "security_binding", "allowlist_confirmation", "real_location_and_reverse_geocode", "qps_and_1000_call_hard_stop"], + required_before_release: probePassed ? ["security_binding", "allowlist_confirmation", "qps_and_1000_call_hard_stop"] : ["controlled_amap_key", "security_binding", "allowlist_confirmation", "real_location_and_reverse_geocode", "qps_and_1000_call_hard_stop"], status: "externally_blocked", schema_version: "1.0", }, null, 2)}\n`); @@ -78,7 +83,7 @@ const result = { automation: ["controlled_real", "manual_review"], blocker, contract_regression: contractPassed ? "passed" : "failed", - external_calls: 0, + external_calls: parsedExternal.real_calls ?? 0, finished_at: new Date().toISOString(), manifest: { path: "tasks.manifest.json", sha256: createHash("sha256").update(readFileSync("tasks.manifest.json")).digest("hex").toUpperCase() }, missing_evidence: [], diff --git a/supervisor/Dada.Supervisor/AmapProbe.cs b/supervisor/Dada.Supervisor/AmapProbe.cs new file mode 100644 index 0000000..5a40075 --- /dev/null +++ b/supervisor/Dada.Supervisor/AmapProbe.cs @@ -0,0 +1,57 @@ +using System.Net; +using System.Text.Json; + +namespace Dada.Supervisor; + +internal static class AmapProbe +{ + private static readonly HttpClient Client = new() { Timeout = TimeSpan.FromSeconds(15) }; + + internal static int Run(string? key) + { + if (string.IsNullOrWhiteSpace(key)) + { + Write("amap_credentials_missing", false, 0, null, null); + return 3; + } + + try + { + var reverse = Call("https://restapi.amap.com/v3/geocode/regeo?location=120.6994,27.9943&extensions=base", key).GetAwaiter().GetResult(); + var geocode = Call($"https://restapi.amap.com/v3/geocode/geo?address={Uri.EscapeDataString("北京市天安门")}", key).GetAwaiter().GetResult(); + var success = reverse.Status == "1" && geocode.Status == "1"; + Write(success ? "amap_probe_passed" : "amap_provider_rejected", success, 2, reverse.Status, geocode.Status); + return success ? 0 : 3; + } + catch (TaskCanceledException) + { + Write("amap_probe_timeout", false, 0, null, null); + return 3; + } + catch (HttpRequestException exception) + { + Write($"amap_probe_network_{exception.StatusCode?.ToString() ?? "error"}", false, 0, null, null); + return 3; + } + catch (JsonException) + { + Write("amap_probe_invalid_response", false, 0, null, null); + return 3; + } + } + + private static async Task Call(string endpoint, string key) + { + using var response = await Client.GetAsync($"{endpoint}&key={Uri.EscapeDataString(key)}"); + response.EnsureSuccessStatusCode(); + await using var stream = await response.Content.ReadAsStreamAsync(); + using var document = await JsonDocument.ParseAsync(stream); + var root = document.RootElement; + return new ProbeResponse(root.GetProperty("status").GetString() ?? "", root.TryGetProperty("infocode", out var code) ? code.GetString() : null); + } + + private static void Write(string code, bool success, int realCalls, string? reverseStatus, string? geocodeStatus) => + Console.WriteLine(JsonSerializer.Serialize(new { code, success, real_calls = realCalls, reverse_status = reverseStatus, geocode_status = geocodeStatus })); + + private sealed record ProbeResponse(string Status, string? InfoCode); +} diff --git a/supervisor/Dada.Supervisor/OfflineCommandRouter.cs b/supervisor/Dada.Supervisor/OfflineCommandRouter.cs index 05700b5..a92fc58 100644 --- a/supervisor/Dada.Supervisor/OfflineCommandRouter.cs +++ b/supervisor/Dada.Supervisor/OfflineCommandRouter.cs @@ -83,6 +83,8 @@ internal static class OfflineCommandRouter store.Write(target, value); WriteResult("credential_saved", true); return 0; + case "probe" when target == CredentialCatalog.ApiAmap: + return AmapProbe.Run(store.Read(target)); default: return Usage(); } @@ -198,7 +200,7 @@ internal static class OfflineCommandRouter private static int Usage() { - WriteResult("usage: configure init|data-root|asset-root; secrets set|status|clear; admin-allowlist add|remove|status; doctor", false); + WriteResult("usage: configure init|data-root|asset-root; secrets set|status|clear|probe; admin-allowlist add|remove|status; doctor", false); return 2; } }