Supports 50 login-free uses

#5
by CarlenBai - opened
package.json CHANGED
@@ -39,10 +39,12 @@
39
  "@radix-ui/react-toggle": "1.1.1",
40
  "@radix-ui/react-toggle-group": "1.1.1",
41
  "@radix-ui/react-tooltip": "latest",
 
42
  "autoprefixer": "^10.4.20",
43
  "class-variance-authority": "^0.7.1",
44
  "clsx": "^2.1.1",
45
  "cmdk": "1.0.4",
 
46
  "date-fns": "4.1.0",
47
  "embla-carousel-react": "8.5.1",
48
  "input-otp": "1.4.1",
 
39
  "@radix-ui/react-toggle": "1.1.1",
40
  "@radix-ui/react-toggle-group": "1.1.1",
41
  "@radix-ui/react-tooltip": "latest",
42
+ "@types/crypto-js": "^4.2.2",
43
  "autoprefixer": "^10.4.20",
44
  "class-variance-authority": "^0.7.1",
45
  "clsx": "^2.1.1",
46
  "cmdk": "1.0.4",
47
+ "crypto-js": "^4.2.0",
48
  "date-fns": "4.1.0",
49
  "embla-carousel-react": "8.5.1",
50
  "input-otp": "1.4.1",
pnpm-lock.yaml CHANGED
@@ -98,6 +98,9 @@ importers:
98
  '@radix-ui/react-tooltip':
99
  specifier: latest
