@southwind-ai/modules 0.1.2 → 0.1.4

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/index.ts CHANGED
@@ -6,3 +6,5 @@ export * from "./src/registry.js";
6
6
  export * from "./src/system-modules.js";
7
7
  export * from "./src/system-module-catalog.js";
8
8
  export * from "./src/system-api-permissions.js";
9
+ export * from "./src/tool-composition.js";
10
+ export * from "./src/ai-network-policy.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@southwind-ai/modules",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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": "0.1.2"
15
+ "@southwind-ai/shared": "0.2.0"
16
16
  },
17
17
  "exports": {
18
18
  ".": "./index.ts"
@@ -0,0 +1,50 @@
1
+ import type {
2
+ AiProviderEgressPolicy,
3
+ AiToolNetworkPolicy,
4
+ ModuleAiOperationDefinition,
5
+ } from "./manifest.js";
6
+
7
+ const providerForbidden: AiProviderEgressPolicy = Object.freeze({
8
+ mode: "forbidden",
9
+ });
10
+ const toolNetworkForbidden: AiToolNetworkPolicy = Object.freeze({
11
+ mode: "forbidden",
12
+ });
13
+
14
+ export function resolveProviderEgress(
15
+ operation: ModuleAiOperationDefinition,
16
+ ): AiProviderEgressPolicy {
17
+ return operation.providerEgress ?? providerForbidden;
18
+ }
19
+
20
+ export function resolveToolNetworkPolicy(
21
+ operation: ModuleAiOperationDefinition,
22
+ ): AiToolNetworkPolicy {
23
+ return operation.toolNetworkPolicy ?? toolNetworkForbidden;
24
+ }
25
+
26
+ export function validateAiNetworkPolicy(
27
+ operation: ModuleAiOperationDefinition,
28
+ ): void {
29
+ const provider = resolveProviderEgress(operation);
30
+ if (provider.mode === "configured-endpoint") {
31
+ if (!provider.deploymentKey.trim()) {
32
+ throw new Error(`${operation.key} Provider Deployment Key 不能为空`);
33
+ }
34
+ } else if (provider.mode !== "forbidden") {
35
+ throw new Error(`${operation.key} Provider 出网策略无效`);
36
+ }
37
+
38
+ const tool = resolveToolNetworkPolicy(operation);
39
+ if (tool.mode !== "forbidden") {
40
+ throw new Error(`${operation.key} Tool 网络策略无效`);
41
+ }
42
+ if (
43
+ operation.publicNetworkAccess === "allowed" &&
44
+ operation.providerEgress === undefined
45
+ ) {
46
+ throw new Error(
47
+ `${operation.key} 旧 publicNetworkAccess=allowed 不能安全映射,请声明 providerEgress`,
48
+ );
49
+ }
50
+ }
@@ -2,12 +2,14 @@ import type {
2
2
  ModuleAiOperationDefinition,
3
3
  ModuleManifest,
4
4
  } from "./manifest.js";
5
+ import { validateAiNetworkPolicy } from "./ai-network-policy.js";
5
6
 
6
7
  interface AiOperationCatalogs {
7
8
  permissions: ReadonlySet<string>;
8
9
  features: ReadonlySet<string>;
9
10
  audits: ReadonlySet<string>;
10
11
  tools: ReadonlySet<string>;
12
+ schemas: ReadonlySet<string>;
11
13
  }
12
14
 
13
15
  export function validateAiOperations(
@@ -85,11 +87,139 @@ function validateOperation(
85
87
  operation.auditAction,
86
88
  catalogs.audits,
87
89
  );
90
+ requiredReference(
91
+ operation.key,
92
+ " JSON Schema",
93
+ operation.inputSchemaKey,
94
+ catalogs.schemas,
95
+ );
96
+ requiredReference(
97
+ operation.key,
98
+ " JSON Schema",
99
+ operation.outputSchemaKey,
100
+ catalogs.schemas,
101
+ );
88
102
  validateCapabilities(operation);
89
103
  validateTools(operation, catalogs.tools);
104
+ validateAiNetworkPolicy(operation);
105
+ validateProviderDataPolicy(operation);
90
106
  validateLimits(operation);
91
107
  }
92
108
 
109
+ const providerDataFields = new Set([
110
+ "instructions",
111
+ "user-prompt",
112
+ "tool-definitions",
113
+ "provider-output",
114
+ "tool-results",
115
+ ]);
116
+
117
+ export function validateProviderDataPolicy(
118
+ operation: ModuleAiOperationDefinition,
119
+ ): void {
120
+ const policy = operation.providerDataPolicy;
121
+ if (operation.providerEgress?.mode !== "configured-endpoint") {
122
+ if (policy) {
123
+ throw new Error(
124
+ `${operation.key} 禁止 Provider 出网时不能声明字段发送策略`,
125
+ );
126
+ }
127
+ return;
128
+ }
129
+ if (!policy?.inputFields.length || !policy.envelopeFields.length) {
130
+ throw new Error(`${operation.key} Provider 字段发送 allowlist 缺失`);
131
+ }
132
+ validateInputFields(operation.key, policy.inputFields);
133
+ const fields = new Set<string>();
134
+ for (const rule of policy.envelopeFields) {
135
+ if (!providerDataFields.has(rule.field)) {
136
+ throw new Error(
137
+ `${operation.key} Provider 字段发送 allowlist 含未知字段`,
138
+ );
139
+ }
140
+ if (fields.has(rule.field)) {
141
+ throw new Error(
142
+ `${operation.key} Provider 字段发送规则重复:${rule.field}`,
143
+ );
144
+ }
145
+ fields.add(rule.field);
146
+ if (!["public", "internal", "sensitive"].includes(rule.classification)) {
147
+ throw new Error(`${operation.key} Provider 字段分类无效:${rule.field}`);
148
+ }
149
+ if (!["reject", "redact"].includes(rule.dlpAction)) {
150
+ throw new Error(`${operation.key} Provider DLP 动作无效:${rule.field}`);
151
+ }
152
+ }
153
+ for (const field of ["instructions", "user-prompt"]) {
154
+ if (!fields.has(field)) {
155
+ throw new Error(
156
+ `${operation.key} Provider 字段发送 allowlist 缺少 ${field}`,
157
+ );
158
+ }
159
+ }
160
+ if (operation.allowedToolKeys.length) {
161
+ for (const field of [
162
+ "tool-definitions",
163
+ "provider-output",
164
+ "tool-results",
165
+ ]) {
166
+ if (!fields.has(field)) {
167
+ throw new Error(
168
+ `${operation.key} 使用 Tool 时 Provider 字段发送 allowlist 缺少 ${field}`,
169
+ );
170
+ }
171
+ }
172
+ }
173
+ }
174
+
175
+ function validateInputFields(
176
+ operationKey: string,
177
+ fields: NonNullable<
178
+ ModuleAiOperationDefinition["providerDataPolicy"]
179
+ >["inputFields"],
180
+ ): void {
181
+ const pointers = new Set<string>();
182
+ for (const rule of fields) {
183
+ const segments = rule.jsonPointer
184
+ .slice(1)
185
+ .split("/")
186
+ .map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~"));
187
+ if (
188
+ !rule.jsonPointer.startsWith("/") ||
189
+ rule.jsonPointer === "/" ||
190
+ rule.jsonPointer.endsWith("/") ||
191
+ rule.jsonPointer.includes("//") ||
192
+ /~(?:[^01]|$)/u.test(rule.jsonPointer) ||
193
+ segments.some((segment) =>
194
+ ["__proto__", "prototype", "constructor"].includes(segment),
195
+ )
196
+ ) {
197
+ throw new Error(`${operationKey} Provider 输入字段 JSON Pointer 无效`);
198
+ }
199
+ if (
200
+ [...pointers].some(
201
+ (pointer) =>
202
+ pointer.startsWith(`${rule.jsonPointer}/`) ||
203
+ rule.jsonPointer.startsWith(`${pointer}/`),
204
+ )
205
+ ) {
206
+ throw new Error(`${operationKey} Provider 输入字段规则不能父子重叠`);
207
+ }
208
+ if (pointers.has(rule.jsonPointer)) {
209
+ throw new Error(
210
+ `${operationKey} Provider 输入字段规则重复:${rule.jsonPointer}`,
211
+ );
212
+ }
213
+ pointers.add(rule.jsonPointer);
214
+ if (!["public", "internal", "sensitive"].includes(rule.classification)) {
215
+ throw new Error(`${operationKey} Provider 输入字段分类无效`);
216
+ }
217
+ if (!["reject", "redact"].includes(rule.dlpAction)) {
218
+ throw new Error(`${operationKey} Provider 输入字段 DLP 动作无效`);
219
+ }
220
+ }
221
+ }
222
+
93
223
  function validateCapabilities(operation: ModuleAiOperationDefinition): void {
94
224
  if (!operation.requiredCapabilities.length) {
95
225
  throw new Error(`${operation.key} 至少需要一个 capability`);
@@ -7,6 +7,7 @@ import { validateAiOperations } from "./ai-operation-composition.js";
7
7
  import { validateImplementationBindings } from "./implementation-bindings.js";
8
8
  import { composeModuleMenus } from "./menu-composition.js";
9
9
  import { validateCapabilityCatalogs } from "./catalog-validation.js";
10
+ import { validateToolContracts } from "./tool-composition.js";
10
11
 
11
12
  export interface ValidatedModuleComposition {
12
13
  modules: readonly ModuleManifest[];
@@ -35,6 +36,7 @@ export function validateModuleComposition(
35
36
  modules.flatMap((module) => module.features.map(({ key }) => key)),
36
37
  );
37
38
  const catalogs = validateCapabilityCatalogs(modules);
39
+ const jsonSchemas = validateToolContracts(modules);
38
40
  const audits = new Set(catalogs.auditActions.map(({ key }) => key));
39
41
  const menus = composeModuleMenus(modules).flatMap((menu) =>
40
42
  flattenMenus([menu]),
@@ -69,6 +71,7 @@ export function validateModuleComposition(
69
71
  features,
70
72
  audits,
71
73
  tools: toolKeys,
74
+ schemas: new Set(jsonSchemas.keys()),
72
75
  });
73
76
  const providers = modules.flatMap((module) => module.providers);
74
77
  unique(
package/src/manifest.ts CHANGED
@@ -4,6 +4,7 @@ import type {
4
4
  AuditActionType,
5
5
  DataGovernancePolicyDefinition,
6
6
  FeatureFlagDefinition,
7
+ JsonObject,
7
8
  MigrationBundle,
8
9
  MigrationDefinition,
9
10
  MenuNode,
@@ -20,9 +21,26 @@ export interface ModuleToolDefinition {
20
21
  key: string;
21
22
  label: string;
22
23
  description: string;
24
+ inputSchemaKey: string;
25
+ outputSchemaKey: string;
26
+ operation: "query" | "command" | "background";
23
27
  permissionKey: string;
24
- featureKey?: string;
28
+ featureKey: string;
25
29
  auditAction: AuditActionType;
30
+ scopeMode: "platform" | "request";
31
+ dataClassification: "public" | "internal" | "sensitive";
32
+ idempotency: "none" | "supported" | "required";
33
+ approval: "none" | "required";
34
+ limits: {
35
+ maxDurationSeconds: number;
36
+ maxInputBytes: number;
37
+ maxOutputBytes: number;
38
+ };
39
+ }
40
+
41
+ export interface ModuleJsonSchemaDefinition {
42
+ key: string;
43
+ schema: JsonObject;
26
44
  }
27
45
 
28
46
  export type AdminRouteSurface = "sidebar" | "account" | "hidden";
@@ -70,6 +88,40 @@ export type AiOperationExecution =
70
88
  | "background"
71
89
  | "live";
72
90
  export type AiOperationPublicNetworkAccess = "forbidden" | "allowed";
91
+ export type AiProviderEgressPolicy =
92
+ | { mode: "forbidden" }
93
+ | {
94
+ mode: "configured-endpoint";
95
+ deploymentKey: string;
96
+ };
97
+ export interface AiToolNetworkPolicy {
98
+ mode: "forbidden";
99
+ }
100
+
101
+ export type AiProviderDataField =
102
+ | "instructions"
103
+ | "user-prompt"
104
+ | "tool-definitions"
105
+ | "provider-output"
106
+ | "tool-results";
107
+ export type AiProviderDlpAction = "reject" | "redact";
108
+
109
+ export interface AiProviderFieldPolicy {
110
+ field: AiProviderDataField;
111
+ classification: AiOperationDataClassification;
112
+ dlpAction: AiProviderDlpAction;
113
+ }
114
+
115
+ export interface AiProviderInputFieldPolicy {
116
+ jsonPointer: string;
117
+ classification: AiOperationDataClassification;
118
+ dlpAction: AiProviderDlpAction;
119
+ }
120
+
121
+ export interface AiProviderDataPolicy {
122
+ inputFields: readonly AiProviderInputFieldPolicy[];
123
+ envelopeFields: readonly AiProviderFieldPolicy[];
124
+ }
73
125
 
74
126
  export interface AiOperationLimits {
75
127
  maxDurationSeconds: number;
@@ -89,7 +141,11 @@ export interface ModuleAiOperationDefinition {
89
141
  allowedToolKeys: readonly string[];
90
142
  dataClassification: AiOperationDataClassification;
91
143
  execution: AiOperationExecution;
92
- publicNetworkAccess: AiOperationPublicNetworkAccess;
144
+ /** @deprecated 使用 providerEgress 与 toolNetworkPolicy 拆分两类网络边界。 */
145
+ publicNetworkAccess?: AiOperationPublicNetworkAccess;
146
+ providerEgress?: AiProviderEgressPolicy;
147
+ providerDataPolicy?: AiProviderDataPolicy;
148
+ toolNetworkPolicy?: AiToolNetworkPolicy;
93
149
  limits: AiOperationLimits;
94
150
  }
95
151
 
@@ -107,6 +163,7 @@ export interface ModuleManifest {
107
163
  migrations: MigrationDefinition[];
108
164
  migrationBundle?: MigrationBundle;
109
165
  schemaExports: ModuleSchemaExportDefinition[];
166
+ jsonSchemas?: ModuleJsonSchemaDefinition[];
110
167
  tasks: ModuleScheduledTaskDefinition[];
111
168
  tools: ModuleToolDefinition[];
112
169
  aiOperations?: ModuleAiOperationDefinition[];
@@ -1,15 +1,66 @@
1
- import type { ModuleManifest } from "./manifest.js";
1
+ import type { ModuleJsonSchemaDefinition, ModuleManifest } from "./manifest.js";
2
+
3
+ export const systemAgentToolSchemas: ModuleJsonSchemaDefinition[] = [
4
+ {
5
+ key: "system.pagination.input.v1",
6
+ schema: {
7
+ type: "object",
8
+ properties: {
9
+ page: { type: "integer", minimum: 1, default: 1 },
10
+ pageSize: {
11
+ type: "integer",
12
+ minimum: 1,
13
+ maximum: 50,
14
+ default: 10,
15
+ },
16
+ },
17
+ additionalProperties: false,
18
+ },
19
+ },
20
+ {
21
+ key: "system.page.output.v1",
22
+ schema: {
23
+ type: "object",
24
+ properties: {
25
+ items: { type: "array", items: { type: "object" } },
26
+ total: { type: "integer", minimum: 0 },
27
+ page: { type: "integer", minimum: 1 },
28
+ pageSize: { type: "integer", minimum: 1 },
29
+ },
30
+ required: ["items", "total", "page", "pageSize"],
31
+ additionalProperties: false,
32
+ },
33
+ },
34
+ ];
35
+
36
+ const queryContract = {
37
+ inputSchemaKey: "system.pagination.input.v1",
38
+ outputSchemaKey: "system.page.output.v1",
39
+ operation: "query" as const,
40
+ scopeMode: "platform" as const,
41
+ dataClassification: "internal" as const,
42
+ idempotency: "none" as const,
43
+ approval: "none" as const,
44
+ limits: {
45
+ maxDurationSeconds: 10,
46
+ maxInputBytes: 4_096,
47
+ maxOutputBytes: 1_048_576,
48
+ },
49
+ };
2
50
 
3
51
  export const systemAgentTools: ModuleManifest["tools"] = [
4
52
  {
53
+ ...queryContract,
5
54
  key: "system.users.list",
6
55
  label: "查询用户",
7
56
  description: "分页查询平台用户账号。",
8
57
  permissionKey: "system.user.read",
9
58
  featureKey: "system.rbac",
10
59
  auditAction: "agent.tool.execute",
60
+ scopeMode: "request",
11
61
  },
12
62
  {
63
+ ...queryContract,
13
64
  key: "system.features.list",
14
65
  label: "查询功能开关",
15
66
  description: "分页查询平台功能开关状态。",
@@ -18,6 +69,7 @@ export const systemAgentTools: ModuleManifest["tools"] = [
18
69
  auditAction: "agent.tool.execute",
19
70
  },
20
71
  {
72
+ ...queryContract,
21
73
  key: "system.audit.list",
22
74
  label: "查询审计日志",
23
75
  description: "分页查询平台审计事件。",
@@ -25,6 +25,11 @@ export function systemApiPermissionBindings(): ApiPermissionBinding[] {
25
25
  ),
26
26
  binding("POST", "/api/search", "system.search.read", "system.search"),
27
27
  // @windy-module system.search end
28
+ // @windy-module system.agent begin
29
+ binding("GET", "/api/mcp", "system.agent.read", "system.agent"),
30
+ binding("POST", "/api/mcp", "system.agent.read", "system.agent"),
31
+ binding("DELETE", "/api/mcp", "system.agent.read", "system.agent"),
32
+ // @windy-module system.agent end
28
33
  ...resourceBindings({
29
34
  path: "users",
30
35
  permissionResource: "user",
@@ -56,6 +61,12 @@ export function systemApiPermissionBindings(): ApiPermissionBinding[] {
56
61
  "system.department.create",
57
62
  "system.rbac",
58
63
  ),
64
+ binding(
65
+ "POST",
66
+ "/api/system/departments/:id/transition",
67
+ "system.department.manage",
68
+ "system.rbac",
69
+ ),
59
70
  binding("GET", "/api/system/menus", "system.menu.read", "system.rbac"),
60
71
  binding(
61
72
  "GET",
@@ -221,12 +232,48 @@ export function systemApiPermissionBindings(): ApiPermissionBinding[] {
221
232
  "system.notification.read",
222
233
  "system.notification",
223
234
  ),
235
+ binding(
236
+ "GET",
237
+ "/api/notifications/preferences",
238
+ "system.notification.read",
239
+ "system.notification",
240
+ ),
241
+ binding(
242
+ "PUT",
243
+ "/api/notifications/preferences",
244
+ "system.notification.read",
245
+ "system.notification",
246
+ ),
224
247
  binding(
225
248
  "POST",
226
249
  "/api/notifications/:id/read",
227
250
  "system.notification.read",
228
251
  "system.notification",
229
252
  ),
253
+ binding(
254
+ "PUT",
255
+ "/api/notifications/:id/favorite",
256
+ "system.notification.read",
257
+ "system.notification",
258
+ ),
259
+ binding(
260
+ "DELETE",
261
+ "/api/notifications/:id/favorite",
262
+ "system.notification.read",
263
+ "system.notification",
264
+ ),
265
+ binding(
266
+ "GET",
267
+ "/api/system/notifications/audience-options",
268
+ "system.notification.manage",
269
+ "system.notification",
270
+ ),
271
+ binding(
272
+ "GET",
273
+ "/api/system/notifications/:id/deliveries",
274
+ "system.notification.manage",
275
+ "system.notification",
276
+ ),
230
277
  binding(
231
278
  "GET",
232
279
  "/api/system/notifications",
@@ -239,6 +286,18 @@ export function systemApiPermissionBindings(): ApiPermissionBinding[] {
239
286
  "system.notification.manage",
240
287
  "system.notification",
241
288
  ),
289
+ binding(
290
+ "PUT",
291
+ "/api/system/notifications/:id",
292
+ "system.notification.update",
293
+ "system.notification",
294
+ ),
295
+ binding(
296
+ "GET",
297
+ "/api/system/notifications/:id/revisions",
298
+ "system.notification.manage",
299
+ "system.notification",
300
+ ),
242
301
  binding(
243
302
  "DELETE",
244
303
  "/api/system/notifications/:id",
@@ -8,6 +8,11 @@ export const systemAuditActions: ModuleManifest["auditActions"] = [
8
8
  "crud.create",
9
9
  "crud.update",
10
10
  "crud.delete",
11
+ {
12
+ key: "system.user-directory.query",
13
+ label: "查询业务用户目录",
14
+ description: "业务模块在平台授权与数据范围内查询启用用户最小投影。",
15
+ },
11
16
  // @windy-module system.configuration begin
12
17
  "config.update",
13
18
  "config.rollback",
@@ -17,9 +22,19 @@ export const systemAuditActions: ModuleManifest["auditActions"] = [
17
22
  // @windy-module system.license end
18
23
  "feature.update",
19
24
  "menu.structure.update",
25
+ {
26
+ key: "organization.department.transition",
27
+ label: "处置部门组织",
28
+ description: "迁移部门关联后停用或删除部门。",
29
+ },
20
30
  // @windy-module system.notification begin
21
31
  "notification.publish",
32
+ "notification.schedule",
33
+ "notification.preference.update",
34
+ "notification.update",
22
35
  "notification.read",
36
+ "notification.favorite",
37
+ "notification.unfavorite",
23
38
  "notification.archive",
24
39
  // @windy-module system.notification end
25
40
  // @windy-module system.workflow begin
@@ -17,6 +17,11 @@ export const systemFeatures: ModuleManifest["features"] = [
17
17
  "RBAC 权限链路",
18
18
  "用户、角色、权限点与数据范围的统一访问控制链路。",
19
19
  ),
20
+ feature(
21
+ "system.user-directory",
22
+ "业务用户目录",
23
+ "向获授权业务模块提供受数据范围约束的启用用户最小只读投影。",
24
+ ),
20
25
  feature(
21
26
  "system.profile",
22
27
  "个人设置",
@@ -85,7 +90,7 @@ export const systemFeatures: ModuleManifest["features"] = [
85
90
  feature(
86
91
  "system.notification",
87
92
  "站内通知",
88
- "站内通知发布、管理、阅读和已读状态闭环。",
93
+ "站内通知发布、修改历史、阅读、收藏和已读状态闭环。",
89
94
  ),
90
95
  // @windy-module system.notification end
91
96
  // @windy-module system.workflow begin
@@ -27,9 +27,17 @@ export const systemModuleDefinitions = [
27
27
  version: "0.1.0",
28
28
  label: "身份与权限",
29
29
  description: "用户、角色、部门、菜单、字典、个人资料和数据范围。",
30
- resources: ["user", "role", "department", "menu", "dict", "profile"],
30
+ resources: [
31
+ "user",
32
+ "user-directory",
33
+ "role",
34
+ "department",
35
+ "menu",
36
+ "dict",
37
+ "profile",
38
+ ],
31
39
  excludedPermissionKeys: ["system.user.reveal", "system.user.export"],
32
- featureKeys: ["system.rbac", "system.profile"],
40
+ featureKeys: ["system.rbac", "system.user-directory", "system.profile"],
33
41
  },
34
42
  // @windy-module system.settings begin
35
43
  {
@@ -116,7 +124,8 @@ export const systemModuleDefinitions = [
116
124
  name: "system.notification",
117
125
  version: "0.1.0",
118
126
  label: "站内通知",
119
- description: "通知发布、阅读、归档与状态管理。",
127
+ description:
128
+ "通知定向与定时发布、用户偏好、外部渠道投递、修改历史、阅读、收藏和归档。",
120
129
  resources: ["notification"],
121
130
  featureKeys: ["system.notification"],
122
131
  },
@@ -218,6 +227,7 @@ function createSystemCapabilityModule(
218
227
  .map((feature) => ({ ...feature, module: definition.name })),
219
228
  migrations: isCore ? aggregate.migrations : [],
220
229
  schemaExports: isCore ? aggregate.schemaExports : [],
230
+ jsonSchemas: isCore ? aggregate.jsonSchemas : [],
221
231
  tasks: aggregate.tasks.filter(ownsGuard),
222
232
  tools: aggregate.tools.filter(ownsGuard),
223
233
  providers: aggregate.providers.filter(ownsGuard),
@@ -225,11 +235,26 @@ function createSystemCapabilityModule(
225
235
  definition.name === "system.data-governance"
226
236
  ? aggregate.dataPolicies
227
237
  : [],
228
- auditActions: isCore ? aggregate.auditActions : [],
238
+ auditActions: isCore
239
+ ? aggregate.auditActions.filter(
240
+ (action) => auditActionKey(action) !== "system.user-directory.query",
241
+ )
242
+ : definition.name === "system.identity"
243
+ ? aggregate.auditActions.filter(
244
+ (action) =>
245
+ auditActionKey(action) === "system.user-directory.query",
246
+ )
247
+ : [],
229
248
  metadata: { kind: "platform-capability" },
230
249
  };
231
250
  }
232
251
 
252
+ function auditActionKey(
253
+ action: ModuleManifest["auditActions"][number],
254
+ ): string {
255
+ return typeof action === "string" ? action : action.key;
256
+ }
257
+
233
258
  function permissionResource(permissionKey: string): string {
234
259
  const parts = permissionKey.split(".");
235
260
  return parts.length >= 3 ? parts.slice(1, -1).join(".") : "";
@@ -1,6 +1,9 @@
1
1
  import type { ModuleManifest } from "./manifest.js";
2
2
  // @windy-module system.agent begin
3
- import { systemAgentTools } from "./system-agent-tools.js";
3
+ import {
4
+ systemAgentToolSchemas,
5
+ systemAgentTools,
6
+ } from "./system-agent-tools.js";
4
7
  // @windy-module system.agent end
5
8
  import { systemAdminRoutes } from "./system-admin-routes.js";
6
9
  import { systemApiPermissionBindings } from "./system-api-permissions.js";
@@ -249,6 +252,11 @@ export function systemFoundationModule(): ModuleManifest {
249
252
  migrationIds: ["0000..0028"],
250
253
  },
251
254
  ],
255
+ jsonSchemas: [
256
+ // @windy-module system.agent begin
257
+ ...systemAgentToolSchemas,
258
+ // @windy-module system.agent end
259
+ ],
252
260
  tasks: [
253
261
  // @windy-module system.scheduler begin
254
262
  {
@@ -262,6 +270,18 @@ export function systemFoundationModule(): ModuleManifest {
262
270
  retryDelaySeconds: 30,
263
271
  },
264
272
  // @windy-module system.scheduler end
273
+ // @windy-module system.notification begin
274
+ {
275
+ key: "system.notification.dispatch",
276
+ label: "通知发布与外部投递",
277
+ description: "发布到期通知并领取持久投递队列,执行渠道重试。",
278
+ permissionKey: "system.notification.manage",
279
+ featureKey: "system.notification",
280
+ intervalSeconds: 5,
281
+ maxAttempts: 3,
282
+ retryDelaySeconds: 30,
283
+ },
284
+ // @windy-module system.notification end
265
285
  // @windy-module system.bulk-data begin
266
286
  {
267
287
  key: "system.bulk-data.dispatch",
@@ -3,6 +3,7 @@ import type { ModuleManifest } from "./manifest.js";
3
3
  const systemResourceLabels = {
4
4
  admin: "管理面准入",
5
5
  user: "用户管理",
6
+ "user-directory": "业务用户目录",
6
7
  role: "角色与权限",
7
8
  department: "部门组织",
8
9
  menu: "菜单管理",
@@ -32,6 +33,7 @@ type SystemAction =
32
33
  | "approve"
33
34
  | "export"
34
35
  | "reveal"
36
+ | "update"
35
37
  | "manage";
36
38
 
37
39
  function permission(
@@ -53,11 +55,13 @@ function permission(
53
55
  ? "新建"
54
56
  : action === "approve"
55
57
  ? "处理"
56
- : action === "reveal"
57
- ? "查看明文"
58
- : action === "export"
59
- ? "治理导出"
60
- : "管理",
58
+ : action === "update"
59
+ ? "修改"
60
+ : action === "reveal"
61
+ ? "查看明文"
62
+ : action === "export"
63
+ ? "治理导出"
64
+ : "管理",
61
65
  module: "system",
62
66
  moduleLabel: "系统基础设施",
63
67
  resource,
@@ -75,6 +79,7 @@ export const systemPermissions: ModuleManifest["permissions"] = [
75
79
  permission("user", "export", "governed-export"),
76
80
  // @windy-module system.data-governance end
77
81
  permission("user", "manage"),
82
+ permission("user-directory", "read"),
78
83
  permission("role", "read"),
79
84
  permission("role", "manage"),
80
85
  permission("department", "read"),
@@ -123,6 +128,7 @@ export const systemPermissions: ModuleManifest["permissions"] = [
123
128
  // @windy-module system.scheduler end
124
129
  // @windy-module system.notification begin
125
130
  permission("notification", "read"),
131
+ permission("notification", "update"),
126
132
  permission("notification", "manage"),
127
133
  // @windy-module system.notification end
128
134
  // @windy-module system.bulk-data begin
@@ -0,0 +1,86 @@
1
+ import type {
2
+ ModuleJsonSchemaDefinition,
3
+ ModuleManifest,
4
+ ModuleToolDefinition,
5
+ } from "./manifest.js";
6
+
7
+ export function validateToolContracts(
8
+ modules: readonly ModuleManifest[],
9
+ ): ReadonlyMap<string, ModuleJsonSchemaDefinition> {
10
+ const schemas = new Map<string, ModuleJsonSchemaDefinition>();
11
+ for (const module of modules) {
12
+ for (const definition of module.jsonSchemas ?? []) {
13
+ validateSchema(module.name, definition);
14
+ if (schemas.has(definition.key)) {
15
+ throw new Error(`JSON Schema Key 重复:${definition.key}`);
16
+ }
17
+ schemas.set(definition.key, definition);
18
+ }
19
+ }
20
+ for (const module of modules) {
21
+ for (const tool of module.tools) {
22
+ validateTool(module.name, tool, schemas);
23
+ }
24
+ }
25
+ return schemas;
26
+ }
27
+
28
+ function validateSchema(
29
+ moduleName: string,
30
+ definition: ModuleJsonSchemaDefinition,
31
+ ): void {
32
+ if (!definition.key.trim()) throw new Error("JSON Schema Key 不能为空");
33
+ if (!definition.key.startsWith(`${moduleName}.`)) {
34
+ throw new Error(
35
+ `JSON Schema ${definition.key} 必须使用模块命名空间 ${moduleName}`,
36
+ );
37
+ }
38
+ if (definition.schema.type !== "object") {
39
+ throw new Error(`JSON Schema ${definition.key} 顶层类型必须是 object`);
40
+ }
41
+ }
42
+
43
+ function validateTool(
44
+ moduleName: string,
45
+ tool: ModuleToolDefinition,
46
+ schemas: ReadonlyMap<string, ModuleJsonSchemaDefinition>,
47
+ ): void {
48
+ const fields: Array<string | undefined> = [
49
+ tool.key,
50
+ tool.label,
51
+ tool.description,
52
+ tool.inputSchemaKey,
53
+ tool.outputSchemaKey,
54
+ tool.featureKey,
55
+ tool.permissionKey,
56
+ tool.auditAction,
57
+ ];
58
+ if (fields.some((value) => !value?.trim())) {
59
+ throw new Error("Agent Tool Key、展示信息与引用字段不能为空");
60
+ }
61
+ const rootNamespace = moduleName.split(".")[0]!;
62
+ if (!tool.key.startsWith(`${rootNamespace}.`)) {
63
+ throw new Error(
64
+ `Agent Tool ${tool.key} 必须使用模块根命名空间 ${rootNamespace}`,
65
+ );
66
+ }
67
+ requireSchema(tool.key, tool.inputSchemaKey, schemas);
68
+ requireSchema(tool.key, tool.outputSchemaKey, schemas);
69
+ if (tool.operation === "query" && tool.approval !== "none") {
70
+ throw new Error(`${tool.key} 查询能力不能要求执行审批`);
71
+ }
72
+ const limits = Object.values(tool.limits);
73
+ if (limits.some((value) => !Number.isSafeInteger(value) || value <= 0)) {
74
+ throw new Error(`${tool.key} 运行与输入输出上限必须是正整数`);
75
+ }
76
+ }
77
+
78
+ function requireSchema(
79
+ toolKey: string,
80
+ schemaKey: string,
81
+ schemas: ReadonlyMap<string, ModuleJsonSchemaDefinition>,
82
+ ): void {
83
+ if (!schemas.has(schemaKey)) {
84
+ throw new Error(`${toolKey} 引用了未注册 JSON Schema:${schemaKey}`);
85
+ }
86
+ }