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.
Files changed (130) hide show
  1. package/dist/client/{286.01c0e3c5fff3cccb.js → 286.a1ee0420172cd5de.js} +1 -1
  2. package/dist/client/302.fbc46ebf5bf300d7.js +10 -0
  3. package/dist/client/562.44b16aad4718b4c7.js +10 -0
  4. package/dist/client/685.ae483e17b6b49c98.js +10 -0
  5. package/dist/client/757.6568d3504ad29352.js +10 -0
  6. package/dist/client/{97.72979a11a067a7c9.js → 97.9b6b2d2b01a4c060.js} +1 -1
  7. package/dist/client/index.js +1 -1
  8. package/dist/client-v2/302.3971233415999b2c.js +10 -0
  9. package/dist/client-v2/562.45d5c504433be38b.js +10 -0
  10. package/dist/client-v2/685.1030370b309b7d4b.js +10 -0
  11. package/dist/client-v2/757.f2bc9cfba07004b0.js +10 -0
  12. package/dist/client-v2/{952.94100128b7757f56.js → 952.f0249eddc153bde1.js} +1 -1
  13. package/dist/client-v2/{97.29c663318eebbd57.js → 97.36a42eff36bb3d8a.js} +1 -1
  14. package/dist/client-v2/index.js +1 -1
  15. package/dist/constants.js +2 -5
  16. package/dist/externalVersion.js +8 -8
  17. package/dist/locale/en-US.json +27 -8
  18. package/dist/locale/vi-VN.json +27 -8
  19. package/dist/locale/zh-CN.json +27 -8
  20. package/dist/server/billing.js +31 -33
  21. package/dist/server/collections/ai-api-config.js +7 -7
  22. package/dist/server/collections/ai-api-group-members.js +62 -0
  23. package/dist/server/collections/ai-api-group-quota-buckets.js +63 -0
  24. package/dist/server/collections/ai-api-model-metadata.js +6 -0
  25. package/dist/server/collections/ai-api-usage-groups.js +74 -0
  26. package/dist/server/collections/ai-api-usage-records.js +2 -0
  27. package/dist/server/middleware/rate-limit.js +7 -6
  28. package/dist/server/migrations/20260813000000-add-prompt-cache-tokens.js +69 -0
  29. package/dist/server/migrations/20260815000000-add-usage-groups.js +149 -0
  30. package/dist/server/migrations/20260816000000-migrate-user-permissions-to-groups.js +169 -0
  31. package/dist/server/migrations/20260816100000-add-model-metadata-system-prompt.js +69 -0
  32. package/dist/server/plugin.js +100 -22
  33. package/dist/server/quota-groups.js +108 -0
  34. package/dist/server/resource/ai-api-config.js +5 -3
  35. package/dist/server/resource/ai-api-usage-groups.js +168 -0
  36. package/dist/server/resource/ai-api-usage-monitor.js +3 -1
  37. package/dist/server/routes/agent-completions.js +2 -1
  38. package/dist/server/routes/chat-completions.js +121 -42
  39. package/dist/server/routes/completions.js +48 -29
  40. package/dist/server/routes/embeddings.js +2 -1
  41. package/dist/server/routes/models.js +2 -1
  42. package/dist/server/routes/router.js +3 -2
  43. package/dist/server/services/file-processor.js +426 -0
  44. package/dist/server/usage.js +37 -3
  45. package/dist/server/utils/direct-llm-context.js +163 -26
  46. package/dist/server/utils/openai-format.js +21 -2
  47. package/dist/server/utils/rate-limiter.js +1 -1
  48. package/dist/server/utils/request-cache.js +61 -0
  49. package/dist/server/utils/resolve-service.js +2 -1
  50. package/dist/server/utils/user-permissions.js +25 -39
  51. package/dist/server/validation.js +7 -0
  52. package/dist/swagger.js +48 -10
  53. package/package.json +1 -1
  54. package/src/client/__tests__/settings-registration.test.tsx +6 -29
  55. package/src/client/plugin.tsx +5 -16
  56. package/src/client-v2/__tests__/settings-registration.test.tsx +6 -32
  57. package/src/client-v2/locale.ts +3 -1
  58. package/src/client-v2/pages/GeneralPage.tsx +0 -5
  59. package/src/client-v2/pages/ModelMetadataPage.tsx +20 -1
  60. package/src/client-v2/pages/UsageGroupsPage.tsx +548 -0
  61. package/src/client-v2/pages/UsagePage.tsx +9 -0
  62. package/src/client-v2/plugin.tsx +4 -13
  63. package/src/constants.ts +0 -7
  64. package/src/locale/en-US.json +27 -8
  65. package/src/locale/vi-VN.json +27 -8
  66. package/src/locale/zh-CN.json +27 -8
  67. package/src/server/__tests__/billing-quota.test.ts +28 -9
  68. package/src/server/__tests__/direct-llm-context.test.ts +209 -10
  69. package/src/server/__tests__/file-processor.test.ts +225 -0
  70. package/src/server/__tests__/models.test.ts +1 -1
  71. package/src/server/__tests__/openai-format.test.ts +12 -2
  72. package/src/server/__tests__/permission-sync.test.ts +34 -35
  73. package/src/server/__tests__/request-body.test.ts +45 -2
  74. package/src/server/__tests__/usage-groups.test.ts +160 -0
  75. package/src/server/__tests__/usage-monitor.test.ts +2 -0
  76. package/src/server/__tests__/usage-route.test.ts +382 -5
  77. package/src/server/__tests__/usage.test.ts +57 -0
  78. package/src/server/__tests__/user-permissions.test.ts +214 -133
  79. package/src/server/__tests__/validation.test.ts +11 -0
  80. package/src/server/billing.ts +36 -39
  81. package/src/server/collections/ai-api-config.ts +9 -7
  82. package/src/server/collections/ai-api-group-members.ts +41 -0
  83. package/src/server/collections/ai-api-group-quota-buckets.ts +42 -0
  84. package/src/server/collections/ai-api-model-metadata.ts +7 -0
  85. package/src/server/collections/ai-api-role-permissions.ts +41 -41
  86. package/src/server/collections/ai-api-usage-groups.ts +53 -0
  87. package/src/server/collections/ai-api-usage-records.ts +2 -0
  88. package/src/server/index.ts +10 -10
  89. package/src/server/middleware/rate-limit.ts +68 -70
  90. package/src/server/migrations/20260813000000-add-prompt-cache-tokens.ts +46 -0
  91. package/src/server/migrations/20260815000000-add-usage-groups.ts +147 -0
  92. package/src/server/migrations/20260816000000-migrate-user-permissions-to-groups.ts +190 -0
  93. package/src/server/migrations/20260816100000-add-model-metadata-system-prompt.ts +46 -0
  94. package/src/server/plugin.ts +121 -30
  95. package/src/server/quota-groups.ts +117 -0
  96. package/src/server/resource/ai-api-config.ts +5 -3
  97. package/src/server/resource/ai-api-usage-groups.ts +171 -0
  98. package/src/server/resource/ai-api-usage-monitor.ts +3 -0
  99. package/src/server/routes/agent-completions.ts +2 -1
  100. package/src/server/routes/chat-completions.ts +173 -47
  101. package/src/server/routes/completions.ts +50 -27
  102. package/src/server/routes/embeddings.ts +2 -1
  103. package/src/server/routes/models.ts +4 -3
  104. package/src/server/routes/router.ts +4 -3
  105. package/src/server/services/__tests__/file-processor.test.ts +184 -0
  106. package/src/server/services/file-processor.ts +513 -0
  107. package/src/server/usage.ts +51 -1
  108. package/src/server/utils/direct-llm-context.ts +218 -31
  109. package/src/server/utils/openai-format.ts +25 -2
  110. package/src/server/utils/rate-limiter.ts +83 -83
  111. package/src/server/utils/request-cache.ts +59 -0
  112. package/src/server/utils/resolve-service.ts +83 -82
  113. package/src/server/utils/user-permissions.ts +49 -69
  114. package/src/server/validation.ts +7 -0
  115. package/src/swagger.ts +52 -11
  116. package/dist/client/123.e6fe04c856ce6417.js +0 -10
  117. package/dist/client/302.fc3a3491b4ec2dfd.js +0 -10
  118. package/dist/client/562.17a0a299d2e5152c.js +0 -10
  119. package/dist/client/757.a01403fb7a1bea01.js +0 -10
  120. package/dist/client/902.e74518750f1e4201.js +0 -10
  121. package/dist/client-v2/123.05f1f649923f93eb.js +0 -10
  122. package/dist/client-v2/302.d27fe4ea9b0b3bf5.js +0 -10
  123. package/dist/client-v2/562.fb2948ee6402de95.js +0 -10
  124. package/dist/client-v2/757.a117ce1cf7119cea.js +0 -10
  125. package/dist/client-v2/902.c7c00a565085438a.js +0 -10
  126. package/dist/server/resource/ai-api-user-permissions.js +0 -75
  127. package/src/client-v2/pages/UserPermissionsPage.tsx +0 -322
  128. package/src/client-v2/pages/UserQuotasPage.tsx +0 -276
  129. package/src/server/__tests__/user-permissions-resource.test.ts +0 -66
  130. package/src/server/resource/ai-api-user-permissions.ts +0 -76
