plugin-ai-api 1.0.15 → 1.0.21
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 +10 -0
- package/dist/client/302.fc3a3491b4ec2dfd.js +10 -0
- package/dist/client/562.17a0a299d2e5152c.js +10 -0
- package/dist/client/757.a01403fb7a1bea01.js +10 -0
- package/dist/client/902.92e1daaf1ab16ebf.js +10 -0
- package/dist/client/97.72979a11a067a7c9.js +10 -0
- package/dist/client/index.js +1 -1
- package/dist/client-v2/302.d27fe4ea9b0b3bf5.js +10 -0
- package/dist/client-v2/562.fb2948ee6402de95.js +10 -0
- package/dist/client-v2/757.a117ce1cf7119cea.js +10 -0
- package/dist/client-v2/902.9054d990ddc223ac.js +10 -0
- package/dist/client-v2/952.94100128b7757f56.js +10 -0
- package/dist/client-v2/97.29c663318eebbd57.js +10 -0
- package/dist/client-v2/index.js +1 -1
- package/dist/constants.js +36 -0
- package/dist/externalVersion.js +9 -10
- package/dist/locale/en-US.json +105 -10
- package/dist/locale/vi-VN.json +105 -0
- package/dist/locale/zh-CN.json +105 -10
- package/dist/server/billing.js +331 -0
- package/dist/server/collections/ai-api-config.js +18 -0
- package/dist/server/collections/ai-api-model-metadata.js +83 -0
- package/dist/server/collections/ai-api-model-prices.js +55 -0
- package/dist/server/collections/ai-api-usage-records.js +9 -0
- package/dist/server/collections/ai-api-user-quota-buckets.js +54 -0
- package/dist/server/collections/ai-api-user-quota-policies.js +62 -0
- package/dist/server/plugin.js +36 -3
- package/dist/server/resource/ai-api-config.js +25 -0
- package/dist/server/resource/ai-api-usage-monitor.js +86 -0
- package/dist/server/routes/agent-completions.js +62 -51
- package/dist/server/routes/auth.js +11 -1
- package/dist/server/routes/chat-completions.js +157 -6
- package/dist/server/routes/completions.js +20 -3
- package/dist/server/routes/models.js +78 -20
- package/dist/server/routes/router.js +108 -23
- package/dist/server/usage.js +19 -2
- package/dist/server/utils/app-observability.js +110 -0
- package/dist/server/utils/streaming.js +15 -1
- package/dist/server/validation.js +120 -0
- package/dist/swagger.js +32 -1
- package/package.json +1 -1
- package/src/client/components/AiApiRolePermissions.tsx +11 -169
- package/src/client/locale.ts +11 -21
- package/src/client/plugin.tsx +82 -48
- package/src/client-v2/__tests__/settings-registration.test.tsx +58 -0
- package/src/client-v2/components/AiApiRolePermissions.tsx +173 -0
- package/src/client-v2/locale.ts +21 -0
- package/src/client-v2/pages/GeneralPage.tsx +183 -0
- package/src/client-v2/pages/ModelMetadataPage.tsx +280 -0
- package/src/client-v2/pages/ModelPricingPage.tsx +285 -0
- package/src/client-v2/pages/RolePermissionsTab.tsx +14 -0
- package/src/client-v2/pages/UsagePage.tsx +248 -0
- package/src/client-v2/pages/UserQuotasPage.tsx +258 -0
- package/src/client-v2/pages/api.ts +16 -0
- package/src/client-v2/plugin.tsx +62 -4
- package/src/constants.ts +21 -0
- package/src/locale/en-US.json +105 -10
- package/src/locale/vi-VN.json +105 -0
- package/src/locale/zh-CN.json +105 -10
- package/src/server/__tests__/app-observability.test.ts +98 -0
- package/src/server/__tests__/billing-quota.test.ts +134 -0
- package/src/server/__tests__/billing.test.ts +33 -0
- package/src/server/__tests__/models.test.ts +74 -0
- package/src/server/__tests__/request-body.test.ts +310 -0
- package/src/server/__tests__/streaming-observability.test.ts +51 -0
- package/src/server/__tests__/usage-monitor.test.ts +63 -0
- package/src/server/__tests__/usage-route.test.ts +4 -0
- package/src/server/billing.ts +387 -0
- package/src/server/collections/ai-api-config.ts +69 -51
- package/src/server/collections/ai-api-model-metadata.ts +72 -0
- package/src/server/collections/ai-api-model-prices.ts +25 -0
- package/src/server/collections/ai-api-usage-records.ts +9 -0
- package/src/server/collections/ai-api-user-quota-buckets.ts +24 -0
- package/src/server/collections/ai-api-user-quota-policies.ts +32 -0
- package/src/server/plugin.ts +47 -5
- package/src/server/resource/ai-api-config.ts +105 -74
- package/src/server/resource/ai-api-usage-monitor.ts +74 -0
- package/src/server/routes/agent-completions.ts +77 -62
- package/src/server/routes/auth.ts +14 -1
- package/src/server/routes/chat-completions.ts +275 -6
- package/src/server/routes/completions.ts +27 -4
- package/src/server/routes/models.ts +290 -195
- package/src/server/routes/router.ts +152 -27
- package/src/server/usage.ts +19 -1
- package/src/server/utils/app-observability.ts +105 -0
- package/src/server/utils/streaming.ts +13 -1
- package/src/server/validation.ts +89 -0
- package/src/swagger.ts +38 -1
- package/dist/client/778.5c452944cb747975.js +0 -10
- package/dist/client/950.83390c5f1d5a97fb.js +0 -10
- package/dist/client-v2/950.42b30b5cc9e32b8f.js +0 -10
- package/src/client/AiApiConfigPage.tsx +0 -309
|
@@ -0,0 +1,120 @@
|
|
|
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 __create = Object.create;
|
|
11
|
+
var __defProp = Object.defineProperty;
|
|
12
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
13
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
14
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
15
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
16
|
+
var __export = (target, all) => {
|
|
17
|
+
for (var name in all)
|
|
18
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
19
|
+
};
|
|
20
|
+
var __copyProps = (to, from, except, desc) => {
|
|
21
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
22
|
+
for (let key of __getOwnPropNames(from))
|
|
23
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
24
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
25
|
+
}
|
|
26
|
+
return to;
|
|
27
|
+
};
|
|
28
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
29
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
30
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
31
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
32
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
33
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
34
|
+
mod
|
|
35
|
+
));
|
|
36
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
37
|
+
var validation_exports = {};
|
|
38
|
+
__export(validation_exports, {
|
|
39
|
+
validateModelMetadata: () => validateModelMetadata,
|
|
40
|
+
validateModelPrice: () => validateModelPrice,
|
|
41
|
+
validateQuotaPolicy: () => validateQuotaPolicy
|
|
42
|
+
});
|
|
43
|
+
module.exports = __toCommonJS(validation_exports);
|
|
44
|
+
var import_dayjs = __toESM(require("dayjs"));
|
|
45
|
+
function requireNonNegativeDecimal(value, field) {
|
|
46
|
+
const normalized = String(value ?? "").trim();
|
|
47
|
+
if (!/^\d+(?:\.\d+)?$/.test(normalized)) {
|
|
48
|
+
throw new Error(`${field} must be a non-negative decimal.`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function requireNonNegativeIntegerOrNull(value, field) {
|
|
52
|
+
if (value === null || value === void 0 || value === "") return;
|
|
53
|
+
const parsed = Number(value);
|
|
54
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) throw new Error(`${field} must be a non-negative integer.`);
|
|
55
|
+
}
|
|
56
|
+
async function validateModelPrice(db, model) {
|
|
57
|
+
requireNonNegativeDecimal(model.get("inputPricePerMillionTokens"), "inputPricePerMillionTokens");
|
|
58
|
+
requireNonNegativeDecimal(model.get("outputPricePerMillionTokens"), "outputPricePerMillionTokens");
|
|
59
|
+
requireNonNegativeDecimal(model.get("fixedCostPerRequest") ?? 0, "fixedCostPerRequest");
|
|
60
|
+
const effectiveFrom = new Date(String(model.get("effectiveFrom")));
|
|
61
|
+
const effectiveToValue = model.get("effectiveTo");
|
|
62
|
+
const effectiveTo = effectiveToValue ? new Date(String(effectiveToValue)) : void 0;
|
|
63
|
+
if (Number.isNaN(effectiveFrom.getTime())) throw new Error("effectiveFrom must be a valid date.");
|
|
64
|
+
if (effectiveTo && (Number.isNaN(effectiveTo.getTime()) || effectiveTo <= effectiveFrom)) {
|
|
65
|
+
throw new Error("effectiveTo must be later than effectiveFrom.");
|
|
66
|
+
}
|
|
67
|
+
if (model.get("enabled") === false) return;
|
|
68
|
+
const overlapFilter = {
|
|
69
|
+
llmService: model.get("llmService"),
|
|
70
|
+
model: model.get("model"),
|
|
71
|
+
enabled: true,
|
|
72
|
+
effectiveFrom: { $lt: effectiveTo ?? /* @__PURE__ */ new Date("9999-12-31T23:59:59.999Z") },
|
|
73
|
+
$or: [{ effectiveTo: null }, { effectiveTo: { $gt: effectiveFrom } }]
|
|
74
|
+
};
|
|
75
|
+
if (model.get("id")) overlapFilter.id = { $ne: model.get("id") };
|
|
76
|
+
const overlap = await db.getRepository("aiApiModelPrices").findOne({
|
|
77
|
+
filter: overlapFilter
|
|
78
|
+
});
|
|
79
|
+
if (overlap) throw new Error("An enabled price already overlaps this effective period.");
|
|
80
|
+
}
|
|
81
|
+
function requirePositiveIntegerOrNull(value, field) {
|
|
82
|
+
if (value === null || value === void 0 || value === "") return;
|
|
83
|
+
const parsed = Number(value);
|
|
84
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) throw new Error(`${field} must be a positive integer.`);
|
|
85
|
+
}
|
|
86
|
+
function validateModelMetadata(model) {
|
|
87
|
+
if (!String(model.get("llmService") ?? "").trim()) throw new Error("llmService is required.");
|
|
88
|
+
if (!String(model.get("model") ?? "").trim()) throw new Error("model is required.");
|
|
89
|
+
requirePositiveIntegerOrNull(model.get("contextWindow"), "contextWindow");
|
|
90
|
+
requirePositiveIntegerOrNull(model.get("maxCompletionTokens"), "maxCompletionTokens");
|
|
91
|
+
const contextWindow = model.get("contextWindow");
|
|
92
|
+
const maxCompletionTokens = model.get("maxCompletionTokens");
|
|
93
|
+
if (contextWindow !== null && contextWindow !== void 0 && contextWindow !== "" && maxCompletionTokens !== null && maxCompletionTokens !== void 0 && maxCompletionTokens !== "" && Number(maxCompletionTokens) > Number(contextWindow)) {
|
|
94
|
+
throw new Error("maxCompletionTokens cannot exceed contextWindow.");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function validateQuotaPolicy(model) {
|
|
98
|
+
if (!["daily", "monthly"].includes(String(model.get("periodType")))) {
|
|
99
|
+
throw new Error("periodType must be daily or monthly.");
|
|
100
|
+
}
|
|
101
|
+
if (!["allow", "use_reserved"].includes(String(model.get("missingUsageBehavior")))) {
|
|
102
|
+
throw new Error("missingUsageBehavior must be allow or use_reserved.");
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
(0, import_dayjs.default)().tz(String(model.get("timezone") || "UTC"));
|
|
106
|
+
} catch {
|
|
107
|
+
throw new Error("timezone must be a valid IANA timezone.");
|
|
108
|
+
}
|
|
109
|
+
requireNonNegativeIntegerOrNull(model.get("requestLimit"), "requestLimit");
|
|
110
|
+
requireNonNegativeIntegerOrNull(model.get("totalTokenLimit"), "totalTokenLimit");
|
|
111
|
+
if (model.get("costLimit") !== null && model.get("costLimit") !== void 0) {
|
|
112
|
+
requireNonNegativeDecimal(model.get("costLimit"), "costLimit");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
116
|
+
0 && (module.exports = {
|
|
117
|
+
validateModelMetadata,
|
|
118
|
+
validateModelPrice,
|
|
119
|
+
validateQuotaPolicy
|
|
120
|
+
});
|
package/dist/swagger.js
CHANGED
|
@@ -277,6 +277,13 @@ var swagger_default = {
|
|
|
277
277
|
rateLimitPerMinute: {
|
|
278
278
|
type: "integer",
|
|
279
279
|
description: "Max requests per minute per user (0 = unlimited)"
|
|
280
|
+
},
|
|
281
|
+
maxRequestBodyMb: {
|
|
282
|
+
type: "integer",
|
|
283
|
+
minimum: 1,
|
|
284
|
+
maximum: 100,
|
|
285
|
+
default: 10,
|
|
286
|
+
description: "Max request body size in megabytes. Requests above this return 413. The gateway buffers each body in memory, so values above 100 are rejected."
|
|
280
287
|
}
|
|
281
288
|
}
|
|
282
289
|
},
|
|
@@ -289,11 +296,35 @@ var swagger_default = {
|
|
|
289
296
|
owned_by: { type: "string" }
|
|
290
297
|
}
|
|
291
298
|
},
|
|
299
|
+
ContentBlock: {
|
|
300
|
+
type: "object",
|
|
301
|
+
description: "A multimodal content block. Only text and image_url blocks are forwarded to the provider; any other type is rejected with 400 unsupported_content_block.",
|
|
302
|
+
properties: {
|
|
303
|
+
type: { type: "string", enum: ["text", "image_url"] },
|
|
304
|
+
text: { type: "string" },
|
|
305
|
+
image_url: {
|
|
306
|
+
type: "object",
|
|
307
|
+
properties: {
|
|
308
|
+
url: {
|
|
309
|
+
type: "string",
|
|
310
|
+
description: "An https URL or a base64 data URL, e.g. data:image/png;base64,iVBORw0KGgo...",
|
|
311
|
+
example: "data:image/png;base64,iVBORw0KGgo..."
|
|
312
|
+
},
|
|
313
|
+
detail: { type: "string", enum: ["auto", "low", "high"] }
|
|
314
|
+
},
|
|
315
|
+
required: ["url"]
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
required: ["type"]
|
|
319
|
+
},
|
|
292
320
|
ChatMessage: {
|
|
293
321
|
type: "object",
|
|
294
322
|
properties: {
|
|
295
323
|
role: { type: "string", enum: ["system", "user", "assistant", "tool"] },
|
|
296
|
-
content: {
|
|
324
|
+
content: {
|
|
325
|
+
description: 'Plain text, or an array of content blocks for multimodal requests. Inline base64 images inflate the payload by about 33%; see "Max request body size" in the gateway settings.',
|
|
326
|
+
oneOf: [{ type: "string" }, { type: "array", items: { $ref: "#/components/schemas/ContentBlock" } }]
|
|
327
|
+
},
|
|
297
328
|
name: { type: "string" },
|
|
298
329
|
tool_call_id: { type: "string" },
|
|
299
330
|
tool_calls: { type: "array", items: { $ref: "#/components/schemas/ToolCall" } }
|
package/package.json
CHANGED
|
@@ -1,169 +1,11 @@
|
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
import { Card, Switch, Select, Space, Typography, Spin, Divider } from 'antd';
|
|
13
|
-
|
|
14
|
-
const { Text } = Typography;
|
|
15
|
-
|
|
16
|
-
interface AiEmployee {
|
|
17
|
-
username: string;
|
|
18
|
-
nickname: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface RolePermRecord {
|
|
22
|
-
id?: number;
|
|
23
|
-
roleName: string;
|
|
24
|
-
enabled: boolean;
|
|
25
|
-
allowAllEmployees: boolean;
|
|
26
|
-
allowedEmployees: string[];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Permission tab shown in Settings → Users & Permissions → [Role] → "AI API" tab.
|
|
31
|
-
* Lets admins control whether a role can use the AI API and which AI Employees it may access.
|
|
32
|
-
*/
|
|
33
|
-
export function AiApiRolePermissions({ role }: { role: any }) {
|
|
34
|
-
const api = useApp().apiClient;
|
|
35
|
-
const [loading, setLoading] = useState(true);
|
|
36
|
-
const [saving, setSaving] = useState(false);
|
|
37
|
-
const [employees, setEmployees] = useState<AiEmployee[]>([]);
|
|
38
|
-
const [record, setRecord] = useState<RolePermRecord | null>(null);
|
|
39
|
-
|
|
40
|
-
const roleName = role?.name;
|
|
41
|
-
|
|
42
|
-
useEffect(() => {
|
|
43
|
-
if (!roleName) return;
|
|
44
|
-
loadData();
|
|
45
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
46
|
-
}, [roleName]);
|
|
47
|
-
|
|
48
|
-
const loadData = async () => {
|
|
49
|
-
setLoading(true);
|
|
50
|
-
try {
|
|
51
|
-
const [permRes, empRes] = await Promise.all([
|
|
52
|
-
api.request({
|
|
53
|
-
url: 'aiApiRolePermissions',
|
|
54
|
-
params: { filter: { roleName }, paginate: false },
|
|
55
|
-
}),
|
|
56
|
-
api.request({ url: 'aiEmployees:list', params: { paginate: false } }),
|
|
57
|
-
]);
|
|
58
|
-
|
|
59
|
-
const existing = permRes?.data?.data?.[0];
|
|
60
|
-
setRecord(
|
|
61
|
-
existing
|
|
62
|
-
? {
|
|
63
|
-
id: existing.id,
|
|
64
|
-
roleName: existing.roleName,
|
|
65
|
-
enabled: !!existing.enabled,
|
|
66
|
-
allowAllEmployees: existing.allowAllEmployees !== false,
|
|
67
|
-
allowedEmployees: existing.allowedEmployees || [],
|
|
68
|
-
}
|
|
69
|
-
: {
|
|
70
|
-
roleName,
|
|
71
|
-
enabled: false,
|
|
72
|
-
allowAllEmployees: true,
|
|
73
|
-
allowedEmployees: [],
|
|
74
|
-
},
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
setEmployees(
|
|
78
|
-
(empRes?.data?.data || []).map((e: any) => ({
|
|
79
|
-
username: e.username,
|
|
80
|
-
nickname: e.nickname || e.username,
|
|
81
|
-
})),
|
|
82
|
-
);
|
|
83
|
-
} catch (err) {
|
|
84
|
-
console.error('Failed to load AI API role permissions:', err);
|
|
85
|
-
} finally {
|
|
86
|
-
setLoading(false);
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const save = async (patch: Partial<RolePermRecord>) => {
|
|
91
|
-
if (!record) return;
|
|
92
|
-
const next = { ...record, ...patch };
|
|
93
|
-
setRecord(next);
|
|
94
|
-
setSaving(true);
|
|
95
|
-
try {
|
|
96
|
-
if (next.id) {
|
|
97
|
-
await api.request({
|
|
98
|
-
url: `aiApiRolePermissions/${next.id}`,
|
|
99
|
-
method: 'PUT',
|
|
100
|
-
data: next,
|
|
101
|
-
});
|
|
102
|
-
} else {
|
|
103
|
-
const res = await api.request({
|
|
104
|
-
url: 'aiApiRolePermissions',
|
|
105
|
-
method: 'POST',
|
|
106
|
-
data: next,
|
|
107
|
-
});
|
|
108
|
-
const created = res?.data?.data;
|
|
109
|
-
if (created?.id) {
|
|
110
|
-
setRecord({ ...next, id: created.id });
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
} catch (err) {
|
|
114
|
-
console.error('Failed to save AI API role permissions:', err);
|
|
115
|
-
// Revert on error
|
|
116
|
-
setRecord(record);
|
|
117
|
-
} finally {
|
|
118
|
-
setSaving(false);
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
if (loading) return <Spin />;
|
|
123
|
-
|
|
124
|
-
return (
|
|
125
|
-
<Card bordered={false}>
|
|
126
|
-
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
|
127
|
-
<Space>
|
|
128
|
-
<Switch checked={!!record?.enabled} loading={saving} onChange={(checked) => save({ enabled: checked })} />
|
|
129
|
-
<Text strong>Allow this role to use the AI API</Text>
|
|
130
|
-
</Space>
|
|
131
|
-
|
|
132
|
-
{record?.enabled && (
|
|
133
|
-
<>
|
|
134
|
-
<Divider style={{ margin: '8px 0' }} />
|
|
135
|
-
<Space>
|
|
136
|
-
<Switch
|
|
137
|
-
checked={!!record?.allowAllEmployees}
|
|
138
|
-
loading={saving}
|
|
139
|
-
onChange={(checked) => save({ allowAllEmployees: checked })}
|
|
140
|
-
/>
|
|
141
|
-
<Text>Allow all AI Employees</Text>
|
|
142
|
-
</Space>
|
|
143
|
-
|
|
144
|
-
{!record?.allowAllEmployees && (
|
|
145
|
-
<div>
|
|
146
|
-
<Text type="secondary" style={{ display: 'block', marginBottom: 8 }}>
|
|
147
|
-
Select which AI Employees this role may use:
|
|
148
|
-
</Text>
|
|
149
|
-
<Select
|
|
150
|
-
mode="multiple"
|
|
151
|
-
allowClear
|
|
152
|
-
style={{ width: '100%', maxWidth: 480 }}
|
|
153
|
-
placeholder="Select allowed AI Employees"
|
|
154
|
-
value={record?.allowedEmployees || []}
|
|
155
|
-
options={employees.map((e) => ({
|
|
156
|
-
label: `${e.nickname} (${e.username})`,
|
|
157
|
-
value: e.username,
|
|
158
|
-
}))}
|
|
159
|
-
onChange={(vals) => save({ allowedEmployees: vals })}
|
|
160
|
-
disabled={saving}
|
|
161
|
-
/>
|
|
162
|
-
</div>
|
|
163
|
-
)}
|
|
164
|
-
</>
|
|
165
|
-
)}
|
|
166
|
-
</Space>
|
|
167
|
-
</Card>
|
|
168
|
-
);
|
|
169
|
-
}
|
|
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
|
+
// Implementation lives in client-v2; imports may only flow v1 -> v2, never back.
|
|
11
|
+
export { AiApiRolePermissions } from '../../client-v2/components/AiApiRolePermissions';
|
package/src/client/locale.ts
CHANGED
|
@@ -1,21 +1,11 @@
|
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
import pkg from './../../package.json';
|
|
13
|
-
|
|
14
|
-
export function useT() {
|
|
15
|
-
const engine = useFlowEngine();
|
|
16
|
-
return (str: string) => engine.context.t(str, { ns: [pkg.name, 'client'] });
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function tExpr(key: string) {
|
|
20
|
-
return _tExpr(key, { ns: [pkg.name, 'client'] });
|
|
21
|
-
}
|
|
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
|
+
// Implementation lives in client-v2; imports may only flow v1 -> v2, never back.
|
|
11
|
+
export { tExpr, useT } from '../client-v2/locale';
|
package/src/client/plugin.tsx
CHANGED
|
@@ -1,48 +1,82 @@
|
|
|
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 { Plugin, lazy } from '@nocobase/client';
|
|
11
|
-
import PluginACLClient from '@nocobase/plugin-acl/client';
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
title: this.t('
|
|
27
|
-
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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 { Plugin, lazy } from '@nocobase/client';
|
|
11
|
+
import PluginACLClient from '@nocobase/plugin-acl/client';
|
|
12
|
+
import { AI_API_ACL_SNIPPET } from '../constants';
|
|
13
|
+
import React from 'react';
|
|
14
|
+
|
|
15
|
+
const AiApiConfigPage = React.lazy(() => import('../client-v2/pages/GeneralPage'));
|
|
16
|
+
const AiApiModelPricingPage = React.lazy(() => import('../client-v2/pages/ModelPricingPage'));
|
|
17
|
+
const AiApiModelMetadataPage = React.lazy(() => import('../client-v2/pages/ModelMetadataPage'));
|
|
18
|
+
const AiApiUserQuotasPage = React.lazy(() => import('../client-v2/pages/UserQuotasPage'));
|
|
19
|
+
const AiApiUsagePage = React.lazy(() => import('../client-v2/pages/UsagePage'));
|
|
20
|
+
const { AiApiRolePermissions } = lazy(() => import('./components/AiApiRolePermissions'), 'AiApiRolePermissions');
|
|
21
|
+
|
|
22
|
+
export class PluginAiApiClient extends Plugin {
|
|
23
|
+
async load() {
|
|
24
|
+
this.app.pluginSettingsManager.add('ai-api', {
|
|
25
|
+
icon: 'ApiOutlined',
|
|
26
|
+
title: this.t('AI API Gateway'),
|
|
27
|
+
aclSnippet: AI_API_ACL_SNIPPET,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
this.app.pluginSettingsManager.add('ai-api.config', {
|
|
31
|
+
title: this.t('Configuration'),
|
|
32
|
+
Component: AiApiConfigPage,
|
|
33
|
+
aclSnippet: AI_API_ACL_SNIPPET,
|
|
34
|
+
sort: 1,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
this.app.pluginSettingsManager.add('ai-api.model-pricing', {
|
|
38
|
+
title: this.t('Model pricing'),
|
|
39
|
+
Component: AiApiModelPricingPage,
|
|
40
|
+
aclSnippet: AI_API_ACL_SNIPPET,
|
|
41
|
+
sort: 2,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
this.app.pluginSettingsManager.add('ai-api.model-metadata', {
|
|
45
|
+
title: this.t('Model metadata'),
|
|
46
|
+
Component: AiApiModelMetadataPage,
|
|
47
|
+
aclSnippet: AI_API_ACL_SNIPPET,
|
|
48
|
+
sort: 3,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
this.app.pluginSettingsManager.add('ai-api.user-quotas', {
|
|
52
|
+
title: this.t('User quotas'),
|
|
53
|
+
Component: AiApiUserQuotasPage,
|
|
54
|
+
aclSnippet: AI_API_ACL_SNIPPET,
|
|
55
|
+
sort: 4,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
this.app.pluginSettingsManager.add('ai-api.usage', {
|
|
59
|
+
title: this.t('Usage'),
|
|
60
|
+
Component: AiApiUsagePage,
|
|
61
|
+
aclSnippet: AI_API_ACL_SNIPPET,
|
|
62
|
+
sort: 5,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Add "AI API" tab in Settings → Users & Permissions → [Role]
|
|
66
|
+
const aclPlugin = this.app.pm.get(PluginACLClient);
|
|
67
|
+
if (aclPlugin?.settingsUI) {
|
|
68
|
+
aclPlugin.settingsUI.addPermissionsTab(({ t, TabLayout, activeRole }) => ({
|
|
69
|
+
key: 'aiApi',
|
|
70
|
+
label: t('AI API', { ns: ['plugin-ai-api', 'client'] }),
|
|
71
|
+
sort: 25,
|
|
72
|
+
children: (
|
|
73
|
+
<TabLayout>
|
|
74
|
+
<AiApiRolePermissions role={activeRole} />
|
|
75
|
+
</TabLayout>
|
|
76
|
+
),
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default PluginAiApiClient;
|
|
@@ -0,0 +1,58 @@
|
|
|
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 { createMockClient } from '@nocobase/client-v2';
|
|
11
|
+
import { AI_API_ACL_SNIPPET } from '../../constants';
|
|
12
|
+
import { PluginAiApiClient } from '../plugin';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Guards the settings registration itself rather than any page's contents.
|
|
16
|
+
*
|
|
17
|
+
* Two regressions are cheap to reintroduce and invisible until a non-admin logs in:
|
|
18
|
+
* omitting `aclSnippet` makes addPageTabItem default to `pm.ai-api.<key>`, a snippet the
|
|
19
|
+
* server never registers, and omitting `sort` silently reorders the tabs alphabetically.
|
|
20
|
+
*/
|
|
21
|
+
describe('AI API v2 settings registration', () => {
|
|
22
|
+
async function loadPlugin() {
|
|
23
|
+
const app = createMockClient();
|
|
24
|
+
await new PluginAiApiClient({}, app).load();
|
|
25
|
+
return app;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
it('registers every page under the one snippet the server exposes', async () => {
|
|
29
|
+
const app = await loadPlugin();
|
|
30
|
+
const menu = app.pluginSettingsManager.get('ai-api', false);
|
|
31
|
+
|
|
32
|
+
expect(menu?.aclSnippet).toBe(AI_API_ACL_SNIPPET);
|
|
33
|
+
for (const child of menu?.children ?? []) {
|
|
34
|
+
expect(app.pluginSettingsManager.getAclSnippet(child.name), child.name).toBe(AI_API_ACL_SNIPPET);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('hides the menu and every tab when the role denies that snippet', async () => {
|
|
39
|
+
const app = await loadPlugin();
|
|
40
|
+
app.pluginSettingsManager.setAclSnippets([`!${AI_API_ACL_SNIPPET}`]);
|
|
41
|
+
|
|
42
|
+
expect(app.pluginSettingsManager.get('ai-api')).toBeNull();
|
|
43
|
+
expect(app.pluginSettingsManager.getList().map((item) => item.name)).not.toContain('ai-api');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('keeps registration order instead of sorting tabs by name', async () => {
|
|
47
|
+
const app = await loadPlugin();
|
|
48
|
+
app.pluginSettingsManager.setAclSnippets([]);
|
|
49
|
+
|
|
50
|
+
expect(app.pluginSettingsManager.get('ai-api')?.children?.map((item) => item.name)).toEqual([
|
|
51
|
+
'ai-api.index',
|
|
52
|
+
'ai-api.model-pricing',
|
|
53
|
+
'ai-api.model-metadata',
|
|
54
|
+
'ai-api.user-quotas',
|
|
55
|
+
'ai-api.usage',
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
});
|