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
|
@@ -31,9 +31,9 @@ import {
|
|
|
31
31
|
bindingForScreenshot,
|
|
32
32
|
assertScreenshotOperation,
|
|
33
33
|
assertScreenshotProducerPlan,
|
|
34
|
+
canReserveScreenshot,
|
|
34
35
|
sameScreenshotArtifact,
|
|
35
36
|
sameScreenshotBinding,
|
|
36
|
-
sameScreenshotDeployment,
|
|
37
37
|
screenshotModuleName,
|
|
38
38
|
screenshotPurpose,
|
|
39
39
|
} from "./screenshot-intake-v2-validation.js";
|
|
@@ -42,8 +42,6 @@ const MAX_ENTRIES = 1_024;
|
|
|
42
42
|
const MAX_TTL_MS = 60_000;
|
|
43
43
|
const SWEEP_INTERVAL_MS = 10_000;
|
|
44
44
|
|
|
45
|
-
export type { ProjectAiScreenshotIntakeFactoryV2 } from "./screenshot-intake-durable.js";
|
|
46
|
-
|
|
47
45
|
export class ScreenshotIntakeRuntimeV2 {
|
|
48
46
|
readonly #records = new Map<string, ScreenshotIntakeRecordV2>();
|
|
49
47
|
readonly #sweeper: ReturnType<typeof setInterval>;
|
|
@@ -211,6 +209,7 @@ export class ScreenshotIntakeRuntimeV2 {
|
|
|
211
209
|
token: string,
|
|
212
210
|
operationKey: string,
|
|
213
211
|
deployment: AiRuntimeDeploymentV1 | undefined,
|
|
212
|
+
expectedContextDigest: string | null | undefined,
|
|
214
213
|
context: RequestGuardContext,
|
|
215
214
|
signal: AbortSignal = AbortSignal.timeout(10_000),
|
|
216
215
|
): Promise<{
|
|
@@ -226,13 +225,12 @@ export class ScreenshotIntakeRuntimeV2 {
|
|
|
226
225
|
context,
|
|
227
226
|
);
|
|
228
227
|
if (
|
|
229
|
-
!
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
!sameScreenshotDeployment(record.deployment, deployment)
|
|
228
|
+
!canReserveScreenshot(
|
|
229
|
+
record,
|
|
230
|
+
operationKey,
|
|
231
|
+
deployment,
|
|
232
|
+
expectedContextDigest,
|
|
233
|
+
)
|
|
236
234
|
) {
|
|
237
235
|
await this.cancel(record);
|
|
238
236
|
throw unavailable();
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AiRuntimeDeploymentV1,
|
|
3
3
|
ModuleAiArtifactRef,
|
|
4
|
+
ModuleAiJsonValue,
|
|
4
5
|
} from "@southwind-ai/server-sdk";
|
|
6
|
+
import type { ModuleAiOperationDefinition } from "@southwind-ai/modules";
|
|
5
7
|
import type { RequestGuardContext } from "../guards.js";
|
|
6
8
|
import { AgentRunFacadeError } from "./run-errors.js";
|
|
9
|
+
import { expectedScreenshotContextDigest } from "./screenshot-context-attestation.js";
|
|
7
10
|
import type { ScreenshotIntakeRuntimeV2 } from "./screenshot-intake-v2.js";
|
|
8
11
|
|
|
9
12
|
export interface ScreenshotRunBinding {
|
|
@@ -15,14 +18,26 @@ export interface ScreenshotRunBinding {
|
|
|
15
18
|
export async function reserveScreenshotForRun(
|
|
16
19
|
intake: ScreenshotIntakeRuntimeV2 | undefined,
|
|
17
20
|
token: string | undefined,
|
|
18
|
-
|
|
21
|
+
definition: ModuleAiOperationDefinition,
|
|
22
|
+
input: ModuleAiJsonValue,
|
|
19
23
|
deployment: AiRuntimeDeploymentV1 | undefined,
|
|
20
24
|
context: RequestGuardContext,
|
|
21
25
|
signal: AbortSignal,
|
|
22
26
|
): Promise<ScreenshotRunBinding | undefined> {
|
|
23
27
|
if (!token) return undefined;
|
|
24
28
|
if (!intake) throw unavailable();
|
|
25
|
-
|
|
29
|
+
const contextDigest = await expectedScreenshotContextDigest(
|
|
30
|
+
definition,
|
|
31
|
+
input,
|
|
32
|
+
);
|
|
33
|
+
return await intake.reserve(
|
|
34
|
+
token,
|
|
35
|
+
definition.key,
|
|
36
|
+
deployment,
|
|
37
|
+
contextDigest,
|
|
38
|
+
context,
|
|
39
|
+
signal,
|
|
40
|
+
);
|
|
26
41
|
}
|
|
27
42
|
|
|
28
43
|
function unavailable(): AgentRunFacadeError {
|
|
@@ -1,19 +1,15 @@
|
|
|
1
|
+
import type { AiRuntimeDeploymentV1 } from "@southwind-ai/server-sdk";
|
|
1
2
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
} from "
|
|
3
|
+
assertAiRuntimeReadinessPin,
|
|
4
|
+
sameAiRuntimeDeployment,
|
|
5
|
+
} from "../ai-runtime-readiness/deployment-pin.js";
|
|
5
6
|
import { AgentRuntimeError } from "./run-contracts.js";
|
|
6
7
|
|
|
7
8
|
export function sameVisionReadinessDeployment(
|
|
8
9
|
actual: AiRuntimeDeploymentV1,
|
|
9
10
|
expected: AiRuntimeDeploymentV1,
|
|
10
11
|
): boolean {
|
|
11
|
-
return (
|
|
12
|
-
actual.key === expected.key &&
|
|
13
|
-
actual.identity === expected.identity &&
|
|
14
|
-
actual.releaseDigest === expected.releaseDigest &&
|
|
15
|
-
actual.offersDigest === expected.offersDigest
|
|
16
|
-
);
|
|
12
|
+
return sameAiRuntimeDeployment(actual, expected);
|
|
17
13
|
}
|
|
18
14
|
|
|
19
15
|
export function unavailableVisionReadiness(): AgentRuntimeError {
|
|
@@ -28,47 +24,9 @@ export async function assertVisionReadinessPin(
|
|
|
28
24
|
expected: AiRuntimeDeploymentV1,
|
|
29
25
|
health: () => Promise<Response>,
|
|
30
26
|
): Promise<void> {
|
|
31
|
-
const response = await boundedHealth(health);
|
|
32
|
-
if (response.status !== 200) throw unavailableVisionReadiness();
|
|
33
|
-
if (!response.headers.get("content-type")?.includes("application/json")) {
|
|
34
|
-
throw unavailableVisionReadiness();
|
|
35
|
-
}
|
|
36
|
-
const length = Number(response.headers.get("content-length") ?? "0");
|
|
37
|
-
if (!Number.isSafeInteger(length) || length < 0 || length > 64 * 1_024) {
|
|
38
|
-
throw unavailableVisionReadiness();
|
|
39
|
-
}
|
|
40
|
-
let snapshot: unknown;
|
|
41
27
|
try {
|
|
42
|
-
|
|
43
|
-
if (new TextEncoder().encode(text).byteLength > 64 * 1_024) {
|
|
44
|
-
throw new Error("health body too large");
|
|
45
|
-
}
|
|
46
|
-
snapshot = JSON.parse(text);
|
|
28
|
+
await assertAiRuntimeReadinessPin(expected, health);
|
|
47
29
|
} catch {
|
|
48
30
|
throw unavailableVisionReadiness();
|
|
49
31
|
}
|
|
50
|
-
const parsed = parseAiRuntimeReadinessV1(snapshot, new Date().toISOString());
|
|
51
|
-
if (
|
|
52
|
-
!parsed.ok ||
|
|
53
|
-
parsed.value.status !== "ready" ||
|
|
54
|
-
!sameVisionReadinessDeployment(parsed.value.deployment, expected)
|
|
55
|
-
) {
|
|
56
|
-
throw unavailableVisionReadiness();
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function boundedHealth(
|
|
61
|
-
health: () => Promise<Response>,
|
|
62
|
-
): Promise<Response> {
|
|
63
|
-
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
64
|
-
try {
|
|
65
|
-
return await Promise.race([
|
|
66
|
-
health(),
|
|
67
|
-
new Promise<Response>((_, reject) => {
|
|
68
|
-
timeout = setTimeout(() => reject(unavailableVisionReadiness()), 5_000);
|
|
69
|
-
}),
|
|
70
|
-
]);
|
|
71
|
-
} finally {
|
|
72
|
-
if (timeout) clearTimeout(timeout);
|
|
73
|
-
}
|
|
74
32
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseAiRuntimeReadinessV1,
|
|
3
|
+
type AiRuntimeDeploymentV1,
|
|
4
|
+
} from "@southwind-ai/server-sdk";
|
|
5
|
+
|
|
6
|
+
export function sameAiRuntimeDeployment(
|
|
7
|
+
actual: AiRuntimeDeploymentV1,
|
|
8
|
+
expected: AiRuntimeDeploymentV1,
|
|
9
|
+
): boolean {
|
|
10
|
+
return (
|
|
11
|
+
actual.key === expected.key &&
|
|
12
|
+
actual.identity === expected.identity &&
|
|
13
|
+
actual.releaseDigest === expected.releaseDigest &&
|
|
14
|
+
actual.offersDigest === expected.offersDigest
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function assertAiRuntimeReadinessPin(
|
|
19
|
+
expected: AiRuntimeDeploymentV1,
|
|
20
|
+
health: () => Promise<Response>,
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
const response = await boundedHealth(health);
|
|
23
|
+
if (response.status !== 200) throw unavailable();
|
|
24
|
+
if (!response.headers.get("content-type")?.includes("application/json")) {
|
|
25
|
+
throw unavailable();
|
|
26
|
+
}
|
|
27
|
+
const length = Number(response.headers.get("content-length") ?? "0");
|
|
28
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > 64 * 1_024) {
|
|
29
|
+
throw unavailable();
|
|
30
|
+
}
|
|
31
|
+
let snapshot: unknown;
|
|
32
|
+
try {
|
|
33
|
+
const text = await response.text();
|
|
34
|
+
if (new TextEncoder().encode(text).byteLength > 64 * 1_024) {
|
|
35
|
+
throw new Error("health body too large");
|
|
36
|
+
}
|
|
37
|
+
snapshot = JSON.parse(text);
|
|
38
|
+
} catch {
|
|
39
|
+
throw unavailable();
|
|
40
|
+
}
|
|
41
|
+
const parsed = parseAiRuntimeReadinessV1(snapshot, new Date().toISOString());
|
|
42
|
+
if (
|
|
43
|
+
!parsed.ok ||
|
|
44
|
+
parsed.value.status !== "ready" ||
|
|
45
|
+
!sameAiRuntimeDeployment(parsed.value.deployment, expected)
|
|
46
|
+
) {
|
|
47
|
+
throw unavailable();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function boundedHealth(
|
|
52
|
+
health: () => Promise<Response>,
|
|
53
|
+
): Promise<Response> {
|
|
54
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
55
|
+
try {
|
|
56
|
+
return await Promise.race([
|
|
57
|
+
health(),
|
|
58
|
+
new Promise<Response>((_, reject) => {
|
|
59
|
+
timeout = setTimeout(() => reject(unavailable()), 5_000);
|
|
60
|
+
}),
|
|
61
|
+
]);
|
|
62
|
+
} finally {
|
|
63
|
+
if (timeout) clearTimeout(timeout);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function unavailable(): Error {
|
|
68
|
+
return new Error("AI Runtime readiness pin mismatch");
|
|
69
|
+
}
|
|
@@ -4,7 +4,10 @@ import type {
|
|
|
4
4
|
AiProviderDataPolicy,
|
|
5
5
|
AiProviderEgressPolicy,
|
|
6
6
|
} from "@southwind-ai/modules";
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
AiRuntimeDeploymentV1,
|
|
9
|
+
ModuleAiJsonValue,
|
|
10
|
+
} from "@southwind-ai/server-sdk";
|
|
8
11
|
|
|
9
12
|
export type EmbeddingRuntimeItem =
|
|
10
13
|
| { readonly id: string; readonly text: string }
|
|
@@ -36,6 +39,8 @@ export interface EmbeddingRuntimeOperation {
|
|
|
36
39
|
};
|
|
37
40
|
readonly providerEgress: AiProviderEgressPolicy;
|
|
38
41
|
readonly providerDataPolicy?: AiProviderDataPolicy;
|
|
42
|
+
/** Host-projected immutable deployment facts; never supplied by a Module. */
|
|
43
|
+
readonly readinessPin?: AiRuntimeDeploymentV1;
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
export interface EmbeddingBatchResponse {
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { ModuleManifest } from "@southwind-ai/modules";
|
|
3
|
+
import { featureKey } from "@southwind-ai/shared";
|
|
4
|
+
import type {
|
|
5
|
+
AiRuntimeDeploymentV1,
|
|
6
|
+
ModuleAiJsonValue,
|
|
7
|
+
ModuleAiOperationReadinessV1,
|
|
8
|
+
} from "@southwind-ai/server-sdk";
|
|
9
|
+
import { buildScopeContext } from "../data-access/context.js";
|
|
10
|
+
import { createRequestGuardContext } from "../guards.js";
|
|
11
|
+
import { initializeModuleHosts } from "../module-host.js";
|
|
12
|
+
import { createServerRuntime } from "../runtime.js";
|
|
13
|
+
import type { EmbeddingRuntimeOperation } from "./contracts.js";
|
|
14
|
+
import { EmbeddingFacade } from "./facade.js";
|
|
15
|
+
|
|
16
|
+
const operationKey = "analysis.embedding.image";
|
|
17
|
+
const deployment: AiRuntimeDeploymentV1 = {
|
|
18
|
+
key: "embedding.primary",
|
|
19
|
+
identity: "windy-deployment-v1_0f26eb34-f384-4ec9-98f3-caf8d582291f",
|
|
20
|
+
releaseDigest: `sha256:${"a".repeat(64)}`,
|
|
21
|
+
offersDigest: `sha256:${"b".repeat(64)}`,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe("EmbeddingFacade readiness admission", () => {
|
|
25
|
+
test("unavailable 与 probe 异常在业务 prepare、Artifact、Run 前稳定失败关闭", async () => {
|
|
26
|
+
for (const inspect of [
|
|
27
|
+
async () => unavailableProjection(),
|
|
28
|
+
async () => {
|
|
29
|
+
throw new Error("provider secret");
|
|
30
|
+
},
|
|
31
|
+
]) {
|
|
32
|
+
const fixture = await createFixture(true, inspect);
|
|
33
|
+
|
|
34
|
+
await expect(fixture.start()).rejects.toMatchObject({
|
|
35
|
+
code: "RUNTIME_UNAVAILABLE",
|
|
36
|
+
message: "Embedding Runtime 当前不可用",
|
|
37
|
+
status: 503,
|
|
38
|
+
});
|
|
39
|
+
expect(fixture.calls).toEqual({
|
|
40
|
+
readiness: 1,
|
|
41
|
+
prepare: 0,
|
|
42
|
+
artifact: 0,
|
|
43
|
+
run: 0,
|
|
44
|
+
remote: 0,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("ready projection 固定 deployment pin 后才创建 Artifact 与 Run", async () => {
|
|
50
|
+
const fixture = await createFixture(true, async () => readyProjection());
|
|
51
|
+
|
|
52
|
+
await expect(fixture.start()).resolves.toMatchObject({
|
|
53
|
+
runId: "embedding_server_bound",
|
|
54
|
+
status: "accepted",
|
|
55
|
+
});
|
|
56
|
+
expect(fixture.calls).toEqual({
|
|
57
|
+
readiness: 1,
|
|
58
|
+
prepare: 1,
|
|
59
|
+
artifact: 1,
|
|
60
|
+
run: 1,
|
|
61
|
+
remote: 1,
|
|
62
|
+
});
|
|
63
|
+
expect(fixture.request?.readinessPin).toEqual(deployment);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("未声明 readiness 的旧 Operation 保留兼容路径", async () => {
|
|
67
|
+
const fixture = await createFixture(false, async () => {
|
|
68
|
+
throw new Error("legacy operation must not inspect V1");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await expect(fixture.start()).resolves.toMatchObject({
|
|
72
|
+
runId: "embedding_server_bound",
|
|
73
|
+
});
|
|
74
|
+
expect(fixture.calls.readiness).toBe(0);
|
|
75
|
+
expect(fixture.request?.readinessPin).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
async function createFixture(
|
|
80
|
+
withReadiness: boolean,
|
|
81
|
+
inspect: () => Promise<ModuleAiOperationReadinessV1>,
|
|
82
|
+
) {
|
|
83
|
+
const module = embeddingModule(withReadiness);
|
|
84
|
+
const calls = { readiness: 0, prepare: 0, artifact: 0, run: 0, remote: 0 };
|
|
85
|
+
let request: EmbeddingRuntimeOperation | undefined;
|
|
86
|
+
const hosts = await initializeModuleHosts({
|
|
87
|
+
modules: [module],
|
|
88
|
+
installations: [
|
|
89
|
+
{
|
|
90
|
+
manifest: module,
|
|
91
|
+
setup(host) {
|
|
92
|
+
host.registerAiOperationHandlers([
|
|
93
|
+
{
|
|
94
|
+
operationKey,
|
|
95
|
+
prepareEmbedding: (input: ModuleAiJsonValue) => {
|
|
96
|
+
calls.prepare += 1;
|
|
97
|
+
return { options: input };
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
]);
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
const facade = new EmbeddingFacade(
|
|
106
|
+
[module],
|
|
107
|
+
hosts,
|
|
108
|
+
createServerRuntime({}, { featureCatalog: module.features }),
|
|
109
|
+
{
|
|
110
|
+
async start(value: EmbeddingRuntimeOperation) {
|
|
111
|
+
calls.remote += 1;
|
|
112
|
+
request = value;
|
|
113
|
+
return { runId: "embedding_server_bound" };
|
|
114
|
+
},
|
|
115
|
+
} as never,
|
|
116
|
+
{
|
|
117
|
+
async rotate() {
|
|
118
|
+
calls.artifact += 1;
|
|
119
|
+
return artifact("rotated");
|
|
120
|
+
},
|
|
121
|
+
} as never,
|
|
122
|
+
{
|
|
123
|
+
async issue() {
|
|
124
|
+
calls.run += 1;
|
|
125
|
+
return { runId: "embedding_server_bound" };
|
|
126
|
+
},
|
|
127
|
+
async bind() {},
|
|
128
|
+
} as never,
|
|
129
|
+
{
|
|
130
|
+
async inspect() {
|
|
131
|
+
calls.readiness += 1;
|
|
132
|
+
return inspect();
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
return {
|
|
137
|
+
calls,
|
|
138
|
+
get request() {
|
|
139
|
+
return request;
|
|
140
|
+
},
|
|
141
|
+
start: () =>
|
|
142
|
+
facade.start(
|
|
143
|
+
operationKey,
|
|
144
|
+
[{ id: "image", source: artifact("submitted") }],
|
|
145
|
+
{},
|
|
146
|
+
"embedding-1",
|
|
147
|
+
guardContext(),
|
|
148
|
+
AbortSignal.timeout(1_000),
|
|
149
|
+
),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function embeddingModule(withReadiness: boolean): ModuleManifest {
|
|
154
|
+
return {
|
|
155
|
+
name: "analysis",
|
|
156
|
+
version: "0.1.0",
|
|
157
|
+
label: "分析",
|
|
158
|
+
description: "分析",
|
|
159
|
+
menus: [],
|
|
160
|
+
adminRoutes: [],
|
|
161
|
+
permissions: [
|
|
162
|
+
{
|
|
163
|
+
key: "analysis.embedding.execute",
|
|
164
|
+
label: "向量化",
|
|
165
|
+
module: "analysis",
|
|
166
|
+
moduleLabel: "分析",
|
|
167
|
+
resource: "embedding",
|
|
168
|
+
resourceLabel: "向量化",
|
|
169
|
+
action: "manage",
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
apiPermissions: [],
|
|
173
|
+
features: [
|
|
174
|
+
{
|
|
175
|
+
key: featureKey("analysis.embedding"),
|
|
176
|
+
label: "向量化",
|
|
177
|
+
module: "analysis",
|
|
178
|
+
visible: "visible",
|
|
179
|
+
enabled: true,
|
|
180
|
+
stage: "stable",
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
migrations: [],
|
|
184
|
+
schemaExports: [],
|
|
185
|
+
jsonSchemas: [
|
|
186
|
+
{ key: "analysis.embedding.input.v1", schema: { type: "object" } },
|
|
187
|
+
{ key: "analysis.embedding.output.v1", schema: { type: "object" } },
|
|
188
|
+
],
|
|
189
|
+
tasks: [],
|
|
190
|
+
tools: [],
|
|
191
|
+
aiOperations: [
|
|
192
|
+
{
|
|
193
|
+
key: operationKey,
|
|
194
|
+
kind: "embedding",
|
|
195
|
+
featureKey: "analysis.embedding",
|
|
196
|
+
permissionKey: "analysis.embedding.execute",
|
|
197
|
+
auditAction: "analysis.embedding.execute",
|
|
198
|
+
inputSchemaKey: "analysis.embedding.input.v1",
|
|
199
|
+
outputSchemaKey: "analysis.embedding.output.v1",
|
|
200
|
+
inputModalities: ["image"],
|
|
201
|
+
...(withReadiness
|
|
202
|
+
? {
|
|
203
|
+
readiness: {
|
|
204
|
+
contractVersion: 1 as const,
|
|
205
|
+
variant: "image",
|
|
206
|
+
inputModalities: ["image"] as const,
|
|
207
|
+
outputModalities: ["embedding"] as const,
|
|
208
|
+
expectedDeployment: deployment,
|
|
209
|
+
minConcurrentRuns: 1,
|
|
210
|
+
minQueueDepth: 0,
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
: {}),
|
|
214
|
+
requiredCapabilities: ["embedding.image"],
|
|
215
|
+
allowedToolKeys: [],
|
|
216
|
+
dataClassification: "sensitive",
|
|
217
|
+
execution: "background",
|
|
218
|
+
providerEgress: { mode: "forbidden" },
|
|
219
|
+
limits: {
|
|
220
|
+
maxDurationSeconds: 60,
|
|
221
|
+
maxInputBytes: 4_096,
|
|
222
|
+
maxOutputBytes: 4_096,
|
|
223
|
+
maxUnits: 1,
|
|
224
|
+
maxArtifacts: 1,
|
|
225
|
+
maxPixelsPerArtifact: 1_000,
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
providers: [],
|
|
230
|
+
auditActions: ["analysis.embedding.execute"],
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function guardContext() {
|
|
235
|
+
return createRequestGuardContext(new Request("http://localhost"), {
|
|
236
|
+
resolveActor: () => ({
|
|
237
|
+
id: "actor-1",
|
|
238
|
+
type: "user",
|
|
239
|
+
name: "测试用户",
|
|
240
|
+
roleCodes: ["analyst"],
|
|
241
|
+
permissionKeys: ["analysis.embedding.execute"],
|
|
242
|
+
scopeContext: buildScopeContext("actor-1", undefined, [
|
|
243
|
+
{
|
|
244
|
+
id: "analyst",
|
|
245
|
+
code: "analyst",
|
|
246
|
+
dataScope: "self",
|
|
247
|
+
departmentIds: [],
|
|
248
|
+
},
|
|
249
|
+
]),
|
|
250
|
+
}),
|
|
251
|
+
resolveLicense: () => ({ status: "valid" }),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function artifact(value: string) {
|
|
256
|
+
return {
|
|
257
|
+
value,
|
|
258
|
+
mediaType: "image/png",
|
|
259
|
+
byteSize: 128,
|
|
260
|
+
sha256: "c".repeat(64),
|
|
261
|
+
expiresAt: "2030-01-01T00:00:00.000Z",
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function readyProjection(): ModuleAiOperationReadinessV1 {
|
|
266
|
+
return {
|
|
267
|
+
schemaVersion: 1,
|
|
268
|
+
operationKey,
|
|
269
|
+
runtimeKind: "embedding",
|
|
270
|
+
observedAt: new Date().toISOString(),
|
|
271
|
+
status: "ready",
|
|
272
|
+
validUntil: new Date(Date.now() + 60_000).toISOString(),
|
|
273
|
+
deployment,
|
|
274
|
+
offers: [
|
|
275
|
+
{
|
|
276
|
+
variant: "image",
|
|
277
|
+
capabilities: ["embedding.image"],
|
|
278
|
+
inputModalities: ["image"],
|
|
279
|
+
outputModalities: ["embedding"],
|
|
280
|
+
limits: {
|
|
281
|
+
maxDurationSeconds: 60,
|
|
282
|
+
maxInputBytes: 4_096,
|
|
283
|
+
maxOutputBytes: 4_096,
|
|
284
|
+
maxUnits: 1,
|
|
285
|
+
maxArtifacts: 1,
|
|
286
|
+
maxPixelsPerArtifact: 1_000,
|
|
287
|
+
maxConcurrentRuns: 1,
|
|
288
|
+
maxQueueDepth: 0,
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
unavailableReasons: [],
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function unavailableProjection(): ModuleAiOperationReadinessV1 {
|
|
297
|
+
return {
|
|
298
|
+
schemaVersion: 1,
|
|
299
|
+
operationKey,
|
|
300
|
+
runtimeKind: "embedding",
|
|
301
|
+
observedAt: new Date().toISOString(),
|
|
302
|
+
status: "unavailable",
|
|
303
|
+
deployment: null,
|
|
304
|
+
offers: [],
|
|
305
|
+
unavailableReasons: [{ code: "DOWNSTREAM_PROBE_FAILED", retryable: true }],
|
|
306
|
+
};
|
|
307
|
+
}
|
|
@@ -31,10 +31,10 @@ import {
|
|
|
31
31
|
embeddingErrorStatus,
|
|
32
32
|
type RegisteredEmbeddingOperation,
|
|
33
33
|
} from "./facade-types.js";
|
|
34
|
+
import { type ReadinessPort, requireReadiness } from "./readiness-admission.js";
|
|
34
35
|
|
|
35
36
|
export class EmbeddingFacade {
|
|
36
37
|
readonly #operations: Map<string, RegisteredEmbeddingOperation>;
|
|
37
|
-
|
|
38
38
|
constructor(
|
|
39
39
|
modules: readonly ModuleManifest[],
|
|
40
40
|
hosts: InitializedModuleHosts,
|
|
@@ -42,6 +42,7 @@ export class EmbeddingFacade {
|
|
|
42
42
|
private readonly remote: EmbeddingRuntimePort,
|
|
43
43
|
private readonly artifacts: AiArtifactRuntime,
|
|
44
44
|
private readonly runs: EmbeddingRunAuthorizations,
|
|
45
|
+
private readonly readiness: ReadinessPort,
|
|
45
46
|
) {
|
|
46
47
|
const handlers = new Map(
|
|
47
48
|
hosts.aiOperationHandlers.map(({ registration, moduleName }) => [
|
|
@@ -68,7 +69,6 @@ export class EmbeddingFacade {
|
|
|
68
69
|
),
|
|
69
70
|
);
|
|
70
71
|
}
|
|
71
|
-
|
|
72
72
|
async embed(
|
|
73
73
|
operationKey: string,
|
|
74
74
|
items: readonly EmbeddingFacadeItem[],
|
|
@@ -113,7 +113,6 @@ export class EmbeddingFacade {
|
|
|
113
113
|
throw error;
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
|
-
|
|
117
116
|
async start(
|
|
118
117
|
operationKey: string,
|
|
119
118
|
items: readonly EmbeddingFacadeItem[],
|
|
@@ -133,6 +132,7 @@ export class EmbeddingFacade {
|
|
|
133
132
|
operationKey,
|
|
134
133
|
prepared.items,
|
|
135
134
|
prepared.request.options,
|
|
135
|
+
prepared.request.readinessPin,
|
|
136
136
|
);
|
|
137
137
|
const binding = await this.runs.issue({
|
|
138
138
|
operationKey,
|
|
@@ -176,7 +176,6 @@ export class EmbeddingFacade {
|
|
|
176
176
|
throw error;
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
|
-
|
|
180
179
|
async authorizeRun(runId: string, context: RequestGuardContext) {
|
|
181
180
|
const binding = await this.runs.require(runId, context);
|
|
182
181
|
const operation = this.#operations.get(binding.operationKey);
|
|
@@ -226,7 +225,6 @@ export class EmbeddingFacade {
|
|
|
226
225
|
await this.remote.cancel(runId, undefined, signal);
|
|
227
226
|
await this.auditTerminal(binding, context, "cancelled");
|
|
228
227
|
}
|
|
229
|
-
|
|
230
228
|
async openRunArtifact(runId: string, itemId: string, signal: AbortSignal) {
|
|
231
229
|
const initial = await this.runs.find(runId);
|
|
232
230
|
if (!initial) throw unavailableArtifact();
|
|
@@ -250,7 +248,6 @@ export class EmbeddingFacade {
|
|
|
250
248
|
signal,
|
|
251
249
|
);
|
|
252
250
|
}
|
|
253
|
-
|
|
254
251
|
private async prepare(
|
|
255
252
|
operationKey: string,
|
|
256
253
|
items: readonly EmbeddingFacadeItem[],
|
|
@@ -266,6 +263,10 @@ export class EmbeddingFacade {
|
|
|
266
263
|
404,
|
|
267
264
|
);
|
|
268
265
|
await authorizeEmbeddingOperation(this.runtime, operation, context);
|
|
266
|
+
const readinessPin = await requireReadiness(
|
|
267
|
+
this.readiness,
|
|
268
|
+
operation.definition,
|
|
269
|
+
);
|
|
269
270
|
const governed = governEmbeddingInput(items, options, operation.definition);
|
|
270
271
|
const plan = await operation.registration.prepareEmbedding(governed, {
|
|
271
272
|
actor: toModuleActor(context.actor),
|
|
@@ -316,6 +317,7 @@ export class EmbeddingFacade {
|
|
|
316
317
|
},
|
|
317
318
|
providerEgress: resolveProviderEgress(operation.definition),
|
|
318
319
|
providerDataPolicy: operation.definition.providerDataPolicy,
|
|
320
|
+
readinessPin,
|
|
319
321
|
},
|
|
320
322
|
};
|
|
321
323
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ModuleAiOperationDefinition } from "@southwind-ai/modules";
|
|
2
|
+
import type {
|
|
3
|
+
AiRuntimeDeploymentV1,
|
|
4
|
+
ModuleAiOperationReadinessPortV1,
|
|
5
|
+
} from "@southwind-ai/server-sdk";
|
|
6
|
+
import { EmbeddingFacadeError } from "./error.js";
|
|
7
|
+
|
|
8
|
+
export type ReadinessPort = ModuleAiOperationReadinessPortV1;
|
|
9
|
+
|
|
10
|
+
export async function requireReadiness(
|
|
11
|
+
readiness: ModuleAiOperationReadinessPortV1,
|
|
12
|
+
definition: ModuleAiOperationDefinition,
|
|
13
|
+
): Promise<AiRuntimeDeploymentV1 | undefined> {
|
|
14
|
+
if (!definition.readiness) return undefined;
|
|
15
|
+
try {
|
|
16
|
+
const projected = await readiness.inspect({ operationKey: definition.key });
|
|
17
|
+
if (projected.status !== "ready") throw new Error("not ready");
|
|
18
|
+
return projected.deployment;
|
|
19
|
+
} catch {
|
|
20
|
+
throw new EmbeddingFacadeError(
|
|
21
|
+
"RUNTIME_UNAVAILABLE",
|
|
22
|
+
"Embedding Runtime 当前不可用",
|
|
23
|
+
503,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|