plugin-ai-api 1.0.25 → 1.0.28
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/{286.01c0e3c5fff3cccb.js → 286.a1ee0420172cd5de.js} +1 -1
- package/dist/client/302.fbc46ebf5bf300d7.js +10 -0
- package/dist/client/562.44b16aad4718b4c7.js +10 -0
- package/dist/client/685.ae483e17b6b49c98.js +10 -0
- package/dist/client/{757.56952e321dc399b7.js → 757.6568d3504ad29352.js} +1 -1
- package/dist/client/{97.72979a11a067a7c9.js → 97.9b6b2d2b01a4c060.js} +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client-v2/302.3971233415999b2c.js +10 -0
- package/dist/client-v2/562.45d5c504433be38b.js +10 -0
- package/dist/client-v2/685.1030370b309b7d4b.js +10 -0
- package/dist/client-v2/{757.db678ca1aa6c422c.js → 757.f2bc9cfba07004b0.js} +1 -1
- package/dist/client-v2/{952.94100128b7757f56.js → 952.f0249eddc153bde1.js} +1 -1
- package/dist/client-v2/{97.29c663318eebbd57.js → 97.36a42eff36bb3d8a.js} +1 -1
- package/dist/client-v2/index.js +1 -1
- package/dist/constants.js +2 -5
- package/dist/locale/en-US.json +26 -8
- package/dist/locale/vi-VN.json +26 -8
- package/dist/locale/zh-CN.json +26 -8
- package/dist/server/billing.js +25 -32
- package/dist/server/collections/ai-api-config.js +1 -7
- package/dist/server/collections/ai-api-group-members.js +62 -0
- package/dist/server/collections/ai-api-group-quota-buckets.js +63 -0
- package/dist/server/collections/ai-api-model-metadata.js +6 -0
- package/dist/server/collections/ai-api-usage-groups.js +74 -0
- package/dist/server/collections/ai-api-usage-records.js +1 -0
- package/dist/server/middleware/rate-limit.js +7 -6
- package/dist/server/migrations/20260815000000-add-usage-groups.js +149 -0
- package/dist/server/migrations/20260816000000-migrate-user-permissions-to-groups.js +169 -0
- package/dist/server/migrations/20260816100000-add-model-metadata-system-prompt.js +69 -0
- package/dist/server/plugin.js +90 -22
- package/dist/server/quota-groups.js +108 -0
- package/dist/server/resource/ai-api-config.js +0 -3
- package/dist/server/resource/ai-api-usage-groups.js +168 -0
- package/dist/server/routes/agent-completions.js +2 -1
- package/dist/server/routes/chat-completions.js +32 -32
- package/dist/server/routes/completions.js +16 -19
- package/dist/server/routes/embeddings.js +2 -1
- package/dist/server/routes/models.js +2 -1
- package/dist/server/routes/router.js +3 -2
- package/dist/server/services/file-processor.js +186 -22
- package/dist/server/usage.js +5 -1
- package/dist/server/utils/direct-llm-context.js +13 -11
- package/dist/server/utils/rate-limiter.js +1 -1
- package/dist/server/utils/request-cache.js +61 -0
- package/dist/server/utils/resolve-service.js +2 -1
- package/dist/server/utils/user-permissions.js +25 -39
- package/dist/server/validation.js +7 -0
- package/dist/swagger.js +6 -7
- package/package.json +1 -1
- package/src/client/__tests__/settings-registration.test.tsx +6 -29
- package/src/client/plugin.tsx +5 -16
- package/src/client-v2/__tests__/settings-registration.test.tsx +6 -32
- package/src/client-v2/locale.ts +3 -1
- package/src/client-v2/pages/GeneralPage.tsx +0 -5
- package/src/client-v2/pages/ModelMetadataPage.tsx +20 -1
- package/src/client-v2/pages/UsageGroupsPage.tsx +548 -0
- package/src/client-v2/plugin.tsx +4 -13
- package/src/constants.ts +0 -7
- package/src/locale/en-US.json +26 -8
- package/src/locale/vi-VN.json +26 -8
- package/src/locale/zh-CN.json +26 -8
- package/src/server/__tests__/billing-quota.test.ts +28 -9
- package/src/server/__tests__/direct-llm-context.test.ts +122 -4
- package/src/server/__tests__/file-processor.test.ts +225 -0
- package/src/server/__tests__/models.test.ts +1 -1
- package/src/server/__tests__/permission-sync.test.ts +34 -35
- package/src/server/__tests__/usage-groups.test.ts +160 -0
- package/src/server/__tests__/usage-monitor.test.ts +2 -0
- package/src/server/__tests__/usage-route.test.ts +262 -2
- package/src/server/__tests__/usage.test.ts +38 -0
- package/src/server/__tests__/user-permissions.test.ts +214 -133
- package/src/server/__tests__/validation.test.ts +11 -0
- package/src/server/billing.ts +30 -38
- package/src/server/collections/ai-api-config.ts +1 -7
- package/src/server/collections/ai-api-group-members.ts +41 -0
- package/src/server/collections/ai-api-group-quota-buckets.ts +42 -0
- package/src/server/collections/ai-api-model-metadata.ts +7 -0
- package/src/server/collections/ai-api-usage-groups.ts +53 -0
- package/src/server/collections/ai-api-usage-records.ts +1 -0
- package/src/server/middleware/rate-limit.ts +10 -12
- package/src/server/migrations/20260815000000-add-usage-groups.ts +147 -0
- package/src/server/migrations/20260816000000-migrate-user-permissions-to-groups.ts +190 -0
- package/src/server/migrations/20260816100000-add-model-metadata-system-prompt.ts +46 -0
- package/src/server/plugin.ts +101 -30
- package/src/server/quota-groups.ts +117 -0
- package/src/server/resource/ai-api-config.ts +0 -3
- package/src/server/resource/ai-api-usage-groups.ts +171 -0
- package/src/server/routes/agent-completions.ts +2 -1
- package/src/server/routes/chat-completions.ts +39 -36
- package/src/server/routes/completions.ts +18 -21
- package/src/server/routes/embeddings.ts +2 -1
- package/src/server/routes/models.ts +4 -3
- package/src/server/routes/router.ts +4 -3
- package/src/server/services/file-processor.ts +214 -24
- package/src/server/usage.ts +5 -1
- package/src/server/utils/direct-llm-context.ts +20 -11
- package/src/server/utils/rate-limiter.ts +1 -1
- package/src/server/utils/request-cache.ts +59 -0
- package/src/server/utils/resolve-service.ts +2 -1
- package/src/server/utils/user-permissions.ts +49 -69
- package/src/server/validation.ts +7 -0
- package/src/swagger.ts +7 -8
- package/dist/client/123.e6fe04c856ce6417.js +0 -10
- package/dist/client/302.fc3a3491b4ec2dfd.js +0 -10
- package/dist/client/562.17a0a299d2e5152c.js +0 -10
- package/dist/client/902.e74518750f1e4201.js +0 -10
- package/dist/client-v2/123.05f1f649923f93eb.js +0 -10
- package/dist/client-v2/302.d27fe4ea9b0b3bf5.js +0 -10
- package/dist/client-v2/562.fb2948ee6402de95.js +0 -10
- package/dist/client-v2/902.c7c00a565085438a.js +0 -10
- package/dist/server/resource/ai-api-user-permissions.js +0 -75
- package/src/client-v2/pages/UserPermissionsPage.tsx +0 -322
- package/src/client-v2/pages/UserQuotasPage.tsx +0 -276
- package/src/server/__tests__/user-permissions-resource.test.ts +0 -66
- package/src/server/resource/ai-api-user-permissions.ts +0 -76
package/dist/server/plugin.js
CHANGED
|
@@ -44,7 +44,7 @@ var import_server = require("@nocobase/server");
|
|
|
44
44
|
var import_router = require("./routes/router");
|
|
45
45
|
var import_ai_api_config = __toESM(require("./resource/ai-api-config"));
|
|
46
46
|
var import_ai_api_usage_monitor = __toESM(require("./resource/ai-api-usage-monitor"));
|
|
47
|
-
var
|
|
47
|
+
var import_ai_api_usage_groups = __toESM(require("./resource/ai-api-usage-groups"));
|
|
48
48
|
var import_rate_limiter = require("./utils/rate-limiter");
|
|
49
49
|
var import_role_permission = require("./middleware/role-permission");
|
|
50
50
|
var import_user_permissions = require("./utils/user-permissions");
|
|
@@ -59,7 +59,7 @@ import_dayjs.default.extend(import_timezone.default);
|
|
|
59
59
|
class PluginAiApiServer extends import_server.Plugin {
|
|
60
60
|
/**
|
|
61
61
|
* Singleton rate limiter — lives for the entire plugin lifetime, shared across all requests.
|
|
62
|
-
* Uses a 1-minute sliding window to enforce rateLimitPerMinute from
|
|
62
|
+
* Uses a 1-minute sliding window to enforce rateLimitPerMinute from the user's usage group.
|
|
63
63
|
*/
|
|
64
64
|
rateLimiter = new import_rate_limiter.RateLimiter(6e4);
|
|
65
65
|
/**
|
|
@@ -77,8 +77,54 @@ class PluginAiApiServer extends import_server.Plugin {
|
|
|
77
77
|
this.app.db.on("aiApiModelMetadata.beforeSave", (model) => {
|
|
78
78
|
(0, import_validation.validateModelMetadata)(model);
|
|
79
79
|
});
|
|
80
|
-
this.app.db.on("
|
|
80
|
+
this.app.db.on("aiApiUsageGroups.beforeSave", async (model, options) => {
|
|
81
81
|
(0, import_validation.validateQuotaPolicy)(model);
|
|
82
|
+
if (model.get("isDefault")) {
|
|
83
|
+
const filter = { isDefault: true };
|
|
84
|
+
if (model.get("id")) filter.id = { $ne: model.get("id") };
|
|
85
|
+
const other = await this.db.getRepository("aiApiUsageGroups").findOne({
|
|
86
|
+
filter,
|
|
87
|
+
transaction: options == null ? void 0 : options.transaction
|
|
88
|
+
});
|
|
89
|
+
if (other) {
|
|
90
|
+
throw new Error("Only one default usage group is allowed.");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
this.app.db.on("aiApiGroupMembers.beforeSave", async (model, options) => {
|
|
95
|
+
const userId = model.get("userId");
|
|
96
|
+
if (!userId) return;
|
|
97
|
+
const existing = await this.db.getRepository("aiApiGroupMembers").findOne({
|
|
98
|
+
filter: { userId },
|
|
99
|
+
transaction: options == null ? void 0 : options.transaction
|
|
100
|
+
});
|
|
101
|
+
if (existing && existing.get("id") !== model.get("id")) {
|
|
102
|
+
throw new Error("User already belongs to another usage group.");
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
this.app.db.on("aiApiUsageGroups.beforeDestroy", async (model, options) => {
|
|
106
|
+
if (model.get("isDefault")) {
|
|
107
|
+
throw new Error("The default usage group cannot be deleted.");
|
|
108
|
+
}
|
|
109
|
+
const members = await this.db.getRepository("aiApiGroupMembers").find({
|
|
110
|
+
filter: { groupId: model.get("id") },
|
|
111
|
+
transaction: options == null ? void 0 : options.transaction
|
|
112
|
+
});
|
|
113
|
+
if (members.length === 0) return;
|
|
114
|
+
const defaultGroup = await this.db.getRepository("aiApiUsageGroups").findOne({
|
|
115
|
+
filter: { isDefault: true },
|
|
116
|
+
transaction: options == null ? void 0 : options.transaction
|
|
117
|
+
});
|
|
118
|
+
if (!defaultGroup) {
|
|
119
|
+
throw new Error("Default usage group is missing; cannot reassign members.");
|
|
120
|
+
}
|
|
121
|
+
for (const member of members) {
|
|
122
|
+
await this.db.getRepository("aiApiGroupMembers").update({
|
|
123
|
+
filterByTk: member.get("id"),
|
|
124
|
+
values: { groupId: defaultGroup.get("id") },
|
|
125
|
+
transaction: options == null ? void 0 : options.transaction
|
|
126
|
+
});
|
|
127
|
+
}
|
|
82
128
|
});
|
|
83
129
|
}
|
|
84
130
|
async load() {
|
|
@@ -95,18 +141,18 @@ class PluginAiApiServer extends import_server.Plugin {
|
|
|
95
141
|
this.app.use((0, import_router.createAiLlmRouter)(this), { after: "idp-oauth-resource-auth", before: "resourcer" });
|
|
96
142
|
this.app.resourceManager.define(import_ai_api_config.default);
|
|
97
143
|
this.app.resourceManager.define(import_ai_api_usage_monitor.default);
|
|
98
|
-
this.app.resourceManager.define(
|
|
144
|
+
this.app.resourceManager.define(import_ai_api_usage_groups.default);
|
|
99
145
|
this.app.db.on("aiApiRolePermissions.afterSave", (model) => {
|
|
100
146
|
(0, import_role_permission.invalidateRolePermissionCache)(model.get("roleName"));
|
|
101
147
|
});
|
|
102
148
|
this.app.db.on("aiApiRolePermissions.afterDestroy", (model) => {
|
|
103
149
|
(0, import_role_permission.invalidateRolePermissionCache)(model.get("roleName"));
|
|
104
150
|
});
|
|
105
|
-
this.app.db.on("
|
|
106
|
-
this.
|
|
151
|
+
this.app.db.on("aiApiUsageGroups.afterSave", (model, options) => {
|
|
152
|
+
this.invalidateGroupAccess(model.get("id"), options == null ? void 0 : options.transaction);
|
|
107
153
|
});
|
|
108
|
-
this.app.db.on("
|
|
109
|
-
this.
|
|
154
|
+
this.app.db.on("aiApiUsageGroups.afterDestroy", (model, options) => {
|
|
155
|
+
this.invalidateGroupAccess(model.get("id"), options == null ? void 0 : options.transaction);
|
|
110
156
|
});
|
|
111
157
|
this.app.acl.registerSnippet({
|
|
112
158
|
name: import_constants.AI_API_ACL_SNIPPET,
|
|
@@ -115,38 +161,35 @@ class PluginAiApiServer extends import_server.Plugin {
|
|
|
115
161
|
"aiApiRolePermissions:*",
|
|
116
162
|
"aiApiModelPrices:*",
|
|
117
163
|
"aiApiModelMetadata:*",
|
|
118
|
-
"
|
|
119
|
-
"
|
|
120
|
-
"
|
|
164
|
+
"aiApiUsageGroups:*",
|
|
165
|
+
"aiApiGroupMembers:*",
|
|
166
|
+
"aiApiGroupQuotaBuckets:list",
|
|
167
|
+
"aiApiGroupQuotaBuckets:get",
|
|
121
168
|
"aiApiUsageRecords:list",
|
|
122
169
|
"aiApiUsageRecords:get",
|
|
123
170
|
"aiApiUsageMonitor:summary"
|
|
124
171
|
]
|
|
125
172
|
});
|
|
126
|
-
this.app.acl.registerSnippet({
|
|
127
|
-
name: import_constants.AI_API_USER_PERMISSIONS_SNIPPET,
|
|
128
|
-
actions: ["aiApiUserPermissions:*"]
|
|
129
|
-
});
|
|
130
173
|
this.gcInterval = setInterval(() => this.rateLimiter.gc(), 5 * 60 * 1e3);
|
|
131
174
|
this.gcInterval.unref();
|
|
132
175
|
}
|
|
133
176
|
/**
|
|
134
|
-
* Drop a
|
|
177
|
+
* Drop a group's cached model-access scope on every node.
|
|
135
178
|
*
|
|
136
179
|
* The local call is not redundant: syncMessageManager hardcodes skipSelf, so the publishing
|
|
137
180
|
* node never receives its own message. Passing the transaction defers the broadcast until
|
|
138
181
|
* the write commits, so other nodes cannot re-read the old row and re-cache it.
|
|
139
182
|
*/
|
|
140
|
-
|
|
141
|
-
(0, import_user_permissions.
|
|
142
|
-
this.sendSyncMessage({ type: "
|
|
183
|
+
invalidateGroupAccess(groupId, transaction) {
|
|
184
|
+
(0, import_user_permissions.invalidateGroupAccessCache)(groupId);
|
|
185
|
+
this.sendSyncMessage({ type: "invalidateGroupAccess", groupId }, { transaction });
|
|
143
186
|
}
|
|
144
187
|
/**
|
|
145
188
|
* Received only on the *other* nodes (skipSelf), so this must not re-broadcast.
|
|
146
189
|
*/
|
|
147
190
|
async handleSyncMessage(message) {
|
|
148
|
-
if ((message == null ? void 0 : message.type) === "
|
|
149
|
-
(0, import_user_permissions.
|
|
191
|
+
if ((message == null ? void 0 : message.type) === "invalidateGroupAccess") {
|
|
192
|
+
(0, import_user_permissions.invalidateGroupAccessCache)(message.groupId);
|
|
150
193
|
}
|
|
151
194
|
}
|
|
152
195
|
async install() {
|
|
@@ -156,13 +199,38 @@ class PluginAiApiServer extends import_server.Plugin {
|
|
|
156
199
|
values: {
|
|
157
200
|
defaultAiEmployee: "",
|
|
158
201
|
enabledLlmServices: [],
|
|
159
|
-
rateLimitPerMinute: 60,
|
|
160
202
|
quotaEnabled: false,
|
|
161
203
|
pdfRenderPagesAsImages: false,
|
|
162
204
|
defaultReservationOutputTokens: 4096
|
|
163
205
|
}
|
|
164
206
|
});
|
|
165
207
|
}
|
|
208
|
+
const defaultGroup = await this.db.getRepository("aiApiUsageGroups").findOne({
|
|
209
|
+
filter: { isDefault: true }
|
|
210
|
+
});
|
|
211
|
+
if (!defaultGroup) {
|
|
212
|
+
await this.db.getRepository("aiApiUsageGroups").create({
|
|
213
|
+
values: {
|
|
214
|
+
name: "Default",
|
|
215
|
+
isDefault: true,
|
|
216
|
+
quotaMode: "per_user",
|
|
217
|
+
rateLimitPerMinute: 60,
|
|
218
|
+
enabled: false,
|
|
219
|
+
periodType: "monthly",
|
|
220
|
+
timezone: "UTC",
|
|
221
|
+
requestLimit: null,
|
|
222
|
+
totalTokenLimit: null,
|
|
223
|
+
costLimit: null,
|
|
224
|
+
currency: "USD",
|
|
225
|
+
rejectUnpricedModel: true,
|
|
226
|
+
missingUsageBehavior: "use_reserved",
|
|
227
|
+
contextOverflowBehavior: "reject",
|
|
228
|
+
allowedLlmServices: [],
|
|
229
|
+
allowAllModels: true,
|
|
230
|
+
allowedModels: []
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
}
|
|
166
234
|
}
|
|
167
235
|
async afterEnable() {
|
|
168
236
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
12
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __export = (target, all) => {
|
|
15
|
+
for (var name in all)
|
|
16
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
|
+
};
|
|
18
|
+
var __copyProps = (to, from, except, desc) => {
|
|
19
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
20
|
+
for (let key of __getOwnPropNames(from))
|
|
21
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
22
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
23
|
+
}
|
|
24
|
+
return to;
|
|
25
|
+
};
|
|
26
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
|
+
var quota_groups_exports = {};
|
|
28
|
+
__export(quota_groups_exports, {
|
|
29
|
+
getDefaultGroup: () => getDefaultGroup,
|
|
30
|
+
modelToGroup: () => modelToGroup,
|
|
31
|
+
resolveUserGroup: () => resolveUserGroup
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(quota_groups_exports);
|
|
34
|
+
async function getDefaultGroup(ctx) {
|
|
35
|
+
const existing = await ctx.db.getRepository("aiApiUsageGroups").findOne({
|
|
36
|
+
filter: { isDefault: true }
|
|
37
|
+
});
|
|
38
|
+
if (existing) {
|
|
39
|
+
return modelToGroup(existing);
|
|
40
|
+
}
|
|
41
|
+
const created = await ctx.db.getRepository("aiApiUsageGroups").create({
|
|
42
|
+
values: {
|
|
43
|
+
name: "Default",
|
|
44
|
+
isDefault: true,
|
|
45
|
+
quotaMode: "per_user",
|
|
46
|
+
rateLimitPerMinute: 60,
|
|
47
|
+
enabled: false,
|
|
48
|
+
periodType: "monthly",
|
|
49
|
+
timezone: "UTC",
|
|
50
|
+
requestLimit: null,
|
|
51
|
+
totalTokenLimit: null,
|
|
52
|
+
costLimit: null,
|
|
53
|
+
currency: "USD",
|
|
54
|
+
rejectUnpricedModel: true,
|
|
55
|
+
missingUsageBehavior: "use_reserved",
|
|
56
|
+
contextOverflowBehavior: "reject",
|
|
57
|
+
allowedLlmServices: [],
|
|
58
|
+
allowAllModels: true,
|
|
59
|
+
allowedModels: []
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return modelToGroup(created);
|
|
63
|
+
}
|
|
64
|
+
async function resolveUserGroup(ctx, userId) {
|
|
65
|
+
if (userId === void 0 || userId === null) {
|
|
66
|
+
return getDefaultGroup(ctx);
|
|
67
|
+
}
|
|
68
|
+
const member = await ctx.db.getRepository("aiApiGroupMembers").findOne({
|
|
69
|
+
filter: { userId },
|
|
70
|
+
appends: ["group"]
|
|
71
|
+
});
|
|
72
|
+
if (member && member.get("group")) {
|
|
73
|
+
return modelToGroup(member.get("group"));
|
|
74
|
+
}
|
|
75
|
+
return getDefaultGroup(ctx);
|
|
76
|
+
}
|
|
77
|
+
function modelToGroup(model) {
|
|
78
|
+
const record = typeof model.get === "function" ? model.get() : model;
|
|
79
|
+
return {
|
|
80
|
+
id: record.id,
|
|
81
|
+
name: record.name ?? "",
|
|
82
|
+
isDefault: !!record.isDefault,
|
|
83
|
+
quotaMode: record.quotaMode ?? "per_user",
|
|
84
|
+
rateLimitPerMinute: Number(record.rateLimitPerMinute ?? 60),
|
|
85
|
+
enabled: !!record.enabled,
|
|
86
|
+
periodType: record.periodType ?? "monthly",
|
|
87
|
+
timezone: record.timezone ?? "UTC",
|
|
88
|
+
requestLimit: record.requestLimit ?? null,
|
|
89
|
+
totalTokenLimit: record.totalTokenLimit ?? null,
|
|
90
|
+
costLimit: record.costLimit ?? null,
|
|
91
|
+
currency: record.currency ?? "USD",
|
|
92
|
+
rejectUnpricedModel: !!record.rejectUnpricedModel,
|
|
93
|
+
missingUsageBehavior: record.missingUsageBehavior ?? "use_reserved",
|
|
94
|
+
contextOverflowBehavior: record.contextOverflowBehavior ?? "reject",
|
|
95
|
+
allowedLlmServices: stringArray(record.allowedLlmServices),
|
|
96
|
+
allowAllModels: record.allowAllModels !== false,
|
|
97
|
+
allowedModels: stringArray(record.allowedModels)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function stringArray(value) {
|
|
101
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
102
|
+
}
|
|
103
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
104
|
+
0 && (module.exports = {
|
|
105
|
+
getDefaultGroup,
|
|
106
|
+
modelToGroup,
|
|
107
|
+
resolveUserGroup
|
|
108
|
+
});
|
|
@@ -53,7 +53,6 @@ const aiApiConfigResource = {
|
|
|
53
53
|
defaultAiEmployee: "",
|
|
54
54
|
defaultLlmService: "",
|
|
55
55
|
enabledLlmServices: [],
|
|
56
|
-
rateLimitPerMinute: 60,
|
|
57
56
|
maxRequestBodyMb: 10,
|
|
58
57
|
pdfRenderPagesAsImages: false,
|
|
59
58
|
quotaEnabled: false,
|
|
@@ -76,7 +75,6 @@ const aiApiConfigResource = {
|
|
|
76
75
|
defaultAiEmployee: values.defaultAiEmployee ?? "",
|
|
77
76
|
defaultLlmService: values.defaultLlmService ?? "",
|
|
78
77
|
enabledLlmServices: values.enabledLlmServices ?? [],
|
|
79
|
-
rateLimitPerMinute: values.rateLimitPerMinute ?? 60,
|
|
80
78
|
maxRequestBodyMb: coerceMaxRequestBodyMb(values.maxRequestBodyMb ?? DEFAULT_MAX_REQUEST_BODY_MB),
|
|
81
79
|
pdfRenderPagesAsImages: values.pdfRenderPagesAsImages ?? false,
|
|
82
80
|
quotaEnabled: values.quotaEnabled ?? false,
|
|
@@ -90,7 +88,6 @@ const aiApiConfigResource = {
|
|
|
90
88
|
if (values.defaultAiEmployee !== void 0) updateData.defaultAiEmployee = values.defaultAiEmployee;
|
|
91
89
|
if (values.defaultLlmService !== void 0) updateData.defaultLlmService = values.defaultLlmService;
|
|
92
90
|
if (values.enabledLlmServices !== void 0) updateData.enabledLlmServices = values.enabledLlmServices;
|
|
93
|
-
if (values.rateLimitPerMinute !== void 0) updateData.rateLimitPerMinute = values.rateLimitPerMinute;
|
|
94
91
|
if (values.maxRequestBodyMb !== void 0) {
|
|
95
92
|
updateData.maxRequestBodyMb = coerceMaxRequestBodyMb(values.maxRequestBodyMb);
|
|
96
93
|
}
|
|
@@ -0,0 +1,168 @@
|
|
|
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
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
12
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __export = (target, all) => {
|
|
15
|
+
for (var name in all)
|
|
16
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
|
+
};
|
|
18
|
+
var __copyProps = (to, from, except, desc) => {
|
|
19
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
20
|
+
for (let key of __getOwnPropNames(from))
|
|
21
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
22
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
23
|
+
}
|
|
24
|
+
return to;
|
|
25
|
+
};
|
|
26
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
|
+
var ai_api_usage_groups_exports = {};
|
|
28
|
+
__export(ai_api_usage_groups_exports, {
|
|
29
|
+
default: () => ai_api_usage_groups_default
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(ai_api_usage_groups_exports);
|
|
32
|
+
const USER_SUMMARY_FIELDS = ["id", "username", "nickname", "email"];
|
|
33
|
+
async function getByUser(ctx, next) {
|
|
34
|
+
const { userId } = ctx.action.params;
|
|
35
|
+
if (!userId) {
|
|
36
|
+
ctx.throw(400, "userId is required");
|
|
37
|
+
}
|
|
38
|
+
const member = await ctx.db.getRepository("aiApiGroupMembers").findOne({
|
|
39
|
+
filter: { userId },
|
|
40
|
+
appends: ["group"]
|
|
41
|
+
});
|
|
42
|
+
if (member && member.get("group")) {
|
|
43
|
+
ctx.body = member.get("group");
|
|
44
|
+
return next();
|
|
45
|
+
}
|
|
46
|
+
const defaultGroup = await ctx.db.getRepository("aiApiUsageGroups").findOne({
|
|
47
|
+
filter: { isDefault: true }
|
|
48
|
+
});
|
|
49
|
+
if (!defaultGroup) {
|
|
50
|
+
ctx.throw(404, "Default group not found");
|
|
51
|
+
}
|
|
52
|
+
ctx.body = defaultGroup;
|
|
53
|
+
return next();
|
|
54
|
+
}
|
|
55
|
+
async function listUnassignedUsers(ctx, next) {
|
|
56
|
+
const { page = 1, pageSize = 20, keyword } = ctx.action.params;
|
|
57
|
+
const memberRows = await ctx.db.getRepository("aiApiGroupMembers").find({
|
|
58
|
+
fields: ["userId"],
|
|
59
|
+
pageSize: 1e4
|
|
60
|
+
});
|
|
61
|
+
const assignedUserIds = memberRows.map((row) => {
|
|
62
|
+
var _a;
|
|
63
|
+
const value = ((_a = row.get) == null ? void 0 : _a.call(row, "userId")) ?? row.userId;
|
|
64
|
+
return String(value);
|
|
65
|
+
});
|
|
66
|
+
const where = {};
|
|
67
|
+
if (keyword) {
|
|
68
|
+
where.$or = [
|
|
69
|
+
{ username: { $includes: keyword } },
|
|
70
|
+
{ email: { $includes: keyword } },
|
|
71
|
+
{ nickname: { $includes: keyword } }
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
if (assignedUserIds.length > 0) {
|
|
75
|
+
where.id = { $notIn: assignedUserIds };
|
|
76
|
+
}
|
|
77
|
+
const repository = ctx.db.getRepository("users");
|
|
78
|
+
const [rows, count] = await repository.findAndCount({
|
|
79
|
+
filter: where,
|
|
80
|
+
fields: USER_SUMMARY_FIELDS,
|
|
81
|
+
page: Number(page),
|
|
82
|
+
pageSize: Number(pageSize)
|
|
83
|
+
});
|
|
84
|
+
ctx.body = { rows, count };
|
|
85
|
+
return next();
|
|
86
|
+
}
|
|
87
|
+
async function searchUsers(ctx, next) {
|
|
88
|
+
const { keyword, pageSize = 20 } = ctx.action.params;
|
|
89
|
+
const where = {};
|
|
90
|
+
if (keyword) {
|
|
91
|
+
where.$or = [
|
|
92
|
+
{ username: { $includes: keyword } },
|
|
93
|
+
{ email: { $includes: keyword } },
|
|
94
|
+
{ nickname: { $includes: keyword } }
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
const [rows, count] = await ctx.db.getRepository("users").findAndCount({
|
|
98
|
+
filter: where,
|
|
99
|
+
fields: USER_SUMMARY_FIELDS,
|
|
100
|
+
page: 1,
|
|
101
|
+
pageSize: Number(pageSize)
|
|
102
|
+
});
|
|
103
|
+
ctx.body = { rows, count };
|
|
104
|
+
return next();
|
|
105
|
+
}
|
|
106
|
+
async function addMember(ctx, next) {
|
|
107
|
+
const { groupId, userId } = ctx.action.params.values || ctx.action.params || {};
|
|
108
|
+
if (!groupId || !userId) {
|
|
109
|
+
ctx.throw(400, "groupId and userId are required");
|
|
110
|
+
}
|
|
111
|
+
const group = await ctx.db.getRepository("aiApiUsageGroups").findOne({
|
|
112
|
+
filterByTk: groupId
|
|
113
|
+
});
|
|
114
|
+
if (!group) {
|
|
115
|
+
ctx.throw(404, "Usage group not found");
|
|
116
|
+
}
|
|
117
|
+
if (group.get("isDefault")) {
|
|
118
|
+
ctx.throw(400, "Cannot add members to the default group: users without another group use it automatically");
|
|
119
|
+
}
|
|
120
|
+
const existing = await ctx.db.getRepository("aiApiGroupMembers").findOne({
|
|
121
|
+
filter: { userId }
|
|
122
|
+
});
|
|
123
|
+
if (existing) {
|
|
124
|
+
ctx.throw(409, "User already belongs to a group");
|
|
125
|
+
}
|
|
126
|
+
const member = await ctx.db.getRepository("aiApiGroupMembers").create({
|
|
127
|
+
values: { groupId, userId }
|
|
128
|
+
});
|
|
129
|
+
ctx.body = member;
|
|
130
|
+
return next();
|
|
131
|
+
}
|
|
132
|
+
async function removeMember(ctx, next) {
|
|
133
|
+
const { groupId, userId } = ctx.action.params.values || ctx.action.params || {};
|
|
134
|
+
if (!groupId || !userId) {
|
|
135
|
+
ctx.throw(400, "groupId and userId are required");
|
|
136
|
+
}
|
|
137
|
+
const member = await ctx.db.getRepository("aiApiGroupMembers").findOne({
|
|
138
|
+
filter: { groupId, userId }
|
|
139
|
+
});
|
|
140
|
+
if (!member) {
|
|
141
|
+
ctx.throw(404, "Member not found");
|
|
142
|
+
}
|
|
143
|
+
await ctx.db.getRepository("aiApiGroupMembers").destroy({
|
|
144
|
+
filterByTk: member.get("id")
|
|
145
|
+
});
|
|
146
|
+
ctx.body = member;
|
|
147
|
+
return next();
|
|
148
|
+
}
|
|
149
|
+
var ai_api_usage_groups_default = {
|
|
150
|
+
name: "aiApiUsageGroups",
|
|
151
|
+
actions: {
|
|
152
|
+
getByUser: {
|
|
153
|
+
handler: getByUser
|
|
154
|
+
},
|
|
155
|
+
listUnassignedUsers: {
|
|
156
|
+
handler: listUnassignedUsers
|
|
157
|
+
},
|
|
158
|
+
searchUsers: {
|
|
159
|
+
handler: searchUsers
|
|
160
|
+
},
|
|
161
|
+
addMember: {
|
|
162
|
+
handler: addMember
|
|
163
|
+
},
|
|
164
|
+
removeMember: {
|
|
165
|
+
handler: removeMember
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
@@ -33,6 +33,7 @@ var import_openai_format = require("../utils/openai-format");
|
|
|
33
33
|
var import_resolve_service = require("../utils/resolve-service");
|
|
34
34
|
var import_role_permission = require("../middleware/role-permission");
|
|
35
35
|
var import_user_permissions = require("../utils/user-permissions");
|
|
36
|
+
var import_request_cache = require("../utils/request-cache");
|
|
36
37
|
var import_streaming = require("../utils/streaming");
|
|
37
38
|
var import_ai_employee_runtime = require("../utils/ai-employee-runtime");
|
|
38
39
|
var import_usage = require("../usage");
|
|
@@ -60,7 +61,7 @@ async function handleAgentCompletions(ctx, plugin) {
|
|
|
60
61
|
);
|
|
61
62
|
return;
|
|
62
63
|
}
|
|
63
|
-
const config = await
|
|
64
|
+
const config = await (0, import_request_cache.getAiApiConfig)(ctx);
|
|
64
65
|
const defaultAiEmployee = config ? config.get("defaultAiEmployee") || config.defaultAiEmployee : null;
|
|
65
66
|
if (!defaultAiEmployee) {
|
|
66
67
|
ctx.status = 400;
|
|
@@ -27,6 +27,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
27
27
|
var chat_completions_exports = {};
|
|
28
28
|
__export(chat_completions_exports, {
|
|
29
29
|
applyProviderRequestParameters: () => applyProviderRequestParameters,
|
|
30
|
+
extractFinishReason: () => extractFinishReason,
|
|
30
31
|
findContentBlockProblem: () => findContentBlockProblem,
|
|
31
32
|
findMessageProblem: () => findMessageProblem,
|
|
32
33
|
getProviderRequestParameters: () => getProviderRequestParameters,
|
|
@@ -37,8 +38,8 @@ module.exports = __toCommonJS(chat_completions_exports);
|
|
|
37
38
|
var import_openai_format = require("../utils/openai-format");
|
|
38
39
|
var import_resolve_service = require("../utils/resolve-service");
|
|
39
40
|
var import_streaming = require("../utils/streaming");
|
|
40
|
-
var import_role_permission = require("../middleware/role-permission");
|
|
41
41
|
var import_user_permissions = require("../utils/user-permissions");
|
|
42
|
+
var import_request_cache = require("../utils/request-cache");
|
|
42
43
|
var import_usage = require("../usage");
|
|
43
44
|
var import_billing = require("../billing");
|
|
44
45
|
var import_direct_llm_context = require("../utils/direct-llm-context");
|
|
@@ -119,7 +120,7 @@ async function handleChatCompletions(ctx, plugin) {
|
|
|
119
120
|
);
|
|
120
121
|
return;
|
|
121
122
|
}
|
|
122
|
-
const config = await
|
|
123
|
+
const config = await (0, import_request_cache.getAiApiConfig)(ctx);
|
|
123
124
|
if (!await (0, import_user_permissions.enforceModelAccess)(ctx, config == null ? void 0 : config.enabledLlmServices, service, modelId)) {
|
|
124
125
|
return;
|
|
125
126
|
}
|
|
@@ -148,30 +149,7 @@ async function handleChatCompletions(ctx, plugin) {
|
|
|
148
149
|
if (body.frequency_penalty !== void 0) modelOptions.frequencyPenalty = body.frequency_penalty;
|
|
149
150
|
if (body.presence_penalty !== void 0) modelOptions.presencePenalty = body.presence_penalty;
|
|
150
151
|
if (body.stop !== void 0) modelOptions.stop = body.stop;
|
|
151
|
-
let systemPrompt = "";
|
|
152
|
-
if (config == null ? void 0 : config.defaultAiEmployee) {
|
|
153
|
-
if (!(0, import_role_permission.checkEmployeeAccess)(ctx, config.defaultAiEmployee)) {
|
|
154
|
-
ctx.status = 403;
|
|
155
|
-
ctx.body = (0, import_openai_format.toOpenAIError)(
|
|
156
|
-
403,
|
|
157
|
-
`Role is not permitted to use AI Employee '${config.defaultAiEmployee}'. An admin must grant access in Settings \u2192 Users & Permissions \u2192 [Role] \u2192 AI API.`,
|
|
158
|
-
"permission_denied",
|
|
159
|
-
"employee_not_permitted"
|
|
160
|
-
);
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
const employee = await ctx.db.getRepository("aiEmployees").findOne({
|
|
164
|
-
filter: { username: config.defaultAiEmployee }
|
|
165
|
-
});
|
|
166
|
-
if (employee) {
|
|
167
|
-
systemPrompt = employee.about || employee.defaultPrompt || "";
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
152
|
let messages = [...body.messages];
|
|
171
|
-
const hasSystemMessage = messages.some((m) => m.role === "system");
|
|
172
|
-
if (systemPrompt && !hasSystemMessage) {
|
|
173
|
-
messages.unshift({ role: "system", content: systemPrompt });
|
|
174
|
-
}
|
|
175
153
|
messages = await Promise.all(
|
|
176
154
|
messages.map(async (msg) => ({
|
|
177
155
|
...msg,
|
|
@@ -271,12 +249,14 @@ async function handleNonStreamingCompletion(ctx, chatModel, messages, completion
|
|
|
271
249
|
);
|
|
272
250
|
ctx.status = 200;
|
|
273
251
|
const toolCalls = normalizeToolCalls(result.tool_calls);
|
|
252
|
+
const providerFinishReason = extractFinishReason(result);
|
|
274
253
|
ctx.body = (0, import_openai_format.toOpenAIResponse)({
|
|
275
254
|
id: completionId,
|
|
276
255
|
model: modelName,
|
|
277
256
|
content,
|
|
278
257
|
usage: usage ?? { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
279
|
-
toolCalls
|
|
258
|
+
toolCalls,
|
|
259
|
+
...providerFinishReason ? { finishReason: providerFinishReason } : {}
|
|
280
260
|
});
|
|
281
261
|
}
|
|
282
262
|
async function handleStreamingCompletion(ctx, chatModel, messages, completionId, modelName, providerRequestParameters) {
|
|
@@ -300,8 +280,10 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
|
|
|
300
280
|
);
|
|
301
281
|
const requestAbort = (0, import_streaming.createRequestAbortController)(ctx);
|
|
302
282
|
let usage;
|
|
283
|
+
let usageResponseMetadata;
|
|
303
284
|
let providerRequestId;
|
|
304
|
-
let
|
|
285
|
+
let providerFinishReason;
|
|
286
|
+
let sawToolCalls = false;
|
|
305
287
|
try {
|
|
306
288
|
const stream = await chatModel.stream(messages, { ...providerRequestParameters, signal: requestAbort.signal });
|
|
307
289
|
for await (const chunk of stream) {
|
|
@@ -329,7 +311,7 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
|
|
|
329
311
|
const toolCallChunks = normalizeToolCallChunks(chunk.tool_call_chunks);
|
|
330
312
|
if (toolCallChunks.length) {
|
|
331
313
|
(0, import_app_observability.markAiApiFirstProviderOutput)(ctx);
|
|
332
|
-
|
|
314
|
+
sawToolCalls = true;
|
|
333
315
|
await (0, import_streaming.writeResponse)(
|
|
334
316
|
ctx,
|
|
335
317
|
(0, import_openai_format.formatSSE)(
|
|
@@ -342,8 +324,14 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
|
|
|
342
324
|
);
|
|
343
325
|
}
|
|
344
326
|
if (chunk.usage_metadata) {
|
|
345
|
-
|
|
327
|
+
const normalized = (0, import_usage.normalizeUsage)(chunk.usage_metadata);
|
|
328
|
+
if (normalized) {
|
|
329
|
+
usage = normalized;
|
|
330
|
+
usageResponseMetadata = chunk.response_metadata;
|
|
331
|
+
}
|
|
346
332
|
}
|
|
333
|
+
const chunkFinishReason = extractFinishReason(chunk);
|
|
334
|
+
if (chunkFinishReason) providerFinishReason = chunkFinishReason;
|
|
347
335
|
providerRequestId = providerRequestId ?? (0, import_usage.extractProviderRequestId)(chunk);
|
|
348
336
|
}
|
|
349
337
|
await (0, import_streaming.writeResponse)(
|
|
@@ -353,7 +341,7 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
|
|
|
353
341
|
id: completionId,
|
|
354
342
|
model: modelName,
|
|
355
343
|
delta: {},
|
|
356
|
-
finishReason
|
|
344
|
+
finishReason: providerFinishReason ?? (sawToolCalls ? "tool_calls" : "stop")
|
|
357
345
|
})
|
|
358
346
|
)
|
|
359
347
|
);
|
|
@@ -370,7 +358,7 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
|
|
|
370
358
|
);
|
|
371
359
|
}
|
|
372
360
|
await (0, import_streaming.writeResponse)(ctx, (0, import_openai_format.formatSSEDone)());
|
|
373
|
-
(0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
|
|
361
|
+
(0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId }, usageResponseMetadata);
|
|
374
362
|
ctx.state.aiApiStreamResult = { succeeded: true, id: completionId };
|
|
375
363
|
} catch (err) {
|
|
376
364
|
const cancelled = (0, import_streaming.isClientDisconnected)(ctx, err);
|
|
@@ -386,7 +374,7 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
|
|
|
386
374
|
})
|
|
387
375
|
);
|
|
388
376
|
}
|
|
389
|
-
(0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId });
|
|
377
|
+
(0, import_usage.setAiApiUsageResult)(ctx, usage, { gatewayResponseId: completionId, providerRequestId }, usageResponseMetadata);
|
|
390
378
|
ctx.state.aiApiStreamResult = {
|
|
391
379
|
succeeded: false,
|
|
392
380
|
id: completionId,
|
|
@@ -400,6 +388,17 @@ async function handleStreamingCompletion(ctx, chatModel, messages, completionId,
|
|
|
400
388
|
function getErrorMessage(error, fallback) {
|
|
401
389
|
return error instanceof Error && error.message ? error.message : fallback;
|
|
402
390
|
}
|
|
391
|
+
function extractFinishReason(value) {
|
|
392
|
+
if (!isRecord(value)) return void 0;
|
|
393
|
+
const candidates = [
|
|
394
|
+
isRecord(value.response_metadata) ? value.response_metadata.finish_reason : void 0,
|
|
395
|
+
isRecord(value.additional_kwargs) ? value.additional_kwargs.finish_reason : void 0
|
|
396
|
+
];
|
|
397
|
+
for (const candidate of candidates) {
|
|
398
|
+
if (typeof candidate === "string" && candidate.length > 0) return candidate;
|
|
399
|
+
}
|
|
400
|
+
return void 0;
|
|
401
|
+
}
|
|
403
402
|
const SUPPORTED_CONTENT_BLOCK_TYPES = /* @__PURE__ */ new Set(["text", "image_url", "file", "file_url"]);
|
|
404
403
|
const BASE64_DATA_URL_PATTERN = /^data:(\w+\/\w+);base64,([A-Za-z0-9+/]+=*)$/;
|
|
405
404
|
const FILE_BASE64_DATA_URL_PATTERN = /^data:([^;\s]+);base64,([A-Za-z0-9+/]+=*)$/;
|
|
@@ -641,6 +640,7 @@ function serializeToolArguments(value) {
|
|
|
641
640
|
// Annotate the CommonJS export names for ESM import in node:
|
|
642
641
|
0 && (module.exports = {
|
|
643
642
|
applyProviderRequestParameters,
|
|
643
|
+
extractFinishReason,
|
|
644
644
|
findContentBlockProblem,
|
|
645
645
|
findMessageProblem,
|
|
646
646
|
getProviderRequestParameters,
|