@downcity/agent 1.1.153 → 1.1.160

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 (68) hide show
  1. package/bin/agent/local/services/AgentAssemblyService.d.ts.map +1 -1
  2. package/bin/agent/local/services/AgentAssemblyService.js +1 -0
  3. package/bin/agent/local/services/AgentAssemblyService.js.map +1 -1
  4. package/bin/executor/composer/system/default/assets/plugin.prompt.d.ts +1 -1
  5. package/bin/executor/composer/system/default/assets/plugin.prompt.d.ts.map +1 -1
  6. package/bin/executor/composer/system/default/assets/plugin.prompt.js +1 -1
  7. package/bin/executor/composer/system/default/assets/plugin.prompt.js.map +1 -1
  8. package/bin/executor/messages/AssistantFileResource.d.ts.map +1 -1
  9. package/bin/executor/messages/AssistantFileResource.js +106 -15
  10. package/bin/executor/messages/AssistantFileResource.js.map +1 -1
  11. package/bin/executor/tools/plugin/PluginToolBridge.d.ts +5 -1
  12. package/bin/executor/tools/plugin/PluginToolBridge.d.ts.map +1 -1
  13. package/bin/executor/tools/plugin/PluginToolBridge.js +26 -0
  14. package/bin/executor/tools/plugin/PluginToolBridge.js.map +1 -1
  15. package/bin/executor/tools/plugin/PluginToolDefinition.d.ts +11 -0
  16. package/bin/executor/tools/plugin/PluginToolDefinition.d.ts.map +1 -1
  17. package/bin/executor/tools/plugin/PluginToolDefinition.js +12 -3
  18. package/bin/executor/tools/plugin/PluginToolDefinition.js.map +1 -1
  19. package/bin/executor/tools/plugin/PluginToolSchemas.d.ts +4 -0
  20. package/bin/executor/tools/plugin/PluginToolSchemas.d.ts.map +1 -1
  21. package/bin/executor/tools/plugin/PluginToolSchemas.js +11 -1
  22. package/bin/executor/tools/plugin/PluginToolSchemas.js.map +1 -1
  23. package/bin/executor/tools/plugin/types/PluginTool.d.ts +20 -0
  24. package/bin/executor/tools/plugin/types/PluginTool.d.ts.map +1 -1
  25. package/bin/index.d.ts +3 -1
  26. package/bin/index.d.ts.map +1 -1
  27. package/bin/index.js +1 -0
  28. package/bin/index.js.map +1 -1
  29. package/bin/plugin/core/PluginActionFactory.d.ts +99 -0
  30. package/bin/plugin/core/PluginActionFactory.d.ts.map +1 -0
  31. package/bin/plugin/core/PluginActionFactory.js +59 -0
  32. package/bin/plugin/core/PluginActionFactory.js.map +1 -0
  33. package/bin/plugin/core/PluginActionRunner.d.ts.map +1 -1
  34. package/bin/plugin/core/PluginActionRunner.js +14 -1
  35. package/bin/plugin/core/PluginActionRunner.js.map +1 -1
  36. package/bin/plugin/core/PluginLocalExecution.d.ts.map +1 -1
  37. package/bin/plugin/core/PluginLocalExecution.js +15 -1
  38. package/bin/plugin/core/PluginLocalExecution.js.map +1 -1
  39. package/bin/plugin/core/PluginRegistry.d.ts +18 -1
  40. package/bin/plugin/core/PluginRegistry.d.ts.map +1 -1
  41. package/bin/plugin/core/PluginRegistry.js +74 -1
  42. package/bin/plugin/core/PluginRegistry.js.map +1 -1
  43. package/bin/plugin/types/Plugin.d.ts +2 -2
  44. package/bin/plugin/types/Plugin.d.ts.map +1 -1
  45. package/bin/types/plugin/PluginAction.d.ts +40 -2
  46. package/bin/types/plugin/PluginAction.d.ts.map +1 -1
  47. package/bin/types/plugin/PluginRuntime.d.ts +44 -0
  48. package/bin/types/plugin/PluginRuntime.d.ts.map +1 -1
  49. package/package.json +3 -3
  50. package/scripts/image-plugin-job.test.mjs +282 -68
  51. package/scripts/plugin-tool-bridge.test.mjs +113 -2
  52. package/src/agent/local/services/AgentAssemblyService.ts +1 -0
  53. package/src/executor/composer/system/default/assets/plugin.prompt.ts +1 -1
  54. package/src/executor/composer/system/default/assets/plugin.prompt.ts.txt +3 -2
  55. package/src/executor/messages/AssistantFileResource.ts +121 -16
  56. package/src/executor/tools/plugin/PluginToolBridge.ts +30 -0
  57. package/src/executor/tools/plugin/PluginToolDefinition.ts +25 -4
  58. package/src/executor/tools/plugin/PluginToolSchemas.ts +12 -1
  59. package/src/executor/tools/plugin/types/PluginTool.ts +22 -0
  60. package/src/index.ts +11 -0
  61. package/src/plugin/core/PluginActionFactory.ts +185 -0
  62. package/src/plugin/core/PluginActionRunner.ts +14 -1
  63. package/src/plugin/core/PluginLocalExecution.ts +15 -1
  64. package/src/plugin/core/PluginRegistry.ts +89 -1
  65. package/src/plugin/types/Plugin.ts +5 -0
  66. package/src/types/plugin/PluginAction.ts +43 -2
  67. package/src/types/plugin/PluginRuntime.ts +44 -0
  68. package/tsconfig.tsbuildinfo +1 -1
