@southwind-ai/modules 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@southwind-ai/modules",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "engines": {
@@ -12,7 +12,7 @@
12
12
  "!src/**/*.test.ts"
13
13
  ],
14
14
  "dependencies": {
15
- "@southwind-ai/shared": "workspace:*"
15
+ "@southwind-ai/shared": "0.1.2"
16
16
  },
17
17
  "exports": {
18
18
  ".": "./index.ts"
@@ -0,0 +1,145 @@
1
+ import type {
2
+ ModuleAiOperationDefinition,
3
+ ModuleManifest,
4
+ } from "./manifest.js";
5
+
6
+ interface AiOperationCatalogs {
7
+ permissions: ReadonlySet<string>;
8
+ features: ReadonlySet<string>;
9
+ audits: ReadonlySet<string>;
10
+ tools: ReadonlySet<string>;
11
+ }
12
+
13
+ export function validateAiOperations(
14
+ modules: readonly ModuleManifest[],
15
+ catalogs: AiOperationCatalogs,
16
+ ): ModuleAiOperationDefinition[] {
17
+ const operations = modules.flatMap((module) => module.aiOperations || []);
18
+ uniqueValues(
19
+ "AI Operation",
20
+ operations.map(({ key }) => key),
21
+ );
22
+ for (const module of modules) {
23
+ for (const operation of module.aiOperations || []) {
24
+ validateOperation(module.name, operation, catalogs);
25
+ }
26
+ }
27
+ return operations;
28
+ }
29
+
30
+ export function validateAiOperationImplementations(
31
+ declarations: readonly ModuleAiOperationDefinition[],
32
+ implementations: readonly string[] | undefined,
33
+ ): void {
34
+ if (!implementations) return;
35
+ const declared = new Set(declarations.map(({ key }) => key));
36
+ const registered = uniqueValues("AI Operation Handler", implementations);
37
+ for (const key of declared) {
38
+ if (!registered.has(key)) {
39
+ throw new Error(`AI Operation ${key} 缺少 Handler`);
40
+ }
41
+ }
42
+ for (const key of registered) {
43
+ if (!declared.has(key)) {
44
+ throw new Error(`AI Operation Handler ${key} 缺少 Manifest 声明`);
45
+ }
46
+ }
47
+ }
48
+
49
+ function validateOperation(
50
+ moduleName: string,
51
+ operation: ModuleAiOperationDefinition,
52
+ catalogs: AiOperationCatalogs,
53
+ ): void {
54
+ const strings = [
55
+ operation.key,
56
+ operation.featureKey,
57
+ operation.permissionKey,
58
+ operation.auditAction,
59
+ operation.inputSchemaKey,
60
+ operation.outputSchemaKey,
61
+ ];
62
+ if (strings.some((value) => !value.trim())) {
63
+ throw new Error("AI Operation Key 与引用字段不能为空");
64
+ }
65
+ if (!operation.key.startsWith(`${moduleName}.`)) {
66
+ throw new Error(
67
+ `AI Operation ${operation.key} 必须使用模块命名空间 ${moduleName}`,
68
+ );
69
+ }
70
+ requiredReference(
71
+ operation.key,
72
+ "权限",
73
+ operation.permissionKey,
74
+ catalogs.permissions,
75
+ );
76
+ requiredReference(
77
+ operation.key,
78
+ "Feature",
79
+ operation.featureKey,
80
+ catalogs.features,
81
+ );
82
+ requiredReference(
83
+ operation.key,
84
+ "审计动作",
85
+ operation.auditAction,
86
+ catalogs.audits,
87
+ );
88
+ validateCapabilities(operation);
89
+ validateTools(operation, catalogs.tools);
90
+ validateLimits(operation);
91
+ }
92
+
93
+ function validateCapabilities(operation: ModuleAiOperationDefinition): void {
94
+ if (!operation.requiredCapabilities.length) {
95
+ throw new Error(`${operation.key} 至少需要一个 capability`);
96
+ }
97
+ validateNonEmptyValues(
98
+ `${operation.key} capability`,
99
+ operation.requiredCapabilities,
100
+ );
101
+ }
102
+
103
+ function validateTools(
104
+ operation: ModuleAiOperationDefinition,
105
+ tools: ReadonlySet<string>,
106
+ ): void {
107
+ validateNonEmptyValues(`${operation.key} Tool`, operation.allowedToolKeys);
108
+ for (const toolKey of operation.allowedToolKeys) {
109
+ requiredReference(operation.key, "Agent Tool", toolKey, tools);
110
+ }
111
+ }
112
+
113
+ function validateLimits(operation: ModuleAiOperationDefinition): void {
114
+ const values = Object.values(operation.limits);
115
+ if (values.some((value) => !Number.isSafeInteger(value) || value <= 0)) {
116
+ throw new Error(`${operation.key} 运行与输入输出上限必须是正整数`);
117
+ }
118
+ }
119
+
120
+ function validateNonEmptyValues(label: string, values: readonly string[]) {
121
+ if (values.some((value) => !value.trim())) {
122
+ throw new Error(`${label} 不能为空`);
123
+ }
124
+ uniqueValues(label, values);
125
+ }
126
+
127
+ function requiredReference(
128
+ operationKey: string,
129
+ label: string,
130
+ key: string,
131
+ catalog: ReadonlySet<string>,
132
+ ): void {
133
+ if (!catalog.has(key)) {
134
+ throw new Error(`${operationKey} 引用了未注册${label}:${key}`);
135
+ }
136
+ }
137
+
138
+ function uniqueValues(label: string, values: readonly string[]): Set<string> {
139
+ const result = new Set<string>();
140
+ for (const value of values) {
141
+ if (result.has(value)) throw new Error(`${label} Key 重复:${value}`);
142
+ result.add(value);
143
+ }
144
+ return result;
145
+ }
@@ -2,6 +2,9 @@ import {
2
2
  AUDIT_ACTION_DEFINITIONS,
3
3
  isAuditActionDefinition,
4
4
  LICENSE_CATALOG,
5
+ normalizeManifestBundles,
6
+ validateAndOrderBundles,
7
+ validateLicenseCatalog,
5
8
  type AuditActionDefinition,
6
9
  type FeatureFlagDefinition,
7
10
  type LicenseCatalogDefinition,
@@ -15,15 +18,29 @@ export interface PlatformCapabilityCatalogs {
15
18
 
16
19
  export function validateCapabilityCatalogs(
17
20
  modules: readonly ModuleManifest[],
21
+ licenseCatalog: readonly LicenseCatalogDefinition[] = LICENSE_CATALOG,
18
22
  ): PlatformCapabilityCatalogs {
19
23
  const auditActions = composeAuditActionCatalog(modules);
20
- validateFeatureCatalog(modules, LICENSE_CATALOG);
24
+ validateLicenseCatalog(licenseCatalog);
25
+ validateFeatureCatalog(modules, licenseCatalog);
26
+ validateMigrationBundleCatalog(modules);
21
27
  return {
22
28
  auditActions,
23
- licenseVersions: LICENSE_CATALOG,
29
+ licenseVersions: licenseCatalog,
24
30
  };
25
31
  }
26
32
 
33
+ /**
34
+ * 启动期结构校验显式声明 migrationBundle 的模块;无 bundle 的旧式模块
35
+ * 由 drizzle 规范迁移管理,不参与模块级 Bundle 校验。
36
+ */
37
+ export function validateMigrationBundleCatalog(
38
+ modules: readonly ModuleManifest[],
39
+ ): void {
40
+ const bundled = modules.filter((module) => module.migrationBundle);
41
+ validateAndOrderBundles(normalizeManifestBundles(bundled));
42
+ }
43
+
27
44
  export function composeAuditActionCatalog(
28
45
  modules: readonly ModuleManifest[],
29
46
  ): AuditActionDefinition[] {
@@ -3,6 +3,8 @@ import type {
3
3
  ModuleImplementationBindings,
4
4
  ModuleManifest,
5
5
  } from "./manifest.js";
6
+ import { validateAiOperations } from "./ai-operation-composition.js";
7
+ import { validateImplementationBindings } from "./implementation-bindings.js";
6
8
  import { composeModuleMenus } from "./menu-composition.js";
7
9
  import { validateCapabilityCatalogs } from "./catalog-validation.js";
8
10
 
@@ -58,10 +60,16 @@ export function validateModuleComposition(
58
60
  tasks.map(({ key }) => key),
59
61
  );
60
62
  const tools = modules.flatMap((module) => module.tools);
61
- unique(
63
+ const toolKeys = unique(
62
64
  "Agent Tool",
63
65
  tools.map(({ key }) => key),
64
66
  );
67
+ const aiOperations = validateAiOperations(modules, {
68
+ permissions,
69
+ features,
70
+ audits,
71
+ tools: toolKeys,
72
+ });
65
73
  const providers = modules.flatMap((module) => module.providers);
66
74
  unique(
67
75
  "Provider",
@@ -92,30 +100,16 @@ export function validateModuleComposition(
92
100
  }
93
101
  validateMenuRoutes(menus, adminRoutes);
94
102
  validateSchemaMigrations(schemas);
95
- validateImplementations(
96
- "Admin Route",
97
- adminRoutes.map(({ key }) => key),
98
- implementations.adminRoutes,
99
- );
100
- validateImplementations(
101
- "API Route",
102
- apiRoutes.map(apiRouteKey),
103
- implementations.apiRoutes,
104
- );
105
- validateImplementations(
106
- "定时任务",
107
- tasks.map(({ key }) => key),
108
- implementations.tasks,
109
- );
110
- validateImplementations(
111
- "Agent Tool",
112
- tools.map(({ key }) => key),
113
- implementations.tools,
114
- );
115
- validateImplementations(
116
- "Provider",
117
- providers.map(({ key }) => key),
118
- implementations.providers,
103
+ validateImplementationBindings(
104
+ {
105
+ adminRoutes: adminRoutes.map(({ key }) => key),
106
+ apiRoutes: apiRoutes.map(apiRouteKey),
107
+ tasks: tasks.map(({ key }) => key),
108
+ tools: tools.map(({ key }) => key),
109
+ providers: providers.map(({ key }) => key),
110
+ },
111
+ aiOperations,
112
+ implementations,
119
113
  );
120
114
 
121
115
  return { modules, moduleOrder };
@@ -318,18 +312,6 @@ function validateSchemaMigrations(
318
312
  }
319
313
  }
320
314
 
321
- function validateImplementations(
322
- label: string,
323
- declarations: readonly string[],
324
- implementations: readonly string[] | undefined,
325
- ): void {
326
- if (!implementations) return;
327
- const registered = unique(`${label} Handler`, implementations);
328
- for (const key of declarations) {
329
- if (!registered.has(key)) throw new Error(`${label} ${key} 缺少 Handler`);
330
- }
331
- }
332
-
333
315
  function unique(label: string, values: readonly string[]): Set<string> {
334
316
  const result = new Set<string>();
335
317
  for (const value of values) {
@@ -0,0 +1,70 @@
1
+ import { validateAiOperationImplementations } from "./ai-operation-composition.js";
2
+ import type {
3
+ ModuleAiOperationDefinition,
4
+ ModuleImplementationBindings,
5
+ } from "./manifest.js";
6
+
7
+ interface ModuleDeclarationKeys {
8
+ adminRoutes: readonly string[];
9
+ apiRoutes: readonly string[];
10
+ tasks: readonly string[];
11
+ tools: readonly string[];
12
+ providers: readonly string[];
13
+ }
14
+
15
+ export function validateImplementationBindings(
16
+ declarations: ModuleDeclarationKeys,
17
+ aiOperations: readonly ModuleAiOperationDefinition[],
18
+ implementations: ModuleImplementationBindings,
19
+ ): void {
20
+ validateImplementations(
21
+ "Admin Route",
22
+ declarations.adminRoutes,
23
+ implementations.adminRoutes,
24
+ );
25
+ validateImplementations(
26
+ "API Route",
27
+ declarations.apiRoutes,
28
+ implementations.apiRoutes,
29
+ );
30
+ validateImplementations(
31
+ "定时任务",
32
+ declarations.tasks,
33
+ implementations.tasks,
34
+ );
35
+ validateImplementations(
36
+ "Agent Tool",
37
+ declarations.tools,
38
+ implementations.tools,
39
+ );
40
+ validateAiOperationImplementations(
41
+ aiOperations,
42
+ implementations.aiOperations,
43
+ );
44
+ validateImplementations(
45
+ "Provider",
46
+ declarations.providers,
47
+ implementations.providers,
48
+ );
49
+ }
50
+
51
+ function validateImplementations(
52
+ label: string,
53
+ declarations: readonly string[],
54
+ implementations: readonly string[] | undefined,
55
+ ): void {
56
+ if (!implementations) return;
57
+ const registered = unique(`${label} Handler`, implementations);
58
+ for (const key of declarations) {
59
+ if (!registered.has(key)) throw new Error(`${label} ${key} 缺少 Handler`);
60
+ }
61
+ }
62
+
63
+ function unique(label: string, values: readonly string[]): Set<string> {
64
+ const result = new Set<string>();
65
+ for (const value of values) {
66
+ if (result.has(value)) throw new Error(`${label} Key 重复:${value}`);
67
+ result.add(value);
68
+ }
69
+ return result;
70
+ }
package/src/manifest.ts CHANGED
@@ -11,7 +11,10 @@ import type {
11
11
  SearchProviderManifestDefinition,
12
12
  } from "@southwind-ai/shared";
13
13
 
14
- export type { MigrationDefinition, MigrationDirection } from "@southwind-ai/shared";
14
+ export type {
15
+ MigrationDefinition,
16
+ MigrationDirection,
17
+ } from "@southwind-ai/shared";
15
18
 
16
19
  export interface ModuleToolDefinition {
17
20
  key: string;
@@ -59,6 +62,37 @@ export interface ModuleScheduledTaskDefinition {
59
62
  retryDelaySeconds: number;
60
63
  }
61
64
 
65
+ export type AiOperationKind = "agent" | "document" | "speech";
66
+ export type AiOperationDataClassification = "public" | "internal" | "sensitive";
67
+ export type AiOperationExecution =
68
+ | "interactive"
69
+ | "streaming"
70
+ | "background"
71
+ | "live";
72
+ export type AiOperationPublicNetworkAccess = "forbidden" | "allowed";
73
+
74
+ export interface AiOperationLimits {
75
+ maxDurationSeconds: number;
76
+ maxInputBytes: number;
77
+ maxOutputBytes: number;
78
+ }
79
+
80
+ export interface ModuleAiOperationDefinition {
81
+ key: string;
82
+ kind: AiOperationKind;
83
+ featureKey: string;
84
+ permissionKey: string;
85
+ auditAction: AuditActionType;
86
+ inputSchemaKey: string;
87
+ outputSchemaKey: string;
88
+ requiredCapabilities: readonly string[];
89
+ allowedToolKeys: readonly string[];
90
+ dataClassification: AiOperationDataClassification;
91
+ execution: AiOperationExecution;
92
+ publicNetworkAccess: AiOperationPublicNetworkAccess;
93
+ limits: AiOperationLimits;
94
+ }
95
+
62
96
  export interface ModuleManifest {
63
97
  name: string;
64
98
  version: string;
@@ -75,6 +109,7 @@ export interface ModuleManifest {
75
109
  schemaExports: ModuleSchemaExportDefinition[];
76
110
  tasks: ModuleScheduledTaskDefinition[];
77
111
  tools: ModuleToolDefinition[];
112
+ aiOperations?: ModuleAiOperationDefinition[];
78
113
  providers: Array<ModuleProviderDefinition | SearchProviderManifestDefinition>;
79
114
  dataPolicies?: DataGovernancePolicyDefinition[];
80
115
  auditActions: AuditActionRegistration[];
@@ -88,5 +123,6 @@ export interface ModuleImplementationBindings {
88
123
  apiRoutes?: readonly string[];
89
124
  tasks?: readonly string[];
90
125
  tools?: readonly string[];
126
+ aiOperations?: readonly string[];
91
127
  providers?: readonly string[];
92
128
  }
@@ -1,5 +1,8 @@
1
1
  import type { ApiPermissionBinding } from "@southwind-ai/shared";
2
2
  import { binding, resourceBindings } from "./system-api-binding.js";
3
+ // @windy-module system.settings begin
4
+ import { watermarkApiPermissionBindings } from "./system-api-watermark.js";
5
+ // @windy-module system.settings end
3
6
  // @windy-module system.data-governance begin
4
7
  import { dataGovernanceApiPermissionBindings } from "./system-api-data-governance.js";
5
8
  // @windy-module system.data-governance end
@@ -109,6 +112,7 @@ export function systemApiPermissionBindings(): ApiPermissionBinding[] {
109
112
  "system.settings.manage",
110
113
  "system.settings",
111
114
  ),
115
+ ...watermarkApiPermissionBindings(),
112
116
  // @windy-module system.settings end
113
117
  // @windy-module system.configuration begin
114
118
  binding(
@@ -0,0 +1,26 @@
1
+ import type { ApiPermissionBinding } from "@southwind-ai/shared";
2
+ import { binding } from "./system-api-binding.js";
3
+
4
+ export function watermarkApiPermissionBindings(): ApiPermissionBinding[] {
5
+ return [
6
+ binding(
7
+ "GET",
8
+ "/api/platform/watermark-policy",
9
+ "system.watermark.read",
10
+ undefined,
11
+ true,
12
+ ),
13
+ binding(
14
+ "GET",
15
+ "/api/system/settings/watermark",
16
+ "system.watermark.read",
17
+ "system.watermark",
18
+ ),
19
+ binding(
20
+ "PUT",
21
+ "/api/system/settings/watermark",
22
+ "system.watermark.manage",
23
+ "system.watermark",
24
+ ),
25
+ ];
26
+ }
@@ -43,7 +43,7 @@ export const systemFeatures: ModuleManifest["features"] = [
43
43
  feature(
44
44
  "system.watermark",
45
45
  "页面水印",
46
- "带身份信息的页面水印与用户显示偏好。",
46
+ "由平台统一管理并覆盖 Admin 与业务页面的身份水印策略。",
47
47
  ),
48
48
  // @windy-module system.settings end
49
49
  // @windy-module system.license begin