@southwind-ai/shared 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./src/audit.js";
2
+ export * from "./src/ai-operation-readiness.js";
2
3
  // @windy-module system.agent begin
3
4
  export * from "./src/ai-runtime-report.js";
4
5
  // @windy-module system.agent end
@@ -22,4 +23,8 @@ export * from "./src/rbac.js";
22
23
  export * from "./src/ruoyi.js";
23
24
  export * from "./src/scheduler.js";
24
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
29
+ export * from "./src/stable-key.js";
25
30
  export * from "./src/workflow.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@southwind-ai/shared",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "engines": {
@@ -11,6 +11,9 @@
11
11
  "src/**/*.ts",
12
12
  "!src/**/*.test.ts"
13
13
  ],
14
+ "scripts": {
15
+ "typecheck": "tsc --noEmit --project tsconfig.json"
16
+ },
14
17
  "exports": {
15
18
  ".": "./index.ts"
16
19
  },
@@ -0,0 +1,170 @@
1
+ export const AI_RUNTIME_READINESS_SCHEMA_VERSION = 1 as const;
2
+ export const AI_OPERATION_MAX_REQUIRED_CAPABILITIES_V1 = 16 as const;
3
+
4
+ export const AI_RUNTIME_KINDS_V1 = [
5
+ "agent",
6
+ "document",
7
+ "speech",
8
+ "embedding",
9
+ ] as const;
10
+
11
+ export type AiRuntimeKindV1 = (typeof AI_RUNTIME_KINDS_V1)[number];
12
+
13
+ export const AI_RUNTIME_MODALITIES_V1 = [
14
+ "text",
15
+ "image",
16
+ "audio",
17
+ "video",
18
+ "document",
19
+ "embedding",
20
+ ] as const;
21
+
22
+ export type AiRuntimeModalityV1 = (typeof AI_RUNTIME_MODALITIES_V1)[number];
23
+ export type AiRuntimeReleaseDigestV1 = `sha256:${string}`;
24
+ export type AiRuntimeOffersDigestV1 = `sha256:${string}`;
25
+
26
+ export interface AiRuntimeDeploymentV1 {
27
+ readonly key: string;
28
+ readonly identity: string;
29
+ readonly releaseDigest: AiRuntimeReleaseDigestV1;
30
+ readonly offersDigest: AiRuntimeOffersDigestV1;
31
+ }
32
+
33
+ export interface AiOperationRequiredLimitsV1 {
34
+ readonly maxDurationSeconds: number;
35
+ readonly maxInputBytes: number;
36
+ readonly maxOutputBytes: number;
37
+ readonly maxUnits?: number;
38
+ readonly maxMediaDurationSeconds?: number;
39
+ readonly maxArtifacts?: number;
40
+ readonly maxPixelsPerArtifact?: number;
41
+ }
42
+
43
+ export interface AiOperationRequiredResourcesV1 {
44
+ readonly minConcurrentRuns: number;
45
+ readonly minQueueDepth: number;
46
+ }
47
+
48
+ export interface AiOperationReadinessRequirementV1 {
49
+ readonly schemaVersion: typeof AI_RUNTIME_READINESS_SCHEMA_VERSION;
50
+ readonly operationKey: string;
51
+ readonly runtimeKind: AiRuntimeKindV1;
52
+ readonly expectedDeployment: AiRuntimeDeploymentV1;
53
+ readonly variant?: string;
54
+ readonly requiredCapabilities: readonly string[];
55
+ readonly inputModalities: readonly AiRuntimeModalityV1[];
56
+ readonly outputModalities: readonly AiRuntimeModalityV1[];
57
+ readonly limits: AiOperationRequiredLimitsV1;
58
+ readonly resources: AiOperationRequiredResourcesV1;
59
+ }
60
+
61
+ const stableKeyPattern = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/;
62
+ const deploymentIdentityPattern =
63
+ /^windy-deployment-v1_[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
64
+ const digestPattern = /^sha256:[a-f0-9]{64}$/;
65
+ const runtimeKinds = new Set<string>(AI_RUNTIME_KINDS_V1);
66
+ const runtimeModalities = new Set<string>(AI_RUNTIME_MODALITIES_V1);
67
+ const requiredLimitKeys = [
68
+ "maxDurationSeconds",
69
+ "maxInputBytes",
70
+ "maxOutputBytes",
71
+ ] as const;
72
+ const optionalLimitKeys = [
73
+ "maxUnits",
74
+ "maxMediaDurationSeconds",
75
+ "maxArtifacts",
76
+ "maxPixelsPerArtifact",
77
+ ] as const;
78
+
79
+ /** Pure contract guard shared by Manifest composition and Host projection. */
80
+ export function validAiOperationReadinessRequirementV1(
81
+ requirement: unknown,
82
+ ): requirement is AiOperationReadinessRequirementV1 {
83
+ if (!isRecord(requirement)) return false;
84
+ return (
85
+ requirement.schemaVersion === AI_RUNTIME_READINESS_SCHEMA_VERSION &&
86
+ validStableKey(requirement.operationKey) &&
87
+ typeof requirement.runtimeKind === "string" &&
88
+ runtimeKinds.has(requirement.runtimeKind) &&
89
+ validDeployment(requirement.expectedDeployment) &&
90
+ (requirement.variant === undefined ||
91
+ validStableKey(requirement.variant)) &&
92
+ validStableKeyList(
93
+ requirement.requiredCapabilities,
94
+ AI_OPERATION_MAX_REQUIRED_CAPABILITIES_V1,
95
+ ) &&
96
+ validModalities(requirement.inputModalities) &&
97
+ validModalities(requirement.outputModalities) &&
98
+ validLimits(requirement.limits) &&
99
+ validResources(requirement.resources)
100
+ );
101
+ }
102
+
103
+ function validDeployment(value: unknown): value is AiRuntimeDeploymentV1 {
104
+ return (
105
+ isRecord(value) &&
106
+ validStableKey(value.key) &&
107
+ typeof value.identity === "string" &&
108
+ deploymentIdentityPattern.test(value.identity) &&
109
+ typeof value.releaseDigest === "string" &&
110
+ digestPattern.test(value.releaseDigest) &&
111
+ typeof value.offersDigest === "string" &&
112
+ digestPattern.test(value.offersDigest)
113
+ );
114
+ }
115
+
116
+ function validStableKey(value: unknown): value is string {
117
+ return typeof value === "string" && stableKeyPattern.test(value);
118
+ }
119
+
120
+ function validStableKeyList(value: unknown, maximum: number): boolean {
121
+ return (
122
+ Array.isArray(value) &&
123
+ value.length > 0 &&
124
+ value.length <= maximum &&
125
+ value.every(validStableKey) &&
126
+ new Set(value).size === value.length
127
+ );
128
+ }
129
+
130
+ function validModalities(value: unknown): boolean {
131
+ return (
132
+ Array.isArray(value) &&
133
+ value.length > 0 &&
134
+ value.length <= AI_RUNTIME_MODALITIES_V1.length &&
135
+ value.every(
136
+ (item) => typeof item === "string" && runtimeModalities.has(item),
137
+ ) &&
138
+ new Set(value).size === value.length
139
+ );
140
+ }
141
+
142
+ function validLimits(value: unknown): boolean {
143
+ if (!isRecord(value)) return false;
144
+ return (
145
+ requiredLimitKeys.every((key) => positiveSafeInteger(value[key])) &&
146
+ optionalLimitKeys.every(
147
+ (key) => value[key] === undefined || positiveSafeInteger(value[key]),
148
+ )
149
+ );
150
+ }
151
+
152
+ function validResources(value: unknown): boolean {
153
+ return (
154
+ isRecord(value) &&
155
+ positiveSafeInteger(value.minConcurrentRuns) &&
156
+ nonNegativeSafeInteger(value.minQueueDepth)
157
+ );
158
+ }
159
+
160
+ function positiveSafeInteger(value: unknown): boolean {
161
+ return Number.isSafeInteger(value) && Number(value) > 0;
162
+ }
163
+
164
+ function nonNegativeSafeInteger(value: unknown): boolean {
165
+ return Number.isSafeInteger(value) && Number(value) >= 0;
166
+ }
167
+
168
+ function isRecord(value: unknown): value is Record<string, unknown> {
169
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
170
+ }
@@ -81,20 +81,25 @@ export function parseLicenseCatalog(
81
81
  if (unknownField) {
82
82
  throw new Error(`License 版本目录包含未知字段:${unknownField}`);
83
83
  }
84
+ const description = record.description;
85
+ const key = record.key;
86
+ const label = record.label;
87
+ const order = record.order;
84
88
  if (
85
- typeof record.key !== "string" ||
86
- typeof record.label !== "string" ||
87
- (record.description !== undefined &&
88
- typeof record.description !== "string") ||
89
- typeof record.order !== "number"
89
+ typeof key !== "string" ||
90
+ typeof label !== "string" ||
91
+ (description !== undefined && typeof description !== "string") ||
92
+ typeof order !== "number"
90
93
  ) {
91
94
  throw new Error(`License 版本目录第 ${index + 1} 项字段类型无效`);
92
95
  }
96
+ const normalizedDescription =
97
+ typeof description === "string" ? description.trim() : "";
93
98
  return {
94
- key: licenseVersionKey(record.key.trim()),
95
- label: record.label.trim(),
96
- description: record.description?.trim() ?? "",
97
- order: record.order,
99
+ key: licenseVersionKey(key.trim()),
100
+ label: label.trim(),
101
+ description: normalizedDescription,
102
+ order,
98
103
  };
99
104
  });
100
105
  validateLicenseCatalog(catalog);
@@ -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
+ }
@@ -0,0 +1,26 @@
1
+ const STABLE_KEY_PATTERN = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/;
2
+
3
+ export const STABLE_KEY_MAX_LENGTH = 128;
4
+
5
+ export function isStableKey(value: unknown): value is string {
6
+ return (
7
+ typeof value === "string" &&
8
+ value.length <= STABLE_KEY_MAX_LENGTH &&
9
+ STABLE_KEY_PATTERN.test(value)
10
+ );
11
+ }
12
+
13
+ export function requireStableModuleKey(
14
+ kind: string,
15
+ value: unknown,
16
+ moduleName: string,
17
+ ): asserts value is string {
18
+ if (!isStableKey(value)) {
19
+ throw new Error(
20
+ `${kind}必须是最多 ${STABLE_KEY_MAX_LENGTH} 字符的小写稳定 Key`,
21
+ );
22
+ }
23
+ if (value !== moduleName && !value.startsWith(`${moduleName}.`)) {
24
+ throw new Error(`${kind}不属于模块 ${moduleName}:${value}`);
25
+ }
26
+ }