100
  version: 1.2.6(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
 
 
 
101
  autoprefixer:
102
  specifier: ^10.4.20
103
  version: 10.4.21(postcss@8.5.3)
@@ -110,6 +113,9 @@ importers:
110
  cmdk:
111
  specifier: 1.0.4
112
  version: 1.0.4(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
 
 
 
113
  date-fns:
114
  specifier: 4.1.0
115
  version: 4.1.0
@@ -1263,6 +1269,9 @@ packages:
1263
  '@swc/helpers@0.5.15':
1264
  resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
1265
 
 
 
 
1266
  '@types/d3-array@3.2.1':
1267
  resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
1268
 
@@ -1413,6 +1422,9 @@ packages:
1413
  resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
1414
  engines: {node: '>= 8'}
1415
 
 
 
 
1416
  cssesc@3.0.0:
1417
  resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
1418
  engines: {node: '>=4'}
@@ -3129,6 +3141,8 @@ snapshots:
3129
  dependencies:
3130
  tslib: 2.8.1
3131
 
 
 
3132
  '@types/d3-array@3.2.1': {}
3133
 
3134
  '@types/d3-color@3.1.3': {}
@@ -3285,6 +3299,8 @@ snapshots:
3285
  shebang-command: 2.0.0
3286
  which: 2.0.2
3287
 
 
 
3288
  cssesc@3.0.0: {}
3289
 
3290
  csstype@3.1.3: {}
 
98
  '@radix-ui/react-tooltip':
99
  specifier: latest
100
  version: 1.2.6(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
101
+ '@types/crypto-js':
102
+ specifier: ^4.2.2
103
+ version: 4.2.2
104
  autoprefixer:
105
  specifier: ^10.4.20
106
  version: 10.4.21(postcss@8.5.3)
 
113
  cmdk:
114
  specifier: 1.0.4
115
  version: 1.0.4(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
116
+ crypto-js:
117
+ specifier: ^4.2.0
118
+ version: 4.2.0
119
  date-fns:
120
  specifier: 4.1.0
121
  version: 4.1.0
 
1269
  '@swc/helpers@0.5.15':
1270
  resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
1271
 
1272
+ '@types/crypto-js@4.2.2':
1273
+ resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
1274
+
1275
  '@types/d3-array@3.2.1':
1276
  resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
1277
 
 
1422
  resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
1423
  engines: {node: '>= 8'}
1424
 
1425
+ crypto-js@4.2.0:
1426
+ resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
1427
+
1428
  cssesc@3.0.0:
1429
  resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
1430
  engines: {node: '>=4'}
 
3141
  dependencies:
3142
  tslib: 2.8.1
3143
 
3144
+ '@types/crypto-js@4.2.2': {}
3145
+
3146
  '@types/d3-array@3.2.1': {}
3147
 
3148
  '@types/d3-color@3.1.3': {}
 
3299
  shebang-command: 2.0.0
3300
  which: 2.0.2
3301
 
3302
+ crypto-js@4.2.0: {}
3303
+
3304
  cssesc@3.0.0: {}
3305
 
3306
  csstype@3.1.3: {}
src/app/api/auth/check-bypass/route.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { generateFingerprintFromHeaders } from "@/lib/inference-utils";
3
+ import { evaluateLoginBypass } from "@/lib/runtime";
4
+
5
+ export async function GET(request: NextRequest): Promise<NextResponse> {
6
+ const fingerprint = generateFingerprintFromHeaders(request);
7
+ const canBypass = evaluateLoginBypass(fingerprint, true);
8
+ return new NextResponse(String(canBypass), { status: 200 });
9
+ }
src/app/api/generate-code/route.ts CHANGED
@@ -58,7 +58,7 @@ export async function POST(request: NextRequest) {
58
 
59
  // Use streaming response
60
  return createStreamResponse(async (controller) => {
61
- const client = createInferenceClient(tokenResult.token);
62
  let completeResponse = "";
63
 
64
  const messages = [
 
58
 
59
  // Use streaming response
60
  return createStreamResponse(async (controller) => {
61
+ const client = createInferenceClient(tokenResult);
62
  let completeResponse = "";
63
 
64
  const messages = [
src/app/api/improve-prompt/route.ts CHANGED
@@ -51,7 +51,7 @@ export async function POST(request: NextRequest) {
51
 
52
  // Use streaming response
53
  return createStreamResponse(async (controller) => {
54
- const client = createInferenceClient(tokenResult.token);
55
 
56
  const messages = [
57
  {
 
51
 
52
  // Use streaming response
53
  return createStreamResponse(async (controller) => {
54
+ const client = createInferenceClient(tokenResult);
55
 
56
  const messages = [
57
  {
src/components/app-container.tsx CHANGED
@@ -284,7 +284,6 @@ export function AppContainer() {
284
  )}
285
  </div>
286
 
287
- {/* 全局错误显示组件 */}
288
  <ErrorMessage
289
  message={improveError || generationError}
290
  onClose={() => {
 
284
  )}
285
  </div>
286
 
 
287
  <ErrorMessage
288
  message={improveError || generationError}
289
  onClose={() => {
src/components/code-editor.tsx CHANGED
@@ -90,10 +90,10 @@ export function CodeEditor({ code, isLoading = false, onCodeChange }: CodeEditor
90
  style={{
91
  fontSize: '0.875rem',
92
  lineHeight: '1.5',
93
- fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace',
94
  color: '#d4d4d4'
95
  }}
96
- dangerouslySetInnerHTML={{ __html: highlightedCode + '\n' }}
97
  />
98
 
99
  {/* Editable textarea - always transparent */}
 
90
  style={{
91
  fontSize: '0.875rem',
92
  lineHeight: '1.5',
93
+ fontFamily: 'ui-monospace, SFMono-Regular, \"SF Mono\", Consolas, \"Liberation Mono\", Menlo, monospace',
94
  color: '#d4d4d4'
95
  }}
96
+ dangerouslySetInnerHTML={{ __html: highlightedCode }}
97
  />
98
 
99
  {/* Editable textarea - always transparent */}
src/components/prompt-input.tsx CHANGED
@@ -38,7 +38,6 @@ export function PromptInput({
38
  setPrompt(initialPrompt);
39
  }, [initialPrompt]);
40
 
41
- // 当 improveError 改变时通知父组件
42
  useEffect(() => {
43
  if (onImproveError) {
44
  onImproveError(improveError);
@@ -48,7 +47,14 @@ export function PromptInput({
48
  const checkAuth = async (): Promise<boolean> => {
49
  try {
50
  const token = await getInferenceToken();
51
- return !!token;
 
 
 
 
 
 
 
52
  } catch (error) {
53
  setShowAuthError(true);
54
  return false;
 
38
  setPrompt(initialPrompt);
39
  }, [initialPrompt]);
40
 
 
41
  useEffect(() => {
42
  if (onImproveError) {
43
  onImproveError(improveError);
 
47
  const checkAuth = async (): Promise<boolean> => {
48
  try {
49
  const token = await getInferenceToken();
50
+ if (token) {
51
+ return true;
52
+ }
53
+ const canBypass = await fetch("/api/auth/check-bypass").then(res => res.json());
54
+ if (!canBypass) {
55
+ throw new Error("Authentication required");
56
+ }
57
+ return true;
58
  } catch (error) {
59
  setShowAuthError(true);
60
  return false;
src/lib/auth.ts CHANGED
@@ -12,9 +12,5 @@ export async function getInferenceToken(): Promise<string> {
12
  const tokenCookie = cookies.find(cookie => cookie.startsWith('hf_token='));
13
  const token = tokenCookie?.split('=')[1];
14
 
15
- if (!token) {
16
- throw new Error('Authentication required');
17
- }
18
-
19
- return token;
20
  }
 
12
  const tokenCookie = cookies.find(cookie => cookie.startsWith('hf_token='));
13
  const token = tokenCookie?.split('=')[1];
14
 
15
+ return token || "";
 
 
 
 
16
  }
src/lib/inference-utils.ts CHANGED
@@ -1,5 +1,9 @@
1
  import { NextRequest, NextResponse } from "next/server";
2
  import { InferenceClient } from "@huggingface/inference";
 
 
 
 
3
 
4
  // Create a shared TextEncoder instance to be reused
5
  const sharedEncoder = new TextEncoder();
@@ -12,10 +16,34 @@ export interface ModelConfig {
12
  default_enable_thinking?: boolean;
13
  }
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  export async function getInferenceToken(request: NextRequest): Promise<{
16
  token: string;
 
17
  error?: { message: string; status: number; openLogin?: boolean };
18
  }> {
 
 
 
 
 
 
 
 
 
19
  const hf_token = request.cookies.get("hf_token")?.value || process.env.DEFAULT_HF_TOKEN;
20
  if (!hf_token) {
21
  return {
@@ -42,11 +70,11 @@ export function checkTokenLimit(tokensUsed: number, modelConfig: ModelConfig) {
42
  return null;
43
  }
44
 
45
- export function createInferenceClient(token: string): InferenceClient {
46
- const inferenceEndpointUrl = process.env.INFERENCE_ENDPOINT_URL;
47
 
48
  if (inferenceEndpointUrl) {
49
- return new InferenceClient(token, {
50
  endpointUrl: inferenceEndpointUrl,
51
  });
52
  }
 
1
  import { NextRequest, NextResponse } from "next/server";
2
  import { InferenceClient } from "@huggingface/inference";
3
+ import sha256 from "crypto-js/sha256";
4
+ import HmacSHA256 from 'crypto-js/hmac-sha256';
5
+ import Base64 from 'crypto-js/enc-base64';
6
+ import { evaluateLoginBypass } from "@/lib/runtime";
7
 
8
  // Create a shared TextEncoder instance to be reused
9
  const sharedEncoder = new TextEncoder();
 
16
  default_enable_thinking?: boolean;
17
  }
18
 
19
+ export function generateFingerprintFromHeaders(request: NextRequest): string {
20
+ const forwarded = request.headers.get("x-forwarded-for");
21
+ const realIp = request.headers.get("x-real-ip");
22
+ console.log("[Request IP] ", forwarded, realIp);
23
+ const ipFingerprint = forwarded || realIp || "unknown";
24
+ const userAgent = request.headers.get("user-agent") || '';
25
+ const accept = request.headers.get("accept") || '';
26
+ const language = request.headers.get("accept-language") || '';
27
+ const encoding = request.headers.get("accept-encoding") || '';
28
+ const dnt = request.headers.get("dnt") || '';
29
+ const fingerprint = sha256(`${ipFingerprint}-${userAgent}-${accept}-${language}-${encoding}-${dnt}`);
30
+ return Base64.stringify(HmacSHA256(fingerprint, "novita-anysite"));
31
+ }
32
+
33
  export async function getInferenceToken(request: NextRequest): Promise<{
34
  token: string;
35
+ bypassToken?: string;
36
  error?: { message: string; status: number; openLogin?: boolean };
37
  }> {
38
+ const fingerprint = generateFingerprintFromHeaders(request);
39
+ const canBypass = evaluateLoginBypass(fingerprint);
40
+ if (canBypass) {
41
+ return {
42
+ token: "",
43
+ bypassToken: process.env.NOVITA_API_TOKEN,
44
+ };
45
+ }
46
+
47
  const hf_token = request.cookies.get("hf_token")?.value || process.env.DEFAULT_HF_TOKEN;
48
  if (!hf_token) {
49
  return {
 
70
  return null;
71
  }
72
 
73
+ export function createInferenceClient({token, bypassToken}: {token: string, bypassToken?: string}): InferenceClient {
74
+ const inferenceEndpointUrl = token ? process.env.INFERENCE_ENDPOINT_URL : process.env.NOVITA_BASE_URL;
75
 
76
  if (inferenceEndpointUrl) {
77
+ return new InferenceClient(token || bypassToken, {
78
  endpointUrl: inferenceEndpointUrl,
79
  });
80
  }
src/lib/runtime.ts ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const store = new Map<
2
+ string,
3
+ {
4
+ count: number;
5
+ expiresAt: number;
6
+ }
7
+ >();
8
+
9
+ let lastCleanup = 0;
10
+ const CLEANUP_INTERVAL = 1000 * 60 * 60; // Cleanup every 1 hour
11
+
12
+ function clearStore() {
13
+ const now = Date.now();
14
+ for (const [fingerprint, target] of store.entries()) {
15
+ if (target.expiresAt < now) {
16
+ store.delete(fingerprint);
17
+ }
18
+ }
19
+ }
20
+
21
+
22
+ export function evaluateLoginBypass(fingerprint: string, isSkipCount?: boolean): boolean {
23
+ const now = Date.now();
24
+ if (now - lastCleanup > CLEANUP_INTERVAL) {
25
+ clearStore();
26
+ lastCleanup = now;
27
+ }
28
+
29
+ const target = store.get(fingerprint);
30
+ console.log("[Bypass store keys] ", store.keys());
31
+ console.log("[Bypass store target] ", target);
32
+
33
+ if (!target || target.expiresAt < Date.now()) {
34
+ const now = Date.now();
35
+ const expiresAt = now + 1000 * 60 * 60 * 24;
36
+ store.set(fingerprint, { count: 1, expiresAt });
37
+ return true;
38
+ }
39
+
40
+ if (target.count > 50) {
41
+ return false;
42
+ }
43
+
44
+ if (!isSkipCount) {
45
+ target.count++;
46
+ }
47
+ return true;
48
+ }