@@ -140,22 +140,92 @@ async function read_remote_resource(raw_url: string): Promise<{
140
140
  filename?: string;
141
141
  bytes: Buffer;
142
142
  }> {
143
- const response = await fetch(raw_url);
144
- if (!response.ok) {
145
- throw new Error(`Failed to download assistant file resource: ${response.status}`);
143
+ const result = await fetch_with_retry(raw_url);
144
+ if (!result.ok) {
145
+ throw new Error(
146
+ `Failed to download assistant file resource: ${result.status} :: url=${raw_url}`,
147
+ );
146
148
  }
147
- const array_buffer = await response.arrayBuffer();
149
+ const array_buffer = await result.arrayBuffer();
148
150
  const bytes = Buffer.from(array_buffer);
149
151
  if (bytes.length === 0) {
150
- throw new Error("Downloaded assistant file resource is empty");
152
+ throw new Error(`Downloaded assistant file resource is empty :: url=${raw_url}`);
151
153
  }
152
154
  return {
153
- mediaType: response.headers.get("content-type")?.split(";")[0]?.trim(),
155
+ mediaType: result.headers.get("content-type")?.split(";")[0]?.trim(),
154
156
  filename: filename_from_url(raw_url),
155
157
  bytes,
156
158
  };
157
159
  }
158
160
 
161
+ /**
162
+ * 带重试的 fetch 远程资源。
163
+ *
164
+ * 关键点(中文)
165
+ * - 对 transient 网络错误(fetch failed / UND_ERR_* / ECONNRESET 等)做 2 次指数退避重试。
166
+ * - 最终失败时把 error.cause 链展开到 message,方便定位是代理问题、DNS 问题还是上游问题。
167
+ */
168
+ async function fetch_with_retry(
169
+ raw_url: string,
170
+ ): Promise<Response> {
171
+ const delays_ms = [250, 1_000];
172
+ const attempts = delays_ms.length + 1;
173
+ let last_error: unknown;
174
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
175
+ try {
176
+ return await fetch(raw_url);
177
+ } catch (error) {
178
+ last_error = error;
179
+ if (!is_transient_fetch_error(error) || attempt === attempts - 1) {
180
+ throw enrich_fetch_error(error, raw_url);
181
+ }
182
+ const delay_ms = delays_ms[attempt] ?? 1_000;
183
+ await new Promise((resolve) => setTimeout(resolve, delay_ms));
184
+ }
185
+ }
186
+ throw enrich_fetch_error(last_error, raw_url);
187
+ }
188
+
189
+ function is_transient_fetch_error(error: unknown): boolean {
190
+ if (!(error instanceof Error)) return false;
191
+ const text = `${error.message} ${describe_error_cause(error)}`;
192
+ return /fetch failed|UND_ERR|ECONN(RESET|REFUSED|ABORTED)|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|socket hang up/i.test(
193
+ text,
194
+ );
195
+ }
196
+
197
+ function enrich_fetch_error(error: unknown, url: string): Error {
198
+ if (!(error instanceof Error)) {
199
+ return new Error(`Download failed: ${String(error)} :: url=${url}`);
200
+ }
201
+ const cause_text = describe_error_cause(error);
202
+ const enriched = new Error(
203
+ `${error.message}${cause_text ? ` :: cause=${cause_text}` : ""} :: url=${url}`,
204
+ error.cause ? { cause: error.cause } : undefined,
205
+ );
206
+ enriched.stack = error.stack;
207
+ return enriched;
208
+ }
209
+
210
+ function describe_error_cause(error: Error): string {
211
+ const parts: string[] = [];
212
+ let current: unknown = (error as { cause?: unknown }).cause;
213
+ let depth = 0;
214
+ while (current && depth < 3) {
215
+ if (current instanceof Error) {
216
+ const code = (current as { code?: unknown }).code;
217
+ const code_text = typeof code === "string" && code ? `[${code}] ` : "";
218
+ parts.push(`${code_text}${current.message || current.name}`.trim());
219
+ current = (current as { cause?: unknown }).cause;
220
+ } else {
221
+ parts.push(String(current));
222
+ break;
223
+ }
224
+ depth += 1;
225
+ }
226
+ return parts.filter(Boolean).join(" -> ");
227
+ }
228
+
159
229
  async function read_local_resource(
160
230
  projectRoot: string,
161
231
  raw_url: string,
@@ -191,19 +261,55 @@ async function materialize_file_part(params: {
191
261
  if (raw_url.startsWith("resources://")) return params.part;
192
262
 
193
263
  const parsed_data_url = parse_data_url(raw_url);
194
- const source = parsed_data_url
195
- ? {
196
- mediaType: parsed_data_url.media_type,
264
+ if (parsed_data_url) {
265
+ const media_type =
266
+ String(params.part.mediaType || parsed_data_url.media_type || "").trim() ||
267
+ "application/octet-stream";
268
+ const file_path = await write_resource_file({
269
+ projectRoot: params.projectRoot,
270
+ mediaType: media_type,
271
+ filename:
272
+ typeof params.part.filename === "string" ? params.part.filename : undefined,
273
+ bytes: parsed_data_url.bytes,
274
+ });
275
+ return {
276
+ ...params.part,
277
+ mediaType: media_type,
278
+ url: to_resources_url(params.projectRoot, file_path),
279
+ };
280
+ }
281
+
282
+ if (raw_url.startsWith("http://") || raw_url.startsWith("https://")) {
283
+ try {
284
+ const source = await read_remote_resource(raw_url);
285
+ const media_type =
286
+ String(params.part.mediaType || source.mediaType || "").trim() ||
287
+ "application/octet-stream";
288
+ const file_path = await write_resource_file({
289
+ projectRoot: params.projectRoot,
290
+ mediaType: media_type,
197
291
  filename:
198
292
  typeof params.part.filename === "string"
199
293
  ? params.part.filename
200
- : undefined,
201
- bytes: parsed_data_url.bytes,
202
- }
203
- : raw_url.startsWith("http://") || raw_url.startsWith("https://")
204
- ? await read_remote_resource(raw_url)
205
- : await read_local_resource(params.projectRoot, raw_url);
294
+ : source.filename,
295
+ bytes: source.bytes,
296
+ });
297
+ return {
298
+ ...params.part,
299
+ mediaType: media_type,
300
+ url: to_resources_url(params.projectRoot, file_path),
301
+ };
302
+ } catch (error) {
303
+ // 关键点(中文):远程下载失败时保留原始 URL,不让整张图导致 action 失败。
304
+ // 这样图片至少以链接形式存在,用户可手动点击或后续再试。
305
+ console.warn(
306
+ `[AssistantFileResource] remote download failed, keeping original url :: ${describe_error_cause(error instanceof Error ? error : new Error(String(error)))} :: url=${raw_url}`,
307
+ );
308
+ return params.part;
309
+ }
310
+ }
206
311
 
312
+ const source = await read_local_resource(params.projectRoot, raw_url);
207
313
  const media_type =
208
314
  String(params.part.mediaType || source.mediaType || "").trim() ||
209
315
  "application/octet-stream";
@@ -216,7 +322,6 @@ async function materialize_file_part(params: {
216
322
  : source.filename,
217
323
  bytes: source.bytes,
218
324
  });
219
-
220
325
  return {
221
326
  ...params.part,
222
327
  mediaType: media_type,
@@ -15,6 +15,8 @@ import type {
15
15
  PluginCallInput,
16
16
  PluginCallToolFileResult,
17
17
  PluginCallToolResult,
18
+ PluginReadInput,
19
+ PluginReadToolResult,
18
20
  } from "@/executor/tools/plugin/types/PluginTool.js";
19
21
  import { materializeAssistantFileParts } from "@executor/messages/AssistantFileResource.js";
20
22
  import {
@@ -206,3 +208,31 @@ export async function invokePluginCallTool(
206
208
  };
207
209
  }
208
210
  }
211
+
212
+ /**
213
+ * 读取 plugin / action metadata。
214
+ */
215
+ export async function invokePluginReadTool(
216
+ input: PluginReadInput,
217
+ ): Promise<PluginReadToolResult> {
218
+ try {
219
+ const runtime = require_plugin_tool_runtime();
220
+ const data = runtime.read({
221
+ plugin: typeof input.plugin === "string" ? input.plugin : undefined,
222
+ action: typeof input.action === "string" ? input.action : undefined,
223
+ });
224
+ return {
225
+ success: true,
226
+ message: "plugin metadata read",
227
+ data: data as unknown as JsonObject,
228
+ };
229
+ } catch (error) {
230
+ return {
231
+ success: false,
232
+ message: String(error),
233
+ data: {
234
+ error: String(error),
235
+ },
236
+ };
237
+ }
238
+ }
@@ -8,9 +8,19 @@
8
8
  */
9
9
 
10
10
  import { tool } from "ai";
11
- import type { PluginCallInput } from "@/executor/tools/plugin/types/PluginTool.js";
12
- import { invokePluginCallTool, setPluginToolRuntime } from "./PluginToolBridge.js";
13
- import { plugin_call_input_schema } from "./PluginToolSchemas.js";
11
+ import type {
12
+ PluginCallInput,
13
+ PluginReadInput,
14
+ } from "@/executor/tools/plugin/types/PluginTool.js";
15
+ import {
16
+ invokePluginCallTool,
17
+ invokePluginReadTool,
18
+ setPluginToolRuntime,
19
+ } from "./PluginToolBridge.js";
20
+ import {
21
+ plugin_call_input_schema,
22
+ plugin_read_input_schema,
23
+ } from "./PluginToolSchemas.js";
14
24
 
15
25
  export { setPluginToolRuntime } from "./PluginToolBridge.js";
16
26
 
@@ -19,14 +29,25 @@ export { setPluginToolRuntime } from "./PluginToolBridge.js";
19
29
  */
20
30
  export const plugin_call = tool({
21
31
  description:
22
- "Call a registered agent plugin action. Use this for plugin capabilities such as image generation. Generated files may be attached to the final assistant message automatically.",
32
+ "Call a registered agent plugin action. Use plugin_read first when you need the action list, input schema, or examples. Generated files may be attached to the final assistant message automatically.",
23
33
  inputSchema: plugin_call_input_schema,
24
34
  execute: async (input) => await invokePluginCallTool(input as PluginCallInput),
25
35
  });
26
36
 
37
+ /**
38
+ * `plugin_read`:读取已注册 plugin / action metadata。
39
+ */
40
+ export const plugin_read = tool({
41
+ description:
42
+ "Read registered agent plugin metadata, including action names, descriptions, input schemas, and examples. Use this before plugin_call when the payload shape is unclear.",
43
+ inputSchema: plugin_read_input_schema,
44
+ execute: async (input) => await invokePluginReadTool(input as PluginReadInput),
45
+ });
46
+
27
47
  /**
28
48
  * Plugin 工具导出集合。
29
49
  */
30
50
  export const plugin_tools = {
31
51
  plugin_call,
52
+ plugin_read,
32
53
  };
@@ -10,7 +10,7 @@ import { z } from "zod";
10
10
 
11
11
  export const plugin_call_input_schema = z.object({
12
12
  plugin: z.string().describe("Registered plugin name to call, for example image."),
13
- action: z.string().describe("Plugin action name to execute, for example generate."),
13
+ action: z.string().describe("Plugin action name to execute, for example image_create."),
14
14
  payload: z
15
15
  .object({})
16
16
  .passthrough()
@@ -18,3 +18,14 @@ export const plugin_call_input_schema = z.object({
18
18
  .default({})
19
19
  .describe("JSON payload passed to the plugin action."),
20
20
  });
21
+
22
+ export const plugin_read_input_schema = z.object({
23
+ plugin: z
24
+ .string()
25
+ .optional()
26
+ .describe("Registered plugin name to inspect. Omit to list plugins."),
27
+ action: z
28
+ .string()
29
+ .optional()
30
+ .describe("Plugin action name to inspect. Requires plugin."),
31
+ });
@@ -20,6 +20,16 @@ export interface PluginCallInput {
20
20
  payload?: JsonObject;
21
21
  }
22
22
 
23
+ /**
24
+ * plugin_read 输入。
25
+ */
26
+ export interface PluginReadInput {
27
+ /** 要读取的 plugin 名称;不传则列出 plugin 概览。 */
28
+ plugin?: string;
29
+ /** 要读取的 action 名称;仅在 plugin 存在时生效。 */
30
+ action?: string;
31
+ }
32
+
23
33
  /**
24
34
  * plugin_call 产生的 assistant 文件摘要。
25
35
  */
@@ -57,3 +67,15 @@ export interface PluginCallToolResult {
57
67
  /** 返回给模型读取的短摘要数据。 */
58
68
  data?: JsonObject;
59
69
  }
70
+
71
+ /**
72
+ * plugin_read 返回给模型的 metadata。
73
+ */
74
+ export interface PluginReadToolResult {
75
+ /** 调用是否成功。 */
76
+ success: boolean;
77
+ /** 人类可读消息。 */
78
+ message: string;
79
+ /** 读取到的 metadata。 */
80
+ data: JsonObject;
81
+ }
package/src/index.ts CHANGED
@@ -72,6 +72,14 @@ export type {
72
72
 
73
73
  // Plugin 作者 API
74
74
  export { BasePlugin } from "./plugin/core/BasePlugin.js";
75
+ export {
76
+ createAction,
77
+ createPlugin,
78
+ } from "./plugin/core/PluginActionFactory.js";
79
+ export type {
80
+ CreatePluginActionOptions,
81
+ CreatePluginOptions,
82
+ } from "./plugin/core/PluginActionFactory.js";
75
83
 
76
84
  // Session 与即时执行集成
77
85
  export { Executor } from "./executor/Executor.js";
@@ -229,6 +237,9 @@ export type {
229
237
  PluginActionApi,
230
238
  PluginActionCommand,
231
239
  PluginActionCommandInput,
240
+ PluginActionExample,
241
+ PluginActionInputSchema,
242
+ PluginActionMetadata,
232
243
  PluginActionResult,
233
244
  PluginActions,
234
245
  PluginAvailability,
@@ -0,0 +1,185 @@
1
+ /**
2
+ * PluginActionFactory:创建带 metadata 和 schema 的 plugin/action。
3
+ *
4
+ * 关键点(中文)
5
+ * - `createAction` 让 action 的运行时 schema 与 TypeScript 输入类型保持一致。
6
+ * - `createPlugin` 只做轻量对象装配,不强制继承 BasePlugin。
7
+ * - 旧的 class extends BasePlugin 写法仍可继续使用。
8
+ */
9
+
10
+ import type { z } from "zod";
11
+ import type { AgentContext } from "@/types/runtime/agent/AgentContext.js";
12
+ import type { JsonValue } from "@/types/common/Json.js";
13
+ import type {
14
+ PluginAction,
15
+ PluginActionApi,
16
+ PluginActionCommand,
17
+ PluginActionExample,
18
+ PluginActionInputSchema,
19
+ PluginActionResult,
20
+ PluginActions,
21
+ } from "@/types/plugin/PluginAction.js";
22
+ import type { Plugin } from "@/types/plugin/PluginDefinition.js";
23
+ import type {
24
+ PluginAvailability,
25
+ PluginConfigDefinition,
26
+ PluginHooks,
27
+ PluginResolves,
28
+ } from "@/types/plugin/PluginRuntime.js";
29
+ import type {
30
+ PluginCommandContext,
31
+ PluginLifecycle,
32
+ } from "@/types/plugin/PluginCommand.js";
33
+ import type {
34
+ PluginSetupDefinition,
35
+ PluginUsageDefinition,
36
+ } from "@/types/plugin/PluginSetup.js";
37
+ import type { PluginHttpDefinition } from "@/types/plugin/PluginHttp.js";
38
+ import type { StructuredConfig } from "@/types/runtime/agent/AgentContext.js";
39
+
40
+ /**
41
+ * 从 Zod schema 推导 JSON 输入类型。
42
+ */
43
+ type InferZodJson<TSchema extends z.ZodTypeAny> =
44
+ z.infer<TSchema> extends JsonValue ? z.infer<TSchema> : JsonValue;
45
+
46
+ /**
47
+ * createAction 参数。
48
+ */
49
+ export interface CreatePluginActionOptions<
50
+ P extends JsonValue,
51
+ R extends JsonValue,
52
+ > {
53
+ /** Action 用途说明。 */
54
+ description?: string;
55
+ /** Zod 输入 schema。 */
56
+ input_schema?: z.ZodTypeAny | PluginActionInputSchema<P>;
57
+ /** Action 调用示例。 */
58
+ examples?: PluginActionExample<P>[];
59
+ /** disabled 状态下是否仍允许执行。 */
60
+ allowWhenDisabled?: boolean;
61
+ /** CLI 定义。 */
62
+ command?: PluginActionCommand<P>;
63
+ /** HTTP 定义。 */
64
+ api?: PluginActionApi<P>;
65
+ /** Action 执行器。 */
66
+ execute: (params: {
67
+ /** 当前执行上下文。 */
68
+ context: AgentContext;
69
+ /** 已通过 schema 校验后的输入。 */
70
+ input: P;
71
+ /** 兼容旧命名的 payload。 */
72
+ payload: P;
73
+ /** 当前插件名称。 */
74
+ pluginName: string;
75
+ /** 当前 Action 名称。 */
76
+ actionName: string;
77
+ }) => Promise<PluginActionResult<R>> | PluginActionResult<R>;
78
+ }
79
+
80
+ /**
81
+ * createPlugin 参数。
82
+ */
83
+ export interface CreatePluginOptions<TActions extends PluginActions> {
84
+ /** Plugin 稳定名称。 */
85
+ name: string;
86
+ /** Plugin 展示标题。 */
87
+ title?: string;
88
+ /** Plugin 用途说明。 */
89
+ description?: string;
90
+ /** Plugin 显式 action 集合。 */
91
+ actions?: TActions;
92
+ /** Plugin 配置定义。 */
93
+ config?: PluginConfigDefinition<StructuredConfig>;
94
+ /** Plugin setup 定义。 */
95
+ setup?: PluginSetupDefinition;
96
+ /** Plugin usage 定义。 */
97
+ usage?: PluginUsageDefinition;
98
+ /** Plugin hook 集合。 */
99
+ hooks?: PluginHooks;
100
+ /** Plugin resolve 集合。 */
101
+ resolves?: PluginResolves;
102
+ /** Plugin system 文本构建器。 */
103
+ system?: (context: AgentContext) => string | Promise<string>;
104
+ /** 旧生命周期兼容定义。 */
105
+ lifecycle?: PluginLifecycle;
106
+ /** Plugin 可用性检查。 */
107
+ availability?: (
108
+ context: PluginCommandContext | AgentContext,
109
+ ) => Promise<PluginAvailability> | PluginAvailability;
110
+ /** Plugin HTTP 注入定义。 */
111
+ http?: PluginHttpDefinition;
112
+ }
113
+
114
+ /**
115
+ * 归一化输入 schema。
116
+ */
117
+ function normalize_input_schema<P extends JsonValue>(
118
+ input_schema: z.ZodTypeAny | PluginActionInputSchema<P> | undefined,
119
+ ): PluginActionInputSchema<P> | undefined {
120
+ if (!input_schema) return undefined;
121
+ if (typeof (input_schema as z.ZodTypeAny).safeParse === "function") {
122
+ return { zod: input_schema as z.ZodTypeAny };
123
+ }
124
+ return input_schema as PluginActionInputSchema<P>;
125
+ }
126
+
127
+ /**
128
+ * 创建带 metadata 的 action。
129
+ */
130
+ export function createAction<
131
+ TSchema extends z.ZodTypeAny,
132
+ R extends JsonValue = JsonValue,
133
+ >(
134
+ options: CreatePluginActionOptions<InferZodJson<TSchema>, R> & {
135
+ /** Zod 输入 schema。 */
136
+ input_schema?: TSchema | PluginActionInputSchema<InferZodJson<TSchema>>;
137
+ },
138
+ ): PluginAction<InferZodJson<TSchema>, R>;
139
+ export function createAction<R extends JsonValue = JsonValue>(
140
+ options: CreatePluginActionOptions<JsonValue, R>,
141
+ ): PluginAction<JsonValue, R>;
142
+ export function createAction(
143
+ options: CreatePluginActionOptions<JsonValue, JsonValue>,
144
+ ): PluginAction<JsonValue, JsonValue> {
145
+ return {
146
+ ...(options.description ? { description: options.description } : {}),
147
+ ...(options.input_schema
148
+ ? { input_schema: normalize_input_schema(options.input_schema) }
149
+ : {}),
150
+ ...(options.examples ? { examples: options.examples } : {}),
151
+ ...(typeof options.allowWhenDisabled === "boolean"
152
+ ? { allowWhenDisabled: options.allowWhenDisabled }
153
+ : {}),
154
+ ...(options.command ? { command: options.command } : {}),
155
+ ...(options.api ? { api: options.api } : {}),
156
+ execute: options.execute,
157
+ };
158
+ }
159
+
160
+ /**
161
+ * 创建 plugin 对象。
162
+ */
163
+ export function createPlugin<TActions extends PluginActions>(
164
+ options: CreatePluginOptions<TActions>,
165
+ ): Plugin & { actions: TActions } {
166
+ const name = String(options.name || "").trim();
167
+ if (!name) {
168
+ throw new Error("createPlugin requires a non-empty name");
169
+ }
170
+ return {
171
+ name,
172
+ title: String(options.title || name).trim(),
173
+ description: String(options.description || "").trim(),
174
+ actions: options.actions || ({} as TActions),
175
+ ...(options.config ? { config: options.config } : {}),
176
+ ...(options.setup ? { setup: options.setup } : {}),
177
+ ...(options.usage ? { usage: options.usage } : {}),
178
+ ...(options.hooks ? { hooks: options.hooks } : {}),
179
+ ...(options.resolves ? { resolves: options.resolves } : {}),
180
+ ...(options.system ? { system: options.system } : {}),
181
+ ...(options.lifecycle ? { lifecycle: options.lifecycle } : {}),
182
+ ...(options.availability ? { availability: options.availability } : {}),
183
+ ...(options.http ? { http: options.http } : {}),
184
+ };
185
+ }
@@ -58,9 +58,22 @@ export async function invokePluginAction(params: {
58
58
  }
59
59
 
60
60
  try {
61
+ const payload = (params.payload ?? {}) as JsonValue;
62
+ const schema = action.input_schema?.zod;
63
+ const parsed_payload = schema ? schema.safeParse(payload) : null;
64
+ if (parsed_payload && !parsed_payload.success) {
65
+ return {
66
+ success: false,
67
+ error: `Invalid payload for ${params.plugin.name}.${params.actionName}: ${parsed_payload.error.message}`,
68
+ };
69
+ }
70
+ const input_payload = parsed_payload?.success
71
+ ? parsed_payload.data as JsonValue
72
+ : payload;
61
73
  return await action.execute({
62
74
  context: params.context,
63
- payload: (params.payload ?? {}) as JsonValue,
75
+ payload: input_payload,
76
+ input: input_payload,
64
77
  pluginName: params.plugin.name,
65
78
  actionName: params.actionName,
66
79
  });
@@ -135,9 +135,23 @@ export async function runLocalPluginAction(params: {
135
135
  }
136
136
 
137
137
  try {
138
+ const payload = (params.payload ?? {}) as JsonValue;
139
+ const schema = action.input_schema?.zod;
140
+ const parsed_payload = schema ? schema.safeParse(payload) : null;
141
+ if (parsed_payload && !parsed_payload.success) {
142
+ return {
143
+ success: false,
144
+ error: `Invalid payload for ${plugin.name}.${actionName}: ${parsed_payload.error.message}`,
145
+ message: `Invalid payload for ${plugin.name}.${actionName}`,
146
+ };
147
+ }
148
+ const input_payload = parsed_payload?.success
149
+ ? parsed_payload.data as JsonValue
150
+ : payload;
138
151
  return await action.execute({
139
152
  context: context as unknown as AgentContext,
140
- payload: (params.payload ?? {}) as JsonValue,
153
+ payload: input_payload,
154
+ input: input_payload,
141
155
  pluginName: plugin.name,
142
156
  actionName,
143
157
  });