feat: import web relay worker from monorepo
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
.wrangler/
|
||||||
|
.dev.vars
|
||||||
1
migrate-add-avatar-id.sql
Normal file
1
migrate-add-avatar-id.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "apikey" ADD COLUMN "avatar_id" TEXT REFERENCES "avatars"("id") ON DELETE SET NULL;
|
||||||
3
migrate-add-emoji-trigger-fields.sql
Normal file
3
migrate-add-emoji-trigger-fields.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE "emojis" ADD COLUMN "trigger_type" TEXT NOT NULL DEFAULT 'message';
|
||||||
|
ALTER TABLE "emojis" ADD COLUMN "trigger_result" TEXT;
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "emojis_avatar_default_unique_idx" ON "emojis" ("avatar_id") WHERE "trigger_type" = 'default';
|
||||||
14
migrate-add-user-file.sql
Normal file
14
migrate-add-user-file.sql
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "user_file" (
|
||||||
|
"id" TEXT PRIMARY KEY NOT NULL,
|
||||||
|
"user_id" TEXT NOT NULL,
|
||||||
|
"filename" TEXT NOT NULL,
|
||||||
|
"original_name" TEXT NOT NULL,
|
||||||
|
"content_type" TEXT NOT NULL,
|
||||||
|
"size" INTEGER NOT NULL,
|
||||||
|
"s3_key" TEXT NOT NULL,
|
||||||
|
"url" TEXT NOT NULL,
|
||||||
|
"uploaded_at" INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_file_user_id_idx" ON "user_file" ("user_id");
|
||||||
31
package.json
Normal file
31
package.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "@sker-pro/web-relay",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "wrangler dev --ip=0.0.0.0",
|
||||||
|
"deploy": "wrangler deploy",
|
||||||
|
"deploy:prod": "wrangler deploy --env prod",
|
||||||
|
"types": "wrangler types --env-interface CloudflareBindings --env dev",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"db:init:local": "wrangler d1 execute clawshow-db --local --file=./schema.sql",
|
||||||
|
"db:init:remote": "wrangler d1 execute clawshow-db --remote --file=./schema.sql",
|
||||||
|
"db:migrate:add-avatar-id:local": "wrangler d1 execute clawshow-db --local --file=./migrate-add-avatar-id.sql",
|
||||||
|
"db:migrate:add-avatar-id:remote": "wrangler d1 execute clawshow-db --remote --file=./migrate-add-avatar-id.sql",
|
||||||
|
"db:migrate:add-user-file:local": "wrangler d1 execute clawshow-db --local --file=./migrate-add-user-file.sql",
|
||||||
|
"db:migrate:add-user-file:remote": "wrangler d1 execute clawshow-db --remote --file=./migrate-add-user-file.sql",
|
||||||
|
"db:migrate:add-emoji-trigger-fields:local": "powershell -ExecutionPolicy Bypass -File ./migrate-add-emoji-trigger-fields.ps1",
|
||||||
|
"db:migrate:add-emoji-trigger-fields:remote": "powershell -ExecutionPolicy Bypass -File ./migrate-add-emoji-trigger-fields.ps1 -Remote"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@better-auth/api-key": "^1.2.0",
|
||||||
|
"@bowong/auth": "git+https://gitea.bowongai.com/bowong/claushow-auth.git#main",
|
||||||
|
"better-auth": "^1.2.0",
|
||||||
|
"drizzle-orm": "^0.44.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@cloudflare/workers-types": "^4.20250312.0",
|
||||||
|
"wrangler": "^4.14.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
132
schema.sql
Normal file
132
schema.sql
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "user" (
|
||||||
|
"id" TEXT PRIMARY KEY NOT NULL,
|
||||||
|
"name" TEXT,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"email_verified" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"image" TEXT,
|
||||||
|
"created_at" INTEGER NOT NULL,
|
||||||
|
"updated_at" INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "user_email_unique" ON "user" ("email");
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "session" (
|
||||||
|
"id" TEXT PRIMARY KEY NOT NULL,
|
||||||
|
"expires_at" INTEGER NOT NULL,
|
||||||
|
"token" TEXT NOT NULL,
|
||||||
|
"created_at" INTEGER NOT NULL,
|
||||||
|
"updated_at" INTEGER NOT NULL,
|
||||||
|
"ip_address" TEXT,
|
||||||
|
"user_agent" TEXT,
|
||||||
|
"user_id" TEXT NOT NULL,
|
||||||
|
FOREIGN KEY ("user_id") REFERENCES "user"("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "session_token_unique" ON "session" ("token");
|
||||||
|
CREATE INDEX IF NOT EXISTS "session_user_id_idx" ON "session" ("user_id");
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "account" (
|
||||||
|
"id" TEXT PRIMARY KEY NOT NULL,
|
||||||
|
"account_id" TEXT NOT NULL,
|
||||||
|
"provider_id" TEXT NOT NULL,
|
||||||
|
"user_id" TEXT NOT NULL,
|
||||||
|
"access_token" TEXT,
|
||||||
|
"refresh_token" TEXT,
|
||||||
|
"id_token" TEXT,
|
||||||
|
"access_token_expires_at" INTEGER,
|
||||||
|
"refresh_token_expires_at" INTEGER,
|
||||||
|
"scope" TEXT,
|
||||||
|
"password" TEXT,
|
||||||
|
"created_at" INTEGER NOT NULL,
|
||||||
|
"updated_at" INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY ("user_id") REFERENCES "user"("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "account_user_id_idx" ON "account" ("user_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "account_provider_account_idx" ON "account" ("provider_id", "account_id");
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "verification" (
|
||||||
|
"id" TEXT PRIMARY KEY NOT NULL,
|
||||||
|
"identifier" TEXT NOT NULL,
|
||||||
|
"value" TEXT NOT NULL,
|
||||||
|
"expires_at" INTEGER NOT NULL,
|
||||||
|
"created_at" INTEGER,
|
||||||
|
"updated_at" INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "verification_identifier_idx" ON "verification" ("identifier");
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "apikey" (
|
||||||
|
"id" TEXT PRIMARY KEY NOT NULL,
|
||||||
|
"config_id" TEXT,
|
||||||
|
"name" TEXT,
|
||||||
|
"start" TEXT,
|
||||||
|
"prefix" TEXT,
|
||||||
|
"key" TEXT NOT NULL,
|
||||||
|
"reference_id" TEXT,
|
||||||
|
"enabled" INTEGER DEFAULT 1,
|
||||||
|
"rate_limit_enabled" INTEGER DEFAULT 1,
|
||||||
|
"rate_limit_time_window" INTEGER,
|
||||||
|
"rate_limit_max" INTEGER,
|
||||||
|
"request_count" INTEGER DEFAULT 0,
|
||||||
|
"remaining" INTEGER,
|
||||||
|
"refill_interval" INTEGER,
|
||||||
|
"refill_amount" INTEGER,
|
||||||
|
"last_refill_at" INTEGER,
|
||||||
|
"last_request" INTEGER,
|
||||||
|
"permissions" TEXT,
|
||||||
|
"metadata" TEXT,
|
||||||
|
"expires_at" INTEGER,
|
||||||
|
"created_at" INTEGER NOT NULL,
|
||||||
|
"updated_at" INTEGER NOT NULL,
|
||||||
|
"avatar_id" TEXT,
|
||||||
|
FOREIGN KEY ("avatar_id") REFERENCES "avatars"("id") ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "apikey_reference_id_idx" ON "apikey" ("reference_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "apikey_key_idx" ON "apikey" ("key");
|
||||||
|
|
||||||
|
-- 形象/表情包集合表
|
||||||
|
CREATE TABLE IF NOT EXISTS "avatars" (
|
||||||
|
"id" TEXT PRIMARY KEY NOT NULL,
|
||||||
|
"user_id" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"created_at" INTEGER NOT NULL,
|
||||||
|
"updated_at" INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "avatars_user_id_idx" ON "avatars" ("user_id");
|
||||||
|
|
||||||
|
-- 表情表
|
||||||
|
CREATE TABLE IF NOT EXISTS "emojis" (
|
||||||
|
"id" TEXT PRIMARY KEY NOT NULL,
|
||||||
|
"avatar_id" TEXT NOT NULL,
|
||||||
|
"label" TEXT NOT NULL,
|
||||||
|
"url" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"trigger_type" TEXT NOT NULL DEFAULT 'message',
|
||||||
|
"trigger_result" TEXT,
|
||||||
|
"sort_order" INTEGER DEFAULT 0,
|
||||||
|
"created_at" INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY ("avatar_id") REFERENCES "avatars"("id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "emojis_avatar_id_idx" ON "emojis" ("avatar_id");
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "emojis_avatar_default_unique_idx" ON "emojis" ("avatar_id") WHERE "trigger_type" = 'default';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "user_file" (
|
||||||
|
"id" TEXT PRIMARY KEY NOT NULL,
|
||||||
|
"user_id" TEXT NOT NULL,
|
||||||
|
"filename" TEXT NOT NULL,
|
||||||
|
"original_name" TEXT NOT NULL,
|
||||||
|
"content_type" TEXT NOT NULL,
|
||||||
|
"size" INTEGER NOT NULL,
|
||||||
|
"s3_key" TEXT NOT NULL,
|
||||||
|
"url" TEXT NOT NULL,
|
||||||
|
"uploaded_at" INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "user_file_user_id_idx" ON "user_file" ("user_id");
|
||||||
58
src/auth.ts
Normal file
58
src/auth.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* Authentication utilities for the WebSocket relay.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Auth } from '@bowong/auth'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a Better Auth bearer token by querying D1 directly.
|
||||||
|
* Returns user info if valid, null otherwise.
|
||||||
|
*/
|
||||||
|
export async function validateBearerToken(
|
||||||
|
db: D1Database,
|
||||||
|
token: string,
|
||||||
|
): Promise<{ userId: string; userName: string | null } | null> {
|
||||||
|
const result = await db
|
||||||
|
.prepare(
|
||||||
|
`SELECT s.user_id, u.name FROM session s JOIN user u ON s.user_id = u.id WHERE s.token = ? AND s.expires_at > ?`,
|
||||||
|
)
|
||||||
|
.bind(token, Math.floor(Date.now() / 1000))
|
||||||
|
.first<{ user_id: string; name: string | null }>()
|
||||||
|
|
||||||
|
if (!result) return null
|
||||||
|
return { userId: result.user_id, userName: result.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify an API key (lobster identity) via Better Auth api-key plugin.
|
||||||
|
*/
|
||||||
|
export async function verifyApiKeyToken(
|
||||||
|
auth: Auth,
|
||||||
|
apiKeyValue: string,
|
||||||
|
): Promise<{
|
||||||
|
valid: boolean
|
||||||
|
keyId?: string
|
||||||
|
keyName?: string
|
||||||
|
userId?: string
|
||||||
|
metadata?: Record<string, unknown>
|
||||||
|
} | null> {
|
||||||
|
try {
|
||||||
|
const result = await auth.api.verifyApiKey({
|
||||||
|
body: { key: apiKeyValue },
|
||||||
|
})
|
||||||
|
if (!result?.valid || !result.key) return { valid: false }
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
keyId: result.key.id,
|
||||||
|
keyName: result.key.name ?? undefined,
|
||||||
|
userId: result.key.referenceId ?? undefined,
|
||||||
|
metadata:
|
||||||
|
typeof result.key.metadata === 'string'
|
||||||
|
? JSON.parse(result.key.metadata)
|
||||||
|
: (result.key.metadata ?? undefined),
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[auth] API key verification failed:', err)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
1536
src/chat-relay.ts
Normal file
1536
src/chat-relay.ts
Normal file
File diff suppressed because it is too large
Load Diff
216
src/index.ts
Normal file
216
src/index.ts
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
/**
|
||||||
|
* Cloudflare Worker entry point.
|
||||||
|
* - /api/auth/* → Better Auth handler (sign-up, sign-in, session, etc.)
|
||||||
|
* - /api/auth/avatars/* → Avatar management plugin
|
||||||
|
* - /api/auth/emojis/* → Emoji management plugin
|
||||||
|
* - /api/auth/apikeys/* → API key avatar binding plugin
|
||||||
|
* - /ws, /openclaw, /health → ChatRelay Durable Object
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createAuth } from '@bowong/auth'
|
||||||
|
|
||||||
|
export { ChatRelay } from './chat-relay.js'
|
||||||
|
|
||||||
|
interface Env {
|
||||||
|
CHAT_RELAY: DurableObjectNamespace
|
||||||
|
AUTH_DB: D1Database
|
||||||
|
BETTER_AUTH_SECRET: string
|
||||||
|
RELAY_URL?: string
|
||||||
|
ALLOWED_ORIGINS?: string
|
||||||
|
AWS_ACCESS_KEY_ID?: string
|
||||||
|
AWS_SECRET_ACCESS_KEY?: string
|
||||||
|
AWS_BUCKET_NAME?: string
|
||||||
|
AWS_LOCATION?: string
|
||||||
|
AWS_ENDPOINT?: string
|
||||||
|
AWS_PUBLIC_BASE_URL?: string
|
||||||
|
AWS_KEY_PREFIX?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAuthInstance(env: Env, url: URL) {
|
||||||
|
return createAuth({
|
||||||
|
db: env.AUTH_DB,
|
||||||
|
baseURL: env.RELAY_URL ?? url.origin,
|
||||||
|
secret: env.BETTER_AUTH_SECRET || `It3546porQSYeKhQyplQRRjmohE5AmR6rF4WoKq85t0=`,
|
||||||
|
trustedOrigins: env.ALLOWED_ORIGINS ? env.ALLOWED_ORIGINS.split(',') : undefined,
|
||||||
|
s3: env.AWS_ACCESS_KEY_ID && env.AWS_SECRET_ACCESS_KEY && env.AWS_BUCKET_NAME && env.AWS_LOCATION
|
||||||
|
? {
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: env.AWS_ACCESS_KEY_ID,
|
||||||
|
secretAccessKey: env.AWS_SECRET_ACCESS_KEY,
|
||||||
|
bucketName: env.AWS_BUCKET_NAME,
|
||||||
|
region: env.AWS_LOCATION,
|
||||||
|
endpoint: env.AWS_ENDPOINT,
|
||||||
|
publicBaseUrl: env.AWS_PUBLIC_BASE_URL ?? 'https://cdn.bowong.cc',
|
||||||
|
keyPrefix: env.AWS_KEY_PREFIX ?? 'emoji-files',
|
||||||
|
},
|
||||||
|
allowedTypes: ['webm', 'mp4', 'mov', 'png', 'jpg', 'jpeg', 'gif'],
|
||||||
|
maxFileSize: 25 * 1024 * 1024,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function rewriteRequestPath(request: Request, pathname: string): Request {
|
||||||
|
const nextUrl = new URL(request.url)
|
||||||
|
nextUrl.pathname = pathname
|
||||||
|
return new Request(nextUrl.toString(), request)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfiguredAllowedOrigins(env: Env): string[] {
|
||||||
|
if (!env.ALLOWED_ORIGINS) return []
|
||||||
|
return env.ALLOWED_ORIGINS.split(',')
|
||||||
|
.map(o => o.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllowedOrigins(request: Request, env: Env): string[] {
|
||||||
|
const configured = getConfiguredAllowedOrigins(env)
|
||||||
|
if (configured.length > 0) {
|
||||||
|
return configured
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(request.url)
|
||||||
|
const origins = new Set<string>([url.origin])
|
||||||
|
const requestOrigin = request.headers.get('Origin')
|
||||||
|
if (requestOrigin) {
|
||||||
|
origins.add(requestOrigin)
|
||||||
|
}
|
||||||
|
return [...origins]
|
||||||
|
}
|
||||||
|
|
||||||
|
function corsHeaders(request: Request, env: Env): Record<string, string> {
|
||||||
|
const origin = request.headers.get('Origin') ?? ''
|
||||||
|
const allowed = getAllowedOrigins(request, env)
|
||||||
|
const isAllowed = origin && allowed.includes(origin)
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Better-Auth-Token, x-filename, x-file-metadata',
|
||||||
|
'Access-Control-Expose-Headers': 'set-auth-token',
|
||||||
|
'Access-Control-Max-Age': '86400',
|
||||||
|
}
|
||||||
|
|
||||||
|
// When credentials are involved, we MUST return the exact origin, not wildcard
|
||||||
|
if (origin && isAllowed) {
|
||||||
|
headers['Access-Control-Allow-Origin'] = origin
|
||||||
|
headers['Access-Control-Allow-Credentials'] = 'true'
|
||||||
|
} else if (!origin) {
|
||||||
|
headers['Access-Control-Allow-Origin'] = '*'
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCorsHeaders(
|
||||||
|
response: Response,
|
||||||
|
request: Request,
|
||||||
|
env: Env,
|
||||||
|
): Response {
|
||||||
|
const origin = request.headers.get('Origin') ?? ''
|
||||||
|
const allowedOrigins = getAllowedOrigins(request, env)
|
||||||
|
const isAllowedOrigin = origin && allowedOrigins.includes(origin)
|
||||||
|
|
||||||
|
const newHeaders = new Headers()
|
||||||
|
|
||||||
|
// Copy non-CORS headers from the original response
|
||||||
|
for (const [key, value] of response.headers.entries()) {
|
||||||
|
// Skip ALL CORS-related headers from Better Auth
|
||||||
|
if (key.toLowerCase() !== 'access-control-allow-origin' &&
|
||||||
|
key.toLowerCase() !== 'access-control-allow-methods' &&
|
||||||
|
key.toLowerCase() !== 'access-control-allow-headers' &&
|
||||||
|
key.toLowerCase() !== 'access-control-allow-credentials' &&
|
||||||
|
key.toLowerCase() !== 'access-control-expose-headers' &&
|
||||||
|
key.toLowerCase() !== 'access-control-max-age') {
|
||||||
|
newHeaders.set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set our own CORS headers
|
||||||
|
// When credentials are involved, we MUST return the exact origin, not wildcard
|
||||||
|
if (origin && isAllowedOrigin) {
|
||||||
|
newHeaders.set('Access-Control-Allow-Origin', origin)
|
||||||
|
newHeaders.set('Access-Control-Allow-Credentials', 'true')
|
||||||
|
} else if (!origin) {
|
||||||
|
// No Origin header (same-origin or non-browser request) - allow all
|
||||||
|
newHeaders.set('Access-Control-Allow-Origin', '*')
|
||||||
|
}
|
||||||
|
newHeaders.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
|
||||||
|
newHeaders.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Better-Auth-Token, x-filename, x-file-metadata')
|
||||||
|
newHeaders.set('Access-Control-Expose-Headers', 'set-auth-token')
|
||||||
|
newHeaders.set('Access-Control-Max-Age', '86400')
|
||||||
|
newHeaders.append('Vary', 'Origin')
|
||||||
|
|
||||||
|
return new Response(response.body, {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
headers: newHeaders,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async fetch(request: Request, env: Env): Promise<Response> {
|
||||||
|
const url = new URL(request.url)
|
||||||
|
|
||||||
|
// CORS preflight
|
||||||
|
if (request.method === 'OPTIONS') {
|
||||||
|
return new Response(null, {
|
||||||
|
status: 204,
|
||||||
|
headers: corsHeaders(request, env),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
if (url.pathname === '/health') {
|
||||||
|
return new Response(JSON.stringify({ ok: true }), {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...corsHeaders(request, env),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname === '/docs') {
|
||||||
|
const auth = createAuthInstance(env, url)
|
||||||
|
const response = await auth.handler(
|
||||||
|
rewriteRequestPath(request, '/api/auth/reference'),
|
||||||
|
)
|
||||||
|
return addCorsHeaders(response, request, env)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname === '/openapi.json') {
|
||||||
|
const auth = createAuthInstance(env, url)
|
||||||
|
const response = await auth.handler(
|
||||||
|
rewriteRequestPath(request, '/api/auth/open-api/generate-schema'),
|
||||||
|
)
|
||||||
|
return addCorsHeaders(response, request, env)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Better Auth API — handles /api/auth/* (sign-up, sign-in, session, etc.)
|
||||||
|
if (url.pathname.startsWith('/api/auth')) {
|
||||||
|
const origin = request.headers.get('Origin') ?? ''
|
||||||
|
const allowedOrigins = getConfiguredAllowedOrigins(env)
|
||||||
|
|
||||||
|
// Verify origin is allowed before processing auth request
|
||||||
|
if (origin && allowedOrigins.length > 0 && !allowedOrigins.includes(origin)) {
|
||||||
|
return new Response('Origin not allowed', { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const auth = createAuthInstance(env, url)
|
||||||
|
const response = await auth.handler(request)
|
||||||
|
return addCorsHeaders(response, request, env)
|
||||||
|
}
|
||||||
|
|
||||||
|
// All relay routes go to a single global Durable Object instance.
|
||||||
|
const id = env.CHAT_RELAY.idFromName('global-relay')
|
||||||
|
const stub = env.CHAT_RELAY.get(id)
|
||||||
|
|
||||||
|
const response = await stub.fetch(request)
|
||||||
|
|
||||||
|
// Add CORS headers to non-WebSocket responses
|
||||||
|
if (response.webSocket) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
return addCorsHeaders(response, request, env)
|
||||||
|
},
|
||||||
|
} satisfies ExportedHandler<Env>
|
||||||
414
src/protocol.ts
Normal file
414
src/protocol.ts
Normal file
@@ -0,0 +1,414 @@
|
|||||||
|
/**
|
||||||
|
* Re-export shared protocol types for the relay worker.
|
||||||
|
* Duplicated here to avoid cross-package dependency in Cloudflare Worker bundling.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type WebSocketRole = 'browser' | 'openclaw'
|
||||||
|
|
||||||
|
export type AuthMessage = {
|
||||||
|
type: 'auth'
|
||||||
|
role: WebSocketRole
|
||||||
|
token: string
|
||||||
|
sessionId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AuthResultMessage = {
|
||||||
|
type: 'auth_result'
|
||||||
|
ok: boolean
|
||||||
|
error?: string
|
||||||
|
sessionId?: string
|
||||||
|
userId?: string
|
||||||
|
userName?: string
|
||||||
|
lobsterName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChatMessage = {
|
||||||
|
type: 'message'
|
||||||
|
id: string
|
||||||
|
sessionId: string
|
||||||
|
from: 'user' | 'ai'
|
||||||
|
text: string
|
||||||
|
timestamp: number
|
||||||
|
userId?: string
|
||||||
|
userName?: string
|
||||||
|
targetLobsterId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AckMessage = {
|
||||||
|
type: 'ack'
|
||||||
|
messageId: string
|
||||||
|
sessionId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TypingMessage = {
|
||||||
|
type: 'typing'
|
||||||
|
sessionId: string
|
||||||
|
from: 'user' | 'ai'
|
||||||
|
isTyping: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HistoryRequestMessage = {
|
||||||
|
type: 'history_request'
|
||||||
|
sessionId: string
|
||||||
|
before?: number
|
||||||
|
limit?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HistoryResponseMessage = {
|
||||||
|
type: 'history_response'
|
||||||
|
sessionId: string
|
||||||
|
messages: ChatMessage[]
|
||||||
|
hasMore: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PingMessage = {
|
||||||
|
type: 'ping'
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PongMessage = {
|
||||||
|
type: 'pong'
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ErrorMessage = {
|
||||||
|
type: 'error'
|
||||||
|
code: string
|
||||||
|
message: string
|
||||||
|
sessionId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SessionQueueMode =
|
||||||
|
| 'collect'
|
||||||
|
| 'followup'
|
||||||
|
| 'steer'
|
||||||
|
| 'interrupt'
|
||||||
|
| 'steer-backlog'
|
||||||
|
|
||||||
|
export type QueueStatusState = 'processing' | 'enqueued' | 'idle'
|
||||||
|
|
||||||
|
export type QueueStatusSessionEvent = {
|
||||||
|
event: 'queue_status'
|
||||||
|
state: QueueStatusState
|
||||||
|
mode?: SessionQueueMode
|
||||||
|
depth?: number
|
||||||
|
messageId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SessionEventPayload = QueueStatusSessionEvent
|
||||||
|
|
||||||
|
export type SessionEventMessage = {
|
||||||
|
type: 'session_event'
|
||||||
|
sessionId: string
|
||||||
|
timestamp: number
|
||||||
|
payload: SessionEventPayload
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lobster 上/下线推送给 browser
|
||||||
|
export type LobsterStatusMessage = {
|
||||||
|
type: 'lobster_status'
|
||||||
|
action: 'online' | 'offline' | 'list'
|
||||||
|
lobsters: Array<{ lobsterId: string; lobsterName: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Browser 上/下线推送给 openclaw
|
||||||
|
export type UserStatusMessage = {
|
||||||
|
type: 'user_status'
|
||||||
|
action: 'online' | 'offline'
|
||||||
|
sessionId: string
|
||||||
|
userId: string
|
||||||
|
userName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局 WebSocket 动态订阅/退订会话
|
||||||
|
export type SessionJoinMessage = {
|
||||||
|
type: 'session_join'
|
||||||
|
sessionId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SessionLeaveMessage = {
|
||||||
|
type: 'session_leave'
|
||||||
|
sessionId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HistoryClearMessage = {
|
||||||
|
type: 'history_clear'
|
||||||
|
sessionId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HistoryClearResultMessage = {
|
||||||
|
type: 'history_clear_result'
|
||||||
|
sessionId: string
|
||||||
|
ok: boolean
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RelayMessage =
|
||||||
|
| AuthMessage
|
||||||
|
| AuthResultMessage
|
||||||
|
| ChatMessage
|
||||||
|
| AckMessage
|
||||||
|
| TypingMessage
|
||||||
|
| HistoryRequestMessage
|
||||||
|
| HistoryResponseMessage
|
||||||
|
| PingMessage
|
||||||
|
| PongMessage
|
||||||
|
| ErrorMessage
|
||||||
|
| SessionEventMessage
|
||||||
|
| LobsterStatusMessage
|
||||||
|
| UserStatusMessage
|
||||||
|
| SessionJoinMessage
|
||||||
|
| SessionLeaveMessage
|
||||||
|
| HistoryClearMessage
|
||||||
|
| HistoryClearResultMessage
|
||||||
|
|
||||||
|
export function parseRelayMessage(raw: string): RelayMessage | null {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw) as unknown
|
||||||
|
if (typeof parsed !== 'object' || parsed === null || !('type' in parsed)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const msg = parsed as { type: string }
|
||||||
|
|
||||||
|
switch (msg.type) {
|
||||||
|
case 'auth':
|
||||||
|
return isValidAuthMessage(parsed) ? (parsed as AuthMessage) : null
|
||||||
|
case 'auth_result':
|
||||||
|
return isValidAuthResultMessage(parsed) ? (parsed as AuthResultMessage) : null
|
||||||
|
case 'message':
|
||||||
|
return isValidChatMessage(parsed) ? (parsed as ChatMessage) : null
|
||||||
|
case 'ack':
|
||||||
|
return isValidAckMessage(parsed) ? (parsed as AckMessage) : null
|
||||||
|
case 'typing':
|
||||||
|
return isValidTypingMessage(parsed) ? (parsed as TypingMessage) : null
|
||||||
|
case 'history_request':
|
||||||
|
return isValidHistoryRequestMessage(parsed) ? (parsed as HistoryRequestMessage) : null
|
||||||
|
case 'history_response':
|
||||||
|
return isValidHistoryResponseMessage(parsed) ? (parsed as HistoryResponseMessage) : null
|
||||||
|
case 'ping':
|
||||||
|
return isValidPingMessage(parsed) ? (parsed as PingMessage) : null
|
||||||
|
case 'pong':
|
||||||
|
return isValidPongMessage(parsed) ? (parsed as PongMessage) : null
|
||||||
|
case 'error':
|
||||||
|
return isValidErrorMessage(parsed) ? (parsed as ErrorMessage) : null
|
||||||
|
case 'session_event':
|
||||||
|
return isValidSessionEventMessage(parsed) ? (parsed as SessionEventMessage) : null
|
||||||
|
case 'lobster_status':
|
||||||
|
return isValidLobsterStatusMessage(parsed) ? (parsed as LobsterStatusMessage) : null
|
||||||
|
case 'user_status':
|
||||||
|
return isValidUserStatusMessage(parsed) ? (parsed as UserStatusMessage) : null
|
||||||
|
case 'session_join':
|
||||||
|
return isValidSessionJoinMessage(parsed) ? (parsed as SessionJoinMessage) : null
|
||||||
|
case 'session_leave':
|
||||||
|
return isValidSessionLeaveMessage(parsed) ? (parsed as SessionLeaveMessage) : null
|
||||||
|
case 'history_clear':
|
||||||
|
return isValidHistoryClearMessage(parsed) ? (parsed as HistoryClearMessage) : null
|
||||||
|
case 'history_clear_result':
|
||||||
|
return isValidHistoryClearResultMessage(parsed) ? (parsed as HistoryClearResultMessage) : null
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type guards for each message type
|
||||||
|
function isValidAuthMessage(msg: unknown): msg is AuthMessage {
|
||||||
|
return (
|
||||||
|
typeof msg === 'object' && msg !== null &&
|
||||||
|
(msg as AuthMessage).type === 'auth' &&
|
||||||
|
typeof (msg as AuthMessage).role === 'string' &&
|
||||||
|
['browser', 'openclaw'].includes((msg as AuthMessage).role) &&
|
||||||
|
typeof (msg as AuthMessage).token === 'string' &&
|
||||||
|
((msg as AuthMessage).sessionId === undefined || typeof (msg as AuthMessage).sessionId === 'string')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidAuthResultMessage(msg: unknown): msg is AuthResultMessage {
|
||||||
|
return (
|
||||||
|
typeof msg === 'object' && msg !== null &&
|
||||||
|
(msg as AuthResultMessage).type === 'auth_result' &&
|
||||||
|
typeof (msg as AuthResultMessage).ok === 'boolean' &&
|
||||||
|
((msg as AuthResultMessage).error === undefined || typeof (msg as AuthResultMessage).error === 'string') &&
|
||||||
|
((msg as AuthResultMessage).sessionId === undefined || typeof (msg as AuthResultMessage).sessionId === 'string')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidChatMessage(msg: unknown): msg is ChatMessage {
|
||||||
|
return (
|
||||||
|
typeof msg === 'object' && msg !== null &&
|
||||||
|
(msg as ChatMessage).type === 'message' &&
|
||||||
|
typeof (msg as ChatMessage).id === 'string' &&
|
||||||
|
typeof (msg as ChatMessage).sessionId === 'string' &&
|
||||||
|
['user', 'ai'].includes((msg as ChatMessage).from) &&
|
||||||
|
typeof (msg as ChatMessage).text === 'string' &&
|
||||||
|
typeof (msg as ChatMessage).timestamp === 'number'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidAckMessage(msg: unknown): msg is AckMessage {
|
||||||
|
return (
|
||||||
|
typeof msg === 'object' && msg !== null &&
|
||||||
|
(msg as AckMessage).type === 'ack' &&
|
||||||
|
typeof (msg as AckMessage).messageId === 'string' &&
|
||||||
|
typeof (msg as AckMessage).sessionId === 'string'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidTypingMessage(msg: unknown): msg is TypingMessage {
|
||||||
|
return (
|
||||||
|
typeof msg === 'object' && msg !== null &&
|
||||||
|
(msg as TypingMessage).type === 'typing' &&
|
||||||
|
typeof (msg as TypingMessage).sessionId === 'string' &&
|
||||||
|
['user', 'ai'].includes((msg as TypingMessage).from) &&
|
||||||
|
typeof (msg as TypingMessage).isTyping === 'boolean'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidHistoryRequestMessage(msg: unknown): msg is HistoryRequestMessage {
|
||||||
|
const m = msg as HistoryRequestMessage
|
||||||
|
return (
|
||||||
|
typeof msg === 'object' && msg !== null &&
|
||||||
|
m.type === 'history_request' &&
|
||||||
|
typeof m.sessionId === 'string' &&
|
||||||
|
(m.before === undefined || typeof m.before === 'number') &&
|
||||||
|
(m.limit === undefined || typeof m.limit === 'number')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidHistoryResponseMessage(msg: unknown): msg is HistoryResponseMessage {
|
||||||
|
const m = msg as HistoryResponseMessage
|
||||||
|
return (
|
||||||
|
typeof msg === 'object' && msg !== null &&
|
||||||
|
m.type === 'history_response' &&
|
||||||
|
typeof m.sessionId === 'string' &&
|
||||||
|
Array.isArray(m.messages) &&
|
||||||
|
typeof m.hasMore === 'boolean'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidPingMessage(msg: unknown): msg is PingMessage {
|
||||||
|
return (
|
||||||
|
typeof msg === 'object' && msg !== null &&
|
||||||
|
(msg as PingMessage).type === 'ping' &&
|
||||||
|
typeof (msg as PingMessage).timestamp === 'number'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidPongMessage(msg: unknown): msg is PongMessage {
|
||||||
|
return (
|
||||||
|
typeof msg === 'object' && msg !== null &&
|
||||||
|
(msg as PongMessage).type === 'pong' &&
|
||||||
|
typeof (msg as PongMessage).timestamp === 'number'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidErrorMessage(msg: unknown): msg is ErrorMessage {
|
||||||
|
const m = msg as ErrorMessage
|
||||||
|
return (
|
||||||
|
typeof msg === 'object' && msg !== null &&
|
||||||
|
m.type === 'error' &&
|
||||||
|
typeof m.code === 'string' &&
|
||||||
|
typeof m.message === 'string' &&
|
||||||
|
(m.sessionId === undefined || typeof m.sessionId === 'string')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidSessionEventMessage(msg: unknown): msg is SessionEventMessage {
|
||||||
|
const m = msg as SessionEventMessage
|
||||||
|
return (
|
||||||
|
typeof msg === 'object' &&
|
||||||
|
msg !== null &&
|
||||||
|
m.type === 'session_event' &&
|
||||||
|
typeof m.sessionId === 'string' &&
|
||||||
|
typeof m.timestamp === 'number' &&
|
||||||
|
isValidSessionEventPayload(m.payload)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidSessionEventPayload(payload: unknown): payload is SessionEventPayload {
|
||||||
|
if (typeof payload !== 'object' || payload === null) return false
|
||||||
|
const typed = payload as SessionEventPayload
|
||||||
|
if (typed.event !== 'queue_status') return false
|
||||||
|
if (!['processing', 'enqueued', 'idle'].includes(typed.state)) return false
|
||||||
|
if (
|
||||||
|
typed.mode !== undefined &&
|
||||||
|
!['collect', 'followup', 'steer', 'interrupt', 'steer-backlog'].includes(typed.mode)
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
(typed.depth === undefined || typeof typed.depth === 'number') &&
|
||||||
|
(typed.messageId === undefined || typeof typed.messageId === 'string')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidLobsterStatusMessage(msg: unknown): msg is LobsterStatusMessage {
|
||||||
|
const m = msg as LobsterStatusMessage
|
||||||
|
return (
|
||||||
|
typeof msg === 'object' && msg !== null &&
|
||||||
|
m.type === 'lobster_status' &&
|
||||||
|
typeof m.action === 'string' &&
|
||||||
|
['online', 'offline', 'list'].includes(m.action) &&
|
||||||
|
Array.isArray(m.lobsters)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidUserStatusMessage(msg: unknown): msg is UserStatusMessage {
|
||||||
|
const m = msg as UserStatusMessage
|
||||||
|
return (
|
||||||
|
typeof msg === 'object' && msg !== null &&
|
||||||
|
m.type === 'user_status' &&
|
||||||
|
typeof m.action === 'string' &&
|
||||||
|
['online', 'offline'].includes(m.action) &&
|
||||||
|
typeof m.sessionId === 'string' &&
|
||||||
|
typeof m.userId === 'string'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidSessionJoinMessage(msg: unknown): msg is SessionJoinMessage {
|
||||||
|
const m = msg as SessionJoinMessage
|
||||||
|
return (
|
||||||
|
typeof msg === 'object' && msg !== null &&
|
||||||
|
m.type === 'session_join' &&
|
||||||
|
typeof m.sessionId === 'string'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidSessionLeaveMessage(msg: unknown): msg is SessionLeaveMessage {
|
||||||
|
const m = msg as SessionLeaveMessage
|
||||||
|
return (
|
||||||
|
typeof msg === 'object' && msg !== null &&
|
||||||
|
m.type === 'session_leave' &&
|
||||||
|
typeof m.sessionId === 'string'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidHistoryClearMessage(msg: unknown): msg is HistoryClearMessage {
|
||||||
|
const m = msg as HistoryClearMessage
|
||||||
|
return (
|
||||||
|
typeof msg === 'object' && msg !== null &&
|
||||||
|
m.type === 'history_clear' &&
|
||||||
|
typeof m.sessionId === 'string'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidHistoryClearResultMessage(msg: unknown): msg is HistoryClearResultMessage {
|
||||||
|
const m = msg as HistoryClearResultMessage
|
||||||
|
return (
|
||||||
|
typeof msg === 'object' && msg !== null &&
|
||||||
|
m.type === 'history_clear_result' &&
|
||||||
|
typeof m.sessionId === 'string' &&
|
||||||
|
typeof m.ok === 'boolean' &&
|
||||||
|
(m.error === undefined || typeof m.error === 'string')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateMessageId(): string {
|
||||||
|
return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MAX_HISTORY_PER_SESSION = 200
|
||||||
|
export const DEFAULT_HISTORY_LIMIT = 50
|
||||||
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"types": ["@cloudflare/workers-types"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
10875
worker-configuration.d.ts
vendored
Normal file
10875
worker-configuration.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
85
wrangler.jsonc
Normal file
85
wrangler.jsonc
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
{
|
||||||
|
"name": "web-relay",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"compatibility_date": "2025-03-10",
|
||||||
|
"compatibility_flags": [
|
||||||
|
"nodejs_compat"
|
||||||
|
],
|
||||||
|
"migrations": [
|
||||||
|
{
|
||||||
|
"tag": "v1",
|
||||||
|
"new_classes": [
|
||||||
|
"ChatRelay"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"observability": {
|
||||||
|
"logs": {
|
||||||
|
"enabled": true,
|
||||||
|
"invocation_logs": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Shared bindings & dev defaults — available via `wrangler dev`
|
||||||
|
"vars": {
|
||||||
|
"BETTER_AUTH_URL": "http://192.168.0.117:8787/api/auth",
|
||||||
|
"RELAY_URL": "http://192.168.0.117:8787",
|
||||||
|
"BETTER_AUTH_SECRET": "It3546porQSYeKhQyplQRRjmohE5AmR6rF4WoKq85t0=",
|
||||||
|
"OPENCLAW_AUTH_TOKEN": "It3546porQSYeKhQyplQRRjmohE5AmR6rF4WoKq85t0=",
|
||||||
|
"AWS_ACCESS_KEY_ID": "AKIAYRH5NGRSWHN2L4M6",
|
||||||
|
"AWS_SECRET_ACCESS_KEY": "kfAqoOmIiyiywi25xaAkJUQbZ/EKDnzvI6NRCW1l",
|
||||||
|
"AWS_BUCKET_NAME": "modal-media-cache",
|
||||||
|
"AWS_LOCATION": "ap-northeast-2",
|
||||||
|
"ALLOWED_ORIGINS": "http://localhost:3000,http://localhost:3001,http://localhost:3002,http://localhost:3003,http://192.168.0.117:3000"
|
||||||
|
},
|
||||||
|
"durable_objects": {
|
||||||
|
"bindings": [
|
||||||
|
{
|
||||||
|
"name": "CHAT_RELAY",
|
||||||
|
"class_name": "ChatRelay",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"d1_databases": [
|
||||||
|
{
|
||||||
|
"binding": "AUTH_DB",
|
||||||
|
"database_name": "clawshow-db",
|
||||||
|
"database_id": "b2d654ab-20b8-4502-b37f-cbcd1ce80e00",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"prod": {
|
||||||
|
"vars": {
|
||||||
|
"BETTER_AUTH_URL": "https://clawshow-api.bowong.cc/api/auth",
|
||||||
|
"BETTER_AUTH_SECRET": "It3546porQSYeKhQyplQRRjmohE5AmR6rF4WoKq85t0=",
|
||||||
|
"OPENCLAW_AUTH_TOKEN": "It3546porQSYeKhQyplQRRjmohE5AmR6rF4WoKq85t0=",
|
||||||
|
"RELAY_URL": "https://clawshow-api.bowong.cc",
|
||||||
|
"AWS_ACCESS_KEY_ID": "AKIAYRH5NGRSWHN2L4M6",
|
||||||
|
"AWS_SECRET_ACCESS_KEY": "kfAqoOmIiyiywi25xaAkJUQbZ/EKDnzvI6NRCW1l",
|
||||||
|
"AWS_BUCKET_NAME": "modal-media-cache",
|
||||||
|
"AWS_LOCATION": "ap-northeast-2",
|
||||||
|
"ALLOWED_ORIGINS": "http://localhost:3000,http://localhost:3001,http://localhost:3002,http://localhost:3003,https://clawshow.bowong.cc"
|
||||||
|
},
|
||||||
|
"durable_objects": {
|
||||||
|
"bindings": [
|
||||||
|
{
|
||||||
|
"name": "CHAT_RELAY",
|
||||||
|
"class_name": "ChatRelay"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"d1_databases": [
|
||||||
|
{
|
||||||
|
"binding": "AUTH_DB",
|
||||||
|
"database_name": "clawshow-db",
|
||||||
|
"database_id": "b2d654ab-20b8-4502-b37f-cbcd1ce80e00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"pattern": "clawshow-api.bowong.cc",
|
||||||
|
"custom_domain": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user