Merge pull request #1201 from pikasTech/fix/issue-1195-billing-summary-route

修复 v0.3 用户账本 summary 规格路由
This commit is contained in:
Lyon
2026-06-14 13:47:54 +08:00
committed by GitHub
2 changed files with 17 additions and 7 deletions
+7 -6
View File
@@ -315,7 +315,7 @@ async function handleRestAdapter(request, response, url, options) {
return; return;
} }
if (url.pathname === "/v1/usage/summary" && request.method === "GET") { if ((url.pathname === "/v1/usage/summary" || url.pathname === "/v1/billing/summary") && request.method === "GET") {
await handleUsageSummaryHttp(request, response, url, options); await handleUsageSummaryHttp(request, response, url, options);
return; return;
} }
@@ -982,21 +982,22 @@ async function codeAgentOptions(request, response, options, authOptions = {}) {
} }
async function handleUsageSummaryHttp(request, response, url, options) { async function handleUsageSummaryHttp(request, response, url, options) {
const route = url.pathname === "/v1/billing/summary" ? "/v1/billing/summary" : "/v1/usage/summary";
const nextOptions = await codeAgentOptions(request, response, options, { required: true }); const nextOptions = await codeAgentOptions(request, response, options, { required: true });
if (!nextOptions) return; if (!nextOptions) return;
const client = nextOptions.userBillingClient; const client = nextOptions.userBillingClient;
const token = bearerTokenFromRequest(request); const token = bearerTokenFromRequest(request);
if (nextOptions.userBillingAuth?.active !== true || !token) { if (nextOptions.userBillingAuth?.active !== true || !token) {
sendRestError(request, response, 403, "user_billing_auth_required", "usage summary requires a user-billing bearer token", { sendRestError(request, response, 403, "user_billing_auth_required", "usage summary requires a user-billing bearer token", {
operation: "GET /v1/usage/summary", operation: `GET ${route}`,
target: { type: "route", id: "/v1/usage/summary" }, target: { type: "route", id: route },
result: "rejected" result: "rejected"
}); });
return; return;
} }
if (!client?.configured || typeof client.billingSummary !== "function") { if (!client?.configured || typeof client.billingSummary !== "function") {
sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing summary is not configured", { sendRestError(request, response, 503, "user_billing_not_configured", "HWLAB user-billing summary is not configured", {
operation: "GET /v1/usage/summary", operation: `GET ${route}`,
target: { type: "service", id: "hwlab-user-billing" } target: { type: "service", id: "hwlab-user-billing" }
}); });
return; return;
@@ -1005,7 +1006,7 @@ async function handleUsageSummaryHttp(request, response, url, options) {
const result = await client.billingSummary(token, { limit: Math.min(limit, 100) }); const result = await client.billingSummary(token, { limit: Math.min(limit, 100) });
if (!result.ok) { if (!result.ok) {
sendRestError(request, response, result.status ?? 502, result.error?.code ?? "user_billing_summary_failed", result.error?.message ?? "user-billing summary request failed", { sendRestError(request, response, result.status ?? 502, result.error?.code ?? "user_billing_summary_failed", result.error?.message ?? "user-billing summary request failed", {
operation: "GET /v1/usage/summary", operation: `GET ${route}`,
target: { type: "service", id: "hwlab-user-billing" }, target: { type: "service", id: "hwlab-user-billing" },
reason: result.error?.code, reason: result.error?.code,
result: "failed" result: "failed"
@@ -1016,7 +1017,7 @@ async function handleUsageSummaryHttp(request, response, url, options) {
...(result.body ?? {}), ...(result.body ?? {}),
proxy: { proxy: {
serviceId: CLOUD_API_SERVICE_ID, serviceId: CLOUD_API_SERVICE_ID,
route: "/v1/usage/summary", route,
source: "hwlab-user-billing", source: "hwlab-user-billing",
valuesRedacted: true valuesRedacted: true
} }
@@ -677,6 +677,10 @@ test("cloud api proxies R1 user-billing API key and profile self-service routes"
calls.push({ op: "listApiKeys", tokenPrefix: token.slice(0, 8) }); calls.push({ op: "listApiKeys", tokenPrefix: token.slice(0, 8) });
return { ok: true, status: 200, body: { contractVersion: "user-api-key-v1", keys: [{ id: "key_self", prefix: "hwl_self", status: "active" }], count: 1 } }; return { ok: true, status: 200, body: { contractVersion: "user-api-key-v1", keys: [{ id: "key_self", prefix: "hwl_self", status: "active" }], count: 1 } };
}, },
async billingSummary(token, query = {}) {
calls.push({ op: "billingSummary", tokenPrefix: token.slice(0, 8), query });
return { ok: true, status: 200, body: { contractVersion: "user-billing-summary-v1", credits: { balance: 5, reserved: 1, available: 4 }, usage: { totalCredits: 0, recordCount: 0 }, valuesRedacted: true } };
},
async createApiKey(token, body = {}) { async createApiKey(token, body = {}) {
calls.push({ op: "createApiKey", tokenPrefix: token.slice(0, 8), body }); calls.push({ op: "createApiKey", tokenPrefix: token.slice(0, 8), body });
assert.equal(body.name, "Self key"); assert.equal(body.name, "Self key");
@@ -713,6 +717,11 @@ test("cloud api proxies R1 user-billing API key and profile self-service routes"
const list = await fetch(`http://127.0.0.1:${port}/v1/api-keys`, { headers }); const list = await fetch(`http://127.0.0.1:${port}/v1/api-keys`, { headers });
assert.equal(list.status, 200); assert.equal(list.status, 200);
assert.equal((await list.json()).proxy.source, "hwlab-user-billing"); assert.equal((await list.json()).proxy.source, "hwlab-user-billing");
const billingSummary = await fetch(`http://127.0.0.1:${port}/v1/billing/summary?limit=7`, { headers });
assert.equal(billingSummary.status, 200);
const billingSummaryBody = await billingSummary.json();
assert.equal(billingSummaryBody.contractVersion, "user-billing-summary-v1");
assert.equal(billingSummaryBody.proxy.route, "/v1/billing/summary");
const create = await fetch(`http://127.0.0.1:${port}/v1/api-keys`, { method: "POST", headers, body: JSON.stringify({ name: "Self key" }) }); const create = await fetch(`http://127.0.0.1:${port}/v1/api-keys`, { method: "POST", headers, body: JSON.stringify({ name: "Self key" }) });
assert.equal(create.status, 201); assert.equal(create.status, 201);
const update = await fetch(`http://127.0.0.1:${port}/v1/api-keys/key_created`, { method: "PATCH", headers, body: JSON.stringify({ name: "Renamed key" }) }); const update = await fetch(`http://127.0.0.1:${port}/v1/api-keys/key_created`, { method: "PATCH", headers, body: JSON.stringify({ name: "Renamed key" }) });
@@ -724,7 +733,7 @@ test("cloud api proxies R1 user-billing API key and profile self-service routes"
const password = await fetch(`http://127.0.0.1:${port}/v1/users/me/password`, { method: "POST", headers, body: JSON.stringify({ currentPassword: "old-password-1127", newPassword: "new-password-1127" }) }); const password = await fetch(`http://127.0.0.1:${port}/v1/users/me/password`, { method: "POST", headers, body: JSON.stringify({ currentPassword: "old-password-1127", newPassword: "new-password-1127" }) });
assert.equal(password.status, 200); assert.equal(password.status, 200);
assert.equal(JSON.stringify(calls).includes(bearer), false); assert.equal(JSON.stringify(calls).includes(bearer), false);
assert.deepEqual(calls.map((call) => call.op), ["introspect", "listApiKeys", "introspect", "createApiKey", "introspect", "updateApiKey", "introspect", "revokeApiKey", "introspect", "updateProfile", "introspect", "changePassword"]); assert.deepEqual(calls.map((call) => call.op), ["introspect", "listApiKeys", "introspect", "billingSummary", "introspect", "createApiKey", "introspect", "updateApiKey", "introspect", "revokeApiKey", "introspect", "updateProfile", "introspect", "changePassword"]);
} finally { } finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve()))); await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
} }