@downcity/agent 1.1.152 → 1.1.157

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 (64) 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/tools/plugin/PluginToolBridge.d.ts +6 -2
  9. package/bin/executor/tools/plugin/PluginToolBridge.d.ts.map +1 -1
  10. package/bin/executor/tools/plugin/PluginToolBridge.js +71 -17
  11. package/bin/executor/tools/plugin/PluginToolBridge.js.map +1 -1
  12. package/bin/executor/tools/plugin/PluginToolDefinition.d.ts +11 -0
  13. package/bin/executor/tools/plugin/PluginToolDefinition.d.ts.map +1 -1
  14. package/bin/executor/tools/plugin/PluginToolDefinition.js +12 -3
  15. package/bin/executor/tools/plugin/PluginToolDefinition.js.map +1 -1
  16. package/bin/executor/tools/plugin/PluginToolSchemas.d.ts +4 -0
  17. package/bin/executor/tools/plugin/PluginToolSchemas.d.ts.map +1 -1
  18. package/bin/executor/tools/plugin/PluginToolSchemas.js +11 -1
  19. package/bin/executor/tools/plugin/PluginToolSchemas.js.map +1 -1
  20. package/bin/executor/tools/plugin/types/PluginTool.d.ts +37 -0
  21. package/bin/executor/tools/plugin/types/PluginTool.d.ts.map +1 -1
  22. package/bin/index.d.ts +3 -1
  23. package/bin/index.d.ts.map +1 -1
  24. package/bin/index.js +1 -0
  25. package/bin/index.js.map +1 -1
  26. package/bin/plugin/core/PluginActionFactory.d.ts +99 -0
  27. package/bin/plugin/core/PluginActionFactory.d.ts.map +1 -0
  28. package/bin/plugin/core/PluginActionFactory.js +59 -0
  29. package/bin/plugin/core/PluginActionFactory.js.map +1 -0
  30. package/bin/plugin/core/PluginActionRunner.d.ts.map +1 -1
  31. package/bin/plugin/core/PluginActionRunner.js +14 -1
  32. package/bin/plugin/core/PluginActionRunner.js.map +1 -1
  33. package/bin/plugin/core/PluginLocalExecution.d.ts.map +1 -1
  34. package/bin/plugin/core/PluginLocalExecution.js +15 -1
  35. package/bin/plugin/core/PluginLocalExecution.js.map +1 -1
  36. package/bin/plugin/core/PluginRegistry.d.ts +18 -1
  37. package/bin/plugin/core/PluginRegistry.d.ts.map +1 -1
  38. package/bin/plugin/core/PluginRegistry.js +74 -1
  39. package/bin/plugin/core/PluginRegistry.js.map +1 -1
  40. package/bin/plugin/types/Plugin.d.ts +2 -2
  41. package/bin/plugin/types/Plugin.d.ts.map +1 -1
  42. package/bin/types/plugin/PluginAction.d.ts +40 -2
  43. package/bin/types/plugin/PluginAction.d.ts.map +1 -1
  44. package/bin/types/plugin/PluginRuntime.d.ts +44 -0
  45. package/bin/types/plugin/PluginRuntime.d.ts.map +1 -1
  46. package/package.json +3 -3
  47. package/scripts/image-plugin-job.test.mjs +308 -32
  48. package/scripts/plugin-tool-bridge.test.mjs +199 -0
  49. package/src/agent/local/services/AgentAssemblyService.ts +1 -0
  50. package/src/executor/composer/system/default/assets/plugin.prompt.ts +1 -1
  51. package/src/executor/composer/system/default/assets/plugin.prompt.ts.txt +3 -2
  52. package/src/executor/tools/plugin/PluginToolBridge.ts +84 -18
  53. package/src/executor/tools/plugin/PluginToolDefinition.ts +25 -4
  54. package/src/executor/tools/plugin/PluginToolSchemas.ts +12 -1
  55. package/src/executor/tools/plugin/types/PluginTool.ts +40 -0
  56. package/src/index.ts +11 -0
  57. package/src/plugin/core/PluginActionFactory.ts +185 -0
  58. package/src/plugin/core/PluginActionRunner.ts +14 -1
  59. package/src/plugin/core/PluginLocalExecution.ts +15 -1
  60. package/src/plugin/core/PluginRegistry.ts +89 -1
  61. package/src/plugin/types/Plugin.ts +5 -0
  62. package/src/types/plugin/PluginAction.ts +43 -2
  63. package/src/types/plugin/PluginRuntime.ts +44 -0
  64. package/tsconfig.tsbuildinfo +1 -1
