mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
feat(console): keep migrated teams on the new Console (#46830)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { getRequestEvent } from "solid-js/web"
|
||||
import { and, Database, eq, inArray, isNull, sql } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
||||
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
|
||||
import { redirect } from "@solidjs/router"
|
||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
|
||||
@@ -79,8 +80,15 @@ export const getActor = async (workspace?: string): Promise<Actor.Info> => {
|
||||
if (accounts.length) {
|
||||
const user = await Database.use((tx) =>
|
||||
tx
|
||||
.select()
|
||||
.select({
|
||||
id: UserTable.id,
|
||||
workspaceID: UserTable.workspaceID,
|
||||
accountID: UserTable.accountID,
|
||||
role: UserTable.role,
|
||||
migratedAt: WorkspaceTable.migrated_at,
|
||||
})
|
||||
.from(UserTable)
|
||||
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID))
|
||||
.where(
|
||||
and(
|
||||
eq(UserTable.workspaceID, workspace),
|
||||
@@ -93,6 +101,15 @@ export const getActor = async (workspace?: string): Promise<Actor.Info> => {
|
||||
.then((x) => x[0]),
|
||||
)
|
||||
if (user) {
|
||||
if (user.migratedAt) {
|
||||
const destination = Resource.ConsoleMigration.consoleUrl
|
||||
if (!destination) throw new Error("New Console URL is not configured")
|
||||
evt.response.headers.set("Cache-Control", "no-store")
|
||||
throw redirect(`${destination}/login`, {
|
||||
status: evt.request.method === "GET" || evt.request.method === "HEAD" ? 302 : 303,
|
||||
headers: { "Cache-Control": "no-store" },
|
||||
})
|
||||
}
|
||||
await Database.use((tx) =>
|
||||
tx
|
||||
.update(UserTable)
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import { Database, eq } from "@opencode-ai/console-core/drizzle/index.js"
|
||||
import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
|
||||
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
|
||||
|
||||
const paths: Record<string, string | undefined> = {
|
||||
"GET /zen/v1/models": "/openai/v1/models",
|
||||
"POST /zen/v1/chat/completions": "/openai/v1/chat/completions",
|
||||
"POST /zen/v1/responses": "/openai/v1/responses",
|
||||
"POST /zen/v1/messages": "/anthropic/v1/messages",
|
||||
}
|
||||
|
||||
export async function proxyInference(request: Request, clientIP?: string): Promise<Response | undefined> {
|
||||
const url = new URL(request.url)
|
||||
const path =
|
||||
paths[`${request.method} ${url.pathname}`] ??
|
||||
(request.method === "POST" &&
|
||||
/^\/zen\/v1\/models\/[^/]+:(?:generateContent|streamGenerateContent)$/.test(url.pathname)
|
||||
? url.pathname.replace("/zen/v1/models/", "/google/v1beta/models/")
|
||||
: undefined)
|
||||
if (!path) return undefined
|
||||
|
||||
const key = path.startsWith("/anthropic/")
|
||||
? request.headers.get("x-api-key")
|
||||
: path.startsWith("/google/")
|
||||
? request.headers.get("x-goog-api-key")
|
||||
: request.headers.get("authorization")?.split(" ")[1]
|
||||
if (!key || key === "public") return undefined
|
||||
|
||||
// Routing only; the destination owns authentication and revocation after cutover.
|
||||
const workspace = await Database.use((tx) =>
|
||||
tx
|
||||
.select({ migratedAt: WorkspaceTable.migrated_at })
|
||||
.from(KeyTable)
|
||||
.innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID))
|
||||
.where(eq(KeyTable.key, key))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
if (!workspace?.migratedAt) return undefined
|
||||
|
||||
const destination = new URL(Resource.ConsoleMigration.inferenceUrl)
|
||||
destination.pathname = `${destination.pathname.replace(/\/$/, "")}${path}`
|
||||
destination.search = url.search
|
||||
destination.hash = ""
|
||||
|
||||
const forwarded = new Request(destination, request)
|
||||
forwarded.headers.set("authorization", `Bearer ${key}`)
|
||||
const ip = request.headers.get("cf-connecting-ip") ?? clientIP
|
||||
if (ip) forwarded.headers.set("x-real-ip", ip)
|
||||
const requestID = request.headers.get("x-opencode-request-id") ?? request.headers.get("x-opencode-request")
|
||||
if (requestID) forwarded.headers.set("x-opencode-request-id", requestID)
|
||||
|
||||
return fetch(forwarded, { redirect: "error" })
|
||||
}
|
||||
@@ -2,9 +2,10 @@ import { createMiddleware } from "@solidjs/start/middleware"
|
||||
import { LOCALE_HEADER, cookie, fromPathname, strip } from "~/lib/language"
|
||||
import { normalizeReferralCode, referralCookie } from "~/lib/referral-invite"
|
||||
import { sanitizeServerActionRequest } from "~/lib/server-action"
|
||||
import { proxyInference } from "~/lib/inference-proxy"
|
||||
|
||||
export default createMiddleware({
|
||||
onRequest(event) {
|
||||
async onRequest(event) {
|
||||
event.request = sanitizeServerActionRequest(event.request)
|
||||
|
||||
const url = new URL(event.request.url)
|
||||
@@ -19,5 +20,12 @@ export default createMiddleware({
|
||||
|
||||
const referralCode = normalizeReferralCode(url.searchParams.get("ref"))
|
||||
if (referralCode) event.response.headers.append("set-cookie", referralCookie(referralCode))
|
||||
|
||||
return proxyInference(event.request, event.clientAddress).catch(() =>
|
||||
Response.json(
|
||||
{ error: { type: "api_error", message: "Inference routing is unavailable. Please retry later." } },
|
||||
{ status: 503, headers: { "Cache-Control": "no-store" } },
|
||||
),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -38,21 +38,25 @@ const setReload = action(async (form: FormData) => {
|
||||
}
|
||||
|
||||
return json(
|
||||
await Database.use((tx) =>
|
||||
tx
|
||||
.update(BillingTable)
|
||||
.set({
|
||||
reload: reloadValue,
|
||||
...(reloadAmount !== null ? { reloadAmount } : {}),
|
||||
...(reloadTrigger !== null ? { reloadTrigger } : {}),
|
||||
...(reloadValue
|
||||
? {
|
||||
reloadError: null,
|
||||
timeReloadError: null,
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
.where(eq(BillingTable.workspaceID, workspaceID)),
|
||||
await withActor(
|
||||
() =>
|
||||
Database.use((tx) =>
|
||||
tx
|
||||
.update(BillingTable)
|
||||
.set({
|
||||
reload: reloadValue,
|
||||
...(reloadAmount !== null ? { reloadAmount } : {}),
|
||||
...(reloadTrigger !== null ? { reloadTrigger } : {}),
|
||||
...(reloadValue
|
||||
? {
|
||||
reloadError: null,
|
||||
timeReloadError: null,
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
.where(eq(BillingTable.workspaceID, workspaceID)),
|
||||
),
|
||||
workspaceID,
|
||||
),
|
||||
{ revalidate: queryBillingInfo.key },
|
||||
)
|
||||
|
||||
@@ -88,7 +88,7 @@ const updateModel = action(async (form: FormData) => {
|
||||
if (!workspaceID) return { error: formError.workspaceRequired }
|
||||
const enabled = (form.get("enabled") as string | null) === "true"
|
||||
return json(
|
||||
withActor(async () => {
|
||||
await withActor(async () => {
|
||||
if (enabled) {
|
||||
await Model.disable({ model })
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user