feat: add Railway-ready Jyotish chat product

This commit is contained in:
Jesse_Chen
2026-07-15 22:23:17 +08:00
parent 4a268c75f2
commit 4aa0105fa4
77 changed files with 15439 additions and 80 deletions
+11
View File
@@ -0,0 +1,11 @@
const promptExtractionPatterns = [
/(?:ignore|disregard).{0,24}(?:previous|system|developer).{0,24}(?:instruction|prompt)/i,
/(?:忽略|无视).{0,20}(?:之前|以上|系统|开发者).{0,20}(?:指令|提示词)/,
/(?:system prompt|developer message|api[ _-]?key|secret key)/i,
/(?:系统提示词|开发者消息|密钥|skill\s*原文|技能原文)/i,
];
export function blocksPromptExtraction(question: string) {
const normalized = question.normalize("NFKC");
return promptExtractionPatterns.some((pattern) => pattern.test(normalized));
}
+29
View File
@@ -0,0 +1,29 @@
import "server-only";
import { createClient } from "@supabase/supabase-js";
import {
getSupabaseUrl,
SupabaseConfigurationError,
} from "./config";
export function createAdminSupabaseClient() {
const url = getSupabaseUrl();
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
if (!serviceRoleKey) {
throw new SupabaseConfigurationError(["SUPABASE_SERVICE_ROLE_KEY"]);
}
return createClient(url, serviceRoleKey, {
auth: { autoRefreshToken: false, persistSession: false },
});
}
export function isAdminEmail(email: string | null | undefined) {
const configured = process.env.ADMIN_EMAILS;
if (!configured?.trim() || !email) return false;
const normalized = email.trim().toLowerCase();
return configured
.split(",")
.some((candidate) => candidate.trim().toLowerCase() === normalized);
}
+9
View File
@@ -0,0 +1,9 @@
"use client";
import { createBrowserClient } from "@supabase/ssr";
import { getSupabasePublicConfig } from "./config";
export function createBrowserSupabaseClient() {
const { url, anonKey } = getSupabasePublicConfig();
return createBrowserClient(url, anonKey);
}
+21
View File
@@ -0,0 +1,21 @@
import { createHash, randomBytes } from "node:crypto";
const CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
export function normalizeRedeemCode(code: string) {
return code.replace(/\s/g, "").toUpperCase();
}
export function hashRedeemCode(code: string) {
return createHash("sha256").update(code, "utf8").digest("hex");
}
export function generateRedeemCode() {
const bytes = randomBytes(8);
const token = Array.from(bytes, (byte) => CODE_ALPHABET[byte % CODE_ALPHABET.length]).join("");
return `JYOTISH-${token.slice(0, 4)}-${token.slice(4)}`;
}
export function maskRedeemCode(code: string) {
return `JYOTISH-****-${code.slice(-4)}`;
}
+27
View File
@@ -0,0 +1,27 @@
export class SupabaseConfigurationError extends Error {
readonly code = "SUPABASE_NOT_CONFIGURED";
constructor(missing: string[]) {
super(`Supabase 配置缺失:${missing.join(", ")}`);
this.name = "SupabaseConfigurationError";
}
}
export function getSupabaseUrl() {
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
if (!url) throw new SupabaseConfigurationError(["NEXT_PUBLIC_SUPABASE_URL"]);
return url;
}
export function getSupabasePublicConfig() {
const url = getSupabaseUrl();
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
if (!anonKey) {
throw new SupabaseConfigurationError(["NEXT_PUBLIC_SUPABASE_ANON_KEY"]);
}
return { url, anonKey };
}
export function isSupabaseConfigurationError(error: unknown) {
return error instanceof SupabaseConfigurationError;
}
+25
View File
@@ -0,0 +1,25 @@
import "server-only";
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import { getSupabasePublicConfig } from "./config";
export async function createServerSupabaseClient() {
const { url, anonKey } = getSupabasePublicConfig();
const cookieStore = await cookies();
return createServerClient(url, anonKey, {
cookies: {
getAll: () => cookieStore.getAll(),
setAll: (cookiesToSet) => {
try {
cookiesToSet.forEach(({ name, value, options }) => {
cookieStore.set(name, value, options);
});
} catch {
// Server Components cannot write cookies; Route Handlers can.
}
},
},
});
}