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:
Bob.Hou
2026-09-11 18:28:17 -04:00
committed by GitHub
parent 0cd88ec998
commit 171421439d
7 changed files with 156 additions and 7 deletions

View 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

View File

@@ -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");

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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") {

View 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);
});