plugin-ai-api 1.1.1 → 1.1.2
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/README.md +51 -12
- package/dist/client/185.c47663fefaeb0e5b.js +10 -0
- package/dist/client/562.9012cfd1fa04303d.js +10 -0
- package/dist/client/685.b5b1e0a5b825d253.js +10 -0
- package/dist/client/index.js +1 -1
- package/dist/client-v2/185.b552dc91ec2371ba.js +10 -0
- package/dist/client-v2/562.db2984167250b1be.js +10 -0
- package/dist/client-v2/685.cf16e5b829e06f85.js +10 -0
- package/dist/client-v2/index.js +1 -1
- package/dist/externalVersion.js +8 -8
- package/dist/locale/en-US.json +175 -139
- package/dist/locale/vi-VN.json +40 -2
- package/dist/locale/zh-CN.json +40 -2
- package/dist/server/collections/ai-api-model-metadata.js +26 -0
- package/dist/server/collections/ai-api-response-records.js +101 -0
- package/dist/server/collections/ai-api-virtual-models.js +68 -0
- package/dist/server/middleware/response-record-resource.js +66 -0
- package/dist/server/middleware/role-permission.js +43 -18
- package/dist/server/migrations/20260901000000-remove-default-group-members.js +60 -0
- package/dist/server/migrations/20260902000000-seed-default-role-permissions.js +55 -0
- package/dist/server/migrations/20260903000000-seed-sample-response-records.js +170 -0
- package/dist/server/plugin.js +66 -16
- package/dist/server/routes/chat-completions.js +38 -6
- package/dist/server/routes/completions.js +16 -4
- package/dist/server/routes/embeddings.js +25 -6
- package/dist/server/routes/models.js +29 -0
- package/dist/server/routes/responses.js +530 -0
- package/dist/server/routes/router.js +65 -10
- package/dist/server/usage.js +25 -4
- package/dist/server/utils/direct-llm-context.js +1 -1
- package/dist/server/utils/resolve-service.js +24 -0
- package/dist/server/utils/response-store.js +138 -0
- package/dist/server/utils/responses-format.js +686 -0
- package/dist/server/utils/responses-stream.js +330 -0
- package/dist/server/utils/virtual-models.js +238 -0
- package/dist/server/validation.js +44 -2
- package/dist/swagger.js +137 -0
- package/package.json +34 -32
- package/src/__tests__/locale.test.ts +43 -0
- package/src/client/__tests__/settings-registration.test.tsx +1 -0
- package/src/client/plugin.tsx +9 -1
- package/src/client-v2/__tests__/settings-registration.test.tsx +1 -0
- package/src/client-v2/pages/ModelMetadataPage.tsx +44 -0
- package/src/client-v2/pages/ModelRoutingPage.tsx +238 -0
- package/src/client-v2/pages/UsageGroupsPage.tsx +75 -38
- package/src/client-v2/plugin.tsx +8 -0
- package/src/locale/en-US.json +175 -139
- package/src/locale/vi-VN.json +40 -2
- package/src/locale/zh-CN.json +40 -2
- package/src/server/__tests__/embeddings.test.ts +184 -0
- package/src/server/__tests__/models.test.ts +21 -1
- package/src/server/__tests__/response-record-resource.test.ts +50 -0
- package/src/server/__tests__/response-store-integration.test.ts +341 -0
- package/src/server/__tests__/response-store.test.ts +195 -0
- package/src/server/__tests__/responses-contract.test.ts +469 -0
- package/src/server/__tests__/responses-format.test.ts +299 -0
- package/src/server/__tests__/responses-router.test.ts +182 -0
- package/src/server/__tests__/responses-streaming.test.ts +368 -0
- package/src/server/__tests__/responses.test.ts +462 -0
- package/src/server/__tests__/role-permission.test.ts +139 -0
- package/src/server/__tests__/seed-role-permission.test.ts +88 -0
- package/src/server/__tests__/types/responses-sdk.types.test-d.ts +23 -0
- package/src/server/__tests__/usage-groups.test.ts +96 -0
- package/src/server/__tests__/usage-route.test.ts +1 -0
- package/src/server/__tests__/usage.test.ts +14 -0
- package/src/server/__tests__/validation.test.ts +66 -7
- package/src/server/__tests__/virtual-model-routing.test.ts +589 -0
- package/src/server/collections/ai-api-model-metadata.ts +26 -0
- package/src/server/collections/ai-api-response-records.ts +77 -0
- package/src/server/collections/ai-api-virtual-models.ts +58 -0
- package/src/server/middleware/response-record-resource.ts +44 -0
- package/src/server/middleware/role-permission.ts +69 -35
- package/src/server/migrations/20260901000000-remove-default-group-members.ts +56 -0
- package/src/server/migrations/20260902000000-seed-default-role-permissions.ts +46 -0
- package/src/server/migrations/20260903000000-seed-sample-response-records.ts +162 -0
- package/src/server/plugin.ts +84 -20
- package/src/server/resource/ai-api-config.ts +2 -1
- package/src/server/routes/agent-completions.ts +3 -0
- package/src/server/routes/chat-completions.ts +34 -10
- package/src/server/routes/completions.ts +16 -4
- package/src/server/routes/embeddings.ts +32 -10
- package/src/server/routes/models.ts +34 -0
- package/src/server/routes/responses.ts +640 -0
- package/src/server/routes/router.ts +81 -12
- package/src/server/services/__tests__/file-processor.test.ts +1 -0
- package/src/server/usage.ts +29 -2
- package/src/server/utils/app-observability.ts +1 -1
- package/src/server/utils/direct-llm-context.ts +2 -1
- package/src/server/utils/openai-format.ts +1 -0
- package/src/server/utils/resolve-service.ts +39 -1
- package/src/server/utils/response-store.ts +148 -0
- package/src/server/utils/responses-format.ts +974 -0
- package/src/server/utils/responses-stream.ts +384 -0
- package/src/server/utils/virtual-models.ts +320 -0
- package/src/server/validation.ts +49 -0
- package/src/swagger.ts +139 -0
- package/dist/client/562.44b16aad4718b4c7.js +0 -10
- package/dist/client/685.ae483e17b6b49c98.js +0 -10
- package/dist/client-v2/562.45d5c504433be38b.js +0 -10
- package/dist/client-v2/685.1030370b309b7d4b.js +0 -10
- package/dist/server/collections/ai-api-user-permissions.js +0 -67
- package/dist/server/collections/ai-api-user-quota-buckets.js +0 -54
- package/dist/server/collections/ai-api-user-quota-policies.js +0 -63
- package/dist/server/resource/ai-api-usage-groups.js +0 -168
- package/src/server/collections/ai-api-user-permissions.ts +0 -46
- package/src/server/collections/ai-api-user-quota-buckets.ts +0 -24
- package/src/server/collections/ai-api-user-quota-policies.ts +0 -33
- package/src/server/resource/ai-api-usage-groups.ts +0 -171
|
@@ -0,0 +1,66 @@
|
|
|
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 response_record_resource_exports = {};
|
|
28
|
+
__export(response_record_resource_exports, {
|
|
29
|
+
RESPONSE_RECORD_COLLECTION: () => RESPONSE_RECORD_COLLECTION,
|
|
30
|
+
blockResponseRecordResource: () => blockResponseRecordResource,
|
|
31
|
+
targetsResponseRecordCollection: () => targetsResponseRecordCollection
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(response_record_resource_exports);
|
|
34
|
+
const RESPONSE_RECORD_COLLECTION = "aiApiResponseRecords";
|
|
35
|
+
function routeCandidates(ctx) {
|
|
36
|
+
var _a, _b;
|
|
37
|
+
const params = (_a = ctx.action) == null ? void 0 : _a.params;
|
|
38
|
+
const candidates = [(_b = ctx.action) == null ? void 0 : _b.resourceName, params == null ? void 0 : params.resourceName, params == null ? void 0 : params.associatedName, params == null ? void 0 : params.targetCollection];
|
|
39
|
+
return candidates.filter((value) => typeof value === "string").flatMap((value) => value.split("."));
|
|
40
|
+
}
|
|
41
|
+
function targetsResponseRecordCollection(ctx) {
|
|
42
|
+
var _a, _b;
|
|
43
|
+
const repositoryContext = ctx;
|
|
44
|
+
if (repositoryContext.getCurrentRepository) {
|
|
45
|
+
try {
|
|
46
|
+
if (((_b = (_a = repositoryContext.getCurrentRepository()) == null ? void 0 : _a.targetCollection) == null ? void 0 : _b.name) === RESPONSE_RECORD_COLLECTION) return true;
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return routeCandidates(ctx).includes(RESPONSE_RECORD_COLLECTION);
|
|
51
|
+
}
|
|
52
|
+
function blockResponseRecordResource() {
|
|
53
|
+
return async (ctx, next) => {
|
|
54
|
+
if (targetsResponseRecordCollection(ctx)) {
|
|
55
|
+
ctx.throw(404, "Not Found");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
await next();
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
62
|
+
0 && (module.exports = {
|
|
63
|
+
RESPONSE_RECORD_COLLECTION,
|
|
64
|
+
blockResponseRecordResource,
|
|
65
|
+
targetsResponseRecordCollection
|
|
66
|
+
});
|
|
@@ -35,38 +35,63 @@ var import_openai_format = require("../utils/openai-format");
|
|
|
35
35
|
const PERMISSION_TTL_MS = 15e3;
|
|
36
36
|
const permissionCache = /* @__PURE__ */ new Map();
|
|
37
37
|
function invalidateRolePermissionCache(roleName) {
|
|
38
|
-
if (roleName)
|
|
39
|
-
|
|
38
|
+
if (!roleName) {
|
|
39
|
+
permissionCache.clear();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const suffix = `:role:${roleName}`;
|
|
43
|
+
for (const key of permissionCache.keys()) {
|
|
44
|
+
if (key.endsWith(suffix)) permissionCache.delete(key);
|
|
45
|
+
}
|
|
40
46
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return
|
|
47
|
+
function unwrapRecord(raw) {
|
|
48
|
+
if (!raw) return null;
|
|
49
|
+
if (typeof raw.get === "function") {
|
|
50
|
+
const model = raw;
|
|
51
|
+
return {
|
|
52
|
+
roleName: model.get("roleName"),
|
|
53
|
+
enabled: model.get("enabled"),
|
|
54
|
+
allowAllEmployees: model.get("allowAllEmployees"),
|
|
55
|
+
allowedEmployees: model.get("allowedEmployees")
|
|
56
|
+
};
|
|
46
57
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
return raw;
|
|
59
|
+
}
|
|
60
|
+
async function loadRolePermission(ctx, roleName) {
|
|
61
|
+
var _a;
|
|
62
|
+
const cacheKey = `${((_a = ctx.app) == null ? void 0 : _a.name) ?? "main"}:role:${roleName}`;
|
|
63
|
+
const cached = permissionCache.get(cacheKey);
|
|
64
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
65
|
+
return cached.record;
|
|
51
66
|
}
|
|
52
|
-
|
|
67
|
+
const record = unwrapRecord(await ctx.db.getRepository("aiApiRolePermissions").findOne({ filter: { roleName } }));
|
|
68
|
+
permissionCache.set(cacheKey, { record, expiresAt: Date.now() + PERMISSION_TTL_MS });
|
|
69
|
+
return record;
|
|
70
|
+
}
|
|
71
|
+
async function checkRolePermission(ctx) {
|
|
72
|
+
var _a;
|
|
73
|
+
const roleNames = ((_a = ctx.state.currentRoles) == null ? void 0 : _a.length) ? ctx.state.currentRoles : [ctx.state.currentRole || "member"];
|
|
74
|
+
const records = (await Promise.all(roleNames.map((name) => loadRolePermission(ctx, name)))).filter(
|
|
75
|
+
(record) => !!(record == null ? void 0 : record.enabled)
|
|
76
|
+
);
|
|
77
|
+
if (!records.length) {
|
|
53
78
|
ctx.status = 403;
|
|
54
79
|
ctx.body = (0, import_openai_format.toOpenAIError)(
|
|
55
80
|
403,
|
|
56
|
-
`
|
|
81
|
+
`None of the roles [${roleNames.join(", ")}] is authorized to use the AI API. An admin must enable access in Settings \u2192 Users & Permissions \u2192 [Role] \u2192 AI API.`,
|
|
57
82
|
"permission_denied",
|
|
58
83
|
"role_not_permitted"
|
|
59
84
|
);
|
|
60
85
|
return false;
|
|
61
86
|
}
|
|
62
|
-
ctx.state.
|
|
87
|
+
ctx.state.aiApiRolePermissions = records;
|
|
63
88
|
return true;
|
|
64
89
|
}
|
|
65
90
|
function checkEmployeeAccess(ctx, employeeUsername) {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
91
|
+
const perms = ctx.state.aiApiRolePermissions || [];
|
|
92
|
+
return perms.some(
|
|
93
|
+
(perm) => perm.allowAllEmployees || (perm.allowedEmployees || []).includes(employeeUsername)
|
|
94
|
+
);
|
|
70
95
|
}
|
|
71
96
|
// Annotate the CommonJS export names for ESM import in node:
|
|
72
97
|
0 && (module.exports = {
|
|
@@ -0,0 +1,60 @@
|
|
|
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 remove_default_group_members_exports = {};
|
|
28
|
+
__export(remove_default_group_members_exports, {
|
|
29
|
+
default: () => RemoveDefaultGroupMembers
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(remove_default_group_members_exports);
|
|
32
|
+
var import_server = require("@nocobase/server");
|
|
33
|
+
class RemoveDefaultGroupMembers extends import_server.Migration {
|
|
34
|
+
// afterSync: aiApiGroupMembers / aiApiUsageGroups must be registered and synced.
|
|
35
|
+
on = "afterSync";
|
|
36
|
+
async up() {
|
|
37
|
+
const groupCollection = this.db.getCollection("aiApiUsageGroups");
|
|
38
|
+
const memberCollection = this.db.getCollection("aiApiGroupMembers");
|
|
39
|
+
if (!groupCollection || !memberCollection) return;
|
|
40
|
+
if (!await groupCollection.existsInDb() || !await memberCollection.existsInDb()) return;
|
|
41
|
+
const defaultGroup = await this.db.getRepository("aiApiUsageGroups").findOne({
|
|
42
|
+
filter: { isDefault: true }
|
|
43
|
+
});
|
|
44
|
+
if (!defaultGroup) return;
|
|
45
|
+
const groupId = defaultGroup.get("id");
|
|
46
|
+
const members = await this.db.getRepository("aiApiGroupMembers").find({
|
|
47
|
+
filter: { groupId }
|
|
48
|
+
});
|
|
49
|
+
for (const member of members) {
|
|
50
|
+
await this.db.getRepository("aiApiGroupMembers").destroy({ filterByTk: member.get("id") });
|
|
51
|
+
}
|
|
52
|
+
if (members.length) {
|
|
53
|
+
this.app.logger.info(
|
|
54
|
+
`[ai-api] Removed ${members.length} explicit member row(s) from the default usage group; those users now fall back to it implicitly.`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async down() {
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
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 seed_default_role_permissions_exports = {};
|
|
28
|
+
__export(seed_default_role_permissions_exports, {
|
|
29
|
+
default: () => SeedDefaultRolePermissions
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(seed_default_role_permissions_exports);
|
|
32
|
+
var import_server = require("@nocobase/server");
|
|
33
|
+
class SeedDefaultRolePermissions extends import_server.Migration {
|
|
34
|
+
// afterSync: aiApiRolePermissions must be registered and synced before we write to it.
|
|
35
|
+
on = "afterSync";
|
|
36
|
+
async up() {
|
|
37
|
+
const collection = this.db.getCollection("aiApiRolePermissions");
|
|
38
|
+
if (!collection || !await collection.existsInDb()) return;
|
|
39
|
+
const repository = this.db.getRepository("aiApiRolePermissions");
|
|
40
|
+
const rolesToSeed = [
|
|
41
|
+
{ roleName: "root", enabled: true, allowAllEmployees: true, allowedEmployees: [] },
|
|
42
|
+
{ roleName: "admin", enabled: true, allowAllEmployees: true, allowedEmployees: [] }
|
|
43
|
+
];
|
|
44
|
+
for (const values of rolesToSeed) {
|
|
45
|
+
const existing = await repository.findOne({ filter: { roleName: values.roleName } });
|
|
46
|
+
if (existing) continue;
|
|
47
|
+
await repository.create({ values });
|
|
48
|
+
this.app.logger.info(
|
|
49
|
+
`[ai-api] Seeded aiApiRolePermissions for role ${values.roleName} (the built-in bypass was removed).`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async down() {
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
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 seed_sample_response_records_exports = {};
|
|
28
|
+
__export(seed_sample_response_records_exports, {
|
|
29
|
+
default: () => SeedSampleResponseRecord
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(seed_sample_response_records_exports);
|
|
32
|
+
var import_server = require("@nocobase/server");
|
|
33
|
+
const SAMPLE_IDS = ["resp_sample_welcome", "resp_sample_followup"];
|
|
34
|
+
class SeedSampleResponseRecord extends import_server.Migration {
|
|
35
|
+
on = "afterSync";
|
|
36
|
+
async up() {
|
|
37
|
+
const collection = this.db.getCollection("aiApiResponseRecords");
|
|
38
|
+
if (!collection || !await collection.existsInDb()) return;
|
|
39
|
+
if (process.env.NODE_ENV === "production" && process.env.AI_API_SEED_SAMPLE_RESPONSES !== "true") {
|
|
40
|
+
this.app.logger.info(
|
|
41
|
+
"[ai-api] Skipped sample response records in production (set AI_API_SEED_SAMPLE_RESPONSES=true to enable)."
|
|
42
|
+
);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const rootUser = await this.db.getRepository("users").findOne({ filter: { "roles.name": "root" } });
|
|
46
|
+
if (!rootUser) {
|
|
47
|
+
this.app.logger.warn("[ai-api] Skipped sample response records because no root user exists.");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const repository = this.db.getRepository("aiApiResponseRecords");
|
|
51
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
52
|
+
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3);
|
|
53
|
+
const firstOutput = {
|
|
54
|
+
id: SAMPLE_IDS[0],
|
|
55
|
+
object: "response",
|
|
56
|
+
created_at: now,
|
|
57
|
+
completed_at: now,
|
|
58
|
+
status: "completed",
|
|
59
|
+
error: null,
|
|
60
|
+
incomplete_details: null,
|
|
61
|
+
instructions: null,
|
|
62
|
+
max_output_tokens: null,
|
|
63
|
+
model: "sample/gpt-4",
|
|
64
|
+
output: [
|
|
65
|
+
{
|
|
66
|
+
id: "msg_sample_welcome",
|
|
67
|
+
type: "message",
|
|
68
|
+
status: "completed",
|
|
69
|
+
role: "assistant",
|
|
70
|
+
content: [
|
|
71
|
+
{
|
|
72
|
+
type: "output_text",
|
|
73
|
+
text: "Hello! Welcome to the NocoBase AI API. How can I help you today?",
|
|
74
|
+
annotations: []
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
output_text: "Hello! Welcome to the NocoBase AI API. How can I help you today?",
|
|
80
|
+
parallel_tool_calls: true,
|
|
81
|
+
previous_response_id: null,
|
|
82
|
+
reasoning: null,
|
|
83
|
+
service_tier: "default",
|
|
84
|
+
temperature: null,
|
|
85
|
+
text: null,
|
|
86
|
+
tool_choice: "auto",
|
|
87
|
+
tools: [],
|
|
88
|
+
top_p: null,
|
|
89
|
+
truncation: "disabled",
|
|
90
|
+
usage: {
|
|
91
|
+
input_tokens: 15,
|
|
92
|
+
input_tokens_details: { cached_tokens: 0 },
|
|
93
|
+
output_tokens: 20,
|
|
94
|
+
output_tokens_details: { reasoning_tokens: 0 },
|
|
95
|
+
total_tokens: 35
|
|
96
|
+
},
|
|
97
|
+
metadata: { sample: "true", purpose: "documentation" }
|
|
98
|
+
};
|
|
99
|
+
const firstValues = {
|
|
100
|
+
responseId: SAMPLE_IDS[0],
|
|
101
|
+
userId: rootUser.get("id"),
|
|
102
|
+
model: firstOutput.model,
|
|
103
|
+
input: "Hello! I am testing the Responses API.",
|
|
104
|
+
output: firstOutput,
|
|
105
|
+
previousResponseId: null,
|
|
106
|
+
metadata: firstOutput.metadata,
|
|
107
|
+
expiresAt
|
|
108
|
+
};
|
|
109
|
+
const secondOutput = {
|
|
110
|
+
...firstOutput,
|
|
111
|
+
id: SAMPLE_IDS[1],
|
|
112
|
+
created_at: now + 60,
|
|
113
|
+
completed_at: now + 60,
|
|
114
|
+
output: [
|
|
115
|
+
{
|
|
116
|
+
id: "msg_sample_followup",
|
|
117
|
+
type: "message",
|
|
118
|
+
status: "completed",
|
|
119
|
+
role: "assistant",
|
|
120
|
+
content: [
|
|
121
|
+
{
|
|
122
|
+
type: "output_text",
|
|
123
|
+
text: "I can help with questions, code, data analysis, and more.",
|
|
124
|
+
annotations: []
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
],
|
|
129
|
+
output_text: "I can help with questions, code, data analysis, and more.",
|
|
130
|
+
previous_response_id: SAMPLE_IDS[0],
|
|
131
|
+
usage: {
|
|
132
|
+
input_tokens: 25,
|
|
133
|
+
input_tokens_details: { cached_tokens: 0 },
|
|
134
|
+
output_tokens: 15,
|
|
135
|
+
output_tokens_details: { reasoning_tokens: 0 },
|
|
136
|
+
total_tokens: 40
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
const secondValues = {
|
|
140
|
+
responseId: SAMPLE_IDS[1],
|
|
141
|
+
userId: rootUser.get("id"),
|
|
142
|
+
model: secondOutput.model,
|
|
143
|
+
input: "What can you help me with?",
|
|
144
|
+
output: secondOutput,
|
|
145
|
+
previousResponseId: SAMPLE_IDS[0],
|
|
146
|
+
metadata: secondOutput.metadata,
|
|
147
|
+
expiresAt
|
|
148
|
+
};
|
|
149
|
+
const candidates = [firstValues, secondValues];
|
|
150
|
+
await this.db.sequelize.transaction(async (transaction) => {
|
|
151
|
+
for (const values of candidates) {
|
|
152
|
+
const exists = await repository.findOne({ filter: { responseId: values.responseId }, transaction });
|
|
153
|
+
if (!exists) {
|
|
154
|
+
await repository.create({ values, transaction });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
this.app.logger.info("[ai-api] Seeded sample Responses API records for the root user.");
|
|
159
|
+
}
|
|
160
|
+
async down() {
|
|
161
|
+
const repository = this.db.getRepository("aiApiResponseRecords");
|
|
162
|
+
const records = await repository.find({ filter: { responseId: { $in: SAMPLE_IDS } } });
|
|
163
|
+
for (const record of records) {
|
|
164
|
+
const metadata = record.get("metadata");
|
|
165
|
+
if (metadata && typeof metadata === "object" && metadata.sample === "true") {
|
|
166
|
+
await repository.destroy({ filterByTk: record.get("id") });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
package/dist/server/plugin.js
CHANGED
|
@@ -44,10 +44,11 @@ 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 import_ai_api_usage_groups = __toESM(require("./resource/ai-api-usage-groups"));
|
|
48
47
|
var import_rate_limiter = require("./utils/rate-limiter");
|
|
49
48
|
var import_role_permission = require("./middleware/role-permission");
|
|
49
|
+
var import_response_record_resource = require("./middleware/response-record-resource");
|
|
50
50
|
var import_user_permissions = require("./utils/user-permissions");
|
|
51
|
+
var import_response_store = require("./utils/response-store");
|
|
51
52
|
var import_validation = require("./validation");
|
|
52
53
|
var import_constants = require("../constants");
|
|
53
54
|
var import_file_processor = require("./services/file-processor");
|
|
@@ -68,6 +69,7 @@ class PluginAiApiServer extends import_server.Plugin {
|
|
|
68
69
|
*/
|
|
69
70
|
fileProcessorService = new import_file_processor.FileProcessorService();
|
|
70
71
|
gcInterval = null;
|
|
72
|
+
responseCleanupInterval = null;
|
|
71
73
|
async afterAdd() {
|
|
72
74
|
}
|
|
73
75
|
async beforeLoad() {
|
|
@@ -77,6 +79,9 @@ class PluginAiApiServer extends import_server.Plugin {
|
|
|
77
79
|
this.app.db.on("aiApiModelMetadata.beforeSave", (model) => {
|
|
78
80
|
(0, import_validation.validateModelMetadata)(model);
|
|
79
81
|
});
|
|
82
|
+
this.app.db.on("aiApiVirtualModels.beforeSave", (model) => {
|
|
83
|
+
(0, import_validation.validateVirtualModel)(model);
|
|
84
|
+
});
|
|
80
85
|
this.app.db.on("aiApiUsageGroups.beforeSave", async (model, options) => {
|
|
81
86
|
(0, import_validation.validateQuotaPolicy)(model);
|
|
82
87
|
if (model.get("isDefault")) {
|
|
@@ -94,6 +99,16 @@ class PluginAiApiServer extends import_server.Plugin {
|
|
|
94
99
|
this.app.db.on("aiApiGroupMembers.beforeSave", async (model, options) => {
|
|
95
100
|
const userId = model.get("userId");
|
|
96
101
|
if (!userId) return;
|
|
102
|
+
const targetGroupId = model.get("groupId");
|
|
103
|
+
if (targetGroupId) {
|
|
104
|
+
const targetGroup = await this.db.getRepository("aiApiUsageGroups").findOne({
|
|
105
|
+
filterByTk: targetGroupId,
|
|
106
|
+
transaction: options == null ? void 0 : options.transaction
|
|
107
|
+
});
|
|
108
|
+
if (targetGroup == null ? void 0 : targetGroup.get("isDefault")) {
|
|
109
|
+
throw new Error("Cannot add members to the default group: users without another group use it automatically.");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
97
112
|
const existing = await this.db.getRepository("aiApiGroupMembers").findOne({
|
|
98
113
|
filter: { userId },
|
|
99
114
|
transaction: options == null ? void 0 : options.transaction
|
|
@@ -110,18 +125,9 @@ class PluginAiApiServer extends import_server.Plugin {
|
|
|
110
125
|
filter: { groupId: model.get("id") },
|
|
111
126
|
transaction: options == null ? void 0 : options.transaction
|
|
112
127
|
});
|
|
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
128
|
for (const member of members) {
|
|
122
|
-
await this.db.getRepository("aiApiGroupMembers").
|
|
129
|
+
await this.db.getRepository("aiApiGroupMembers").destroy({
|
|
123
130
|
filterByTk: member.get("id"),
|
|
124
|
-
values: { groupId: defaultGroup.get("id") },
|
|
125
131
|
transaction: options == null ? void 0 : options.transaction
|
|
126
132
|
});
|
|
127
133
|
}
|
|
@@ -141,12 +147,16 @@ class PluginAiApiServer extends import_server.Plugin {
|
|
|
141
147
|
this.app.use((0, import_router.createAiLlmRouter)(this), { after: "idp-oauth-resource-auth", before: "resourcer" });
|
|
142
148
|
this.app.resourceManager.define(import_ai_api_config.default);
|
|
143
149
|
this.app.resourceManager.define(import_ai_api_usage_monitor.default);
|
|
144
|
-
this.app.resourceManager.
|
|
145
|
-
|
|
146
|
-
|
|
150
|
+
this.app.resourceManager.use((0, import_response_record_resource.blockResponseRecordResource)(), {
|
|
151
|
+
tag: "aiApiResponseRecordsPrivate",
|
|
152
|
+
after: "auth",
|
|
153
|
+
before: "acl"
|
|
147
154
|
});
|
|
148
|
-
this.app.db.on("aiApiRolePermissions.
|
|
149
|
-
|
|
155
|
+
this.app.db.on("aiApiRolePermissions.afterSave", (model, options) => {
|
|
156
|
+
this.invalidateRolePermissionSync(model.get("roleName"), options == null ? void 0 : options.transaction);
|
|
157
|
+
});
|
|
158
|
+
this.app.db.on("aiApiRolePermissions.afterDestroy", (model, options) => {
|
|
159
|
+
this.invalidateRolePermissionSync(model.get("roleName"), options == null ? void 0 : options.transaction);
|
|
150
160
|
});
|
|
151
161
|
this.app.db.on("aiApiUsageGroups.afterSave", (model, options) => {
|
|
152
162
|
this.invalidateGroupAccess(model.get("id"), options == null ? void 0 : options.transaction);
|
|
@@ -161,6 +171,7 @@ class PluginAiApiServer extends import_server.Plugin {
|
|
|
161
171
|
"aiApiRolePermissions:*",
|
|
162
172
|
"aiApiModelPrices:*",
|
|
163
173
|
"aiApiModelMetadata:*",
|
|
174
|
+
"aiApiVirtualModels:*",
|
|
164
175
|
"aiApiUsageGroups:*",
|
|
165
176
|
"aiApiGroupMembers:*",
|
|
166
177
|
"aiApiGroupQuotaBuckets:list",
|
|
@@ -172,6 +183,21 @@ class PluginAiApiServer extends import_server.Plugin {
|
|
|
172
183
|
});
|
|
173
184
|
this.gcInterval = setInterval(() => this.rateLimiter.gc(), 5 * 60 * 1e3);
|
|
174
185
|
this.gcInterval.unref();
|
|
186
|
+
this.cleanupResponseRecords();
|
|
187
|
+
this.responseCleanupInterval = setInterval(() => this.cleanupResponseRecords(), 24 * 60 * 60 * 1e3);
|
|
188
|
+
this.responseCleanupInterval.unref();
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Drop a role's cached permission record on every node.
|
|
192
|
+
*
|
|
193
|
+
* Mirrors invalidateGroupAccess: the local call is not redundant because
|
|
194
|
+
* syncMessageManager hardcodes skipSelf, so the publishing node never receives
|
|
195
|
+
* its own message. Passing the transaction defers the broadcast until the
|
|
196
|
+
* write commits, so other nodes cannot re-read the old row and re-cache it.
|
|
197
|
+
*/
|
|
198
|
+
invalidateRolePermissionSync(roleName, transaction) {
|
|
199
|
+
(0, import_role_permission.invalidateRolePermissionCache)(roleName);
|
|
200
|
+
this.sendSyncMessage({ type: "invalidateRolePermission", roleName }, { transaction });
|
|
175
201
|
}
|
|
176
202
|
/**
|
|
177
203
|
* Drop a group's cached model-access scope on every node.
|
|
@@ -190,6 +216,8 @@ class PluginAiApiServer extends import_server.Plugin {
|
|
|
190
216
|
async handleSyncMessage(message) {
|
|
191
217
|
if ((message == null ? void 0 : message.type) === "invalidateGroupAccess") {
|
|
192
218
|
(0, import_user_permissions.invalidateGroupAccessCache)(message.groupId);
|
|
219
|
+
} else if ((message == null ? void 0 : message.type) === "invalidateRolePermission") {
|
|
220
|
+
(0, import_role_permission.invalidateRolePermissionCache)(message.roleName);
|
|
193
221
|
}
|
|
194
222
|
}
|
|
195
223
|
async install() {
|
|
@@ -205,6 +233,16 @@ class PluginAiApiServer extends import_server.Plugin {
|
|
|
205
233
|
}
|
|
206
234
|
});
|
|
207
235
|
}
|
|
236
|
+
for (const roleName of ["root", "admin"]) {
|
|
237
|
+
const perm = await this.db.getRepository("aiApiRolePermissions").findOne({
|
|
238
|
+
filter: { roleName }
|
|
239
|
+
});
|
|
240
|
+
if (!perm) {
|
|
241
|
+
await this.db.getRepository("aiApiRolePermissions").create({
|
|
242
|
+
values: { roleName, enabled: true, allowAllEmployees: true, allowedEmployees: [] }
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
208
246
|
const defaultGroup = await this.db.getRepository("aiApiUsageGroups").findOne({
|
|
209
247
|
filter: { isDefault: true }
|
|
210
248
|
});
|
|
@@ -232,6 +270,14 @@ class PluginAiApiServer extends import_server.Plugin {
|
|
|
232
270
|
});
|
|
233
271
|
}
|
|
234
272
|
}
|
|
273
|
+
async cleanupResponseRecords() {
|
|
274
|
+
try {
|
|
275
|
+
const deleted = await (0, import_response_store.cleanupExpiredResponseRecords)({ db: this.db });
|
|
276
|
+
if (deleted > 0) this.app.logger.info(`[ai-api] Cleaned up ${deleted} expired response records`);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
this.app.logger.warn("[ai-api] Failed to clean up expired response records:", error);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
235
281
|
async afterEnable() {
|
|
236
282
|
}
|
|
237
283
|
async afterDisable() {
|
|
@@ -241,6 +287,10 @@ class PluginAiApiServer extends import_server.Plugin {
|
|
|
241
287
|
clearInterval(this.gcInterval);
|
|
242
288
|
this.gcInterval = null;
|
|
243
289
|
}
|
|
290
|
+
if (this.responseCleanupInterval) {
|
|
291
|
+
clearInterval(this.responseCleanupInterval);
|
|
292
|
+
this.responseCleanupInterval = null;
|
|
293
|
+
}
|
|
244
294
|
this.rateLimiter.clear();
|
|
245
295
|
}
|
|
246
296
|
}
|