@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 +2 -0
- package/package.json +2 -2
- package/src/ai-network-policy.ts +50 -0
- package/src/ai-operation-composition.ts +130 -0
- package/src/composition.ts +3 -0
- package/src/manifest.ts +59 -2
- package/src/system-agent-tools.ts +53 -1
- package/src/system-api-permissions.ts +59 -0
- package/src/system-audit-actions.ts +15 -0
- package/src/system-features.ts +6 -1
- package/src/system-module-catalog.ts +29 -4
- package/src/system-modules.ts +21 -1
- package/src/system-permissions.ts +11 -5
- package/src/tool-composition.ts +86 -0
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@southwind-ai/modules",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
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`);
|
package/src/composition.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
package/src/system-features.ts
CHANGED
|
@@ -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: [
|
|
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
|
|
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(".") : "";
|
package/src/system-modules.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { ModuleManifest } from "./manifest.js";
|
|
2
2
|
// @windy-module system.agent begin
|
|
3
|
-
import {
|
|
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 === "
|
|
57
|
-
? "
|
|
58
|
-
: action === "
|
|
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
|
+
}
|