feat: add authenticated remote MCP access and complete business workflows
This commit is contained in:
1 parent
f40f4da781
commit
027a8c1b6a
35 files changed
+4430
-183
No files matched your search
@@ -0,0 +1,159 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Req,
|
||||
Param,
|
||||
Body,
|
||||
Res,
|
||||
ForbiddenException,
|
||||
HttpException,
|
||||
} from '@nestjs/common';
|
||||
import { compare } from 'bcryptjs';
|
||||
import { Response } from 'express';
|
||||
import { z } from 'zod';
|
||||
import { Database } from '../database';
|
||||
import { AuthService, UserRequest } from '../auth';
|
||||
import { AgentOAuth, urls, scopeInput } from './oauth';
|
||||
import { AgentOperations } from './operations';
|
||||
@Controller('api/agent')
|
||||
export class AgentManagementController {
|
||||
constructor(
|
||||
private db: Database,
|
||||
private oauth: AgentOAuth,
|
||||
private operations: AgentOperations,
|
||||
private auth: AuthService,
|
||||
) {}
|
||||
@Get() async list(@Req() r: UserRequest) {
|
||||
const grants = await this.db.agentGrant.findMany({
|
||||
where: { userId: r.userId },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
scopes: true,
|
||||
expiresAt: true,
|
||||
createdAt: true,
|
||||
revokedAt: true,
|
||||
clientId: true,
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 100,
|
||||
});
|
||||
const operations = await this.db.agentOperation.findMany({
|
||||
where: { userId: r.userId },
|
||||
select: {
|
||||
id: true,
|
||||
tool: true,
|
||||
status: true,
|
||||
expiresAt: true,
|
||||
createdAt: true,
|
||||
completedAt: true,
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 100,
|
||||
});
|
||||
const calls = await this.db.agentCall.findMany({
|
||||
where: { userId: r.userId },
|
||||
select: { id: true, tool: true, status: true, createdAt: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 100,
|
||||
});
|
||||
return {
|
||||
mcpUrl: urls().resource.toString(),
|
||||
mode:
|
||||
(await this.db.agentPolicy.findUnique({ where: { userId: r.userId } }))?.mode || 'draft',
|
||||
grants,
|
||||
operations,
|
||||
calls,
|
||||
};
|
||||
}
|
||||
@Put('policy') async policy(@Req() r: UserRequest, @Body() raw: unknown) {
|
||||
const { mode, password } = z
|
||||
.object({ mode: z.enum(['readonly', 'draft', 'direct']), password: z.string().max(72) })
|
||||
.strict()
|
||||
.parse(raw);
|
||||
this.auth.limit(r);
|
||||
const u = await this.db.user.findUniqueOrThrow({ where: { id: r.userId } });
|
||||
if (!(await compare(password, u.passwordHash))) throw new ForbiddenException('密码错误');
|
||||
await this.db.agentPolicy.upsert({
|
||||
where: { userId: r.userId },
|
||||
create: { userId: r.userId, mode },
|
||||
update: { mode },
|
||||
});
|
||||
return { mode };
|
||||
}
|
||||
@Post('tokens') async token(@Req() r: UserRequest, @Body() raw: unknown) {
|
||||
const p = z
|
||||
.object({
|
||||
name: z.string().trim().min(1).max(100),
|
||||
scopes: scopeInput,
|
||||
days: z.number().int().min(1).max(90),
|
||||
password: z.string().max(72),
|
||||
})
|
||||
.strict()
|
||||
.parse(raw);
|
||||
this.auth.limit(r);
|
||||
const u = await this.db.user.findUniqueOrThrow({ where: { id: r.userId } });
|
||||
if (!(await compare(p.password, u.passwordHash))) throw new ForbiddenException('密码错误');
|
||||
return this.db.atomic(async () => {
|
||||
const v = await this.oauth.issue(r.userId, p.name, p.scopes, p.days);
|
||||
return { id: v.grant.id, token: v.tokens.access_token, expiresAt: v.grant.expiresAt };
|
||||
});
|
||||
}
|
||||
@Delete('connections/:id') async revoke(@Req() r: UserRequest, @Param('id') id: string) {
|
||||
z.string().uuid().parse(id);
|
||||
await this.db.agentGrant.updateMany({
|
||||
where: { id, userId: r.userId },
|
||||
data: { revokedAt: new Date() },
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
@Get('authorizations/:id') pending(@Param('id') id: string) {
|
||||
return this.oauth.pending(z.string().uuid().parse(id));
|
||||
}
|
||||
@Post('authorizations/:id') async consent(
|
||||
@Req() r: UserRequest,
|
||||
@Param('id') id: string,
|
||||
@Body() raw: unknown,
|
||||
) {
|
||||
const { approve } = z.object({ approve: z.boolean() }).strict().parse(raw);
|
||||
return this.oauth.consent(r.userId, z.string().uuid().parse(id), approve);
|
||||
}
|
||||
@Get('operations/:id') preview(@Req() r: UserRequest, @Param('id') id: string) {
|
||||
return this.operations.preview(r.userId, z.string().uuid().parse(id));
|
||||
}
|
||||
@Post('operations/:id') async confirm(
|
||||
@Req() r: UserRequest,
|
||||
@Param('id') id: string,
|
||||
@Body() raw: unknown,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
) {
|
||||
this.auth.limit(r);
|
||||
z.string().uuid().parse(id);
|
||||
try {
|
||||
return await this.operations.confirm(r, id, raw, res);
|
||||
} catch (e) {
|
||||
const row = await this.db.agentOperation.findFirst({
|
||||
where: { id, userId: r.userId, status: 'pending' },
|
||||
});
|
||||
if (row) {
|
||||
await this.db.agentOperation.updateMany({
|
||||
where: { id, userId: r.userId, status: 'pending' },
|
||||
data: {
|
||||
result: {
|
||||
status: 'submission_failed',
|
||||
message:
|
||||
e instanceof HttpException ? e.message : '提交失败,账目已回滚;可重试或取消',
|
||||
},
|
||||
},
|
||||
});
|
||||
await this.db.agentCall.create({
|
||||
data: { userId: r.userId, grantId: row.grantId, tool: row.tool, status: 'error' },
|
||||
});
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user