create-windy 0.2.8 → 0.2.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.
Files changed (36) hide show
  1. package/dist/cli.js +8 -3
  2. package/package.json +1 -1
  3. package/template/.windy-template.json +2 -2
  4. package/template/apps/server/Dockerfile +2 -0
  5. package/template/apps/server/src/index.ts +1 -0
  6. package/template/apps/server/src/module-host.test.ts +6 -0
  7. package/template/apps/server/src/module-host.ts +9 -6
  8. package/template/apps/web/Dockerfile +2 -0
  9. package/template/package.json +1 -1
  10. package/template/packages/agent-client/index.ts +3 -0
  11. package/template/packages/agent-client/package.json +27 -0
  12. package/template/packages/agent-client/src/client.test.ts +344 -0
  13. package/template/packages/agent-client/src/client.ts +186 -0
  14. package/template/packages/agent-client/src/error.ts +88 -0
  15. package/template/packages/agent-client/src/event-queue.ts +62 -0
  16. package/template/packages/agent-client/src/http.ts +19 -0
  17. package/template/packages/agent-client/src/run-protocol.test.ts +231 -0
  18. package/template/packages/agent-client/src/run.ts +320 -0
  19. package/template/packages/agent-client/src/types.ts +35 -0
  20. package/template/packages/agent-client/tsconfig.json +21 -0
  21. package/template/packages/agent-contracts/index.ts +7 -0
  22. package/template/packages/agent-contracts/package.json +24 -0
  23. package/template/packages/agent-contracts/src/contracts.test.ts +213 -0
  24. package/template/packages/agent-contracts/src/errors.ts +51 -0
  25. package/template/packages/agent-contracts/src/events.ts +161 -0
  26. package/template/packages/agent-contracts/src/operation.ts +28 -0
  27. package/template/packages/agent-contracts/src/runs.ts +58 -0
  28. package/template/packages/agent-contracts/src/sse.ts +95 -0
  29. package/template/packages/agent-contracts/src/usage.ts +62 -0
  30. package/template/packages/agent-contracts/src/validation.ts +68 -0
  31. package/template/packages/agent-contracts/tsconfig.json +16 -0
  32. package/template/packages/modules/src/ai-operation-composition.test.ts +195 -0
  33. package/template/packages/modules/src/ai-operation-composition.ts +145 -0
  34. package/template/packages/modules/src/composition.ts +19 -37
  35. package/template/packages/modules/src/implementation-bindings.ts +70 -0
  36. package/template/packages/modules/src/manifest.ts +37 -1
