mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-15 19:32:20 +03:00
fix(dashboard): expose Volcano console cookie field and unwrap error objects (#13107)
Both halves are real. A quota fetcher that needs console cookies with no way to enter them is unusable on any headless or remote install, and `new Error($'{'}object{'}'}` rendering `[object Object]` in a toast is exactly what makes a route-guard rejection unreadable.
---
Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the other 19 PRs of this batch. Two in-batch conflicts, both additive and resolved by keeping each side: the `ENVIRONMENT.md` table (#13035 + #13011) and the `chatHelpers.ts` import block (#12975 on the tip + #13017).
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK; `check:docs-counts` migrations ✓
- complexity 2816 / baseline 3218 and cognitive-complexity 1271 / baseline 1437 — both under baseline
- 531 of 532 focused assertions green across the batch's 46 test files
- `check-file-size` rebaselined for the batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_houminxi`, landed on #13038), attributed per PR
The single red is **not this batch**: `tests/unit/combo/quota-weighted-strategy.test.ts` → "A/B isolation: 7 hard-empty + 2 at 0.5% + 1 at 40%, floor=1" asserts an order between two connections of identical weight and flakes on the pure tip too — 2 failures in 4 runs at `origin/release/v3.8.51` with nothing from this batch applied.
⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, untouched here).
Thanks @HouMinXi — the live evidence on these (X500 logs, `storage.sqlite` state, real `/v1/models` probes, the 36-minute outage write-up) is what let a 20-PR batch be reviewed as a unit.
This commit is contained in:
1
changelog.d/fixes/13107-volcengine-console-cookie.md
Normal file
1
changelog.d/fixes/13107-volcengine-console-cookie.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(dashboard):** expose the Volcano Ark console cookie on quota scraping and unwrap connect-error objects so the dashboard shows the upstream message
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Button, Input, Modal } from "@/shared/components";
|
||||
import { providerText, type ProviderMessageTranslator } from "../providerPageHelpers";
|
||||
import { extractErrorMessage } from "@/shared/utils/upstreamError";
|
||||
|
||||
/**
|
||||
* VolcengineConnectModal — phone/SMS-code login for the Volcano Engine console.
|
||||
@@ -66,6 +67,16 @@ function isTerminal(phase: SessionPhase | undefined): boolean {
|
||||
return !!phase && TERMINAL_PHASES.includes(phase);
|
||||
}
|
||||
|
||||
function extractModalError(data: unknown, fallback: string): string {
|
||||
const record = data && typeof data === "object" ? (data as Record<string, unknown>) : null;
|
||||
return (
|
||||
extractErrorMessage(record?.error) ||
|
||||
(typeof record?.error === "string" ? record.error : null) ||
|
||||
(typeof record?.message === "string" ? record.message : null) ||
|
||||
fallback
|
||||
);
|
||||
}
|
||||
|
||||
type VolcengineConnectModalProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
@@ -218,7 +229,7 @@ export default function VolcengineConnectModal({
|
||||
});
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!response.ok || !data?.success || !data?.session) {
|
||||
throw new Error(data?.error || "Failed to start Volcano login");
|
||||
throw new Error(extractModalError(data, "Failed to start Volcano login"));
|
||||
}
|
||||
setSession(data.session);
|
||||
setResendCountdown(
|
||||
@@ -269,7 +280,7 @@ export default function VolcengineConnectModal({
|
||||
void onConnected();
|
||||
}
|
||||
} else {
|
||||
throw new Error(data?.error || "Failed to submit verification code");
|
||||
throw new Error(extractModalError(data, "Failed to submit verification code"));
|
||||
}
|
||||
} catch (error) {
|
||||
notify.error(error instanceof Error ? error.message : "Failed to submit verification code");
|
||||
|
||||
@@ -379,16 +379,13 @@ export default function EditConnectionModal({
|
||||
quotaPerUnit: existingQuotaPerUnit,
|
||||
glmOrganizationId: existingGlmOrganizationId,
|
||||
glmProjectId: existingGlmProjectId,
|
||||
// Console-session credentials are stripped from API responses
|
||||
// (sanitizeProviderSpecificDataForResponse), so there is nothing to
|
||||
// round-trip: start empty and let "blank keeps the stored value" hold —
|
||||
// the quota-scraping assign skips empty fields and the PUT merge
|
||||
// preserves keys the payload does not carry.
|
||||
// Console-session credentials stripped in responses; blank preserves stored values.
|
||||
ollamaCloudUsageCookie: "",
|
||||
alibabaConsoleCookie: "",
|
||||
qwenCloudCookie: "",
|
||||
qwenCloudSecToken: "",
|
||||
alibabaConsoleSecToken: "",
|
||||
volcConsoleCookie: "",
|
||||
ccCompatibleContext1m: ccRequestDefaults.context1m,
|
||||
ccCompatibleRedactThinking: ccRequestDefaults.redactThinking,
|
||||
ccCompatibleSummarizeThinking: ccRequestDefaults.summarizeThinking,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
assignQuotaScrapingProviderData,
|
||||
EMPTY_QUOTA_SCRAPING_FIELDS,
|
||||
QWEN_TOKEN_PLAN_PROVIDERS,
|
||||
VOLCENGINE_PLAN_PROVIDERS,
|
||||
type QuotaScrapingFieldValues,
|
||||
} from "./quotaScrapingFieldValues";
|
||||
|
||||
@@ -149,5 +150,33 @@ export default function QuotaScrapingFields({
|
||||
);
|
||||
}
|
||||
|
||||
if (VOLCENGINE_PLAN_PROVIDERS.has(provider ?? "")) {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 rounded-lg border border-border/50 bg-surface/20 p-4">
|
||||
<Input
|
||||
label={providerText(t, "volcConsoleCookieLabel", "Volcano Engine console cookie")}
|
||||
name="volcConsoleCookie"
|
||||
type="password"
|
||||
value={values.volcConsoleCookie}
|
||||
onChange={(e) => onChange({ volcConsoleCookie: e.target.value })}
|
||||
placeholder="session=...; AccountID=..."
|
||||
hint={providerText(
|
||||
t,
|
||||
"volcConsoleCookieHint",
|
||||
editMode
|
||||
? "Leave blank to keep the stored cookie. To rotate, paste the updated cookie string from console.volcengine.com."
|
||||
: "Required for Volcano Ark Plan quota -- the inference API key cannot read it. " +
|
||||
"How to get it: log in to console.volcengine.com, open Developer Tools (F12), " +
|
||||
"run document.cookie (or inspect Network headers), and paste the cookie string here. " +
|
||||
"It expires with your browser session; re-paste when quota reports an expired session."
|
||||
)}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
autoCapitalize="off"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -12,12 +12,19 @@ import { getProviderConnectionFamilyIds } from "@/shared/constants/providers";
|
||||
/** Providers whose quota lives behind the Qwen/Model Studio console gateway (#9603). */
|
||||
export const QWEN_TOKEN_PLAN_PROVIDERS = new Set(["qwen-cloud-token-plan", "bailian-coding-plan"]);
|
||||
|
||||
/** Providers whose quota lives behind the Volcano Engine console gateway. */
|
||||
export const VOLCENGINE_PLAN_PROVIDERS = new Set([
|
||||
"volcengine-coding-plan",
|
||||
"volcengine-agent-plan",
|
||||
]);
|
||||
|
||||
export type QuotaScrapingFieldValues = {
|
||||
ollamaCloudUsageCookie: string;
|
||||
alibabaConsoleCookie: string;
|
||||
alibabaConsoleSecToken: string;
|
||||
qwenCloudCookie: string;
|
||||
qwenCloudSecToken: string;
|
||||
volcConsoleCookie: string;
|
||||
};
|
||||
|
||||
export const EMPTY_QUOTA_SCRAPING_FIELDS: QuotaScrapingFieldValues = {
|
||||
@@ -26,6 +33,7 @@ export const EMPTY_QUOTA_SCRAPING_FIELDS: QuotaScrapingFieldValues = {
|
||||
alibabaConsoleSecToken: "",
|
||||
qwenCloudCookie: "",
|
||||
qwenCloudSecToken: "",
|
||||
volcConsoleCookie: "",
|
||||
};
|
||||
|
||||
export function assignQuotaScrapingProviderData(
|
||||
@@ -51,5 +59,7 @@ export function assignQuotaScrapingProviderData(
|
||||
if (values.qwenCloudSecToken?.trim()) {
|
||||
target.qwenCloudSecToken = values.qwenCloudSecToken.trim();
|
||||
}
|
||||
} else if (VOLCENGINE_PLAN_PROVIDERS.has(provider ?? "") && values.volcConsoleCookie?.trim()) {
|
||||
target.volcConsoleCookie = values.volcConsoleCookie.trim();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,6 +407,7 @@ export function validateProviderSpecificData(
|
||||
"alibabaConsoleSecToken",
|
||||
"qwenCloudCookie",
|
||||
"qwenCloudSecToken",
|
||||
"volcConsoleCookie",
|
||||
] as const) {
|
||||
const value = data[key];
|
||||
if (value !== undefined && value !== null && typeof value !== "string") {
|
||||
|
||||
100
tests/unit/volcengine-plan-cookie-field.test.ts
Normal file
100
tests/unit/volcengine-plan-cookie-field.test.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* volcengine-plan-cookie-field.test.ts — Volcano Ark Coding/Agent Plan
|
||||
* quota fetchers are cookie-authenticated (API keys cannot query the console
|
||||
* quota API). Expose volcConsoleCookie in QuotaScrapingFields so users can
|
||||
* paste their console cookie from the dashboard without needing local browser automation.
|
||||
*/
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
EMPTY_QUOTA_SCRAPING_FIELDS,
|
||||
assignQuotaScrapingProviderData,
|
||||
} from "../../src/app/(dashboard)/dashboard/providers/[id]/components/modals/quotaScrapingFieldValues.ts";
|
||||
import { extractErrorMessage } from "../../src/shared/utils/upstreamError.ts";
|
||||
const { updateProviderConnectionSchema } = await import("../../src/shared/validation/schemas.ts");
|
||||
|
||||
test("volcengine-coding-plan and volcengine-agent-plan persist the console cookie", () => {
|
||||
for (const provider of ["volcengine-coding-plan", "volcengine-agent-plan"]) {
|
||||
const target: Record<string, unknown> = {};
|
||||
|
||||
assignQuotaScrapingProviderData(
|
||||
provider,
|
||||
{
|
||||
...EMPTY_QUOTA_SCRAPING_FIELDS,
|
||||
volcConsoleCookie: " session=volc-123; AccountID=acc-456 ",
|
||||
},
|
||||
target
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
target.volcConsoleCookie,
|
||||
"session=volc-123; AccountID=acc-456",
|
||||
`cookie must be stored trimmed for ${provider}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("a blank volcConsoleCookie does not overwrite the stored one", () => {
|
||||
for (const provider of ["volcengine-coding-plan", "volcengine-agent-plan"]) {
|
||||
const target: Record<string, unknown> = {};
|
||||
|
||||
assignQuotaScrapingProviderData(
|
||||
provider,
|
||||
{ ...EMPTY_QUOTA_SCRAPING_FIELDS, volcConsoleCookie: " " },
|
||||
target
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
Object.hasOwn(target, "volcConsoleCookie"),
|
||||
false,
|
||||
"blank input must leave the stored cookie untouched"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("a form object without volcConsoleCookie does not throw", () => {
|
||||
const target: Record<string, unknown> = {};
|
||||
const partial = { ...EMPTY_QUOTA_SCRAPING_FIELDS } as Record<string, string>;
|
||||
delete partial.volcConsoleCookie;
|
||||
|
||||
for (const provider of ["volcengine-coding-plan", "volcengine-agent-plan"]) {
|
||||
assert.doesNotThrow(() =>
|
||||
assignQuotaScrapingProviderData(
|
||||
provider,
|
||||
partial as unknown as typeof EMPTY_QUOTA_SCRAPING_FIELDS,
|
||||
target
|
||||
)
|
||||
);
|
||||
}
|
||||
assert.equal(Object.hasOwn(target, "volcConsoleCookie"), false);
|
||||
});
|
||||
|
||||
test("providerSpecificData validation guards the volcConsoleCookie field", () => {
|
||||
const ok = updateProviderConnectionSchema.safeParse({
|
||||
providerSpecificData: { volcConsoleCookie: "session=volc-abc" },
|
||||
});
|
||||
assert.equal(ok.success, true, JSON.stringify(ok.error?.issues));
|
||||
|
||||
const wrongType = updateProviderConnectionSchema.safeParse({
|
||||
providerSpecificData: { volcConsoleCookie: 42 },
|
||||
});
|
||||
assert.equal(wrongType.success, false, "non-string cookie must be rejected");
|
||||
|
||||
const tooLong = updateProviderConnectionSchema.safeParse({
|
||||
providerSpecificData: { volcConsoleCookie: "x".repeat(10_001) },
|
||||
});
|
||||
assert.equal(tooLong.success, false, "oversized cookie must be rejected");
|
||||
});
|
||||
|
||||
test("extractErrorMessage extracts message from structured error objects instead of [object Object]", () => {
|
||||
const localOnlyError = {
|
||||
code: "LOCAL_ONLY",
|
||||
message: "This endpoint requires localhost access",
|
||||
};
|
||||
const extracted = extractErrorMessage(localOnlyError);
|
||||
assert.equal(extracted, "This endpoint requires localhost access");
|
||||
|
||||
const stringError = "Failed to start Volcano login";
|
||||
assert.equal(extractErrorMessage(stringError), null);
|
||||
});
|
||||
Reference in New Issue
Block a user