@@ -4,15 +4,19 @@
4
4
  * 关键点(中文)
5
5
  * - tool 层不直接持有 agent 或 plugin registry,只通过装配期注入的 AgentPlugins 调用 action。
6
6
  * - 如果 action 返回 AI SDK UIMessage,则抽取 file parts 并入最终 assistant 消息。
7
- * - 返回给模型的 tool result 只保留短摘要,避免 data URL 等大内容污染上下文。
7
+ * - 返回给模型的 tool result 只保留短摘要和本地绝对路径,避免 data URL 等大内容污染上下文。
8
8
  */
9
9
 
10
+ import path from "node:path";
10
11
  import type { FileUIPart } from "ai";
11
12
  import type { JsonObject, JsonValue } from "@/types/common/Json.js";
12
13
  import type { AgentPlugins } from "@/plugin/types/Plugin.js";
13
14
  import type {
14
15
  PluginCallInput,
16
+ PluginCallToolFileResult,
15
17
  PluginCallToolResult,
18
+ PluginReadInput,
19
+ PluginReadToolResult,
16
20
  } from "@/executor/tools/plugin/types/PluginTool.js";
17
21
  import { materializeAssistantFileParts } from "@executor/messages/AssistantFileResource.js";
18
22
  import {
@@ -69,27 +73,57 @@ function extract_assistant_file_parts(data: JsonValue | undefined): FileUIPart[]
69
73
  return parts.filter(is_file_part);
70
74
  }
71
75
 
76
+ /**
77
+ * 解析当前项目根目录,保持与 assistant 文件落盘逻辑一致。
78
+ */
79
+ function resolve_project_root(project_root: string | undefined): string {
80
+ const raw = String(project_root || "").trim();
81
+ return path.resolve(raw || process.cwd());
82
+ }
83
+
84
+ /**
85
+ * 将 `resources://` URL 转成本机绝对路径。
86
+ */
87
+ function resolve_resources_file_path(
88
+ project_root: string,
89
+ raw_url: string,
90
+ ): string {
91
+ const prefix = "resources://";
92
+ const raw = String(raw_url || "").trim();
93
+ if (!raw.startsWith(prefix)) return "";
94
+ const relative = raw.slice(prefix.length).replace(/^\/+/, "");
95
+ if (!relative) return "";
96
+
97
+ const file_path = path.resolve(project_root, relative);
98
+ const rel = path.relative(project_root, file_path);
99
+ if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) return "";
100
+ return file_path;
101
+ }
102
+
103
+ /**
104
+ * 构建返回给模型和用户可见的文件摘要。
105
+ */
106
+ function summarize_materialized_files(
107
+ parts: FileUIPart[],
108
+ project_root: string,
109
+ ): PluginCallToolFileResult[] {
110
+ return parts.map((part, index) => ({
111
+ index,
112
+ media_type: part.mediaType,
113
+ filename: typeof part.filename === "string" ? part.filename : "",
114
+ url: String(part.url || ""),
115
+ path: resolve_resources_file_path(project_root, String(part.url || "")),
116
+ }));
117
+ }
118
+
72
119
  /**
73
120
  * 生成给模型读取的短摘要。
74
121
  */
