plugin-ai-api 1.0.24 → 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.6568d3504ad29352.js +10 -0
- 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.f2bc9cfba07004b0.js +10 -0
- 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/externalVersion.js +8 -8
- package/dist/locale/en-US.json +27 -8
- package/dist/locale/vi-VN.json +27 -8
- package/dist/locale/zh-CN.json +27 -8
- package/dist/server/billing.js +31 -33
- package/dist/server/collections/ai-api-config.js +7 -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 +2 -0
- package/dist/server/middleware/rate-limit.js +7 -6
- package/dist/server/migrations/20260813000000-add-prompt-cache-tokens.js +69 -0
- 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 +100 -22
- package/dist/server/quota-groups.js +108 -0
- package/dist/server/resource/ai-api-config.js +5 -3
- package/dist/server/resource/ai-api-usage-groups.js +168 -0
- package/dist/server/resource/ai-api-usage-monitor.js +3 -1
- package/dist/server/routes/agent-completions.js +2 -1
- package/dist/server/routes/chat-completions.js +121 -42
- package/dist/server/routes/completions.js +48 -29
- 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 +426 -0
- package/dist/server/usage.js +37 -3
- package/dist/server/utils/direct-llm-context.js +163 -26
- package/dist/server/utils/openai-format.js +21 -2
- 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 +48 -10
- 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/pages/UsagePage.tsx +9 -0
- package/src/client-v2/plugin.tsx +4 -13
- package/src/constants.ts +0 -7
- package/src/locale/en-US.json +27 -8
- package/src/locale/vi-VN.json +27 -8
- package/src/locale/zh-CN.json +27 -8
- package/src/server/__tests__/billing-quota.test.ts +28 -9
- package/src/server/__tests__/direct-llm-context.test.ts +209 -10
- package/src/server/__tests__/file-processor.test.ts +225 -0
- package/src/server/__tests__/models.test.ts +1 -1
- package/src/server/__tests__/openai-format.test.ts +12 -2
- package/src/server/__tests__/permission-sync.test.ts +34 -35
- package/src/server/__tests__/request-body.test.ts +45 -2
- 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 +382 -5
- package/src/server/__tests__/usage.test.ts +57 -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 +36 -39
- package/src/server/collections/ai-api-config.ts +9 -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-role-permissions.ts +41 -41
- package/src/server/collections/ai-api-usage-groups.ts +53 -0
- package/src/server/collections/ai-api-usage-records.ts +2 -0
- package/src/server/index.ts +10 -10
- package/src/server/middleware/rate-limit.ts +68 -70
- package/src/server/migrations/20260813000000-add-prompt-cache-tokens.ts +46 -0
- 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 +121 -30
- package/src/server/quota-groups.ts +117 -0
- package/src/server/resource/ai-api-config.ts +5 -3
- package/src/server/resource/ai-api-usage-groups.ts +171 -0
- package/src/server/resource/ai-api-usage-monitor.ts +3 -0
- package/src/server/routes/agent-completions.ts +2 -1
- package/src/server/routes/chat-completions.ts +173 -47
- package/src/server/routes/completions.ts +50 -27
- 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/__tests__/file-processor.test.ts +184 -0
- package/src/server/services/file-processor.ts +513 -0
- package/src/server/usage.ts +51 -1
- package/src/server/utils/direct-llm-context.ts +218 -31
- package/src/server/utils/openai-format.ts +25 -2
- package/src/server/utils/rate-limiter.ts +83 -83
- package/src/server/utils/request-cache.ts +59 -0
- package/src/server/utils/resolve-service.ts +83 -82
- package/src/server/utils/user-permissions.ts +49 -69
- package/src/server/validation.ts +7 -0
- package/src/swagger.ts +52 -11
- 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/757.a01403fb7a1bea01.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/757.a117ce1cf7119cea.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
|
@@ -0,0 +1,59 @@
|
|
|
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 type { Context } from '@nocobase/actions';
|
|
11
|
+
import type { Model } from '@nocobase/database';
|
|
12
|
+
import { resolveUserGroup, type AiApiUsageGroup } from '../quota-groups';
|
|
13
|
+
|
|
14
|
+
interface AiApiRequestCache {
|
|
15
|
+
configLoaded: boolean;
|
|
16
|
+
config: Model | null;
|
|
17
|
+
groupKey?: string;
|
|
18
|
+
group?: AiApiUsageGroup;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getCache(ctx: Context): AiApiRequestCache {
|
|
22
|
+
if (!ctx.state.aiApiRequestCache) {
|
|
23
|
+
ctx.state.aiApiRequestCache = { configLoaded: false, config: null };
|
|
24
|
+
}
|
|
25
|
+
return ctx.state.aiApiRequestCache;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns the aiApiConfig row, reading it at most once per request. The same row
|
|
30
|
+
* is consulted by the body limit, mode resolution, model whitelist, quota and
|
|
31
|
+
* billing steps of a single gateway request, so caching it on ctx.state removes
|
|
32
|
+
* several duplicate queries per request.
|
|
33
|
+
*/
|
|
34
|
+
export async function getAiApiConfig(ctx: Context): Promise<Model | null> {
|
|
35
|
+
const cache = getCache(ctx);
|
|
36
|
+
if (!cache.configLoaded) {
|
|
37
|
+
cache.config = (await ctx.db.getRepository('aiApiConfig').findOne()) ?? null;
|
|
38
|
+
cache.configLoaded = true;
|
|
39
|
+
}
|
|
40
|
+
return cache.config;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns the caller's usage group, resolving it at most once per request per
|
|
45
|
+
* user id. Wraps resolveUserGroup, which otherwise runs membership queries on
|
|
46
|
+
* every call site (rate limiting, permissions, billing, context overflow).
|
|
47
|
+
*/
|
|
48
|
+
export async function resolveRequestUserGroup(
|
|
49
|
+
ctx: Context,
|
|
50
|
+
userId: string | number | bigint | undefined | null,
|
|
51
|
+
): Promise<AiApiUsageGroup> {
|
|
52
|
+
const cache = getCache(ctx);
|
|
53
|
+
const groupKey = userId === undefined || userId === null ? '' : String(userId);
|
|
54
|
+
if (!cache.group || cache.groupKey !== groupKey) {
|
|
55
|
+
cache.group = await resolveUserGroup(ctx, userId);
|
|
56
|
+
cache.groupKey = groupKey;
|
|
57
|
+
}
|
|
58
|
+
return cache.group;
|
|
59
|
+
}
|
|
@@ -1,82 +1,83 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* This file is part of the NocoBase (R) project.
|
|
3
|
-
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
-
* Authors: NocoBase Team.
|
|
5
|
-
*
|
|
6
|
-
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
-
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { Context } from '@nocobase/actions';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Context } from '@nocobase/actions';
|
|
11
|
+
import { getAiApiConfig } from './request-cache';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve an LLM service by name or title.
|
|
15
|
+
*/
|
|
16
|
+
export async function resolveLlmService(ctx: Context, serviceKey: string) {
|
|
17
|
+
const repo = ctx.db.getRepository('llmServices');
|
|
18
|
+
|
|
19
|
+
let service = await repo.findOne({ filter: { name: serviceKey } });
|
|
20
|
+
if (!service) {
|
|
21
|
+
service = await repo.findOne({ filter: { title: serviceKey } });
|
|
22
|
+
}
|
|
23
|
+
return service;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a model string to a service + modelId.
|
|
28
|
+
*
|
|
29
|
+
* Strategy (priority order):
|
|
30
|
+
* 1. Try splitting at each "/" position and match the left part against DB (name or title).
|
|
31
|
+
* This handles cases like "Custom LLM (OpenAI Compatible)/qwen/qwen3.6-plus-preview:free"
|
|
32
|
+
* 2. If no service match, use the defaultLlmService from config and treat the ENTIRE
|
|
33
|
+
* model string as the modelId. This allows clients to send just "qwen/qwen3.6-plus-preview:free"
|
|
34
|
+
* or "gpt-4o" without knowing the service name.
|
|
35
|
+
*/
|
|
36
|
+
export async function resolveModelString(
|
|
37
|
+
ctx: Context,
|
|
38
|
+
modelString: string,
|
|
39
|
+
): Promise<{ service: any; modelId: string } | null> {
|
|
40
|
+
const repo = ctx.db.getRepository('llmServices');
|
|
41
|
+
|
|
42
|
+
// ─── Strategy 1: Try splitting at "/" positions ───
|
|
43
|
+
const slashPositions: number[] = [];
|
|
44
|
+
for (let i = 0; i < modelString.length; i++) {
|
|
45
|
+
if (modelString[i] === '/') {
|
|
46
|
+
slashPositions.push(i);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (slashPositions.length > 0) {
|
|
51
|
+
for (const pos of slashPositions) {
|
|
52
|
+
const serviceKey = modelString.substring(0, pos);
|
|
53
|
+
const modelId = modelString.substring(pos + 1);
|
|
54
|
+
if (!serviceKey || !modelId) continue;
|
|
55
|
+
|
|
56
|
+
let service = await repo.findOne({ filter: { name: serviceKey } });
|
|
57
|
+
if (!service) {
|
|
58
|
+
service = await repo.findOne({ filter: { title: serviceKey } });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (service) {
|
|
62
|
+
return { service, modelId };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── Strategy 2: Use default LLM service from config ───
|
|
68
|
+
const config = await getAiApiConfig(ctx);
|
|
69
|
+
if (config?.defaultLlmService) {
|
|
70
|
+
const service = await repo.findOne({ filter: { name: config.defaultLlmService } });
|
|
71
|
+
if (service) {
|
|
72
|
+
return { service, modelId: modelString };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Strategy 3: If only one service enabled, use it ───
|
|
77
|
+
const enabledServices = await repo.find({ filter: { enabled: true } });
|
|
78
|
+
if (enabledServices.length === 1) {
|
|
79
|
+
return { service: enabledServices[0], modelId: modelString };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
import { Context } from '@nocobase/actions';
|
|
11
11
|
import { toOpenAIError } from './openai-format';
|
|
12
|
+
import { resolveRequestUserGroup } from './request-cache';
|
|
13
|
+
import type { AiApiUsageGroup } from '../quota-groups';
|
|
12
14
|
|
|
13
15
|
const SCOPE_TTL_MS = 15_000;
|
|
14
16
|
|
|
@@ -20,114 +22,94 @@ interface CachedScope {
|
|
|
20
22
|
const scopeCache = new Map<string, CachedScope>();
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
|
-
* Resolved
|
|
24
|
-
* whitelist.
|
|
25
|
+
* Resolved LLM access of a user's usage group, layered *under* the global
|
|
26
|
+
* `aiApiConfig.enabledLlmServices` whitelist. Group settings can only ever narrow global
|
|
27
|
+
* access, never widen it. Empty lists on the group mean "no narrowing" — the group inherits
|
|
28
|
+
* the full global configuration, so the default group never locks everyone out.
|
|
25
29
|
*/
|
|
26
30
|
export interface AiApiAccessScope {
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
/** null means "no user-level narrowing"; an empty array denies every service. */
|
|
32
|
-
allowedServices: string[] | null;
|
|
31
|
+
/** The group whose settings produced this scope. */
|
|
32
|
+
groupId?: string | number | bigint;
|
|
33
|
+
/** Empty means no narrowing; non-empty restricts to these services. */
|
|
34
|
+
allowedServices: string[];
|
|
33
35
|
allowAllModels: boolean;
|
|
34
36
|
allowedModels: Set<string>;
|
|
35
37
|
/**
|
|
36
|
-
* True when the lookup itself failed. Distinct from
|
|
37
|
-
* restrictions" and "we cannot tell whether this user has restrictions" must not be
|
|
38
|
-
* or a mid-rolling-upgrade missing table silently lifts every user's restrictions.
|
|
38
|
+
* True when the lookup itself failed. Distinct from an open scope — "this user has no
|
|
39
|
+
* restrictions" and "we cannot tell whether this user has restrictions" must not be
|
|
40
|
+
* conflated, or a mid-rolling-upgrade missing table silently lifts every user's restrictions.
|
|
39
41
|
*/
|
|
40
42
|
lookupFailed: boolean;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
denyAll: false,
|
|
46
|
-
allowedServices: null,
|
|
45
|
+
const OPEN_SCOPE: AiApiAccessScope = {
|
|
46
|
+
allowedServices: [],
|
|
47
47
|
allowAllModels: true,
|
|
48
48
|
allowedModels: new Set(),
|
|
49
49
|
lookupFailed: false,
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
-
const LOOKUP_FAILED_SCOPE: AiApiAccessScope = { ...
|
|
52
|
+
const LOOKUP_FAILED_SCOPE: AiApiAccessScope = { ...OPEN_SCOPE, lookupFailed: true };
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
* Invalidate cached scopes for one
|
|
56
|
-
* called with no argument. Keys are `${appName}:${
|
|
57
|
-
* the user id, so the match is on the suffix.
|
|
55
|
+
* Invalidate cached scopes for one group across every app in this process, or all groups
|
|
56
|
+
* when called with no argument. Keys are `${appName}:group:${groupId}`.
|
|
58
57
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
58
|
+
* Per-process only: in a multi-node deployment other nodes keep serving their cached scope
|
|
59
|
+
* until the 15s TTL expires or the sync message arrives.
|
|
61
60
|
*/
|
|
62
|
-
export function
|
|
63
|
-
if (
|
|
61
|
+
export function invalidateGroupAccessCache(groupId?: string | number | bigint): void {
|
|
62
|
+
if (groupId === undefined || groupId === null) {
|
|
64
63
|
scopeCache.clear();
|
|
65
64
|
return;
|
|
66
65
|
}
|
|
67
|
-
const suffix =
|
|
66
|
+
const suffix = `:group:${groupId}`;
|
|
68
67
|
for (const key of scopeCache.keys()) {
|
|
69
68
|
if (key.endsWith(suffix)) scopeCache.delete(key);
|
|
70
69
|
}
|
|
71
70
|
}
|
|
72
71
|
|
|
73
|
-
/**
|
|
74
|
-
* Sequelize instances expose columns through .get() only — a plain property read returns
|
|
75
|
-
* undefined for most fields. Mirrors valueOf() in billing.ts so plain-object test fixtures
|
|
76
|
-
* work too.
|
|
77
|
-
*/
|
|
78
|
-
function valueOf<T>(row: unknown, name: string): T {
|
|
79
|
-
if (!row) return undefined as T;
|
|
80
|
-
const candidate = row as { get?: (key: string) => unknown };
|
|
81
|
-
if (typeof candidate.get === 'function') return candidate.get(name) as T;
|
|
82
|
-
return (row as Record<string, unknown>)[name] as T;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
72
|
function toStringArray(value: unknown): string[] {
|
|
86
73
|
if (!Array.isArray(value)) return [];
|
|
87
74
|
return value.filter((item): item is string => typeof item === 'string' && item.length > 0);
|
|
88
75
|
}
|
|
89
76
|
|
|
90
|
-
/** Build a scope from
|
|
91
|
-
export function buildAccessScope(
|
|
92
|
-
if (!row) return NO_RECORD_SCOPE;
|
|
93
|
-
if (valueOf<boolean>(row, 'enabled') === false) {
|
|
94
|
-
return { ...NO_RECORD_SCOPE, hasUserRecord: true, denyAll: true, allowedServices: [] };
|
|
95
|
-
}
|
|
77
|
+
/** Build a scope from a usage group. Empty lists mean no narrowing. */
|
|
78
|
+
export function buildAccessScope(group: AiApiUsageGroup): AiApiAccessScope {
|
|
96
79
|
return {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
allowedModels: new Set(toStringArray(valueOf(row, 'allowedModels'))),
|
|
80
|
+
groupId: group.id,
|
|
81
|
+
allowedServices: toStringArray(group.allowedLlmServices),
|
|
82
|
+
allowAllModels: group.allowAllModels !== false,
|
|
83
|
+
allowedModels: new Set(toStringArray(group.allowedModels)),
|
|
102
84
|
lookupFailed: false,
|
|
103
85
|
};
|
|
104
86
|
}
|
|
105
87
|
|
|
106
88
|
/**
|
|
107
|
-
* Load the current user's
|
|
108
|
-
* permission cache uses.
|
|
89
|
+
* Load the access scope of the current user's usage group, cached for 15s per group id —
|
|
90
|
+
* the same TTL the role permission cache uses. Membership is resolved live on every request
|
|
91
|
+
* and memoized for its duration (one indexed query even though several call sites ask), so
|
|
92
|
+
* moving a user between groups needs no invalidation; group edits are invalidated by the
|
|
93
|
+
* afterSave/afterDestroy hooks in plugin.ts.
|
|
109
94
|
*/
|
|
110
95
|
export async function resolveUserAccessScope(ctx: Context): Promise<AiApiAccessScope> {
|
|
111
96
|
const userId = ctx.state.currentUser?.id;
|
|
112
|
-
if (userId === undefined || userId === null) return NO_RECORD_SCOPE;
|
|
113
97
|
|
|
114
|
-
|
|
115
|
-
// different person than user id 1 in another. Without the app prefix they collide here.
|
|
116
|
-
const key = `${ctx.app?.name ?? 'main'}:${userId}`;
|
|
117
|
-
const cached = scopeCache.get(key);
|
|
118
|
-
if (cached && cached.expiresAt > Date.now()) return cached.scope;
|
|
119
|
-
|
|
120
|
-
let scope: AiApiAccessScope;
|
|
98
|
+
let group: AiApiUsageGroup;
|
|
121
99
|
try {
|
|
122
|
-
|
|
123
|
-
scope = buildAccessScope(row);
|
|
100
|
+
group = await resolveRequestUserGroup(ctx, userId);
|
|
124
101
|
} catch (err) {
|
|
125
102
|
// Fail closed. A failed lookup cannot be treated as "no restrictions": during a rolling
|
|
126
|
-
// upgrade the
|
|
127
|
-
ctx.log?.error?.('AI API
|
|
103
|
+
// upgrade the tables may not exist yet, and that must not lift every user's restrictions.
|
|
104
|
+
ctx.log?.error?.('AI API group access lookup failed, denying access:', err);
|
|
128
105
|
return LOOKUP_FAILED_SCOPE;
|
|
129
106
|
}
|
|
130
107
|
|
|
108
|
+
const key = `${ctx.app?.name ?? 'main'}:group:${group.id}`;
|
|
109
|
+
const cached = scopeCache.get(key);
|
|
110
|
+
if (cached && cached.expiresAt > Date.now()) return cached.scope;
|
|
111
|
+
|
|
112
|
+
const scope = buildAccessScope(group);
|
|
131
113
|
scopeCache.set(key, { scope, expiresAt: Date.now() + SCOPE_TTL_MS });
|
|
132
114
|
return scope;
|
|
133
115
|
}
|
|
@@ -137,28 +119,26 @@ function matchesService(list: string[], serviceName?: string, serviceTitle?: str
|
|
|
137
119
|
}
|
|
138
120
|
|
|
139
121
|
/**
|
|
140
|
-
* Effective service check: the global whitelist AND the
|
|
122
|
+
* Effective service check: the global whitelist AND the group settings must both allow it.
|
|
141
123
|
*
|
|
142
124
|
* An empty global whitelist means "expose all services", preserving existing behaviour.
|
|
143
|
-
* An empty
|
|
125
|
+
* An empty group list means "no narrowing" — the group inherits the global configuration.
|
|
144
126
|
*/
|
|
145
127
|
export function isServiceAllowed(
|
|
146
128
|
scope: AiApiAccessScope,
|
|
147
129
|
globalEnabledServices: unknown,
|
|
148
130
|
service: { name?: string; title?: string },
|
|
149
131
|
): boolean {
|
|
150
|
-
|
|
151
|
-
if (scope.denyAll) return false;
|
|
132
|
+
if (scope.lookupFailed) return false;
|
|
152
133
|
const globalList = toStringArray(globalEnabledServices);
|
|
153
134
|
if (globalList.length && !matchesService(globalList, service.name, service.title)) return false;
|
|
154
|
-
if (!scope.
|
|
155
|
-
return matchesService(scope.allowedServices
|
|
135
|
+
if (!scope.allowedServices.length) return true;
|
|
136
|
+
return matchesService(scope.allowedServices, service.name, service.title);
|
|
156
137
|
}
|
|
157
138
|
|
|
158
139
|
/** Model-level narrowing on top of isServiceAllowed, keyed by "serviceName/modelId". */
|
|
159
140
|
export function isModelAllowed(scope: AiApiAccessScope, fullModelId: string): boolean {
|
|
160
|
-
if (scope.
|
|
161
|
-
if (!scope.hasUserRecord) return true;
|
|
141
|
+
if (scope.lookupFailed) return false;
|
|
162
142
|
if (scope.allowAllModels) return true;
|
|
163
143
|
return scope.allowedModels.has(fullModelId);
|
|
164
144
|
}
|
package/src/server/validation.ts
CHANGED
|
@@ -53,6 +53,10 @@ export function validateModelMetadata(model: Model): void {
|
|
|
53
53
|
if (!String(model.get('model') ?? '').trim()) throw new Error('model is required.');
|
|
54
54
|
requirePositiveIntegerOrNull(model.get('contextWindow'), 'contextWindow');
|
|
55
55
|
requirePositiveIntegerOrNull(model.get('maxCompletionTokens'), 'maxCompletionTokens');
|
|
56
|
+
const systemPrompt = model.get('systemPrompt');
|
|
57
|
+
if (systemPrompt !== null && systemPrompt !== undefined && typeof systemPrompt !== 'string') {
|
|
58
|
+
throw new Error('systemPrompt must be a string.');
|
|
59
|
+
}
|
|
56
60
|
|
|
57
61
|
const contextWindow = model.get('contextWindow');
|
|
58
62
|
const maxCompletionTokens = model.get('maxCompletionTokens');
|
|
@@ -73,6 +77,9 @@ export function validateQuotaPolicy(model: Model): void {
|
|
|
73
77
|
if (!['daily', 'monthly'].includes(String(model.get('periodType')))) {
|
|
74
78
|
throw new Error('periodType must be daily or monthly.');
|
|
75
79
|
}
|
|
80
|
+
if (!['share', 'per_user'].includes(String(model.get('quotaMode')))) {
|
|
81
|
+
throw new Error('quotaMode must be share or per_user.');
|
|
82
|
+
}
|
|
76
83
|
if (!['allow', 'use_reserved'].includes(String(model.get('missingUsageBehavior')))) {
|
|
77
84
|
throw new Error('missingUsageBehavior must be allow or use_reserved.');
|
|
78
85
|
}
|
package/src/swagger.ts
CHANGED
|
@@ -59,8 +59,8 @@ export default {
|
|
|
59
59
|
description:
|
|
60
60
|
'Returns the LLM models available to the authenticated caller across registered services. Model IDs are formatted as `serviceName/modelId`.\n\n' +
|
|
61
61
|
"The catalog is user-scoped: it starts from `enabledLlmServices` in the AI API configuration, then narrows to the caller's " +
|
|
62
|
-
'`
|
|
63
|
-
'users may receive different lists from the same request.',
|
|
62
|
+
'usage group settings (`allowedLlmServices` / `allowedModels`). Group settings can only narrow the global whitelist, never ' +
|
|
63
|
+
'widen it, so two users may receive different lists from the same request.',
|
|
64
64
|
security: [{ BearerAuth: [] }],
|
|
65
65
|
responses: {
|
|
66
66
|
200: {
|
|
@@ -252,17 +252,16 @@ export default {
|
|
|
252
252
|
enum: ['llm', 'agent'],
|
|
253
253
|
description: 'Default AI mode',
|
|
254
254
|
},
|
|
255
|
-
defaultAiEmployee: {
|
|
255
|
+
defaultAiEmployee: {
|
|
256
|
+
type: 'string',
|
|
257
|
+
description: 'Default AI employee name (agent mode only; direct LLM mode ignores it)',
|
|
258
|
+
},
|
|
256
259
|
defaultLlmService: { type: 'string', description: 'Default LLM service name' },
|
|
257
260
|
enabledLlmServices: {
|
|
258
261
|
type: 'array',
|
|
259
262
|
items: { type: 'string' },
|
|
260
263
|
description:
|
|
261
|
-
'List of enabled LLM service names. This is the outer bound for every caller;
|
|
262
|
-
},
|
|
263
|
-
rateLimitPerMinute: {
|
|
264
|
-
type: 'integer',
|
|
265
|
-
description: 'Max requests per minute per user (0 = unlimited)',
|
|
264
|
+
'List of enabled LLM service names. This is the outer bound for every caller; usage group settings can only narrow it further.',
|
|
266
265
|
},
|
|
267
266
|
maxRequestBodyMb: {
|
|
268
267
|
type: 'integer',
|
|
@@ -273,6 +272,13 @@ export default {
|
|
|
273
272
|
'Max request body size in megabytes. Requests above this return 413. ' +
|
|
274
273
|
'The gateway buffers each body in memory, so values above 100 are rejected.',
|
|
275
274
|
},
|
|
275
|
+
pdfRenderPagesAsImages: {
|
|
276
|
+
type: 'boolean',
|
|
277
|
+
default: false,
|
|
278
|
+
description:
|
|
279
|
+
'When true, PDF file/file_url blocks are rendered to per-page PNG images and sent as image_url blocks. ' +
|
|
280
|
+
'Requires a registered PdfToImageRenderer. When false or no renderer is available, PDFs are forwarded as file blocks.',
|
|
281
|
+
},
|
|
276
282
|
},
|
|
277
283
|
},
|
|
278
284
|
ModelObject: {
|
|
@@ -287,10 +293,10 @@ export default {
|
|
|
287
293
|
ContentBlock: {
|
|
288
294
|
type: 'object',
|
|
289
295
|
description:
|
|
290
|
-
'A multimodal content block.
|
|
291
|
-
'
|
|
296
|
+
'A multimodal content block. text, image_url, file and file_url blocks are forwarded; ' +
|
|
297
|
+
'file and file_url blocks are first run through the configurable file processor service.',
|
|
292
298
|
properties: {
|
|
293
|
-
type: { type: 'string', enum: ['text', 'image_url'] },
|
|
299
|
+
type: { type: 'string', enum: ['text', 'image_url', 'file', 'file_url'] },
|
|
294
300
|
text: { type: 'string' },
|
|
295
301
|
image_url: {
|
|
296
302
|
type: 'object',
|
|
@@ -304,6 +310,35 @@ export default {
|
|
|
304
310
|
},
|
|
305
311
|
required: ['url'],
|
|
306
312
|
},
|
|
313
|
+
file: {
|
|
314
|
+
type: 'object',
|
|
315
|
+
properties: {
|
|
316
|
+
file_data: {
|
|
317
|
+
type: 'string',
|
|
318
|
+
description: 'A base64 data URL, e.g. data:application/pdf;base64,JVBERi0...',
|
|
319
|
+
example: 'data:application/pdf;base64,JVBERi0...',
|
|
320
|
+
},
|
|
321
|
+
filename: { type: 'string' },
|
|
322
|
+
mime_type: {
|
|
323
|
+
type: 'string',
|
|
324
|
+
description: 'MIME type of the file, e.g. application/pdf',
|
|
325
|
+
example: 'application/pdf',
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
required: ['file_data'],
|
|
329
|
+
},
|
|
330
|
+
file_url: {
|
|
331
|
+
type: 'object',
|
|
332
|
+
properties: {
|
|
333
|
+
url: {
|
|
334
|
+
type: 'string',
|
|
335
|
+
description:
|
|
336
|
+
'An http(s) URL pointing to a file. The gateway downloads the file and converts it to a file block.',
|
|
337
|
+
example: 'https://example.com/document.pdf',
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
required: ['url'],
|
|
341
|
+
},
|
|
307
342
|
},
|
|
308
343
|
required: ['type'],
|
|
309
344
|
},
|
|
@@ -366,6 +401,12 @@ export default {
|
|
|
366
401
|
prompt_tokens: { type: 'integer' },
|
|
367
402
|
completion_tokens: { type: 'integer' },
|
|
368
403
|
total_tokens: { type: 'integer' },
|
|
404
|
+
prompt_tokens_details: {
|
|
405
|
+
type: 'object',
|
|
406
|
+
properties: {
|
|
407
|
+
cached_tokens: { type: 'integer' },
|
|
408
|
+
},
|
|
409
|
+
},
|
|
369
410
|
},
|
|
370
411
|
},
|
|
371
412
|
},
|
|
@@ -1,10 +0,0 @@
|
|
|
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
|
-
"use strict";(self.webpackChunkplugin_ai_api=self.webpackChunkplugin_ai_api||[]).push([["123"],{46:function(e,t,r){r.r(t),r.d(t,{default:function(){return p}});var n=r(155),l=r.n(n),a=r(59),o=r(694),i=r(650),u=r(630);function s(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=Array(t);r<t;r++)n[r]=e[r];return n}function c(e,t,r,n,l,a,o){try{var i=e[a](o),u=i.value}catch(e){r(e);return}i.done?t(u):Promise.resolve(u).then(n,l)}function d(e){return function(){var t=this,r=arguments;return new Promise(function(n,l){var a=e.apply(t,r);function o(e){c(a,n,l,o,i,"next",e)}function i(e){c(a,n,l,o,i,"throw",e)}o(void 0)})}}function m(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var r,n,l=null==e?null:"u">typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=l){var a=[],o=!0,i=!1;try{for(l=l.call(e);!(o=(r=l.next()).done)&&(a.push(r.value),!t||a.length!==t);o=!0);}catch(e){i=!0,n=e}finally{try{o||null==l.return||l.return()}finally{if(i)throw n}}return a}}(e,t)||function(e,t){if(e){if("string"==typeof e)return s(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);if("Object"===r&&e.constructor&&(r=e.constructor.name),"Map"===r||"Set"===r)return Array.from(r);if("Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return s(e,t)}}(e,t)||function(){throw TypeError("Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function f(e,t){var r,n,l,a={label:0,sent:function(){if(1&l[0])throw l[1];return l[1]},trys:[],ops:[]},o=Object.create(("function"==typeof Iterator?Iterator:Object).prototype),i=Object.defineProperty;return i(o,"next",{value:u(0)}),i(o,"throw",{value:u(1)}),i(o,"return",{value:u(2)}),"function"==typeof Symbol&&i(o,Symbol.iterator,{value:function(){return this}}),o;function u(i){return function(u){var s=[i,u];if(r)throw TypeError("Generator is already executing.");for(;o&&(o=0,s[0]&&(a=0)),a;)try{if(r=1,n&&(l=2&s[0]?n.return:s[0]?n.throw||((l=n.return)&&l.call(n),0):n.next)&&!(l=l.call(n,s[1])).done)return l;switch(n=0,l&&(s=[2&s[0],l.value]),s[0]){case 0:case 1:l=s;break;case 4:return a.label++,{value:s[1],done:!1};case 5:a.label++,n=s[1],s=[0];continue;case 7:s=a.ops.pop(),a.trys.pop();continue;default:if(!(l=(l=a.trys).length>0&&l[l.length-1])&&(6===s[0]||2===s[0])){a=0;continue}if(3===s[0]&&(!l||s[1]>l[0]&&s[1]<l[3])){a.label=s[1];break}if(6===s[0]&&a.label<l[1]){a.label=l[1],l=s;break}if(l&&a.label<l[2]){a.label=l[2],a.ops.push(s);break}l[2]&&a.ops.pop(),a.trys.pop();continue}s=t.call(e,a)}catch(e){s=[6,e],n=0}finally{r=l=0}if(5&s[0])throw s[1];return{value:s[0]?s[1]:void 0,done:!0}}}}function p(){var e=(0,o.useFlowContext)(),t=(0,i.k)(),r=m(a.Form.useForm(),1)[0],s=a.Form.useWatch("allowedLlmServices",r),c=a.Form.useWatch("allowAllModels",r),p=m((0,n.useState)([]),2),v=p[0],h=p[1],b=m((0,n.useState)(0),2),y=b[0],w=b[1],S=m((0,n.useState)(1),2),g=S[0],E=S[1],k=m((0,n.useState)([]),2),A=k[0],M=k[1],I=m((0,n.useState)(""),2),F=I[0],C=I[1],U=m((0,n.useState)([]),2),x=U[0],L=U[1],P=m((0,n.useState)(!1),2),T=P[0],O=P[1],j=m((0,n.useState)(!1),2),z=j[0],q=j[1],B=m((0,n.useState)(),2),D=B[0],N=B[1],_=m((0,n.useState)(!1),2),G=_[0],V=_[1],W=(0,n.useCallback)(function(){return d(function(){var t,r,n,l,o,i,s;return f(this,function(c){switch(c.label){case 0:O(!0),c.label=1;case 1:return c.trys.push([1,3,4,5]),[4,Promise.all([e.api.request({url:"aiApiUserPermissions:list",method:"get",params:{page:g,pageSize:20,appends:["user"],sort:"-updatedAt"}}),e.api.request({url:"ai:listAllEnabledModels",method:"get"})])];case 2:return o=(l=m.apply(void 0,[c.sent(),2]))[0],i=l[1],h((0,u.m)(o,[])),w(null!=(t=null==o||null==(n=o.data)||null==(r=n.meta)?void 0:r.count)?t:0),L((0,u.m)(i,[])),[3,5];case 3:return s=c.sent(),a.message.error((0,u.g)(s)),[3,5];case 4:return O(!1),[7];case 5:return[2]}})})()},[e.api,g]);(0,n.useEffect)(function(){W()},[W]);var J=(0,n.useCallback)(function(t){return d(function(){var r,n;return f(this,function(l){switch(l.label){case 0:return l.trys.push([0,2,,3]),[4,e.api.request({url:"aiApiUserPermissions:listUsers",method:"get",params:{keyword:t,pageSize:50,excludeGranted:!D}})];case 1:return r=l.sent(),M((0,u.m)(r,[])),[3,3];case 2:return n=l.sent(),a.message.error((0,u.g)(n)),[3,3];case 3:return[2]}})})()},[e.api,D]);(0,n.useEffect)(function(){if(G){var e=setTimeout(function(){return J(F)},300);return function(){return clearTimeout(e)}}},[G,F,J]);var K=(0,n.useMemo)(function(){return x.map(function(e){return{label:e.llmServiceTitle||e.llmService,value:e.llmService}})},[x]),$=(0,n.useMemo)(function(){var e=new Set(s||[]);return x.filter(function(t){return e.has(t.llmService)}).flatMap(function(e){return(e.enabledModels||[]).map(function(t){return{label:"".concat(e.llmServiceTitle||e.llmService," / ").concat(t.label||t.value),value:"".concat(e.llmService,"/").concat(t.value)}})})},[x,s]),H=function(e){N(e),C(""),M(e.user?[e.user]:[]),r.setFieldsValue({userId:e.userId,enabled:e.enabled,allowAllModels:e.allowAllModels,allowedLlmServices:e.allowedLlmServices||[],allowedModels:e.allowedModels||[]}),V(!0)},Q=function(e){var t;return(null==e?void 0:e.nickname)||(null==e?void 0:e.username)||(null==e?void 0:e.email)||String(null!=(t=null==e?void 0:e.id)?t:"")},R=function(e){var t;return(null==(t=K.find(function(t){return t.value===e}))?void 0:t.label)||e},X=[{title:t("User"),key:"user",width:180,render:function(e,t){return Q(t.user)||String(t.userId)}},{title:t("Allowed LLM services"),dataIndex:"allowedLlmServices",key:"allowedLlmServices",render:function(e){return(null==e?void 0:e.length)?l().createElement(a.Space,{size:[0,4],wrap:!0},e.map(function(e){return l().createElement(a.Tag,{key:e},R(e))})):l().createElement(a.Tag,{color:"red"},t("No service allowed"))}},{title:t("Allowed models"),key:"allowedModels",width:220,render:function(e,r){return r.allowAllModels?l().createElement(a.Tag,{color:"blue"},t("All models of allowed services")):l().createElement(a.Space,{size:[0,4],wrap:!0},(r.allowedModels||[]).map(function(e){return l().createElement(a.Tag,{key:e},e)}))}},{title:t("Status"),dataIndex:"enabled",key:"enabled",width:100,render:function(e){return l().createElement(a.Tag,{color:e?"green":"default"},e?t("Enabled"):t("Disabled"))}},{title:t("Actions"),key:"actions",width:150,fixed:"right",render:function(r,n){return l().createElement(a.Space,{size:0},l().createElement(a.Button,{type:"link",onClick:function(){return H(n)}},t("Edit")),l().createElement(a.Popconfirm,{title:t("Delete this permission?"),onConfirm:function(){return d(function(){var r;return f(this,function(l){switch(l.label){case 0:return l.trys.push([0,3,,4]),[4,e.api.request({url:"aiApiUserPermissions:destroy/".concat(n.id),method:"post"})];case 1:return l.sent(),a.message.success(t("Deleted successfully")),[4,W()];case 2:return l.sent(),[3,4];case 3:return r=l.sent(),a.message.error((0,u.g)(r)),[3,4];case 4:return[2]}})})()}},l().createElement(a.Button,{type:"link",danger:!0},t("Delete"))))}}];return l().createElement(a.Card,{title:t("User LLM permissions"),extra:l().createElement(a.Button,{type:"primary",onClick:function(){N(void 0),C(""),r.setFieldsValue({enabled:!0,allowedLlmServices:[],allowAllModels:!0,allowedModels:[]}),V(!0)}},t("Add permission"))},l().createElement(a.Alert,{type:"info",showIcon:!0,style:{marginBottom:16},message:t("Users listed here are limited to the services selected below. Users without a record fall back to the general configuration.")}),l().createElement(a.Table,{rowKey:"id",columns:X,dataSource:v,loading:T,scroll:{x:1e3},pagination:{current:g,pageSize:20,total:y,showSizeChanger:!1,onChange:E}}),l().createElement(a.Modal,{title:D?t("Edit permission"):t("Add permission"),open:G,onCancel:function(){return V(!1)},onOk:function(){return d(function(){var n,l;return f(this,function(o){switch(o.label){case 0:return[4,r.validateFields()];case 1:n=o.sent(),q(!0),o.label=2;case 2:return o.trys.push([2,5,6,7]),[4,e.api.request({url:D?"aiApiUserPermissions:update/".concat(D.id):"aiApiUserPermissions:create",method:"post",data:n})];case 3:return o.sent(),a.message.success(t("Saved successfully")),V(!1),[4,W()];case 4:return o.sent(),[3,7];case 5:return l=o.sent(),a.message.error((0,u.g)(l)),[3,7];case 6:return q(!1),[7];case 7:return[2]}})})()},confirmLoading:z,destroyOnClose:!0},l().createElement(a.Form,{form:r,layout:"vertical",preserve:!1},l().createElement(a.Form.Item,{name:"userId",label:t("User"),rules:[{required:!0}]},l().createElement(a.Select,{disabled:!!D,showSearch:!0,filterOption:!1,onSearch:C,notFoundContent:null,options:A.map(function(e){return{label:Q(e),value:e.id}})})),l().createElement(a.Form.Item,{name:"allowedLlmServices",label:t("Allowed LLM services"),extra:t("Only services also enabled in the general configuration take effect.")},l().createElement(a.Select,{mode:"multiple",allowClear:!0,showSearch:!0,optionFilterProp:"label",options:K})),l().createElement(a.Form.Item,{name:"allowAllModels",label:t("Allow all models"),valuePropName:"checked"},l().createElement(a.Switch,null)),!1===c&&l().createElement(a.Form.Item,{name:"allowedModels",label:t("Allowed models")},l().createElement(a.Select,{mode:"multiple",allowClear:!0,showSearch:!0,optionFilterProp:"label",options:$})),l().createElement(a.Form.Item,{name:"enabled",label:t("Enabled"),valuePropName:"checked"},l().createElement(a.Switch,null)))))}},650:function(e,t,r){r.d(t,{k:function(){return a}});var n=r(694),l=JSON.parse('{"UU":"plugin-ai-api"}');function a(){var e=(0,n.useFlowEngine)();return function(t){return e.context.t(t,{ns:[l.UU,"client"]})}}},630:function(e,t,r){function n(e,t){var r,n;return e&&(void 0===e?"undefined":e&&"u">typeof Symbol&&e.constructor===Symbol?"symbol":typeof e)=="object"?null!=(r=null==(n=e.data)?void 0:n.data)?r:t:t}function l(e){var t;return(null!=(t=Error)&&"u">typeof Symbol&&t[Symbol.hasInstance]?!!t[Symbol.hasInstance](e):e instanceof t)?e.message:String(e)}r.d(t,{g:function(){return l},m:function(){return n}})}}]);
|
|
@@ -1,10 +0,0 @@
|
|
|
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
|
-
"use strict";(self.webpackChunkplugin_ai_api=self.webpackChunkplugin_ai_api||[]).push([["302"],{581:function(e,t,r){r.r(t),r.d(t,{default:function(){return d}});var n=r(155),a=r.n(n),l=r(59),o=r(694),i=r(650),u=r(630);function c(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=Array(t);r<t;r++)n[r]=e[r];return n}function s(e,t,r,n,a,l,o){try{var i=e[l](o),u=i.value}catch(e){r(e);return}i.done?t(u):Promise.resolve(u).then(n,a)}function m(e){return function(){var t=this,r=arguments;return new Promise(function(n,a){var l=e.apply(t,r);function o(e){s(l,n,a,o,i,"next",e)}function i(e){s(l,n,a,o,i,"throw",e)}o(void 0)})}}function p(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){var r,n,a=null==e?null:"u">typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(null!=a){var l=[],o=!0,i=!1;try{for(a=a.call(e);!(o=(r=a.next()).done)&&(l.push(r.value),!t||l.length!==t);o=!0);}catch(e){i=!0,n=e}finally{try{o||null==a.return||a.return()}finally{if(i)throw n}}return l}}(e,t)||function(e,t){if(e){if("string"==typeof e)return c(e,t);var r=Object.prototype.toString.call(e).slice(8,-1);if("Object"===r&&e.constructor&&(r=e.constructor.name),"Map"===r||"Set"===r)return Array.from(r);if("Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r))return c(e,t)}}(e,t)||function(){throw TypeError("Invalid attempt to destructure non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function f(e,t){var r,n,a,l={label:0,sent:function(){if(1&a[0])throw a[1];return a[1]},trys:[],ops:[]},o=Object.create(("function"==typeof Iterator?Iterator:Object).prototype),i=Object.defineProperty;return i(o,"next",{value:u(0)}),i(o,"throw",{value:u(1)}),i(o,"return",{value:u(2)}),"function"==typeof Symbol&&i(o,Symbol.iterator,{value:function(){return this}}),o;function u(i){return function(u){var c=[i,u];if(r)throw TypeError("Generator is already executing.");for(;o&&(o=0,c[0]&&(l=0)),l;)try{if(r=1,n&&(a=2&c[0]?n.return:c[0]?n.throw||((a=n.return)&&a.call(n),0):n.next)&&!(a=a.call(n,c[1])).done)return a;switch(n=0,a&&(c=[2&c[0],a.value]),c[0]){case 0:case 1:a=c;break;case 4:return l.label++,{value:c[1],done:!1};case 5:l.label++,n=c[1],c=[0];continue;case 7:c=l.ops.pop(),l.trys.pop();continue;default:if(!(a=(a=l.trys).length>0&&a[a.length-1])&&(6===c[0]||2===c[0])){l=0;continue}if(3===c[0]&&(!a||c[1]>a[0]&&c[1]<a[3])){l.label=c[1];break}if(6===c[0]&&l.label<a[1]){l.label=a[1],a=c;break}if(a&&l.label<a[2]){l.label=a[2],l.ops.push(c);break}a[2]&&l.ops.pop(),l.trys.pop();continue}c=t.call(e,l)}catch(e){c=[6,e],n=0}finally{r=a=0}if(5&c[0])throw c[1];return{value:c[0]?c[1]:void 0,done:!0}}}}var y={mode:"llm",enabledLlmServices:[],rateLimitPerMinute:60,maxRequestBodyMb:10,quotaEnabled:!1,defaultReservationOutputTokens:4096};function d(){var e=(0,o.useFlowContext)(),t=(0,i.k)(),r=p(l.Form.useForm(),1)[0],c=l.Form.useWatch("mode",r),s=p((0,n.useState)(!0),2),d=s[0],b=s[1],h=p((0,n.useState)(!1),2),v=h[0],g=h[1],E=p((0,n.useState)([]),2),S=E[0],w=E[1],I=p((0,n.useState)([]),2),k=I[0],F=I[1],P=p((0,n.useState)(),2),A=P[0],C=P[1],O=(0,n.useCallback)(function(){return m(function(){var t,n,a,l,o;return f(this,function(i){switch(i.label){case 0:b(!0),C(void 0),i.label=1;case 1:return i.trys.push([1,3,4,5]),[4,Promise.all([e.api.request({url:"aiApiConfig:get",method:"get"}),e.api.request({url:"ai:listLLMServices",method:"get"}),e.api.request({url:"aiEmployees:list",method:"get",params:{paginate:!1}})])];case 2:return n=(t=p.apply(void 0,[i.sent(),3]))[0],a=t[1],l=t[2],r.setFieldsValue(function(e){for(var t=1;t<arguments.length;t++){var r=null!=arguments[t]?arguments[t]:{},n=Object.keys(r);"function"==typeof Object.getOwnPropertySymbols&&(n=n.concat(Object.getOwnPropertySymbols(r).filter(function(e){return Object.getOwnPropertyDescriptor(r,e).enumerable}))),n.forEach(function(t){var n;n=r[t],t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n})}return e}({},y,(0,u.m)(n,{}))),w((0,u.m)(a,[])),F((0,u.m)(l,[])),[3,5];case 3:return o=i.sent(),C((0,u.g)(o)),[3,5];case 4:return b(!1),[7];case 5:return[2]}})})()},[e.api,r]);(0,n.useEffect)(function(){O()},[O]);var x=S.map(function(e){return{label:e.title||e.name,value:e.name}}),L=k.map(function(e){return{label:e.nickname?"".concat(e.nickname," (").concat(e.username,")"):e.username,value:e.username}}),T="".concat(window.location.origin,"/api/ai-llm/v1");return a().createElement(l.Card,{title:t("Configuration"),loading:d},A?a().createElement(l.Alert,{type:"error",showIcon:!0,message:A,style:{marginBottom:16}}):null,a().createElement(l.Form,{form:r,layout:"vertical",style:{maxWidth:720},initialValues:y},a().createElement(l.Form.Item,{name:"mode",label:t("API mode"),rules:[{required:!0}]},a().createElement(l.Select,{options:[{label:t("Direct LLM"),value:"llm"},{label:t("AI Employee agent"),value:"agent"}]})),a().createElement(l.Form.Item,{name:"defaultLlmService",label:t("Default LLM service")},a().createElement(l.Select,{allowClear:!0,showSearch:!0,optionFilterProp:"label",options:x})),a().createElement(l.Form.Item,{name:"enabledLlmServices",label:t("Enabled LLM Services")},a().createElement(l.Select,{mode:"multiple",allowClear:!0,showSearch:!0,optionFilterProp:"label",options:x})),"agent"===c?a().createElement(l.Form.Item,{name:"defaultAiEmployee",label:t("Default AI Employee")},a().createElement(l.Select,{allowClear:!0,showSearch:!0,optionFilterProp:"label",placeholder:t("Select an AI Employee"),options:L})):null,a().createElement(l.Form.Item,{name:"rateLimitPerMinute",label:t("Rate Limit"),rules:[{required:!0}]},a().createElement(l.InputNumber,{min:1,style:{width:"100%"}})),a().createElement(l.Form.Item,{name:"maxRequestBodyMb",label:t("Max request body size (MB)"),extra:t("Raise this to accept inline base64 images. Base64 adds about 33% to the original file size."),rules:[{required:!0}]},a().createElement(l.InputNumber,{min:1,max:100,precision:0,style:{width:"100%"}})),a().createElement(l.Form.Item,{name:"quotaEnabled",label:t("Enable user quotas"),valuePropName:"checked"},a().createElement(l.Switch,null)),a().createElement(l.Form.Item,{name:"defaultReservationOutputTokens",label:t("Default reserved output tokens"),rules:[{required:!0}]},a().createElement(l.InputNumber,{min:1,style:{width:"100%"}})),a().createElement(l.Space,null,a().createElement(l.Button,{type:"primary",loading:v,onClick:function(){return m(function(){var n,a;return f(this,function(o){switch(o.label){case 0:return[4,r.validateFields()];case 1:n=o.sent(),g(!0),o.label=2;case 2:return o.trys.push([2,4,5,6]),[4,e.api.request({url:"aiApiConfig:save",method:"post",data:n})];case 3:return o.sent(),l.message.success(t("Configuration saved")),[3,6];case 4:return a=o.sent(),l.message.error("".concat(t("Failed to save configuration"),": ").concat((0,u.g)(a))),[3,6];case 5:return g(!1),[7];case 6:return[2]}})})()}},t("Save Configuration")),a().createElement(l.Button,{onClick:O},t("Refresh")))),a().createElement(l.Card,{title:t("Usage guide"),size:"small",style:{marginTop:24}},a().createElement(l.Alert,{type:"info",showIcon:!0,message:t("OpenAI-compatible endpoint"),description:a().createElement(l.Space,{direction:"vertical",size:4},a().createElement(l.Typography.Text,null,t("Base URL")),a().createElement(l.Typography.Text,{code:!0,copyable:!0},T),a().createElement(l.Typography.Text,null,t("Use a NocoBase API key as the Bearer token."))),style:{marginBottom:16}}),a().createElement(l.Typography.Paragraph,null,t("List available models")),a().createElement(l.Typography.Paragraph,{code:!0,copyable:!0},"curl ".concat(T,'/models -H "Authorization: Bearer <your-api-key>"')),a().createElement(l.Typography.Paragraph,null,t("Send a chat completion")),a().createElement(l.Typography.Paragraph,{code:!0,copyable:!0},"curl ".concat(T,'/chat/completions \\\n -H "Authorization: Bearer <your-api-key>" \\\n -H "Content-Type: application/json" \\\n -d \'{"model":"<service>/<model>","messages":[{"role":"user","content":"Hello"}]}\''))))}},650:function(e,t,r){r.d(t,{k:function(){return l}});var n=r(694),a=JSON.parse('{"UU":"plugin-ai-api"}');function l(){var e=(0,n.useFlowEngine)();return function(t){return e.context.t(t,{ns:[a.UU,"client"]})}}},630:function(e,t,r){function n(e,t){var r,n;return e&&(void 0===e?"undefined":e&&"u">typeof Symbol&&e.constructor===Symbol?"symbol":typeof e)=="object"?null!=(r=null==(n=e.data)?void 0:n.data)?r:t:t}function a(e){var t;return(null!=(t=Error)&&"u">typeof Symbol&&t[Symbol.hasInstance]?!!t[Symbol.hasInstance](e):e instanceof t)?e.message:String(e)}r.d(t,{g:function(){return a},m:function(){return n}})}}]);
|