@@ -0,0 +1,161 @@
1
+ import { parseAgentError, type AgentError } from "./errors.js";
2
+ import { parseAgentUsage, type AgentUsage } from "./usage.js";
3
+ import {
4
+ AgentContractParseError,
5
+ positiveIntegerAt,
6
+ recordAt,
7
+ stringAt,
8
+ } from "./validation.js";
9
+
10
+ export interface AgentEventBase {
11
+ runId: string;
12
+ sequence: number;
13
+ }
14
+
15
+ export interface AgentRunStartedEvent extends AgentEventBase {
16
+ type: "run.started";
17
+ }
18
+
19
+ export interface AgentOutputDeltaEvent extends AgentEventBase {
20
+ type: "output.delta";
21
+ text: string;
22
+ }
23
+
24
+ export interface AgentToolRequestedEvent extends AgentEventBase {
25
+ type: "tool.requested";
26
+ toolCallId: string;
27
+ toolKey: string;
28
+ }
29
+
30
+ export interface AgentApprovalRequiredEvent extends AgentEventBase {
31
+ type: "approval.required";
32
+ workItemId: string;
33
+ }
34
+
35
+ export interface AgentToolCompletedEvent extends AgentEventBase {
36
+ type: "tool.completed";
37
+ toolCallId: string;
38
+ }
39
+
40
+ export interface AgentUsageReportedEvent extends AgentEventBase {
41
+ type: "usage.reported";
42
+ usage: AgentUsage;
43
+ }
44
+
45
+ export interface AgentRunCompletedEvent extends AgentEventBase {
46
+ type: "run.completed";
47
+ }
48
+
49
+ export interface AgentRunFailedEvent extends AgentEventBase {
50
+ type: "run.failed";
51
+ error: AgentError;
52
+ }
53
+
54
+ export interface AgentRunCancelledEvent extends AgentEventBase {
55
+ type: "run.cancelled";
56
+ }
57
+
58
+ export type AgentEvent =
59
+ | AgentRunStartedEvent
60
+ | AgentOutputDeltaEvent
61
+ | AgentToolRequestedEvent
62
+ | AgentApprovalRequiredEvent
63
+ | AgentToolCompletedEvent
64
+ | AgentUsageReportedEvent
65
+ | AgentRunCompletedEvent
66
+ | AgentRunFailedEvent
67
+ | AgentRunCancelledEvent;
68
+
69
+ export type AgentEventType = AgentEvent["type"];
70
+ export type TerminalAgentEvent =
71
+ | AgentRunCompletedEvent
72
+ | AgentRunFailedEvent
73
+ | AgentRunCancelledEvent;
74
+
75
+ const eventTypes: ReadonlySet<string> = new Set<AgentEventType>([
76
+ "run.started",
77
+ "output.delta",
78
+ "tool.requested",
79
+ "approval.required",
80
+ "tool.completed",
81
+ "usage.reported",
82
+ "run.completed",
83
+ "run.failed",
84
+ "run.cancelled",
85
+ ]);
86
+
87
+ export function parseAgentEvent(input: unknown): AgentEvent {
88
+ const record = recordAt(input, "event");
89
+ const type = stringAt(record, "type", "event");
90
+ if (!eventTypes.has(type)) {
91
+ throw new AgentContractParseError("event.type", "事件类型无效");
92
+ }
93
+ const eventType = type as AgentEventType;
94
+ const base = {
95
+ runId: stringAt(record, "runId", "event"),
96
+ sequence: positiveIntegerAt(record, "sequence", "event"),
97
+ };
98
+
99
+ switch (eventType) {
100
+ case "run.started":
101
+ case "run.completed":
102
+ case "run.cancelled":
103
+ return { type: eventType, ...base };
104
+ case "output.delta":
105
+ return {
106
+ type: eventType,
107
+ ...base,
108
+ text: stringAt(record, "text", "event"),
109
+ };
110
+ case "tool.requested":
111
+ return {
112
+ type: eventType,
113
+ ...base,
114
+ toolCallId: stringAt(record, "toolCallId", "event"),
115
+ toolKey: stringAt(record, "toolKey", "event"),
116
+ };
117
+ case "approval.required":
118
+ return {
119
+ type: eventType,
120
+ ...base,
121
+ workItemId: stringAt(record, "workItemId", "event"),
122
+ };
123
+ case "tool.completed":
124
+ return {
125
+ type: eventType,
126
+ ...base,
127
+ toolCallId: stringAt(record, "toolCallId", "event"),
128
+ };
129
+ case "usage.reported":
130
+ return {
131
+ type: eventType,
132
+ ...base,
133
+ usage: parseAgentUsage(record.usage),
134
+ };
135
+ case "run.failed":
136
+ return {
137
+ type: eventType,
138
+ ...base,
139
+ error: parseAgentError(record.error),
140
+ };
141
+ }
142
+ }
143
+
144
+ export function isAgentEvent(input: unknown): input is AgentEvent {
145
+ try {
146
+ parseAgentEvent(input);
147
+ return true;
148
+ } catch {
149
+ return false;
150
+ }
151
+ }
152
+
153
+ export function isTerminalAgentEvent(
154
+ event: AgentEvent,
155
+ ): event is TerminalAgentEvent {
156
+ return (
157
+ event.type === "run.completed" ||
158
+ event.type === "run.failed" ||
159
+ event.type === "run.cancelled"
160
+ );
161
+ }
@@ -0,0 +1,28 @@
1
+ declare const operationTypes: unique symbol;
2
+
3
+ /**
4
+ * 业务侧稳定的 Agent Operation 引用。
5
+ *
6
+ * 输入类型只在编译期存在;远端输出必须通过运行时 parser 收窄。
7
+ */
8
+ export interface AgentOperation<Input, Output> {
9
+ readonly key: string;
10
+ readonly parseOutput: (value: unknown) => Output;
11
+ readonly [operationTypes]?: {
12
+ readonly input: Input;
13
+ };
14
+ }
15
+
16
+ export type AgentOperationInput<T> =
17
+ T extends AgentOperation<infer Input, unknown> ? Input : never;
18
+
19
+ export type AgentOperationOutput<T> =
20
+ T extends AgentOperation<unknown, infer Output> ? Output : never;
21
+
22
+ export function defineAgentOperation<Input, Output>(
23
+ key: string,
24
+ parseOutput: (value: unknown) => Output,
25
+ ): AgentOperation<Input, Output> {
26
+ if (!key.trim()) throw new TypeError("Agent Operation key 不能为空");
27
+ return Object.freeze({ key, parseOutput });
28
+ }
@@ -0,0 +1,58 @@
1
+ import { AgentContractParseError, recordAt, stringAt } from "./validation.js";
2
+
3
+ export type AgentRunStatus =
4
+ | "accepted"
5
+ | "running"
6
+ | "awaiting-approval"
7
+ | "completed"
8
+ | "failed"
9
+ | "cancelled";
10
+
11
+ export interface StartAgentRunRequest<Input> {
12
+ operationKey: string;
13
+ input: Input;
14
+ }
15
+
16
+ export interface StartAgentRunResponse {
17
+ runId: string;
18
+ status: AgentRunStatus;
19
+ eventsUrl: string;
20
+ }
21
+
22
+ const terminalStatuses: ReadonlySet<AgentRunStatus> = new Set([
23
+ "completed",
24
+ "failed",
25
+ "cancelled",
26
+ ]);
27
+ const runStatuses: ReadonlySet<string> = new Set<AgentRunStatus>([
28
+ "accepted",
29
+ "running",
30
+ "awaiting-approval",
31
+ "completed",
32
+ "failed",
33
+ "cancelled",
34
+ ]);
35
+
36
+ export function parseStartAgentRunResponse(
37
+ input: unknown,
38
+ ): StartAgentRunResponse {
39
+ const record = recordAt(input, "startAgentRunResponse");
40
+ const status = stringAt(record, "status", "startAgentRunResponse");
41
+ if (!runStatuses.has(status)) {
42
+ throw new AgentContractParseError(
43
+ "startAgentRunResponse.status",
44
+ "Run 状态无效",
45
+ );
46
+ }
47
+ return {
48
+ runId: stringAt(record, "runId", "startAgentRunResponse"),
49
+ status: status as AgentRunStatus,
50
+ eventsUrl: stringAt(record, "eventsUrl", "startAgentRunResponse"),
51
+ };
52
+ }
53
+
54
+ export function isTerminalAgentRunStatus(
55
+ status: AgentRunStatus,
56
+ ): status is Extract<AgentRunStatus, "completed" | "failed" | "cancelled"> {
57
+ return terminalStatuses.has(status);
58
+ }
@@ -0,0 +1,95 @@
1
+ import { parseAgentEvent, type AgentEvent } from "./events.js";
2
+ import { AgentContractParseError } from "./validation.js";
3
+
4
+ export interface DecodedAgentSseEventFrame {
5
+ kind: "event";
6
+ id: number;
7
+ event: AgentEvent;
8
+ }
9
+
10
+ export interface DecodedAgentSseCommentFrame {
11
+ kind: "comment";
12
+ comment: string;
13
+ }
14
+
15
+ export type DecodedAgentSseFrame =
16
+ | DecodedAgentSseEventFrame
17
+ | DecodedAgentSseCommentFrame;
18
+
19
+ export interface SplitAgentSseFramesResult {
20
+ frames: string[];
21
+ remainder: string;
22
+ }
23
+
24
+ export function splitAgentSseFrames(buffer: string): SplitAgentSseFramesResult {
25
+ const frames: string[] = [];
26
+ const separator = /\r?\n\r?\n/g;
27
+ let start = 0;
28
+ for (const match of buffer.matchAll(separator)) {
29
+ const index = match.index;
30
+ frames.push(buffer.slice(start, index));
31
+ start = index + match[0].length;
32
+ }
33
+ return { frames, remainder: buffer.slice(start) };
34
+ }
35
+
36
+ export function decodeAgentSseFrame(frame: string): DecodedAgentSseFrame {
37
+ const lines = frame.replace(/\r\n/g, "\n").split("\n");
38
+ const fields = new Map<string, string>();
39
+ const comments: string[] = [];
40
+
41
+ for (const line of lines) {
42
+ if (!line) continue;
43
+ if (line.startsWith(":")) {
44
+ comments.push(line.slice(1).trimStart());
45
+ continue;
46
+ }
47
+ const colon = line.indexOf(":");
48
+ const field = colon < 0 ? line : line.slice(0, colon);
49
+ const rawValue = colon < 0 ? "" : line.slice(colon + 1);
50
+ const value = rawValue.startsWith(" ") ? rawValue.slice(1) : rawValue;
51
+ if (!["id", "event", "data"].includes(field)) {
52
+ throw new AgentContractParseError("sse", `不支持字段 ${field}`);
53
+ }
54
+ if (fields.has(field)) {
55
+ throw new AgentContractParseError("sse", `字段 ${field} 重复`);
56
+ }
57
+ fields.set(field, value);
58
+ }
59
+
60
+ if (fields.size === 0 && comments.length > 0) {
61
+ return { kind: "comment", comment: comments.join("\n") };
62
+ }
63
+ if (comments.length > 0) {
64
+ throw new AgentContractParseError("sse", "事件帧不能混入 comment");
65
+ }
66
+
67
+ const idValue = fields.get("id");
68
+ const eventType = fields.get("event");
69
+ const data = fields.get("data");
70
+ if (idValue === undefined || eventType === undefined || data === undefined) {
71
+ throw new AgentContractParseError(
72
+ "sse",
73
+ "事件帧必须包含 id、event 和 data",
74
+ );
75
+ }
76
+ if (!/^[1-9]\d*$/.test(idValue) || !Number.isSafeInteger(Number(idValue))) {
77
+ throw new AgentContractParseError("sse.id", "必须是正安全整数");
78
+ }
79
+
80
+ let parsed: unknown;
81
+ try {
82
+ parsed = JSON.parse(data);
83
+ } catch {
84
+ throw new AgentContractParseError("sse.data", "必须是单行 JSON");
85
+ }
86
+ const event = parseAgentEvent(parsed);
87
+ const id = Number(idValue);
88
+ if (event.type !== eventType) {
89
+ throw new AgentContractParseError("sse.event", "与 data.type 不一致");
90
+ }
91
+ if (event.sequence !== id) {
92
+ throw new AgentContractParseError("sse.id", "与 data.sequence 不一致");
93
+ }
94
+ return { kind: "event", id, event };
95
+ }
@@ -0,0 +1,62 @@
1
+ import {
2
+ AgentContractParseError,
3
+ nonNegativeIntegerAt,
4
+ recordAt,
5
+ } from "./validation.js";
6
+
7
+ export interface AgentUsageMeasurements {
8
+ inputTokens?: number;
9
+ outputTokens?: number;
10
+ cachedInputTokens?: number;
11
+ reasoningTokens?: number;
12
+ totalTokens?: number;
13
+ firstTokenLatencyMs?: number;
14
+ }
15
+
16
+ type AtLeastOne<T> = {
17
+ [Key in keyof T]-?: Required<Pick<T, Key>> & Partial<Omit<T, Key>>;
18
+ }[keyof T];
19
+
20
+ export type AgentUsage =
21
+ | ({
22
+ source: "provider-reported" | "estimated";
23
+ } & AtLeastOne<AgentUsageMeasurements>)
24
+ | {
25
+ source: "unknown";
26
+ };
27
+
28
+ const measurementKeys = [
29
+ "inputTokens",
30
+ "outputTokens",
31
+ "cachedInputTokens",
32
+ "reasoningTokens",
33
+ "totalTokens",
34
+ "firstTokenLatencyMs",
35
+ ] as const satisfies readonly (keyof AgentUsageMeasurements)[];
36
+
37
+ export function parseAgentUsage(input: unknown): AgentUsage {
38
+ const record = recordAt(input, "usage");
39
+ const source = record.source;
40
+ if (source === "unknown") {
41
+ if (measurementKeys.some((key) => record[key] !== undefined)) {
42
+ throw new AgentContractParseError(
43
+ "usage",
44
+ "unknown 用量不能携带伪造的数值",
45
+ );
46
+ }
47
+ return { source };
48
+ }
49
+ if (source !== "provider-reported" && source !== "estimated") {
50
+ throw new AgentContractParseError("usage.source", "用量来源无效");
51
+ }
52
+
53
+ const measurements: AgentUsageMeasurements = {};
54
+ for (const key of measurementKeys) {
55
+ const value = nonNegativeIntegerAt(record, key, "usage");
56
+ if (value !== undefined) measurements[key] = value;
57
+ }
58
+ if (Object.keys(measurements).length === 0) {
59
+ throw new AgentContractParseError("usage", "已知来源至少需要一个用量数值");
60
+ }
61
+ return { source, ...measurements } as AgentUsage;
62
+ }
@@ -0,0 +1,68 @@
1
+ export class AgentContractParseError extends Error {
2
+ constructor(
3
+ readonly path: string,
4
+ message: string,
5
+ ) {
6
+ super(`${path}: ${message}`);
7
+ this.name = "AgentContractParseError";
8
+ }
9
+ }
10
+
11
+ export function recordAt(
12
+ input: unknown,
13
+ path: string,
14
+ ): Record<string, unknown> {
15
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
16
+ throw new AgentContractParseError(path, "必须是对象");
17
+ }
18
+ return input as Record<string, unknown>;
19
+ }
20
+
21
+ export function stringAt(
22
+ input: Record<string, unknown>,
23
+ key: string,
24
+ path: string,
25
+ ): string {
26
+ const value = input[key];
27
+ if (typeof value !== "string" || !value.trim()) {
28
+ throw new AgentContractParseError(`${path}.${key}`, "必须是非空字符串");
29
+ }
30
+ return value;
31
+ }
32
+
33
+ export function booleanAt(
34
+ input: Record<string, unknown>,
35
+ key: string,
36
+ path: string,
37
+ ): boolean {
38
+ const value = input[key];
39
+ if (typeof value !== "boolean") {
40
+ throw new AgentContractParseError(`${path}.${key}`, "必须是布尔值");
41
+ }
42
+ return value;
43
+ }
44
+
45
+ export function nonNegativeIntegerAt(
46
+ input: Record<string, unknown>,
47
+ key: string,
48
+ path: string,
49
+ ): number | undefined {
50
+ const value = input[key];
51
+ if (value === undefined) return undefined;
52
+ if (!Number.isSafeInteger(value) || Number(value) < 0) {
53
+ throw new AgentContractParseError(`${path}.${key}`, "必须是非负安全整数");
54
+ }
55
+ return Number(value);
56
+ }
57
+
58
+ export function positiveIntegerAt(
59
+ input: Record<string, unknown>,
60
+ key: string,
61
+ path: string,
62
+ ): number {
63
+ const value = nonNegativeIntegerAt(input, key, path);
64
+ if (value === undefined || value === 0) {
65
+ throw new AgentContractParseError(`${path}.${key}`, "必须是正安全整数");
66
+ }
67
+ return value;
68
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "esnext",
4
+ "module": "esnext",
5
+ "lib": ["esnext", "dom"],
6
+ "moduleResolution": "bundler",
7
+ "strict": true,
8
+ "noImplicitAny": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "outDir": "dist",
12
+ "types": ["bun"]
13
+ },
14
+ "include": ["src/**/*", "index.ts"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }
@@ -0,0 +1,195 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type {
3
+ ModuleAiOperationDefinition,
4
+ ModuleManifest,
5
+ } from "./manifest.js";
6
+ import { validateModuleComposition } from "./composition.js";
7
+
8
+ describe("AI Operation Composition", () => {
9
+ test("统一注册 Agent、Document 与 Speech Operation 并绑定实现", () => {
10
+ const module = fixture("business");
11
+ module.aiOperations = [
12
+ operation("business.agent", "agent", ["business.tool"]),
13
+ operation("business.document", "document"),
14
+ operation("business.speech", "speech"),
15
+ ];
16
+
17
+ expect(() =>
18
+ validateModuleComposition([module], {
19
+ aiOperations: module.aiOperations.map(({ key }) => key),
20
+ }),
21
+ ).not.toThrow();
22
+ });
23
+
24
+ test("Operation Key 全局唯一且必须使用所属模块命名空间", () => {
25
+ const first = fixture("first");
26
+ const second = fixture("second");
27
+ first.aiOperations = [operation("first.generate")];
28
+ second.aiOperations = [operation("first.generate")];
29
+
30
+ expect(() => validateModuleComposition([first, second])).toThrow(
31
+ "AI Operation Key 重复:first.generate",
32
+ );
33
+
34
+ second.aiOperations = [operation("foreign.generate")];
35
+ expect(() => validateModuleComposition([second])).toThrow(
36
+ "必须使用模块命名空间 second",
37
+ );
38
+ });
39
+
40
+ test.each([
41
+ ["permissionKey", "missing.permission", "未注册权限"],
42
+ ["featureKey", "missing.feature", "未注册Feature"],
43
+ ["auditAction", "missing.audit", "未注册审计动作"],
44
+ ] as const)("拒绝未知 %s 引用", (field, value, message) => {
45
+ const module = fixture("business");
46
+ const definition = operation("business.generate");
47
+ module.aiOperations = [{ ...definition, [field]: value }];
48
+ expect(() => validateModuleComposition([module])).toThrow(message);
49
+ });
50
+
51
+ test("拒绝未知 Tool、空 capability 和非法限制", () => {
52
+ const module = fixture("business");
53
+ module.aiOperations = [
54
+ operation("business.generate", "agent", ["missing.tool"]),
55
+ ];
56
+ expect(() => validateModuleComposition([module])).toThrow(
57
+ "未注册Agent Tool",
58
+ );
59
+
60
+ module.aiOperations[0] = {
61
+ ...operation("business.generate"),
62
+ requiredCapabilities: [],
63
+ };
64
+ expect(() => validateModuleComposition([module])).toThrow(
65
+ "至少需要一个 capability",
66
+ );
67
+
68
+ module.aiOperations[0] = {
69
+ ...operation("business.generate"),
70
+ limits: { ...operation("business.generate").limits, maxInputBytes: 0 },
71
+ };
72
+ expect(() => validateModuleComposition([module])).toThrow(
73
+ "上限必须是正整数",
74
+ );
75
+ });
76
+
77
+ test("拒绝空 schema、capability 和 Tool Key", () => {
78
+ const module = fixture("business");
79
+ module.aiOperations = [
80
+ { ...operation("business.generate"), inputSchemaKey: " " },
81
+ ];
82
+ expect(() => validateModuleComposition([module])).toThrow(
83
+ "Key 与引用字段不能为空",
84
+ );
85
+
86
+ module.aiOperations[0] = {
87
+ ...operation("business.generate"),
88
+ requiredCapabilities: [" "],
89
+ };
90
+ expect(() => validateModuleComposition([module])).toThrow(
91
+ "capability 不能为空",
92
+ );
93
+
94
+ module.aiOperations[0] = {
95
+ ...operation("business.generate"),
96
+ allowedToolKeys: [" "],
97
+ };
98
+ expect(() => validateModuleComposition([module])).toThrow("Tool 不能为空");
99
+ });
100
+
101
+ test("Operation Handler 必须与 Manifest 声明一一对应", () => {
102
+ const module = fixture("business");
103
+ module.aiOperations = [operation("business.generate")];
104
+
105
+ expect(() =>
106
+ validateModuleComposition([module], { aiOperations: [] }),
107
+ ).toThrow("AI Operation business.generate 缺少 Handler");
108
+ expect(() =>
109
+ validateModuleComposition([module], {
110
+ aiOperations: ["business.generate", "business.undeclared"],
111
+ }),
112
+ ).toThrow("AI Operation Handler business.undeclared 缺少 Manifest 声明");
113
+ });
114
+ });
115
+
116
+ function operation(
117
+ key: string,
118
+ kind: ModuleAiOperationDefinition["kind"] = "agent",
119
+ allowedToolKeys: readonly string[] = [],
120
+ ): ModuleAiOperationDefinition {
121
+ const namespace = key.split(".")[0]!;
122
+ return {
123
+ key,
124
+ kind,
125
+ featureKey: `${namespace}.ai`,
126
+ permissionKey: `${namespace}.ai.execute`,
127
+ auditAction: `${namespace}.ai.execute`,
128
+ inputSchemaKey: `${namespace}.ai.input.v1`,
129
+ outputSchemaKey: `${namespace}.ai.output.v1`,
130
+ requiredCapabilities: ["text-generation"],
131
+ allowedToolKeys,
132
+ dataClassification: "internal",
133
+ execution: kind === "speech" ? "live" : "streaming",
134
+ publicNetworkAccess: "forbidden",
135
+ limits: {
136
+ maxDurationSeconds: 60,
137
+ maxInputBytes: 1_048_576,
138
+ maxOutputBytes: 1_048_576,
139
+ },
140
+ };
141
+ }
142
+
143
+ function fixture(name: string): ModuleManifest {
144
+ const permissionKey = `${name}.ai.execute`;
145
+ const featureKey = `${name}.ai`;
146
+ return {
147
+ name,
148
+ version: "0.1.0",
149
+ label: name,
150
+ description: name,
151
+ menus: [],
152
+ adminRoutes: [],
153
+ permissions: [
154
+ {
155
+ key: permissionKey,
156
+ label: "执行 AI",
157
+ module: name,
158
+ resource: "ai",
159
+ action: "manage",
160
+ },
161
+ ],
162
+ apiPermissions: [],
163
+ features: [
164
+ {
165
+ key: featureKey,
166
+ label: "AI",
167
+ module: name,
168
+ visible: "visible",
169
+ enabled: true,
170
+ stage: "stable",
171
+ },
172
+ ],
173
+ migrations: [],
174
+ schemaExports: [],
175
+ tasks: [],
176
+ tools: [
177
+ {
178
+ key: `${name}.tool`,
179
+ label: "业务工具",
180
+ description: "AI Operation 测试工具。",
181
+ permissionKey,
182
+ featureKey,
183
+ auditAction: `${name}.ai.execute`,
184
+ },
185
+ ],
186
+ providers: [],
187
+ auditActions: [
188
+ {
189
+ key: `${name}.ai.execute`,
190
+ label: "执行 AI",
191
+ description: "执行 AI Operation。",
192
+ },
193
+ ],
194
+ };
195
+ }