@@ -0,0 +1,74 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ var __defProp = Object.defineProperty;
11
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
+ var __getOwnPropNames = Object.getOwnPropertyNames;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __export = (target, all) => {
15
+ for (var name in all)
16
+ __defProp(target, name, { get: all[name], enumerable: true });
17
+ };
18
+ var __copyProps = (to, from, except, desc) => {
19
+ if (from && typeof from === "object" || typeof from === "function") {
20
+ for (let key of __getOwnPropNames(from))
21
+ if (!__hasOwnProp.call(to, key) && key !== except)
22
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
23
+ }
24
+ return to;
25
+ };
26
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
27
+ var ai_api_usage_groups_exports = {};
28
+ __export(ai_api_usage_groups_exports, {
29
+ default: () => ai_api_usage_groups_default
30
+ });
31
+ module.exports = __toCommonJS(ai_api_usage_groups_exports);
32
+ var import_database = require("@nocobase/database");
33
+ var ai_api_usage_groups_default = (0, import_database.defineCollection)({
34
+ name: "aiApiUsageGroups",
35
+ autoGenId: true,
36
+ fields: [
37
+ { name: "name", type: "string", allowNull: false },
38
+ { name: "isDefault", type: "boolean", defaultValue: false, index: true },
39
+ { name: "quotaMode", type: "string", allowNull: false, defaultValue: "per_user" },
40
+ { name: "rateLimitPerMinute", type: "integer", allowNull: false, defaultValue: 60 },
41
+ { name: "enabled", type: "boolean", defaultValue: true, index: true },
42
+ { name: "periodType", type: "string", allowNull: false, defaultValue: "monthly" },
43
+ { name: "timezone", type: "string", allowNull: false, defaultValue: "UTC" },
44
+ { name: "requestLimit", type: "bigInt", allowNull: true },
45
+ { name: "totalTokenLimit", type: "bigInt", allowNull: true },
46
+ { name: "costLimit", type: "decimal", precision: 20, scale: 8, allowNull: true },
47
+ { name: "currency", type: "string", allowNull: false, defaultValue: "USD" },
48
+ { name: "rejectUnpricedModel", type: "boolean", defaultValue: true },
49
+ { name: "missingUsageBehavior", type: "string", allowNull: false, defaultValue: "use_reserved" },
50
+ { name: "contextOverflowBehavior", type: "string", allowNull: false, defaultValue: "reject" },
51
+ // Model access: empty lists mean "no narrowing" — the group inherits the full
52
+ // global configuration. Non-empty lists narrow what members may use.
53
+ {
54
+ name: "allowedLlmServices",
55
+ type: "json",
56
+ defaultValue: [],
57
+ comment: "Empty means all globally enabled services; otherwise narrows to these services."
58
+ },
59
+ { name: "allowAllModels", type: "boolean", defaultValue: true },
60
+ {
61
+ name: "allowedModels",
62
+ type: "json",
63
+ defaultValue: [],
64
+ comment: 'Array of "serviceName/modelId" members may use (when allowAllModels=false).'
65
+ }
66
+ ],
67
+ indexes: [
68
+ {
69
+ fields: ["isDefault"],
70
+ unique: true,
71
+ where: { isDefault: true }
72
+ }
73
+ ]
74
+ });
@@ -62,11 +62,13 @@ var ai_api_usage_records_default = (0, import_database.defineCollection)({
62
62
  { name: "inputTokens", type: "integer", allowNull: true },
63
63
  { name: "outputTokens", type: "integer", allowNull: true },
64
64
  { name: "totalTokens", type: "integer", allowNull: true },
65
+ { name: "promptCacheTokens", type: "integer", allowNull: true },
65
66
  { name: "estimatedCost", type: "decimal", allowNull: true, precision: 20, scale: 8 },
66
67
  { name: "currency", type: "string", allowNull: true },
67
68
  { name: "costStatus", type: "string", allowNull: true, index: true },
68
69
  { name: "modelPriceId", type: "bigInt", allowNull: true, index: true },
69
70
  { name: "quotaPolicyId", type: "bigInt", allowNull: true, index: true },
71
+ { name: "groupId", type: "bigInt", allowNull: true, index: true },
70
72
  { name: "inputPricePerMillionTokens", type: "decimal", allowNull: true, precision: 20, scale: 10 },
71
73
  { name: "outputPricePerMillionTokens", type: "decimal", allowNull: true, precision: 20, scale: 10 },
72
74
  { name: "fixedCostPerRequest", type: "decimal", allowNull: true, precision: 20, scale: 10 },
@@ -30,6 +30,7 @@ __export(rate_limit_exports, {
30
30
  });
31
31
  module.exports = __toCommonJS(rate_limit_exports);
32
32
  var import_openai_format = require("../utils/openai-format");
33
+ var import_request_cache = require("../utils/request-cache");
33
34
  function createRateLimitMiddleware(limiter) {
34
35
  return async (ctx) => {
35
36
  var _a, _b, _c;
@@ -37,13 +38,13 @@ function createRateLimitMiddleware(limiter) {
37
38
  if (userId === void 0 || userId === null) return true;
38
39
  let limit = 60;
39
40
  try {
40
- const config = await ctx.db.getRepository("aiApiConfig").findOne();
41
- const configLimit = config == null ? void 0 : config.rateLimitPerMinute;
42
- if (configLimit && configLimit > 0) {
43
- limit = configLimit;
41
+ const group = await (0, import_request_cache.resolveRequestUserGroup)(ctx, userId);
42
+ const groupLimit = group == null ? void 0 : group.rateLimitPerMinute;
43
+ if (groupLimit && groupLimit > 0) {
44
+ limit = groupLimit;
44
45
  }
45
- } catch (configErr) {
46
- (_c = (_b = ctx.app) == null ? void 0 : _b.logger) == null ? void 0 : _c.warn("[ai-api] Rate limit config read failed, using default (60/min)", configErr);
46
+ } catch (err) {
47
+ (_c = (_b = ctx.app) == null ? void 0 : _b.logger) == null ? void 0 : _c.warn("[ai-api] Rate limit group resolution failed, using default (60/min)", err);
47
48
  }
48
49
  const result = limiter.check(userId, limit);
49
50
  if (!result.allowed) {
@@ -0,0 +1,69 @@
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 add_prompt_cache_tokens_exports = {};
28
+ __export(add_prompt_cache_tokens_exports, {
29
+ default: () => AddPromptCacheTokensToUsageRecords
30
+ });
31
+ module.exports = __toCommonJS(add_prompt_cache_tokens_exports);
32
+ var import_server = require("@nocobase/server");
33
+ class AddPromptCacheTokensToUsageRecords extends import_server.Migration {
34
+ on = "beforeLoad";
35
+ async up() {
36
+ const collection = this.db.getCollection("aiApiUsageRecords");
37
+ if (!collection) return;
38
+ const field = collection.getField("promptCacheTokens");
39
+ if (!field) {
40
+ collection.addField("promptCacheTokens", { type: "integer", allowNull: true });
41
+ }
42
+ if (await collection.existsInDb()) {
43
+ const tableName = collection.getTableNameWithSchema();
44
+ const exists = await this.tableColumnExists(tableName, "promptCacheTokens");
45
+ if (!exists) {
46
+ await this.queryInterface.addColumn(tableName, "promptCacheTokens", {
47
+ type: this.db.sequelize.getDialect() === "sqlite" ? "INTEGER" : "INTEGER",
48
+ allowNull: true
49
+ });
50
+ }
51
+ }
52
+ }
53
+ async down() {
54
+ const collection = this.db.getCollection("aiApiUsageRecords");
55
+ if (!collection) return;
56
+ if (await collection.existsInDb()) {
57
+ const tableName = collection.getTableNameWithSchema();
58
+ const exists = await this.tableColumnExists(tableName, "promptCacheTokens");
59
+ if (exists) {
60
+ await this.queryInterface.removeColumn(tableName, "promptCacheTokens");
61
+ }
62
+ }
63
+ collection.removeField("promptCacheTokens");
64
+ }
65
+ async tableColumnExists(tableName, columnName) {
66
+ const columns = await this.queryInterface.describeTable(tableName);
67
+ return Object.prototype.hasOwnProperty.call(columns, columnName);
68
+ }
69
+ }
@@ -0,0 +1,149 @@
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 add_usage_groups_exports = {};
28
+ __export(add_usage_groups_exports, {
29
+ default: () => AddUsageGroups
30
+ });
31
+ module.exports = __toCommonJS(add_usage_groups_exports);
32
+ var import_server = require("@nocobase/server");
33
+ class AddUsageGroups extends import_server.Migration {
34
+ // Must run afterSync: plugin collections are only registered once the app has
35
+ // loaded, and the new group tables must exist before seeding.
36
+ on = "afterSync";
37
+ async up() {
38
+ const groupCollection = this.getGroupCollection();
39
+ if (!groupCollection) {
40
+ throw new Error("AI API usage groups migration could not resolve the aiApiUsageGroups collection.");
41
+ }
42
+ const configCollection = this.db.getCollection("aiApiConfig");
43
+ let rateLimitPerMinute = 60;
44
+ let quotaEnabled = false;
45
+ if (configCollection && await configCollection.existsInDb()) {
46
+ const tableName = configCollection.getTableNameWithSchema();
47
+ const columns = await this.queryInterface.describeTable(tableName);
48
+ if (columns.rateLimitPerMinute) {
49
+ const legacyRateLimit = await this.readLegacyRateLimit(configCollection.quotedTableName());
50
+ if (legacyRateLimit && legacyRateLimit > 0) {
51
+ rateLimitPerMinute = legacyRateLimit;
52
+ }
53
+ }
54
+ const config = await this.db.getRepository("aiApiConfig").findOne();
55
+ if (config) {
56
+ quotaEnabled = !!config.get("quotaEnabled");
57
+ }
58
+ }
59
+ const existingDefault = await this.db.getRepository("aiApiUsageGroups").findOne({
60
+ filter: { isDefault: true }
61
+ });
62
+ if (!existingDefault) {
63
+ await this.db.getRepository("aiApiUsageGroups").create({
64
+ values: {
65
+ name: "Default",
66
+ isDefault: true,
67
+ quotaMode: "per_user",
68
+ rateLimitPerMinute,
69
+ enabled: quotaEnabled,
70
+ periodType: "monthly",
71
+ timezone: "UTC",
72
+ requestLimit: null,
73
+ totalTokenLimit: null,
74
+ costLimit: null,
75
+ currency: "USD",
76
+ rejectUnpricedModel: true,
77
+ missingUsageBehavior: "use_reserved",
78
+ contextOverflowBehavior: "reject"
79
+ }
80
+ });
81
+ }
82
+ if (configCollection && await configCollection.existsInDb()) {
83
+ const tableName = configCollection.getTableNameWithSchema();
84
+ const columns = await this.queryInterface.describeTable(tableName);
85
+ if (columns.rateLimitPerMinute) {
86
+ await this.queryInterface.removeColumn(tableName, "rateLimitPerMinute");
87
+ }
88
+ }
89
+ await this.migrateLegacyQuotaPolicies();
90
+ }
91
+ async down() {
92
+ const configCollection = this.db.getCollection("aiApiConfig");
93
+ if (configCollection && await configCollection.existsInDb()) {
94
+ const tableName = configCollection.getTableNameWithSchema();
95
+ const columns = await this.queryInterface.describeTable(tableName);
96
+ if (!columns.rateLimitPerMinute) {
97
+ await this.queryInterface.addColumn(tableName, "rateLimitPerMinute", {
98
+ type: "INTEGER",
99
+ allowNull: false,
100
+ defaultValue: 60
101
+ });
102
+ }
103
+ }
104
+ }
105
+ getGroupCollection() {
106
+ return this.db.getCollection("aiApiUsageGroups");
107
+ }
108
+ async readLegacyRateLimit(quotedTable) {
109
+ const qi = this.db.sequelize.getQueryInterface();
110
+ const [rows] = await this.db.sequelize.query(
111
+ `SELECT ${qi.quoteIdentifier("rateLimitPerMinute")} FROM ${quotedTable} LIMIT 1`
112
+ );
113
+ const first = rows == null ? void 0 : rows[0];
114
+ const value = Number(first == null ? void 0 : first.rateLimitPerMinute);
115
+ return Number.isFinite(value) ? value : void 0;
116
+ }
117
+ async migrateLegacyQuotaPolicies() {
118
+ const defaultGroup = await this.db.getRepository("aiApiUsageGroups").findOne({
119
+ filter: { isDefault: true }
120
+ });
121
+ if (!defaultGroup) return;
122
+ const policyCollection = this.db.getCollection("aiApiUserQuotaPolicies");
123
+ const memberCollection = this.db.getCollection("aiApiGroupMembers");
124
+ const bucketCollection = this.db.getCollection("aiApiGroupQuotaBuckets");
125
+ if (!policyCollection || !memberCollection || !bucketCollection) return;
126
+ const policies = await this.db.getRepository("aiApiUserQuotaPolicies").find({
127
+ pageSize: 1e3
128
+ });
129
+ for (const policy of policies) {
130
+ const userId = this.valueOf(policy, "userId");
131
+ if (userId === void 0 || userId === null) continue;
132
+ const existingMember = await this.db.getRepository("aiApiGroupMembers").findOne({
133
+ filter: { userId }
134
+ });
135
+ if (!existingMember) {
136
+ await this.db.getRepository("aiApiGroupMembers").create({
137
+ values: { groupId: this.valueOf(defaultGroup, "id"), userId }
138
+ });
139
+ }
140
+ }
141
+ }
142
+ valueOf(model, name) {
143
+ if (!model) return void 0;
144
+ if (typeof model.get === "function") {
145
+ return model.get(name);
146
+ }
147
+ return model[name];
148
+ }
149
+ }
@@ -0,0 +1,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
+ 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 migrate_user_permissions_to_groups_exports = {};
28
+ __export(migrate_user_permissions_to_groups_exports, {
29
+ default: () => MigrateUserPermissionsToGroups
30
+ });
31
+ module.exports = __toCommonJS(migrate_user_permissions_to_groups_exports);
32
+ var import_server = require("@nocobase/server");
33
+ class MigrateUserPermissionsToGroups extends import_server.Migration {
34
+ // Must run afterSync: the new allowedLlmServices/allowAllModels/allowedModels
35
+ // columns on aiApiUsageGroups only exist once collections have been synced.
36
+ on = "afterSync";
37
+ async up() {
38
+ const collection = this.db.getCollection("aiApiUserPermissions");
39
+ if (!collection || !await collection.existsInDb()) return;
40
+ const repository = this.db.getRepository("aiApiUserPermissions");
41
+ const rows = [];
42
+ for (let page = 1; ; page += 1) {
43
+ const batch = await repository.find({ page, pageSize: 1e3, sort: "id" });
44
+ rows.push(...batch);
45
+ if (batch.length < 1e3) break;
46
+ }
47
+ if (!rows.length) return;
48
+ const migratable = [];
49
+ const deniedUserIds = [];
50
+ for (const row of rows) {
51
+ const userId = this.valueOf(row, "userId");
52
+ if (userId === void 0 || userId === null) continue;
53
+ const enabled = this.valueOf(row, "enabled") !== false;
54
+ const allowedLlmServices = this.stringArray(this.valueOf(row, "allowedLlmServices"));
55
+ const allowAllModels = this.valueOf(row, "allowAllModels") !== false;
56
+ const allowedModels = this.stringArray(this.valueOf(row, "allowedModels"));
57
+ const hasEffectiveGrant = allowedLlmServices.length > 0 && (allowAllModels || allowedModels.length > 0);
58
+ if (!enabled || !hasEffectiveGrant) {
59
+ deniedUserIds.push(userId);
60
+ continue;
61
+ }
62
+ migratable.push({ userId, sig: { allowedLlmServices, allowAllModels, allowedModels } });
63
+ }
64
+ if (deniedUserIds.length) {
65
+ this.app.logger.warn(
66
+ `[ai-api] ${deniedUserIds.length} aiApiUserPermissions row(s) denied all access (enabled=false or empty grant) and cannot be migrated to usage groups. These users now inherit the default group's access. Affected userIds: ${deniedUserIds.join(", ")}. Use role permissions to block API access entirely.`
67
+ );
68
+ }
69
+ if (!migratable.length) return;
70
+ const defaultGroup = await this.db.getRepository("aiApiUsageGroups").findOne({
71
+ filter: { isDefault: true }
72
+ });
73
+ const quotaValues = this.quotaValuesFrom(defaultGroup);
74
+ const bySignature = /* @__PURE__ */ new Map();
75
+ for (const { userId, sig } of migratable) {
76
+ const key = JSON.stringify({
77
+ allowedLlmServices: [...sig.allowedLlmServices].sort(),
78
+ allowAllModels: sig.allowAllModels,
79
+ allowedModels: [...sig.allowedModels].sort()
80
+ });
81
+ const entry = bySignature.get(key);
82
+ if (entry) {
83
+ entry.userIds.push(userId);
84
+ } else {
85
+ bySignature.set(key, { sig, userIds: [userId] });
86
+ }
87
+ }
88
+ let index = 0;
89
+ for (const { sig, userIds } of bySignature.values()) {
90
+ index += 1;
91
+ const group = await this.db.getRepository("aiApiUsageGroups").create({
92
+ values: {
93
+ ...quotaValues,
94
+ name: `Migrated permissions ${index}`,
95
+ isDefault: false,
96
+ quotaMode: "per_user",
97
+ allowedLlmServices: sig.allowedLlmServices,
98
+ allowAllModels: sig.allowAllModels,
99
+ allowedModels: sig.allowedModels
100
+ }
101
+ });
102
+ const groupId = this.valueOf(group, "id");
103
+ for (const userId of userIds) {
104
+ const existing = await this.db.getRepository("aiApiGroupMembers").findOne({ filter: { userId } });
105
+ if (existing) {
106
+ await this.db.getRepository("aiApiGroupMembers").update({
107
+ filterByTk: this.valueOf(existing, "id"),
108
+ values: { groupId }
109
+ });
110
+ } else {
111
+ await this.db.getRepository("aiApiGroupMembers").create({ values: { groupId, userId } });
112
+ }
113
+ }
114
+ }
115
+ this.app.logger.info(
116
+ `[ai-api] Migrated ${migratable.length} aiApiUserPermissions row(s) into ${bySignature.size} usage group(s).`
117
+ );
118
+ }
119
+ async down() {
120
+ const groups = await this.db.getRepository("aiApiUsageGroups").find({
121
+ filter: { "name.$startsWith": "Migrated permissions" }
122
+ });
123
+ for (const group of groups) {
124
+ const groupId = this.valueOf(group, "id");
125
+ await this.db.getRepository("aiApiGroupMembers").destroy({ filter: { groupId } });
126
+ await this.db.getRepository("aiApiUsageGroups").destroy({ filterByTk: groupId });
127
+ }
128
+ }
129
+ quotaValuesFrom(defaultGroup) {
130
+ if (!defaultGroup) {
131
+ return {
132
+ rateLimitPerMinute: 60,
133
+ enabled: false,
134
+ periodType: "monthly",
135
+ timezone: "UTC",
136
+ requestLimit: null,
137
+ totalTokenLimit: null,
138
+ costLimit: null,
139
+ currency: "USD",
140
+ rejectUnpricedModel: true,
141
+ missingUsageBehavior: "use_reserved",
142
+ contextOverflowBehavior: "reject"
143
+ };
144
+ }
145
+ return {
146
+ rateLimitPerMinute: Number(this.valueOf(defaultGroup, "rateLimitPerMinute") ?? 60),
147
+ enabled: !!this.valueOf(defaultGroup, "enabled"),
148
+ periodType: this.valueOf(defaultGroup, "periodType") ?? "monthly",
149
+ timezone: this.valueOf(defaultGroup, "timezone") ?? "UTC",
150
+ requestLimit: this.valueOf(defaultGroup, "requestLimit") ?? null,
151
+ totalTokenLimit: this.valueOf(defaultGroup, "totalTokenLimit") ?? null,
152
+ costLimit: this.valueOf(defaultGroup, "costLimit") ?? null,
153
+ currency: this.valueOf(defaultGroup, "currency") ?? "USD",
154
+ rejectUnpricedModel: !!this.valueOf(defaultGroup, "rejectUnpricedModel"),
155
+ missingUsageBehavior: this.valueOf(defaultGroup, "missingUsageBehavior") ?? "use_reserved",
156
+ contextOverflowBehavior: this.valueOf(defaultGroup, "contextOverflowBehavior") ?? "reject"
157
+ };
158
+ }
159
+ stringArray(value) {
160
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.length > 0) : [];
161
+ }
162
+ valueOf(model, name) {
163
+ if (!model) return void 0;
164
+ if (typeof model.get === "function") {
165
+ return model.get(name);
166
+ }
167
+ return model[name];
168
+ }
169
+ }
@@ -0,0 +1,69 @@
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 add_model_metadata_system_prompt_exports = {};
28
+ __export(add_model_metadata_system_prompt_exports, {
29
+ default: () => AddSystemPromptToModelMetadata
30
+ });
31
+ module.exports = __toCommonJS(add_model_metadata_system_prompt_exports);
32
+ var import_server = require("@nocobase/server");
33
+ class AddSystemPromptToModelMetadata extends import_server.Migration {
34
+ on = "beforeLoad";
35
+ async up() {
36
+ const collection = this.db.getCollection("aiApiModelMetadata");
37
+ if (!collection) return;
38
+ const field = collection.getField("systemPrompt");
39
+ if (!field) {
40
+ collection.addField("systemPrompt", { type: "text", allowNull: true });
41
+ }
42
+ if (await collection.existsInDb()) {
43
+ const tableName = collection.getTableNameWithSchema();
44
+ const exists = await this.tableColumnExists(tableName, "systemPrompt");
45
+ if (!exists) {
46
+ await this.queryInterface.addColumn(tableName, "systemPrompt", {
47
+ type: "TEXT",
48
+ allowNull: true
49
+ });
50
+ }
51
+ }
52
+ }
53
+ async down() {
54
+ const collection = this.db.getCollection("aiApiModelMetadata");
55
+ if (!collection) return;
56
+ if (await collection.existsInDb()) {
57
+ const tableName = collection.getTableNameWithSchema();
58
+ const exists = await this.tableColumnExists(tableName, "systemPrompt");
59
+ if (exists) {
60
+ await this.queryInterface.removeColumn(tableName, "systemPrompt");
61
+ }
62
+ }
63
+ collection.removeField("systemPrompt");
64
+ }
65
+ async tableColumnExists(tableName, columnName) {
66
+ const columns = await this.queryInterface.describeTable(tableName);
67
+ return Object.prototype.hasOwnProperty.call(columns, columnName);
68
+ }
69
+ }