feat(auth): add authentication routes and user token handling
- Implemented user authentication routes in `auth.ts` for fetching current user info and admin verification. - Added caching mechanism for user tokens to improve performance. - Created middleware for admin authentication. feat(opencode): create OpenCode client route - Added `opencode-cnb` route for creating OpenCode clients with session management. - Integrated OpenCode SDK for client operations and session handling. refactor(client): encapsulate OpenCode client creation - Created a utility function `getClient` in `client.ts` to initialize OpenCode clients. test(opencode): add tests for OpenCode routes - Implemented test cases for OpenCode routes in `list.ts` to validate functionality. - Created common utilities for testing in `common.ts`.
This commit is contained in:
@@ -54,13 +54,13 @@
|
||||
"@kevisual/use-config": "^1.0.30",
|
||||
"@opencode-ai/plugin": "^1.2.24",
|
||||
"@types/bun": "^1.3.10",
|
||||
"@types/node": "^25.4.0",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/send": "^1.2.1",
|
||||
"@types/ws": "^8.18.1",
|
||||
"chalk": "^5.6.2",
|
||||
"commander": "^14.0.3",
|
||||
"cross-env": "^10.1.0",
|
||||
"dayjs": "^1.11.19",
|
||||
"dayjs": "^1.11.20",
|
||||
"dotenv": "^17.3.1",
|
||||
"get-port": "^7.1.0",
|
||||
"nanoid": "^5.1.6",
|
||||
@@ -76,7 +76,7 @@
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.1007.0",
|
||||
"@aws-sdk/client-s3": "^3.1008.0",
|
||||
"@kevisual/js-filter": "^0.0.6",
|
||||
"@kevisual/oss": "^0.0.20",
|
||||
"@kevisual/video-tools": "^0.0.13",
|
||||
|
||||
119
assistant/src/routes/auth.ts
Normal file
119
assistant/src/routes/auth.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { app, assistantConfig } from '../app.ts';
|
||||
import { authCache } from '@/module/cache/auth.ts';
|
||||
|
||||
import { logger } from '@/module/logger.ts';
|
||||
const getTokenUser = async (token: string) => {
|
||||
const query = assistantConfig.query
|
||||
const res = await query.post({
|
||||
path: 'user',
|
||||
key: 'me',
|
||||
token: token,
|
||||
});
|
||||
return res;
|
||||
}
|
||||
export const getTokenUserCache = async (token: string) => {
|
||||
const tokenUser = await authCache.get(token);
|
||||
if (tokenUser) {
|
||||
return {
|
||||
code: 200,
|
||||
data: tokenUser,
|
||||
};
|
||||
}
|
||||
const res = await getTokenUser(token);
|
||||
if (res.code === 200) {
|
||||
authCache.set(token, res.data);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
export const checkAuth = async (ctx: any, isAdmin = false) => {
|
||||
const config = assistantConfig.getConfig();
|
||||
const { auth = {} } = config;
|
||||
const token = ctx.query.token;
|
||||
logger.debug('checkAuth', ctx.query, { token });
|
||||
if (!token) {
|
||||
return {
|
||||
code: 401,
|
||||
message: '未登录',
|
||||
}
|
||||
}
|
||||
// 鉴权代理
|
||||
let tokenUser = await authCache.get(token);
|
||||
if (!tokenUser) {
|
||||
const tokenUserRes = await getTokenUser(token);
|
||||
if (tokenUserRes.code !== 200) {
|
||||
return {
|
||||
code: tokenUserRes.code,
|
||||
message: '验证失败' + tokenUserRes.message,
|
||||
}
|
||||
} else {
|
||||
tokenUser = tokenUserRes.data;
|
||||
}
|
||||
authCache.set(token, tokenUser);
|
||||
}
|
||||
ctx.state = {
|
||||
...ctx.state,
|
||||
token,
|
||||
tokenUser,
|
||||
};
|
||||
const { username } = tokenUser;
|
||||
if (!auth.username) {
|
||||
// 初始管理员账号
|
||||
auth.username = username;
|
||||
assistantConfig.setConfig({ auth });
|
||||
}
|
||||
if (isAdmin && auth.username) {
|
||||
const admins = config.auth?.admin || [];
|
||||
let isCheckAdmin = false;
|
||||
const admin = auth.username;
|
||||
if (admin === username) {
|
||||
isCheckAdmin = true;
|
||||
}
|
||||
if (!isCheckAdmin && admins.length > 0 && admins.includes(username)) {
|
||||
isCheckAdmin = true;
|
||||
}
|
||||
if (!isCheckAdmin) {
|
||||
return {
|
||||
code: 403,
|
||||
message: '非管理员用户',
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
code: 200,
|
||||
data: { tokenUser, token }
|
||||
}
|
||||
};
|
||||
app
|
||||
.route({
|
||||
path: 'auth',
|
||||
id: 'auth',
|
||||
description: '获取当前登录用户信息, 第一个登录的用户为管理员用户',
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
if (!ctx.query?.token && ctx.appId === app.appId) {
|
||||
return;
|
||||
}
|
||||
const authResult = await checkAuth(ctx);
|
||||
if (authResult.code !== 200) {
|
||||
ctx.throw(authResult.code, authResult.message);
|
||||
}
|
||||
})
|
||||
.addTo(app);
|
||||
app
|
||||
.route({
|
||||
path: 'auth-admin',
|
||||
id: 'auth-admin',
|
||||
description: '管理员鉴权, 获取用户信息,并验证是否为管理员。',
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
// logger.debug('query', ctx.query);
|
||||
if (!ctx.query?.token && ctx.appId === app.appId) {
|
||||
return;
|
||||
}
|
||||
ctx.state.isAdmin = true;
|
||||
const authResult = await checkAuth(ctx, true);
|
||||
if (authResult.code !== 200) {
|
||||
ctx.throw(authResult.code, authResult.message);
|
||||
}
|
||||
})
|
||||
.addTo(app);
|
||||
@@ -1,4 +1,4 @@
|
||||
import { app, assistantConfig } from '../app.ts';
|
||||
|
||||
import './config/index.ts';
|
||||
import './client/index.ts';
|
||||
import './shop-install/index.ts';
|
||||
@@ -11,121 +11,8 @@ import './remote/index.ts';
|
||||
// import './kevisual/index.ts'
|
||||
import './cnb-board/index.ts';
|
||||
|
||||
import { authCache } from '@/module/cache/auth.ts';
|
||||
import './auth.ts';
|
||||
|
||||
import { getTokenUserCache, checkAuth } from './auth.ts';
|
||||
export { getTokenUserCache, checkAuth }
|
||||
|
||||
import { logger } from '@/module/logger.ts';
|
||||
const getTokenUser = async (token: string) => {
|
||||
const query = assistantConfig.query
|
||||
const res = await query.post({
|
||||
path: 'user',
|
||||
key: 'me',
|
||||
token: token,
|
||||
});
|
||||
return res;
|
||||
}
|
||||
export const getTokenUserCache = async (token: string) => {
|
||||
const tokenUser = await authCache.get(token);
|
||||
if (tokenUser) {
|
||||
return {
|
||||
code: 200,
|
||||
data: tokenUser,
|
||||
};
|
||||
}
|
||||
const res = await getTokenUser(token);
|
||||
if (res.code === 200) {
|
||||
authCache.set(token, res.data);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
export const checkAuth = async (ctx: any, isAdmin = false) => {
|
||||
const config = assistantConfig.getConfig();
|
||||
const { auth = {} } = config;
|
||||
const token = ctx.query.token;
|
||||
logger.debug('checkAuth', ctx.query, { token });
|
||||
if (!token) {
|
||||
return {
|
||||
code: 401,
|
||||
message: '未登录',
|
||||
}
|
||||
}
|
||||
// 鉴权代理
|
||||
let tokenUser = await authCache.get(token);
|
||||
if (!tokenUser) {
|
||||
const tokenUserRes = await getTokenUser(token);
|
||||
if (tokenUserRes.code !== 200) {
|
||||
return {
|
||||
code: tokenUserRes.code,
|
||||
message: '验证失败' + tokenUserRes.message,
|
||||
}
|
||||
} else {
|
||||
tokenUser = tokenUserRes.data;
|
||||
}
|
||||
authCache.set(token, tokenUser);
|
||||
}
|
||||
ctx.state = {
|
||||
...ctx.state,
|
||||
token,
|
||||
tokenUser,
|
||||
};
|
||||
const { username } = tokenUser;
|
||||
if (!auth.username) {
|
||||
// 初始管理员账号
|
||||
auth.username = username;
|
||||
assistantConfig.setConfig({ auth });
|
||||
}
|
||||
if (isAdmin && auth.username) {
|
||||
const admins = config.auth?.admin || [];
|
||||
let isCheckAdmin = false;
|
||||
const admin = auth.username;
|
||||
if (admin === username) {
|
||||
isCheckAdmin = true;
|
||||
}
|
||||
if (!isCheckAdmin && admins.length > 0 && admins.includes(username)) {
|
||||
isCheckAdmin = true;
|
||||
}
|
||||
if (!isCheckAdmin) {
|
||||
return {
|
||||
code: 403,
|
||||
message: '非管理员用户',
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
code: 200,
|
||||
data: { tokenUser, token }
|
||||
}
|
||||
};
|
||||
app
|
||||
.route({
|
||||
path: 'auth',
|
||||
id: 'auth',
|
||||
description: '获取当前登录用户信息, 第一个登录的用户为管理员用户',
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
if (!ctx.query?.token && ctx.appId === app.appId) {
|
||||
return;
|
||||
}
|
||||
const authResult = await checkAuth(ctx);
|
||||
if (authResult.code !== 200) {
|
||||
ctx.throw(authResult.code, authResult.message);
|
||||
}
|
||||
})
|
||||
.addTo(app);
|
||||
app
|
||||
.route({
|
||||
path: 'auth-admin',
|
||||
id: 'auth-admin',
|
||||
description: '管理员鉴权, 获取用户信息,并验证是否为管理员。',
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
// logger.debug('query', ctx.query);
|
||||
if (!ctx.query?.token && ctx.appId === app.appId) {
|
||||
return;
|
||||
}
|
||||
ctx.state.isAdmin = true;
|
||||
const authResult = await checkAuth(ctx, true);
|
||||
if (authResult.code !== 200) {
|
||||
ctx.throw(authResult.code, authResult.message);
|
||||
}
|
||||
})
|
||||
.addTo(app);
|
||||
|
||||
68
assistant/src/routes/opencode/cnb.ts
Normal file
68
assistant/src/routes/opencode/cnb.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { app } from '@/app.ts'
|
||||
import { z } from 'zod';
|
||||
import { getClient } from './module/client.ts';
|
||||
import dayjs from 'dayjs';
|
||||
import { Session } from '@opencode-ai/sdk';
|
||||
|
||||
app.route({
|
||||
path: 'opencode-cnb',
|
||||
key: 'question',
|
||||
middleware: ['auth-admin'],
|
||||
description: '创建 OpenCode 客户端',
|
||||
metadata: {
|
||||
args: {
|
||||
question: z.string().describe('问题'),
|
||||
baseUrl: z.string().optional().describe('OpenCode 服务地址,默认为 http://localhost:4096'),
|
||||
directory: z.string().optional().describe('运行目录,默认为根目录'),
|
||||
messageID: z.string().optional().describe('消息 ID,选填'),
|
||||
sessionId: z.string().optional().describe('会话 ID,选填'),
|
||||
parts: z.array(z.any()).optional().describe('消息内容的分块,优先于 question 参数'),
|
||||
}
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const { question, baseUrl, directory = '/workspace', messageID, sessionId, parts } = ctx.query;
|
||||
const client = await getClient({ baseUrl: baseUrl });
|
||||
if (!client) {
|
||||
ctx.body = { content: `OpenCode 客户端获取失败` };
|
||||
return;
|
||||
}
|
||||
if (!question) {
|
||||
ctx.body = { content: `问题不能为空` };
|
||||
return;
|
||||
}
|
||||
// const sessionList = await client.session.list()
|
||||
let session: Session | null = null;
|
||||
// const hasSession = sessionList.data.find(s => s.directory === directory);
|
||||
// if (hasSession) {
|
||||
// session = hasSession;
|
||||
// } else {
|
||||
if (sessionId) {
|
||||
try {
|
||||
const getSession = await client.session.get({ path: { id: sessionId } });
|
||||
session = getSession.data;
|
||||
} catch (error) {
|
||||
// 无法获取会话,继续往下走创建会话的逻辑
|
||||
}
|
||||
}
|
||||
if (!session) {
|
||||
const createSession = await client.session.create({
|
||||
query: {
|
||||
directory,
|
||||
},
|
||||
})
|
||||
session = createSession.data;
|
||||
}
|
||||
let _parts: any[] = parts ?? [{ type: "text", text: question }];
|
||||
const message = await client.session.prompt({
|
||||
body: {
|
||||
messageID: messageID,
|
||||
parts: _parts,
|
||||
},
|
||||
path: {
|
||||
id: sessionId || session.id,
|
||||
},
|
||||
})
|
||||
const data = message.data;
|
||||
|
||||
ctx.body = { content: `已经启动`, data };
|
||||
}).addTo(app);
|
||||
@@ -7,7 +7,7 @@ import { useKey } from '@kevisual/use-config';
|
||||
app.route({
|
||||
path: 'opencode',
|
||||
key: 'create',
|
||||
middleware: ['auth'],
|
||||
middleware: ['auth-admin'],
|
||||
description: '创建 OpenCode 客户端',
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
@@ -16,7 +16,7 @@ app.route({
|
||||
title: '创建 OpenCode 客户端',
|
||||
summary: '创建 OpenCode 客户端,如果存在则复用',
|
||||
args: {
|
||||
port: tool.schema.number().optional().describe('OpenCode 服务端口,默认为 5000')
|
||||
port: tool.schema.number().optional().describe('OpenCode 服务端口,默认为 4096')
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -25,11 +25,11 @@ app.route({
|
||||
ctx.body = { content: `${opencodeManager.url} OpenCode 客户端已就绪` };
|
||||
}).addTo(app);
|
||||
|
||||
// 关闭 opencode 客户端 5000
|
||||
// 关闭 opencode 客户端 4096
|
||||
app.route({
|
||||
path: 'opencode',
|
||||
key: 'close',
|
||||
middleware: ['auth'],
|
||||
middleware: ['auth-admin'],
|
||||
description: '关闭 OpenCode 客户端',
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
@@ -38,7 +38,7 @@ app.route({
|
||||
title: '关闭 OpenCode 客户端',
|
||||
summary: '关闭 OpenCode 客户端, 未提供端口则关闭默认端口',
|
||||
args: {
|
||||
port: tool.schema.number().optional().describe('OpenCode 服务端口,默认为 5000')
|
||||
port: tool.schema.number().optional().describe('OpenCode 服务端口,默认为 4096')
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -51,7 +51,7 @@ app.route({
|
||||
app.route({
|
||||
path: 'opencode',
|
||||
key: 'restart',
|
||||
middleware: ['auth'],
|
||||
middleware: ['auth-admin'],
|
||||
description: '重启 OpenCode 客户端',
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
@@ -60,7 +60,7 @@ app.route({
|
||||
title: '重启 OpenCode 客户端',
|
||||
summary: '重启 OpenCode 客户端',
|
||||
args: {
|
||||
port: tool.schema.number().optional().describe('OpenCode 服务端口,默认为 5000')
|
||||
port: tool.schema.number().optional().describe('OpenCode 服务端口,默认为 4096')
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -73,7 +73,7 @@ app.route({
|
||||
app.route({
|
||||
path: 'opencode',
|
||||
key: 'getUrl',
|
||||
middleware: ['auth'],
|
||||
middleware: ['auth-admin'],
|
||||
description: '获取 OpenCode 服务 URL',
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
@@ -82,7 +82,7 @@ app.route({
|
||||
title: '获取 OpenCode 服务 URL',
|
||||
summary: '获取当前 OpenCode 服务的 URL 地址',
|
||||
args: {
|
||||
port: tool.schema.number().optional().describe('OpenCode 服务端口,默认为 5000')
|
||||
port: tool.schema.number().optional().describe('OpenCode 服务端口,默认为 4096')
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -91,7 +91,7 @@ app.route({
|
||||
const cnbURL = useKey('CNB_VSCODE_PROXY_URI') as string | undefined;
|
||||
let content = `本地访问地址: ${url}`
|
||||
if (cnbURL) {
|
||||
content += `\n云端访问地址: ${cnbURL.replace('{{port}}', '5000')}`;
|
||||
content += `\n云端访问地址: ${cnbURL.replace('{{port}}', '4096')}`;
|
||||
}
|
||||
ctx.body = { content };
|
||||
}).addTo(app);
|
||||
@@ -114,7 +114,7 @@ app.route({
|
||||
app.route({
|
||||
path: 'opencode',
|
||||
key: 'runProject',
|
||||
middleware: ['auth'],
|
||||
middleware: ['auth-admin'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
|
||||
8
assistant/src/routes/opencode/module/client.ts
Normal file
8
assistant/src/routes/opencode/module/client.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk"
|
||||
|
||||
export const getClient = async (opts?: { baseUrl?: string }) => {
|
||||
const client = await createOpencodeClient({
|
||||
baseUrl: opts?.baseUrl ?? "http://localhost:4096",
|
||||
})
|
||||
return client;
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import getPort from "get-port";
|
||||
import os from "node:os";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
const DEFAULT_PORT = 5000;
|
||||
const DEFAULT_PORT = 4096;
|
||||
|
||||
export class OpencodeManager {
|
||||
private static instance: OpencodeManager | null = null;
|
||||
@@ -57,7 +57,7 @@ export class OpencodeManager {
|
||||
|
||||
async createOpencodeProject({
|
||||
directory,
|
||||
port = 5000
|
||||
port = DEFAULT_PORT
|
||||
}: { directory?: string, port?: number }): Promise<OpencodeClient> {
|
||||
const client = createOpencodeClient({
|
||||
baseUrl: `http://localhost:${port}`,
|
||||
|
||||
9
assistant/src/routes/opencode/test/common.ts
Normal file
9
assistant/src/routes/opencode/test/common.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { app } from '@/app.ts';
|
||||
import '../cnb.ts'
|
||||
import "@/routes/auth.ts"
|
||||
import util from "node:util"
|
||||
export { app }
|
||||
|
||||
export const showMore = (data: any) => {
|
||||
return util.inspect(data, { depth: null, colors: true })
|
||||
}
|
||||
14
assistant/src/routes/opencode/test/list.ts
Normal file
14
assistant/src/routes/opencode/test/list.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { app, showMore } from './common.ts';
|
||||
|
||||
const main = async () => {
|
||||
const res = await app.run({
|
||||
path: 'opencode-cnb',
|
||||
key: 'question',
|
||||
payload: {
|
||||
question: '当前的projects目录下有哪些文件?',
|
||||
}
|
||||
}, { appId: app.appId });
|
||||
console.log('res', showMore(res));
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user