Skip to content

Commit 67f1f4d

Browse files
authored
✨ feat: support password auth and error (#22)
* ✨ feat: Server 端支持密码鉴权 & 完善错误码处理逻辑 * 🎨 fix: 修正 package.json 的引入问题 * 💬 style: 优化设置文案 * 💬 style: 优化错误文案 * ✨ feat: 支持 ChatList loading 渲染 * ✨ feat: 新增密码输入或 api key 输入组件 * 🌐 style: 修正 i18n 文案问题 * 🔧 chore: remove error lint rule * ✨ feat: 支持展示 OpenAI 的业务错误内容 * ✨ feat: 完成 OpenAI 报错内容展示优化
1 parent 62f2332 commit 67f1f4d

29 files changed

Lines changed: 607 additions & 89 deletions

.eslintrc.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
const config = require('@lobehub/lint').eslint;
22

33
config.extends.push('plugin:@next/next/recommended');
4-
//config.extends.push('plugin:@next/next/core-web-vitals');
54

65
config.rules['unicorn/no-negated-condition'] = 0;
6+
config.rules['unicorn/prefer-type-error'] = 0;
7+
78
module.exports = config;

src/config/server.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
declare global {
2+
// eslint-disable-next-line @typescript-eslint/no-namespace
3+
namespace NodeJS {
4+
interface ProcessEnv {
5+
ACCESS_CODE?: string;
6+
OPENAI_API_KEY?: string;
7+
OPENAI_PROXY_URL?: string;
8+
}
9+
}
10+
}
11+
12+
export const getServerConfig = () => {
13+
if (typeof process === 'undefined') {
14+
throw new Error('[Server Config] you are importing a nodejs-only module outside of nodejs');
15+
}
16+
17+
return {
18+
ACCESS_CODE: process.env.ACCESS_CODE,
19+
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
20+
OPENAI_PROXY_URL: process.env.OPENAI_PROXY_URL,
21+
};
22+
};

src/const/fetch.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export const OPENAI_SERVICE_ERROR_CODE = 555;
2-
31
export const OPENAI_API_KEY_HEADER_KEY = 'X-OPENAI-API-KEY';
2+
3+
export const LOBE_CHAT_ACCESS_CODE = 'X-LOBE_CHAT_ACCESS_CODE';

src/locales/create.ts

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,12 @@ import type { Namespaces } from '@/types/locale';
77

88
import resources from './resources';
99

10-
// const getRes = (res: Resources, namespace: Namespaces[]) => {
11-
// const newRes: any = {};
12-
// for (const [locale, value] of Object.entries(res)) {
13-
// newRes[locale] = {};
14-
// for (const ns of namespace) {
15-
// newRes[locale][ns] = value[ns];
16-
// }
17-
// }
18-
// return newRes;
19-
// };
20-
2110
export const createI18nNext = (namespace?: Namespaces[] | Namespaces) => {
2211
const ns: Namespaces[] = namespace
2312
? isArray(namespace)
24-
? ['common', ...namespace]
25-
: ['common', namespace]
26-
: ['common'];
13+
? ['error', 'common', ...namespace]
14+
: ['error', 'common', namespace]
15+
: ['error', 'common'];
2716
return (
2817
i18n
2918
// detect user language

src/locales/default/common.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export default {
3030
},
3131
feedback: '反馈与建议',
3232
import: '导入配置',
33+
moreSetting: '更多设置...',
3334
newAgent: '新建助手',
3435
noDescription: '暂无描述',
3536
ok: '确定',

src/locales/default/error.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export default {
2+
response: {
3+
400: '很抱歉,服务器不明白您的请求,请确认您的请求参数是否正确',
4+
401: '很抱歉,服务器拒绝了您的请求,可能是因为您的权限不足或未提供有效的身份验证',
5+
403: '很抱歉,服务器拒绝了您的请求,您没有访问此内容的权限 ',
6+
404: '很抱歉,服务器找不到您请求的页面或资源,请确认您的 URL 是否正确',
7+
429: '很抱歉,您的请求太多,服务器有点累了,请稍后再试',
8+
500: '很抱歉,服务器似乎遇到了一些困难,暂时无法完成您的请求,请稍后再试',
9+
502: '很抱歉,服务器似乎迷失了方向,暂时无法提供服务,请稍后再试',
10+
503: '很抱歉,服务器当前无法处理您的请求,可能是由于过载或正在进行维护,请稍后再试',
11+
504: '很抱歉,服务器没有等到上游服务器的回应,请稍后再试',
12+
13+
InvalidAccessCode: '密码不正确或为空,请输入正确的访问密码,或者添加自定义 OpenAI API Key',
14+
OpenAIBizError: '请求 OpenAI 服务出错,请根据以下信息排查或重试',
15+
},
16+
unlock: {
17+
apikey: {
18+
description: '输入你的 OpenAI API Key 即可绕过密码验证。应用不会记录你的 API Key',
19+
title: '使用自定义 API Key',
20+
},
21+
confirm: '确认并重试',
22+
password: {
23+
description: '管理员已开启应用加密,输入应用密码后即可解锁应用。密码只需填写一次',
24+
title: '输入密码解锁应用',
25+
},
26+
},
27+
};

src/locales/resources/zh_CN.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import common from '../default/common';
2+
import error from '../default/error';
23
import plugin from '../default/plugin';
34
import setting from '../default/setting';
45

56
const resources = {
67
common,
8+
error,
79
plugin,
810
setting,
911
} as const;

src/pages/api/auth.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { getServerConfig } from '@/config/server';
2+
import { ErrorType } from '@/types/fetch';
3+
4+
interface AuthConfig {
5+
accessCode?: string | null;
6+
apiKey?: string | null;
7+
}
8+
9+
export const checkAuth = ({ apiKey, accessCode }: AuthConfig) => {
10+
const { ACCESS_CODE } = getServerConfig();
11+
12+
// 如果存在 apiKey
13+
if (apiKey) {
14+
return { auth: true };
15+
}
16+
17+
// 如果不存在,则检查 accessCode
18+
if (!ACCESS_CODE) return { auth: true };
19+
20+
if (accessCode !== ACCESS_CODE) {
21+
return { auth: false, error: ErrorType.InvalidAccessCode };
22+
}
23+
24+
return { auth: true };
25+
};

src/pages/api/error.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { ErrorResponse, ErrorType } from '@/types/fetch';
2+
3+
const getStatus = (errorType: ErrorType) => {
4+
switch (errorType) {
5+
case ErrorType.InvalidAccessCode: {
6+
return 401;
7+
}
8+
9+
case ErrorType.OpenAIBizError: {
10+
return 577;
11+
}
12+
}
13+
};
14+
15+
export const createErrorResponse = (errorType: ErrorType, body?: any) => {
16+
const statusCode = getStatus(errorType);
17+
18+
const data: ErrorResponse = { body, errorType };
19+
20+
return new Response(JSON.stringify(data), { status: statusCode });
21+
};

src/pages/api/openai.api.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
1-
import { StreamingTextResponse } from 'ai';
2-
3-
import { OPENAI_API_KEY_HEADER_KEY } from '@/const/fetch';
1+
import { LOBE_CHAT_ACCESS_CODE, OPENAI_API_KEY_HEADER_KEY } from '@/const/fetch';
2+
import { ErrorType } from '@/types/fetch';
43
import { OpenAIStreamPayload } from '@/types/openai';
54

5+
import { checkAuth } from './auth';
6+
import { createErrorResponse } from './error';
67
import { createChatCompletion } from './openai';
78

89
export const runtime = 'edge';
910

1011
export default async function handler(req: Request) {
1112
const payload = (await req.json()) as OpenAIStreamPayload;
1213
const apiKey = req.headers.get(OPENAI_API_KEY_HEADER_KEY);
14+
const accessCode = req.headers.get(LOBE_CHAT_ACCESS_CODE);
15+
16+
const result = checkAuth({ accessCode, apiKey });
1317

14-
const stream = await createChatCompletion({ OPENAI_API_KEY: apiKey, payload });
18+
if (!result.auth) {
19+
return createErrorResponse(result.error as ErrorType);
20+
}
1521

16-
return new StreamingTextResponse(stream);
22+
return createChatCompletion({ OPENAI_API_KEY: apiKey, payload });
1723
}

0 commit comments

Comments
 (0)