create-windy 0.6.0 → 0.6.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/dist/cli.js +142 -37
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/_bunfig.toml +5 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/app.py +27 -20
- package/template/apps/embedding-server/src/southwind_embedding_server/config.py +111 -4
- package/template/apps/embedding-server/src/southwind_embedding_server/contracts.py +13 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/pin_cli.py +62 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/readiness.py +259 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/readiness_contracts.py +221 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/readiness_http.py +91 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/run_service.py +23 -3
- package/template/apps/embedding-server/tests/test_config.py +13 -0
- package/template/apps/embedding-server/tests/test_readiness.py +202 -0
- package/template/apps/embedding-server/tests/test_run_http.py +76 -4
- package/template/apps/embedding-server/tests/test_run_service.py +3 -1
- package/template/apps/server/Dockerfile +10 -5
- package/template/apps/server/src/agent/drizzle-screenshot-intake-repository.integration.test.ts +3 -0
- package/template/apps/server/src/agent/run-facade.ts +2 -1
- package/template/apps/server/src/agent/run-route-screenshot-context.test.ts +13 -0
- package/template/apps/server/src/agent/runtime-bootstrap.ts +2 -0
- package/template/apps/server/src/agent/screenshot-context-admission.test.ts +249 -0
- package/template/apps/server/src/agent/screenshot-context-attestation.ts +60 -0
- package/template/apps/server/src/agent/screenshot-intake-bootstrap.ts +2 -4
- package/template/apps/server/src/agent/screenshot-intake-test-support.ts +4 -0
- package/template/apps/server/src/agent/screenshot-intake-v2-validation.ts +24 -0
- package/template/apps/server/src/agent/screenshot-intake-v2.test.ts +7 -0
- package/template/apps/server/src/agent/screenshot-intake-v2.ts +8 -10
- package/template/apps/server/src/agent/screenshot-run-binding.ts +17 -2
- package/template/apps/server/src/agent/vision-readiness-pin.ts +6 -48
- package/template/apps/server/src/ai-runtime-readiness/deployment-pin.ts +69 -0
- package/template/apps/server/src/embedding/contracts.ts +6 -1
- package/template/apps/server/src/embedding/facade-readiness.test.ts +307 -0
- package/template/apps/server/src/embedding/facade.ts +8 -6
- package/template/apps/server/src/embedding/readiness-admission.ts +26 -0
- package/template/apps/server/src/embedding/remote-runtime.test.ts +85 -0
- package/template/apps/server/src/embedding/remote-runtime.ts +21 -0
- package/template/apps/server/src/embedding/run-fingerprint.test.ts +26 -0
- package/template/apps/server/src/embedding/run-fingerprint.ts +11 -2
- package/template/apps/server/src/module-host/ai-operation-port.test.ts +72 -0
- package/template/apps/server/src/module-host/ai-operation-port.ts +43 -0
- package/template/apps/server/src/module-host/ai-operation-speech-normalize.test.ts +186 -0
- package/template/apps/web/Dockerfile +1 -1
- package/template/docker-compose.yml +11 -0
- package/template/docs/architecture/ai-runtime-readiness.md +6 -0
- package/template/docs/platform/vision-ai-runtime.md +22 -1
- package/template/package.json +2 -0
- package/template/packages/config/package.json +1 -1
- package/template/packages/database/package.json +1 -1
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/modules/index.ts +1 -0
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/ai-operation-composition.ts +2 -0
- package/template/packages/modules/src/ai-screenshot-context.test.ts +72 -0
- package/template/packages/modules/src/ai-screenshot-context.ts +50 -0
- package/template/packages/modules/src/manifest.ts +8 -0
- package/template/packages/server-sdk/package.json +1 -1
- package/template/packages/shared/index.ts +3 -0
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/screenshot-context-digest.test.ts +46 -0
- package/template/packages/shared/src/screenshot-context-digest.ts +72 -0
|
@@ -9,6 +9,7 @@ export * from "./src/system-module-catalog.js";
|
|
|
9
9
|
export * from "./src/system-api-permissions.js";
|
|
10
10
|
export * from "./src/tool-composition.js";
|
|
11
11
|
export * from "./src/ai-network-policy.js";
|
|
12
|
+
export * from "./src/ai-screenshot-context.js";
|
|
12
13
|
export * from "./src/project-module-assembly.js";
|
|
13
14
|
export * from "./src/module-host-range.js";
|
|
14
15
|
export * from "./src/admin-ui-manifest.js";
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
} from "./manifest.js";
|
|
5
5
|
import { validateAiNetworkPolicy } from "./ai-network-policy.js";
|
|
6
6
|
import { validateAiOperationReadinessContract } from "./ai-operation-readiness-contract.js";
|
|
7
|
+
import { validateAiScreenshotContext } from "./ai-screenshot-context.js";
|
|
7
8
|
|
|
8
9
|
interface AiOperationCatalogs {
|
|
9
10
|
permissions: ReadonlySet<string>;
|
|
@@ -106,6 +107,7 @@ function validateOperation(
|
|
|
106
107
|
validateProviderDataPolicy(operation);
|
|
107
108
|
validateLimits(operation);
|
|
108
109
|
validateAiOperationReadinessContract(operation);
|
|
110
|
+
validateAiScreenshotContext(operation);
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
const providerDataFields = new Set([
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { validateModuleComposition } from "./composition.js";
|
|
3
|
+
import { fixture, operation } from "./ai-operation-composition.test-support.js";
|
|
4
|
+
|
|
5
|
+
describe("AI Screenshot context composition", () => {
|
|
6
|
+
test("只允许 readiness Vision Operation 固定结构化 input pointer", () => {
|
|
7
|
+
const module = fixture("business");
|
|
8
|
+
const definition = operation("business.vision");
|
|
9
|
+
module.aiOperations = [
|
|
10
|
+
{
|
|
11
|
+
...definition,
|
|
12
|
+
inputModalities: ["text", "image"],
|
|
13
|
+
requiredCapabilities: ["text-generation", "vision"],
|
|
14
|
+
limits: {
|
|
15
|
+
...definition.limits,
|
|
16
|
+
maxArtifacts: 1,
|
|
17
|
+
maxPixelsPerArtifact: 20_000_000,
|
|
18
|
+
},
|
|
19
|
+
readiness: readiness(),
|
|
20
|
+
screenshotContext: {
|
|
21
|
+
contractVersion: 1,
|
|
22
|
+
contextInputPointer: "/uiContext",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
expect(() => validateModuleComposition([module])).not.toThrow();
|
|
28
|
+
module.aiOperations[0] = {
|
|
29
|
+
...module.aiOperations[0]!,
|
|
30
|
+
screenshotContext: {
|
|
31
|
+
contractVersion: 1,
|
|
32
|
+
contextInputPointer: "/uiContext/__proto__",
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
expect(() => validateModuleComposition([module])).toThrow(
|
|
36
|
+
"Screenshot context input JSON Pointer 无效",
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("文本 Operation 不能伪装 Screenshot context opt-in", () => {
|
|
41
|
+
const module = fixture("business");
|
|
42
|
+
module.aiOperations = [
|
|
43
|
+
{
|
|
44
|
+
...operation("business.answer"),
|
|
45
|
+
screenshotContext: {
|
|
46
|
+
contractVersion: 1,
|
|
47
|
+
contextInputPointer: "/uiContext",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
expect(() => validateModuleComposition([module])).toThrow(
|
|
52
|
+
"只能绑定 readiness Vision Operation",
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function readiness() {
|
|
58
|
+
return {
|
|
59
|
+
contractVersion: 1 as const,
|
|
60
|
+
variant: "vision",
|
|
61
|
+
inputModalities: ["text", "image"] as const,
|
|
62
|
+
outputModalities: ["text"] as const,
|
|
63
|
+
expectedDeployment: {
|
|
64
|
+
key: "ai.openai-compatible",
|
|
65
|
+
identity: "windy-deployment-v1_f31e8a65-92cb-4d03-8a77-3c1b5e9d4f04",
|
|
66
|
+
releaseDigest: `sha256:${"d".repeat(64)}` as const,
|
|
67
|
+
offersDigest: `sha256:${"e".repeat(64)}` as const,
|
|
68
|
+
},
|
|
69
|
+
minConcurrentRuns: 1,
|
|
70
|
+
minQueueDepth: 0,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ModuleAiOperationDefinition } from "./manifest.js";
|
|
2
|
+
|
|
3
|
+
const MAX_POINTER_LENGTH = 256;
|
|
4
|
+
|
|
5
|
+
export function validateAiScreenshotContext(
|
|
6
|
+
operation: ModuleAiOperationDefinition,
|
|
7
|
+
): void {
|
|
8
|
+
const binding = operation.screenshotContext;
|
|
9
|
+
if (!binding) return;
|
|
10
|
+
if (
|
|
11
|
+
binding.contractVersion !== 1 ||
|
|
12
|
+
operation.kind !== "agent" ||
|
|
13
|
+
!operation.inputModalities?.includes("image") ||
|
|
14
|
+
!operation.requiredCapabilities.includes("vision") ||
|
|
15
|
+
!operation.readiness
|
|
16
|
+
) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`${operation.key} Screenshot context 只能绑定 readiness Vision Operation`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
parseAiScreenshotContextInputPointer(binding.contextInputPointer);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseAiScreenshotContextInputPointer(
|
|
25
|
+
pointer: string,
|
|
26
|
+
): readonly string[] {
|
|
27
|
+
if (
|
|
28
|
+
pointer.length > MAX_POINTER_LENGTH ||
|
|
29
|
+
!pointer.startsWith("/") ||
|
|
30
|
+
pointer === "/" ||
|
|
31
|
+
pointer.endsWith("/") ||
|
|
32
|
+
pointer.includes("//") ||
|
|
33
|
+
/~(?:[^01]|$)/u.test(pointer)
|
|
34
|
+
) {
|
|
35
|
+
throw new Error("Screenshot context input JSON Pointer 无效");
|
|
36
|
+
}
|
|
37
|
+
const segments = pointer
|
|
38
|
+
.slice(1)
|
|
39
|
+
.split("/")
|
|
40
|
+
.map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~"));
|
|
41
|
+
if (
|
|
42
|
+
segments.some(
|
|
43
|
+
(segment) =>
|
|
44
|
+
!segment || ["__proto__", "prototype", "constructor"].includes(segment),
|
|
45
|
+
)
|
|
46
|
+
) {
|
|
47
|
+
throw new Error("Screenshot context input JSON Pointer 无效");
|
|
48
|
+
}
|
|
49
|
+
return segments;
|
|
50
|
+
}
|
|
@@ -189,6 +189,12 @@ export interface AiOperationReadinessContractV1 {
|
|
|
189
189
|
readonly minQueueDepth: number;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
/** Host 对该 pointer 指向的结构化 Run input 重算 Screenshot context digest。 */
|
|
193
|
+
export interface AiScreenshotContextBindingV1 {
|
|
194
|
+
readonly contractVersion: 1;
|
|
195
|
+
readonly contextInputPointer: string;
|
|
196
|
+
}
|
|
197
|
+
|
|
192
198
|
export interface ModuleAiOperationDefinition {
|
|
193
199
|
key: string;
|
|
194
200
|
kind: AiOperationKind;
|
|
@@ -201,6 +207,8 @@ export interface ModuleAiOperationDefinition {
|
|
|
201
207
|
inputModalities?: readonly AiOperationInputModality[];
|
|
202
208
|
/** additive V1:固定 variant、模态与不可变 deployment pin。 */
|
|
203
209
|
readiness?: AiOperationReadinessContractV1;
|
|
210
|
+
/** 仅显式 opt-in 的 Vision Operation 可把 Screenshot Intake 绑定到结构化上下文。 */
|
|
211
|
+
screenshotContext?: AiScreenshotContextBindingV1;
|
|
204
212
|
requiredCapabilities: readonly string[];
|
|
205
213
|
allowedToolKeys: readonly string[];
|
|
206
214
|
dataClassification: AiOperationDataClassification;
|
|
@@ -23,5 +23,8 @@ export * from "./src/rbac.js";
|
|
|
23
23
|
export * from "./src/ruoyi.js";
|
|
24
24
|
export * from "./src/scheduler.js";
|
|
25
25
|
export * from "./src/search-provider.js";
|
|
26
|
+
// @windy-module system.agent begin
|
|
27
|
+
export * from "./src/screenshot-context-digest.js";
|
|
28
|
+
// @windy-module system.agent end
|
|
26
29
|
export * from "./src/stable-key.js";
|
|
27
30
|
export * from "./src/workflow.js";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
canonicalizeScreenshotContextV1,
|
|
4
|
+
computeScreenshotContextDigestV1,
|
|
5
|
+
isScreenshotContextDigestV1,
|
|
6
|
+
} from "./screenshot-context-digest.js";
|
|
7
|
+
|
|
8
|
+
describe("Screenshot context digest V1", () => {
|
|
9
|
+
test("Web 与 Host 对 key 顺序不同的结构化上下文得到同一摘要", async () => {
|
|
10
|
+
const first = {
|
|
11
|
+
version: 1,
|
|
12
|
+
pageKey: "case.detail",
|
|
13
|
+
overlayStack: [{ title: "导入物证", kind: "dialog" }],
|
|
14
|
+
};
|
|
15
|
+
const reordered = {
|
|
16
|
+
overlayStack: [{ kind: "dialog", title: "导入物证" }],
|
|
17
|
+
pageKey: "case.detail",
|
|
18
|
+
version: 1,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
expect(canonicalizeScreenshotContextV1(first)).toBe(
|
|
22
|
+
'{"overlayStack":[{"kind":"dialog","title":"导入物证"}],"pageKey":"case.detail","version":1}',
|
|
23
|
+
);
|
|
24
|
+
expect(await computeScreenshotContextDigestV1(first)).toBe(
|
|
25
|
+
"sha256:3e274a367806437443ac78bb6955445c5af2ba924f89d113ba6f652c1d6a3905",
|
|
26
|
+
);
|
|
27
|
+
expect(await computeScreenshotContextDigestV1(reordered)).toBe(
|
|
28
|
+
await computeScreenshotContextDigestV1(first),
|
|
29
|
+
);
|
|
30
|
+
expect(
|
|
31
|
+
isScreenshotContextDigestV1(
|
|
32
|
+
await computeScreenshotContextDigestV1(first),
|
|
33
|
+
),
|
|
34
|
+
).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("拒绝非有限数值与非 plain JSON", async () => {
|
|
38
|
+
expect(() => canonicalizeScreenshotContextV1(Number.NaN)).toThrow(
|
|
39
|
+
"数值无效",
|
|
40
|
+
);
|
|
41
|
+
expect(() => canonicalizeScreenshotContextV1(new Date() as never)).toThrow(
|
|
42
|
+
"plain JSON",
|
|
43
|
+
);
|
|
44
|
+
expect(isScreenshotContextDigestV1(`sha256:${"A".repeat(64)}`)).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { JsonValue } from "./json.js";
|
|
2
|
+
|
|
3
|
+
const MAX_DEPTH = 32;
|
|
4
|
+
const MAX_NODES = 4_096;
|
|
5
|
+
const MAX_CANONICAL_BYTES = 256 * 1_024;
|
|
6
|
+
const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
7
|
+
|
|
8
|
+
export type ScreenshotContextDigestV1 = `sha256:${string}`;
|
|
9
|
+
|
|
10
|
+
/** Web/Host 共用的稳定 JSON:object key 排序,array 顺序与 JSON 数值语义保持不变。 */
|
|
11
|
+
export function canonicalizeScreenshotContextV1(value: JsonValue): string {
|
|
12
|
+
let nodes = 0;
|
|
13
|
+
const visit = (item: JsonValue, depth: number): string => {
|
|
14
|
+
nodes += 1;
|
|
15
|
+
if (nodes > MAX_NODES || depth > MAX_DEPTH) {
|
|
16
|
+
throw new Error("Screenshot context 超过稳定摘要边界");
|
|
17
|
+
}
|
|
18
|
+
if (
|
|
19
|
+
item === null ||
|
|
20
|
+
typeof item === "boolean" ||
|
|
21
|
+
typeof item === "string"
|
|
22
|
+
) {
|
|
23
|
+
return JSON.stringify(item);
|
|
24
|
+
}
|
|
25
|
+
if (typeof item === "number") {
|
|
26
|
+
if (!Number.isFinite(item))
|
|
27
|
+
throw new Error("Screenshot context 数值无效");
|
|
28
|
+
return JSON.stringify(item);
|
|
29
|
+
}
|
|
30
|
+
if (Array.isArray(item)) {
|
|
31
|
+
if (item.length > MAX_NODES) {
|
|
32
|
+
throw new Error("Screenshot context 数组超过稳定摘要边界");
|
|
33
|
+
}
|
|
34
|
+
return `[${item.map((entry) => visit(entry, depth + 1)).join(",")}]`;
|
|
35
|
+
}
|
|
36
|
+
const prototype = Object.getPrototypeOf(item);
|
|
37
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
38
|
+
throw new Error("Screenshot context 必须是 plain JSON");
|
|
39
|
+
}
|
|
40
|
+
const keys = Object.keys(item).sort();
|
|
41
|
+
return `{${keys
|
|
42
|
+
.map(
|
|
43
|
+
(key) =>
|
|
44
|
+
`${JSON.stringify(key)}:${visit(item[key] as JsonValue, depth + 1)}`,
|
|
45
|
+
)
|
|
46
|
+
.join(",")}}`;
|
|
47
|
+
};
|
|
48
|
+
const canonical = visit(value, 0);
|
|
49
|
+
if (new TextEncoder().encode(canonical).byteLength > MAX_CANONICAL_BYTES) {
|
|
50
|
+
throw new Error("Screenshot context 超过稳定摘要字节边界");
|
|
51
|
+
}
|
|
52
|
+
return canonical;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function computeScreenshotContextDigestV1(
|
|
56
|
+
value: JsonValue,
|
|
57
|
+
): Promise<ScreenshotContextDigestV1> {
|
|
58
|
+
const bytes = new TextEncoder().encode(
|
|
59
|
+
canonicalizeScreenshotContextV1(value),
|
|
60
|
+
);
|
|
61
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
|
|
62
|
+
const hex = [...new Uint8Array(digest)]
|
|
63
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
64
|
+
.join("");
|
|
65
|
+
return `sha256:${hex}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function isScreenshotContextDigestV1(
|
|
69
|
+
value: unknown,
|
|
70
|
+
): value is ScreenshotContextDigestV1 {
|
|
71
|
+
return typeof value === "string" && DIGEST_PATTERN.test(value);
|
|
72
|
+
}
|