plugin-ai-api 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/index.js +1 -1
- package/dist/client-v2/index.js +1 -1
- package/dist/externalVersion.js +9 -9
- package/dist/server/collections/ai-api-usage-records.js +63 -0
- package/dist/server/middleware/role-permission.js +15 -5
- package/dist/server/plugin.js +8 -1
- package/dist/server/routes/agent-completions.js +53 -53
- package/dist/server/routes/auth.js +37 -7
- package/dist/server/routes/router.js +22 -1
- package/dist/server/usage.js +80 -0
- package/dist/server/utils/ai-employee-runtime.js +59 -0
- package/package.json +1 -1
- package/src/server/__tests__/agent-completions.test.ts +26 -0
- package/src/server/collections/ai-api-usage-records.ts +33 -0
- package/src/server/middleware/role-permission.ts +79 -66
- package/src/server/plugin.ts +99 -89
- package/src/server/routes/agent-completions.ts +450 -428
- package/src/server/routes/auth.ts +142 -111
- package/src/server/routes/router.ts +304 -283
- package/src/server/usage.ts +52 -0
- package/src/server/utils/ai-employee-runtime.ts +71 -0
|
@@ -1,111 +1,142 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* This file is part of the NocoBase (R) project.
|
|
3
|
-
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
-
* Authors: NocoBase Team.
|
|
5
|
-
*
|
|
6
|
-
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
-
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { Context
|
|
11
|
-
import { toOpenAIError } from '../utils/openai-format';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Authentication middleware for /api/ai-llm/v1/* routes.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
ctx.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Context } from '@nocobase/actions';
|
|
11
|
+
import { toOpenAIError } from '../utils/openai-format';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Authentication middleware for /api/ai-llm/v1/* routes.
|
|
15
|
+
*
|
|
16
|
+
* Resolve the principal prepared by NocoBase auth/ACL middleware. OIDC resource
|
|
17
|
+
* authentication rewrites the external access token to an internal session token
|
|
18
|
+
* and sets currentUser before this gateway runs. API keys are the one case where
|
|
19
|
+
* we still need to decode the token locally because they are signed directly by
|
|
20
|
+
* NocoBase and carry a fixed roleName.
|
|
21
|
+
*/
|
|
22
|
+
export async function authenticateBearer(ctx: Context): Promise<boolean> {
|
|
23
|
+
const authHeader = ctx.get('Authorization') || '';
|
|
24
|
+
if (!authHeader.startsWith('Bearer ')) {
|
|
25
|
+
ctx.status = 401;
|
|
26
|
+
ctx.body = toOpenAIError(
|
|
27
|
+
401,
|
|
28
|
+
'Missing or invalid Authorization header. Expected: Bearer <access-token>',
|
|
29
|
+
'invalid_request_error',
|
|
30
|
+
'invalid_api_key',
|
|
31
|
+
);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const token = authHeader.substring(7).trim();
|
|
36
|
+
if (!token) {
|
|
37
|
+
ctx.status = 401;
|
|
38
|
+
ctx.body = toOpenAIError(401, 'API key is empty', 'invalid_request_error', 'invalid_api_key');
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
if (ctx.state.currentUser) {
|
|
44
|
+
if (!ctx.state.currentRole) {
|
|
45
|
+
const requestedRole = ctx.get('X-Role');
|
|
46
|
+
const rolesRepository = ctx.db.getRepository('users.roles', ctx.state.currentUser.id);
|
|
47
|
+
const roles = await rolesRepository.find({ fields: ['name'] });
|
|
48
|
+
const roleNames = roles.map((role: { name: string }) => role.name);
|
|
49
|
+
ctx.state.currentRole = roleNames.includes(requestedRole) ? requestedRole : roleNames[0];
|
|
50
|
+
ctx.state.currentRoles = ctx.state.currentRole ? [ctx.state.currentRole] : roleNames;
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Try to authenticate using the token as a NocoBase JWT/API token
|
|
56
|
+
// NocoBase API keys are essentially JWT tokens signed with the app secret
|
|
57
|
+
const jwt = ctx.app['authManager']?.jwt;
|
|
58
|
+
if (!jwt) {
|
|
59
|
+
ctx.status = 500;
|
|
60
|
+
ctx.body = toOpenAIError(500, 'JWT system not available', 'server_error');
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let decoded: { userId?: string | number; roleName?: string; temp?: boolean };
|
|
65
|
+
try {
|
|
66
|
+
decoded = await jwt.decode(token);
|
|
67
|
+
} catch (e) {
|
|
68
|
+
ctx.status = 401;
|
|
69
|
+
ctx.body = toOpenAIError(401, 'Incorrect API key provided', 'invalid_request_error', 'invalid_api_key');
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!decoded || !decoded.userId) {
|
|
74
|
+
ctx.status = 401;
|
|
75
|
+
ctx.body = toOpenAIError(401, 'Invalid or expired API key', 'invalid_request_error', 'invalid_api_key');
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// This fallback is for API keys only. A normal login/OIDC token should have
|
|
80
|
+
// been resolved by NocoBase auth middleware and must not be treated as the
|
|
81
|
+
// member role merely because it has no roleName claim.
|
|
82
|
+
if (!decoded.roleName) {
|
|
83
|
+
ctx.status = 401;
|
|
84
|
+
ctx.body = toOpenAIError(
|
|
85
|
+
401,
|
|
86
|
+
'Token was not resolved by the NocoBase auth middleware',
|
|
87
|
+
'invalid_request_error',
|
|
88
|
+
'invalid_api_key',
|
|
89
|
+
);
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const user = await ctx.db.getRepository('users').findOne({
|
|
94
|
+
filterByTk: decoded.userId,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (!user) {
|
|
98
|
+
ctx.status = 401;
|
|
99
|
+
ctx.body = toOpenAIError(401, 'User not found for this API key', 'invalid_request_error', 'invalid_api_key');
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Set user context for downstream handlers
|
|
104
|
+
ctx.state.currentUser = user;
|
|
105
|
+
const rolesRepository = ctx.db.getRepository('users.roles', user.id);
|
|
106
|
+
const roles = await rolesRepository.find({ fields: ['name'] });
|
|
107
|
+
const roleNames = roles.map((role: { name: string }) => role.name);
|
|
108
|
+
if (!roleNames.includes(decoded.roleName)) {
|
|
109
|
+
ctx.status = 403;
|
|
110
|
+
ctx.body = toOpenAIError(
|
|
111
|
+
403,
|
|
112
|
+
'The API key role is no longer assigned to this user',
|
|
113
|
+
'permission_denied',
|
|
114
|
+
'role_not_permitted',
|
|
115
|
+
);
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
ctx.state.currentRole = decoded.roleName;
|
|
119
|
+
ctx.state.currentRoles = [decoded.roleName];
|
|
120
|
+
ctx.state.aiApiAuthType = 'apiKey';
|
|
121
|
+
|
|
122
|
+
// Also set ctx.auth for AIEmployee compatibility.
|
|
123
|
+
// Only expose non-sensitive fields — exclude password, token, 2FA secrets, etc.
|
|
124
|
+
if (!ctx.auth) {
|
|
125
|
+
(ctx as any).auth = {};
|
|
126
|
+
}
|
|
127
|
+
const SAFE_USER_FIELDS = new Set(['id', 'username', 'nickname', 'email', 'createdAt', 'updatedAt']);
|
|
128
|
+
const rawUser = user.toJSON?.() ?? {};
|
|
129
|
+
const safeUser: Record<string, any> = { id: user.id };
|
|
130
|
+
for (const field of SAFE_USER_FIELDS) {
|
|
131
|
+
if (rawUser[field] !== undefined) safeUser[field] = rawUser[field];
|
|
132
|
+
}
|
|
133
|
+
(ctx as any).auth.user = safeUser;
|
|
134
|
+
|
|
135
|
+
return true;
|
|
136
|
+
} catch (err) {
|
|
137
|
+
ctx.log.error('AI API auth error:', err);
|
|
138
|
+
ctx.status = 401;
|
|
139
|
+
ctx.body = toOpenAIError(401, 'Authentication failed', 'invalid_request_error', 'invalid_api_key');
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|