First running version
This commit is contained in:
723
dev/build-detail.md
Normal file
723
dev/build-detail.md
Normal file
@@ -0,0 +1,723 @@
|
||||
# MCPletA2A Platform - 详细开发计划
|
||||
|
||||
> 基于 Platform.png 架构图、Flow.png 参考流程及 MCPlet-spec-v202603-03 编写。
|
||||
|
||||
---
|
||||
|
||||
## 1. 目标
|
||||
|
||||
| 产出 | 路径 |
|
||||
|------|------|
|
||||
| 平台实现 | `/Users/qingjie.du/HDD/my-prjs/MCPletA2A/platform_impl/` |
|
||||
| 参考实现 | `/Users/qingjie.du/HDD/my-prjs/MCPletA2A/reference_impl/` |
|
||||
|
||||
**平台实现**:MCPlet Agent Profile Host,含 Director Agent、A2A 协议层、MCPlet Pool 管理与权限执行、Passkey Web Page、Dashboard、可替换 LLM 适配器。
|
||||
|
||||
**参考实现**:「降低餐厅取消率」场景——情報収集・分析 Agent → 企画・Plan Agent(店长 Passkey 确认)→ 発信・発注・発令 Agent(发邮件),驱动对应 MCPlets。
|
||||
|
||||
---
|
||||
|
||||
## 2. 技术栈
|
||||
|
||||
| 层次 | 选型 | 理由 |
|
||||
|------|------|------|
|
||||
| 语言 | TypeScript 5.x, Node.js 20+ | 与 reference_impl_restaurant 一致 |
|
||||
| MCP | `@modelcontextprotocol/sdk` | 规范要求 |
|
||||
| HTTP | Node.js 原生 `http` | 与现有参考实现一致,轻量 |
|
||||
| 配置 | YAML + `js-yaml` | 可读性强,企业级友好 |
|
||||
| 定时任务 | `node-cron` | Director Agent schedule |
|
||||
| LLM 默认 | `@anthropic-ai/sdk` (claude-sonnet-4-6) | 通过适配器接口可替换任意 LLM |
|
||||
| 测试 | `jest` + `ts-jest` | 标准 TS 测试方案 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 完整目录结构
|
||||
|
||||
```
|
||||
/MCPletA2A/
|
||||
├── platform_impl/
|
||||
│ ├── src/
|
||||
│ │ ├── types/
|
||||
│ │ │ ├── a2a.ts # A2AAgentCard / Envelope / TaskRequest / TaskResponse
|
||||
│ │ │ ├── mcplet.ts # MCPletMeta / MCPletType / Visibility / PoolName
|
||||
│ │ │ └── config.ts # PlatformConfig / PoolPolicy / AgentConfig / DirectorAgentConfig
|
||||
│ │ ├── config/
|
||||
│ │ │ └── loader.ts # 加载并验证 platform.yaml → PlatformConfig
|
||||
│ │ ├── discovery/
|
||||
│ │ │ └── mcplet-discovery.ts # MCP tools/list + 热重载 + 合规校验
|
||||
│ │ ├── pools/
|
||||
│ │ │ └── pool-registry.ts # Pool 注册表 + canAgentAccessPool + getToolsForAgent
|
||||
│ │ ├── llm/
|
||||
│ │ │ ├── llm-adapter.ts # LLMAdapter 接口定义
|
||||
│ │ │ └── claude-adapter.ts # Claude 实现(@anthropic-ai/sdk)
|
||||
│ │ ├── agents/
|
||||
│ │ │ ├── base-agent.ts # BaseAgent 抽象类(含 dispatchMCPlet 权限检查)
|
||||
│ │ │ └── director-agent.ts # Director Agent(cron 触发,防并发)
|
||||
│ │ ├── a2a/
|
||||
│ │ │ ├── local-bus.ts # A2A local protocol(进程内注册+路由)
|
||||
│ │ │ └── external-endpoint.ts# A2A 外部 HTTP 端点(鉴权+Pool校验)
|
||||
│ │ ├── passkey/
|
||||
│ │ │ └── passkey-server.ts # Passkey Web Page(localhost 模式,动态端口)
|
||||
│ │ ├── dashboard/
|
||||
│ │ │ └── dashboard-server.ts # Dashboard HTTP 服务(MCPlet 列表+审计日志)
|
||||
│ │ ├── host/
|
||||
│ │ │ └── mcplet-host.ts # MCPlet Host 主类,整合全部模块
|
||||
│ │ └── index.ts # 程序入口
|
||||
│ ├── config/
|
||||
│ │ └── platform.yaml # 平台配置模板
|
||||
│ ├── public/
|
||||
│ │ ├── passkey/
|
||||
│ │ │ └── index.html # Passkey Web Page(minimal, strict CSP)
|
||||
│ │ └── dashboard/
|
||||
│ │ └── index.html # Dashboard 页面
|
||||
│ ├── package.json
|
||||
│ └── tsconfig.json
|
||||
│
|
||||
└── reference_impl/
|
||||
├── mcplets/
|
||||
│ ├── media-pool/
|
||||
│ │ ├── site-access/
|
||||
│ │ │ └── index.ts # read_site_stats (read, media-pool, model-visible)
|
||||
│ │ ├── email/
|
||||
│ │ │ └── index.ts # send_email (action, media-pool, passkey strict)
|
||||
│ │ └── sns/
|
||||
│ │ └── index.ts # post_sns (action, media-pool, passkey strict)
|
||||
│ ├── info-pool/
|
||||
│ │ ├── web-access/
|
||||
│ │ │ └── index.ts # fetch_web_content (read, info-pool, model-visible)
|
||||
│ │ └── api-access/
|
||||
│ │ └── index.ts # call_external_api (read, info-pool, model-visible)
|
||||
│ └── internal/
|
||||
│ ├── crm/
|
||||
│ │ └── index.ts # query_crm (read, no pool, model-visible)
|
||||
│ ├── erp/
|
||||
│ │ └── index.ts # query_erp (read, no pool, model-visible)
|
||||
│ └── hr/
|
||||
│ └── index.ts # query_hr (read, no pool, model-visible)
|
||||
├── agents/
|
||||
│ ├── info-gathering/
|
||||
│ │ └── index.ts # 情報収集・分析 Agent (accessiblePools: [info-pool])
|
||||
│ ├── planning/
|
||||
│ │ └── index.ts # 企画・Plan Agent (accessiblePools: [])
|
||||
│ └── dispatch/
|
||||
│ └── index.ts # 発信・発注・発令 Agent (accessiblePools: [media-pool])
|
||||
├── mock-services/
|
||||
│ ├── server.ts # Mock HTTP 服务器(端口 5100,挂载所有端点)
|
||||
│ └── data/
|
||||
│ ├── customers.json # CRM:含 cancel_tendency 字段的顾客数据(5条)
|
||||
│ ├── inventory.json # ERP:各商品库存
|
||||
│ ├── reservations.json # 明日の予約一覧
|
||||
│ └── weather.json # 天気予報(固定:明日は雨)
|
||||
├── config/
|
||||
│ └── reference.yaml # 参考实现配置(场景、schedule、agents、pools、mockServices)
|
||||
├── package.json
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 核心类型定义(`src/types/`)
|
||||
|
||||
### 4.1 `a2a.ts`
|
||||
|
||||
```typescript
|
||||
// Spec Section 18
|
||||
|
||||
export interface A2AAgentCard {
|
||||
agentId: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
requestedPools?: string[];
|
||||
inputSchema?: object;
|
||||
outputSchema?: object;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export interface A2AMessageEnvelope {
|
||||
messageId: string; // UUID v4
|
||||
contextId?: string; // UUID v4, stable across delegated workflow
|
||||
senderId: string;
|
||||
recipientId: string;
|
||||
timestamp?: string; // ISO 8601 UTC
|
||||
locale?: string; // BCP 47
|
||||
}
|
||||
|
||||
export interface A2ATaskRequest extends A2AMessageEnvelope {
|
||||
type: 'task_request';
|
||||
payload: {
|
||||
parameters: Record<string, unknown>;
|
||||
history?: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface A2ATaskResponse extends A2AMessageEnvelope {
|
||||
type: 'task_response';
|
||||
replyToMessageId: string;
|
||||
status: 'success' | 'error' | 'timeout' | 'cancelled' | 'partial';
|
||||
payload?: {
|
||||
result?: unknown;
|
||||
error?: { message: string; code?: string };
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 `mcplet.ts`
|
||||
|
||||
```typescript
|
||||
export type MCPletType = 'read' | 'prepare' | 'action';
|
||||
export type Visibility = 'model' | 'app';
|
||||
|
||||
export interface MCPletMeta {
|
||||
mcpletType: MCPletType;
|
||||
pool?: string;
|
||||
visibility: Visibility[];
|
||||
mcpletToolResultSchemaUri?: string;
|
||||
auth?: {
|
||||
required: 'passkey';
|
||||
enforcement: 'strict' | 'host-only';
|
||||
promptMessage?: string;
|
||||
};
|
||||
}
|
||||
|
||||
// MCPlet 工具结果信封(Spec Section 9.1)
|
||||
export interface MCPletToolResult<T = unknown> {
|
||||
result?: T;
|
||||
error?: { message: string; code: string };
|
||||
_meta: {
|
||||
timestamp: string;
|
||||
toolId: string;
|
||||
mcpletType: MCPletType;
|
||||
visibility: Visibility[];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 `config.ts`
|
||||
|
||||
```typescript
|
||||
export interface PoolPolicy {
|
||||
rateLimitPerMinute?: number;
|
||||
domainAllowlist?: string[];
|
||||
}
|
||||
|
||||
export interface AgentConfig {
|
||||
class: string; // Agent 类名,用于注册表查找
|
||||
accessiblePools: string[];
|
||||
a2aCard?: Partial<A2AAgentCard>;
|
||||
}
|
||||
|
||||
export interface DirectorAgentConfig {
|
||||
schedule: string; // cron 表达式
|
||||
promptTemplate: string;
|
||||
targetAgentId: string; // 接收指令的 Agent
|
||||
maxRetries: number;
|
||||
backoffMs: number;
|
||||
}
|
||||
|
||||
export interface PasskeyConfig {
|
||||
mode: 'localhost' | 'https';
|
||||
rpId: string;
|
||||
fido2ServerUrl?: string;
|
||||
}
|
||||
|
||||
export interface PlatformConfig {
|
||||
llm: { provider: string; model: string; apiKey?: string };
|
||||
pools: Record<string, PoolPolicy>;
|
||||
agents: Record<string, AgentConfig>;
|
||||
directorAgent?: DirectorAgentConfig;
|
||||
externalAgents?: Array<{ agentId: string; apiKey: string; accessiblePools: string[] }>;
|
||||
passkey?: PasskeyConfig;
|
||||
dashboard?: { port: number };
|
||||
a2aExternalEndpoint?: { port: number };
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 各模块详细规格
|
||||
|
||||
### 5.1 `discovery/mcplet-discovery.ts`
|
||||
|
||||
```typescript
|
||||
class MCPletDiscovery {
|
||||
constructor(private mcpClient: MCPClient, private poolRegistry: PoolRegistry) {}
|
||||
|
||||
async discover(): Promise<ToolDefinition[]>
|
||||
// 1. 调用 tools/list
|
||||
// 2. 过滤: 无 _meta.mcpletType → reject
|
||||
// 3. 过滤: action + visibility含model + 无auth → reject (记录 warn)
|
||||
// 4. 按 _meta.pool 注册到 poolRegistry
|
||||
// 5. 返回合规工具列表
|
||||
|
||||
subscribeToChanges(): void
|
||||
// 监听 notifications/tools/list_changed → 重新执行 discover()
|
||||
// 新增工具重新验证,删除工具从路由表移除
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 `pools/pool-registry.ts`
|
||||
|
||||
```typescript
|
||||
class PoolRegistry {
|
||||
constructor(private poolPolicies: Record<string, PoolPolicy>) {}
|
||||
|
||||
registerTool(tool: ToolDefinition, poolName?: string): void
|
||||
canAgentAccess(agentId: string, poolName: string | undefined, agentPools: string[]): boolean
|
||||
// - pool-less 工具:任何 Agent 可访问
|
||||
// - pool 工具:agentPools 必须包含该 pool
|
||||
|
||||
getToolsForAgent(agentId: string, agentPools: string[]): ToolDefinition[]
|
||||
// 返回该 Agent 被授权访问的全部工具(用于构建 LLM tool set)
|
||||
|
||||
checkRateLimit(poolName: string): boolean
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 `llm/llm-adapter.ts`
|
||||
|
||||
```typescript
|
||||
export interface Message {
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ToolDef {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: object;
|
||||
}
|
||||
|
||||
export interface LLMToolCall {
|
||||
toolName: string;
|
||||
arguments: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface LLMResponse {
|
||||
text?: string;
|
||||
toolCalls?: LLMToolCall[];
|
||||
}
|
||||
|
||||
export interface LLMAdapter {
|
||||
chat(messages: Message[], tools?: ToolDef[]): Promise<LLMResponse>;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 `agents/base-agent.ts`
|
||||
|
||||
```typescript
|
||||
abstract class BaseAgent {
|
||||
constructor(
|
||||
public readonly agentId: string,
|
||||
public readonly accessiblePools: string[],
|
||||
protected poolRegistry: PoolRegistry,
|
||||
protected mcpClient: MCPClient,
|
||||
protected llm: LLMAdapter,
|
||||
) {}
|
||||
|
||||
abstract handle(task: A2ATaskRequest): Promise<A2ATaskResponse>;
|
||||
|
||||
protected getAuthorizedTools(): ToolDef[]
|
||||
// 调用 poolRegistry.getToolsForAgent,仅返回 model-visible 工具
|
||||
|
||||
protected async invokeMCPlet(toolName: string, args: object): Promise<MCPletToolResult>
|
||||
// 1. 检查工具是否在授权 Pool 内(否则抛出 403 等价错误)
|
||||
// 2. 检查 mcpletType:action 工具走 Passkey 拦截流程
|
||||
// 3. 发送 MCP tools/call
|
||||
// 4. 返回结果
|
||||
|
||||
protected buildSuccessResponse(task: A2ATaskRequest, result: unknown): A2ATaskResponse
|
||||
protected buildErrorResponse(task: A2ATaskRequest, message: string, code?: string): A2ATaskResponse
|
||||
}
|
||||
```
|
||||
|
||||
### 5.5 `agents/director-agent.ts`
|
||||
|
||||
```typescript
|
||||
class DirectorAgent {
|
||||
private running = false;
|
||||
private cronJob: cron.ScheduledTask;
|
||||
|
||||
constructor(
|
||||
private config: DirectorAgentConfig,
|
||||
private llm: LLMAdapter,
|
||||
private localBus: A2ALocalBus,
|
||||
) {}
|
||||
|
||||
start(): void
|
||||
// node-cron.schedule(config.schedule, this.run.bind(this))
|
||||
|
||||
private async run(): Promise<void>
|
||||
// 1. if (this.running) { log "skipping, previous cycle still active"; return; }
|
||||
// 2. this.running = true
|
||||
// 3. try:
|
||||
// a. LLM(config.promptTemplate) → instruction
|
||||
// b. 解析失败 → log + return(不 dispatch)
|
||||
// c. localBus.sendTask(config.targetAgentId, instruction)
|
||||
// d. 重试逻辑:最多 config.maxRetries 次,间隔 config.backoffMs
|
||||
// 4. finally: this.running = false
|
||||
|
||||
stop(): void
|
||||
}
|
||||
```
|
||||
|
||||
### 5.6 `a2a/local-bus.ts`
|
||||
|
||||
```typescript
|
||||
class A2ALocalBus {
|
||||
private agents = new Map<string, BaseAgent>();
|
||||
|
||||
register(agent: BaseAgent): void
|
||||
|
||||
async sendTask(request: A2ATaskRequest): Promise<A2ATaskResponse>
|
||||
// 1. 查找 recipientId 对应 Agent(未找到 → error response)
|
||||
// 2. 调用 agent.handle(request)
|
||||
// 3. local bus 消息 MUST NOT 路由到进程外(纯内存调用)
|
||||
}
|
||||
```
|
||||
|
||||
### 5.7 `a2a/external-endpoint.ts`
|
||||
|
||||
```typescript
|
||||
// HTTP POST /a2a/task
|
||||
// Headers: Authorization: Bearer <apiKey>
|
||||
|
||||
class A2AExternalEndpoint {
|
||||
constructor(
|
||||
private config: PlatformConfig,
|
||||
private localBus: A2ALocalBus,
|
||||
private poolRegistry: PoolRegistry,
|
||||
) {}
|
||||
|
||||
start(port: number): void
|
||||
|
||||
private async handleTask(req, res): Promise<void>
|
||||
// 1. 验证 Bearer token → 找到对应外部 Agent 配置(否则 401)
|
||||
// 2. 解析 A2ATaskRequest(Spec Section 18 schema)
|
||||
// 3. 检查 recipientId 对应 Agent 的工具 ∩ 外部 Agent 授权 Pool(否则 403)
|
||||
// 4. 转发到 localBus.sendTask
|
||||
// 5. 返回 A2ATaskResponse JSON
|
||||
}
|
||||
```
|
||||
|
||||
### 5.8 `passkey/passkey-server.ts`
|
||||
|
||||
```typescript
|
||||
class PasskeyServer {
|
||||
private port: number;
|
||||
|
||||
async startCeremony(promptMessage: string): Promise<PasskeyAssertion>
|
||||
// 1. 动态绑定 loopback 端口
|
||||
// 2. 在系统浏览器打开 http://localhost:{port}/passkey?msg=...
|
||||
// 3. 等待回调(POST /passkey/callback)或超时(< 60s)
|
||||
// 4. 超时/取消 → 抛出错误(Host 返回 MCP Error)
|
||||
// 5. 回调成功 → 返回 assertion,关闭页面和端口
|
||||
|
||||
stop(): void
|
||||
}
|
||||
|
||||
// public/passkey/index.html:
|
||||
// - 读取 ?msg query param
|
||||
// - 调用 navigator.credentials.get()(WebAuthn)
|
||||
// - POST assertion 到 /passkey/callback
|
||||
// - 严格 CSP(no external scripts)
|
||||
```
|
||||
|
||||
### 5.9 `host/mcplet-host.ts`
|
||||
|
||||
```typescript
|
||||
class MCPletHost {
|
||||
async start(configPath: string): Promise<void>
|
||||
// 按序初始化:
|
||||
// 1. loadConfig(configPath) → PlatformConfig
|
||||
// 2. new PoolRegistry(config.pools)
|
||||
// 3. new MCPletDiscovery(mcpClient, poolRegistry) → discover()
|
||||
// 4. new LLMAdapter(config.llm) // ClaudeAdapter 或其他
|
||||
// 5. new A2ALocalBus()
|
||||
// 6. 注册参考实现中的 Agent(Info/Plan/Dispatch)
|
||||
// 7. new DirectorAgent(config.directorAgent, llm, localBus) → start()
|
||||
// 8. if (config.passkey) → new PasskeyServer(config.passkey)
|
||||
// 9. if (config.dashboard) → new DashboardServer(...) → start()
|
||||
// 10. if (config.a2aExternalEndpoint) → new A2AExternalEndpoint(...) → start()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 参考实现 MCPlet 规格
|
||||
|
||||
所有 MCPlet 遵循以下模式(以 `email/index.ts` 为例):
|
||||
|
||||
```typescript
|
||||
import { registerModelTool, registerAppTool } from '../../../platform_impl/src/mcplet-lib';
|
||||
|
||||
// send_email: action, media-pool, app-only(需 Passkey 授权后由 dispatch agent 调用)
|
||||
registerAppTool(server, {
|
||||
name: 'send_email',
|
||||
title: 'メール送信',
|
||||
description: 'Send email to specified recipients',
|
||||
inputSchema: SendEmailSchema,
|
||||
mcpletType: 'action',
|
||||
pool: 'media-pool',
|
||||
visibility: ['app'],
|
||||
auth: {
|
||||
required: 'passkey',
|
||||
enforcement: 'strict',
|
||||
promptMessage: 'メール送信を承認してください'
|
||||
},
|
||||
handler: sendEmailHandler,
|
||||
});
|
||||
```
|
||||
|
||||
### Mock Service 端点(port 5100)
|
||||
|
||||
| 端点 | 方法 | 说明 | 对应 MCPlet |
|
||||
| ---- | ---- | ---- | ---------- |
|
||||
| `/crm/customers?filter=rain_cancel_tendency` | GET | 雨天取消倾向高的顾客列表(5条固定数据) | query_crm |
|
||||
| `/crm/reservations?date=YYYY-MM-DD` | GET | 指定日期预约列表 | query_crm |
|
||||
| `/erp/inventory?item=dessert` | GET | 甜点库存 | query_erp |
|
||||
| `/weather/forecast?date=YYYY-MM-DD` | GET | 天气预报(固定返回「明日は雨」) | fetch_web_content |
|
||||
| `/site/stats` | GET | EPARK 站点访问数据 | read_site_stats |
|
||||
| `/email/send` | POST | Mock 发信(记录日志,不真实发送) | send_email |
|
||||
| `/sns/post` | POST | Mock SNS(记录日志) | post_sns |
|
||||
|
||||
MCPlet handler 通过 `config.mockServiceUrl`(`http://localhost:5100`)调用以上端点,替换真实服务只需修改配置,handler 代码不变。
|
||||
|
||||
---
|
||||
|
||||
## 7. 参考实现 MCPlet 规格
|
||||
|
||||
| MCPlet | Tool 名 | mcpletType | pool | visibility | auth |
|
||||
|--------|---------|-----------|------|------------|------|
|
||||
| サイトアクセス | `read_site_stats` | read | media-pool | [model] | — |
|
||||
| Email | `send_email` | action | media-pool | [app] | passkey strict |
|
||||
| SNS | `post_sns` | action | media-pool | [app] | passkey strict |
|
||||
| 外部Web | `fetch_web_content` | read | info-pool | [model] | — |
|
||||
| 外部API | `call_external_api` | read | info-pool | [model] | — |
|
||||
| CRM | `query_crm` | read | (none) | [model] | — |
|
||||
| ERP | `query_erp` | read | (none) | [model] | — |
|
||||
| HR | `query_hr` | read | (none) | [model] | — |
|
||||
|
||||
---
|
||||
|
||||
## 7. 参考实现 Agent 规格
|
||||
|
||||
### 7.1 情報収集・分析 Agent
|
||||
|
||||
```typescript
|
||||
class InfoGatheringAgent extends BaseAgent {
|
||||
// agentId: 'info-gathering-agent'
|
||||
// accessiblePools: ['info-pool']
|
||||
// 可用工具: fetch_web_content, call_external_api, query_crm, query_erp, query_hr (pool-less)
|
||||
|
||||
async handle(task: A2ATaskRequest): Promise<A2ATaskResponse> {
|
||||
// 1. 将 task.payload.parameters 拼入 system prompt
|
||||
// 2. LLM 根据任务决定调用哪些 read MCPlets
|
||||
// 3. 执行工具调用循环(tool_use → invokeMCPlet → 返回结果给 LLM)
|
||||
// 4. LLM 输出分析总结
|
||||
// 5. 返回 success response(result: { analysis, rawData })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 企画・Plan Agent
|
||||
|
||||
```typescript
|
||||
class PlanningAgent extends BaseAgent {
|
||||
// agentId: 'planning-agent'
|
||||
// accessiblePools: [] (只访问 pool-less MCPlets)
|
||||
// 可用工具: query_crm, query_erp, query_hr
|
||||
|
||||
async handle(task: A2ATaskRequest): Promise<A2ATaskResponse> {
|
||||
// 1. 接收来自 info-gathering 的分析结果
|
||||
// 2. 查询必要的 CRM/ERP 数据(pool-less MCPlets)
|
||||
// 3. LLM 生成 Plan(含具体施策、对象顾客列表、邮件文案)
|
||||
// 4. 通过 Passkey Web Page 请求店长审批(auth.enforcement: host-only 模式)
|
||||
// 5. 审批通过 → 返回 success(result: { plan, approvedAt })
|
||||
// 6. 审批拒绝/超时 → 返回 cancelled
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 発信・発注・発令 Agent
|
||||
|
||||
```typescript
|
||||
class DispatchAgent extends BaseAgent {
|
||||
// agentId: 'dispatch-agent'
|
||||
// accessiblePools: ['media-pool']
|
||||
// 可用工具: read_site_stats, send_email, post_sns (需 Passkey)
|
||||
|
||||
async handle(task: A2ATaskRequest): Promise<A2ATaskResponse> {
|
||||
// 1. 接收企划方案(plan + 顾客列表 + 邮件文案)
|
||||
// 2. 对 send_email 工具调用:
|
||||
// a. BaseAgent.invokeMCPlet 拦截 action 工具
|
||||
// b. 调用 PasskeyServer.startCeremony()
|
||||
// c. 拿到 assertion → 注入 params._meta.mcplet_auth
|
||||
// d. 真正执行 MCP tools/call
|
||||
// 3. 记录发送结果到审计日志
|
||||
// 4. 返回 success(result: { sent: n, failed: m })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 参考实现配置 `reference.yaml`
|
||||
|
||||
```yaml
|
||||
llm:
|
||||
provider: claude
|
||||
model: claude-sonnet-4-6
|
||||
apiKey: ${ANTHROPIC_API_KEY}
|
||||
|
||||
pools:
|
||||
media-pool:
|
||||
rateLimitPerMinute: 60
|
||||
info-pool:
|
||||
rateLimitPerMinute: 120
|
||||
|
||||
agents:
|
||||
info-gathering-agent:
|
||||
class: InfoGatheringAgent
|
||||
accessiblePools: [info-pool]
|
||||
planning-agent:
|
||||
class: PlanningAgent
|
||||
accessiblePools: []
|
||||
dispatch-agent:
|
||||
class: DispatchAgent
|
||||
accessiblePools: [media-pool]
|
||||
|
||||
directorAgent:
|
||||
schedule: "0 7 * * *"
|
||||
targetAgentId: info-gathering-agent
|
||||
promptTemplate: |
|
||||
今日の天気予報と在庫情報、キャンセル傾向の高い予約客情報を収集・分析し、
|
||||
キャンセル率を下げる施策を立案してください。
|
||||
maxRetries: 3
|
||||
backoffMs: 5000
|
||||
|
||||
passkey:
|
||||
mode: localhost
|
||||
rpId: localhost
|
||||
|
||||
dashboard:
|
||||
port: 4000
|
||||
|
||||
a2aExternalEndpoint:
|
||||
port: 4001
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 完整流程(Flow.png 场景)
|
||||
|
||||
```
|
||||
[cron 07:00]
|
||||
│
|
||||
▼
|
||||
DirectorAgent.run()
|
||||
│ LLM(promptTemplate) → "キャンセル率低減タスク開始"
|
||||
│
|
||||
▼
|
||||
A2ALocalBus.sendTask("info-gathering-agent", task)
|
||||
│
|
||||
▼
|
||||
InfoGatheringAgent.handle(task)
|
||||
├─ invokeMCPlet("fetch_web_content", {url: "天気予報"}) ← info-pool
|
||||
├─ invokeMCPlet("call_external_api", {source: "在庫API"}) ← info-pool
|
||||
└─ invokeMCPlet("query_crm", {filter: "雨天キャンセル傾向"}) ← pool-less
|
||||
│ → LLM分析 → { analysis: "明日は雨、在庫あり、高キャンセル客10名" }
|
||||
│
|
||||
▼
|
||||
A2ALocalBus.sendTask("planning-agent", { analysis })
|
||||
│
|
||||
▼
|
||||
PlanningAgent.handle(task)
|
||||
├─ invokeMCPlet("query_erp", {item: "デザート"}) ← pool-less
|
||||
└─ LLM生成企划: "無料デザートキャンペーン、対象: 10名"
|
||||
│ PasskeyServer.startCeremony("キャンペーン計画を承認してください")
|
||||
│ [店长在浏览器中进行 WebAuthn 认证]
|
||||
│ → { plan, approvedAt }
|
||||
│
|
||||
▼
|
||||
A2ALocalBus.sendTask("dispatch-agent", { plan })
|
||||
│
|
||||
▼
|
||||
DispatchAgent.handle(task)
|
||||
└─ invokeMCPlet("send_email", { to: [...10名...], body: "..." })
|
||||
├─ 拦截 action 工具
|
||||
├─ PasskeyServer.startCeremony("メール送信を承認してください")
|
||||
├─ 注入 params._meta.mcplet_auth
|
||||
└─ MCP tools/call → Email MCPlet 后端验证 Passkey → 发送邮件
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 开发阶段与顺序
|
||||
|
||||
```
|
||||
Phase 1: 项目骨架
|
||||
platform_impl: package.json, tsconfig.json, 目录结构
|
||||
reference_impl: package.json, tsconfig.json, 目录结构
|
||||
|
||||
Phase 2: 类型层(src/types/)
|
||||
a2a.ts, mcplet.ts, config.ts
|
||||
|
||||
Phase 3: 配置加载(src/config/loader.ts)
|
||||
+ platform.yaml 示例
|
||||
|
||||
Phase 4: Pool 管理(src/pools/pool-registry.ts)
|
||||
+ MCPlet 发现(src/discovery/mcplet-discovery.ts)
|
||||
|
||||
Phase 5: LLM 适配器(src/llm/)
|
||||
llm-adapter.ts(接口) + claude-adapter.ts(实现)
|
||||
|
||||
Phase 6: Agent 框架(src/agents/)
|
||||
base-agent.ts(含 invokeMCPlet 权限检查 + action 拦截骨架)
|
||||
director-agent.ts(cron + 防并发 + 重试)
|
||||
|
||||
Phase 7: A2A 协议(src/a2a/)
|
||||
local-bus.ts(进程内路由)
|
||||
external-endpoint.ts(HTTP + 鉴权 + Pool校验)
|
||||
|
||||
Phase 8: Host 主入口(src/host/mcplet-host.ts + src/index.ts)
|
||||
整合 Phase 2-7,平台实现可独立运行
|
||||
|
||||
Phase 9: 参考实现 MCPlets
|
||||
info-pool: fetch_web_content, call_external_api
|
||||
internal: query_crm, query_erp, query_hr
|
||||
media-pool: read_site_stats, send_email, post_sns
|
||||
|
||||
Phase 10: 参考实现 Agents
|
||||
InfoGatheringAgent, PlanningAgent, DispatchAgent
|
||||
+ reference.yaml
|
||||
|
||||
Phase 11: Passkey Web Page(src/passkey/ + public/passkey/)
|
||||
localhost 模式,动态端口,WebAuthn ceremony
|
||||
|
||||
Phase 12: Dashboard(src/dashboard/ + public/dashboard/)
|
||||
MCPlet 列表 + action 审计日志展示
|
||||
|
||||
Phase 13: 集成测试
|
||||
完整流程 E2E:Director → 情報収集 → 企画 → 発信
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 验收标准(Checklist)
|
||||
|
||||
### 平台实现
|
||||
- [ ] MCPlet 发现:无 `_meta.mcpletType` 工具被拒绝;`action + model-visible + 无auth` 被拒绝
|
||||
- [ ] Pool 权限:Agent 调用授权范围外工具时得到明确错误
|
||||
- [ ] Director Agent:cron 按时触发;LLM 解析失败时跳过不 panic;同一 Director 不并发执行
|
||||
- [ ] A2A Local Bus:进程内消息路由正确
|
||||
- [ ] A2A 外部端点:未授权 → 401;Pool 范围外 → 403;合法请求转发 → 正确响应
|
||||
- [ ] Passkey Web Page:localhost 动态端口;assertion 回调后关闭;超时返回 MCP Error
|
||||
- [ ] Dashboard:可查看 MCPlet 列表 + 最近 action 审计日志
|
||||
|
||||
### 参考实现
|
||||
- [ ] 各 MCPlet 正确注册(mcpletType / pool / visibility / auth)
|
||||
- [ ] InfoGatheringAgent 只能访问 info-pool + pool-less MCPlets
|
||||
- [ ] PlanningAgent 只能访问 pool-less MCPlets
|
||||
- [ ] DispatchAgent 只能访问 media-pool MCPlets
|
||||
- [ ] 完整场景流程一次性跑通:Director → 情報収集 → 企画 → Passkey 审批 → 発信
|
||||
- [ ] send_email 调用经过 Passkey 拦截 + assertion 注入 + 后端验证
|
||||
Reference in New Issue
Block a user