create-windy 0.6.8 → 0.6.10
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/README.md +4 -1
- package/dist/cli.js +1012 -329
- package/package.json +1 -1
- package/template/.windy-template.json +3 -3
- package/template/README.md +8 -2
- package/template/apps/agent-server/src/southwind_agent_server/contracts.py +2 -0
- package/template/apps/agent-server/tests/test_multimodal.py +30 -0
- package/template/apps/agent-server/tests/test_postgres_store.py +1 -0
- package/template/apps/agent-server/tests/test_sqlite_store.py +1 -0
- package/template/apps/server/src/agent/remote-runtime.test.ts +1 -0
- package/template/apps/server/src/agent/run-audit.ts +1 -0
- package/template/apps/server/src/agent/run-contracts.ts +1 -0
- package/template/apps/server/src/agent/run-facade.ts +2 -2
- package/template/apps/server/src/agent/run-operation-registry.ts +7 -0
- package/template/apps/server/src/agent/run-output-schema.ts +15 -0
- package/template/apps/server/src/agent/run-release.ts +11 -0
- package/template/apps/server/src/agent/supervisor-output-schema.test.ts +95 -0
- package/template/apps/server/src/agent/vision-input.ts +37 -2
- package/template/apps/server/src/license/pairing-http.ts +138 -0
- package/template/apps/server/src/license/pairing-routes.test.ts +282 -0
- package/template/apps/server/src/license/pairing-routes.ts +257 -0
- package/template/apps/server/src/license/pairing-service.test.ts +320 -0
- package/template/apps/server/src/license/pairing-service.ts +321 -0
- package/template/apps/server/src/license/restriction.test.ts +11 -0
- package/template/apps/server/src/license/restriction.ts +5 -0
- package/template/apps/server/src/license/routes.ts +8 -1
- package/template/apps/server/src/license/runtime-service.ts +20 -0
- package/template/apps/server/src/runtime-development.ts +1 -0
- package/template/apps/web/package.json +6 -4
- package/template/apps/web/src/components/license/LicensePairingCard.vue +177 -0
- package/template/apps/web/src/components/license/LicensePairingCard.webtest.ts +113 -0
- package/template/apps/web/src/composables/useLicensePairing.ts +168 -0
- package/template/apps/web/src/composables/useLicenseRuntime.ts +11 -0
- package/template/apps/web/src/layout/AppSidebar.vue +1 -0
- package/template/apps/web/src/pages/license/LicensePage.vue +8 -0
- package/template/apps/web/src/pages/ui/UiComponentPage.vue +7 -0
- package/template/apps/web/src/pages/ui/UiComponentPage.webtest.ts +94 -0
- package/template/apps/web/src/pages/ui/component-catalog.ts +248 -0
- package/template/apps/web/src/pages/ui/components/ComponentPreviewCard.vue +50 -0
- package/template/apps/web/src/pages/ui/components/DataComponentShowcases.vue +131 -0
- package/template/apps/web/src/pages/ui/components/FeedbackComponentShowcases.vue +158 -0
- package/template/apps/web/src/pages/ui/components/FoundationComponentShowcases.vue +129 -0
- package/template/apps/web/src/pages/ui/components/NavigationComponentShowcases.vue +155 -0
- package/template/apps/web/src/pages/ui/components/ProjectComponentShowcases.vue +31 -0
- package/template/apps/web/src/pages/ui/components/UiComponentGallery.vue +238 -0
- package/template/apps/web/src/router/index.ts +2 -0
- package/template/apps/web/src/router/manifest-routes.webtest.ts +1 -0
- package/template/apps/web/src/router/platform-admin-route-bindings.ts +3 -1
- package/template/apps/web/src/services/license-api.ts +60 -0
- package/template/apps/web/src/style.webtest.ts +5 -6
- package/template/docs/architecture/ai-runtime.md +5 -0
- package/template/docs/development/business-web-routes.md +3 -1
- package/template/docs/development/ui-component-gallery.md +44 -0
- package/template/docs/license/license-pairing-activation.md +67 -0
- package/template/docs/operations/incremental-delivery.md +10 -9
- package/template/docs/platform/agent-supervisor-output-contract.md +80 -0
- package/template/packages/ai-client/package.json +1 -1
- package/template/packages/ai-client/src/agent/contracts/final-answer.test.ts +67 -0
- package/template/packages/ai-client/src/agent/contracts/final-answer.ts +53 -0
- package/template/packages/ai-client/src/agent/contracts/index.ts +1 -0
- package/template/packages/modules/index.ts +1 -0
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/system-admin-routes.ts +8 -0
- package/template/packages/modules/src/system-admin-ui-catalog.ts +6 -2
- package/template/packages/modules/src/system-agent-supervisor.test.ts +48 -0
- package/template/packages/modules/src/system-agent-supervisor.ts +101 -0
- package/template/packages/modules/src/system-api-control-plane.ts +37 -0
- package/template/packages/modules/src/system-features.ts +5 -0
- package/template/packages/modules/src/system-license-pairing.test.ts +44 -0
- package/template/packages/modules/src/system-module-catalog.ts +10 -19
- package/template/packages/modules/src/system-module-menu-selection.ts +15 -0
- package/template/packages/modules/src/system-module-schema-catalog.ts +14 -0
- package/template/packages/modules/src/system-modules.ts +13 -0
- package/template/packages/modules/src/system-permissions.ts +2 -0
- package/template/packages/modules/src/system-ui-components.test.ts +36 -0
- package/template/packages/server-sdk/index.ts +5 -0
- package/template/packages/server-sdk/package.json +3 -1
- package/template/packages/server-sdk/src/ai-operation-registration.ts +4 -0
- package/template/packages/server-sdk/src/system-agent-supervisor.test.ts +87 -0
- package/template/packages/server-sdk/src/system-agent-supervisor.ts +89 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"templateCommit": "
|
|
4
|
-
"generatorVersion": "0.6.
|
|
3
|
+
"templateCommit": "1916707e888868d817a115dc8a96244a28e41d2c",
|
|
4
|
+
"generatorVersion": "0.6.10",
|
|
5
5
|
"modules": [
|
|
6
6
|
{
|
|
7
7
|
"name": "system",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"name": "system.agent",
|
|
36
|
-
"version": "0.1.
|
|
36
|
+
"version": "0.1.1"
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
"name": "system.operations",
|
package/template/README.md
CHANGED
|
@@ -35,6 +35,10 @@ Drizzle migration,再启动 Server 与 Web;不要跳过 migration 后直接
|
|
|
35
35
|
BuildKit 中的 Bun 包下载缓存,只下载新增或发生变化的依赖;冷安装每 15 秒输出一次
|
|
36
36
|
不含 registry 地址或凭据的进度心跳。
|
|
37
37
|
|
|
38
|
+
Windows 10/11 可在原生 PowerShell 中配合 Docker Desktop 直接运行 `bun run dev` 与
|
|
39
|
+
`bun run dev:build`,不需要 WSL。Windows 是本地开发宿主;生产交付仍使用 Linux
|
|
40
|
+
amd64 容器。
|
|
41
|
+
|
|
38
42
|
| 服务 | 地址 |
|
|
39
43
|
| ---- | ---- |
|
|
40
44
|
| Web | http://localhost:18746 |
|
|
@@ -132,7 +136,8 @@ browser-safe `./admin` 与 project-owned 的
|
|
|
132
136
|
`apps/web/src/app/web-composition.ts` 装配页面,不复制第二份 Feature、Permission
|
|
133
137
|
或路由清单。整个 `apps/web/src` 与整个 `apps/server/src/app` 都是
|
|
134
138
|
project-owned,客户可以修改 Router、layout、页面、Host 和 Server 组合;普通
|
|
135
|
-
`windy update`
|
|
139
|
+
`windy update` 不会整文件覆盖或三方合并这些文件。版本 recipe 只有在显式登记
|
|
140
|
+
path 与确定性语义锚点后才能做幂等追加;锚点缺失或不唯一时必须在写入前停止。
|
|
136
141
|
|
|
137
142
|
精确生成来源保留在 `.windy/generation.json`。当前版本已提供可回滚的
|
|
138
143
|
`windy update` 与 `windy module list/enable/disable`。当前内置模块仍依赖
|
|
@@ -166,7 +171,8 @@ bun run format
|
|
|
166
171
|
更新前无需提交、暂存或清理业务改动。`--dry-run` 只生成计划,不写项目;无关 WIP
|
|
167
172
|
保持原样,命中同一路径时按冻结快照与三方合并 fail closed。正式更新先备份,再做
|
|
168
173
|
package 字段级合并、共享脚手架三方合并与完整验证。整个 `apps/web/src` 和
|
|
169
|
-
`apps/server/src/app`
|
|
174
|
+
`apps/server/src/app` 默认保持项目版本;已登记版本 recipe 只修改其声明的窄锚点。
|
|
175
|
+
任何冲突都会在写入前中止,模块启停在交互
|
|
170
176
|
终端确认;CI、Agent 或其它非 TTY 环境必须显式传入 `--yes`。
|
|
171
177
|
无并发变化的验证失败会自动回滚;中断恢复使用
|
|
172
178
|
`bun run windy doctor --repair`。该命令在提交点前保留并发编辑并回滚其余路径,
|
|
@@ -135,6 +135,7 @@ class RuntimeRelease(CanonicalModel):
|
|
|
135
135
|
workflow_version: str = Field(min_length=1, max_length=80)
|
|
136
136
|
prompt_version: str = Field(min_length=1, max_length=80)
|
|
137
137
|
route_version: str = Field(min_length=1, max_length=80)
|
|
138
|
+
output_schema_version: str = Field(min_length=1, max_length=80)
|
|
138
139
|
tool_schema_version: str = Field(min_length=1, max_length=80)
|
|
139
140
|
manifest_digest: str = Field(pattern=r"^[a-f0-9]{64}$")
|
|
140
141
|
|
|
@@ -166,6 +167,7 @@ def legacy_release() -> RuntimeRelease:
|
|
|
166
167
|
workflow_version="legacy",
|
|
167
168
|
prompt_version="legacy",
|
|
168
169
|
route_version="legacy",
|
|
170
|
+
output_schema_version="legacy",
|
|
169
171
|
tool_schema_version="legacy",
|
|
170
172
|
manifest_digest="0" * 64,
|
|
171
173
|
)
|
|
@@ -49,3 +49,33 @@ def test_typed_image_part_and_json_schema_output() -> None:
|
|
|
49
49
|
with pytest.raises(SafeEngineError) as invalid:
|
|
50
50
|
final_output('{"name":42}', schema)
|
|
51
51
|
assert invalid.value.code == "OUTPUT_VALIDATION_FAILED"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_supervisor_final_answer_schema_rejects_intermediate_text() -> None:
|
|
55
|
+
schema: dict[str, JsonValue] = {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"properties": {
|
|
58
|
+
"finalAnswer": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"properties": {
|
|
61
|
+
"schemaVersion": {"const": 1},
|
|
62
|
+
"text": {"type": "string", "minLength": 1},
|
|
63
|
+
},
|
|
64
|
+
"required": ["schemaVersion", "text"],
|
|
65
|
+
"additionalProperties": False,
|
|
66
|
+
},
|
|
67
|
+
"nodes": {"type": "array"},
|
|
68
|
+
},
|
|
69
|
+
"required": ["finalAnswer"],
|
|
70
|
+
"additionalProperties": False,
|
|
71
|
+
}
|
|
72
|
+
assert final_output(
|
|
73
|
+
'{"finalAnswer":{"schemaVersion":1,"text":"最终回答"},"nodes":[]}',
|
|
74
|
+
schema,
|
|
75
|
+
) == {
|
|
76
|
+
"finalAnswer": {"schemaVersion": 1, "text": "最终回答"},
|
|
77
|
+
"nodes": [],
|
|
78
|
+
}
|
|
79
|
+
with pytest.raises(SafeEngineError) as invalid:
|
|
80
|
+
final_output('{"nodes":[{"text":"中间节点","total":2}]}', schema)
|
|
81
|
+
assert invalid.value.code == "OUTPUT_VALIDATION_FAILED"
|
|
@@ -174,6 +174,7 @@ def fixture(run_id: str, idempotency_key: str) -> RunRecord:
|
|
|
174
174
|
"workflowVersion": "workflow-1",
|
|
175
175
|
"promptVersion": "prompt-1",
|
|
176
176
|
"routeVersion": "route-1",
|
|
177
|
+
"outputSchemaVersion": "output-schema-1",
|
|
177
178
|
"toolSchemaVersion": "schema-1",
|
|
178
179
|
"manifestDigest": "0" * 64,
|
|
179
180
|
},
|
|
@@ -210,6 +210,7 @@ function runtimeRelease(): AgentRuntimeOperation["release"] {
|
|
|
210
210
|
workflowVersion: "workflow-1",
|
|
211
211
|
promptVersion: "prompt-1",
|
|
212
212
|
routeVersion: "route-1",
|
|
213
|
+
outputSchemaVersion: "output-schema-1",
|
|
213
214
|
toolSchemaVersion: "schema-1",
|
|
214
215
|
manifestDigest: "0".repeat(64),
|
|
215
216
|
};
|
|
@@ -41,6 +41,7 @@ export function auditAgentRun(input: {
|
|
|
41
41
|
workflowVersion: input.release.workflowVersion,
|
|
42
42
|
promptVersion: input.release.promptVersion,
|
|
43
43
|
routeVersion: input.release.routeVersion,
|
|
44
|
+
outputSchemaVersion: input.release.outputSchemaVersion,
|
|
44
45
|
toolSchemaVersion: input.release.toolSchemaVersion,
|
|
45
46
|
traceId: input.traceId,
|
|
46
47
|
},
|
|
@@ -56,6 +56,7 @@ export interface AgentRuntimeRelease {
|
|
|
56
56
|
readonly workflowVersion: string;
|
|
57
57
|
readonly promptVersion: string;
|
|
58
58
|
readonly routeVersion: string;
|
|
59
|
+
readonly outputSchemaVersion: string;
|
|
59
60
|
readonly toolSchemaVersion: string;
|
|
60
61
|
readonly manifestDigest: string;
|
|
61
62
|
}
|
|
@@ -30,7 +30,6 @@ import {
|
|
|
30
30
|
} from "./provider-input-governance.js";
|
|
31
31
|
import {
|
|
32
32
|
rotateVisionArtifacts,
|
|
33
|
-
validateOutputSchema,
|
|
34
33
|
validateVisionArtifacts,
|
|
35
34
|
} from "./vision-input.js";
|
|
36
35
|
import { requireVisionOperationReadiness } from "./vision-readiness.js";
|
|
@@ -55,6 +54,7 @@ import {
|
|
|
55
54
|
buildRegisteredOperations,
|
|
56
55
|
type RegisteredOperation,
|
|
57
56
|
} from "./run-operation-registry.js";
|
|
57
|
+
import { validateRegisteredOutputSchema } from "./run-output-schema.js";
|
|
58
58
|
export { AgentRunFacadeError } from "./run-errors.js";
|
|
59
59
|
|
|
60
60
|
type ServerRuntime = ReturnType<typeof createServerRuntime>;
|
|
@@ -157,7 +157,7 @@ export class AgentRunFacade {
|
|
|
157
157
|
400,
|
|
158
158
|
);
|
|
159
159
|
}
|
|
160
|
-
|
|
160
|
+
validateRegisteredOutputSchema(plan.outputSchema, operation);
|
|
161
161
|
const rotatedArtifacts = await rotateVisionArtifacts(
|
|
162
162
|
artifacts,
|
|
163
163
|
operation.definition,
|
|
@@ -13,6 +13,7 @@ export interface RegisteredOperation {
|
|
|
13
13
|
feature: ReturnType<typeof findFeature>;
|
|
14
14
|
tools: AgentRuntimeOperation["allowedTools"];
|
|
15
15
|
release: AgentRuntimeOperation["release"];
|
|
16
|
+
outputSchema?: NonNullable<ModuleManifest["jsonSchemas"]>[number]["schema"];
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export function buildRegisteredOperations(
|
|
@@ -26,6 +27,11 @@ export function buildRegisteredOperations(
|
|
|
26
27
|
]),
|
|
27
28
|
);
|
|
28
29
|
const features = modules.flatMap(({ features }) => features);
|
|
30
|
+
const schemas = new Map(
|
|
31
|
+
modules.flatMap(({ jsonSchemas }) =>
|
|
32
|
+
(jsonSchemas ?? []).map((schema) => [schema.key, schema.schema] as const),
|
|
33
|
+
),
|
|
34
|
+
);
|
|
29
35
|
return new Map(
|
|
30
36
|
modules.flatMap((module) =>
|
|
31
37
|
(module.aiOperations ?? []).map((definition) => {
|
|
@@ -37,6 +43,7 @@ export function buildRegisteredOperations(
|
|
|
37
43
|
...handler,
|
|
38
44
|
feature: findFeature(features, definition.featureKey),
|
|
39
45
|
tools: toolDefinitions(modules, definition.allowedToolKeys),
|
|
46
|
+
outputSchema: schemas.get(definition.outputSchemaKey),
|
|
40
47
|
release: buildAgentRuntimeRelease(
|
|
41
48
|
modules,
|
|
42
49
|
module,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ModuleAiJsonValue } from "@southwind-ai/server-sdk";
|
|
2
|
+
import type { RegisteredOperation } from "./run-operation-registry.js";
|
|
3
|
+
import { validateOutputSchema } from "./vision-input.js";
|
|
4
|
+
|
|
5
|
+
export function validateRegisteredOutputSchema(
|
|
6
|
+
schema: ModuleAiJsonValue | undefined,
|
|
7
|
+
operation: RegisteredOperation,
|
|
8
|
+
): void {
|
|
9
|
+
validateOutputSchema(
|
|
10
|
+
schema,
|
|
11
|
+
operation.definition,
|
|
12
|
+
operation.registration?.supervisor,
|
|
13
|
+
operation.outputSchema,
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -20,6 +20,7 @@ export function buildAgentRuntimeRelease(
|
|
|
20
20
|
const coordinates = {
|
|
21
21
|
operationVersion: module.version,
|
|
22
22
|
...versions,
|
|
23
|
+
outputSchemaVersion: outputSchemaDigest(modules, definition),
|
|
23
24
|
toolSchemaVersion: operationSchemaDigest(modules, definition),
|
|
24
25
|
manifestDigest: digest(module),
|
|
25
26
|
};
|
|
@@ -29,6 +30,16 @@ export function buildAgentRuntimeRelease(
|
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
function outputSchemaDigest(
|
|
34
|
+
modules: readonly ModuleManifest[],
|
|
35
|
+
definition: ModuleAiOperationDefinition,
|
|
36
|
+
): string {
|
|
37
|
+
const schema = modules
|
|
38
|
+
.flatMap(({ jsonSchemas }) => jsonSchemas ?? [])
|
|
39
|
+
.find(({ key }) => key === definition.outputSchemaKey)?.schema;
|
|
40
|
+
return digest(schema);
|
|
41
|
+
}
|
|
42
|
+
|
|
32
43
|
function operationSchemaDigest(
|
|
33
44
|
modules: readonly ModuleManifest[],
|
|
34
45
|
definition: ModuleAiOperationDefinition,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
composeSystemAgentFinalAnswerSchemaV1,
|
|
4
|
+
type ModuleAiOperationDefinition,
|
|
5
|
+
} from "@southwind-ai/modules";
|
|
6
|
+
import { validateOutputSchema } from "./vision-input.js";
|
|
7
|
+
|
|
8
|
+
const definition: ModuleAiOperationDefinition = {
|
|
9
|
+
key: "business.copilot",
|
|
10
|
+
kind: "agent",
|
|
11
|
+
featureKey: "business.copilot",
|
|
12
|
+
permissionKey: "business.copilot.execute",
|
|
13
|
+
auditAction: "business.copilot.execute",
|
|
14
|
+
inputSchemaKey: "business.copilot.input.v1",
|
|
15
|
+
outputSchemaKey: "business.copilot.output.v1",
|
|
16
|
+
requiredCapabilities: ["text-generation"],
|
|
17
|
+
allowedToolKeys: [],
|
|
18
|
+
dataClassification: "internal",
|
|
19
|
+
execution: "interactive",
|
|
20
|
+
providerEgress: { mode: "configured-endpoint", deploymentKey: "ai.default" },
|
|
21
|
+
providerDataPolicy: {
|
|
22
|
+
inputFields: [
|
|
23
|
+
{
|
|
24
|
+
jsonPointer: "/prompt",
|
|
25
|
+
classification: "internal",
|
|
26
|
+
dlpAction: "redact",
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
envelopeFields: [
|
|
30
|
+
{
|
|
31
|
+
field: "instructions",
|
|
32
|
+
classification: "internal",
|
|
33
|
+
dlpAction: "redact",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
field: "user-prompt",
|
|
37
|
+
classification: "internal",
|
|
38
|
+
dlpAction: "redact",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
field: "output-schema",
|
|
42
|
+
classification: "internal",
|
|
43
|
+
dlpAction: "redact",
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
toolNetworkPolicy: { mode: "forbidden" },
|
|
48
|
+
limits: {
|
|
49
|
+
maxDurationSeconds: 60,
|
|
50
|
+
maxInputBytes: 4_096,
|
|
51
|
+
maxOutputBytes: 1_048_576,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const supervisor = {
|
|
56
|
+
contractVersion: 1,
|
|
57
|
+
finalAnswerSchemaVersion: 1,
|
|
58
|
+
} as const;
|
|
59
|
+
|
|
60
|
+
describe("Supervisor output schema admission", () => {
|
|
61
|
+
test("只接受与 Manifest 完全一致的 canonical Schema", () => {
|
|
62
|
+
const schema = composeSystemAgentFinalAnswerSchemaV1({
|
|
63
|
+
properties: { citations: { type: "array" } },
|
|
64
|
+
});
|
|
65
|
+
expect(() =>
|
|
66
|
+
validateOutputSchema(schema, definition, supervisor, schema),
|
|
67
|
+
).not.toThrow();
|
|
68
|
+
expect(() =>
|
|
69
|
+
validateOutputSchema(
|
|
70
|
+
composeSystemAgentFinalAnswerSchemaV1(),
|
|
71
|
+
definition,
|
|
72
|
+
supervisor,
|
|
73
|
+
schema,
|
|
74
|
+
),
|
|
75
|
+
).toThrow("与 Manifest 声明不一致");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("拒绝缺失 Schema 或 legacy text Schema", () => {
|
|
79
|
+
expect(() =>
|
|
80
|
+
validateOutputSchema(undefined, definition, supervisor),
|
|
81
|
+
).toThrow("必须提供 typed 输出 Schema");
|
|
82
|
+
expect(() =>
|
|
83
|
+
validateOutputSchema(
|
|
84
|
+
{
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: { text: { type: "string" } },
|
|
87
|
+
required: ["text"],
|
|
88
|
+
additionalProperties: false,
|
|
89
|
+
},
|
|
90
|
+
definition,
|
|
91
|
+
supervisor,
|
|
92
|
+
),
|
|
93
|
+
).toThrow("canonical finalAnswer V1");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
assertSystemAgentFinalAnswerSchemaV1,
|
|
3
|
+
type ModuleAiOperationDefinition,
|
|
4
|
+
} from "@southwind-ai/modules";
|
|
2
5
|
import type {
|
|
3
6
|
ModuleAiArtifactRef,
|
|
7
|
+
ModuleAiOperationHandlerRegistration,
|
|
4
8
|
ModuleAiJsonValue,
|
|
5
9
|
} from "@southwind-ai/server-sdk";
|
|
6
10
|
import type { AiArtifactRuntime } from "../ai-artifact/runtime.js";
|
|
@@ -56,8 +60,13 @@ export async function rotateVisionArtifacts(
|
|
|
56
60
|
export function validateOutputSchema(
|
|
57
61
|
schema: ModuleAiJsonValue | undefined,
|
|
58
62
|
definition: ModuleAiOperationDefinition,
|
|
63
|
+
supervisor?: ModuleAiOperationHandlerRegistration["supervisor"],
|
|
64
|
+
declaredSchema?: ModuleAiJsonValue,
|
|
59
65
|
) {
|
|
60
|
-
if (schema === undefined)
|
|
66
|
+
if (schema === undefined) {
|
|
67
|
+
if (supervisor) throw invalid("Supervisor 必须提供 typed 输出 Schema");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
61
70
|
if (
|
|
62
71
|
!schema ||
|
|
63
72
|
typeof schema !== "object" ||
|
|
@@ -70,6 +79,16 @@ export function validateOutputSchema(
|
|
|
70
79
|
400,
|
|
71
80
|
);
|
|
72
81
|
}
|
|
82
|
+
if (supervisor) {
|
|
83
|
+
try {
|
|
84
|
+
assertSystemAgentFinalAnswerSchemaV1(schema);
|
|
85
|
+
} catch {
|
|
86
|
+
throw invalid("Supervisor 输出 Schema 缺少 canonical finalAnswer V1");
|
|
87
|
+
}
|
|
88
|
+
if (canonicalJson(schema) !== canonicalJson(declaredSchema)) {
|
|
89
|
+
throw invalid("Supervisor 运行时输出 Schema 与 Manifest 声明不一致");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
73
92
|
if (
|
|
74
93
|
definition.providerEgress?.mode === "configured-endpoint" &&
|
|
75
94
|
!definition.providerDataPolicy?.envelopeFields.some(
|
|
@@ -84,6 +103,22 @@ export function validateOutputSchema(
|
|
|
84
103
|
}
|
|
85
104
|
}
|
|
86
105
|
|
|
106
|
+
function canonicalJson(value: unknown): string {
|
|
107
|
+
return JSON.stringify(canonicalValue(value));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function canonicalValue(value: unknown): unknown {
|
|
111
|
+
if (Array.isArray(value)) return value.map(canonicalValue);
|
|
112
|
+
if (value !== null && typeof value === "object") {
|
|
113
|
+
return Object.fromEntries(
|
|
114
|
+
Object.entries(value as Record<string, unknown>)
|
|
115
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
116
|
+
.map(([key, item]) => [key, canonicalValue(item)]),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
|
|
87
122
|
export function validateVisionArtifacts(
|
|
88
123
|
artifacts: readonly Pick<
|
|
89
124
|
ModuleAiArtifactRef,
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { boundedJsonBodyErrorResponse } from "@southwind-ai/license";
|
|
2
|
+
import { LicensePairingError } from "./pairing-service.js";
|
|
3
|
+
|
|
4
|
+
export function createPairingCorsPolicy(
|
|
5
|
+
env: Record<string, string | undefined>,
|
|
6
|
+
) {
|
|
7
|
+
const configured = env.WINDY_LICENSE_PAIRING_ORIGINS;
|
|
8
|
+
const candidates =
|
|
9
|
+
configured === undefined
|
|
10
|
+
? env.NODE_ENV === "production"
|
|
11
|
+
? []
|
|
12
|
+
: ["http://localhost:9843"]
|
|
13
|
+
: configured.split(",").map((value) => value.trim());
|
|
14
|
+
const origins = new Set(candidates.filter(isExactHttpOrigin));
|
|
15
|
+
return {
|
|
16
|
+
enabled: origins.size > 0,
|
|
17
|
+
accept: (request: Request) => {
|
|
18
|
+
const origin = request.headers.get("origin") || "";
|
|
19
|
+
return origins.has(origin) ? origin : undefined;
|
|
20
|
+
},
|
|
21
|
+
headers: (origin: string) => ({
|
|
22
|
+
"access-control-allow-origin": origin,
|
|
23
|
+
"access-control-allow-methods": "POST, OPTIONS",
|
|
24
|
+
"access-control-allow-headers": "Content-Type",
|
|
25
|
+
"access-control-allow-private-network": "true",
|
|
26
|
+
vary: "Origin, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Request-Private-Network",
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function allowedPreflightHeaders(value: string | null): boolean {
|
|
32
|
+
if (!value) return true;
|
|
33
|
+
return value
|
|
34
|
+
.split(",")
|
|
35
|
+
.map((header) => header.trim().toLowerCase())
|
|
36
|
+
.every((header) => header === "content-type");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function requireCallbackOrigin(
|
|
40
|
+
value: unknown,
|
|
41
|
+
request: Request,
|
|
42
|
+
): string {
|
|
43
|
+
const callbackOrigin = requiredText(value, "callbackOrigin");
|
|
44
|
+
if (!isExactHttpOrigin(callbackOrigin)) {
|
|
45
|
+
throw new LicensePairingError(
|
|
46
|
+
"PAIRING_CALLBACK_ORIGIN_INVALID",
|
|
47
|
+
400,
|
|
48
|
+
"callbackOrigin 必须是精确的 HTTP(S) Origin",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
if (request.headers.get("origin") !== callbackOrigin) {
|
|
52
|
+
throw new LicensePairingError(
|
|
53
|
+
"PAIRING_CALLBACK_ORIGIN_MISMATCH",
|
|
54
|
+
403,
|
|
55
|
+
"请求 Origin 与 callbackOrigin 不一致",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return callbackOrigin;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function requireDeliveryContentType(request: Request): void {
|
|
62
|
+
const value = request.headers.get("content-type")?.toLowerCase() || "";
|
|
63
|
+
if (value.startsWith("application/json") || value.startsWith("text/plain")) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
throw new LicensePairingError(
|
|
67
|
+
"PAIRING_CONTENT_TYPE_INVALID",
|
|
68
|
+
415,
|
|
69
|
+
"投递只接受 application/json 或 text/plain JSON",
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function requiredText(value: unknown, name: string): string {
|
|
74
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
75
|
+
throw new LicensePairingError(
|
|
76
|
+
"PAIRING_REQUEST_INVALID",
|
|
77
|
+
400,
|
|
78
|
+
`${name} 必须为非空字符串`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function pairingErrorResponse(
|
|
83
|
+
error: unknown,
|
|
84
|
+
headers: Record<string, string> = {},
|
|
85
|
+
): Response {
|
|
86
|
+
const bodyError = boundedJsonBodyErrorResponse(error);
|
|
87
|
+
if (bodyError) return noStore(bodyError, headers);
|
|
88
|
+
if (error instanceof LicensePairingError) {
|
|
89
|
+
return json(
|
|
90
|
+
{ error: error.code, reason: error.message, ...error.details },
|
|
91
|
+
error.status,
|
|
92
|
+
headers,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function corsRejected(): Response {
|
|
99
|
+
return json({ error: "PAIRING_CORS_REJECTED" }, 403, { vary: "Origin" });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function json(
|
|
103
|
+
body: unknown,
|
|
104
|
+
status = 200,
|
|
105
|
+
headers: Record<string, string> = {},
|
|
106
|
+
): Response {
|
|
107
|
+
return Response.json(body, { status, headers: responseHeaders(headers) });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function noStore(
|
|
111
|
+
response: Response,
|
|
112
|
+
headers: Record<string, string> = {},
|
|
113
|
+
) {
|
|
114
|
+
const nextHeaders = new Headers(response.headers);
|
|
115
|
+
for (const [key, value] of Object.entries(responseHeaders(headers))) {
|
|
116
|
+
nextHeaders.set(key, value);
|
|
117
|
+
}
|
|
118
|
+
return new Response(response.body, {
|
|
119
|
+
status: response.status,
|
|
120
|
+
headers: nextHeaders,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function responseHeaders(headers: Record<string, string>) {
|
|
125
|
+
return { ...headers, "cache-control": "no-store" };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isExactHttpOrigin(value: string): boolean {
|
|
129
|
+
try {
|
|
130
|
+
const url = new URL(value);
|
|
131
|
+
return (
|
|
132
|
+
(url.protocol === "http:" || url.protocol === "https:") &&
|
|
133
|
+
url.origin === value
|
|
134
|
+
);
|
|
135
|
+
} catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|