feat(console): add batch workspace block endpoints (#48491)

This commit is contained in:
黑墨水鱼 2026-09-11 21:06:26 +08:00 committed by GitHub
parent 193de13a88
commit 5de2f241c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 86 additions and 1 deletions

View file

@ -0,0 +1,22 @@
import type { APIEvent } from "@solidjs/start/server"
import { Workspace } from "@opencode-ai/console-core/workspace.js"
import { safeEqual } from "@opencode-ai/console-core/util/crypto.js"
import { Resource } from "@opencode-ai/console-resource"
import z from "zod"
const Body = z.object({ workspaceIDs: z.array(z.string().startsWith("wrk_")).min(1) })
export async function POST(event: APIEvent) {
if (!safeEqual(event.request.headers.get("authorization") ?? "", `Bearer ${Resource.SUPPORT_API_KEY.value}`)) {
return Response.json({ error: "Unauthorized" }, { status: 401 })
}
const body = Body.safeParse(await event.request.json().catch(() => undefined))
if (!body.success) {
return Response.json({ error: "Invalid request", issues: body.error.issues }, { status: 400 })
}
// Missing IDs do not fail the batch; they are reported per item in the result.
return Workspace.blockBatch(body.data)
.then((result) => Response.json({ success: true, message: "Workspaces blocked", result }))
.catch((error) => Response.json({ error: error instanceof Error ? error.message : String(error) }, { status: 400 }))
}

View file

@ -0,0 +1,22 @@
import type { APIEvent } from "@solidjs/start/server"
import { Workspace } from "@opencode-ai/console-core/workspace.js"
import { safeEqual } from "@opencode-ai/console-core/util/crypto.js"
import { Resource } from "@opencode-ai/console-resource"
import z from "zod"
const Body = z.object({ workspaceIDs: z.array(z.string().startsWith("wrk_")).min(1) })
export async function POST(event: APIEvent) {
if (!safeEqual(event.request.headers.get("authorization") ?? "", `Bearer ${Resource.SUPPORT_API_KEY.value}`)) {
return Response.json({ error: "Unauthorized" }, { status: 401 })
}
const body = Body.safeParse(await event.request.json().catch(() => undefined))
if (!body.success) {
return Response.json({ error: "Invalid request", issues: body.error.issues }, { status: 400 })
}
// Missing IDs do not fail the batch; they are reported per item in the result.
return Workspace.unblockBatch(body.data)
.then((result) => Response.json({ success: true, message: "Workspaces unblocked", result }))
.catch((error) => Response.json({ error: error instanceof Error ? error.message : String(error) }, { status: 400 }))
}

View file

@ -8,7 +8,7 @@ import { BillingTable } from "./schema/billing.sql"
import { WorkspaceTable } from "./schema/workspace.sql"
import { AccountTable } from "./schema/account.sql"
import { Key } from "./key"
import { and, eq, isNull, sql } from "drizzle-orm"
import { and, eq, inArray, isNull, sql } from "drizzle-orm"
export namespace Workspace {
export const Region = z.enum(["us", "eu", "sg", "cn"])
@ -109,6 +109,26 @@ export namespace Workspace {
},
)
export const blockBatch = fn(
z.object({
workspaceIDs: z.array(Identifier.schema("workspace")).min(1),
}),
async (input) => {
const { applied, notFound } = await setBlockedBatch(input, true)
return { blocked: applied, notFound }
},
)
export const unblockBatch = fn(
z.object({
workspaceIDs: z.array(Identifier.schema("workspace")).min(1),
}),
async (input) => {
const { applied, notFound } = await setBlockedBatch(input, false)
return { unblocked: applied, notFound }
},
)
export const remove = fn(z.void(), async () => {
await Database.use((tx) =>
tx
@ -118,3 +138,24 @@ export namespace Workspace {
)
})
}
// No size cap by design; large IN lists degrade on Vitess, so chunks stay fixed-size and
// sequential to share one connection inside ambient transactions. Existence comes from the
// follow-up select rather than rowsAffected, so no-op rows (already in the target state)
// still report as applied instead of "not found".
const setBlockedBatch = async (input: { workspaceIDs: string[] }, isBlocked: boolean) => {
const ids = [...new Set(input.workspaceIDs)]
return await Database.use(async (tx) => {
const applied = new Set<string>()
for (let index = 0; index < ids.length; index += 500) {
const chunk = ids.slice(index, index + 500)
await tx.update(WorkspaceTable).set({ is_blocked: isBlocked }).where(inArray(WorkspaceTable.id, chunk))
const rows = await tx.select({ id: WorkspaceTable.id }).from(WorkspaceTable).where(inArray(WorkspaceTable.id, chunk))
for (const row of rows) applied.add(row.id)
}
return {
applied: ids.filter((id) => applied.has(id)),
notFound: ids.filter((id) => !applied.has(id)),
}
})
}