75
- function summarize_action_data(data: JsonValue | undefined): JsonObject {
122
+ function summarize_action_data(data: JsonValue | undefined): JsonObject | undefined {
76
123
  const message = to_json_object(data);
77
124
  const parts: unknown[] = Array.isArray(message?.parts) ? message.parts : [];
78
- const file_parts = parts.filter(is_file_part);
79
125
  if (parts.length > 0) {
80
- return {
81
- kind: "ui_message",
82
- role: typeof message?.role === "string" ? message.role : "assistant",
83
- part_count: parts.length,
84
- file_count: file_parts.length,
85
- files: file_parts.map((part, index) => ({
86
- index,
87
- mediaType: part.mediaType,
88
- filename: typeof part.filename === "string" ? part.filename : "",
89
- // 关键点(中文):不把 data URL 或长 URL 原样返回给模型,完整内容只进入 assistant file part。
90
- has_url: Boolean(part.url),
91
- })),
92
- };
126
+ return undefined;
93
127
  }
94
128
  if (data === undefined) return {};
95
129
  return {
@@ -135,29 +169,33 @@ export async function invokePluginCallTool(
135
169
  action,
136
170
  payload,
137
171
  });
172
+ const project_root = resolve_project_root(getSessionRunContext()?.projectRoot);
138
173
  const raw_file_parts = result.success
139
174
  ? extract_assistant_file_parts(result.data)
140
175
  : [];
141
176
  const file_parts =
142
177
  raw_file_parts.length > 0
143
178
  ? await materializeAssistantFileParts({
144
- projectRoot: getSessionRunContext()?.projectRoot,
179
+ projectRoot: project_root,
145
180
  parts: raw_file_parts,
146
181
  })
147
182
  : [];
183
+ const files = summarize_materialized_files(file_parts, project_root);
148
184
  if (file_parts.length > 0) {
149
185
  enqueueAssistantFileParts(file_parts);
150
186
  }
187
+ const data = summarize_action_data(result.data);
151
188
  return {
152
189
  success: result.success,
153
190
  plugin,
154
191
  action,
155
192
  assistant_file_count: file_parts.length,
193
+ ...(files.length > 0 ? { files } : {}),
156
194
  message:
157
195
  String(result.message || result.error || "").trim() ||
158
196
  (result.success ? "plugin action completed" : "plugin action failed"),
159
197
  ...(result.error ? { error: result.error } : {}),
160
- data: summarize_action_data(result.data),
198
+ ...(data === undefined ? {} : { data }),
161
199
  };
162
200
  } catch (error) {
163
201
  return {
@@ -170,3 +208,31 @@ export async function invokePluginCallTool(
170
208
  };
171
209
  }
172
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,32 @@ 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
+
33
+ /**
34
+ * plugin_call 产生的 assistant 文件摘要。
35
+ */
36
+ export interface PluginCallToolFileResult {
37
+ /** 文件在本轮 assistant 文件列表中的顺序。 */
38
+ index: number;
39
+ /** 文件 MIME 类型,例如 `image/png`。 */
40
+ media_type: string;
41
+ /** 原始文件名;若上游未提供则为空字符串。 */
42
+ filename: string;
43
+ /** 持久化到历史消息中的资源 URL,通常为 `resources://.downcity/resources/...`。 */
44
+ url: string;
45
+ /** 当前机器可直接打开的绝对文件路径。 */
46
+ path: string;
47
+ }
48
+
23
49
  /**
24
50
  * plugin_call 返回给模型的摘要结果。
25
51
  */
@@ -32,6 +58,8 @@ export interface PluginCallToolResult {
32
58
  action: string;
33
59
  /** 本次 action 产生并写入 assistant 消息的 file part 数量。 */
34
60
  assistant_file_count: number;
61
+ /** 本次 action 产生的文件摘要,包含可直接打开的绝对路径。 */
62
+ files?: PluginCallToolFileResult[];
35
63
  /** 人类可读消息。 */
36
64
  message: string;
37
65
  /** 错误信息。 */
@@ -39,3 +67,15 @@ export interface PluginCallToolResult {
39
67
  /** 返回给模型读取的短摘要数据。 */
40
68
  data?: JsonObject;
41
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
  });
@@ -14,6 +14,8 @@ import type {
14
14
  PluginActionResult,
15
15
  AgentPlugins,
16
16
  PluginAvailability,
17
+ PluginActionReadView,
18
+ PluginReadView,
17
19
  PluginView,
18
20
  } from "@/plugin/types/Plugin.js";
19
21
  import type { AgentContext } from "@/types/runtime/agent/AgentContext.js";
@@ -128,6 +130,60 @@ export class PluginRegistry implements AgentPlugins {
128
130
  .sort((a, b) => a.name.localeCompare(b.name));
129
131
  }
130
132
 
133
+ /**
134
+ * 读取 action metadata。
135
+ */
136
+ private readAction(
137
+ actionName: string,
138
+ action: NonNullable<Plugin["actions"]>[string],
139
+ ): PluginActionReadView {
140
+ return {
141
+ name: actionName,
142
+ description: String(action.description || "").trim(),
143
+ has_input_schema: Boolean(action.input_schema),
144
+ ...(action.input_schema?.json_schema
145
+ ? { input_schema: action.input_schema.json_schema }
146
+ : {}),
147
+ ...(action.examples ? { examples: action.examples } : {}),
148
+ allow_when_disabled: action.allowWhenDisabled === true,
149
+ has_command: Boolean(action.command),
150
+ has_api: Boolean(action.api),
151
+ };
152
+ }
153
+
154
+ /**
155
+ * 读取 plugin / action metadata。
156
+ */
157
+ read(params: {
158
+ plugin?: string;
159
+ action?: string;
160
+ }): PluginReadView | { plugins: PluginView[] } {
161
+ const pluginName = String(params.plugin || "").trim();
162
+ if (!pluginName) {
163
+ return { plugins: this.list() };
164
+ }
165
+ const plugin = this.get(pluginName);
166
+ if (!plugin) {
167
+ return {
168
+ name: pluginName,
169
+ title: pluginName,
170
+ description: "",
171
+ actions: [],
172
+ };
173
+ }
174
+ const actionName = String(params.action || "").trim();
175
+ const actions = Object.entries(plugin.actions || {})
176
+ .filter(([name]) => !actionName || name === actionName)
177
+ .sort(([left], [right]) => left.localeCompare(right))
178
+ .map(([name, action]) => this.readAction(name, action));
179
+ return {
180
+ name: plugin.name,
181
+ title: String(plugin.title || plugin.name || "").trim(),
182
+ description: String(plugin.description || "").trim(),
183
+ actions,
184
+ };
185
+ }
186
+
131
187
  /**
132
188
  * 检查 plugin 可用性。
133
189
  */
@@ -162,6 +218,28 @@ export class PluginRegistry implements AgentPlugins {
162
218
  };
163
219
  }
164
220
 
221
+ /**
222
+ * 按 action schema 校验 payload。
223
+ */
224
+ private parseActionPayload(params: {
225
+ pluginName: string;
226
+ actionName: string;
227
+ payload: JsonValue;
228
+ action: NonNullable<Plugin["actions"]>[string];
229
+ }): PluginActionResult<JsonValue> | { input: JsonValue } {
230
+ const schema = params.action.input_schema?.zod;
231
+ if (!schema) return { input: params.payload };
232
+ const parsed = schema.safeParse(params.payload);
233
+ if (parsed.success) {
234
+ return { input: parsed.data as JsonValue };
235
+ }
236
+ return {
237
+ success: false,
238
+ error: `Invalid payload for ${params.pluginName}.${params.actionName}: ${parsed.error.message}`,
239
+ message: `Invalid payload for ${params.pluginName}.${params.actionName}`,
240
+ };
241
+ }
242
+
165
243
  /**
166
244
  * 运行 plugin action。
167
245
  */
@@ -208,9 +286,19 @@ export class PluginRegistry implements AgentPlugins {
208
286
  }
209
287
 
210
288
  try {
289
+ const parsed_payload = this.parseActionPayload({
290
+ pluginName: plugin.name,
291
+ actionName,
292
+ payload: (params.payload ?? {}) as JsonValue,
293
+ action,
294
+ });
295
+ if (!("input" in parsed_payload)) {
296
+ return parsed_payload;
297
+ }
211
298
  return await action.execute({
212
299
  context,
213
- payload: (params.payload ?? {}) as JsonValue,
300
+ payload: parsed_payload.input,
301
+ input: parsed_payload.input,
214
302
  pluginName: plugin.name,
215
303
  actionName,
216
304
  });
@@ -24,9 +24,12 @@ export type {
24
24
  PluginActionApi,
25
25
  PluginActionCommand,
26
26
  PluginActionCommandInput,
27
+ PluginActionExample,
27
28
  PluginActionInvokeParams,
28
29
  PluginActionInvokePort,
29
30
  PluginActionInvokeResult,
31
+ PluginActionInputSchema,
32
+ PluginActionMetadata,
30
33
  PluginActionResult,
31
34
  PluginActions,
32
35
  } from "@/types/plugin/PluginAction.js";
@@ -38,8 +41,10 @@ export type {
38
41
  PluginHooks,
39
42
  PluginPipelineHook,
40
43
  AgentPlugins,
44
+ PluginActionReadView,
41
45
  PluginResolveHook,
42
46
  PluginResolves,
47
+ PluginReadView,
43
48
  PluginView,
44
49
  } from "@/types/plugin/PluginRuntime.js";
45
50
  export type {