Quellcode durchsuchen

feat:新增AI智能助手,可用,但功能还未完善

Jerry Liang vor 3 Monaten
Ursprung
Commit
62e05f734d
39 geänderte Dateien mit 7551 neuen und 104 gelöschten Zeilen
  1. 823 0
      docs/ACP_Protocol_Specification.md
  2. 1226 0
      docs/ai-assistant-plan.md
  3. 594 0
      docs/kiro-cli-agent-implementation.md
  4. 1269 0
      docs/kiro-cli-configuration-guide.md
  5. 3 0
      package.json
  6. 63 32
      pnpm-lock.yaml
  7. 31 0
      service/.kiro/settings/mcp.json
  8. 25 0
      service/.kiro/steering/project.md
  9. 10 46
      service/cmd/deploy.sh
  10. 18 0
      service/cmd/deploy_mcp.sh
  11. 54 0
      service/cmd/deploy_toolbox.sh
  12. 58 0
      service/cmd/mcp/main.go
  13. 39 0
      service/cmd/mcp/tools/alarm.go
  14. 80 0
      service/cmd/mcp/tools/plane.go
  15. 49 0
      service/cmd/mcp/tools/powerFlow.go
  16. 39 0
      service/cmd/mcp/tools/shutdown.go
  17. 76 0
      service/cmd/mcp/tools/simCard.go
  18. 70 0
      service/cmd/mcp/tools/siteInfo.go
  19. 9 5
      service/cmd/only_build.sh
  20. 8 0
      service/cmd/only_build_mcp.sh
  21. 8 0
      service/cmd/only_build_toolbox.sh
  22. 68 0
      service/cmd/test_acp/main.go
  23. 238 0
      service/controllers/aiChatControllers.go
  24. 9 6
      service/go.mod
  25. 15 11
      service/go.sum
  26. 4 0
      service/main.go
  27. 39 0
      service/routes/aiChatRoutes.go
  28. 1 0
      service/routes/routes.go
  29. 408 0
      service/services/aiChatServices.go
  30. 985 0
      service/utils/acp/acp.go
  31. 174 0
      service/utils/acp/filestore.go
  32. 221 0
      src/layout/header/Actions/AiChat/ChatPanel.vue
  33. 239 0
      src/layout/header/Actions/AiChat/MessageItem.vue
  34. 64 0
      src/layout/header/Actions/AiChat/chatDB.ts
  35. 35 0
      src/layout/header/Actions/AiChat/index.vue
  36. 60 0
      src/layout/header/Actions/AiChat/interface.ts
  37. 433 0
      src/layout/header/Actions/AiChat/useAiChat.ts
  38. 4 2
      src/layout/header/Actions/index.vue
  39. 2 2
      src/views/site-info-manage/index.vue

+ 823 - 0
docs/ACP_Protocol_Specification.md

@@ -0,0 +1,823 @@
+# ACP (Agent Client Protocol) 协议详细描述文档
+
+> 基于 `vscode-acp` 项目源码分析提取,SDK 版本:`@agentclientprotocol/sdk ^0.21.1`
+
+---
+
+## 1. 协议概述
+
+ACP (Agent Client Protocol) 是一种用于 **AI 编程代理(Agent)与客户端(Client/IDE)之间通信** 的标准化协议。它定义了客户端如何启动、管理和与 AI 代理交互的完整生命周期,类似于 LSP (Language Server Protocol) 的设计理念,但专门面向 AI 代理场景。
+
+### 1.1 核心设计理念
+
+- **进程隔离**:Agent 以独立子进程方式运行,通过 stdin/stdout 与客户端通信
+- **协议版本化**:每次连接通过 `initialize` 握手交换 `protocolVersion`
+- **能力协商**:客户端和代理通过初始化阶段声明各自支持的能力
+- **会话管理**:支持创建、加载、恢复、列举会话
+- **双向通信**:代理可以向客户端发起请求(如文件读写、终端操作、权限请求)
+
+### 1.2 传输层
+
+| 属性 | 描述 |
+|------|------|
+| 传输方式 | 子进程的 stdin(写入)/ stdout(读取) |
+| 序列化格式 | **ND-JSON**(Newline Delimited JSON) |
+| 流适配 | Node.js Stream → Web Streams API (`ReadableStream` / `WritableStream`) |
+| SDK 入口 | `ndJsonStream(writable, readable)` 创建双向流 |
+
+---
+
+## 2. 连接生命周期
+
+### 2.1 Agent 进程启动
+
+```
+Client                                          Agent Process
+  |                                                  |
+  |--- spawn(command, args, {stdio: pipe}) --------->|
+  |                                                  |
+  |<========= stdin/stdout pipe established ========>|
+```
+
+Agent 配置结构:
+```typescript
+interface AgentConfigEntry {
+  command: string;       // 启动命令,如 "npx"
+  args?: string[];       // 命令行参数
+  env?: Record<string, string>;  // 环境变量
+  displayName?: string;  // 显示名称
+}
+```
+
+### 2.2 初始化握手(Initialize)
+
+连接建立后,客户端必须发送 `initialize` 请求:
+
+**请求方向**:Client → Agent
+
+```typescript
+// 客户端发送的初始化参数
+{
+  protocolVersion: PROTOCOL_VERSION,   // 协议版本号
+  clientInfo: {
+    name: 'vscode-acp-client',         // 客户端名称
+    version: '0.2.0',                  // 客户端版本
+  },
+  clientCapabilities: {
+    fs: {
+      readTextFile: true,              // 支持文件读取
+      writeTextFile: true,             // 支持文件写入
+    },
+    terminal: true,                    // 支持终端操作
+  },
+}
+```
+
+**响应结构** (`InitializeResponse`):
+
+```typescript
+interface InitializeResponse {
+  agentInfo?: {
+    name: string;         // Agent 名称
+    title?: string;       // Agent 显示标题
+    version?: string;     // Agent 版本
+  };
+  agentCapabilities?: AgentCapabilities;  // Agent 能力声明
+  authMethods?: AuthMethod[];              // 支持的认证方式
+}
+
+interface AgentCapabilities {
+  sessionCapabilities?: {
+    list?: boolean;    // 是否支持 session/list
+    resume?: boolean;  // 是否支持 session/resume
+  };
+  loadSession?: boolean;  // 是否支持 session/load
+}
+
+interface AuthMethod {
+  id: string;           // 认证方式 ID
+  name: string;         // 认证方式名称
+  description?: string; // 认证方式描述
+}
+```
+
+### 2.3 认证流程(Authentication)
+
+当 Agent 需要认证时(错误码 `-32000`),客户端启动认证流程:
+
+```
+Client                                Agent
+  |                                     |
+  |-- newSession() ------------------->|
+  |<-- Error: auth required (-32000) --|
+  |                                     |
+  |-- authenticate({methodId}) ------->|
+  |<-- success ------------------------|
+  |                                     |
+  |-- newSession() (retry) ----------->|
+  |<-- NewSessionResponse -------------|
+```
+
+认证请求:
+```typescript
+{ methodId: string }  // 用户选择的认证方式 ID
+```
+
+---
+
+## 3. 会话管理(Session Management)
+
+### 3.1 创建新会话(session/new)
+
+**请求方向**:Client → Agent
+
+```typescript
+// 请求参数
+interface NewSessionRequest {
+  cwd: string;              // 工作目录路径
+  mcpServers: any[];        // MCP 服务器列表(当前为空数组)
+}
+
+// 响应
+interface NewSessionResponse {
+  sessionId: string;                       // 会话唯一标识
+  modes?: SessionModeState;                // 可用模式(legacy)
+  models?: SessionModelState;              // 可用模型(experimental)
+  configOptions?: SessionConfigOption[];   // 通用配置选项(新版,优先级高于 modes/models)
+}
+```
+
+### 3.2 列举会话(session/list)
+
+**前提**:Agent 在初始化时声明 `sessionCapabilities.list = true`
+
+```typescript
+// 请求参数
+interface ListSessionsRequest {
+  cwd?: string;      // 按工作目录过滤
+  cursor?: string;   // 分页游标
+}
+
+// 响应
+interface ListSessionsResponse {
+  sessions: SessionInfo[];   // 会话列表
+  nextCursor?: string;       // 下一页游标(分页)
+}
+
+interface SessionInfo {
+  sessionId: string;
+  title?: string;
+  createdAt?: string;
+  updatedAt?: string;
+}
+```
+
+### 3.3 加载会话(session/load)
+
+**前提**:Agent 声明 `loadSession = true`
+
+重量级操作,完整重放会话历史:
+
+```typescript
+// 请求参数
+interface LoadSessionRequest {
+  sessionId: string;
+  cwd: string;
+  mcpServers: any[];
+}
+
+// 响应(同 NewSessionResponse)
+interface LoadSessionResponse {
+  modes?: SessionModeState;
+  models?: SessionModelState;
+  configOptions?: SessionConfigOption[];
+}
+```
+
+加载过程中 Agent 会通过 `session/update` 通知回放所有历史消息。
+
+### 3.4 恢复会话(session/resume)
+
+**前提**:Agent 声明 `sessionCapabilities.resume = true`
+
+轻量级操作,不回放历史:
+
+```typescript
+// 请求参数
+interface ResumeSessionRequest {
+  sessionId: string;
+  cwd: string;
+  mcpServers: any[];
+}
+
+// 响应
+interface ResumeSessionResponse {
+  modes?: SessionModeState;
+  models?: SessionModelState;
+  configOptions?: SessionConfigOption[];
+}
+```
+
+---
+
+## 4. 对话交互(Prompt/Response)
+
+### 4.1 发送提示(prompt)
+
+**请求方向**:Client → Agent
+
+```typescript
+// 请求参数
+interface PromptRequest {
+  sessionId: string;
+  prompt: ContentBlock[];    // 内容块数组
+}
+
+interface ContentBlock {
+  type: 'text';              // 内容类型
+  text: string;              // 文本内容
+}
+
+// 响应
+interface PromptResponse {
+  stopReason: string;   // 停止原因:"end_turn" | "error" | "cancelled" 等
+  usage?: {             // Token 用量统计(可选)
+    // 具体字段取决于 Agent 实现
+  };
+}
+```
+
+### 4.2 取消当前轮次(cancel)
+
+**请求方向**:Client → Agent
+
+```typescript
+interface CancelRequest {
+  sessionId: string;
+}
+```
+
+---
+
+## 5. 会话实时通知(Session Notifications)
+
+Agent 通过 `session/update` 向客户端推送实时状态更新。这是 ACP 协议中最丰富的通知机制。
+
+**通知方向**:Agent → Client
+
+```typescript
+interface SessionNotification {
+  sessionId: string;
+  update: SessionUpdate;  // 更新内容(以下各种类型之一)
+}
+```
+
+### 5.1 消息流(Streaming)
+
+#### `agent_message_chunk` — Agent 文本流式输出
+
+```typescript
+{
+  sessionUpdate: 'agent_message_chunk',
+  content: {
+    type: 'text',
+    text: string     // 增量文本片段
+  }
+}
+```
+
+#### `user_message_chunk` — 用户消息回放(仅 session/load 使用)
+
+```typescript
+{
+  sessionUpdate: 'user_message_chunk',
+  content: {
+    type: 'text',
+    text: string
+  }
+}
+```
+
+#### `agent_thought_chunk` — Agent 思维过程流式输出
+
+```typescript
+{
+  sessionUpdate: 'agent_thought_chunk',
+  content: {
+    type: 'text',
+    text: string     // 思考过程增量文本
+  }
+}
+```
+
+### 5.2 工具调用(Tool Calls)
+
+#### `tool_call` — 新工具调用
+
+```typescript
+{
+  sessionUpdate: 'tool_call',
+  toolCallId: string,        // 工具调用唯一ID
+  title: string,             // 工具调用标题/描述
+  status: 'pending' | 'running' | 'completed' | 'failed'
+}
+```
+
+#### `tool_call_update` — 工具调用状态更新
+
+```typescript
+{
+  sessionUpdate: 'tool_call_update',
+  toolCallId: string,        // 对应的工具调用ID
+  status: 'pending' | 'running' | 'completed' | 'failed',
+  title?: string             // 可选的标题更新
+}
+```
+
+### 5.3 计划(Plan)
+
+#### `plan` — Agent 执行计划
+
+```typescript
+{
+  sessionUpdate: 'plan',
+  entries: PlanEntry[]
+}
+
+interface PlanEntry {
+  title?: string;
+  description?: string;
+  content?: string;
+  status: 'pending' | 'in_progress' | 'completed'
+}
+```
+
+### 5.4 会话配置更新
+
+#### `current_mode_update` — 模式变更通知
+
+```typescript
+{
+  sessionUpdate: 'current_mode_update',
+  currentModeId: string     // 新的当前模式 ID
+}
+```
+
+#### `config_option_update` — 配置选项全量更新
+
+```typescript
+{
+  sessionUpdate: 'config_option_update',
+  configOptions: SessionConfigOption[]   // 完整的配置选项数组
+}
+```
+
+#### `available_commands_update` — 可用命令更新
+
+```typescript
+{
+  sessionUpdate: 'available_commands_update',
+  availableCommands: AvailableCommand[]
+}
+
+interface AvailableCommand {
+  name: string;              // 命令名(用于 /slash 命令)
+  description: string;       // 命令描述
+  input?: {
+    hint?: string;           // 输入提示文字
+  };
+}
+```
+
+#### `session_info_update` — 会话元信息更新
+
+```typescript
+{
+  sessionUpdate: 'session_info_update',
+  title?: string | null,     // 会话标题(null 表示清除)
+  updatedAt?: string | null  // 最后更新时间(ISO 格式)
+}
+```
+
+---
+
+## 6. Agent → Client 请求(反向调用)
+
+Agent 可以向客户端发起以下请求,由客户端实现 `Client` 接口来处理。
+
+### 6.1 文件系统操作
+
+#### `readTextFile` — 读取文本文件
+
+```typescript
+// 请求
+interface ReadTextFileRequest {
+  path: string;        // 文件绝对路径
+  line?: number;       // 起始行号(1-based)
+  limit?: number;      // 读取行数限制
+}
+
+// 响应
+interface ReadTextFileResponse {
+  content: string;     // 文件内容
+}
+```
+
+**实现特性**:
+- 优先读取编辑器中未保存的内容(dirty buffer)
+- 支持行范围读取(line + limit)
+
+#### `writeTextFile` — 写入文本文件
+
+```typescript
+// 请求
+interface WriteTextFileRequest {
+  path: string;        // 文件绝对路径
+  content: string;     // 要写入的内容
+}
+
+// 响应
+interface WriteTextFileResponse {}  // 空响应表示成功
+```
+
+**实现特性**:
+- 自动创建父目录
+- 写入后在编辑器中打开文件
+
+### 6.2 终端操作
+
+#### `createTerminal` — 创建终端
+
+```typescript
+// 请求
+interface CreateTerminalRequest {
+  command: string;             // 要执行的命令
+  args?: string[];             // 命令参数
+  cwd?: string;               // 工作目录
+  env?: EnvVar[];              // 环境变量列表
+  outputByteLimit?: number;    // 输出缓冲区大小限制(默认 1MB)
+}
+
+interface EnvVar {
+  name: string;
+  value: string;
+}
+
+// 响应
+interface CreateTerminalResponse {
+  terminalId: string;          // 终端唯一标识
+}
+```
+
+#### `terminalOutput` — 获取终端输出
+
+```typescript
+// 请求
+interface TerminalOutputRequest {
+  terminalId: string;
+}
+
+// 响应
+interface TerminalOutputResponse {
+  output: string;              // 累积的输出内容
+  truncated: boolean;          // 是否因超出限制被截断
+  exitStatus?: {               // 进程已退出时提供
+    exitCode: number | null;
+    signal: string | null;
+  };
+}
+```
+
+#### `waitForTerminalExit` — 等待终端退出
+
+```typescript
+// 请求
+interface WaitForTerminalExitRequest {
+  terminalId: string;
+}
+
+// 响应
+interface WaitForTerminalExitResponse {
+  exitCode: number | null;
+  signal: string | null;
+}
+```
+
+#### `killTerminal` — 终止终端
+
+```typescript
+// 请求
+interface KillTerminalRequest {
+  terminalId: string;
+}
+
+// 响应
+interface KillTerminalResponse {}  // 空响应
+```
+
+#### `releaseTerminal` — 释放终端资源
+
+```typescript
+// 请求
+interface ReleaseTerminalRequest {
+  terminalId: string;
+}
+
+// 响应
+interface ReleaseTerminalResponse {}  // 空响应
+```
+
+**实现特性**:
+- 如果进程仍在运行,先发送 SIGTERM
+- VS Code 终端保持可见(不自动关闭),符合 ACP 规范
+
+### 6.3 权限请求
+
+#### `requestPermission` — 请求用户授权
+
+```typescript
+// 请求
+interface RequestPermissionRequest {
+  toolCall?: {
+    title?: string;            // 权限请求标题
+  };
+  options: PermissionOption[]; // Agent 提供的选项列表
+}
+
+interface PermissionOption {
+  optionId: string;            // 选项唯一 ID
+  name: string;                // 选项显示名称
+  kind: 'allow_once' | 'allow_always' | 'deny' | string;  // 选项类型
+}
+
+// 响应
+interface RequestPermissionResponse {
+  outcome: {
+    outcome: 'selected' | 'cancelled';
+    optionId?: string;         // 用户选择的选项 ID
+  };
+}
+```
+
+---
+
+## 7. 会话配置系统
+
+### 7.1 Session Mode(模式 — Legacy)
+
+```typescript
+interface SessionModeState {
+  currentModeId: string;
+  availableModes: SessionMode[];
+}
+
+interface SessionMode {
+  id: string;
+  name: string;
+  description?: string;
+}
+```
+
+**设置模式**:Client → Agent
+```typescript
+// setSessionMode 请求
+{ sessionId: string; modeId: string; }
+```
+
+### 7.2 Session Model(模型 — Experimental)
+
+```typescript
+interface SessionModelState {
+  currentModelId: string;
+  availableModels: SessionModel[];
+}
+
+interface SessionModel {
+  modelId: string;
+  name: string;
+  description?: string;
+}
+```
+
+**设置模型**:Client → Agent(unstable API)
+```typescript
+// unstable_setSessionModel 请求
+{ sessionId: string; modelId: string; }
+```
+
+### 7.3 Session Config Options(通用配置选项 — 推荐)
+
+这是取代 modes/models 的新统一 API。当 Agent 同时提供 `configOptions` 和 `modes` 时,客户端**必须**仅使用 `configOptions`。
+
+```typescript
+interface SessionConfigOption {
+  id: string;                              // 配置项唯一 ID
+  type: 'select';                          // 配置类型(目前仅支持 select)
+  name: string;                            // 配置项名称
+  description?: string;                    // 配置项描述
+  category: 'mode' | 'model' | 'thought_level' | string;  // 分类
+  currentValue: string;                    // 当前值
+  options: SelectOption[] | GroupedOptions[];  // 可选值列表
+}
+
+interface SelectOption {
+  value: string;
+  name: string;
+  description?: string;
+}
+
+interface GroupedOptions {
+  group: string;
+  name?: string;
+  options: SelectOption[];
+}
+```
+
+**设置配置选项**:Client → Agent
+```typescript
+// setSessionConfigOption 请求
+{
+  sessionId: string;
+  configId: string;
+  value: string;
+}
+
+// 响应 — 包含更新后的完整 configOptions(级联更新)
+{
+  configOptions: SessionConfigOption[];
+}
+```
+
+---
+
+## 8. 协议错误码
+
+| 错误码 | 含义 | 触发场景 |
+|--------|------|----------|
+| `-32000` | 需要认证 (Auth Required) | `newSession` / `listSessions` 调用时 Agent 需要认证 |
+
+错误通过 `RequestError` 类传递:
+```typescript
+class RequestError extends Error {
+  code: number;
+  message: string;
+}
+```
+
+---
+
+## 9. 完整交互流程示例
+
+```
+┌─────────┐                                    ┌─────────┐
+│  Client │                                    │  Agent  │
+└────┬────┘                                    └────┬────┘
+     │                                              │
+     │──── spawn process (stdin/stdout pipe) ──────>│
+     │                                              │
+     │──── initialize ─────────────────────────────>│
+     │<─── InitializeResponse (agentInfo, caps) ────│
+     │                                              │
+     │──── newSession({cwd, mcpServers}) ──────────>│
+     │<─── NewSessionResponse {sessionId, modes} ───│
+     │                                              │
+     │<─── session/update: available_commands ───────│
+     │<─── session/update: config_option_update ────│
+     │                                              │
+     │──── prompt({sessionId, prompt}) ────────────>│
+     │                                              │
+     │<─── session/update: agent_thought_chunk ─────│ (思考过程)
+     │<─── session/update: agent_thought_chunk ─────│
+     │                                              │
+     │<─── session/update: tool_call ───────────────│ (工具调用)
+     │                                              │
+     │<─── requestPermission ───────────────────────│ (请求权限)
+     │──── {outcome: 'selected', optionId} ────────>│
+     │                                              │
+     │<─── readTextFile({path}) ────────────────────│ (读取文件)
+     │──── {content} ──────────────────────────────>│
+     │                                              │
+     │<─── writeTextFile({path, content}) ──────────│ (写入文件)
+     │──── {} ─────────────────────────────────────>│
+     │                                              │
+     │<─── createTerminal({command, args}) ─────────│ (创建终端)
+     │──── {terminalId} ───────────────────────────>│
+     │                                              │
+     │<─── waitForTerminalExit({terminalId}) ───────│
+     │──── {exitCode} ────────────────────────────>│
+     │                                              │
+     │<─── session/update: tool_call_update ────────│ (工具完成)
+     │<─── session/update: agent_message_chunk ─────│ (回复内容)
+     │<─── session/update: agent_message_chunk ─────│
+     │                                              │
+     │<─── PromptResponse {stopReason: 'end_turn'}──│
+     │                                              │
+     │<─── session/update: session_info_update ─────│ (标题更新)
+     │                                              │
+     │──── cancel({sessionId}) ────────────────────>│ (可选:取消)
+     │                                              │
+     │──── [kill process] ─────────────────────────>│ (断开连接)
+     │                                              │
+```
+
+---
+
+## 10. Agent 注册表
+
+ACP 提供了公共的 Agent 注册表服务:
+
+- **注册表地址**:`https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json`
+- **缓存策略**:客户端缓存 5 分钟
+
+注册表数据结构:
+```typescript
+interface Registry {
+  agents: RegistryAgent[];
+}
+
+interface RegistryAgent {
+  name: string;
+  description?: string;
+  command: string;
+  args?: string[];
+  homepage?: string;
+}
+```
+
+---
+
+## 11. 已知兼容的 ACP Agent
+
+| Agent 名称 | 命令 | 参数 |
+|------------|------|------|
+| GitHub Copilot | `npx` | `@github/copilot-language-server@latest --acp` |
+| Claude Code | `npx` | `@agentclientprotocol/claude-agent-acp@latest` |
+| Gemini CLI | `npx` | `@google/gemini-cli@latest --experimental-acp` |
+| Qwen Code | `npx` | `@qwen-code/qwen-code@latest --acp --experimental-skills` |
+| Auggie CLI | `npx` | `@augmentcode/auggie@latest --acp` |
+| Qoder CLI | `npx` | `@qoder-ai/qodercli@latest --acp` |
+| Codex CLI | `npx` | `@zed-industries/codex-acp@latest` |
+| OpenCode | `npx` | `opencode-ai@latest acp` |
+| OpenClaw | `npx` | `openclaw acp` |
+| Kiro CLI | `kiro-cli` | `acp` |
+| Hermes Agent | `hermes` | `acp` |
+
+---
+
+## 12. 客户端能力声明(Client Capabilities)
+
+客户端在 `initialize` 时声明支持的能力:
+
+```typescript
+interface ClientCapabilities {
+  fs?: {
+    readTextFile?: boolean;   // 支持 Agent 读取文件
+    writeTextFile?: boolean;  // 支持 Agent 写入文件
+  };
+  terminal?: boolean;          // 支持 Agent 操作终端
+}
+```
+
+Agent 据此判断可以向客户端发起哪些反向请求。
+
+---
+
+## 13. 协议设计特点总结
+
+1. **基于 JSON-RPC 风格**:请求-响应模式 + 通知模式
+2. **ND-JSON 传输**:轻量级,易于解析和调试
+3. **流式输出**:通过 `*_chunk` 通知实现实时流式展示
+4. **双向通信**:Agent 既接收指令,也可主动向客户端发起操作请求
+5. **能力协商**:初始化时双方声明能力,避免调用不支持的功能
+6. **认证集成**:支持多种认证方式,认证失败后自动重试
+7. **会话持久化**:支持会话的创建、列举、加载(回放历史)和轻量恢复
+8. **配置灵活性**:从 legacy modes/models 到统一的 configOptions,平滑演进
+9. **权限控制**:Agent 操作需要用户授权,支持自动批准策略
+10. **版本化设计**:通过 `protocolVersion` 确保客户端和代理的兼容性
+
+---
+
+## 14. 附录:消息流方向汇总
+
+| 方法 | 方向 | 类型 | 说明 |
+|------|------|------|------|
+| `initialize` | Client → Agent | Request | 协议握手 |
+| `authenticate` | Client → Agent | Request | 认证 |
+| `newSession` | Client → Agent | Request | 创建会话 |
+| `listSessions` | Client → Agent | Request | 列举会话 |
+| `loadSession` | Client → Agent | Request | 加载会话(重放历史) |
+| `resumeSession` | Client → Agent | Request | 恢复会话(无重放) |
+| `prompt` | Client → Agent | Request | 发送提示 |
+| `cancel` | Client → Agent | Request | 取消当前轮次 |
+| `setSessionMode` | Client → Agent | Request | 设置模式 |
+| `setSessionConfigOption` | Client → Agent | Request | 设置配置选项 |
+| `unstable_setSessionModel` | Client → Agent | Request | 设置模型 |
+| `session/update` | Agent → Client | Notification | 会话状态推送 |
+| `readTextFile` | Agent → Client | Request | 读取文件 |
+| `writeTextFile` | Agent → Client | Request | 写入文件 |
+| `createTerminal` | Agent → Client | Request | 创建终端 |
+| `terminalOutput` | Agent → Client | Request | 获取终端输出 |
+| `waitForTerminalExit` | Agent → Client | Request | 等待终端退出 |
+| `killTerminal` | Agent → Client | Request | 终止终端 |
+| `releaseTerminal` | Agent → Client | Request | 释放终端 |
+| `requestPermission` | Agent → Client | Request | 请求用户授权 |

+ 1226 - 0
docs/ai-assistant-plan.md

@@ -0,0 +1,1226 @@
+# FranklinWH-ToolBox AI 助手方案
+
+## 1. 协议栈说明
+
+| 协议                             | 用途                   | 状态                               |
+| -------------------------------- | ---------------------- | ---------------------------------- |
+| **MCP** (Model Context Protocol) | AI Agent ↔ 工具/数据源 | Kiro CLI 原生支持,主力协议        |
+| **A2A** (Agent2Agent Protocol)   | Agent ↔ Agent 协作     | Linux Foundation 维护,v1.0 已发布 |
+| ~~ACP~~                          | ~~Agent 通信~~         | 2025.8 已合并入 A2A,不再独立存在  |
+
+**建议:** 项目内 AI 助手核心是 **Kiro CLI + 自定义 MCP Server**。如果后续需要多 Agent 协作再引入 A2A。
+
+---
+
+## 2. 现有系统架构
+
+### 2.1 前端(src/)
+
+| 项目       | 说明                                             |
+| ---------- | ------------------------------------------------ |
+| 框架       | Vue 3 + TypeScript + Vite                        |
+| UI 库      | Naive UI                                         |
+| 状态管理   | Pinia(自定义 defineSetupStore 模板)            |
+| 样式       | UnoCSS (Tailwind 兼容)                           |
+| 图表       | @antv/g2                                         |
+| HTTP       | 自定义 FetchClient(支持拦截器、download、blob) |
+| 代码编辑器 | Monaco Editor                                    |
+
+**前端目录结构:**
+
+```
+src/
+├── composable/        # 组合式函数
+├── components/        # 公共组件
+├── event/             # 事件系统
+├── injection/         # 依赖注入
+├── router/            # 路由配置
+├── service/           # API 层(按模块分目录:feedback/, siteInfoManage/, simCardManage/ 等)
+├── stores/            # Pinia Store(modules/data/, modules/views/)
+├── theme/             # 主题配置
+├── types/             # 全局类型
+├── utils/             # 工具函数
+└── views/             # 页面视图(按功能模块分目录)
+    ├── feedback/
+    ├── site-info-manage/
+    ├── sites-monitor/
+    ├── sim-card-manage/
+    ├── batch-download/
+    └── ...
+```
+
+### 2.2 后端(service/)
+
+| 项目     | 说明                                                                   |
+| -------- | ---------------------------------------------------------------------- |
+| 框架     | Go + Fiber v2                                                          |
+| 数据库   | MongoDB + PostgreSQL + MySQL                                           |
+| 缓存     | Redis(自定义 cache 层)                                               |
+| 对象存储 | MinIO                                                                  |
+| 任务队列 | Asynq (Redis-based)                                                    |
+| 外部 API | FranklinWH Energy API、Plane V2 Web API、Acceleronix API、SmartHub API |
+| 认证     | Casdoor JWT                                                            |
+| MQTT     | FWH Pro MQTT                                                           |
+
+**后端目录结构:**
+
+```
+service/
+├── main.go                 # 入口(初始化所有依赖 + 启动 HTTP 服务)
+├── config/                 # 配置加载(Viper,从 .env 读取)
+├── controllers/            # HTTP 路由处理器(Fiber Handler)
+├── services/               # 业务逻辑层
+├── repositories/           # 数据访问层
+│   ├── mongo/              # MongoDB 操作
+│   ├── redis/              # Redis 操作
+│   ├── pgsql/              # PostgreSQL 操作
+│   ├── mysql/              # MySQL 操作
+│   ├── api/                # 外部 API 客户端
+│   │   ├── fwh_pro/        # FranklinWH Energy/OSS API
+│   │   ├── plane_v1/       # Plane V1 API(API Key 认证)
+│   │   ├── plane_v2/       # Plane V2 Web API(Session 认证)
+│   │   ├── acceleronix/    # Acceleronix SIM 卡 API
+│   │   └── smartHub/       # SmartHub SIM 卡 API
+│   ├── mqtt/               # MQTT 通信
+│   └── events/             # 事件通信(xLog TCP)
+├── routes/                 # 路由注册
+├── middleware/             # 中间件(认证等)
+├── domain/                 # 领域模型
+├── dto/                    # 数据传输对象
+├── database/               # 数据库连接初始化
+├── cache/                  # Redis 缓存封装
+├── task/                   # 任务队列(Asynq)
+├── scheduler/              # 定时任务
+├── jobs/                   # 后台作业
+└── utils/                  # 工具包
+```
+
+### 2.3 后端各层职责
+
+```
+HTTP Request
+    │
+    ▼
+┌──────────────┐
+│ controllers/ │  解析参数、校验、调用 service、返回 JSON
+└──────┬───────┘
+       │
+       ▼
+┌──────────────┐
+│  services/   │  业务逻辑编排、数据转换、调用多个 repository
+└──────┬───────┘
+       │
+       ▼
+┌──────────────┐
+│repositories/ │  数据库 CRUD、外部 API 调用、缓存操作
+└──────────────┘
+```
+
+---
+
+## 3. 整体架构(AI 助手视角)
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│                        开发者终端                                  │
+│                        Kiro CLI                                   │
+│                    (AI Agent / Host)                              │
+└────────────┬────────────────────────────────┬────────────────────┘
+             │ MCP (stdio)                    │ MCP (stdio)
+             ▼                                ▼
+┌────────────────────────┐      ┌──────────────────────────────┐
+│   内部工具 MCP Server   │      │      外部服务 MCP Server      │
+│        (Go)            │      │        (社区开源)              │
+├────────────────────────┤      ├──────────────────────────────┤
+│• query_site_info       │      │• AWS Docs 搜索               │
+│• search_sites          │      │• Brave 网络搜索              │
+│• query_sim_card        │      └──────────────────────────────┘
+│• query_power_flow      │
+│• create_plane_issue    │
+│• query_plane_issues    │
+│• query_alarm_stats     │
+└────────────┬───────────┘
+             │ 直接 import 复用
+             ▼
+┌────────────────────────────────────────────────────────────────┐
+│                    现有后端 service/ 代码                         │
+├────────────────────────────────────────────────────────────────┤
+│  services/          │  repositories/         │  domain/dto     │
+│  (业务逻辑)         │  (数据访问+外部API)     │  (数据模型)      │
+└─────────┬───────────┴──────────┬─────────────┴─────────────────┘
+          │                      │
+          ▼                      ▼
+┌──────────────┐  ┌──────────────────────────────────────────────┐
+│  MongoDB     │  │  外部系统                                      │
+│  PostgreSQL  │  │  • FranklinWH Energy API                      │
+│  Redis       │  │  • Plane V2 Web API                           │
+│  MinIO       │  │  • Acceleronix / SmartHub API                 │
+└──────────────┘  └──────────────────────────────────────────────┘
+```
+
+### 3.1 MCP Tools Server 与现有后端的关系
+
+**核心原则:MCP Tools Server 是现有后端 service 层的另一个入口,不是独立系统。**
+
+| 对比     | 现有 HTTP Server (`main.go`)          | MCP Tools Server                   |
+| -------- | ------------------------------------- | ---------------------------------- |
+| 入口     | Fiber HTTP 路由                       | stdio JSON-RPC                     |
+| 调用方   | 前端浏览器                            | Kiro CLI (AI Agent)                |
+| 认证     | Casdoor JWT                           | 无需(本地 stdio 进程)            |
+| 复用层   | controllers → services → repositories | **直接调用 services/repositories** |
+| 输出格式 | JSON HTTP Response                    | MCP Tool Result (text/JSON)        |
+
+**具体复用方式:**
+
+```go
+// MCP Tools Server 的 go.mod 中引用现有 service 模块
+// 方式一:同一 module,不同 main 入口
+//   service/cmd/mcp/main.go  (MCP Server 入口)
+//   service/main.go          (HTTP Server 入口)
+
+// 方式二:独立 module,replace 引用
+// mcp-servers/tools/go.mod:
+//   require fwh-toolbox-server v0.0.0
+//   replace fwh-toolbox-server => ../../service
+```
+
+**推荐方式一**(同一 module):在 `service/cmd/mcp/` 下创建 MCP Server 入口,共享整个 service 代码库。
+
+```
+service/
+├── main.go                    # 现有 HTTP Server 入口
+├── cmd/
+│   └── mcp/
+│       └── main.go            # MCP Tools Server 入口
+├── services/                  # 共享:业务逻辑
+├── repositories/              # 共享:数据访问
+├── domain/                    # 共享:领域模型
+├── dto/                       # 共享:DTO
+├── config/                    # 共享:配置
+├── database/                  # 共享:数据库连接
+└── cache/                     # 共享:缓存
+```
+
+**MCP Server 入口初始化流程:**
+
+```go
+// service/cmd/mcp/main.go
+func main() {
+    // 1. 加载配置(同 HTTP Server)
+    cfg, _ := config.LoadConfig()
+
+    // 2. 初始化数据库连接(同 HTTP Server)
+    database.Init(&cfg.App)
+    cache.Init(cfg.App.RedisAddr)
+    repositories.PlaneWebHttpInit(&cfg.PlaneWebApi)
+
+    // 3. 创建 MCP Server(替代 Fiber HTTP 路由)
+    s := server.NewMCPServer("fwh-tools", "1.0.0",
+        server.WithToolCapabilities(false),
+        server.WithRecovery(),
+    )
+
+    // 4. 注册 Tools(调用 services/ 层函数)
+    tools.RegisterSiteInfoTools(s)
+    tools.RegisterSimCardTools(s)
+    tools.RegisterPowerFlowTools(s)
+    tools.RegisterAlarmTools(s)
+    tools.RegisterShutdownTools(s)
+    tools.RegisterPlaneTools(s)
+
+    // 5. 启动 stdio 通信(替代 app.Listen)
+    server.ServeStdio(s)
+}
+```
+
+**不需要的部分:**
+
+- 不需要 Fiber 框架
+- 不需要 controllers/ 层(MCP handler 直接调用 services)
+- 不需要 middleware/(stdio 无需 HTTP 认证)
+- 不需要 routes/(MCP 用 tool 注册替代)
+
+---
+
+## 4. 知识库 MCP Server(独立 VitePress 项目)
+
+知识库是一个独立的 VitePress 文档项目,通过 `vitepress-plugin-mcp` 插件将文档暴露为 MCP Server。
+
+### 4.1 概述
+
+| 项目     | 说明                                               |
+| -------- | -------------------------------------------------- |
+| 位置     | 独立 Git 仓库(不在 FranklinWH-ToolBox 内)        |
+| 框架     | VitePress(Markdown 文档站)                       |
+| 内容     | 团队知识:编码规范、架构模板、技术文档、最佳实践等 |
+| MCP 插件 | `vitepress-plugin-mcp`(npm 包)                   |
+| MCP 传输 | Streamable HTTP(远程 MCP Server)                 |
+| 配置层级 | `./.kiro/settings/mcp.json`(后端运行目录级别)    |
+
+### 4.2 VitePress 知识库项目搭建
+
+**Step 1:创建 VitePress 项目**
+
+```bash
+mkdir team-knowledge && cd team-knowledge
+pnpm init
+pnpm add -D vitepress vitepress-plugin-mcp
+mkdir docs
+```
+
+**Step 2:编写文档内容**
+
+```
+team-knowledge/
+├── docs/
+│   ├── index.md                    # 首页
+│   ├── coding/
+│   │   ├── frontend.md             # 前端编码规范
+│   │   ├── backend.md              # 后端编码规范
+│   │   └── git.md                  # Git 提交规范
+│   ├── architecture/
+│   │   ├── vue-project.md          # Vue 项目架构模板
+│   │   ├── go-service.md           # Go 后端架构模板
+│   │   └── mcp-server.md           # MCP Server 开发指南
+│   ├── devops/
+│   │   ├── deployment.md           # 部署流程
+│   │   └── monitoring.md           # 监控方案
+│   └── troubleshooting/
+│       ├── common-issues.md        # 常见问题排查
+│       └── fwh-specific.md         # FranklinWH 业务相关
+├── .vitepress/
+│   └── config.ts                   # VitePress + MCP 配置
+├── package.json
+└── pnpm-lock.yaml
+```
+
+**Step 3:配置 VitePress + MCP 插件**
+
+```ts
+// .vitepress/config.ts
+import { defineConfig } from 'vitepress'
+import { MCPPlugin } from 'vitepress-plugin-mcp'
+
+export default defineConfig({
+  title: 'Team Knowledge Base',
+  description: '团队知识库',
+  themeConfig: {
+    search: {
+      provider: 'local', // 必须启用 local search(插件依赖它的索引)
+      options: {},
+    },
+    sidebar: [
+      {
+        text: '编码规范',
+        items: [
+          { text: '前端', link: '/coding/frontend' },
+          { text: '后端', link: '/coding/backend' },
+          { text: 'Git', link: '/coding/git' },
+        ],
+      },
+      {
+        text: '架构模板',
+        items: [
+          { text: 'Vue 项目', link: '/architecture/vue-project' },
+          { text: 'Go 后端', link: '/architecture/go-service' },
+        ],
+      },
+    ],
+  },
+  vite: {
+    plugins: [
+      MCPPlugin({ port: 4100 }), // MCP Server 监听端口
+    ],
+  },
+})
+```
+
+**Step 4:启动**
+
+```bash
+# 开发模式(同时启动 VitePress + MCP Server)
+pnpm vitepress dev docs
+
+# MCP Server 自动在 http://localhost:4100 启动
+```
+
+**Step 5:验证 MCP Server**
+
+```bash
+# 测试 MCP Server 是否可用
+curl http://localhost:4100/mcp
+```
+
+### 4.3 插件暴露的 Tools
+
+`vitepress-plugin-mcp` 自动提供以下工具:
+
+| Tool 名称              | 描述                                                 | 参数            |
+| ---------------------- | ---------------------------------------------------- | --------------- |
+| `search_documentation` | 全文搜索文档内容(基于 VitePress local search 索引) | `query: string` |
+| `get_page_content`     | 获取指定页面的完整 Markdown 内容                     | `path: string`  |
+| `list_pages`           | 列出所有文档页面                                     | 无              |
+
+### 4.4 在 Kiro 中配置连接
+
+知识库是**远程 MCP Server**(HTTP),在后端运行目录的 Kiro 配置中使用 `url` 字段连接:
+
+**`./.kiro/settings/mcp.json` 中添加:**
+
+```json
+{
+  "mcpServers": {
+    "team-knowledge": {
+      "url": "http://lochost:4100/mcp"
+    }
+  }
+}
+```
+
+### 4.5 部署方案
+
+| 环境     | 方式                                 | 地址                                 |
+| -------- | ------------------------------------ | ------------------------------------ |
+| 本地开发 | `pnpm vitepress dev docs`            | `http://localhost:4100/mcp`          |
+| 团队共享 | 部署到内网服务器(PM2/systemd 常驻) | `http://knowledge.internal:4100/mcp` |
+| Docker   | 容器化部署                           | `http://knowledge:4100/mcp`          |
+
+**PM2 部署示例:**
+
+```bash
+# ecosystem.config.js
+module.exports = {
+  apps: [{
+    name: 'knowledge-mcp',
+    script: 'npx',
+    args: 'vitepress dev docs --port 5173',
+    cwd: '/opt/team-knowledge',
+    env: { NODE_ENV: 'production' }
+  }]
+}
+```
+
+**注意:** `vitepress-plugin-mcp` 的 MCP Server 与 VitePress dev server 绑定运行。生产环境需要保持 `vitepress dev` 常驻运行(不是 `vitepress build`)。
+
+### 4.6 使用效果
+
+配置完成后,在 Kiro CLI 中可以直接使用知识库:
+
+```
+> 帮我查一下前端编码规范中关于组件命名的约定
+
+Kiro 会自动调用 team-knowledge MCP Server 的 search_documentation 工具,
+搜索知识库内容并返回相关文档片段。
+```
+
+### 4.7 添加新文档的流程
+
+```
+1. 在 team-knowledge 仓库中编写 Markdown 文件
+2. git commit && git push
+3. 服务器自动重启(PM2 watch 或 CI/CD)
+4. MCP Server 自动索引新内容
+5. Kiro 下次搜索即可获取新文档
+```
+
+---
+
+## 5. 内部工具 MCP Server 详细设计
+
+### 5.1 技术选型
+
+| 项目      | 选择                          | 理由                                   |
+| --------- | ----------------------------- | -------------------------------------- |
+| 语言      | Go                            | 与后端一致,直接 import service 层代码 |
+| SDK       | `github.com/mark3labs/mcp-go` | 社区最活跃的 Go MCP SDK                |
+| Transport | stdio                         | Kiro CLI 本地调用,最简单              |
+| 构建产物  | 单二进制`fwh-mcp-server`      | `go build -o fwh-mcp-server ./cmd/mcp` |
+
+### 5.2 暴露的 Tools 清单
+
+| Tool 名称                   | 输入参数                              | 描述                    | 复用的 service 函数               |
+| --------------------------- | ------------------------------------- | ----------------------- | --------------------------------- |
+| `query_site_info`           | `sn: string`                          | 查询站点设备详细信息    | `services.GetSiteInfoData`        |
+| `search_sites`              | `page, size: int, keyword?: string`   | 搜索站点列表            | `services.QuerySiteInfoDataList`  |
+| `query_sim_card`            | `giccid: string`                      | 查询单张 SIM 卡状态     | `services.GetSimCardInfo`         |
+| `search_sim_cards`          | `page, size: int, status?, platform?` | 搜索 SIM 卡列表         | `services.GetSimCardBatchInfo`    |
+| `query_power_flow`          | `sn: string, date: string`            | 查询站点功率流数据      | `services.QuerySitePowerFlow`     |
+| `query_alarm_statistics`    | `date?: string`                       | 查询告警统计            | `services.GetAlarmStatistics`     |
+| `query_shutdown_statistics` | `date: string`                        | 查询停机统计            | `services.GetShutdownStatistics`  |
+| `create_feedback`           | `title, type, priority, description`  | 创建 Plane Intake Issue | `services.SubmitFeedback`         |
+| `query_feedback_history`    | 无                                    | 查询 Feedback 提交历史  | `services.GetFeedbackHistory`     |
+
+### 5.3 代码结构
+
+```
+service/
+├── cmd/
+│   └── mcp/
+│       ├── main.go              # MCP Server 入口
+│       └── tools/
+│           ├── siteInfo.go      # 站点信息相关 tools
+│           ├── simCard.go       # SIM 卡相关 tools
+│           ├── powerFlow.go     # 功率流相关 tools
+│           ├── alarm.go         # 告警统计 tools
+│           ├── shutdown.go      # 停机统计 tools
+│           └── plane.go         # Plane/Feedback tools
+├── main.go                      # 现有 HTTP Server(不改动)
+├── services/                    # 共享
+├── repositories/                # 共享
+└── ...
+```
+
+### 5.4 代码示例
+
+**入口 `service/cmd/mcp/main.go`:**
+
+```go
+package main
+
+import (
+    "fmt"
+    "log"
+    "os"
+
+    "github.com/mark3labs/mcp-go/server"
+
+    "fwh-toolbox-server/cache"
+    "fwh-toolbox-server/config"
+    "fwh-toolbox-server/database"
+    "fwh-toolbox-server/repositories"
+
+    "fwh-toolbox-server/cmd/mcp/tools"
+)
+
+func main() {
+    // 加载配置
+    cfg, err := config.LoadConfig()
+    if err != nil {
+        log.Fatalf("加载配置失败: %v", err)
+    }
+
+    // 初始化基础设施(与 HTTP Server 相同)
+    if err := database.Init(&cfg.App); err != nil {
+        log.Fatalf("数据库初始化失败: %v", err)
+    }
+    cache.Init(cfg.App.RedisAddr)
+    repositories.PlaneWebHttpInit(&cfg.PlaneWebApi)
+
+    // 创建 MCP Server
+    s := server.NewMCPServer(
+        "fwh-tools",
+        "1.0.0",
+        server.WithToolCapabilities(false),
+        server.WithRecovery(),
+    )
+
+    // 注册所有 Tools
+    tools.RegisterSiteInfoTools(s)
+    tools.RegisterSimCardTools(s)
+    tools.RegisterPowerFlowTools(s)
+    tools.RegisterAlarmTools(s)
+    tools.RegisterShutdownTools(s)
+    tools.RegisterPlaneTools(s)
+
+    // 启动 stdio 通信
+    fmt.Fprintln(os.Stderr, "[fwh-mcp-server] started (stdio)")
+    if err := server.ServeStdio(s); err != nil {
+        log.Fatalf("[fwh-mcp-server] error: %v", err)
+    }
+}
+```
+
+**工具示例 `service/cmd/mcp/tools/siteInfo.go`:**
+
+```go
+package tools
+
+import (
+    "context"
+    "encoding/json"
+    "fmt"
+
+    "github.com/mark3labs/mcp-go/mcp"
+    "github.com/mark3labs/mcp-go/server"
+
+    "fwh-toolbox-server/services"
+)
+
+func RegisterSiteInfoTools(s *server.MCPServer) {
+    // Tool: 查询站点信息
+    s.AddTool(
+        mcp.NewTool("query_site_info",
+            mcp.WithDescription("查询单个站点设备的详细信息,包括版本、网络状态、激活状态、SN、固件等"),
+            mcp.WithString("sn", mcp.Required(), mcp.Description("站点 SN 序列号")),
+        ),
+        handleQuerySiteInfo,
+    )
+
+    // Tool: 搜索站点列表
+    s.AddTool(
+        mcp.NewTool("search_sites",
+            mcp.WithDescription("搜索站点设备列表,支持分页"),
+            mcp.WithNumber("page", mcp.Description("页码,从1开始,默认1")),
+            mcp.WithNumber("size", mcp.Description("每页数量,默认10")),
+            mcp.WithString("keyword", mcp.Description("搜索关键字(匹配 SN、设备名)")),
+        ),
+        handleSearchSites,
+    )
+}
+
+func handleQuerySiteInfo(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+    sn, err := req.RequireString("sn")
+    if err != nil {
+        return mcp.NewToolResultError("参数 sn 必填"), nil
+    }
+
+    data, err := services.GetSiteInfoData(sn)
+    if err != nil {
+        return mcp.NewToolResultError(fmt.Sprintf("查询失败: %v", err)), nil
+    }
+
+    jsonBytes, _ := json.MarshalIndent(data, "", "  ")
+    return mcp.NewToolResultText(string(jsonBytes)), nil
+}
+
+func handleSearchSites(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+    page := int(req.GetFloat("page", 1))
+    size := int(req.GetFloat("size", 10))
+    keyword := req.GetString("keyword", "")
+
+    data, err := services.QuerySiteInfoDataList(uint(page), uint(size), keyword, nil, nil, nil)
+    if err != nil {
+        return mcp.NewToolResultError(fmt.Sprintf("搜索失败: %v", err)), nil
+    }
+
+    jsonBytes, _ := json.MarshalIndent(data, "", "  ")
+    return mcp.NewToolResultText(string(jsonBytes)), nil
+}
+```
+
+---
+
+## 6. 外部服务 MCP Server
+
+直接使用社区开源 Server,不需要自己实现。**不使用 Git MCP Server。**
+
+| Server            | 用途                     | 包名                                       |
+| ----------------- | ------------------------ | ------------------------------------------ |
+| AWS Documentation | 搜索 AWS 文档、最佳实践  | `@aws/aws-documentation-mcp-server`        |
+| Brave Search      | 网络搜索(技术问题排查) | `@modelcontextprotocol/server-bravesearch` |
+
+---
+
+## 7. Kiro CLI 配置
+
+所有 Kiro 配置基于后端程序运行目录(`service/`)的相对路径 `./.kiro/`。
+
+### 7.1 工作区 MCP 配置
+
+**`service/.kiro/settings/mcp.json`:**
+
+```json
+{
+  "mcpServers": {
+    "fwh-tools": {
+      "command": "./fwh-mcp-server",
+      "args": [],
+      "env": {
+        "MONGO_DSN": "${MONGO_DSN}",
+        "REDIS_ADDR": "${REDIS_ADDR}",
+        "PLANE_WEB_HOST": "${PLANE_WEB_HOST}",
+        "PLANE_WEB_EMAIL": "${PLANE_WEB_EMAIL}",
+        "PLANE_WEB_PASSWORD": "${PLANE_WEB_PASSWORD}",
+        "PLANE_WEB_WORKSPACE_SLUG": "${PLANE_WEB_WORKSPACE_SLUG}",
+        "PLANE_WEB_PROJECT_ID": "${PLANE_WEB_PROJECT_ID}"
+      }
+    },
+    "team-knowledge": {
+      "url": "http://knowledge.internal:4100/mcp"
+    },
+    "aws-docs": {
+      "command": "npx",
+      "args": ["-y", "@aws/aws-documentation-mcp-server"]
+    },
+    "web-search": {
+      "command": "npx",
+      "args": ["-y", "@modelcontextprotocol/server-bravesearch"],
+      "env": {
+        "BRAVE_API_KEY": "${BRAVE_API_KEY}"
+      }
+    }
+  }
+}
+```
+
+### 7.2 Steering 文件
+
+**`service/.kiro/steering/project.md`:**
+
+```markdown
+# FranklinWH-ToolBox 项目上下文
+
+## 项目概述
+
+内部工具平台,前端 Vue3 + Naive UI,后端 Go Fiber + MongoDB。
+
+## 可用 MCP 工具
+
+- fwh-tools: 查询站点、SIM卡、功率流、告警、Plane Issue
+- team-knowledge: 搜索团队知识库文档
+- aws-docs: 搜索 AWS 文档
+- web-search: 网络搜索
+
+## 注意事项
+
+- 不要直接操作 Git
+- 修改代码后需 go build / npm run build 验证
+- 后端分层:controllers → services → repositories
+```
+
+### 7.3 部署目录结构
+
+```
+/var/www/fwh-toolbox/server/          # 后端运行目录(WorkingDirectory)
+├── fwh-toolbox-server                # HTTP Server 二进制
+├── fwh-mcp-server                    # MCP Tools Server 二进制
+├── .kiro/
+│   ├── settings/
+│   │   └── mcp.json                  # MCP Server 配置(command: "./fwh-mcp-server")
+│   └── steering/
+│       └── project.md                # Agent 指令文件
+└── .env                              # 环境变量配置
+```
+
+**源码目录结构:**
+
+```
+service/
+├── .kiro/                            # Kiro 配置(开发环境 + 部署时拷贝)
+│   ├── settings/
+│   │   └── mcp.json
+│   └── steering/
+│       └── project.md
+├── cmd/
+│   └── mcp/
+│       ├── main.go                   # MCP Server 入口
+│       └── tools/                    # 各工具实现
+│           ├── siteInfo.go
+│           ├── simCard.go
+│           ├── powerFlow.go
+│           ├── alarm.go
+│           ├── shutdown.go
+│           └── plane.go
+├── main.go                           # HTTP Server 入口
+└── ...
+```
+
+### 7.4 后端 ACP Client 引用
+
+ACP Client 启动 `kiro-cli acp` 时指定工作目录为后端运行目录,Kiro 自动从 `./.kiro/settings/mcp.json` 加载配置:
+
+```go
+cmd := exec.Command("kiro-cli", "acp")
+cmd.Dir = serviceDir  // 后端运行目录(含 .kiro/ 和 fwh-mcp-server)
+```
+
+---
+
+## 8. 构建与运行
+
+### 8.1 构建 MCP Tools Server
+
+```bash
+cd service
+go build -o fwh-mcp-server ./cmd/mcp
+```
+
+### 8.2 本地测试
+
+```bash
+# 直接运行测试
+echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | ./fwh-mcp-server
+
+# 通过 Kiro CLI 测试(在 service/ 目录下)
+kiro chat  # 然后在对话中使用工具
+```
+
+---
+
+## 9. 实现步骤
+
+| 阶段        | 内容                                  | 输出物                         | 预估时间 |
+| ----------- | ------------------------------------- | ------------------------------ | -------- |
+| **Phase 1** | 创建`service/cmd/mcp/` 目录结构和入口 | main.go + go build 通过        | 0.5 天   |
+| **Phase 2** | 实现站点信息 Tools                    | tools/siteInfo.go              | 1 天     |
+| **Phase 3** | 实现 SIM 卡 + 功率流 Tools            | tools/simCard.go, powerFlow.go | 1 天     |
+| **Phase 4** | 实现告警 + Plane Tools                | tools/alarm.go, plane.go       | 1 天     |
+| **Phase 5** | 配置`.kiro/` + Steering + 联调        | 配置文件 + 端到端测试          | 0.5 天   |
+| **Phase 6** | 配置外部 MCP Server (AWS Docs, Brave) | npm 安装 + 环境变量配置        | 0.5 天   |
+
+**总计:约 4.5 天**
+
+---
+
+## 10. 前端 AI Chat 与后端通讯设计
+
+### 10.1 架构概览
+
+```
+┌────────────────────────────────┐
+│        前端 (Vue 3)             │
+│   src/views/ai-chat/           │
+│  ┌──────────────────────────┐  │
+│  │   ChatPanel 组件          │  │
+│  │  • 消息列表 (Markdown)    │  │
+│  │  • 输入框                 │  │
+│  │  • 流式输出显示           │  │
+│  └────────────┬─────────────┘  │
+│               │ SSE / fetch     │
+└───────────────┼────────────────┘
+                │
+                ▼
+┌────────────────────────────────┐
+│        后端 (Go Fiber)          │
+│   service/controllers/          │
+│   aiChatControllers.go          │
+│  ┌──────────────────────────┐  │
+│  │  POST /ai/chat            │  │
+│  │  • 接收用户消息           │  │
+│  │  • 调用 LLM API          │  │
+│  │  • SSE 流式返回           │  │
+│  └────────────┬─────────────┘  │
+│               │                 │
+│               ▼                 │
+│  ┌──────────────────────────┐  │
+│  │  LLM Provider             │  │
+│  │  • Claude API (Anthropic) │  │
+│  │  • 或 OpenAI API          │  │
+│  │  • 或 AWS Bedrock         │  │
+│  └──────────────────────────┘  │
+│               │                 │
+│               ▼                 │
+│  ┌──────────────────────────┐  │
+│  │  MCP Tools (可选)         │  │
+│  │  • 复用 services 层       │  │
+│  │  • AI 可调用内部工具      │  │
+│  └──────────────────────────┘  │
+└────────────────────────────────┘
+```
+
+### 10.2 通讯协议:SSE(Server-Sent Events)
+
+选择 SSE 而非 WebSocket 的原因:
+
+- AI 对话是单向流(服务端推送 token 流给前端)
+- SSE 基于 HTTP,天然支持认证 header
+- 无需额外的连接管理
+- 断线自动重连
+
+### 10.3 后端引擎:Kiro ACP Client
+
+后端实现 ACP (Agent Client Protocol) Client,通过 JSON-RPC 2.0 与 `kiro-cli acp` 子进程结构化通讯。
+
+**为什么选 ACP 而非直接 exec Kiro CLI:**
+
+| 对比项         | exec kiro chat        | ACP Client                                 |
+| -------------- | --------------------- | ------------------------------------------ |
+| 通讯格式       | 非结构化文本输出      | JSON-RPC 2.0 结构化                        |
+| 会话管理       | 自己实现              | Kiro 原生支持(session/new, session/load) |
+| 工具调用可见性 | 无法感知              | ToolCall/ToolCallUpdate 实时事件           |
+| 取消操作       | kill 进程             | session/cancel 优雅取消                    |
+| 会话持久化     | 自己实现 history.json | Kiro 自动持久化到 ~/.kiro/sessions/        |
+| 协议稳定性     | CLI 输出格式可能变    | ACP 是开放标准                             |
+
+**架构:**
+
+```
+前端 SSE ←── 后端 (ACP Client) ←── JSON-RPC stdin/stdout ←── kiro-cli acp
+                                                                    │
+                                                                    ├── 自动调用 MCP Server
+                                                                    ├── 自动管理会话上下文
+                                                                    └── 自动持久化对话历史
+```
+
+### 10.4 后端接口设计
+
+**`POST /api/ai/chat`** — 发送消息
+
+请求:
+
+```json
+{
+  "message": "帮我查一下 SN 为 ABC123 的站点信息",
+  "sessionId": "sess_abc123" // 可选,不传则使用默认会话
+}
+```
+
+响应(SSE 流,将 ACP 事件转发给前端):
+
+```
+Content-Type: text/event-stream
+
+event: message
+data: {"type":"text","content":"正在查询站点信息..."}
+
+event: tool_call
+data: {"type":"tool_call","name":"query_site_info","status":"running"}
+
+event: tool_update
+data: {"type":"tool_update","name":"query_site_info","status":"completed"}
+
+event: message
+data: {"type":"text","content":"该站点的固件版本是 v3.2.1..."}
+
+event: done
+data: {"type":"done","sessionId":"sess_abc123"}
+```
+
+**SSE 事件映射:**
+
+| SSE Event     | ACP Update Type     | 说明                  |
+| ------------- | ------------------- | --------------------- |
+| `message`     | `AgentMessageChunk` | AI 文本流(逐块推送) |
+| `tool_call`   | `ToolCall`          | Kiro 调用 MCP 工具    |
+| `tool_update` | `ToolCallUpdate`    | 工具执行进度          |
+| `done`        | `TurnEnd`           | 本轮对话结束          |
+| `error`       | -                   | 错误                  |
+
+**其他接口:**
+
+| 接口               | 方法   | 描述                   |
+| ------------------ | ------ | ---------------------- |
+| `/ai/chat`         | POST   | 发送消息,SSE 流式返回 |
+| `/ai/sessions`     | GET    | 获取当前用户会话列表   |
+| `/ai/sessions/new` | POST   | 创建新会话             |
+| `/ai/sessions/:id` | DELETE | 删除会话               |
+| `/ai/cancel`       | POST   | 取消当前操作           |
+
+### 10.5 后端 ACP Client 实现
+
+**核心数据结构:**
+
+```go
+// ACPSession 单个用户的 ACP 连接
+type ACPSession struct {
+    UserId     string
+    SessionId  string           // Kiro ACP session ID
+    Process    *exec.Cmd        // kiro-cli acp 长驻进程
+    Stdin      io.WriteCloser   // JSON-RPC 请求写入
+    Stdout     *bufio.Reader    // JSON-RPC 响应/通知读取
+    Mutex      sync.Mutex       // 同一用户串行对话
+    LastActive time.Time
+    RequestId  int64            // JSON-RPC 请求 ID 自增
+}
+
+// ACPManager 管理所有用户的 ACP 会话
+type ACPManager struct {
+    sessions    map[string]*ACPSession  // userId -> session
+    mu          sync.RWMutex
+    maxIdle     time.Duration           // 空闲超时
+    projectDir  string                  // 项目目录
+}
+```
+
+**完整通讯流程:**
+
+```go
+// 1. 启动 kiro-cli acp(每用户一个长驻进程)
+func (m *ACPManager) GetOrCreate(userId string) (*ACPSession, error) {
+    if sess, exists := m.sessions[userId]; exists {
+        sess.LastActive = time.Now()
+        return sess, nil
+    }
+
+    cmd := exec.Command("kiro-cli", "acp")
+    cmd.Dir = m.projectDir  // 含 .kiro/settings/mcp.json
+
+    stdin, _ := cmd.StdinPipe()
+    stdout, _ := cmd.StdoutPipe()
+    cmd.Start()
+
+    sess := &ACPSession{
+        UserId:  userId,
+        Process: cmd,
+        Stdin:   stdin,
+        Stdout:  bufio.NewReader(stdout),
+    }
+
+    // 2. initialize 握手
+    sess.sendRPC("initialize", map[string]any{
+        "protocolVersion": 1,
+        "clientCapabilities": map[string]any{
+            "fs": map[string]any{"readTextFile": true, "writeTextFile": true},
+        },
+        "clientInfo": map[string]any{"name": "fwh-toolbox", "version": "1.0.0"},
+    })
+
+    // 3. 创建会话
+    result := sess.sendRPC("session/new", map[string]any{"cwd": m.projectDir})
+    sess.SessionId = result.SessionId
+
+    m.sessions[userId] = sess
+    return sess, nil
+}
+
+// 4. 发送消息并流式读取
+func (sess *ACPSession) Prompt(message string, events chan<- SSEEvent) {
+    sess.Mutex.Lock()
+    defer sess.Mutex.Unlock()
+
+    sess.sendRPC("session/prompt", map[string]any{
+        "sessionId": sess.SessionId,
+        "content":   []map[string]any{{"type": "text", "text": message}},
+    })
+
+    // 持续读取 session/notification 直到 TurnEnd
+    for {
+        notif := sess.readNotification()
+        switch notif.Params.UpdateType {
+        case "AgentMessageChunk":
+            events <- SSEEvent{Type: "message", Content: notif.Params.Text}
+        case "ToolCall":
+            events <- SSEEvent{Type: "tool_call", Name: notif.Params.ToolName}
+        case "ToolCallUpdate":
+            events <- SSEEvent{Type: "tool_update", Name: notif.Params.ToolName}
+        case "TurnEnd":
+            events <- SSEEvent{Type: "done", SessionId: sess.SessionId}
+            close(events)
+            return
+        }
+    }
+}
+
+// 5. 取消操作
+func (sess *ACPSession) Cancel() {
+    sess.sendRPC("session/cancel", map[string]any{"sessionId": sess.SessionId})
+}
+```
+
+**Controller 层:**
+
+```go
+func AiChat(c *fiber.Ctx) error {
+    userId := middleware.AuthGetUserName(c)
+    var req struct {
+        Message   string `json:"message"`
+        SessionId string `json:"sessionId"`
+    }
+    c.BodyParser(&req)
+
+    sess, err := acpManager.GetOrCreate(userId)
+    if err != nil {
+        return Error(c, "AI 会话创建失败")
+    }
+
+    // SSE 流式响应
+    c.Set("Content-Type", "text/event-stream")
+    c.Set("Cache-Control", "no-cache")
+
+    c.Context().SetBodyStreamWriter(func(w *bufio.Writer) {
+        events := make(chan SSEEvent, 50)
+        go sess.Prompt(req.Message, events)
+
+        for event := range events {
+            fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event.Type, event.JSON())
+            w.Flush()
+        }
+    })
+    return nil
+}
+```
+
+### 10.6 多会话管理(ACP 原生能力)
+
+Kiro ACP 原生支持多会话,后端无需自己实现持久化。
+
+**ACP 会话方法:**
+
+| 方法             | 用途                                   |
+| ---------------- | -------------------------------------- |
+| `session/new`    | 创建新会话(Kiro 分配 ID,自动持久化) |
+| `session/load`   | 按 ID 恢复历史会话                     |
+| `session/prompt` | 在指定会话中对话                       |
+| `session/cancel` | 取消当前操作                           |
+
+**会话生命周期:**
+
+```
+用户首次 → 启动 kiro-cli acp 进程 → initialize → session/new
+                                                       │
+用户对话 → session/prompt → 读取 notifications → SSE   │ Kiro 自动
+                                                       │ 持久化
+用户加载历史 → session/load(sessionId)                  │
+                                                       ▼
+空闲 30min → kill 进程 → 下次使用时重启 + session/load 恢复
+```
+
+**Kiro 自动持久化目录:**
+
+```
+~/.kiro/sessions/cli/
+├── <session-id>.json       # 会话元数据
+└── <session-id>.jsonl      # 对话事件日志
+```
+
+### 10.7 限制与注意事项
+
+| 项目       | 说明                                                    |
+| ---------- | ------------------------------------------------------- |
+| 并发限制   | 同一用户 Mutex 串行(ACP 不支持同 session 并行 prompt) |
+| 最大进程数 | 限制 20 个并发 kiro-cli acp 进程                        |
+| 空闲超时   | 30 分钟无活动 kill 进程(再次使用时自动恢复)           |
+| 请求超时   | 单次 prompt 最多等待 120 秒                             |
+| MCP 配置   | 所有会话共享项目 .kiro/settings/mcp.json                |
+| Kiro 订阅  | 需确保服务器上 Kiro 已登录且订阅有效                    |
+
+### 10.8 前端实现要点
+
+**实现位置:** `src/layout/header/Actions` 中,作为顶部栏全局操作按钮,点击展开 AI Chat 侧边抽屉。
+
+**目录结构:**
+
+```
+src/layout/header/Actions/
+├── AiChat/
+│   ├── index.vue             # 入口按钮 + NDrawer 容器
+│   ├── ChatPanel.vue         # 聊天面板(消息列表 + 输入)
+│   ├── MessageItem.vue       # 单条消息(支持 Markdown 渲染)
+│   ├── ToolCallCard.vue      # 工具调用展示卡片
+│   ├── StreamingText.vue     # 流式文本显示组件
+│   ├── useAiChat.ts          # 核心逻辑:SSE 连接、消息管理
+│   └── interface.ts          # 类型定义
+```
+
+**入口组件 `AiChat/index.vue`:**
+
+```vue
+<template>
+  <!-- 顶部栏按钮 -->
+  <NButton text @click="drawerShow = true">
+    <template #icon>
+      <span class="i-ph:chat-circle-dots" />
+    </template>
+  </NButton>
+
+  <!-- 侧边抽屉 -->
+  <NDrawer v-model:show="drawerShow" :width="480" placement="right">
+    <NDrawerContent title="AI 助手" closable>
+      <ChatPanel />
+    </NDrawerContent>
+  </NDrawer>
+</template>
+```
+
+**`useAiChat.ts` 核心逻辑:**
+
+```typescript
+export function useAiChat() {
+  const messages = ref<ChatMessage[]>([])
+  const isStreaming = ref(false)
+
+  async function sendMessage(content: string) {
+    // 添加用户消息
+    messages.value.push({ role: 'user', content })
+    isStreaming.value = true
+
+    // 添加 AI 消息占位
+    const aiMessage: ChatMessage = { role: 'assistant', content: '', toolCalls: [] }
+    messages.value.push(aiMessage)
+
+    // 发起 SSE 请求(使用框架的 fetch,自动带 token)
+    const response = await fetch(baseUrl + '/ai/chat', {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        Authorization: `Bearer ${token}`,
+      },
+      body: JSON.stringify({
+        message: content,
+        history: messages.value.slice(0, -1), // 不含当前占位
+      }),
+    })
+
+    // 读取 SSE 流
+    const reader = response.body!.getReader()
+    const decoder = new TextDecoder()
+
+    while (true) {
+      const { done, value } = await reader.read()
+      if (done) break
+
+      const text = decoder.decode(value)
+      // 解析 SSE 事件,逐步更新 aiMessage.content
+      parseSSEEvents(text, aiMessage)
+    }
+
+    isStreaming.value = false
+  }
+
+  return { messages, isStreaming, sendMessage }
+}
+```
+
+**`ChatPanel.vue` 基本结构:**
+
+```vue
+<template>
+  <div class="flex h-full flex-col">
+    <!-- 消息列表 -->
+    <div class="flex-1 overflow-y-auto p-4 space-y-4">
+      <MessageItem v-for="msg in messages" :key="msg.id" :message="msg" />
+      <!-- 流式输出中的光标闪烁 -->
+      <span v-if="isStreaming" class="animate-pulse">▊</span>
+    </div>
+
+    <!-- 输入区域 -->
+    <div class="border-t p-4">
+      <NInput
+        v-model:value="inputText"
+        type="textarea"
+        :autosize="{ minRows: 1, maxRows: 4 }"
+        placeholder="输入消息...(Shift+Enter 换行)"
+        @keydown.enter.exact.prevent="handleSend"
+      />
+    </div>
+  </div>
+</template>
+```
+
+### 10.9 消息渲染
+
+| 内容类型 | 渲染方式                           |
+| -------- | ---------------------------------- |
+| 普通文本 | Markdown 渲染(支持代码高亮)      |
+| 工具调用 | 折叠卡片,显示工具名 + 参数 + 结果 |
+| 表格数据 | 自动识别 JSON 数组,渲染为表格     |
+| 错误信息 | 红色提示卡片                       |
+
+## 11. 安全考虑
+
+| 风险            | 措施                                                         |
+| --------------- | ------------------------------------------------------------ |
+| AI 误操作 Git   | 不配置 Git MCP Server                                        |
+| 敏感数据泄露    | Tools 只返回必要字段,不返回密码/token/密钥                  |
+| AI 误操作 Plane | create_feedback 限制只能创建 Intake,不能删除/修改已有 Issue |
+| MCP Server 崩溃 | 使用`server.WithRecovery()` 中间件恢复 panic                 |
+| 环境变量泄露    | 使用`${VAR}` 引用,不硬编码                                  |
+| 数据量过大      | Tools 返回结果限制最大条目数,分页查询                       |
+| Kiro 进程泄漏   | SessionManager 定时扫描超时进程并 kill                       |
+| 用户并发冲突    | 同一用户 Mutex 锁,防止并行调用 Kiro                         |
+| 磁盘占用        | 会话目录定期清理,限制历史文件大小                           |
+| Kiro 订阅额度   | 监控使用量,超额时返回友好提示                               |
+
+---
+
+## 12. 关于 A2A(原 ACP)
+
+当前阶段不需要 A2A。适合引入的场景:
+
+- 多个独立 AI Agent 协作(代码审查 Agent + 测试 Agent + 部署 Agent)
+- 跨团队/跨系统的 Agent 通信
+
+如果后续需要:
+
+- A2A Go SDK: `github.com/a2aproject/a2a-go`
+- A2A 规范: `https://a2aprotocol.ai`

+ 594 - 0
docs/kiro-cli-agent-implementation.md

@@ -0,0 +1,594 @@
+# FWH ToolBox Agent — Kiro CLI 配置实现方案
+
+> 创建日期:2026-08-04
+> 状态:方案设计(未实施)
+
+---
+
+## 目录
+
+1. [概述](#1-概述)
+2. [配置文件清单](#2-配置文件清单)
+3. [Agent 配置详解](#3-agent-配置详解)
+4. [MCP Server 接入](#4-mcp-server-接入)
+5. [知识库接入](#5-知识库接入)
+6. [权限控制与文件生成策略](#6-权限控制与文件生成策略)
+7. [Steering 引导文件](#7-steering-引导文件)
+8. [启动与使用方式](#8-启动与使用方式)
+9. [注意事项与限制](#9-注意事项与限制)
+10. [后续扩展计划](#10-后续扩展计划)
+
+---
+
+## 1. 概述
+
+### 1.1 目标
+
+创建名为 `fwh-toolbox-agent` 的 Kiro CLI Agent,用于:
+
+- 通过 MCP Server 查询 FranklinWH ToolBox 的业务数据(站点、SIM卡、告警、停机、功率流等)
+- 结合产品知识库和数据知识库,提供智能问答与数据分析
+- 生成数据统计报告或技术文档(Excel/PDF/Markdown),通过临时目录或 MinIO 提供下载
+- 提交问题反馈到 Plane 项目管理系统
+
+### 1.2 核心约束
+
+| 约束项         | 说明                                      |
+| -------------- | ----------------------------------------- |
+| 不可修改源码   | Agent 完全没有项目源码的写权限            |
+| 只读访问       | 可读取项目文件用于理解上下文              |
+| 可创建临时文件 | 仅限指定临时目录,用于导出数据/文档       |
+| MCP 数据接入   | 通过 stdio 模式的 MCP Server 获取业务数据 |
+| 知识库驱动     | 两套知识库:产品知识 + 数据字段解释       |
+
+### 1.3 技术架构
+
+```
+┌─────────────────────────────────────────────────┐
+│                Kiro CLI Chat                      │
+│            (fwh-toolbox-agent)                    │
+├─────────────────────────────────────────────────┤
+│  Tools: read, grep, glob, code, knowledge        │
+│  (无 write / shell 仅限安全命令)                  │
+├──────────┬──────────────┬───────────────────────┤
+│ MCP Server│  知识库(产品) │  知识库(数据)          │
+│ (stdio)   │  Wiki MD源码  │  docs/knowledge       │
+└──────────┴──────────────┴───────────────────────┘
+```
+
+---
+
+## 2. 配置文件清单
+
+实施时需要创建/修改的文件:
+
+```
+.kiro/
+├── agents/
+│   └── fwh-toolbox-agent.json    # Agent 主配置
+├── steering/
+│   └── fwh-toolbox-context.md    # 业务上下文引导文件
+├── settings/
+│   └── cli.json                  # Workspace 级设置(可选)
+└── skills/
+    └── data-export/
+        └── SKILL.md              # 数据导出技能(按需加载)
+```
+
+---
+
+## 3. Agent 配置详解
+
+### 3.1 配置文件位置
+
+```
+.kiro/agents/fwh-toolbox-agent.json
+```
+
+### 3.2 配置结构说明
+
+```jsonc
+{
+  "name": "fwh-toolbox-agent",
+  "description": "FranklinWH ToolBox 数据查询与分析助手,可查询站点/SIM卡/告警/停机数据,生成统计报告,提交问题反馈",
+  "prompt": "file://.kiro/prompts/fwh-toolbox-agent.md",
+
+  // === 可用工具 ===
+  "tools": [
+    "read",           // 读取文件
+    "grep",           // 搜索文件内容
+    "glob",           // 查找文件路径
+    "code",           // 代码智能(AST搜索)
+    "knowledge",      // 知识库检索
+    "shell",          // Shell(严格限制)
+    "web_search",     // 网页搜索(技术文档查找)
+    "web_fetch"       // 网页抓取
+  ],
+
+  // === 自动信任的工具(无需用户确认) ===
+  "allowedTools": [
+    "read",
+    "grep",
+    "glob",
+    "code",
+    "knowledge",
+    "web_search",
+    "web_fetch",
+    "@fwh-mcp/*"
+  ],
+
+  // === 工具细粒度设置 ===
+  "toolsSettings": {
+    "shell": {
+      "allowedCommands": [
+        "date",
+        "cat /tmp/fwh-export/.*",
+        "ls /tmp/fwh-export/.*",
+        "wc .*"
+      ],
+      "deniedCommands": [
+        "rm .*",
+        "mv .*",
+        "cp .*",
+        "sudo .*",
+        "chmod .*",
+        "chown .*",
+        "git .*",
+        "npm .*",
+        "pnpm .*",
+        "go .*"
+      ],
+      "denyByDefault": true
+    },
+    "web_fetch": {
+      "trusted": [
+        ".*docs\\.franklinwh\\.com.*",
+        ".*franklinwh\\.com.*"
+      ],
+      "blocked": [
+        ".*github\\.com.*"
+      ]
+    }
+  },
+
+  // === 上下文资源 ===
+  "resources": [
+    "file://docs/knowledge/**/*.md",
+    "skill://.kiro/skills/**/SKILL.md"
+  ],
+
+  // === MCP Server ===
+  "mcpServers": {
+    "fwh-mcp": {
+      "command": "/var/www/fwh-toolbox/server/fwh-mcp-server",
+      "args": [],
+      "env": {},
+      "timeout": 30000
+    }
+  },
+
+  // === 欢迎消息 ===
+  "welcomeMessage": "FWH ToolBox Agent 已就绪。我可以帮你查询站点信息、SIM卡状态、告警/停机统计、功率流数据,也可以生成数据报告或提交问题反馈。请问有什么需要?"
+}
+```
+
+### 3.3 字段说明
+
+| 字段              | 用途             | 备注                                     |
+| ----------------- | ---------------- | ---------------------------------------- |
+| `name`          | Agent 标识名     | 文件名必须与此一致(不含 .json)         |
+| `description`   | 简短描述         | 用于`agent list` 展示                  |
+| `prompt`        | 系统提示词       | 引用外部 md 文件,便于维护               |
+| `tools`         | 可用工具白名单   | **不包含 `write`**,确保无写权限 |
+| `allowedTools`  | 自动信任列表     | 所有 MCP tools 自动信任                  |
+| `toolsSettings` | 工具级别权限     | shell 严格限制为安全只读命令             |
+| `resources`     | 始终加载的上下文 | 数据知识库 + 技能文件                    |
+| `mcpServers`    | MCP 服务器定义   | stdio 模式本地二进制                     |
+
+### 3.4 关于 `write` 工具的缺失
+
+**设计决策**:`tools` 列表中不包含 `write` 工具,这意味着 Agent 完全无法创建或修改任何文件。
+
+**文件生成替代方案**(见第 6 节详述):
+
+- 通过 `shell` 的白名单命令写入 `/tmp/fwh-export/` 目录
+- 或扩展 MCP Server 增加文件导出相关 Tool
+
+---
+
+## 4. MCP Server 接入
+
+### 4.1 服务信息
+
+| 项目       | 值                                                      |
+| ---------- | ------------------------------------------------------- |
+| 二进制路径 | `/var/www/fwh-toolbox/server/fwh-mcp-server`          |
+| 通信模式   | stdio(标准输入/输出)                                  |
+| 协议       | MCP (Model Context Protocol)                            |
+| 依赖       | 数据库(MySQL/PostgreSQL/MongoDB)、Redis、配置文件(.env) |
+| 源码位置   | `service/cmd/mcp/main.go`                             |
+| 部署脚本   | `service/cmd/deploy_mcp.sh`                           |
+
+### 4.2 注册的 Tools 清单
+
+| Tool 名称                     | 功能描述                                                          | 必选参数                          | 可选参数                                                 |
+| ----------------------------- | ----------------------------------------------------------------- | --------------------------------- | -------------------------------------------------------- |
+| `query_site_info`           | 查询单个站点设备详细信息(版本、网络、激活状态、固件、SN 等)     | `sn` (站点序列号)               | —                                                       |
+| `search_sites`              | 搜索站点设备列表,支持分页和关键字                                | —                                | `page`, `size`, `keyword`                          |
+| `query_power_flow`          | 查询站点指定日期的功率流数据(PV/Grid/aPower/HomeLoad/Generator) | `sn`, `date` (yyyy-MM-dd)     | —                                                       |
+| `query_sim_card`            | 查询单张 SIM 卡详细信息(状态、平台、用量)                       | `giccid` (19位)                 | —                                                       |
+| `search_sim_cards`          | 搜索 SIM 卡列表,支持状态/平台过滤                                | —                                | `page`, `size`, `search`, `status`, `platform` |
+| `query_alarm_statistics`    | 查询告警统计数据(按日期汇总各类告警数量)                        | —                                | `date` (yyyy-MM-dd,不传则查最新)                      |
+| `query_shutdown_statistics` | 查询停机统计数据(按日期汇总停机站点信息)                        | —                                | `date` (yyyy-MM-dd,不传则查最新)                      |
+| `create_feedback`           | 在 Plane 创建 Intake Issue(问题反馈/需求提交)                   | `title`, `type` (bug/feature) | `priority`, `description`, `category`              |
+| `query_feedback_history`    | 查询 Plane Intake Issue 提交历史                                  | —                                | —                                                       |
+
+### 4.3 Agent 配置中的 MCP 定义
+
+```jsonc
+"mcpServers": {
+  "fwh-mcp": {
+    "command": "/var/www/fwh-toolbox/server/fwh-mcp-server",
+    "args": [],
+    "env": {
+      // 若 MCP Server 需要额外环境变量可在此配置
+      // 例如:"CONFIG_PATH": "/var/www/fwh-toolbox/server/.env"
+    },
+    "timeout": 30000
+  }
+}
+```
+
+### 4.4 注意事项
+
+1. **工作目录问题**:MCP Server 启动时的工作目录(cwd)决定了它能否找到 `.env` 配置文件。Kiro CLI 以 stdio 模式启动 MCP 进程时,cwd 默认为 Kiro CLI 的当前目录(即项目根目录),而非 `/var/www/fwh-toolbox/server`。
+
+   **解决方案**(任选其一):
+
+   - **(推荐)** 在 MCP Server 代码中用绝对路径加载配置,不依赖 cwd
+   - 将 `.env` 软链接到项目根目录
+   - 使用 wrapper script 先 cd 到目标目录再启动
+2. **进程冲突**:如果 MCP Server 已通过 systemd 开机自启在后台运行,Kiro CLI 再启动一个新进程可能导致端口/资源冲突。但由于 MCP Server 使用 stdio 模式(无端口监听),且数据库连接是独立的,所以**可以安全并行运行**。
+3. **超时配置**:默认 30 秒超时。若某些查询(如大范围搜索)耗时较长,可适当调大 `timeout` 值。
+4. **环境变量传递**:如果 MCP Server 的 `.env` 依赖 `GIN_MODE`、`DB_HOST` 等环境变量,需要在 `env` 字段中显式传递,或确保这些变量在 Kiro CLI 的 shell 环境中已设置。
+
+---
+
+## 5. 知识库接入
+
+### 5.1 两套知识库概览
+
+| 知识库     | 用途                              | 源路径                                                       | 格式     |
+| ---------- | --------------------------------- | ------------------------------------------------------------ | -------- |
+| 产品知识库 | FranklinWH 产品硬件/软件/系统知识 | `/home/jerry-remote/web/FranklinWH-Wiki/client/docs`       | Markdown |
+| 数据知识库 | 数据库字段解释、业务数据说明      | `/home/jerry-remote/web/FranklinWH-ToolBox/docs/knowledge` | Markdown |
+
+### 5.2 产品知识库
+
+**源路径**:`/home/jerry-remote/web/FranklinWH-Wiki/client/docs`
+
+**内容分类**:
+
+```
+docs/
+├── Hardware/          # 硬件知识
+│   ├── Gate/          #   aGate 系列(1.0, 1.2, 1.3, MSA_C)
+│   ├── Power/         #   aPower 系列(1.0, 1.2, 1.3, 2, S)
+│   └── Other/         #   其他配件(SplitCT, MeterKit, aPBox 等)
+├── Software/          # 软件知识
+│   ├── PCS.md         #   PCS 控制策略
+│   ├── EnergyStatistics.md  # 能量统计
+│   ├── ScriptingForSchedulingConfiguration.md  # 调度脚本
+│   └── ...            #   OTA、VPP、黑启动、电池加热等
+├── System/            # 系统知识
+│   ├── AlarmTable.md  #   告警代码表(重要!)
+│   ├── EventMessage.md#   事件消息列表
+│   ├── SystemConfig.md#   系统配置
+│   └── SystemDistribution.md  # 系统分布
+└── Other/             # 其他
+    ├── HostSideSoftware.md
+    └── TariffScheduleTemplates.md
+```
+
+**接入方式 — Kiro Knowledge 工具索引**:
+
+```bash
+# 在 Kiro CLI 中执行(首次需要手动索引)
+/knowledge add "FWH-Wiki-产品知识库" /home/jerry-remote/web/FranklinWH-Wiki/client/docs
+```
+
+**排除项**(索引时应排除):
+
+- `.vitepress/` — VitePress 配置和缓存
+- `public/` — 静态资源(PDF、PPT、图片)
+- `index.md` — 仅导航页,无实质内容
+- `casdoor/` — 认证相关,非产品知识
+
+**为什么选择 Markdown 源码而非 HTML**:
+
+- Markdown 语义清晰,AI 理解效率高
+- 文件体积小,索引速度快
+- 直接与源码仓库同步,无需构建
+- 无 HTML 标签/CSS/JS 噪音
+
+### 5.3 数据知识库
+
+**源路径**:`/home/jerry-remote/web/FranklinWH-ToolBox/docs/knowledge`
+
+**当前状态**:目录已创建,内容待补充。
+
+**规划内容**:
+
+- 数据库表字段含义说明
+- MCP Tool 返回数据字段解释
+- 业务术语表(SN、GICCID、告警码含义等)
+- 功率流数据解读指南
+- 常见查询场景示例
+
+**建议的文件结构**:
+
+```
+docs/knowledge/
+├── README.md                    # 知识库说明
+├── site-info-fields.md          # 站点信息字段解释
+├── power-flow-guide.md          # 功率流数据解读
+├── alarm-codes.md               # 告警代码与严重程度
+├── sim-card-status.md           # SIM卡状态说明
+├── shutdown-analysis.md         # 停机统计分析指南
+└── glossary.md                  # 业务术语表
+```
+
+**接入方式**:
+
+```bash
+# 数据知识库索引
+/knowledge add "FWH-数据知识库" /home/jerry-remote/web/FranklinWH-ToolBox/docs/knowledge
+```
+
+### 5.4 知识库配置建议(Workspace Settings)
+
+在 `.kiro/settings/cli.json` 中可配置知识库参数:
+
+```jsonc
+{
+  "knowledge.defaultIncludePatterns": ["**/*.md"],
+  "knowledge.defaultExcludePatterns": [
+    "**/node_modules/**",
+    "**/.vitepress/**",
+    "**/public/**",
+    "**/cache/**"
+  ],
+  "knowledge.indexType": "best",   // 语义搜索(推荐)
+  "knowledge.chunkSize": 1000,
+  "knowledge.chunkOverlap": 200
+}
+```
+
+### 5.5 Agent resources 配置
+
+对于始终需要加载的关键知识(如术语表),可通过 `resources` 字段直接引入:
+
+```jsonc
+"resources": [
+  "file://docs/knowledge/**/*.md",           // 数据知识库全量加载
+  "skill://.kiro/skills/**/SKILL.md"         // 技能按需加载
+]
+```
+
+> **注意**:产品知识库文件较多且体积大,不适合通过 `resources` 全量加载到上下文,应通过 `knowledge` 工具按需检索。
+
+---
+
+## 6. 权限控制与文件生成策略
+
+### 6.1 权限模型
+
+```
+┌─────────────────────────────────────────────────────────┐
+│                    权限控制总览                            │
+├─────────────────────┬───────────────────────────────────┤
+│ 读取项目文件         │ ✅ 允许(read/grep/glob/code)     │
+│ 修改项目源码         │ ❌ 禁止(不包含 write 工具)       │
+│ 执行 shell 命令     │ ⚠️  严格白名单(denyByDefault)    │
+│ 调用 MCP Tools      │ ✅ 允许(自动信任)                 │
+│ 搜索知识库          │ ✅ 允许(自动信任)                 │
+│ 创建临时文件        │ ✅ 仅限 /tmp/fwh-export/ 目录      │
+│ 上传到 MinIO        │ ✅ 通过扩展 MCP Tool 实现          │
+└─────────────────────┴───────────────────────────────────┘
+```
+
+### 6.2 文件生成方案
+
+由于 Agent 没有 `write` 工具,不能直接创建文件。需要通过以下两种方案实现文件生成:
+
+#### 方案 A:Shell 白名单 + 临时目录(轻量方案)
+
+**原理**:在 `shell` 工具的 `allowedCommands` 中加入写入临时目录的命令。
+
+**配置**:
+
+```jsonc
+"toolsSettings": {
+  "shell": {
+    "allowedCommands": [
+      "mkdir -p /tmp/fwh-export",
+      "tee /tmp/fwh-export/.*",
+      "cat .* > /tmp/fwh-export/.*",
+      "ls -la /tmp/fwh-export/.*",
+      "date",
+      "wc .*"
+    ],
+    "deniedCommands": [
+      "rm -rf .*",
+      "sudo .*",
+      "chmod .*",
+      "chown .*"
+    ],
+    "denyByDefault": true
+  }
+}
+```
+
+**限制**:
+
+- 只能生成纯文本/Markdown/CSV 文件
+- 无法生成 Excel(.xlsx) 或 PDF 二进制文件
+- 用户需要通过 `scp` 或 SSH 文件浏览器下载
+
+#### 方案 B:扩展 MCP Server(推荐方案)
+
+**原理**:在 MCP Server 中新增文件生成相关 Tool,由 Go 后端调用 `excelize`(Excel)或其他库生成文件,上传到 MinIO 并返回下载链接。
+
+**需要新增的 MCP Tools**:
+
+| Tool 名称          | 功能                     | 参数                                                                    |
+| ------------------ | ------------------------ | ----------------------------------------------------------------------- |
+| `export_report`  | 生成数据统计报告         | `type`(excel/pdf/csv), `template`, `data_query`, `expire_hours` |
+| `get_export_url` | 获取已生成文件的下载链接 | `file_id`                                                             |
+| `list_exports`   | 列出已生成的导出文件     | `page`, `size`                                                      |
+
+**实现示例(伪代码)**:
+
+```go
+// service/cmd/mcp/tools/export.go
+func RegisterExportTools(s *server.MCPServer) {
+    s.AddTool(
+        mcp.NewTool("export_report",
+            mcp.WithDescription("生成数据统计报告并上传到 MinIO,返回有时效的下载链接"),
+            mcp.WithString("type", mcp.Required(), mcp.Description("文件类型:excel, pdf, csv, markdown")),
+            mcp.WithString("title", mcp.Required(), mcp.Description("报告标题")),
+            mcp.WithString("content", mcp.Required(), mcp.Description("报告内容(JSON 结构化数据或 Markdown 文本)")),
+            mcp.WithNumber("expire_hours", mcp.Description("下载链接过期时间(小时),默认 24")),
+        ),
+        handleExportReport,
+    )
+}
+
+func handleExportReport(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+    // 1. 解析参数
+    // 2. 根据 type 生成文件(excelize / go-pdf / csv / md)
+    // 3. 上传到 MinIO(设置过期策略)
+    // 4. 生成预签名下载 URL(带 expire_hours 过期)
+    // 5. 返回下载 URL 给 Agent
+}
+```
+
+**MinIO 过期策略**:
+
+- 使用 MinIO 的 Presigned URL(`GetPresignedURL`),URL 自带过期时间
+- 或配置 Bucket Lifecycle Rule,自动清理 N 天前的临时文件
+- 建议 Bucket 名:`fwh-export-temp`,Lifecycle 设为 7 天自动删除
+
+**优势**:
+
+- 可生成 Excel/PDF 等二进制格式
+- 返回可直接访问的下载链接
+- 过期自动清理,不占用长期存储
+- Agent 只需描述要导出的数据,后端负责格式化
+
+### 6.3 推荐实施路径
+
+```
+第一阶段:使用方案 A(shell 白名单写入临时目录)
+    ↓ 验证 Agent 工作流可行
+第二阶段:实现方案 B(MCP export_report Tool)
+    ↓ 支持 Excel/PDF 二进制文件生成
+第三阶段:前端集成下载入口(可选)
+    ↓ ToolBox Web UI 中展示导出历史和下载链接
+```
+
+### 6.4 提示词中的权限引导
+
+在 Agent 的系统提示词中需要明确告知权限限制:
+
+```markdown
+## 权限说明
+- 你没有写入项目文件的权限。如果用户要求修改代码,告知没有操作权限。
+- 你可以通过 export_report 工具生成数据报告,会返回带过期时间的下载链接。
+- 如果 export_report 工具不可用,你可以将内容输出为 Markdown 格式直接展示在对话中。
+- 任何需要持久化的操作都应通过 MCP Tools 完成。
+```
+
+---
+
+## 7. Steering 引导文件
+
+### 7.1 文件位置
+
+```
+.kiro/steering/fwh-toolbox-context.md
+```
+
+### 7.2 内容规划
+
+```markdown
+# FWH ToolBox Agent 业务上下文
+
+## 产品背景
+FranklinWH 是一家家用储能系统公司,核心产品包括:
+- aGate:家庭能源管理网关(1.0/1.2/1.3/MSA_C 系列)
+- aPower:储能逆变器(1.0/1.2/1.3/2/S 系列)
+- 其他配件:SplitCT、MeterKit、aPBox、Generator Module
+
+## ToolBox 平台说明
+FWH ToolBox 是内部运维工具平台,提供:
+- 站点设备信息管理与查询
+- SIM 卡管理(SmartHub / Acceleronix 平台)
+- 告警/停机统计分析
+- 功率流数据可视化
+- 固件配置管理
+- 批量数据下载
+- 问题反馈提交(对接 Plane 项目管理)
+
+## 数据查询约定
+- 站点通过 SN(序列号)唯一标识
+- SIM 卡通过 GICCID(19位)唯一标识
+- 日期格式统一为 yyyy-MM-dd
+- 分页默认 page=1, size=10
+
+## 回复规范
+- 数据查询结果以结构化表格展示
+- 告警代码需关联知识库中的 AlarmTable 解释含义
+- 功率流数据回复时注明单位(W/kW/kWh)
+- 涉及多个站点时优先汇总统计,再按需展开详情
+```
+
+### 7.3 Prompt 文件
+
+建议创建独立的 prompt 文件,Agent 配置中通过 `file://` 引用:
+
+**文件位置**:`.kiro/prompts/fwh-toolbox-agent.md`
+
+**内容要点**:
+
+```markdown
+# FWH ToolBox Agent
+
+你是 FranklinWH ToolBox 平台的数据查询与分析助手。
+
+## 核心能力
+1. 通过 MCP Tools 查询站点、SIM卡、告警、停机、功率流等业务数据
+2. 结合产品知识库解答 FranklinWH 产品相关技术问题
+3. 生成数据统计报告(Excel/PDF/Markdown)
+4. 向 Plane 提交问题反馈和功能需求
+
+## 权限约束
+- ❌ 不能修改任何项目源码文件
+- ❌ 不能执行危险 shell 命令
+- ✅ 可以读取项目文件以理解代码逻辑
+- ✅ 可以通过 MCP Tools 查询和操作业务数据
+- ✅ 可以生成报告到临时目录或 MinIO
+
+## 工作流程
+1. 理解用户需求
+2. 如需产品知识 → 检索知识库
+3. 如需业务数据 → 调用 MCP Tools
+4. 如需生成文件 → 调用 export_report 或输出 Markdown
+5. 组织结果,清晰回复
+
+## 回复风格
+- 使用中文回复
+- 数据展示优先使用表格格式
+- 技术解释简洁清晰
+- 不确定的信息明确标注
+```

+ 1269 - 0
docs/kiro-cli-configuration-guide.md

@@ -0,0 +1,1269 @@
+# Kiro CLI 运行环境配置详细指南
+
+本文档详细描述 Kiro CLI 的所有配置项、配置文件位置、参数说明及示例。
+
+---
+
+## 目录
+
+1. [配置体系概览](#1-配置体系概览)
+2. [目录结构](#2-目录结构)
+3. [全局设置 (cli.json)](#3-全局设置-clijson)
+4. [Agent 配置](#4-agent-配置)
+5. [Steering 文件(引导文件)](#5-steering-文件引导文件)
+6. [Skills 技能文件](#6-skills-技能文件)
+7. [Hooks 钩子系统](#7-hooks-钩子系统)
+8. [MCP 服务器配置](#8-mcp-服务器配置)
+9. [LSP 代码智能配置](#9-lsp-代码智能配置)
+10. [环境变量](#10-环境变量)
+11. [常用命令速查](#11-常用命令速查)
+
+---
+
+## 1. 配置体系概览
+
+Kiro CLI 的配置分为多个层级,优先级从高到低:
+
+```
+Session(会话级)> Workspace(工作区级)> Global(全局级)
+```
+
+| 层级      | 说明                       | 适用范围   |
+| --------- | -------------------------- | ---------- |
+| Session   | 当前会话临时设置           | 仅当前会话 |
+| Workspace | 项目目录下的`.kiro/`     | 仅当前项目 |
+| Global    | 用户主目录下的`~/.kiro/` | 所有项目   |
+
+---
+
+## 2. 目录结构
+
+### 全局目录 `~/.kiro/`
+
+```
+~/.kiro/
+├── agents/                    # 全局 Agent 配置
+│   ├── my-agent.json
+│   └── rust-expert.json
+├── prompts/                   # 全局提示词文件
+│   └── rust-expert.txt
+├── skills/                    # 全局技能文件
+│   └── my-skill/
+│       └── SKILL.md
+├── steering/                  # 全局引导文件
+│   ├── coding-standards.md
+│   └── preferences.md
+├── settings/
+│   ├── cli.json               # 全局设置
+│   └── mcp.json               # 全局 MCP 配置
+├── hooks/                     # 钩子脚本存放目录(可自定义)
+│   └── validate-write.sh
+├── sessions/cli/              # 保存的聊天会话
+└── .cli_bash_history          # Shell 命令历史
+```
+
+### 工作区目录 `.kiro/`(项目根目录下)
+
+```
+.kiro/
+├── agents/                    # 工作区 Agent 配置(优先级高于全局)
+│   └── project-helper.json
+├── skills/                    # 工作区技能文件
+│   └── db-helper/
+│       └── SKILL.md
+├── steering/                  # 工作区引导文件
+│   └── team-conventions.md
+└── settings/
+    ├── cli.json               # 工作区设置(覆盖全局)
+    └── mcp.json               # 工作区 MCP 配置
+```
+
+### 项目根目录其他配置文件
+
+```
+项目根目录/
+├── AGENTS.md                  # 项目 Agent 指令(自动加载)
+├── README.md                  # 项目说明(自动加载)
+├── AmazonQ.md                 # 旧版项目指令(如存在则加载)
+├── lsp.json                   # LSP 代码智能配置
+└── .amazonq/                  # 旧版配置(向后兼容)
+    ├── mcp.json
+    └── rules/**/*.md
+```
+
+---
+
+## 3. 全局设置 (cli.json)
+
+### 文件位置
+
+| 作用域 | 文件路径                      |
+| ------ | ----------------------------- |
+| 全局   | `~/.kiro/settings/cli.json` |
+| 工作区 | `.kiro/settings/cli.json`   |
+
+### 完整设置项列表
+
+#### 3.1 Chat 聊天相关
+
+| 设置项                                     | 类型    | 默认值      | 说明                                      |
+| ------------------------------------------ | ------- | ----------- | ----------------------------------------- |
+| `chat.defaultAgent`                      | string  | _(无)_    | 新会话使用的默认 Agent 名称               |
+| `chat.defaultModel`                      | string  | _(无)_    | 默认 AI 模型 ID                           |
+| `chat.enableThinking`                    | boolean | -           | 启用思考工具                              |
+| `chat.showThinking`                      | boolean | `false`   | 显示思考/推理块(仅启动时生效)           |
+| `chat.enableKnowledge`                   | boolean | -           | 启用知识库功能                            |
+| `chat.enableCodeIntelligence`            | boolean | -           | 启用代码智能(LSP)                       |
+| `chat.enableSubagent`                    | boolean | -           | 启用子 Agent 功能                         |
+| `chat.enableTodoList`                    | boolean | -           | 启用待办列表功能                          |
+| `chat.enableNotifications`               | boolean | -           | 启用桌面通知                              |
+| `chat.greeting.enabled`                  | boolean | -           | 启动时显示欢迎消息                        |
+| `chat.allowAnimations`                   | boolean | `true`    | 启用加载动画(旋转、进度条)              |
+| `chat.allowAsciiArt`                     | boolean | `true`    | 启用 Unicode 符号,false 则降级为纯 ASCII |
+| `chat.allowIcons`                        | boolean | `true`    | 显示状态指示图标                          |
+| `chat.terminalTitle`                     | boolean | `false`   | 通过 OSC 0 更新终端窗口标题               |
+| `chat.disableAutoCompaction`             | boolean | -           | 禁用自动上下文压缩                        |
+| `chat.disableGranularTrust`              | boolean | -           | 禁用细粒度信任选项                        |
+| `chat.disableInheritingDefaultResources` | boolean | `false`   | 禁用自定义 Agent 继承默认资源             |
+| `chat.autoExpandToolOutput`              | boolean | -           | 始终展示完整工具输出                      |
+| `chat.disableWrap`                       | boolean | -           | 禁用聊天输出自动换行                      |
+| `chat.historyMode`                       | string  | `session` | 提示历史范围:`session` 或 `global`   |
+
+#### 3.2 快捷键
+
+| 设置项                            | 类型   | 默认值     | 说明           |
+| --------------------------------- | ------ | ---------- | -------------- |
+| `chat.keybindings.cancelStream` | string | `esc`    | 取消流式输出   |
+| `chat.keybindings.closeMenu`    | string | `esc`    | 关闭菜单/面板  |
+| `chat.keybindings.quit`         | string | `ctrl+c` | 退出(需双击) |
+
+快捷键语法:`修饰符+键`,大小写不敏感。
+
+- 修饰符:`ctrl`、`shift`、`alt`(别名 `meta`、`cmd`)
+- 键名:`esc`、`enter`、`tab`、`up`/`down`/`left`/`right`、`space`、`backspace`、`delete`、`home`、`end`、`pageup`/`pagedown`、`a-z`、`0-9`
+
+#### 3.3 遥测
+
+| 设置项                | 类型    | 默认值 | 说明                                                |
+| --------------------- | ------- | ------ | --------------------------------------------------- |
+| `telemetry.enabled` | boolean | -      | 启用/禁用遥测(**全局设置,不可工作区覆盖**) |
+
+#### 3.4 知识库
+
+| 设置项                               | 类型   | 默认值 | 说明                                              |
+| ------------------------------------ | ------ | ------ | ------------------------------------------------- |
+| `knowledge.defaultIncludePatterns` | array  | -      | 索引时包含的文件 glob 模式                        |
+| `knowledge.defaultExcludePatterns` | array  | -      | 索引时排除的文件 glob 模式                        |
+| `knowledge.maxFiles`               | number | -      | 索引的最大文件数                                  |
+| `knowledge.chunkSize`              | number | -      | 文本分块大小                                      |
+| `knowledge.chunkOverlap`           | number | -      | 分块之间的重叠大小                                |
+| `knowledge.indexType`              | string | -      | 索引类型:`fast`(BM25)或 `best`(语义搜索) |
+
+#### 3.5 API
+
+| 设置项                        | 类型   | 默认值 | 说明                                     |
+| ----------------------------- | ------ | ------ | ---------------------------------------- |
+| `api.timeout`               | number | -      | API 请求超时(毫秒)                     |
+| `api.codewhisperer.service` | string | -      | CodeWhisperer 端点(**全局设置**) |
+| `api.q.service`             | string | -      | Q 服务端点(**全局设置**)         |
+
+#### 3.6 MCP
+
+| 设置项                       | 类型   | 默认值 | 说明                         |
+| ---------------------------- | ------ | ------ | ---------------------------- |
+| `mcp.initTimeout`          | number | -      | MCP 服务器初始化超时(毫秒) |
+| `mcp.noInteractiveTimeout` | number | -      | 非交互式 MCP 超时(毫秒)    |
+
+#### 3.7 上下文压缩
+
+| 设置项                                     | 类型   | 默认值 | 说明                           |
+| ------------------------------------------ | ------ | ------ | ------------------------------ |
+| `compaction.excludeContextWindowPercent` | number | -      | 从压缩中排除的上下文窗口百分比 |
+| `compaction.excludeMessages`             | number | -      | 从压缩中排除的最近消息数量     |
+
+#### 3.8 Hooks
+
+| 设置项               | 类型    | 默认值 | 说明                                            |
+| -------------------- | ------- | ------ | ----------------------------------------------- |
+| `hooks.showStatus` | boolean | -      | 是否显示 hook 执行状态(设为 false 则静默运行) |
+
+### cli.json 完整示例
+
+**文件位置:`~/.kiro/settings/cli.json`**
+
+```json
+{
+  "chat.defaultAgent": "my-vue-agent",
+  "chat.defaultModel": "anthropic.claude-3-5-sonnet-20241022-v2:0",
+  "chat.enableThinking": true,
+  "chat.showThinking": false,
+  "chat.enableKnowledge": true,
+  "chat.enableCodeIntelligence": true,
+  "chat.enableSubagent": true,
+  "chat.enableTodoList": true,
+  "chat.enableNotifications": true,
+  "chat.greeting.enabled": true,
+  "chat.allowAnimations": true,
+  "chat.allowAsciiArt": true,
+  "chat.allowIcons": true,
+  "chat.terminalTitle": false,
+  "chat.disableAutoCompaction": false,
+  "chat.disableGranularTrust": false,
+  "chat.disableInheritingDefaultResources": false,
+  "chat.autoExpandToolOutput": false,
+  "chat.disableWrap": false,
+  "chat.historyMode": "global",
+  "chat.keybindings.cancelStream": "esc",
+  "chat.keybindings.closeMenu": "esc",
+  "chat.keybindings.quit": "ctrl+c",
+  "telemetry.enabled": true,
+  "knowledge.defaultIncludePatterns": ["**/*.ts", "**/*.vue", "**/*.md"],
+  "knowledge.defaultExcludePatterns": ["node_modules/**", "dist/**"],
+  "knowledge.maxFiles": 1000,
+  "knowledge.chunkSize": 512,
+  "knowledge.chunkOverlap": 50,
+  "knowledge.indexType": "best",
+  "api.timeout": 30000,
+  "mcp.initTimeout": 120000,
+  "mcp.noInteractiveTimeout": 30000,
+  "compaction.excludeContextWindowPercent": 10,
+  "compaction.excludeMessages": 5
+}
+```
+
+---
+
+## 4. Agent 配置
+
+### 文件位置
+
+| 作用域 | 文件路径                       | 优先级                   |
+| ------ | ------------------------------ | ------------------------ |
+| 工作区 | `.kiro/agents/<name>.json`   | 高(覆盖同名全局 Agent) |
+| 全局   | `~/.kiro/agents/<name>.json` | 低                       |
+
+文件名(不含 `.json`)即为 Agent 名称。
+
+### 所有字段说明
+
+| 字段                 | 类型         | 必填 | 说明                                                      |
+| -------------------- | ------------ | ---- | --------------------------------------------------------- |
+| `name`             | string       | 否   | Agent 名称标识                                            |
+| `description`      | string       | 否   | 人类可读描述                                              |
+| `prompt`           | string       | 否   | 系统提示词(支持内联文本或`file:///path` 引用外部文件) |
+| `model`            | string       | 否   | 指定模型 ID,未指定则使用默认模型                         |
+| `tools`            | array        | 否   | 可用工具列表                                              |
+| `allowedTools`     | array        | 否   | 自动批准的工具(无需用户确认),支持通配符                |
+| `toolsSettings`    | object       | 否   | 工具级别的详细配置                                        |
+| `resources`        | array        | 否   | 上下文资源列表                                            |
+| `hooks`            | object/array | 否   | 触发器钩子配置                                            |
+| `mcpServers`       | object       | 否   | MCP 服务器配置                                            |
+| `toolAliases`      | object       | 否   | 工具名称重映射(解决命名冲突)                            |
+| `useLegacyMcpJson` | boolean      | 否   | 是否加载旧版`mcp.json` 配置                             |
+| `keyboardShortcut` | string       | 否   | 快速切换到此 Agent 的快捷键                               |
+| `welcomeMessage`   | string       | 否   | 切换到此 Agent 时显示的欢迎消息                           |
+
+### 内置工具列表
+
+以下工具可在 `tools` 字段中使用:
+
+| 工具名         | 别名                              | 说明                      |
+| -------------- | --------------------------------- | ------------------------- |
+| `read`       | `fs_read`, `fsRead`           | 读取文件/目录/图片        |
+| `write`      | `fs_write`, `fsWrite`         | 创建/编辑文件             |
+| `shell`      | `execute_bash`, `execute_cmd` | 执行终端命令              |
+| `grep`       | -                                 | 正则文本搜索              |
+| `glob`       | -                                 | 文件路径模式匹配          |
+| `code`       | -                                 | 代码智能(AST 搜索、LSP) |
+| `use_aws`    | -                                 | AWS CLI API 调用          |
+| `web_fetch`  | `webFetch`                      | 获取 URL 内容             |
+| `web_search` | -                                 | 网络搜索                  |
+| `knowledge`  | -                                 | 知识库索引和搜索          |
+| `subagent`   | `agent_crew`, `use_subagent`  | 子 Agent 流水线           |
+| `todo_list`  | `task`, `todo`                | 待办任务管理              |
+| `introspect` | -                                 | 查询 Kiro CLI 自身文档    |
+
+特殊值:
+
+- `"*"` — 所有工具(仅用于 `tools` 字段)
+- `"@builtin"` — 所有内置工具(可用于 `allowedTools`)
+
+### tools 与 allowedTools 的区别
+
+- **`tools`**:Agent 能使用哪些工具(能力范围)
+- **`allowedTools`**:哪些工具使用时无需用户确认(信任范围)
+
+### allowedTools 通配符模式
+
+```json
+{
+  "allowedTools": [
+    "read",              // 精确匹配内置工具
+    "fs_*",             // 前缀通配:匹配 fs_read, fs_write
+    "@git/git_status",  // 精确匹配 MCP 工具
+    "@server/read_*",   // MCP 工具通配
+    "@git",             // 该 MCP 服务器的所有工具
+    "@builtin"          // 所有内置工具
+  ]
+}
+```
+
+### toolsSettings 详细配置
+
+#### write 工具设置
+
+```json
+{
+  "toolsSettings": {
+    "write": {
+      "allowedPaths": ["src/**", "tests/**"],
+      "deniedPaths": ["node_modules/**", "/etc/**"]
+    }
+  }
+}
+```
+
+| 参数             | 类型  | 说明                     |
+| ---------------- | ----- | ------------------------ |
+| `allowedPaths` | array | 允许写入的路径 glob 模式 |
+| `deniedPaths`  | array | 禁止写入的路径 glob 模式 |
+
+#### shell 工具设置
+
+```json
+{
+  "toolsSettings": {
+    "shell": {
+      "allowedCommands": ["git status", "npm test", "pnpm build"],
+      "deniedCommands": ["rm -rf *", "sudo *", "shutdown"],
+      "autoAllowReadonly": true,
+      "denyByDefault": false
+    }
+  }
+}
+```
+
+| 参数                  | 类型    | 默认值    | 说明                                                  |
+| --------------------- | ------- | --------- | ----------------------------------------------------- |
+| `allowedCommands`   | array   | -         | 自动批准的命令(正则模式)                            |
+| `deniedCommands`    | array   | -         | 始终拒绝的命令(正则模式)                            |
+| `autoAllowReadonly` | boolean | -         | 自动批准只读命令(如`ls`、`cat`、`git status`) |
+| `denyByDefault`     | boolean | `false` | 默认拒绝未在 allowedCommands 中的命令                 |
+
+#### web_fetch 工具设置
+
+```json
+{
+  "toolsSettings": {
+    "web_fetch": {
+      "trusted": [".*docs\\.aws\\.amazon\\.com.*", ".*vuejs\\.org.*"],
+      "blocked": [".*pastebin\\.com.*"]
+    }
+  }
+}
+```
+
+| 参数        | 类型  | 说明                                          |
+| ----------- | ----- | --------------------------------------------- |
+| `trusted` | array | 自动允许的 URL 正则模式                       |
+| `blocked` | array | 始终阻止的 URL 正则模式(优先级高于 trusted) |
+
+### resources 资源配置
+
+支持两种 URI 协议:
+
+| 协议         | 加载方式 | 说明                                         |
+| ------------ | -------- | -------------------------------------------- |
+| `file://`  | 始终加载 | 内容始终存在于上下文窗口                     |
+| `skill://` | 按需加载 | 仅元数据(名称、描述)初始加载,内容按需加载 |
+
+```json
+{
+  "resources": [
+    "file://README.md",
+    "file://src/**/*.vue",
+    "file://docs/api.md",
+    "skill://.kiro/skills/**/SKILL.md"
+  ]
+}
+```
+
+### 默认资源继承
+
+自定义 Agent 默认会自动继承以下资源:
+
+| 资源                                  | 说明               |
+| ------------------------------------- | ------------------ |
+| `file://AGENTS.md`                  | 项目 Agent 指令    |
+| `file://README.md`                  | 项目说明           |
+| `skill://.kiro/skills/*/SKILL.md`   | 工作区技能         |
+| `skill://~/.kiro/skills/*/SKILL.md` | 全局技能           |
+| `file://~/.kiro/steering/**/*.md`   | 全局引导文件       |
+| `file://.kiro/steering/**/*.md`     | 工作区引导文件     |
+| `file://AmazonQ.md`                 | 旧版指令(如存在) |
+
+设置 `chat.disableInheritingDefaultResources` 为 `true` 可禁用此继承。
+
+### Agent 完整示例
+
+**文件位置:`.kiro/agents/vue-dev.json`**
+
+```json
+{
+  "name": "vue-dev",
+  "description": "Vue 3 + TypeScript 开发助手",
+  "prompt": "你是一个 Vue 3 + TypeScript 专家。使用 Composition API、Naive UI 组件库和 UnoCSS。遵循项目现有的代码风格和架构。",
+  "model": "anthropic.claude-3-5-sonnet-20241022-v2:0",
+  "tools": [
+    "read", "write", "shell", "grep", "glob", "code", "web_fetch", "knowledge"
+  ],
+  "allowedTools": [
+    "read", "grep", "glob", "code"
+  ],
+  "toolsSettings": {
+    "write": {
+      "allowedPaths": ["src/**", "docs/**", "tests/**"],
+      "deniedPaths": ["node_modules/**", "dist/**"]
+    },
+    "shell": {
+      "allowedCommands": [
+        "pnpm (dev|build|test|lint)",
+        "git (status|diff|log|branch)",
+        "npx vue-tsc"
+      ],
+      "deniedCommands": ["rm -rf", "sudo"],
+      "autoAllowReadonly": true
+    },
+    "web_fetch": {
+      "trusted": [".*vuejs\\.org.*", ".*naiveui\\.com.*"],
+      "blocked": [".*pastebin\\.com.*"]
+    }
+  },
+  "resources": [
+    "file://src/**/*.vue",
+    "file://src/**/*.ts",
+    "file://package.json",
+    "file://tsconfig.json",
+    "skill://.kiro/skills/**/SKILL.md"
+  ],
+  "hooks": {
+    "agentSpawn": [
+      { "command": "git branch --show-current && node -v" }
+    ],
+    "preToolUse": [
+      {
+        "matcher": "write",
+        "command": "echo 'Writing file...'"
+      }
+    ]
+  },
+  "mcpServers": {
+    "git": {
+      "command": "mcp-server-git",
+      "args": ["--stdio"]
+    }
+  },
+  "keyboardShortcut": "ctrl+shift+v",
+  "welcomeMessage": "Vue 3 开发助手已就绪!有什么可以帮你的?"
+}
+```
+
+---
+
+## 5. Steering 文件(引导文件)
+
+Steering 文件是持久化的指令文档,用于在所有会话中引导 Agent 行为。
+
+### 文件位置
+
+| 作用域 | 目录路径              |
+| ------ | --------------------- |
+| 工作区 | `.kiro/steering/`   |
+| 全局   | `~/.kiro/steering/` |
+
+目录会被递归扫描,任何层级的 `.md` 文件都会被加载。
+
+### 加载规则
+
+- **内置默认 Agent (`kiro_default`)**:自动加载 steering 文件
+- **自定义 Agent**:需要手动在 `resources` 中声明:
+  ```json
+  { "resources": ["file://.kiro/steering/**/*.md"] }
+  ```
+
+### inclusion 控制
+
+Steering 文件支持可选的 YAML frontmatter 来控制加载时机:
+
+| inclusion 值         | 行为                                   |
+| -------------------- | -------------------------------------- |
+| `always`           | 始终加载(默认,等同于无 frontmatter) |
+| `fileMatch`        | 仅在处理匹配文件时加载(排除自动加载) |
+| `manual`           | 仅通过`/context add` 手动加载        |
+| _(无 frontmatter)_ | 始终加载                               |
+
+### Steering 文件示例
+
+**文件位置:`.kiro/steering/coding-standards.md`**
+
+```markdown
+# 项目编码规范
+
+## Vue 组件
+- 使用 Composition API + `<script setup>` 语法
+- 组件命名使用 PascalCase
+- Props 必须使用 TypeScript 类型定义
+- 使用 Naive UI 组件库
+
+## TypeScript
+- 严格模式,禁止 `any`
+- 函数参数和返回值必须有类型标注
+- 使用 `es-toolkit` 代替 lodash
+
+## 样式
+- 使用 UnoCSS 原子类
+- 避免写自定义 CSS,除非必要
+- 暗色模式必须适配
+```
+
+**文件位置:`~/.kiro/steering/personal-preferences.md`**
+
+```markdown
+# 个人偏好
+
+- 回答使用中文
+- 代码注释使用英文
+- 优先给出完整可运行的代码
+- 修改代码前先阅读现有实现
+- 保持简洁,不要过度设计
+```
+
+**带 inclusion 控制的示例:`.kiro/steering/vue-rules.md`**
+
+```markdown
+---
+inclusion: always
+---
+
+# Vue 开发规则
+
+- 所有组件必须定义 `name` 选项
+- 使用 `defineProps` 和 `defineEmits` 宏
+- 大型表单使用 Naive UI 的 NForm
+```
+
+---
+
+## 6. Skills 技能文件
+
+Skills 是按需加载的资源,启动时仅加载元数据,实际内容在需要时才加载,节省上下文窗口空间。
+
+### 文件位置
+
+| 作用域 | 目录路径                                 |
+| ------ | ---------------------------------------- |
+| 工作区 | `.kiro/skills/<skill-name>/SKILL.md`   |
+| 全局   | `~/.kiro/skills/<skill-name>/SKILL.md` |
+
+### 格式要求
+
+Skill 文件**必须**以 YAML frontmatter 开头:
+
+```markdown
+---
+name: vue-component-patterns
+description: Vue 3 组件设计模式指南。在设计新组件或重构现有组件时使用。
+---
+
+# Vue 3 组件设计模式
+
+## Compound Components Pattern
+
+...详细内容...
+```
+
+| frontmatter 字段 | 必填 | 说明                                     |
+| ---------------- | ---- | ---------------------------------------- |
+| `name`         | 是   | 技能名称                                 |
+| `description`  | 是   | 描述(Agent 根据此决定是否加载完整内容) |
+
+### 在 Agent 中引用 Skills
+
+```json
+{
+  "resources": [
+    "skill://.kiro/skills/**/SKILL.md",
+    "skill://~/.kiro/skills/**/SKILL.md"
+  ]
+}
+```
+
+### Skills 示例
+
+**文件位置:`.kiro/skills/naive-ui-patterns/SKILL.md`**
+
+```markdown
+---
+name: naive-ui-patterns
+description: Naive UI 组件库常用模式和最佳实践。在使用 Naive UI 组件开发界面时参考。
+---
+
+# Naive UI 常用模式
+
+## 表格 (DataTable)
+
+使用 `NDataTable` 时的标准模式:
+
+\`\`\`vue
+<script setup lang="ts">
+import { NDataTable } from 'naive-ui'
+import type { DataTableColumns } from 'naive-ui'
+
+interface RowData {
+  id: string
+  name: string
+}
+
+const columns: DataTableColumns<RowData> = [
+  { title: 'ID', key: 'id' },
+  { title: '名称', key: 'name' }
+]
+</script>
+\`\`\`
+
+## 表单 (Form)
+
+...
+```
+
+---
+
+## 7. Hooks 钩子系统
+
+Hooks 允许在 Agent 生命周期的特定时间点执行自定义命令。
+
+### 配置位置
+
+Hooks 配置在 **Agent 配置文件** 中(`.kiro/agents/<name>.json` 或 `~/.kiro/agents/<name>.json`)。
+
+### 触发时机
+
+| 触发器               | 时机                 | 可否阻止工具执行       |
+| -------------------- | -------------------- | ---------------------- |
+| `agentSpawn`       | Agent 初始化时       | -                      |
+| `userPromptSubmit` | 用户提交消息时       | -                      |
+| `preToolUse`       | 工具执行前           | ✅ 可通过退出码 2 阻止 |
+| `postToolUse`      | 工具执行后           | -                      |
+| `stop`             | Assistant 完成回复时 | -                      |
+
+### 退出码含义
+
+| 退出码 | 含义                                             |
+| ------ | ------------------------------------------------ |
+| `0`  | 成功,STDOUT 加入 Agent 上下文                   |
+| `2`  | (仅 preToolUse)阻止工具执行,STDERR 返回给 LLM |
+| 其他   | 失败,STDERR 作为警告显示给用户                  |
+
+### 两种配置格式
+
+#### Object 格式(推荐)
+
+```json
+{
+  "hooks": {
+    "agentSpawn": [
+      {
+        "command": "git status && git branch --show-current",
+        "timeout_ms": 5000,
+        "cache_ttl_seconds": 0
+      }
+    ],
+    "userPromptSubmit": [
+      { "command": "date '+%Y-%m-%d %H:%M:%S'" }
+    ],
+    "preToolUse": [
+      {
+        "matcher": "write",
+        "command": "~/.kiro/hooks/validate-write.sh",
+        "timeout_ms": 10000,
+        "max_output_size": 10240
+      }
+    ],
+    "postToolUse": [
+      {
+        "matcher": "shell",
+        "command": "echo 'Command executed'"
+      }
+    ],
+    "stop": [
+      { "command": "echo 'Turn complete'" }
+    ]
+  }
+}
+```
+
+Object 格式字段:
+
+| 字段                  | 必填 | 类型   | 默认值    | 说明                                      |
+| --------------------- | ---- | ------ | --------- | ----------------------------------------- |
+| `command`           | 是   | string | -         | 要执行的命令                              |
+| `matcher`           | 否   | string | -         | 工具匹配模式(仅 preToolUse/postToolUse) |
+| `timeout_ms`        | 否   | number | `10000` | 最大执行时间(毫秒)                      |
+| `max_output_size`   | 否   | number | `10240` | 最大输出字节数(超出则截断)              |
+| `cache_ttl_seconds` | 否   | number | `0`     | 缓存成功结果的秒数(0=不缓存)            |
+
+#### Array 格式
+
+```json
+{
+  "hooks": [
+    {
+      "name": "show-git-status",
+      "trigger": "agentSpawn",
+      "action": { "type": "command", "command": "git status" },
+      "timeout": 10,
+      "enabled": true
+    },
+    {
+      "name": "validate-write",
+      "trigger": "preToolUse",
+      "matcher": "write",
+      "action": { "type": "command", "command": "~/.kiro/hooks/validate-write.sh" },
+      "timeout": 30,
+      "enabled": true
+    }
+  ]
+}
+```
+
+Array 格式字段:
+
+| 字段        | 必填 | 类型    | 默认值   | 说明                                        |
+| ----------- | ---- | ------- | -------- | ------------------------------------------- |
+| `name`    | 否   | string  | -        | 可读名称                                    |
+| `trigger` | 是   | string  | -        | 触发器名称                                  |
+| `matcher` | 否   | string  | -        | 工具匹配模式                                |
+| `action`  | 是   | object  | -        | `{ "type": "command", "command": "..." }` |
+| `timeout` | 否   | number  | `10`   | 最大执行时间(**秒**)                |
+| `enabled` | 否   | boolean | `true` | 设为 false 可禁用而不删除                   |
+
+### Matcher 匹配模式
+
+| 模式               | 说明                      | 示例                     |
+| ------------------ | ------------------------- | ------------------------ |
+| 精确匹配           | 匹配特定工具              | `"write"`、`"shell"` |
+| 通配符             | 匹配工具名模式            | `"fs_*"`               |
+| MCP 服务器全部工具 | 匹配某个 MCP 服务器       | `"@git"`               |
+| MCP 特定工具       | 匹配 MCP 服务器的特定工具 | `"@git/status"`        |
+| 所有工具           | 匹配所有                  | `"*"`                  |
+| 所有内置工具       | 仅内置                    | `"@builtin"`           |
+| 无 matcher         | 应用于所有工具            | _(不设置)_             |
+
+### Hook 收到的 STDIN 数据
+
+Hook 通过 STDIN 接收 JSON 格式的事件数据:
+
+```json
+{
+  "hook_event_name": "preToolUse",
+  "cwd": "/home/user/project",
+  "tool_name": "fs_write",
+  "tool_input": {
+    "path": "/home/user/project/src/App.vue",
+    "command": "create",
+    "content": "..."
+  }
+}
+```
+
+### 环境变量
+
+Hook 脚本可访问 `KIRO_SESSION_ID` 环境变量(当前会话 UUID)。
+
+### Hook 脚本示例
+
+**文件位置:`~/.kiro/hooks/validate-write.sh`**
+
+```bash
+#!/bin/bash
+# 读取 hook 事件
+EVENT=$(cat)
+
+# 提取文件路径
+FILE=$(echo "$EVENT" | jq -r '.tool_input.path // empty')
+
+# 阻止写入 .env 文件
+if [[ "$FILE" == *".env"* ]]; then
+  echo "禁止修改 .env 文件" >&2
+  exit 2  # 退出码 2 = 阻止工具执行
+fi
+
+exit 0  # 允许执行
+```
+
+---
+
+## 8. MCP 服务器配置
+
+MCP (Model Context Protocol) 服务器为 Agent 提供额外的工具能力。
+
+### 配置位置
+
+MCP 服务器可在以下位置配置:
+
+| 位置       | 路径                                                | 说明                           |
+| ---------- | --------------------------------------------------- | ------------------------------ |
+| Agent 配置 | `.kiro/agents/<name>.json` 的 `mcpServers` 字段 | 特定 Agent 专用                |
+| 全局 MCP   | `~/.kiro/settings/mcp.json`                       | 所有 Agent 可用                |
+| 工作区 MCP | `.kiro/settings/mcp.json`                         | 当前项目所有 Agent 可用        |
+| 旧版全局   | `~/.aws/amazonq/mcp.json`                         | 需要`useLegacyMcpJson: true` |
+| 旧版工作区 | `.amazonq/mcp.json`                               | 需要`useLegacyMcpJson: true` |
+
+### 三种服务器类型
+
+#### 8.1 本地 (stdio) 服务器
+
+通过子进程启动,使用标准输入/输出通信。
+
+```json
+{
+  "mcpServers": {
+    "git": {
+      "command": "mcp-server-git",
+      "args": ["--stdio"],
+      "env": {
+        "GIT_DIR": "/path/to/repo"
+      },
+      "timeout": 120000,
+      "disabled": false,
+      "disabledTools": ["dangerous_tool"]
+    }
+  }
+}
+```
+
+| 字段              | 必填 | 类型    | 默认值     | 说明                   |
+| ----------------- | ---- | ------- | ---------- | ---------------------- |
+| `command`       | 是   | string  | -          | 启动服务器的命令       |
+| `args`          | 否   | array   | -          | 命令参数               |
+| `env`           | 否   | object  | -          | 环境变量               |
+| `timeout`       | 否   | number  | `120000` | 请求超时(毫秒)       |
+| `disabled`      | 否   | boolean | `false`  | 是否禁用此服务器       |
+| `disabledTools` | 否   | array   | -          | 禁用该服务器的特定工具 |
+
+#### 8.2 远程 (HTTP) 服务器
+
+通过 HTTP/SSE 连接远程服务器。
+
+```json
+{
+  "mcpServers": {
+    "remote-api": {
+      "url": "https://mcp.example.com/sse",
+      "headers": {
+        "Authorization": "Bearer $API_TOKEN"
+      },
+      "timeout": 120000,
+      "disabled": false,
+      "disabledTools": [],
+      "forceAuth": false
+    }
+  }
+}
+```
+
+| 字段              | 必填 | 类型    | 默认值     | 说明                         |
+| ----------------- | ---- | ------- | ---------- | ---------------------------- |
+| `url`           | 是   | string  | -          | HTTP 端点 URL                |
+| `headers`       | 否   | object  | -          | HTTP 请求头                  |
+| `oauth`         | 否   | object  | -          | OAuth 配置对象               |
+| `oauthScopes`   | 否   | array   | -          | OAuth 作用域                 |
+| `timeout`       | 否   | number  | `120000` | 请求超时(毫秒)             |
+| `disabled`      | 否   | boolean | `false`  | 是否禁用                     |
+| `disabledTools` | 否   | array   | -          | 禁用特定工具                 |
+| `forceAuth`     | 否   | boolean | `false`  | 跳过无认证尝试,直接走 OAuth |
+
+**OAuth 配置对象**:
+
+```json
+{
+  "mcpServers": {
+    "slack": {
+      "url": "https://mcp.slack.com/mcp",
+      "oauth": {
+        "clientId": "my-app-id",
+        "clientSecret": "my-secret",
+        "redirectUri": "http://localhost:7778/oauth/callback",
+        "oauthScopes": ["search:read", "channels:read"]
+      }
+    }
+  }
+}
+```
+
+| oauth 字段       | 说明                                                  |
+| ---------------- | ----------------------------------------------------- |
+| `clientId`     | 预注册的 OAuth 客户端 ID(设置后跳过动态客户端注册)  |
+| `clientSecret` | OAuth 客户端密钥(机密客户端)                        |
+| `redirectUri`  | 回调 URI(格式:`host:port`、`:port` 或完整 URL) |
+| `oauthScopes`  | 请求的 OAuth 作用域                                   |
+
+#### 8.3 Registry 服务器
+
+从组织 MCP 注册表解析的服务器(企业版功能)。
+
+```json
+{
+  "mcpServers": {
+    "github": {
+      "type": "registry",
+      "env": {
+        "GITHUB_TOKEN": "$GITHUB_TOKEN"
+      },
+      "timeout": 60000
+    }
+  }
+}
+```
+
+| 字段        | 必填 | 类型   | 说明                               |
+| ----------- | ---- | ------ | ---------------------------------- |
+| `type`    | 是   | string | 必须为`"registry"`               |
+| `env`     | 否   | object | 环境变量(覆盖注册表默认值)       |
+| `headers` | 否   | object | HTTP 头(远程 registry 服务器)    |
+| `timeout` | 否   | number | 请求超时                           |
+| `oauth`   | 否   | object | OAuth 配置(远程 registry 服务器) |
+
+### MCP 配置完整示例
+
+**在 Agent 配置中(`.kiro/agents/full-stack.json`):**
+
+```json
+{
+  "name": "full-stack",
+  "tools": ["read", "write", "shell", "code"],
+  "mcpServers": {
+    "git": {
+      "command": "mcp-server-git",
+      "args": ["--stdio"]
+    },
+    "github": {
+      "command": "mcp-server-github",
+      "args": ["--stdio"],
+      "env": {
+        "GITHUB_TOKEN": "$GITHUB_TOKEN"
+      }
+    },
+    "postgres": {
+      "command": "mcp-server-postgres",
+      "args": ["--stdio"],
+      "env": {
+        "DATABASE_URL": "$DATABASE_URL"
+      },
+      "disabledTools": ["drop_table", "truncate"]
+    }
+  }
+}
+```
+
+---
+
+## 9. LSP 代码智能配置
+
+### 文件位置
+
+项目根目录下的 `lsp.json`。
+
+### 初始化
+
+```bash
+# 在聊天中执行
+/code init
+
+# 强制重新初始化
+/code init -f
+```
+
+首次执行 `/code init` 后会创建 `lsp.json`,后续启动时自动加载。删除 `lsp.json` 即可禁用。
+
+### 预配置的语言服务器
+
+| 语言          | 文件扩展名                        | 服务器                     | 安装命令                                                     |
+| ------------- | --------------------------------- | -------------------------- | ------------------------------------------------------------ |
+| TypeScript/JS | `.ts` `.js` `.tsx` `.jsx` | typescript-language-server | `npm install -g typescript-language-server typescript`     |
+| Rust          | `.rs`                           | rust-analyzer              | `rustup component add rust-analyzer`                       |
+| Python        | `.py`                           | pyright                    | `pip install pyright`                                      |
+| Go            | `.go`                           | gopls                      | `go install golang.org/x/tools/gopls@latest`               |
+| Java          | `.java`                         | jdtls                      | `brew install jdtls` (macOS)                               |
+| Kotlin        | `.kt` `.kts`                  | kotlin-lsp                 | [GitHub 下载](https://github.com/fwcd/kotlin-language-server) |
+| Ruby          | `.rb`                           | solargraph                 | `gem install solargraph`                                   |
+| C/C++         | `.c` `.cpp` `.h` `.hpp`   | clangd                     | `apt install clangd` (Linux)                               |
+
+### 自定义语言服务器
+
+**文件位置:项目根目录 `lsp.json`**
+
+```json
+{
+  "languages": {
+    "vue": {
+      "name": "vue-language-server",
+      "command": "vue-language-server",
+      "args": ["--stdio"],
+      "file_extensions": ["vue"],
+      "project_patterns": ["package.json", "vite.config.ts"],
+      "exclude_patterns": ["**/node_modules/**", "**/dist/**"],
+      "multi_workspace": false,
+      "initialization_options": {
+        "typescript": {
+          "tsdk": "node_modules/typescript/lib"
+        }
+      },
+      "request_timeout_secs": 60
+    }
+  }
+}
+```
+
+| 字段                       | 必填 | 类型    | 默认值    | 说明                            |
+| -------------------------- | ---- | ------- | --------- | ------------------------------- |
+| `name`                   | 是   | string  | -         | 语言服务器显示名                |
+| `command`                | 是   | string  | -         | 启动命令                        |
+| `args`                   | 否   | array   | -         | 命令参数(通常`["--stdio"]`) |
+| `file_extensions`        | 是   | array   | -         | 处理的文件扩展名                |
+| `project_patterns`       | 否   | array   | -         | 项目根标识文件                  |
+| `exclude_patterns`       | 否   | array   | -         | 排除的 glob 模式                |
+| `multi_workspace`        | 否   | boolean | `false` | 是否支持多工作区                |
+| `initialization_options` | 否   | object  | -         | LSP 初始化选项                  |
+| `request_timeout_secs`   | 否   | number  | `60`    | 请求超时(秒)                  |
+
+---
+
+## 10. 环境变量
+
+以下环境变量可影响 Kiro CLI 的运行行为:
+
+| 变量名                  | 说明                                             |
+| ----------------------- | ------------------------------------------------ |
+| `KIRO_HOME`           | 覆盖全局配置目录(默认`~/.kiro`)              |
+| `KIRO_DATA_DIR`       | 覆盖运行时数据目录(默认因平台而异)             |
+| `KIRO_ASCII_MODE=1`   | 强制 ASCII 模式,忽略`chat.allowAsciiArt` 设置 |
+| `KIRO_CHAT_LOG_FILE`  | 自定义日志文件路径                               |
+| `KIRO_LOG_NO_COLOR=1` | 禁用日志中的 ANSI 颜色                           |
+| `KIRO_SESSION_ID`     | (Hook 脚本可用)当前会话 UUID                   |
+| `$EDITOR`             | 编辑 Agent 配置时使用的编辑器                    |
+
+### 数据目录默认位置
+
+| 平台    | 路径                                       |
+| ------- | ------------------------------------------ |
+| macOS   | `~/Library/Application Support/kiro-cli` |
+| Linux   | `~/.local/share/kiro-cli`                |
+| Windows | `%LOCALAPPDATA%\kiro-cli`                |
+
+### 日志文件默认位置
+
+| 平台    | 路径                                        |
+| ------- | ------------------------------------------- |
+| macOS   | `$TMPDIR/kiro-log/kiro-chat.log`          |
+| Linux   | `$XDG_RUNTIME_DIR/kiro-log/kiro-chat.log` |
+| Windows | `%TEMP%/kiro-log/logs/kiro-chat.log`      |
+
+### 示例
+
+```bash
+# 使用自定义配置目录
+KIRO_HOME=/opt/team/kiro kiro-cli chat
+
+# 使用自定义数据目录(企业部署场景)
+KIRO_DATA_DIR=/opt/kiro/data kiro-cli chat
+
+# 自定义日志路径
+KIRO_CHAT_LOG_FILE=/tmp/kiro-debug.log kiro-cli chat
+
+# 强制 ASCII 模式(SSH/无 Unicode 终端)
+KIRO_ASCII_MODE=1 kiro-cli chat
+```
+
+---
+
+## 11. 常用命令速查
+
+### 设置管理
+
+```bash
+# 查看所有可用设置
+kiro-cli settings list --all
+
+# 获取设置值
+kiro-cli settings chat.defaultModel
+
+# 修改设置
+kiro-cli settings chat.defaultModel "anthropic.claude-3-5-sonnet-20241022-v2:0"
+kiro-cli settings chat.defaultAgent "vue-dev"
+kiro-cli settings telemetry.enabled false
+kiro-cli settings knowledge.indexType "best"
+
+# 删除设置(恢复默认)
+kiro-cli settings --delete chat.defaultModel
+
+# 通配符删除
+kiro-cli settings --delete "knowledge.*"
+
+# 在编辑器中打开设置文件
+kiro-cli settings open
+```
+
+### Agent 管理
+
+```bash
+# 列出所有 Agent
+kiro-cli agent list
+
+# 创建 Agent
+kiro-cli agent create my-agent
+kiro-cli agent create my-agent --from kiro_default
+kiro-cli agent create my-agent --directory .kiro/agents
+
+# 编辑 Agent
+kiro-cli agent edit my-agent
+
+# 验证配置
+kiro-cli agent validate --path .kiro/agents/my-agent.json
+
+# 设置默认 Agent
+kiro-cli agent set-default my-agent
+```
+
+### 聊天会话
+
+```bash
+# 启动聊天
+kiro-cli chat
+
+# 使用指定 Agent 和模型
+kiro-cli chat --agent vue-dev --model <model-id>
+
+# 信任所有工具(无需确认)
+kiro-cli chat --trust-all-tools
+
+# 信任特定工具
+kiro-cli chat --trust-tools=read,grep,code
+
+# 恢复上次会话
+kiro-cli chat --resume
+
+# 选择会话恢复
+kiro-cli chat --resume-picker
+
+# 无头模式(自动化)
+kiro-cli chat --no-interactive --trust-all-tools "运行测试并汇总结果"
+
+# 设置推理强度
+kiro-cli chat --effort high "Review this code"
+```
+
+### TUI 内斜杠命令
+
+```
+/settings              打开设置菜单
+/settings display      显示设置(动画、ASCII、图标)
+/settings theme        主题设置
+/settings terminal     终端多行输入配置
+/settings history      历史记录范围
+
+/agent                 列出/切换 Agent
+/agent create          创建新 Agent
+/agent edit            编辑当前 Agent
+
+/model                 切换模型
+/tools                 查看可用工具
+/hooks                 查看当前 Hook 配置
+/context               查看上下文窗口使用情况
+/context show          显示上下文文件详情
+/context add <path>    添加临时上下文文件
+
+/code init             初始化代码智能
+/code init -f          强制重新初始化
+/code status           查看 LSP 状态
+/code overview         代码库概览
+
+/mcp                   查看 MCP 服务器状态
+/mcp add               添加 MCP 服务器
+/mcp remove            移除 MCP 服务器
+
+/chat save             保存当前会话
+/chat load             加载历史会话
+
+/guide                 切换到帮助 Agent
+/help                  查看所有斜杠命令
+```
+
+---
+
+## 快速上手:为当前项目配置 Kiro CLI
+
+### 步骤 1:创建工作区配置目录
+
+```bash
+mkdir -p .kiro/agents .kiro/steering .kiro/skills
+```
+
+### 步骤 2:创建项目 Agent
+
+创建 `.kiro/agents/project-dev.json`:
+
+```json
+{
+  "name": "project-dev",
+  "description": "FranklinWH ToolBox 项目开发助手",
+  "prompt": "你是一个 Vue 3 + TypeScript + Naive UI 项目的开发助手。项目使用 Vite 构建,pnpm 管理依赖,UnoCSS 编写样式。",
+  "tools": ["read", "write", "shell", "grep", "glob", "code", "web_fetch"],
+  "allowedTools": ["read", "grep", "glob", "code"],
+  "toolsSettings": {
+    "shell": {
+      "allowedCommands": ["pnpm", "npx", "git (status|diff|log|branch)"],
+      "autoAllowReadonly": true
+    },
+    "write": {
+      "deniedPaths": ["node_modules/**", "dist/**"]
+    }
+  },
+  "resources": [
+    "file://src/**/*.vue",
+    "file://src/**/*.ts",
+    "file://package.json"
+  ],
+  "hooks": {
+    "agentSpawn": [
+      { "command": "git branch --show-current" }
+    ]
+  }
+}
+```
+
+### 步骤 3:添加引导文件
+
+创建 `.kiro/steering/project-rules.md`:
+
+```markdown
+# 项目规范
+
+- 使用 Vue 3 Composition API + <script setup>
+- UI 库:Naive UI
+- 样式:UnoCSS 原子类
+- 工具函数库:es-toolkit
+- 日期处理:dayjs
+- 图表:@antv/g2 和 uplot
+- 包管理器:pnpm(使用 pnpm@10)
+```
+
+### 步骤 4:设为默认 Agent
+
+```bash
+kiro-cli settings chat.defaultAgent project-dev
+```
+
+---
+
+> 本文档基于 Kiro CLI 官方文档整理,配置项可能随版本更新而变化。使用 `kiro-cli settings list --all` 查看当前版本支持的所有设置。

+ 3 - 0
package.json

@@ -45,10 +45,12 @@
     "casdoor-vue-sdk": "^1.6.0",
     "chroma-js": "^3.2.0",
     "dayjs": "^1.11.20",
+    "dompurify": "^3.4.12",
     "es-toolkit": "^1.45.1",
     "file-saver": "^2.0.5",
     "idb": "^8.0.3",
     "jwt-decode": "^4.0.0",
+    "marked": "^18.0.7",
     "monaco-editor": "^0.55.1",
     "monaco-editor-vue3": "^1.0.5",
     "naive-ui": "^2.44.1",
@@ -67,6 +69,7 @@
     "@playwright/test": "^1.58.2",
     "@tsconfig/node22": "^22.0.5",
     "@types/chroma-js": "^3.1.2",
+    "@types/dompurify": "^3.2.0",
     "@types/file-saver": "^2.0.7",
     "@types/jsdom": "^28.0.1",
     "@types/node": "^25.5.0",

+ 63 - 32
pnpm-lock.yaml

@@ -80,6 +80,9 @@ importers:
       dayjs:
         specifier: ^1.11.20
         version: 1.11.20
+      dompurify:
+        specifier: ^3.4.12
+        version: 3.4.12
       es-toolkit:
         specifier: ^1.45.1
         version: 1.45.1
@@ -92,6 +95,9 @@ importers:
       jwt-decode:
         specifier: ^4.0.0
         version: 4.0.0
+      marked:
+        specifier: ^18.0.7
+        version: 18.0.7
       monaco-editor:
         specifier: ^0.55.1
         version: 0.55.1
@@ -141,6 +147,9 @@ importers:
       '@types/chroma-js':
         specifier: ^3.1.2
         version: 3.1.2
+      '@types/dompurify':
+        specifier: ^3.2.0
+        version: 3.2.0
       '@types/file-saver':
         specifier: ^2.0.7
         version: 2.0.7
@@ -1755,6 +1764,10 @@ packages:
   '@types/deep-eql@4.0.2':
     resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
 
+  '@types/dompurify@3.2.0':
+    resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==}
+    deprecated: This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed.
+
   '@types/esrecurse@4.3.1':
     resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==}
 
@@ -1871,50 +1884,50 @@ packages:
     resolution: {integrity: sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260604.1':
-    resolution: {integrity: sha512-zs616um9UuaODLsNlCu5Aw95rFcTV4u3hVt090r6k0lVvTxfaJOv8HKA6BpIotcEYlZlMQowrMSYCCdedo7iyA==}
+  '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260707.2':
+    resolution: {integrity: sha512-wny2pgKjGbiZtnOIHVa3tXC1UfDqxNEFzyPGmiqybedG8hipG2Nfp0l5UxbaKCjkLacUpH/W5bP2hBOMVhCOzg==}
     engines: {node: '>=16.20.0'}
     cpu: [arm64]
     os: [darwin]
 
-  '@typescript/native-preview-darwin-x64@7.0.0-dev.20260604.1':
-    resolution: {integrity: sha512-pOdNAf2pwc9JBjo8gUsvLs5uqg7d0AhYXfE8/3zvPKBlIZG+mTcvEWW1hPawlWxXzf/vxnP4dgYwUHAMDghhKA==}
+  '@typescript/native-preview-darwin-x64@7.0.0-dev.20260707.2':
+    resolution: {integrity: sha512-Afc7M5zOwo+GpfcYwz5Z8HMB2tPVsui7nNIqEuuFB73MPdVqNn/Wmpe4tP4MRri0AtJnJknoHBaTJ/VDAp/Jhw==}
     engines: {node: '>=16.20.0'}
     cpu: [x64]
     os: [darwin]
 
-  '@typescript/native-preview-linux-arm64@7.0.0-dev.20260604.1':
-    resolution: {integrity: sha512-GjIrt6YHP3bbOWBCCE08SlBSDf84Lnjn3Td822/lOX9nm6ODlA/HI7rtGh7KzS/fxehep2Vy4dXU4Il12X1s5A==}
+  '@typescript/native-preview-linux-arm64@7.0.0-dev.20260707.2':
+    resolution: {integrity: sha512-iITBa2WjjTI5N9t5l7Z4KoOSI+2zBlhbvFzsD/f8qX8QoKjz/Y4DPyBDgezYi8nkqjjksbgSOJ3/ykzhwrB9cg==}
     engines: {node: '>=16.20.0'}
     cpu: [arm64]
     os: [linux]
 
-  '@typescript/native-preview-linux-arm@7.0.0-dev.20260604.1':
-    resolution: {integrity: sha512-IKaZL3i5HKmKqwb2IZEXW1j68fVg1HsvAaXkbrkOIG/J5Eyksu5tEnTzucrIY1oPdzgHT+y2HpDIYp2sGeHFvw==}
+  '@typescript/native-preview-linux-arm@7.0.0-dev.20260707.2':
+    resolution: {integrity: sha512-hJm/UOqZTr9FHmR7uNm8VGX4oKtfWk0Jem0zPeJFNC8ckGUfSBueyiEYMZB+XmRc1aG4x1E46y3CplP4CLHvGQ==}
     engines: {node: '>=16.20.0'}
     cpu: [arm]
     os: [linux]
 
-  '@typescript/native-preview-linux-x64@7.0.0-dev.20260604.1':
-    resolution: {integrity: sha512-twQ7XDjsmaHIevN1MjeRYIVLUPL0fBm8A0jg1FhGYPckhTxEBiHIpJf9E//eFidAdrEHjeTSBP1jJw3GNAensg==}
+  '@typescript/native-preview-linux-x64@7.0.0-dev.20260707.2':
+    resolution: {integrity: sha512-du0dzi6y97Po5vDNdPJTyyijHCpaS22JLRnKZEJXBDaO9gCIymOv/5QQokFRuOlQm0bWl3i9PF4OVdGP6uAOQA==}
     engines: {node: '>=16.20.0'}
     cpu: [x64]
     os: [linux]
 
-  '@typescript/native-preview-win32-arm64@7.0.0-dev.20260604.1':
-    resolution: {integrity: sha512-QBRxaVT3SFiNfOhwYb/56ddpHWPMFdfiJ4zkFJIjaAXeZ/ssWNHM9lH7yR++GrE9VTsUZ4eVSZfOmQbzRERhgw==}
+  '@typescript/native-preview-win32-arm64@7.0.0-dev.20260707.2':
+    resolution: {integrity: sha512-SsAwfhyHJ1akgBc+99z4+hwdbHsdWaKB8EwCNIMA6JfSLMeUjffrYvxu+vfMyxVtOVOz7RrRXRoiDiu4a2sCtg==}
     engines: {node: '>=16.20.0'}
     cpu: [arm64]
     os: [win32]
 
-  '@typescript/native-preview-win32-x64@7.0.0-dev.20260604.1':
-    resolution: {integrity: sha512-hR7YHoRpm88Q86gK6/IMayWUA+ROdHGzOPKK8EBp1xD/Cg2Bh5AXbut8HcyUDx/bcGQvnuht/XsW47bT3to9mg==}
+  '@typescript/native-preview-win32-x64@7.0.0-dev.20260707.2':
+    resolution: {integrity: sha512-DL4u27stv0fo71sVhOzHSwE+YMZsbBijVI+kg5dLDLilSH79WFTJ8RSQ46vJrCMt+Gjlv/JOZP1PuLJDfioYeQ==}
     engines: {node: '>=16.20.0'}
     cpu: [x64]
     os: [win32]
 
-  '@typescript/native-preview@7.0.0-dev.20260604.1':
-    resolution: {integrity: sha512-A3/9yZTt2V5NlDURcVJ4mN2YjfeQTXCRyLuENKrNdGhO+y59mC/2UDr7UvpB3Li+83TRAuhDN8SBoM+7gkHdzQ==}
+  '@typescript/native-preview@7.0.0-dev.20260707.2':
+    resolution: {integrity: sha512-oUGp+Rep/hqMhPunyinsALUwSlzHINSxitifPiSaeqoKOKD2OlR9NE3TaPqwsl4NlGslsOSUXI1JotWQzpYCPg==}
     engines: {node: '>=16.20.0'}
     hasBin: true
 
@@ -2898,6 +2911,9 @@ packages:
   dompurify@3.2.7:
     resolution: {integrity: sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==}
 
+  dompurify@3.4.12:
+    resolution: {integrity: sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==}
+
   domutils@2.8.0:
     resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==}
 
@@ -3764,6 +3780,11 @@ packages:
     engines: {node: '>= 18'}
     hasBin: true
 
+  marked@18.0.7:
+    resolution: {integrity: sha512-iDVQ5ldaiKXn6b2JroX5kgRfmwgqolW7NpaEzTl1k/2Zh1njIEN9yniyLV/mOvWwtsE8OGgkjsCYvijuPk1dtA==}
+    engines: {node: '>= 20'}
+    hasBin: true
+
   mdast-util-find-and-replace@3.0.2:
     resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==}
 
@@ -6528,6 +6549,10 @@ snapshots:
 
   '@types/deep-eql@4.0.2': {}
 
+  '@types/dompurify@3.2.0':
+    dependencies:
+      dompurify: 3.4.12
+
   '@types/esrecurse@4.3.1': {}
 
   '@types/estree@1.0.8': {}
@@ -6679,36 +6704,36 @@ snapshots:
       '@typescript-eslint/types': 8.57.2
       eslint-visitor-keys: 5.0.1
 
-  '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260604.1':
+  '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260707.2':
     optional: true
 
-  '@typescript/native-preview-darwin-x64@7.0.0-dev.20260604.1':
+  '@typescript/native-preview-darwin-x64@7.0.0-dev.20260707.2':
     optional: true
 
-  '@typescript/native-preview-linux-arm64@7.0.0-dev.20260604.1':
+  '@typescript/native-preview-linux-arm64@7.0.0-dev.20260707.2':
     optional: true
 
-  '@typescript/native-preview-linux-arm@7.0.0-dev.20260604.1':
+  '@typescript/native-preview-linux-arm@7.0.0-dev.20260707.2':
     optional: true
 
-  '@typescript/native-preview-linux-x64@7.0.0-dev.20260604.1':
+  '@typescript/native-preview-linux-x64@7.0.0-dev.20260707.2':
     optional: true
 
-  '@typescript/native-preview-win32-arm64@7.0.0-dev.20260604.1':
+  '@typescript/native-preview-win32-arm64@7.0.0-dev.20260707.2':
     optional: true
 
-  '@typescript/native-preview-win32-x64@7.0.0-dev.20260604.1':
+  '@typescript/native-preview-win32-x64@7.0.0-dev.20260707.2':
     optional: true
 
-  '@typescript/native-preview@7.0.0-dev.20260604.1':
+  '@typescript/native-preview@7.0.0-dev.20260707.2':
     optionalDependencies:
-      '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260604.1
-      '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260604.1
-      '@typescript/native-preview-linux-arm': 7.0.0-dev.20260604.1
-      '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260604.1
-      '@typescript/native-preview-linux-x64': 7.0.0-dev.20260604.1
-      '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260604.1
-      '@typescript/native-preview-win32-x64': 7.0.0-dev.20260604.1
+      '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260707.2
+      '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260707.2
+      '@typescript/native-preview-linux-arm': 7.0.0-dev.20260707.2
+      '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260707.2
+      '@typescript/native-preview-linux-x64': 7.0.0-dev.20260707.2
+      '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260707.2
+      '@typescript/native-preview-win32-x64': 7.0.0-dev.20260707.2
 
   '@umami/node@0.4.0': {}
 
@@ -7905,6 +7930,10 @@ snapshots:
     optionalDependencies:
       '@types/trusted-types': 2.0.7
 
+  dompurify@3.4.12:
+    optionalDependencies:
+      '@types/trusted-types': 2.0.7
+
   domutils@2.8.0:
     dependencies:
       dom-serializer: 1.4.1
@@ -8802,6 +8831,8 @@ snapshots:
 
   marked@14.0.0: {}
 
+  marked@18.0.7: {}
+
   mdast-util-find-and-replace@3.0.2:
     dependencies:
       '@types/mdast': 4.0.4
@@ -10438,7 +10469,7 @@ snapshots:
 
   vue-vine-tsc@1.7.27(typescript@6.0.2):
     dependencies:
-      '@typescript/native-preview': 7.0.0-dev.20260604.1
+      '@typescript/native-preview': 7.0.0-dev.20260707.2
       '@volar/language-core': 2.4.20
       '@volar/typescript': 2.4.20
       '@vue-vine/language-service': 1.7.27(typescript@6.0.2)

+ 31 - 0
service/.kiro/settings/mcp.json

@@ -0,0 +1,31 @@
+{
+  "mcpServers": {
+    "fwh-tools": {
+      "command": "./fwh-mcp-server",
+      "args": [],
+      "env": {
+        "MONGO_DSN": "${MONGO_DSN}",
+        "REDIS_ADDR": "${REDIS_ADDR}",
+        "PLANE_WEB_HOST": "${PLANE_WEB_HOST}",
+        "PLANE_WEB_EMAIL": "${PLANE_WEB_EMAIL}",
+        "PLANE_WEB_PASSWORD": "${PLANE_WEB_PASSWORD}",
+        "PLANE_WEB_WORKSPACE_SLUG": "${PLANE_WEB_WORKSPACE_SLUG}",
+        "PLANE_WEB_PROJECT_ID": "${PLANE_WEB_PROJECT_ID}"
+      }
+    },
+    "team-knowledge": {
+      "url": "http://knowledge.internal:4100/mcp"
+    },
+    "aws-docs": {
+      "command": "npx",
+      "args": ["-y", "@aws/aws-documentation-mcp-server"]
+    },
+    "web-search": {
+      "command": "npx",
+      "args": ["-y", "@modelcontextprotocol/server-bravesearch"],
+      "env": {
+        "BRAVE_API_KEY": "${BRAVE_API_KEY}"
+      }
+    }
+  }
+}

+ 25 - 0
service/.kiro/steering/project.md

@@ -0,0 +1,25 @@
+# FranklinWH-ToolBox 项目上下文
+
+## 项目概述
+FranklinWH-ToolBox 是 FranklinWH 内部工具平台。
+- 前端:Vue 3 + Naive UI + TypeScript + Vite
+- 后端:Go (Fiber) + MongoDB + PostgreSQL + Redis
+- 部署:Linux 服务器
+
+## 可用 MCP 工具
+- fwh-tools: 查询站点信息、SIM卡数据、功率流、告警统计、Plane Issue
+- team-knowledge: 搜索团队知识库文档
+- aws-docs: 搜索 AWS 文档
+- web-search: 网络搜索技术资料
+
+## 开发规范
+- 前端组件使用 Composition API + script setup
+- 后端采用 Controller → Service → Repository 分层
+- API 路由注册在 service/routes/
+- Plane V2 Web API 用于项目管理集成
+
+## 注意事项
+- 不要直接操作 Git(无 Git MCP Server)
+- 修改代码前先查阅项目规范
+- 后端修改后需要 go build 验证
+- 前端修改后需要 npm run build 验证

+ 10 - 46
service/cmd/deploy.sh

@@ -1,54 +1,18 @@
 #!/bin/bash
 set -e
 
-# ===== 配置部分 =====
-APP_NAME="fwh-toolbox-server"                       # 服务名称
-APP_DIR="/var/www/fwh-toolbox/server"               # 二进制运行路径
-APP_PATH="$APP_DIR/$APP_NAME"                       # 二进制安装路径
-SERVICE_FILE="/etc/systemd/system/$APP_NAME.service"
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
 
-# ===== 编译程序 =====
-echo "🚀 Building Go program..."
-GOOS=linux GOARCH=amd64 go build -o $APP_NAME ./   # 按需修改 ./ 为你的项目路径
+echo "===== 部署全部 ====="
+echo ""
 
-# ===== 复制程序到目标路径 =====
-echo "📦 Copying binary to $APP_PATH..."
-sudo mv $APP_NAME $APP_PATH
-sudo chmod +x $APP_PATH
+# 部署主程序
+bash "$SCRIPT_DIR/deploy_toolbox.sh"
 
-# ===== 安装 systemd 服务(如果不存在) =====
-if [ ! -f "$SERVICE_FILE" ]; then
-    echo "📝 Creating systemd service: $SERVICE_FILE"
-    sudo tee $SERVICE_FILE > /dev/null <<EOF
-[Unit]
-Description=$APP_NAME service
-After=network.target
+echo ""
 
-[Service]
-ExecStart=$APP_PATH
-Restart=always
-User=root
-WorkingDirectory=$APP_DIR
-CPUQuota=200%
-MemoryLimit=2G
+# 部署 MCP Server
+bash "$SCRIPT_DIR/deploy_mcp.sh"
 
-[Install]
-WantedBy=multi-user.target
-EOF
-
-    echo "✅ Service file created."
-    sudo systemctl daemon-reload
-    sudo systemctl enable $APP_NAME
-fi
-
-# ===== 启动或重启服务 =====
-if systemctl is-active --quiet $APP_NAME; then
-    echo "🔄 Restarting $APP_NAME service..."
-    sudo systemctl restart $APP_NAME
-else
-    echo "▶️ Starting $APP_NAME service..."
-    sudo systemctl start $APP_NAME
-fi
-
-# ===== 状态检查 =====
-sudo systemctl status $APP_NAME --no-pager
+echo ""
+echo "===== 全部部署完成 ====="

+ 18 - 0
service/cmd/deploy_mcp.sh

@@ -0,0 +1,18 @@
+#!/bin/bash
+set -e
+
+# ===== 配置部分 =====
+MCP_NAME="fwh-mcp-server"
+APP_DIR="/var/www/fwh-toolbox/server"
+MCP_PATH="$APP_DIR/$MCP_NAME"
+
+# ===== 编译程序 =====
+echo "🚀 Building $MCP_NAME..."
+GOOS=linux GOARCH=amd64 go build -o $MCP_NAME ./cmd/mcp/
+
+# ===== 复制程序到目标路径 =====
+echo "📦 Copying binary to $MCP_PATH..."
+sudo mv $MCP_NAME $MCP_PATH
+sudo chmod +x $MCP_PATH
+
+echo "✅ $MCP_NAME deployed to $MCP_PATH"

+ 54 - 0
service/cmd/deploy_toolbox.sh

@@ -0,0 +1,54 @@
+#!/bin/bash
+set -e
+
+# ===== 配置部分 =====
+APP_NAME="fwh-toolbox-server"
+APP_DIR="/var/www/fwh-toolbox/server"
+APP_PATH="$APP_DIR/$APP_NAME"
+SERVICE_FILE="/etc/systemd/system/$APP_NAME.service"
+
+# ===== 编译程序 =====
+echo "🚀 Building $APP_NAME..."
+GOOS=linux GOARCH=amd64 go build -o $APP_NAME ./
+
+# ===== 复制程序到目标路径 =====
+echo "📦 Copying binary to $APP_PATH..."
+sudo mv $APP_NAME $APP_PATH
+sudo chmod +x $APP_PATH
+
+# ===== 安装 systemd 服务(如果不存在) =====
+if [ ! -f "$SERVICE_FILE" ]; then
+    echo "📝 Creating systemd service: $SERVICE_FILE"
+    sudo tee $SERVICE_FILE > /dev/null <<EOF
+[Unit]
+Description=$APP_NAME service
+After=network.target
+
+[Service]
+ExecStart=$APP_PATH
+Restart=always
+User=root
+WorkingDirectory=$APP_DIR
+CPUQuota=200%
+MemoryLimit=2G
+
+[Install]
+WantedBy=multi-user.target
+EOF
+
+    echo "✅ Service file created."
+    sudo systemctl daemon-reload
+    sudo systemctl enable $APP_NAME
+fi
+
+# ===== 启动或重启服务 =====
+if systemctl is-active --quiet $APP_NAME; then
+    echo "🔄 Restarting $APP_NAME service..."
+    sudo systemctl restart $APP_NAME
+else
+    echo "▶️ Starting $APP_NAME service..."
+    sudo systemctl start $APP_NAME
+fi
+
+# ===== 状态检查 =====
+sudo systemctl status $APP_NAME --no-pager

+ 58 - 0
service/cmd/mcp/main.go

@@ -0,0 +1,58 @@
+package main
+
+import (
+	"fmt"
+	"log"
+	"os"
+
+	"github.com/mark3labs/mcp-go/server"
+
+	"fwh-toolbox-server/cache"
+	"fwh-toolbox-server/config"
+	"fwh-toolbox-server/database"
+	"fwh-toolbox-server/logger"
+	"fwh-toolbox-server/repositories"
+
+	"fwh-toolbox-server/cmd/mcp/tools"
+)
+
+func main() {
+	// 加载配置(复用现有 config)
+	cfg, err := config.LoadConfig()
+	if err != nil {
+		log.Fatalf("加载配置失败: %v", err)
+	}
+
+	// 初始化 logger
+	logger.Init(cfg.App.AppEnv)
+	defer logger.Log.Sync()
+
+	// 初始化基础设施(与 HTTP Server 相同)
+	if err := database.Init(&cfg.App); err != nil {
+		log.Fatalf("数据库初始化失败: %v", err)
+	}
+	cache.Init(cfg.App.RedisAddr)
+	repositories.PlaneWebHttpInit(&cfg.PlaneWebApi)
+
+	// 创建 MCP Server
+	s := server.NewMCPServer(
+		"fwh-tools",
+		"1.0.0",
+		server.WithToolCapabilities(false),
+		server.WithRecovery(),
+	)
+
+	// 注册所有 Tools
+	tools.RegisterSiteInfoTools(s)
+	tools.RegisterSimCardTools(s)
+	tools.RegisterPowerFlowTools(s)
+	tools.RegisterAlarmTools(s)
+	tools.RegisterShutdownTools(s)
+	tools.RegisterPlaneTools(s)
+
+	// 启动 stdio 通信
+	fmt.Fprintln(os.Stderr, "[fwh-mcp-server] started (stdio)")
+	if err := server.ServeStdio(s); err != nil {
+		log.Fatalf("[fwh-mcp-server] error: %v", err)
+	}
+}

+ 39 - 0
service/cmd/mcp/tools/alarm.go

@@ -0,0 +1,39 @@
+package tools
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+
+	"github.com/mark3labs/mcp-go/mcp"
+	"github.com/mark3labs/mcp-go/server"
+
+	"fwh-toolbox-server/services"
+)
+
+func RegisterAlarmTools(s *server.MCPServer) {
+	s.AddTool(
+		mcp.NewTool("query_alarm_statistics",
+			mcp.WithDescription("查询告警统计数据(按日期汇总各类告警数量)"),
+			mcp.WithString("date", mcp.Description("统计日期,格式 yyyy-MM-dd(不传则查最新)")),
+		),
+		handleQueryAlarmStatistics,
+	)
+}
+
+func handleQueryAlarmStatistics(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	date := req.GetString("date", "")
+
+	var datePtr *string
+	if date != "" {
+		datePtr = &date
+	}
+
+	data, err := services.GetAlarmStatistics(datePtr)
+	if err != nil {
+		return mcp.NewToolResultError(fmt.Sprintf("查询告警统计失败: %v", err)), nil
+	}
+
+	jsonBytes, _ := json.MarshalIndent(data, "", "  ")
+	return mcp.NewToolResultText(string(jsonBytes)), nil
+}

+ 80 - 0
service/cmd/mcp/tools/plane.go

@@ -0,0 +1,80 @@
+package tools
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+
+	"github.com/mark3labs/mcp-go/mcp"
+	"github.com/mark3labs/mcp-go/server"
+
+	"fwh-toolbox-server/dto"
+	"fwh-toolbox-server/services"
+)
+
+func RegisterPlaneTools(s *server.MCPServer) {
+	// Tool: 创建反馈
+	s.AddTool(
+		mcp.NewTool("create_feedback",
+			mcp.WithDescription("在 Plane 创建 Intake Issue(问题反馈或需求提交)"),
+			mcp.WithString("title", mcp.Required(), mcp.Description("标题")),
+			mcp.WithString("type", mcp.Required(), mcp.Description("类型:bug 或 feature")),
+			mcp.WithString("priority", mcp.Description("优先级:urgent, high, medium, low, none(默认 medium)")),
+			mcp.WithString("description", mcp.Description("详细描述")),
+			mcp.WithString("category", mcp.Description("分类:bug类(bug,ui,performance,other) / feature类(new,improvement,experience)")),
+		),
+		handleCreateFeedback,
+	)
+
+	// Tool: 查询反馈历史
+	s.AddTool(
+		mcp.NewTool("query_feedback_history",
+			mcp.WithDescription("查询 Plane Intake Issue 提交历史(带 Feedback 标签的记录)"),
+		),
+		handleQueryFeedbackHistory,
+	)
+}
+
+func handleCreateFeedback(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	title, err := req.RequireString("title")
+	if err != nil {
+		return mcp.NewToolResultError("参数 title 必填"), nil
+	}
+	feedbackType, err := req.RequireString("type")
+	if err != nil {
+		return mcp.NewToolResultError("参数 type 必填"), nil
+	}
+	if feedbackType != "bug" && feedbackType != "feature" {
+		return mcp.NewToolResultError("type 必须是 bug 或 feature"), nil
+	}
+
+	priority := req.GetString("priority", "medium")
+	description := req.GetString("description", "")
+	category := req.GetString("category", "")
+
+	body := dto.FeedbackSubmitDTO{
+		Title:       title,
+		Type:        feedbackType,
+		Priority:    priority,
+		Description: description,
+		Category:    category,
+	}
+
+	result, err := services.SubmitFeedback(body, "MCP-Agent")
+	if err != nil {
+		return mcp.NewToolResultError(fmt.Sprintf("创建失败: %v", err)), nil
+	}
+
+	jsonBytes, _ := json.MarshalIndent(result, "", "  ")
+	return mcp.NewToolResultText(string(jsonBytes)), nil
+}
+
+func handleQueryFeedbackHistory(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	data, err := services.GetFeedbackHistory("")
+	if err != nil {
+		return mcp.NewToolResultError(fmt.Sprintf("查询失败: %v", err)), nil
+	}
+
+	jsonBytes, _ := json.MarshalIndent(data, "", "  ")
+	return mcp.NewToolResultText(string(jsonBytes)), nil
+}

+ 49 - 0
service/cmd/mcp/tools/powerFlow.go

@@ -0,0 +1,49 @@
+package tools
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"time"
+
+	"github.com/mark3labs/mcp-go/mcp"
+	"github.com/mark3labs/mcp-go/server"
+
+	"fwh-toolbox-server/services"
+)
+
+func RegisterPowerFlowTools(s *server.MCPServer) {
+	s.AddTool(
+		mcp.NewTool("query_power_flow",
+			mcp.WithDescription("查询站点指定日期的功率流数据(PV、Grid、aPower、HomeLoad、Generator)"),
+			mcp.WithString("sn", mcp.Required(), mcp.Description("站点 SN 序列号")),
+			mcp.WithString("date", mcp.Required(), mcp.Description("日期,格式 yyyy-MM-dd")),
+		),
+		handleQueryPowerFlow,
+	)
+}
+
+func handleQueryPowerFlow(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	sn, err := req.RequireString("sn")
+	if err != nil {
+		return mcp.NewToolResultError("参数 sn 必填"), nil
+	}
+	dateStr, err := req.RequireString("date")
+	if err != nil {
+		return mcp.NewToolResultError("参数 date 必填"), nil
+	}
+
+	// 解析日期
+	day, err := time.Parse("2006-01-02", dateStr)
+	if err != nil {
+		return mcp.NewToolResultError(fmt.Sprintf("日期格式错误,需要 yyyy-MM-dd: %v", err)), nil
+	}
+
+	data, err := services.QuerySitePowerFlow(sn, day)
+	if err != nil {
+		return mcp.NewToolResultError(fmt.Sprintf("查询功率流失败: %v", err)), nil
+	}
+
+	jsonBytes, _ := json.MarshalIndent(data, "", "  ")
+	return mcp.NewToolResultText(string(jsonBytes)), nil
+}

+ 39 - 0
service/cmd/mcp/tools/shutdown.go

@@ -0,0 +1,39 @@
+package tools
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+
+	"github.com/mark3labs/mcp-go/mcp"
+	"github.com/mark3labs/mcp-go/server"
+
+	"fwh-toolbox-server/services"
+)
+
+func RegisterShutdownTools(s *server.MCPServer) {
+	s.AddTool(
+		mcp.NewTool("query_shutdown_statistics",
+			mcp.WithDescription("查询停机统计数据(按日期汇总停机站点信息)"),
+			mcp.WithString("date", mcp.Description("统计日期,格式 yyyy-MM-dd(不传则查最新)")),
+		),
+		handleQueryShutdownStatistics,
+	)
+}
+
+func handleQueryShutdownStatistics(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	date := req.GetString("date", "")
+
+	var datePtr *string
+	if date != "" {
+		datePtr = &date
+	}
+
+	data, err := services.GetShutdownStatistics(datePtr)
+	if err != nil {
+		return mcp.NewToolResultError(fmt.Sprintf("查询停机统计失败: %v", err)), nil
+	}
+
+	jsonBytes, _ := json.MarshalIndent(data, "", "  ")
+	return mcp.NewToolResultText(string(jsonBytes)), nil
+}

+ 76 - 0
service/cmd/mcp/tools/simCard.go

@@ -0,0 +1,76 @@
+package tools
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+
+	"github.com/mark3labs/mcp-go/mcp"
+	"github.com/mark3labs/mcp-go/server"
+
+	"fwh-toolbox-server/services"
+)
+
+func RegisterSimCardTools(s *server.MCPServer) {
+	// Tool: 查询单张SIM卡
+	s.AddTool(
+		mcp.NewTool("query_sim_card",
+			mcp.WithDescription("查询单张 SIM 卡的详细信息,包括状态、平台、用量等"),
+			mcp.WithString("giccid", mcp.Required(), mcp.Description("SIM 卡 GICCID(19位)")),
+		),
+		handleQuerySimCard,
+	)
+
+	// Tool: 搜索SIM卡列表
+	s.AddTool(
+		mcp.NewTool("search_sim_cards",
+			mcp.WithDescription("搜索 SIM 卡列表,支持按状态和平台过滤"),
+			mcp.WithNumber("page", mcp.Description("页码,从1开始,默认1")),
+			mcp.WithNumber("size", mcp.Description("每页数量,默认10")),
+			mcp.WithString("search", mcp.Description("搜索关键字(GICCID)")),
+			mcp.WithString("status", mcp.Description("状态过滤:UNKNOWN, NOT_ACTIVATED, ACTIVATED, DEACTIVATED, RETIRED(逗号分隔多选)")),
+			mcp.WithString("platform", mcp.Description("平台过滤:SmartHub, Acceleronix(逗号分隔多选)")),
+		),
+		handleSearchSimCards,
+	)
+}
+
+func handleQuerySimCard(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	giccid, err := req.RequireString("giccid")
+	if err != nil {
+		return mcp.NewToolResultError("参数 giccid 必填"), nil
+	}
+
+	data, err := services.GetSimCardInfo(giccid)
+	if err != nil {
+		return mcp.NewToolResultError(fmt.Sprintf("查询失败: %v", err)), nil
+	}
+
+	jsonBytes, _ := json.MarshalIndent(data, "", "  ")
+	return mcp.NewToolResultText(string(jsonBytes)), nil
+}
+
+func handleSearchSimCards(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	page := uint(req.GetFloat("page", 1))
+	size := uint(req.GetFloat("size", 10))
+	search := req.GetString("search", "")
+	status := req.GetString("status", "")
+	platform := req.GetString("platform", "")
+
+	var statusPtr *string
+	if status != "" {
+		statusPtr = &status
+	}
+	var platformPtr *string
+	if platform != "" {
+		platformPtr = &platform
+	}
+
+	data, err := services.GetSimCardBatchInfo(page, size, search, statusPtr, platformPtr)
+	if err != nil {
+		return mcp.NewToolResultError(fmt.Sprintf("搜索失败: %v", err)), nil
+	}
+
+	jsonBytes, _ := json.MarshalIndent(data, "", "  ")
+	return mcp.NewToolResultText(string(jsonBytes)), nil
+}

+ 70 - 0
service/cmd/mcp/tools/siteInfo.go

@@ -0,0 +1,70 @@
+package tools
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+
+	"github.com/mark3labs/mcp-go/mcp"
+	"github.com/mark3labs/mcp-go/server"
+
+	"fwh-toolbox-server/dto"
+	"fwh-toolbox-server/services"
+)
+
+func RegisterSiteInfoTools(s *server.MCPServer) {
+	// Tool: 查询站点信息
+	s.AddTool(
+		mcp.NewTool("query_site_info",
+			mcp.WithDescription("查询单个站点设备的详细信息,包括版本、网络状态、激活状态、固件、SN 等"),
+			mcp.WithString("sn", mcp.Required(), mcp.Description("站点 SN 序列号")),
+		),
+		handleQuerySiteInfo,
+	)
+
+	// Tool: 搜索站点列表
+	s.AddTool(
+		mcp.NewTool("search_sites",
+			mcp.WithDescription("搜索站点设备列表,支持分页和关键字搜索"),
+			mcp.WithNumber("page", mcp.Description("页码,从1开始,默认1")),
+			mcp.WithNumber("size", mcp.Description("每页数量,默认10")),
+			mcp.WithString("keyword", mcp.Description("搜索关键字(匹配 SN、设备名)")),
+		),
+		handleSearchSites,
+	)
+}
+
+func handleQuerySiteInfo(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	sn, err := req.RequireString("sn")
+	if err != nil {
+		return mcp.NewToolResultError("参数 sn 必填"), nil
+	}
+
+	data, err := services.GetSiteInfoData(sn)
+	if err != nil {
+		return mcp.NewToolResultError(fmt.Sprintf("查询失败: %v", err)), nil
+	}
+
+	jsonBytes, _ := json.MarshalIndent(data, "", "  ")
+	return mcp.NewToolResultText(string(jsonBytes)), nil
+}
+
+func handleSearchSites(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	page := uint(req.GetFloat("page", 1))
+	size := uint(req.GetFloat("size", 10))
+	keyword := req.GetString("keyword", "")
+
+	param := dto.SiteInfoRequest{
+		PageNum:  page,
+		PageSize: size,
+		Search:   keyword,
+	}
+
+	data, err := services.QuerySiteInfoDataList(param)
+	if err != nil {
+		return mcp.NewToolResultError(fmt.Sprintf("搜索失败: %v", err)), nil
+	}
+
+	jsonBytes, _ := json.MarshalIndent(data, "", "  ")
+	return mcp.NewToolResultText(string(jsonBytes)), nil
+}

+ 9 - 5
service/cmd/only_build.sh

@@ -1,9 +1,13 @@
 #!/bin/bash
 set -e
 
-# ===== 配置部分 =====
-APP_NAME="fwh-toolbox-server"                       # 服务名称
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
 
-# ===== 编译程序 =====
-echo "🚀 Building Go program..."
-GOOS=linux GOARCH=amd64 go build -o $APP_NAME ./   # 按需修改 ./ 为你的项目路径
+echo "===== 编译全部 ====="
+echo ""
+
+bash "$SCRIPT_DIR/only_build_toolbox.sh"
+bash "$SCRIPT_DIR/only_build_mcp.sh"
+
+echo ""
+echo "===== 全部编译完成 ====="

+ 8 - 0
service/cmd/only_build_mcp.sh

@@ -0,0 +1,8 @@
+#!/bin/bash
+set -e
+
+MCP_NAME="fwh-mcp-server"
+
+echo "🚀 Building $MCP_NAME..."
+GOOS=linux GOARCH=amd64 go build -o $MCP_NAME ./cmd/mcp/
+echo "✅ $MCP_NAME built successfully"

+ 8 - 0
service/cmd/only_build_toolbox.sh

@@ -0,0 +1,8 @@
+#!/bin/bash
+set -e
+
+APP_NAME="fwh-toolbox-server"
+
+echo "🚀 Building $APP_NAME..."
+GOOS=linux GOARCH=amd64 go build -o $APP_NAME ./
+echo "✅ $APP_NAME built successfully"

+ 68 - 0
service/cmd/test_acp/main.go

@@ -0,0 +1,68 @@
+package main
+
+import (
+	"bufio"
+	"encoding/json"
+	"fmt"
+	"os/exec"
+	"time"
+)
+
+func main() {
+	cmd := exec.Command("kiro-cli", "acp")
+	cmd.Dir = "."
+
+	stdin, _ := cmd.StdinPipe()
+	stdout, _ := cmd.StdoutPipe()
+
+	if err := cmd.Start(); err != nil {
+		fmt.Printf("启动失败: %v\n", err)
+		return
+	}
+
+	scanner := bufio.NewScanner(stdout)
+	scanner.Buffer(make([]byte, 0, 1024*1024), 1024*1024)
+
+	// 等待进程就绪
+	time.Sleep(500 * time.Millisecond)
+
+	// 发送 initialize
+	req := map[string]any{
+		"jsonrpc": "2.0",
+		"id":      1,
+		"method":  "initialize",
+		"params": map[string]any{
+			"protocolVersion":    1,
+			"clientCapabilities": map[string]any{},
+			"clientInfo":         map[string]any{"name": "test", "version": "1.0.0"},
+		},
+	}
+	data, _ := json.Marshal(req)
+	data = append(data, '\n')
+	stdin.Write(data)
+
+	fmt.Println("已发送 initialize,等待响应...")
+
+	// 读取响应(10秒超时)
+	done := make(chan string, 1)
+	go func() {
+		if scanner.Scan() {
+			done <- scanner.Text()
+		} else {
+			done <- fmt.Sprintf("scan error: %v", scanner.Err())
+		}
+	}()
+
+	select {
+	case line := <-done:
+		if len(line) > 200 {
+			line = line[:200]
+		}
+		fmt.Printf("响应: %s\n", line)
+	case <-time.After(10 * time.Second):
+		fmt.Println("超时!")
+	}
+
+	stdin.Close()
+	cmd.Process.Kill()
+}

+ 238 - 0
service/controllers/aiChatControllers.go

@@ -0,0 +1,238 @@
+package controllers
+
+import (
+	"bufio"
+	"encoding/json"
+	"fmt"
+	"fwh-toolbox-server/middleware"
+	"fwh-toolbox-server/services"
+
+	"github.com/gofiber/fiber/v2"
+)
+
+var acpManager *services.ACPManager
+
+// InitAIChat 初始化 AI Chat 模块
+func InitAIChat(serviceDir string) {
+	acpManager = services.NewACPManager(serviceDir, []string{"HOME=/home/jerry-remote"})
+}
+
+// AiChat 发送消息(SSE 流式响应)
+// POST /ai/chat
+func AiChat(c *fiber.Ctx) error {
+	var req struct {
+		Message   string `json:"message"`
+		SessionId string `json:"sessionId"`
+	}
+	if err := c.BodyParser(&req); err != nil {
+		return Fail(c, "Invalid request body")
+	}
+	if req.Message == "" {
+		return Fail(c, "message 不能为空")
+	}
+
+	userId := middleware.AuthGetUserName(c)
+
+	sess, err := acpManager.GetOrCreateSession(userId)
+	if err != nil {
+		return Error(c, fmt.Sprintf("AI 会话创建失败: %v", err))
+	}
+
+	// SSE 流式响应
+	c.Set("Content-Type", "text/event-stream")
+	c.Set("Cache-Control", "no-cache")
+	c.Set("Connection", "keep-alive")
+	c.Set("X-Accel-Buffering", "no")
+
+	c.Context().SetBodyStreamWriter(func(w *bufio.Writer) {
+		// 如果有欢迎消息且是新会话,先发送 welcome 事件
+		if sess.WelcomeMessage != "" {
+			welcomeEvent, _ := json.Marshal(services.AIChatEvent{Type: "welcome", Content: sess.WelcomeMessage})
+			fmt.Fprintf(w, "event: welcome\ndata: %s\n\n", string(welcomeEvent))
+			w.Flush()
+		}
+
+		eventCh := make(chan services.AIChatEvent, 50)
+
+		go acpManager.Prompt(sess, req.Message, eventCh)
+
+		for event := range eventCh {
+			data, _ := json.Marshal(event)
+			fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event.Type, string(data))
+			w.Flush()
+		}
+	})
+	return nil
+}
+
+// GetAiSessions 获取当前用户的会话信息
+// GET /ai/sessions
+func GetAiSessions(c *fiber.Ctx) error {
+	userId := middleware.AuthGetUserName(c)
+	sess, err := acpManager.GetOrCreateSession(userId)
+	if err != nil {
+		return Error(c, fmt.Sprintf("AI 会话创建失败: %v", err))
+	}
+
+	if sess == nil {
+		return Success(c, fiber.Map{
+			"active":    false,
+			"sessionId": "",
+		})
+	}
+
+	return Success(c, fiber.Map{
+		"active":         true,
+		"sessionId":      sess.SessionId,
+		"title":          sess.Title,
+		"welcomeMessage": sess.WelcomeMessage,
+		"lastActive":     sess.LastActive,
+	})
+}
+
+// GetAiSessionList 获取用户的会话历史列表
+// GET /ai/sessions/list
+func GetAiSessionList(c *fiber.Ctx) error {
+	userId := middleware.AuthGetUserName(c)
+	list := acpManager.GetSessionList(userId)
+	return Success(c, list)
+}
+
+// CreateAiSession 创建新的 AI 会话(保留旧会话到历史)
+// POST /ai/sessions/new
+func CreateAiSession(c *fiber.Ctx) error {
+	userId := middleware.AuthGetUserName(c)
+
+	// 创建新会话(旧会话自动归档到历史)
+	sess, err := acpManager.CreateNewSession(userId)
+	if err != nil {
+		return Error(c, fmt.Sprintf("创建会话失败: %v", err))
+	}
+
+	return Success(c, fiber.Map{
+		"sessionId": sess.SessionId,
+	})
+}
+
+// LoadAiSession 加载历史会话(恢复 AI 上下文)
+// POST /ai/sessions/:id/load
+func LoadAiSession(c *fiber.Ctx) error {
+	sessionId := c.Params("id")
+	if sessionId == "" {
+		return Fail(c, "sessionId 不能为空")
+	}
+
+	userId := middleware.AuthGetUserName(c)
+	sess, err := acpManager.LoadSession(userId, sessionId)
+	if err != nil {
+		return Error(c, fmt.Sprintf("加载会话失败: %v", err))
+	}
+
+	return Success(c, fiber.Map{
+		"sessionId":      sess.SessionId,
+		"title":          sess.Title,
+		"replayMessages": sess.ReplayMessages,
+	})
+}
+
+// UpdateAiSessionTitle 更新会话标题
+// PATCH /ai/sessions/:id
+func UpdateAiSessionTitle(c *fiber.Ctx) error {
+	sessionId := c.Params("id")
+	if sessionId == "" {
+		return Fail(c, "sessionId 不能为空")
+	}
+
+	var req struct {
+		Title string `json:"title"`
+	}
+	if err := c.BodyParser(&req); err != nil {
+		return Fail(c, "Invalid request body")
+	}
+
+	userId := middleware.AuthGetUserName(c)
+	acpManager.UpdateSessionTitle(userId, sessionId, req.Title)
+	return Success[any](c, nil, "标题已更新")
+}
+
+// DeleteAiSession 删除 AI 会话
+// DELETE /ai/sessions/:id
+func DeleteAiSession(c *fiber.Ctx) error {
+	sessionId := c.Params("id")
+	userId := middleware.AuthGetUserName(c)
+
+	if sessionId == "" {
+		// 无 id 参数,删除当前活跃会话
+		acpManager.DeleteSession(userId)
+	} else {
+		// 删除指定会话
+		acpManager.DeleteSessionFromHistory(userId, sessionId)
+	}
+
+	return Success[any](c, nil, "会话已删除")
+}
+
+// CancelAiChat 取消当前 AI 操作
+// POST /ai/cancel
+func CancelAiChat(c *fiber.Ctx) error {
+	userId := middleware.AuthGetUserName(c)
+	sess := acpManager.GetSessionInfo(userId)
+	if sess == nil {
+		return Fail(c, "没有活跃的会话")
+	}
+	acpManager.Cancel(sess)
+	return Success[any](c, nil, "已取消")
+}
+
+// ResolvePermission 前端回传写权限选择
+// POST /ai/permission
+func ResolvePermission(c *fiber.Ctx) error {
+	var req struct {
+		OptionId string `json:"optionId"`
+	}
+	if err := c.BodyParser(&req); err != nil {
+		return Fail(c, "Invalid request body")
+	}
+	if req.OptionId == "" {
+		return Fail(c, "optionId 不能为空")
+	}
+
+	userId := middleware.AuthGetUserName(c)
+	if err := acpManager.ResolvePermission(userId, req.OptionId); err != nil {
+		return Fail(c, err.Error())
+	}
+
+	return Success[any](c, nil, "已回复")
+}
+
+// DownloadAiFile 下载 AI 生成的文件
+// GET /ai/files/download?key=xxx
+func DownloadAiFile(c *fiber.Ctx) error {
+	key := c.Query("key")
+	if key == "" {
+		return Fail(c, "key 不能为空")
+	}
+
+	userId := middleware.AuthGetUserName(c)
+	sess := acpManager.GetSessionInfo(userId)
+	if sess == nil {
+		return Fail(c, "没有活跃的会话")
+	}
+
+	// 从 FileStore 查找文件记录
+	record := sess.Client.FileStore.GetFileByKey(key)
+	if record == nil {
+		return Fail(c, "文件不存在")
+	}
+
+	// 从 MinIO 读取文件内容
+	content, err := sess.Client.FileStore.ReadFile(record.Path)
+	if err != nil {
+		return Error(c, err.Error())
+	}
+
+	// 设置下载头(文件名为原始名,不含 uuid 前缀)
+	c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, record.FileName))
+	c.Set("Content-Type", "application/octet-stream")
+	return c.SendString(content)
+}

+ 9 - 6
service/go.mod

@@ -1,14 +1,18 @@
 module fwh-toolbox-server
 
-go 1.25.0
+go 1.25.5
 
 require (
 	github.com/bwmarrin/snowflake v0.3.0
 	github.com/casdoor/casdoor-go-sdk v1.21.0
+	github.com/eclipse/paho.mqtt.golang v1.5.1
 	github.com/go-echarts/go-echarts/v2 v2.6.6
 	github.com/gofiber/fiber/v2 v2.52.9
 	github.com/gofiber/swagger v1.1.1
 	github.com/google/uuid v1.6.0
+	github.com/hibiken/asynq v0.26.0
+	github.com/hibiken/asynqmon v0.7.2
+	github.com/mark3labs/mcp-go v0.57.0
 	github.com/redis/go-redis/v9 v9.21.0
 	github.com/robfig/cron/v3 v3.0.1
 	github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1
@@ -28,17 +32,17 @@ require (
 )
 
 require (
-	github.com/eclipse/paho.mqtt.golang v1.5.1 // indirect
+	github.com/google/jsonschema-go v0.4.2 // indirect
 	github.com/gorilla/mux v1.8.1 // indirect
 	github.com/gorilla/websocket v1.5.3 // indirect
-	github.com/hibiken/asynq v0.26.0 // indirect
-	github.com/hibiken/asynqmon v0.7.2 // indirect
 	github.com/mattn/go-sqlite3 v1.14.16 // indirect
 	github.com/richardlehane/mscfb v1.0.4 // indirect
 	github.com/richardlehane/msoleps v1.0.4 // indirect
+	github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
 	github.com/tiendc/go-deepcopy v1.7.1 // indirect
 	github.com/xuri/efp v0.0.1 // indirect
 	github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
+	github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
 	go.uber.org/atomic v1.11.0 // indirect
 	golang.org/x/time v0.15.0 // indirect
 	google.golang.org/protobuf v1.36.11 // indirect
@@ -51,7 +55,6 @@ require (
 	github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
 	github.com/andybalholm/brotli v1.1.0 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
-	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
 	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/fasthttp/websocket v1.5.3 // indirect
 	github.com/fsnotify/fsnotify v1.8.0 // indirect
@@ -110,7 +113,7 @@ require (
 	golang.org/x/mod v0.28.0 // indirect
 	golang.org/x/net v0.46.0 // indirect
 	golang.org/x/sync v0.17.0 // indirect
-	golang.org/x/sys v0.46.0 // indirect
+	golang.org/x/sys v0.46.0
 	golang.org/x/text v0.30.0 // indirect
 	golang.org/x/tools v0.37.0 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect

+ 15 - 11
service/go.sum

@@ -40,8 +40,9 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
+github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
@@ -127,10 +128,11 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
+github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
 github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -190,6 +192,8 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN
 github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
 github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
 github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mark3labs/mcp-go v0.57.0 h1:jzWKyCzdWnwnZt05cvcQQ+ngiUl2RnixXJa7Kj4qP1E=
+github.com/mark3labs/mcp-go v0.57.0/go.mod h1:+8WclSK1ZUweCP3hvktSji8n8ABG/95QaEkeVE/Uwas=
 github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
 github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@@ -255,8 +259,6 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
 github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
 github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
 github.com/redis/go-redis/v9 v9.0.4/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
-github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
-github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
 github.com/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E=
 github.com/redis/go-redis/v9 v9.21.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
 github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
@@ -269,13 +271,15 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
 github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
 github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
 github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
-github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
-github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
 github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
 github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
 github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
 github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
 github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
+github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
+github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
 github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
 github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
 github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I=
@@ -289,8 +293,6 @@ github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
 github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
 github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
-github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
-github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
 github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
 github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
 github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
@@ -337,11 +339,15 @@ github.com/xuri/excelize/v2 v2.10.0 h1:8aKsP7JD39iKLc6dH5Tw3dgV3sPRh8uRVXu/fMstf
 github.com/xuri/excelize/v2 v2.10.0/go.mod h1:SC5TzhQkaOsTWpANfm+7bJCldzcnU/jrhqkTi/iBHBU=
 github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
 github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
+github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
+github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
 github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
 github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
+github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
 go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
 go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
@@ -439,8 +445,6 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
-golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
 golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
 golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=

+ 4 - 0
service/main.go

@@ -3,6 +3,7 @@ package main
 import (
 	"fwh-toolbox-server/cache"
 	"fwh-toolbox-server/config"
+	"fwh-toolbox-server/controllers"
 	"fwh-toolbox-server/database"
 	"fwh-toolbox-server/repositories"
 	"fwh-toolbox-server/repositories/api/fwh_pro"
@@ -92,6 +93,9 @@ func main() {
 
 	routes.Setup(app)
 
+	// 初始化 AI Chat (ACP Client)
+	controllers.InitAIChat(".")
+
 	// 启动第一个监听
 	go func() {
 		if err := networkEvents.InitEvio(); err != nil {

+ 39 - 0
service/routes/aiChatRoutes.go

@@ -0,0 +1,39 @@
+package routes
+
+import (
+	"fwh-toolbox-server/controllers"
+
+	"github.com/gofiber/fiber/v2"
+)
+
+func AiChatSetup(api fiber.Router) {
+	// 发送消息(SSE 流式响应)
+	api.Post("/ai/chat", controllers.AiChat)
+
+	// 获取当前用户会话信息
+	api.Get("/ai/sessions", controllers.GetAiSessions)
+
+	// 获取会话历史列表
+	api.Get("/ai/sessions/list", controllers.GetAiSessionList)
+
+	// 创建新会话
+	api.Post("/ai/sessions/new", controllers.CreateAiSession)
+
+	// 加载历史会话(恢复 AI 上下文)
+	api.Post("/ai/sessions/:id/load", controllers.LoadAiSession)
+
+	// 更新会话标题
+	api.Patch("/ai/sessions/:id", controllers.UpdateAiSessionTitle)
+
+	// 删除会话
+	api.Delete("/ai/sessions/:id", controllers.DeleteAiSession)
+
+	// 取消当前操作
+	api.Post("/ai/cancel", controllers.CancelAiChat)
+
+	// 回复写权限请求
+	api.Post("/ai/permission", controllers.ResolvePermission)
+
+	// 下载 AI 生成的文件
+	api.Get("/ai/files/download", controllers.DownloadAiFile)
+}

+ 1 - 0
service/routes/routes.go

@@ -34,6 +34,7 @@ func Setup(app *fiber.App) {
 	FaultRecordSetup(api)
 	SimCardManageSetup(api)
 	FeedbackSetup(api)
+	AiChatSetup(api)
 
 	app.Get("/swagger/*", swagger.HandlerDefault)
 

+ 408 - 0
service/services/aiChatServices.go

@@ -0,0 +1,408 @@
+package services
+
+import (
+	"encoding/json"
+	"fmt"
+	"fwh-toolbox-server/utils/acp"
+	"sync"
+	"time"
+
+	"go.uber.org/zap"
+)
+
+// AIChatEvent SSE 事件(输出给前端)
+type AIChatEvent struct {
+	Type       string `json:"type"`                // text, tool_call, tool_update, done, error
+	Content    string `json:"content,omitempty"`   // 文本内容
+	ToolName   string `json:"name,omitempty"`      // 工具名
+	Status     string `json:"status,omitempty"`    // 工具状态
+	SessionId  string `json:"sessionId,omitempty"` // 会话ID
+	StopReason string `json:"stopReason,omitempty"`
+}
+
+// ACPSession 单个用户的 ACP 会话
+type ACPSession struct {
+	UserId         string
+	SessionId      string
+	Title          string
+	WelcomeMessage string
+	ReplayMessages []acp.ReplayMessage // session/load 回放的历史消息
+	Client         *acp.Client
+	Mutex          sync.Mutex
+	LastActive     time.Time
+	CreatedAt      time.Time
+}
+
+// SessionListItem 会话列表项(返回给前端)
+type SessionListItem struct {
+	SessionId  string `json:"sessionId"`
+	Title      string `json:"title"`
+	CreatedAt  int64  `json:"createdAt"`
+	LastActive int64  `json:"lastActive"`
+	IsActive   bool   `json:"isActive"` // 是否为当前激活的会话
+}
+
+// ACPManager 管理所有用户的 ACP 会话
+type ACPManager struct {
+	sessions   map[string]*ACPSession       // key: userId -> 当前活跃会话
+	history    map[string][]SessionListItem // key: userId -> 历史会话列表(元数据)
+	mu         sync.RWMutex
+	maxIdle    time.Duration
+	serviceDir string
+	acpConfig  acp.ClientConfig
+}
+
+// NewACPManager 创建 ACP 管理器
+func NewACPManager(serviceDir string, env []string) *ACPManager {
+	mgr := &ACPManager{
+		sessions:   make(map[string]*ACPSession),
+		history:    make(map[string][]SessionListItem),
+		maxIdle:    30 * time.Minute,
+		serviceDir: serviceDir,
+		acpConfig: acp.ClientConfig{
+			Command:    "kiro-cli",
+			Args:       []string{"acp", "--agent", "fwh-toolbox-agent"},
+			WorkingDir: serviceDir,
+			Env:        env,
+		},
+	}
+
+	// 定时清理空闲会话
+	go func() {
+		ticker := time.NewTicker(5 * time.Minute)
+		defer ticker.Stop()
+		for range ticker.C {
+			mgr.CleanupIdle()
+		}
+	}()
+
+	return mgr
+}
+
+// GetOrCreateSession 获取或创建用户的 ACP 会话
+func (m *ACPManager) GetOrCreateSession(userId string) (*ACPSession, error) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	if sess, ok := m.sessions[userId]; ok {
+		if sess.Client.IsAlive() {
+			sess.LastActive = time.Now()
+			return sess, nil
+		}
+		// 进程已退出,清理后重新创建
+		sess.Client.Close()
+		delete(m.sessions, userId)
+	}
+
+	return m.createNewSessionLocked(userId)
+}
+
+// CreateNewSession 强制创建一个新会话(保留旧会话到历史列表)
+func (m *ACPManager) CreateNewSession(userId string) (*ACPSession, error) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	// 将当前活跃会话存入历史
+	if sess, ok := m.sessions[userId]; ok {
+		m.archiveSessionLocked(userId, sess)
+		sess.Client.Close()
+		delete(m.sessions, userId)
+	}
+
+	return m.createNewSessionLocked(userId)
+}
+
+// LoadSession 加载历史会话(恢复 AI 上下文)
+func (m *ACPManager) LoadSession(userId string, sessionId string) (*ACPSession, error) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	// 将当前活跃会话存入历史
+	if sess, ok := m.sessions[userId]; ok {
+		if sess.SessionId == sessionId {
+			// 已经是当前会话,直接返回
+			sess.LastActive = time.Now()
+			return sess, nil
+		}
+		m.archiveSessionLocked(userId, sess)
+		sess.Client.Close()
+		delete(m.sessions, userId)
+	}
+
+	// 创建新 ACP Client 并加载历史会话
+	client, err := acp.NewClient(m.acpConfig)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := client.Initialize(); err != nil {
+		client.Close()
+		return nil, err
+	}
+
+	// 使用 session/load 恢复会话(含回放历史消息)
+	loadedId, replayMessages, err := client.LoadSession(sessionId, m.serviceDir)
+	if err != nil {
+		client.Close()
+		return nil, fmt.Errorf("加载会话失败: %w", err)
+	}
+
+	zap.L().Info("[AIChat] session/load 回放完成",
+		zap.String("sessionId", loadedId),
+		zap.Int("replayCount", len(replayMessages)),
+	)
+
+	// 从历史中找到标题
+	title := ""
+	if userHistory, ok := m.history[userId]; ok {
+		for i, item := range userHistory {
+			if item.SessionId == sessionId {
+				title = item.Title
+				// 从历史列表中移除(因为已变为活跃)
+				m.history[userId] = append(userHistory[:i], userHistory[i+1:]...)
+				break
+			}
+		}
+	}
+
+	sess := &ACPSession{
+		UserId:         userId,
+		SessionId:      loadedId,
+		Title:          title,
+		ReplayMessages: replayMessages,
+		Client:         client,
+		LastActive:     time.Now(),
+		CreatedAt:      time.Now(),
+	}
+	client.UserId = userId
+
+	m.sessions[userId] = sess
+	zap.L().Info("[AIChat] 会话加载成功", zap.String("userId", userId), zap.String("sessionId", loadedId))
+	return sess, nil
+}
+
+// GetSessionList 获取用户的会话列表(含当前活跃 + 历史)
+func (m *ACPManager) GetSessionList(userId string) []SessionListItem {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+
+	var list []SessionListItem
+
+	// 当前活跃会话排在最前
+	if sess, ok := m.sessions[userId]; ok {
+		list = append(list, SessionListItem{
+			SessionId:  sess.SessionId,
+			Title:      sess.Title,
+			CreatedAt:  sess.CreatedAt.UnixMilli(),
+			LastActive: sess.LastActive.UnixMilli(),
+			IsActive:   true,
+		})
+	}
+
+	// 历史会话
+	if userHistory, ok := m.history[userId]; ok {
+		list = append(list, userHistory...)
+	}
+
+	return list
+}
+
+// UpdateSessionTitle 更新会话标题
+func (m *ACPManager) UpdateSessionTitle(userId string, sessionId string, title string) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	// 检查当前活跃会话
+	if sess, ok := m.sessions[userId]; ok && sess.SessionId == sessionId {
+		sess.Title = title
+		return
+	}
+
+	// 检查历史会话
+	if userHistory, ok := m.history[userId]; ok {
+		for i := range userHistory {
+			if userHistory[i].SessionId == sessionId {
+				userHistory[i].Title = title
+				return
+			}
+		}
+	}
+}
+
+// DeleteSessionFromHistory 从历史列表中删除某会话
+func (m *ACPManager) DeleteSessionFromHistory(userId string, sessionId string) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	// 如果是当前活跃会话,关闭它
+	if sess, ok := m.sessions[userId]; ok && sess.SessionId == sessionId {
+		sess.Client.Close()
+		delete(m.sessions, userId)
+		return
+	}
+
+	// 从历史列表中删除
+	if userHistory, ok := m.history[userId]; ok {
+		for i, item := range userHistory {
+			if item.SessionId == sessionId {
+				m.history[userId] = append(userHistory[:i], userHistory[i+1:]...)
+				return
+			}
+		}
+	}
+}
+
+// createNewSessionLocked 内部方法:创建新会话(调用时必须持有 mu 锁)
+func (m *ACPManager) createNewSessionLocked(userId string) (*ACPSession, error) {
+	client, err := acp.NewClient(m.acpConfig)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := client.Initialize(); err != nil {
+		client.Close()
+		return nil, err
+	}
+
+	sessionId, welcomeMsg, err := client.NewSession(m.serviceDir)
+	if err != nil {
+		client.Close()
+		return nil, err
+	}
+
+	now := time.Now()
+	sess := &ACPSession{
+		UserId:         userId,
+		SessionId:      sessionId,
+		Title:          "",
+		WelcomeMessage: welcomeMsg,
+		Client:         client,
+		LastActive:     now,
+		CreatedAt:      now,
+	}
+	client.UserId = userId
+
+	m.sessions[userId] = sess
+	zap.L().Info("[AIChat] 会话创建成功", zap.String("userId", userId), zap.String("sessionId", sessionId))
+	return sess, nil
+}
+
+// archiveSessionLocked 内部方法:将会话存入历史(调用时必须持有 mu 锁)
+func (m *ACPManager) archiveSessionLocked(userId string, sess *ACPSession) {
+	item := SessionListItem{
+		SessionId:  sess.SessionId,
+		Title:      sess.Title,
+		CreatedAt:  sess.CreatedAt.UnixMilli(),
+		LastActive: sess.LastActive.UnixMilli(),
+		IsActive:   false,
+	}
+
+	if _, ok := m.history[userId]; !ok {
+		m.history[userId] = []SessionListItem{}
+	}
+
+	// 插入到历史列表头部(最近的在前)
+	m.history[userId] = append([]SessionListItem{item}, m.history[userId]...)
+
+	// 限制历史数量(最多保留 50 个)
+	if len(m.history[userId]) > 50 {
+		m.history[userId] = m.history[userId][:50]
+	}
+}
+
+// Prompt 发送消息并流式读取响应
+func (m *ACPManager) Prompt(sess *ACPSession, message string, eventCh chan<- AIChatEvent) {
+	sess.Mutex.Lock()
+	defer sess.Mutex.Unlock()
+
+	sess.LastActive = time.Now()
+
+	// 首次发消息时自动设置标题(取消息前 30 字)
+	if sess.Title == "" && message != "" {
+		title := message
+		if len([]rune(title)) > 30 {
+			title = string([]rune(title)[:30]) + "..."
+		}
+		sess.Title = title
+	}
+
+	// 用 acp.Client.Prompt 流式读取
+	acpEvents := make(chan acp.Event, 50)
+	go sess.Client.Prompt(sess.SessionId, message, acpEvents)
+
+	// 转换 acp.Event → AIChatEvent
+	for event := range acpEvents {
+		switch event.Type {
+		case "text":
+			eventCh <- AIChatEvent{Type: "text", Content: event.Content}
+		case "tool_call":
+			eventCh <- AIChatEvent{Type: "tool_call", ToolName: event.ToolName, Status: event.ToolStatus}
+		case "tool_update":
+			eventCh <- AIChatEvent{Type: "tool_update", ToolName: event.ToolName, Status: event.ToolStatus}
+		case "permission":
+			eventCh <- AIChatEvent{Type: "permission", Content: toJSON(event.Permission)}
+		case "file_created":
+			eventCh <- AIChatEvent{Type: "file_created", Content: toJSON(event.File)}
+		case "done":
+			eventCh <- AIChatEvent{Type: "done", SessionId: event.SessionId, StopReason: event.StopReason}
+		case "error":
+			eventCh <- AIChatEvent{Type: "error", Content: event.Content}
+		}
+	}
+
+	close(eventCh)
+}
+
+// Cancel 取消当前操作
+func (m *ACPManager) Cancel(sess *ACPSession) {
+	sess.Client.Cancel(sess.SessionId)
+}
+
+// ResolvePermission 前端回传权限选择
+func (m *ACPManager) ResolvePermission(userId string, optionId string) error {
+	sess := m.GetSessionInfo(userId)
+	if sess == nil {
+		return fmt.Errorf("没有活跃的会话")
+	}
+	sess.Client.ResolvePermission(optionId)
+	return nil
+}
+
+// DeleteSession 删除指定用户的当前活跃会话
+func (m *ACPManager) DeleteSession(userId string) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	if sess, ok := m.sessions[userId]; ok {
+		sess.Client.Close()
+		delete(m.sessions, userId)
+		zap.L().Info("[AIChat] 会话已删除", zap.String("userId", userId))
+	}
+}
+
+// GetSessionInfo 获取用户会话信息
+func (m *ACPManager) GetSessionInfo(userId string) *ACPSession {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+	return m.sessions[userId]
+}
+
+// CleanupIdle 清理空闲超时的会话
+func (m *ACPManager) CleanupIdle() {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	now := time.Now()
+	for userId, sess := range m.sessions {
+		if now.Sub(sess.LastActive) > m.maxIdle {
+			sess.Client.Close()
+			delete(m.sessions, userId)
+			zap.L().Info("[AIChat] 清理空闲会话", zap.String("userId", userId))
+		}
+	}
+}
+
+func toJSON(v any) string {
+	data, _ := json.Marshal(v)
+	return string(data)
+}

+ 985 - 0
service/utils/acp/acp.go

@@ -0,0 +1,985 @@
+// Package acp 实现 ACP (Agent Client Protocol) 客户端
+// 基于 ACP v1 规范,通过 stdin/stdout ND-JSON 与 Agent 子进程通讯
+package acp
+
+import (
+	"bufio"
+	"encoding/json"
+	"fmt"
+	"io"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"sync"
+	"sync/atomic"
+
+	"go.uber.org/zap"
+)
+
+// ============ JSON-RPC 2.0 基础类型 ============
+
+// Request JSON-RPC 请求
+type Request struct {
+	JSONRPC string `json:"jsonrpc"`
+	ID      int64  `json:"id"`
+	Method  string `json:"method"`
+	Params  any    `json:"params,omitempty"`
+}
+
+// Response JSON-RPC 响应
+type Response struct {
+	JSONRPC string          `json:"jsonrpc"`
+	ID      *int64          `json:"id,omitempty"`
+	Method  string          `json:"method,omitempty"`
+	Result  json.RawMessage `json:"result,omitempty"`
+	Error   *RPCError       `json:"error,omitempty"`
+	Params  json.RawMessage `json:"params,omitempty"`
+}
+
+// RPCError JSON-RPC 错误
+type RPCError struct {
+	Code    int    `json:"code"`
+	Message string `json:"message"`
+}
+
+// ============ ACP 协议类型 ============
+
+// InitializeParams 初始化请求参数
+type InitializeParams struct {
+	ProtocolVersion    int                `json:"protocolVersion"`
+	ClientInfo         ClientInfo         `json:"clientInfo"`
+	ClientCapabilities ClientCapabilities `json:"clientCapabilities"`
+}
+
+// ClientInfo 客户端信息
+type ClientInfo struct {
+	Name    string `json:"name"`
+	Version string `json:"version"`
+}
+
+// ClientCapabilities 客户端能力声明
+type ClientCapabilities struct {
+	FS       *FSCapabilities `json:"fs,omitempty"`
+	Terminal bool            `json:"terminal,omitempty"`
+}
+
+// FSCapabilities 文件系统能力
+type FSCapabilities struct {
+	ReadTextFile  bool `json:"readTextFile"`
+	WriteTextFile bool `json:"writeTextFile"`
+}
+
+// NewSessionParams 创建会话参数
+type NewSessionParams struct {
+	CWD        string `json:"cwd"`
+	MCPServers []any  `json:"mcpServers"`
+}
+
+// NewSessionResult 创建会话响应
+type NewSessionResult struct {
+	SessionId string        `json:"sessionId"`
+	Modes     *SessionModes `json:"modes,omitempty"`
+}
+
+// SessionModes 会话模式
+type SessionModes struct {
+	CurrentModeId  string        `json:"currentModeId"`
+	AvailableModes []SessionMode `json:"availableModes"`
+}
+
+// SessionMode 单个模式
+type SessionMode struct {
+	Id          string       `json:"id"`
+	Name        string       `json:"name"`
+	Description string       `json:"description"`
+	Meta        *SessionMeta `json:"_meta,omitempty"`
+}
+
+// SessionMeta 模式元数据
+type SessionMeta struct {
+	WelcomeMessage string `json:"welcomeMessage"`
+}
+
+// PromptParams 发送消息参数
+type PromptParams struct {
+	SessionId string         `json:"sessionId"`
+	Prompt    []ContentBlock `json:"prompt"`
+}
+
+// ContentBlock 内容块
+type ContentBlock struct {
+	Type string `json:"type"`
+	Text string `json:"text"`
+}
+
+// PromptResult prompt 响应
+type PromptResult struct {
+	StopReason string `json:"stopReason"`
+}
+
+// CancelParams 取消参数
+type CancelParams struct {
+	SessionId string `json:"sessionId"`
+}
+
+// ============ Session Update 事件类型 ============
+
+// SessionUpdate 会话更新通知
+type SessionUpdate struct {
+	SessionId string          `json:"sessionId"`
+	Update    json.RawMessage `json:"update"`
+}
+
+// UpdateBase 更新基础类型
+type UpdateBase struct {
+	SessionUpdate string `json:"sessionUpdate"`
+}
+
+// AgentMessageChunk Agent 文本流式输出
+type AgentMessageChunk struct {
+	SessionUpdate string       `json:"sessionUpdate"` // "agent_message_chunk"
+	MessageId     string       `json:"messageId,omitempty"`
+	Content       ContentBlock `json:"content"`
+}
+
+// ToolCall 工具调用通知
+type ToolCall struct {
+	SessionUpdate string `json:"sessionUpdate"` // "tool_call"
+	ToolCallId    string `json:"toolCallId"`
+	Title         string `json:"title"`
+	Status        string `json:"status"` // pending, running, completed, failed
+}
+
+// ToolCallUpdate 工具调用状态更新
+type ToolCallUpdate struct {
+	SessionUpdate string `json:"sessionUpdate"` // "tool_call_update"
+	ToolCallId    string `json:"toolCallId"`
+	Title         string `json:"title,omitempty"`
+	Status        string `json:"status"`
+}
+
+// ============ 客户端事件(对外输出) ============
+
+// Event 输出给上层的事件
+type Event struct {
+	Type       string // "text", "tool_call", "tool_update", "done", "error", "permission", "file_created"
+	Content    string
+	ToolName   string
+	ToolStatus string
+	SessionId  string
+	StopReason string
+	// 权限请求相关
+	Permission *PermissionEvent `json:"permission,omitempty"`
+	// 文件创建相关
+	File *FileRecord `json:"file,omitempty"`
+}
+
+// PermissionEvent 权限请求事件(推给前端)
+type PermissionEvent struct {
+	RequestId string             `json:"requestId"` // 用于前端回传
+	Title     string             `json:"title"`     // 权限标题
+	Options   []PermissionOption `json:"options"`   // 可选选项
+}
+
+// PermissionOption 权限选项
+type PermissionOption struct {
+	OptionId string `json:"optionId"`
+	Name     string `json:"name"`
+	Kind     string `json:"kind"` // allow_once, allow_always, deny
+}
+
+// ============ ACP Client 实现 ============
+
+// Client ACP 客户端(管理单个 Agent 进程)
+type Client struct {
+	cmd       *exec.Cmd
+	stdin     io.WriteCloser
+	scanner   *bufio.Scanner
+	sessionId string
+	requestId int64
+	mu        sync.Mutex // 保护 stdin 写入
+	readMu    sync.Mutex // 保护 stdout 读取
+
+	// 权限请求等待机制
+	permissionCh chan string  // 接收前端选择的 optionId
+	eventCh      chan<- Event // 当前 prompt 的事件通道(用于推送 permission 事件)
+
+	// 文件存储
+	FileStore *FileStore
+	UserId    string
+
+	// 文本累积(用于合并日志输出)
+	textBuffer string
+}
+
+// ClientConfig 客户端配置
+type ClientConfig struct {
+	Command    string   // Agent 命令(如 "kiro-cli")
+	Args       []string // 命令参数(如 ["acp"])
+	WorkingDir string   // 工作目录
+	Env        []string // 额外环境变量(如 "HOME=/home/user")
+}
+
+// NewClient 创建并启动 ACP Client
+func NewClient(cfg ClientConfig) (*Client, error) {
+	cmd := exec.Command(cfg.Command, cfg.Args...)
+
+	// 确保工作目录是绝对路径
+	absDir, err := filepath.Abs(cfg.WorkingDir)
+	if err != nil {
+		absDir = cfg.WorkingDir
+	}
+	cmd.Dir = absDir
+
+	// 设置环境变量
+	cmd.Env = append(os.Environ(), cfg.Env...)
+
+	stdin, err := cmd.StdinPipe()
+	if err != nil {
+		return nil, fmt.Errorf("创建 stdin pipe 失败: %w", err)
+	}
+
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		return nil, fmt.Errorf("创建 stdout pipe 失败: %w", err)
+	}
+
+	// 捕获 stderr
+	stderrPipe, _ := cmd.StderrPipe()
+	go func() {
+		if stderrPipe != nil {
+			s := bufio.NewScanner(stderrPipe)
+			for s.Scan() {
+				zap.L().Warn("[ACP] agent stderr", zap.String("line", s.Text()))
+			}
+		}
+	}()
+
+	if err := cmd.Start(); err != nil {
+		return nil, fmt.Errorf("启动 Agent 进程失败: %w", err)
+	}
+
+	scanner := bufio.NewScanner(stdout)
+	scanner.Buffer(make([]byte, 0, 2*1024*1024), 2*1024*1024) // 2MB buffer
+
+	return &Client{
+		cmd:          cmd,
+		stdin:        stdin,
+		scanner:      scanner,
+		permissionCh: make(chan string, 1),
+		FileStore:    NewFileStore(),
+	}, nil
+}
+
+// ResolvePermission 前端回传权限选择结果
+func (c *Client) ResolvePermission(optionId string) {
+	select {
+	case c.permissionCh <- optionId:
+	default:
+		// 没有等待中的权限请求,忽略
+	}
+}
+
+// Initialize 执行初始化握手
+func (c *Client) Initialize() error {
+	params := InitializeParams{
+		ProtocolVersion: 1,
+		ClientInfo: ClientInfo{
+			Name:    "fwh-toolbox",
+			Version: "1.0.0",
+		},
+		ClientCapabilities: ClientCapabilities{
+			FS: &FSCapabilities{
+				ReadTextFile:  true,
+				WriteTextFile: true,
+			},
+			Terminal: false, // 不声明终端能力,强制 Agent 使用 writeTextFile
+		},
+	}
+
+	_, err := c.sendAndWait("initialize", params)
+	return err
+}
+
+// NewSession 创建新会话,返回 sessionId 和 welcomeMessage
+func (c *Client) NewSession(cwd string) (string, string, error) {
+	params := NewSessionParams{
+		CWD:        cwd,
+		MCPServers: []any{},
+	}
+
+	result, err := c.sendAndWait("session/new", params)
+	if err != nil {
+		return "", "", err
+	}
+
+	var res NewSessionResult
+	if err := json.Unmarshal(result, &res); err != nil {
+		return "", "", fmt.Errorf("解析 session/new 响应失败: %w", err)
+	}
+
+	c.sessionId = res.SessionId
+
+	// 提取当前 mode 的 welcomeMessage
+	var welcomeMsg string
+	if res.Modes != nil {
+		for _, mode := range res.Modes.AvailableModes {
+			if mode.Id == res.Modes.CurrentModeId && mode.Meta != nil {
+				welcomeMsg = mode.Meta.WelcomeMessage
+				break
+			}
+		}
+	}
+
+	return res.SessionId, welcomeMsg, nil
+}
+
+// ReplayMessage 回放的单条消息
+type ReplayMessage struct {
+	Role      string `json:"role"` // "user" 或 "assistant"
+	Content   string `json:"content"`
+	MessageId string `json:"messageId,omitempty"`
+}
+
+// LoadSession 加载已有会话(恢复历史对话上下文),返回回放的消息列表
+func (c *Client) LoadSession(sessionId string, cwd string) (string, []ReplayMessage, error) {
+	params := map[string]any{
+		"sessionId":  sessionId,
+		"cwd":        cwd,
+		"mcpServers": []any{},
+	}
+
+	// 发送 session/load 请求
+	id := c.sendRequest("session/load", params)
+
+	// 收集回放消息
+	var replayMessages []ReplayMessage
+	currentMsg := &ReplayMessage{}
+
+	// 读取回放事件直到收到 session/load 的 response
+	for c.scanner.Scan() {
+		line := c.scanner.Text()
+		if line == "" {
+			continue
+		}
+
+		var msg struct {
+			JSONRPC string          `json:"jsonrpc"`
+			ID      json.RawMessage `json:"id,omitempty"`
+			Method  string          `json:"method,omitempty"`
+			Result  json.RawMessage `json:"result,omitempty"`
+			Error   *RPCError       `json:"error,omitempty"`
+			Params  json.RawMessage `json:"params,omitempty"`
+		}
+		if err := json.Unmarshal([]byte(line), &msg); err != nil {
+			continue
+		}
+
+		// 检查是否是 session/load 的响应
+		hasId := len(msg.ID) > 0 && string(msg.ID) != "null"
+		if hasId && msg.Method == "" {
+			var numId int64
+			json.Unmarshal(msg.ID, &numId)
+			if numId == id {
+				// session/load 完成
+				if msg.Error != nil {
+					return "", nil, fmt.Errorf("session/load 错误: %s", msg.Error.Message)
+				}
+				c.sessionId = sessionId
+				// 追加最后一条未关闭的消息
+				if currentMsg.Content != "" {
+					replayMessages = append(replayMessages, *currentMsg)
+				}
+				return sessionId, replayMessages, nil
+			}
+		}
+
+		// 处理回放的 session/update 通知
+		if msg.Method == "session/update" || msg.Method == "_kiro.dev/session/update" {
+			var notif SessionUpdate
+			if err := json.Unmarshal(msg.Params, &notif); err != nil {
+				continue
+			}
+			var base UpdateBase
+			if err := json.Unmarshal(notif.Update, &base); err != nil {
+				continue
+			}
+
+			switch base.SessionUpdate {
+			case "user_message_chunk":
+				// 新的用户消息 → 保存上一条并开始新消息
+				if currentMsg.Content != "" {
+					replayMessages = append(replayMessages, *currentMsg)
+				}
+				var chunk AgentMessageChunk
+				json.Unmarshal(notif.Update, &chunk)
+				currentMsg = &ReplayMessage{Role: "user", Content: chunk.Content.Text, MessageId: chunk.MessageId}
+
+			case "agent_message_chunk":
+				// AI 消息
+				var chunk AgentMessageChunk
+				json.Unmarshal(notif.Update, &chunk)
+				if currentMsg.Role != "assistant" || (chunk.MessageId != "" && chunk.MessageId != currentMsg.MessageId) {
+					// 新的 AI 消息 → 保存上一条
+					if currentMsg.Content != "" {
+						replayMessages = append(replayMessages, *currentMsg)
+					}
+					currentMsg = &ReplayMessage{Role: "assistant", Content: chunk.Content.Text, MessageId: chunk.MessageId}
+				} else {
+					// 同一条消息的续写
+					currentMsg.Content += chunk.Content.Text
+				}
+
+			case "agent_thought_chunk":
+				// 思考内容忽略(不显示给用户)
+
+			case "tool_call", "tool_call_update", "tool_call_chunk":
+				// 工具调用回放忽略
+
+			default:
+				// 其他类型忽略
+			}
+			continue
+		}
+
+		// 忽略其他通知(kiro 扩展等)
+	}
+
+	return "", nil, fmt.Errorf("session/load 未收到响应(进程可能已退出)")
+}
+
+// Prompt 发送消息并流式读取事件
+// eventCh 会在 turn 结束或出错时关闭
+func (c *Client) Prompt(sessionId string, message string, eventCh chan<- Event) {
+	defer close(eventCh)
+
+	// 设置当前事件通道(供 handleAgentRequest 推送 permission 事件)
+	c.eventCh = eventCh
+
+	params := PromptParams{
+		SessionId: sessionId,
+		Prompt: []ContentBlock{
+			{Type: "text", Text: message},
+		},
+	}
+
+	// 发送 prompt 请求
+	promptId := c.sendRequest("session/prompt", params)
+	zap.L().Debug("[ACP] prompt 已发送", zap.Int64("promptId", promptId), zap.String("sessionId", sessionId))
+
+	// 持续读取直到收到 prompt 的 response
+	for c.scanner.Scan() {
+		line := c.scanner.Text()
+		if line == "" {
+			continue
+		}
+
+		// 用宽松结构解析,兼容所有消息格式
+		var msg struct {
+			JSONRPC string          `json:"jsonrpc"`
+			ID      json.RawMessage `json:"id,omitempty"`
+			Method  string          `json:"method,omitempty"`
+			Result  json.RawMessage `json:"result,omitempty"`
+			Error   *RPCError       `json:"error,omitempty"`
+			Params  json.RawMessage `json:"params,omitempty"`
+		}
+		if err := json.Unmarshal([]byte(line), &msg); err != nil {
+			zap.L().Warn("[ACP] JSON解析失败,跳过", zap.String("line", line[:min(len(line), 100)]))
+			continue
+		}
+
+		// 解析 ID(兼容 int64 和 string)
+		hasId := len(msg.ID) > 0 && string(msg.ID) != "null"
+		var numericId int64
+		isPromptResp := false
+		if hasId {
+			json.Unmarshal(msg.ID, &numericId)
+			isPromptResp = numericId == promptId && msg.Method == ""
+		}
+
+		// 收到 prompt 的响应 → turn 结束
+		if isPromptResp {
+			zap.L().Debug("[ACP] 收到 prompt 响应(turn结束)", zap.Int64("id", numericId))
+			// 打印本轮完整回复
+			if c.textBuffer != "" {
+				zap.L().Info("[ACP] AI 回复完整内容", zap.String("text", c.textBuffer))
+				c.textBuffer = ""
+			}
+			var result PromptResult
+			if msg.Error != nil {
+				eventCh <- Event{Type: "error", Content: msg.Error.Message}
+			} else if err := json.Unmarshal(msg.Result, &result); err == nil {
+				eventCh <- Event{Type: "done", SessionId: sessionId, StopReason: result.StopReason}
+			}
+			return
+		}
+
+		// 处理 notification (session/update 或 _kiro.dev/session/update)
+		if !hasId && (msg.Method == "session/update" || msg.Method == "_kiro.dev/session/update") {
+			c.handleSessionUpdate(msg.Params, eventCh)
+			continue
+		}
+
+		// 处理 Agent→Client 反向请求(有 ID + Method = 请求)
+		if hasId && msg.Method != "" {
+			zap.L().Debug("[ACP] 收到 Agent 反向请求", zap.String("method", msg.Method), zap.String("id", string(msg.ID)))
+			c.handleAgentRequest(msg.ID, msg.Method, msg.Params)
+			continue
+		}
+
+		// 未匹配的消息(kiro扩展通知等)
+		if msg.Method != "" {
+			c.handleExtensionNotification(msg.Method, msg.Params, eventCh)
+		} else {
+			zap.L().Debug("[ACP] 未处理的消息", zap.String("line", line[:min(len(line), 200)]))
+		}
+	}
+
+	// Scanner 结束说明进程退出
+	eventCh <- Event{Type: "error", Content: "Agent 进程已断开"}
+}
+
+// Cancel 取消当前操作
+func (c *Client) Cancel(sessionId string) {
+	c.sendRequest("session/cancel", CancelParams{SessionId: sessionId})
+}
+
+// GetSessionId 获取当前会话 ID
+func (c *Client) GetSessionId() string {
+	return c.sessionId
+}
+
+// Close 关闭 ACP Client,终止 Agent 进程
+func (c *Client) Close() {
+	// 关闭 stdin 通知 Agent 退出
+	c.stdin.Close()
+
+	// 终止进程
+	if c.cmd.Process != nil {
+		c.cmd.Process.Kill()
+		// Wait 回收子进程资源,防止僵尸进程
+		go c.cmd.Wait()
+	}
+
+	// 解除可能阻塞在 permissionCh 上的 goroutine
+	select {
+	case c.permissionCh <- "deny":
+	default:
+	}
+
+	// 清理 FileStore
+	if c.FileStore != nil {
+		c.FileStore.mu.Lock()
+		c.FileStore.files = nil
+		c.FileStore.mu.Unlock()
+	}
+}
+
+// IsAlive 检查 Agent 进程是否存活
+func (c *Client) IsAlive() bool {
+	return c.cmd.ProcessState == nil
+}
+
+// ============ 内部方法 ============
+
+// sendRequest 发送请求(不等待响应),返回 request ID
+func (c *Client) sendRequest(method string, params any) int64 {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	id := atomic.AddInt64(&c.requestId, 1)
+	req := Request{
+		JSONRPC: "2.0",
+		ID:      id,
+		Method:  method,
+		Params:  params,
+	}
+
+	data, err := json.Marshal(req)
+	if err != nil {
+		zap.L().Error("[ACP] 序列化请求失败", zap.Error(err))
+		return id
+	}
+
+	data = append(data, '\n')
+	if _, err := c.stdin.Write(data); err != nil {
+		zap.L().Error("[ACP] 写入 stdin 失败", zap.String("method", method), zap.Error(err))
+	}
+
+	return id
+}
+
+// sendAndWait 发送请求并等待对应 ID 的响应
+func (c *Client) sendAndWait(method string, params any) (json.RawMessage, error) {
+	id := c.sendRequest(method, params)
+
+	// 读取响应(跳过 notification 和其他消息)
+	for c.scanner.Scan() {
+		line := c.scanner.Text()
+		if line == "" {
+			continue
+		}
+
+		var resp Response
+		if err := json.Unmarshal([]byte(line), &resp); err != nil {
+			continue
+		}
+
+		// 匹配 ID 的响应
+		if resp.ID != nil && *resp.ID == id {
+			if resp.Error != nil {
+				return nil, fmt.Errorf("ACP 错误 [%d]: %s", resp.Error.Code, resp.Error.Message)
+			}
+			return resp.Result, nil
+		}
+
+		// 跳过 notification(session/new 之前可能收到 kiro 扩展通知)
+	}
+
+	return nil, fmt.Errorf("未收到 %s 的响应(进程可能已退出)", method)
+}
+
+// handleSessionUpdate 解析并分发 session/update 通知
+func (c *Client) handleSessionUpdate(params json.RawMessage, eventCh chan<- Event) {
+	var notif SessionUpdate
+	if err := json.Unmarshal(params, &notif); err != nil {
+		return
+	}
+
+	// 解析 update 的类型
+	var base UpdateBase
+	if err := json.Unmarshal(notif.Update, &base); err != nil {
+		return
+	}
+
+	// zap.L().Debug("[ACP] session/update", zap.String("type", base.SessionUpdate))
+
+	switch base.SessionUpdate {
+	case "agent_message_chunk":
+		var chunk AgentMessageChunk
+		if err := json.Unmarshal(notif.Update, &chunk); err == nil {
+			c.textBuffer += chunk.Content.Text
+			eventCh <- Event{Type: "text", Content: chunk.Content.Text}
+		}
+
+	case "agent_thought_chunk":
+		var chunk AgentMessageChunk
+		if err := json.Unmarshal(notif.Update, &chunk); err == nil {
+			c.textBuffer += chunk.Content.Text
+			eventCh <- Event{Type: "text", Content: chunk.Content.Text}
+		}
+
+	case "tool_call":
+		var tc ToolCall
+		if err := json.Unmarshal(notif.Update, &tc); err == nil {
+			zap.L().Debug("[ACP] → tool_call", zap.String("title", tc.Title), zap.String("status", tc.Status), zap.String("id", tc.ToolCallId))
+			eventCh <- Event{Type: "tool_call", ToolName: tc.Title, ToolStatus: tc.Status}
+		}
+
+	case "tool_call_update":
+		var tcu ToolCallUpdate
+		if err := json.Unmarshal(notif.Update, &tcu); err == nil {
+			zap.L().Debug("[ACP] → tool_call_update", zap.String("title", tcu.Title), zap.String("status", tcu.Status), zap.String("id", tcu.ToolCallId))
+			eventCh <- Event{Type: "tool_update", ToolName: tcu.Title, ToolStatus: tcu.Status}
+		}
+
+	case "tool_call_chunk":
+		// 工具调用的流式参数输出(Kiro 扩展)
+		zap.L().Debug("[ACP] → tool_call_chunk (ignored)")
+
+	case "plan":
+		zap.L().Debug("[ACP] → plan update")
+
+	default:
+		zap.L().Debug("[ACP] → 未知 session/update 类型", zap.String("type", base.SessionUpdate))
+	}
+}
+
+// handleExtensionNotification 处理 Kiro 扩展通知
+func (c *Client) handleExtensionNotification(method string, params json.RawMessage, eventCh chan<- Event) {
+	switch method {
+	case "_kiro.dev/commands/available":
+		// 可用命令和工具列表
+		var data struct {
+			SessionId string `json:"sessionId"`
+			Commands  []struct {
+				Name        string `json:"name"`
+				Description string `json:"description"`
+			} `json:"commands"`
+			Tools []struct {
+				Name        string `json:"name"`
+				Description string `json:"description"`
+				Source      string `json:"source"`
+			} `json:"tools"`
+		}
+		if err := json.Unmarshal(params, &data); err == nil {
+			zap.L().Debug("[ACP] 可用命令/工具更新",
+				zap.Int("commands", len(data.Commands)),
+				zap.Int("tools", len(data.Tools)),
+			)
+			// 推送工具列表给前端(可选)
+			if len(data.Tools) > 0 {
+				toolNames := make([]string, 0, len(data.Tools))
+				for _, t := range data.Tools {
+					toolNames = append(toolNames, t.Name)
+				}
+				eventCh <- Event{Type: "tools_available", Content: toJSONStr(toolNames)}
+			}
+		}
+
+	case "_kiro.dev/metadata":
+		// 上下文使用量和耗时
+		var data struct {
+			SessionId              string  `json:"sessionId"`
+			ContextUsagePercentage float64 `json:"contextUsagePercentage"`
+			TurnDurationMs         int64   `json:"turnDurationMs,omitempty"`
+		}
+		if err := json.Unmarshal(params, &data); err == nil {
+			if data.TurnDurationMs > 0 {
+				zap.L().Debug("[ACP] metadata",
+					zap.Float64("contextUsage%", data.ContextUsagePercentage),
+					zap.Int64("turnMs", data.TurnDurationMs),
+				)
+				eventCh <- Event{
+					Type: "metadata",
+					Content: toJSONStr(map[string]any{
+						"contextUsagePercent": data.ContextUsagePercentage,
+						"turnDurationMs":      data.TurnDurationMs,
+					}),
+				}
+			}
+		}
+
+	case "_kiro.dev/subagent/list_update":
+		// 子代理列表更新
+		zap.L().Debug("[ACP] 子代理列表更新")
+
+	case "_kiro.dev/mcp/server_initialized":
+		// MCP Server 初始化完成
+		var data struct {
+			ServerName string `json:"serverName"`
+		}
+		json.Unmarshal(params, &data)
+		zap.L().Info("[ACP] MCP Server 初始化完成", zap.String("server", data.ServerName))
+
+	case "_kiro.dev/compaction/status":
+		// 上下文压缩状态
+		zap.L().Debug("[ACP] 上下文压缩中")
+
+	case "_kiro.dev/clear/status":
+		// 会话清除状态
+		zap.L().Debug("[ACP] 会话清除中")
+
+	default:
+		zap.L().Debug("[ACP] 未处理的扩展通知", zap.String("method", method))
+	}
+}
+
+// toJSONStr 序列化为 JSON 字符串
+func toJSONStr(v any) string {
+	data, _ := json.Marshal(v)
+	return string(data)
+}
+
+// handleAgentRequest 处理 Agent→Client 的反向请求
+func (c *Client) handleAgentRequest(rawId json.RawMessage, method string, rawParams json.RawMessage) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	zap.L().Debug("[ACP] 处理反向请求", zap.String("method", method), zap.String("id", string(rawId)), zap.String("params", string(rawParams[:min(len(rawParams), 200)])))
+
+	var respData []byte
+
+	switch method {
+	case "readTextFile":
+		// 读取文件:先从 FileStore 查找(MinIO),再从本地文件系统
+		var params struct {
+			Path  string `json:"path"`
+			Line  *int   `json:"line,omitempty"`
+			Limit *int   `json:"limit,omitempty"`
+		}
+		if err := json.Unmarshal(rawParams, &params); err == nil && params.Path != "" {
+			var content string
+			var readErr error
+
+			// 优先从 FileStore(MinIO)读取
+			if c.FileStore.HasFile(params.Path) {
+				content, readErr = c.FileStore.ReadFile(params.Path)
+				zap.L().Debug("[ACP] readTextFile 从 MinIO 读取", zap.String("path", params.Path))
+			} else {
+				// 回退到本地文件系统
+				data, err := os.ReadFile(params.Path)
+				if err == nil {
+					content = string(data)
+					zap.L().Debug("[ACP] readTextFile 从本地读取", zap.String("path", params.Path))
+				} else {
+					readErr = err
+				}
+			}
+
+			if readErr == nil {
+				result, _ := json.Marshal(map[string]string{"content": content})
+				respData, _ = json.Marshal(map[string]any{
+					"jsonrpc": "2.0",
+					"id":      rawId,
+					"result":  json.RawMessage(result),
+				})
+			}
+		}
+		if respData == nil {
+			respData, _ = json.Marshal(map[string]any{
+				"jsonrpc": "2.0",
+				"id":      rawId,
+				"error":   map[string]any{"code": -1, "message": "file not found"},
+			})
+		}
+
+	case "writeTextFile":
+		// 写入文件到 MinIO(不写本地)
+		var params struct {
+			Path    string `json:"path"`
+			Content string `json:"content"`
+		}
+		if err := json.Unmarshal(rawParams, &params); err == nil && params.Path != "" {
+			record, err := c.FileStore.WriteFile(c.UserId, c.sessionId, params.Path, params.Content)
+			if err == nil {
+				respData, _ = json.Marshal(map[string]any{
+					"jsonrpc": "2.0",
+					"id":      rawId,
+					"result":  map[string]any{},
+				})
+				// 推送 file_created 事件给前端
+				if c.eventCh != nil {
+					c.eventCh <- Event{Type: "file_created", File: record}
+				}
+			} else {
+				zap.L().Error("[ACP] writeTextFile MinIO上传失败", zap.Error(err))
+			}
+		}
+		if respData == nil {
+			respData, _ = json.Marshal(map[string]any{
+				"jsonrpc": "2.0",
+				"id":      rawId,
+				"error":   map[string]any{"code": -1, "message": "write failed"},
+			})
+		}
+
+	case "requestPermission", "session/request_permission":
+		// 解析权限请求参数
+		var params struct {
+			ToolCall *struct {
+				Title string `json:"title"`
+			} `json:"toolCall"`
+			Options []struct {
+				OptionId string `json:"optionId"`
+				Name     string `json:"name"`
+				Kind     string `json:"kind"`
+			} `json:"options"`
+		}
+		json.Unmarshal(rawParams, &params)
+
+		title := ""
+		if params.ToolCall != nil {
+			title = params.ToolCall.Title
+		}
+
+		// 判断是否涉及写操作(需要手动授权)
+		isWriteOp := true
+		// isWriteOp := false
+		// if title != "" {
+		// 	for _, keyword := range []string{"write", "Write", "create", "Create", "delete", "Delete", "修改", "写入", "创建", "删除"} {
+		// 		if len(title) >= len(keyword) && containsStr(title, keyword) {
+		// 			isWriteOp = true
+		// 			break
+		// 		}
+		// 	}
+		// }
+
+		if isWriteOp && c.eventCh != nil {
+			// 写操作:推送权限请求给前端,等待前端回复
+			options := make([]PermissionOption, 0, len(params.Options))
+			for _, opt := range params.Options {
+				options = append(options, PermissionOption{
+					OptionId: opt.OptionId,
+					Name:     opt.Name,
+					Kind:     opt.Kind,
+				})
+			}
+
+			requestId := fmt.Sprintf("perm_%s", string(rawId))
+			c.eventCh <- Event{
+				Type: "permission",
+				Permission: &PermissionEvent{
+					RequestId: requestId,
+					Title:     title,
+					Options:   options,
+				},
+			}
+
+			// 释放 mu 锁后等待前端回复(否则会死锁)
+			c.mu.Unlock()
+			selectedOptionId := <-c.permissionCh
+			c.mu.Lock()
+
+			respData, _ = json.Marshal(map[string]any{
+				"jsonrpc": "2.0",
+				"id":      rawId,
+				"result": map[string]any{
+					"outcome": map[string]any{
+						"outcome":  "selected",
+						"optionId": selectedOptionId,
+					},
+				},
+			})
+		} else {
+			// 非写操作:自动允许
+			respData, _ = json.Marshal(map[string]any{
+				"jsonrpc": "2.0",
+				"id":      rawId,
+				"result": map[string]any{
+					"outcome": map[string]any{
+						"outcome":  "selected",
+						"optionId": "allow_once",
+					},
+				},
+			})
+		}
+
+	default:
+		// 未知请求返回错误
+		respData, _ = json.Marshal(map[string]any{
+			"jsonrpc": "2.0",
+			"id":      rawId,
+			"error":   map[string]any{"code": -32601, "message": "method not supported"},
+		})
+	}
+
+	if respData != nil {
+		zap.L().Debug("[ACP] 发送反向响应", zap.String("method", method), zap.String("resp", string(respData[:min(len(respData), 200)])))
+		respData = append(respData, '\n')
+		c.stdin.Write(respData)
+	} else {
+		zap.L().Warn("[ACP] 反向请求无响应数据", zap.String("method", method))
+	}
+}
+
+// containsStr 简单字符串包含检测
+func containsStr(s, substr string) bool {
+	return len(s) >= len(substr) && (s == substr || len(s) > 0 && stringContains(s, substr))
+}
+
+func stringContains(s, substr string) bool {
+	for i := 0; i <= len(s)-len(substr); i++ {
+		if s[i:i+len(substr)] == substr {
+			return true
+		}
+	}
+	return false
+}
+
+func min(a, b int) int {
+	if a < b {
+		return a
+	}
+	return b
+}

+ 174 - 0
service/utils/acp/filestore.go

@@ -0,0 +1,174 @@
+package acp
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"path/filepath"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/google/uuid"
+	"github.com/minio/minio-go/v7"
+	minioUtil "fwh-toolbox-server/utils/minio"
+	"go.uber.org/zap"
+)
+
+const AIFileBucket = "fwh-toolbox-ai-file"
+const AIFileExpireDays = 7
+
+// FileRecord 文件记录
+type FileRecord struct {
+	Key       string `json:"key"`       // MinIO object key
+	FileName  string `json:"fileName"`  // 原始文件名
+	Path      string `json:"path"`      // Agent 请求的路径
+	Size      int64  `json:"size"`      // 文件大小
+	CreatedAt int64  `json:"createdAt"` // 创建时间戳
+}
+
+// FileStore 会话级别的虚拟文件系统
+type FileStore struct {
+	mu    sync.RWMutex
+	files map[string]*FileRecord // path → FileRecord(同路径覆盖更新)
+}
+
+// NewFileStore 创建文件存储
+func NewFileStore() *FileStore {
+	return &FileStore{
+		files: make(map[string]*FileRecord),
+	}
+}
+
+// WriteFile 将内容上传到 MinIO,返回文件记录
+func (fs *FileStore) WriteFile(userId, sessionId, path, content string) (*FileRecord, error) {
+	fileName := filepath.Base(path)
+	uniqueId := uuid.New().String()[:8]
+	objectKey := fmt.Sprintf("%s/%s/%s_%s", userId, sessionId, uniqueId, fileName)
+
+	// 上传到 MinIO(带7天过期)
+	reader := strings.NewReader(content)
+	ctx := context.Background()
+
+	_, err := minioUtil.Client.PutObject(ctx, AIFileBucket, objectKey, reader, int64(len(content)), minio.PutObjectOptions{
+		ContentType: detectContentType(fileName),
+		Expires:     time.Now().Add(AIFileExpireDays * 24 * time.Hour),
+	})
+	if err != nil {
+		return nil, fmt.Errorf("上传文件到MinIO失败: %w", err)
+	}
+
+	record := &FileRecord{
+		Key:       objectKey,
+		FileName:  fileName,
+		Path:      path,
+		Size:      int64(len(content)),
+		CreatedAt: time.Now().Unix(),
+	}
+
+	// 存入内存映射(同路径覆盖)
+	fs.mu.Lock()
+	fs.files[path] = record
+	fs.mu.Unlock()
+
+	zap.L().Info("[ACP-FileStore] 文件已保存到MinIO",
+		zap.String("path", path),
+		zap.String("key", objectKey),
+		zap.String("fileName", fileName),
+	)
+
+	return record, nil
+}
+
+// ReadFile 从 MinIO 读取文件内容
+func (fs *FileStore) ReadFile(path string) (string, error) {
+	fs.mu.RLock()
+	record, ok := fs.files[path]
+	fs.mu.RUnlock()
+
+	if !ok {
+		return "", fmt.Errorf("文件不存在: %s", path)
+	}
+
+	ctx := context.Background()
+	obj, err := minioUtil.Client.GetObject(ctx, AIFileBucket, record.Key, minio.GetObjectOptions{})
+	if err != nil {
+		return "", fmt.Errorf("从MinIO读取文件失败: %w", err)
+	}
+	defer obj.Close()
+
+	data, err := io.ReadAll(obj)
+	if err != nil {
+		return "", fmt.Errorf("读取文件内容失败: %w", err)
+	}
+
+	return string(data), nil
+}
+
+// HasFile 检查路径是否有文件记录
+func (fs *FileStore) HasFile(path string) bool {
+	fs.mu.RLock()
+	defer fs.mu.RUnlock()
+	_, ok := fs.files[path]
+	return ok
+}
+
+// GetFiles 获取所有文件记录
+func (fs *FileStore) GetFiles() []FileRecord {
+	fs.mu.RLock()
+	defer fs.mu.RUnlock()
+
+	result := make([]FileRecord, 0, len(fs.files))
+	for _, record := range fs.files {
+		result = append(result, *record)
+	}
+	return result
+}
+
+// GetFileByKey 按 MinIO key 查找文件记录
+func (fs *FileStore) GetFileByKey(key string) *FileRecord {
+	fs.mu.RLock()
+	defer fs.mu.RUnlock()
+
+	for _, record := range fs.files {
+		if record.Key == key {
+			return record
+		}
+	}
+	return nil
+}
+
+// detectContentType 根据文件扩展名推断 Content-Type
+func detectContentType(fileName string) string {
+	ext := strings.ToLower(filepath.Ext(fileName))
+	switch ext {
+	case ".txt":
+		return "text/plain; charset=utf-8"
+	case ".json":
+		return "application/json"
+	case ".html", ".htm":
+		return "text/html"
+	case ".css":
+		return "text/css"
+	case ".js":
+		return "application/javascript"
+	case ".ts":
+		return "application/typescript"
+	case ".go":
+		return "text/x-go"
+	case ".py":
+		return "text/x-python"
+	case ".md":
+		return "text/markdown"
+	case ".csv":
+		return "text/csv"
+	case ".xml":
+		return "application/xml"
+	case ".yaml", ".yml":
+		return "text/yaml"
+	case ".sh":
+		return "text/x-shellscript"
+	default:
+		return "application/octet-stream"
+	}
+}

+ 221 - 0
src/layout/header/Actions/AiChat/ChatPanel.vue

@@ -0,0 +1,221 @@
+<script setup lang="ts">
+import { NInput, NButton, NEmpty, NScrollbar, NPopconfirm, NSpin } from 'naive-ui'
+import { ref, nextTick, watch, onMounted } from 'vue'
+import { useAiChat } from './useAiChat'
+import MessageItem from './MessageItem.vue'
+
+const {
+  messages,
+  isStreaming,
+  welcomeMessage,
+  sessionList,
+  currentSessionId,
+  isLoadingSession,
+  sendMessage,
+  cancelChat,
+  clearMessages,
+  resolvePermission,
+  fetchSession,
+  createNewSession,
+  loadSession,
+  deleteSession,
+} = useAiChat()
+
+const inputText = ref('')
+const scrollRef = ref<InstanceType<typeof NScrollbar>>()
+const showSidebar = ref(false)
+
+onMounted(() => {
+  fetchSession()
+})
+
+// 自动滚动到底部
+watch(
+  () => messages.value[messages.value.length - 1]?.content,
+  () => {
+    nextTick(() => {
+      scrollRef.value?.scrollTo({ top: 99999, behavior: 'smooth' })
+    })
+  },
+)
+
+function handleSend() {
+  if (!inputText.value.trim() || isStreaming.value) return
+  const msg = inputText.value
+  inputText.value = ''
+  sendMessage(msg)
+}
+
+function handleKeydown(e: KeyboardEvent) {
+  if (e.key === 'Enter' && !e.shiftKey) {
+    e.preventDefault()
+    handleSend()
+  }
+}
+
+function handleNewSession() {
+  createNewSession()
+  showSidebar.value = false
+}
+
+function handleLoadSession(sessionId: string) {
+  loadSession(sessionId)
+  showSidebar.value = false
+}
+
+function formatTime(ts: number): string {
+  if (!ts) return ''
+  const d = new Date(ts)
+  const now = new Date()
+  const diff = now.getTime() - d.getTime()
+  if (diff < 60000) return '刚刚'
+  if (diff < 3600000) return `${Math.floor(diff / 60000)} 分钟前`
+  if (diff < 86400000) return `${Math.floor(diff / 3600000)} 小时前`
+  if (diff < 604800000) return `${Math.floor(diff / 86400000)} 天前`
+  return d.toLocaleDateString()
+}
+</script>
+
+<template>
+  <div class="relative flex h-full flex-col">
+    <!-- 顶部操作栏 -->
+    <div class="flex items-center justify-between border-b px-3 py-2 dark:border-neutral-700">
+      <div class="flex items-center gap-2">
+        <NButton text size="small" @click="showSidebar = !showSidebar">
+          <span class="i-ph:list text-sm" />
+        </NButton>
+        <span class="text-xs text-neutral-400">AI 助手</span>
+      </div>
+      <div class="flex items-center gap-1">
+        <NButton text size="small" title="新建对话" @click="handleNewSession">
+          <span class="i-ph:plus text-sm" />
+        </NButton>
+        <NButton text size="small" title="清空当前消息" @click="clearMessages">
+          <span class="i-ph:trash text-sm" />
+        </NButton>
+      </div>
+    </div>
+
+    <!-- 会话列表侧边栏 -->
+    <Transition name="slide">
+      <div
+        v-if="showSidebar"
+        class="absolute inset-y-0 left-0 z-10 w-56 border-r bg-white flex flex-col dark:border-neutral-700 dark:bg-neutral-800"
+      >
+        <div class="flex items-center justify-between border-b px-3 py-2 dark:border-neutral-700">
+          <span class="text-xs font-medium text-neutral-500">对话历史</span>
+          <NButton text size="tiny" @click="showSidebar = false">
+            <span class="i-ph:x text-xs" />
+          </NButton>
+        </div>
+        <NScrollbar class="flex-1">
+          <div class="p-2 space-y-1">
+            <div
+              v-for="sess in sessionList"
+              :key="sess.sessionId"
+              class="group cursor-pointer rounded-md px-2 py-1.5 text-xs transition-colors hover:bg-neutral-100 dark:hover:bg-neutral-700"
+              :class="{ 'bg-blue-50 dark:bg-blue-900/20': sess.isActive }"
+              @click="handleLoadSession(sess.sessionId)"
+            >
+              <div class="flex items-center justify-between">
+                <span class="flex-1 truncate text-neutral-700 dark:text-neutral-300">
+                  {{ sess.title || '新对话' }}
+                </span>
+                <NPopconfirm @positive-click.stop="deleteSession(sess.sessionId)">
+                  <template #trigger>
+                    <NButton text size="tiny" class="hidden! group-hover:block!" @click.stop>
+                      <span class="i-ph:trash text-xs text-neutral-400" />
+                    </NButton>
+                  </template>
+                  删除此对话?
+                </NPopconfirm>
+              </div>
+              <div class="mt-0.5 text-neutral-400 text-[10px]">
+                {{ formatTime(sess.lastActive) }}
+              </div>
+            </div>
+            <NEmpty v-if="sessionList.length === 0" description="暂无历史对话" size="small" />
+          </div>
+        </NScrollbar>
+      </div>
+    </Transition>
+
+    <!-- 加载中遮罩 -->
+    <div
+      v-if="isLoadingSession"
+      class="absolute inset-0 z-20 flex items-center justify-center bg-white/60 dark:bg-neutral-900/60"
+    >
+      <NSpin size="medium" />
+    </div>
+
+    <!-- 消息列表 -->
+    <NScrollbar ref="scrollRef" class="flex-1">
+      <div class="space-y-3 p-4">
+        <NEmpty v-if="messages.length === 0 && !welcomeMessage" description="输入消息开始对话" />
+        <div
+          v-else-if="messages.length === 0 && welcomeMessage"
+          class="text-sm text-neutral-500 whitespace-pre-wrap"
+        >
+          {{ welcomeMessage }}
+        </div>
+        <MessageItem
+          v-for="msg in messages"
+          :key="msg.id"
+          :message="msg"
+          @resolve-permission="resolvePermission"
+        />
+        <!-- 流式输出中的提示 -->
+        <div
+          v-if="isStreaming && messages[messages.length - 1]?.content === ''"
+          class="flex justify-start"
+        >
+          <div
+            class="rounded-lg bg-neutral-100 px-3 py-2 text-sm text-neutral-500 dark:bg-neutral-700"
+          >
+            <span class="animate-pulse">思考中...</span>
+          </div>
+        </div>
+      </div>
+    </NScrollbar>
+
+    <!-- 输入区域 -->
+    <div class="border-t p-3 dark:border-neutral-700">
+      <div class="flex gap-2">
+        <NInput
+          v-model:value="inputText"
+          type="textarea"
+          :autosize="{ minRows: 1, maxRows: 4 }"
+          placeholder="输入消息...(Shift+Enter 换行)"
+          :disabled="isStreaming"
+          @keydown="handleKeydown"
+        />
+        <div class="flex flex-col justify-end">
+          <NButton
+            v-if="!isStreaming"
+            type="primary"
+            size="small"
+            :disabled="!inputText.trim()"
+            @click="handleSend"
+          >
+            <span class="i-ph:paper-plane-tilt" />
+          </NButton>
+          <NButton v-else type="error" size="small" @click="cancelChat">
+            <span class="i-ph:stop-circle" />
+          </NButton>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.slide-enter-active,
+.slide-leave-active {
+  transition: transform 0.2s ease;
+}
+
+.slide-enter-from,
+.slide-leave-to {
+  transform: translateX(-100%);
+}
+</style>

+ 239 - 0
src/layout/header/Actions/AiChat/MessageItem.vue

@@ -0,0 +1,239 @@
+<script setup lang="ts">
+import { NTag, NSpin, NButton, NCard } from 'naive-ui'
+import { computed } from 'vue'
+import { marked } from 'marked'
+import DOMPurify from 'dompurify'
+import type { ChatMessage } from './interface'
+
+const props = defineProps<{
+  message: ChatMessage
+}>()
+
+const emit = defineEmits<{
+  resolvePermission: [optionId: string]
+}>()
+
+const isUser = computed(() => props.message.role === 'user')
+
+const formattedContent = computed(() => {
+  if (isUser.value) {
+    // 用户消息保持纯文本显示
+    return props.message.content.replace(/\n/g, '<br/>')
+  }
+  // AI 回复使用 Markdown 渲染
+  const raw = marked.parse(props.message.content, { async: false }) as string
+  return DOMPurify.sanitize(raw)
+})
+
+function getDownloadUrl(key: string): string {
+  const base = import.meta.env.BASE_URL || '/'
+  return `${base}api/ai/files/download?key=${encodeURIComponent(key)}`
+}
+
+function formatSize(bytes: number): string {
+  if (bytes < 1024) return `${bytes}B`
+  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`
+  return `${(bytes / 1024 / 1024).toFixed(1)}MB`
+}
+</script>
+
+<template>
+  <div class="flex" :class="isUser ? 'justify-end' : 'justify-start'">
+    <div
+      class="max-w-[80%] rounded-lg px-3 py-2 text-sm"
+      :class="isUser
+        ? 'bg-blue-500 text-white'
+        : 'bg-neutral-100 text-neutral-800 dark:bg-neutral-700 dark:text-neutral-200'"
+    >
+      <!-- 工具调用标签 -->
+      <div v-if="message.toolCalls?.length" class="mb-2 flex flex-wrap gap-1">
+        <NTag
+          v-for="tc in message.toolCalls"
+          :key="tc.name"
+          size="small"
+          :type="tc.status === 'running' ? 'warning' : tc.status === 'completed' ? 'success' : 'error'"
+        >
+          <template #icon>
+            <NSpin v-if="tc.status === 'running'" :size="10" />
+          </template>
+          {{ tc.name }}
+        </NTag>
+      </div>
+
+      <!-- 权限请求 UI -->
+      <div v-if="message.permission && !message.permission.resolved" class="my-2">
+        <NCard size="small" :bordered="true" class="!bg-yellow-50 dark:!bg-yellow-900/20">
+          <div class="mb-2 text-xs font-medium text-yellow-700 dark:text-yellow-300">
+            🔒 写操作授权请求
+          </div>
+          <div class="mb-2 text-xs text-neutral-600 dark:text-neutral-300">
+            {{ message.permission.title }}
+          </div>
+          <div class="flex flex-wrap gap-2">
+            <NButton
+              v-for="opt in message.permission.options"
+              :key="opt.optionId"
+              size="tiny"
+              :type="opt.kind === 'deny' ? 'error' : opt.kind === 'allow_always' ? 'warning' : 'success'"
+              @click="emit('resolvePermission', opt.optionId)"
+            >
+              {{ opt.name }}
+            </NButton>
+          </div>
+        </NCard>
+      </div>
+
+      <!-- 权限已授权提示 -->
+      <div v-if="message.permission?.resolved" class="my-1">
+        <NTag size="small" type="success" :bordered="false">
+          ✅ 已授权
+        </NTag>
+      </div>
+
+      <!-- 文件下载按钮 -->
+      <div v-if="message.files?.length" class="my-2 space-y-1">
+        <div v-for="file in message.files" :key="file.key" class="flex items-center gap-2">
+          <NButton
+            size="tiny"
+            type="primary"
+            tag="a"
+            :href="getDownloadUrl(file.key)"
+            target="_blank"
+          >
+            <template #icon>
+              <span class="i-ph:download-simple" />
+            </template>
+            {{ file.fileName }}
+            <span class="ml-1 text-xs opacity-70">({{ formatSize(file.size) }})</span>
+          </NButton>
+        </div>
+      </div>
+
+      <!-- 消息内容 -->
+      <div
+        v-if="message.content"
+        :class="{ 'markdown-body': !isUser }"
+        v-html="formattedContent"
+      />
+    </div>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.markdown-body {
+  line-height: 1.6;
+
+  :deep(h1),
+  :deep(h2),
+  :deep(h3),
+  :deep(h4),
+  :deep(h5),
+  :deep(h6) {
+    margin-top: 0.5em;
+    margin-bottom: 0.25em;
+    font-weight: 600;
+    line-height: 1.4;
+  }
+
+  :deep(h1) { font-size: 1.25em; }
+  :deep(h2) { font-size: 1.15em; }
+  :deep(h3) { font-size: 1.05em; }
+
+  :deep(p) {
+    margin: 0.4em 0;
+  }
+
+  :deep(ul),
+  :deep(ol) {
+    padding-left: 1.5em;
+    margin: 0.4em 0;
+  }
+
+  :deep(li) {
+    margin: 0.2em 0;
+  }
+
+  :deep(code) {
+    background: rgba(127, 127, 127, 0.15);
+    border-radius: 3px;
+    padding: 0.15em 0.4em;
+    font-size: 0.9em;
+    font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
+  }
+
+  :deep(pre) {
+    background: rgba(0, 0, 0, 0.06);
+    border-radius: 6px;
+    padding: 0.75em 1em;
+    margin: 0.5em 0;
+    overflow-x: auto;
+
+    code {
+      background: none;
+      padding: 0;
+      border-radius: 0;
+      font-size: 0.85em;
+    }
+  }
+
+  :deep(blockquote) {
+    border-left: 3px solid rgba(127, 127, 127, 0.4);
+    padding-left: 0.75em;
+    margin: 0.5em 0;
+    color: rgba(127, 127, 127, 0.8);
+  }
+
+  :deep(table) {
+    border-collapse: collapse;
+    margin: 0.5em 0;
+    width: 100%;
+    font-size: 0.9em;
+  }
+
+  :deep(th),
+  :deep(td) {
+    border: 1px solid rgba(127, 127, 127, 0.3);
+    padding: 0.3em 0.6em;
+    text-align: left;
+  }
+
+  :deep(th) {
+    background: rgba(127, 127, 127, 0.1);
+    font-weight: 600;
+  }
+
+  :deep(hr) {
+    border: none;
+    border-top: 1px solid rgba(127, 127, 127, 0.3);
+    margin: 0.75em 0;
+  }
+
+  :deep(a) {
+    color: #3b82f6;
+    text-decoration: underline;
+  }
+
+  :deep(img) {
+    max-width: 100%;
+    border-radius: 4px;
+  }
+
+  :deep(> :first-child) {
+    margin-top: 0;
+  }
+
+  :deep(> :last-child) {
+    margin-bottom: 0;
+  }
+}
+
+:global(.dark) .markdown-body {
+  :deep(pre) {
+    background: rgba(255, 255, 255, 0.06);
+  }
+
+  :deep(a) {
+    color: #60a5fa;
+  }
+}
+</style>

+ 64 - 0
src/layout/header/Actions/AiChat/chatDB.ts

@@ -0,0 +1,64 @@
+import { openDB } from 'idb'
+import type { ChatMessage, LocalChatSession } from './interface'
+
+const DB_NAME = 'ai-chat-history'
+const DB_VERSION = 1
+const STORE_NAME = 'sessions'
+
+function getDB() {
+  return openDB(DB_NAME, DB_VERSION, {
+    upgrade(db) {
+      if (!db.objectStoreNames.contains(STORE_NAME)) {
+        const store = db.createObjectStore(STORE_NAME, { keyPath: 'sessionId' })
+        store.createIndex('updatedAt', 'updatedAt')
+      }
+    },
+  })
+}
+
+export const chatDB = {
+  /** 保存会话消息到 IndexedDB */
+  async saveSession(sessionId: string, title: string, messages: ChatMessage[]) {
+    const db = await getDB()
+    const existing = await db.get(STORE_NAME, sessionId)
+    const session: LocalChatSession = {
+      sessionId,
+      title,
+      messages: [...messages],
+      createdAt: existing?.createdAt || Date.now(),
+      updatedAt: Date.now(),
+    }
+    await db.put(STORE_NAME, session)
+  },
+
+  /** 获取会话消息 */
+  async getSession(sessionId: string): Promise<LocalChatSession | undefined> {
+    const db = await getDB()
+    return db.get(STORE_NAME, sessionId)
+  },
+
+  /** 删除会话 */
+  async deleteSession(sessionId: string) {
+    const db = await getDB()
+    await db.delete(STORE_NAME, sessionId)
+  },
+
+  /** 获取所有本地会话列表(按更新时间倒序) */
+  async getAllSessions(): Promise<LocalChatSession[]> {
+    const db = await getDB()
+    const all = await db.getAll(STORE_NAME)
+    return all.sort((a, b) => b.updatedAt - a.updatedAt)
+  },
+
+  /** 获取所有本地缓存的 sessionId 列表 */
+  async getAllSessionIds(): Promise<string[]> {
+    const db = await getDB()
+    return db.getAllKeys(STORE_NAME) as Promise<string[]>
+  },
+
+  /** 清除所有本地会话数据 */
+  async clearAll() {
+    const db = await getDB()
+    await db.clear(STORE_NAME)
+  },
+}

+ 35 - 0
src/layout/header/Actions/AiChat/index.vue

@@ -0,0 +1,35 @@
+<script setup lang="ts">
+import { NButton, NDrawer, NDrawerContent } from 'naive-ui'
+import { ref } from 'vue'
+import ChatPanel from './ChatPanel.vue'
+
+defineOptions({
+  name: 'AiChatAction',
+})
+
+const drawerShow = ref(false)
+const drawerWidth = ref(480)
+</script>
+
+<template>
+  <!-- 顶部栏按钮 -->
+  <NButton text :focusable="false" @click="drawerShow = true">
+    <template #icon>
+      <span class="i-ph:chat-circle-dots text-lg" />
+    </template>
+  </NButton>
+
+  <!-- 侧边抽屉(可拖拽宽度) -->
+  <NDrawer
+    v-model:show="drawerShow"
+    v-model:width="drawerWidth"
+    :min-width="320"
+    :max-width="800"
+    resizable
+    placement="right"
+  >
+    <NDrawerContent closable content-class="!p-0 h-full">
+      <ChatPanel />
+    </NDrawerContent>
+  </NDrawer>
+</template>

+ 60 - 0
src/layout/header/Actions/AiChat/interface.ts

@@ -0,0 +1,60 @@
+export interface ChatMessage {
+  id: string
+  role: 'user' | 'assistant'
+  content: string
+  toolCalls?: ToolCallInfo[]
+  permission?: PermissionRequest
+  files?: FileInfo[]
+  timestamp: number
+}
+
+export interface ToolCallInfo {
+  name: string
+  status: 'running' | 'completed' | 'error'
+}
+
+export interface PermissionRequest {
+  requestId: string
+  title: string
+  options: PermissionOption[]
+  resolved?: boolean
+}
+
+export interface PermissionOption {
+  optionId: string
+  name: string
+  kind: string
+}
+
+export interface FileInfo {
+  key: string
+  fileName: string
+  size: number
+}
+
+export interface AIChatSession {
+  active: boolean
+  sessionId: string
+  title?: string
+  welcomeMessage?: string
+  lastActive?: string
+}
+
+// ============ 会话历史管理 ============
+
+export interface SessionListItem {
+  sessionId: string
+  title: string
+  createdAt: number
+  lastActive: number
+  isActive: boolean
+}
+
+// IndexedDB 本地存储的会话消息
+export interface LocalChatSession {
+  sessionId: string
+  title: string
+  messages: ChatMessage[]
+  createdAt: number
+  updatedAt: number
+}

+ 433 - 0
src/layout/header/Actions/AiChat/useAiChat.ts

@@ -0,0 +1,433 @@
+import { ref, reactive } from 'vue'
+import type { ChatMessage, ToolCallInfo, SessionListItem } from './interface'
+import { useUserStore } from '@/stores/user'
+import { chatDB } from './chatDB'
+
+export function useAiChat() {
+  const messages = ref<ChatMessage[]>([])
+  const isStreaming = ref(false)
+  const currentSessionId = ref('')
+  const welcomeMessage = ref('')
+  const sessionList = ref<SessionListItem[]>([])
+  const isLoadingSession = ref(false)
+
+  const userStore = useUserStore()
+
+  function getBaseUrl(): string {
+    const base = import.meta.env.BASE_URL || '/'
+    return `${base}api`
+  }
+
+  // ============ 会话列表管理 ============
+
+  // 获取会话列表
+  async function fetchSessionList() {
+    try {
+      const resp = await fetch(`${getBaseUrl()}/ai/sessions/list`, {
+        headers: { Authorization: `Bearer ${userStore.token}` },
+      })
+      if (resp.ok) {
+        const json = await resp.json()
+        if (json.data) {
+          sessionList.value = json.data
+
+          // 清理本地缓存中不存在于后端列表的会话
+          const serverIds = new Set(json.data.map((s: any) => s.sessionId))
+          const localIds = await chatDB.getAllSessionIds()
+          for (const localId of localIds) {
+            if (!serverIds.has(localId)) {
+              await chatDB.deleteSession(localId)
+            }
+          }
+        }
+      }
+    } catch {
+      // 忽略
+    }
+  }
+
+  // 获取当前会话信息
+  async function fetchSession() {
+    try {
+      const resp = await fetch(`${getBaseUrl()}/ai/sessions`, {
+        headers: { Authorization: `Bearer ${userStore.token}` },
+      })
+      if (resp.ok) {
+        const json = await resp.json()
+        if (json.data?.welcomeMessage) {
+          welcomeMessage.value = json.data.welcomeMessage
+        }
+        if (json.data?.sessionId) {
+          currentSessionId.value = json.data.sessionId
+          // 从 IndexedDB 恢复本地消息
+          await loadLocalMessages(json.data.sessionId)
+        }
+      }
+    } catch {
+      // 忽略
+    }
+    // 同时获取会话列表
+    await fetchSessionList()
+  }
+
+  // 创建新会话
+  async function createNewSession() {
+    try {
+      const resp = await fetch(`${getBaseUrl()}/ai/sessions/new`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+          Authorization: `Bearer ${userStore.token}`,
+        },
+      })
+      if (resp.ok) {
+        const json = await resp.json()
+        if (json.data?.sessionId) {
+          // 保存当前会话消息到 IndexedDB
+          if (currentSessionId.value && messages.value.length > 0) {
+            await saveLocalMessages()
+          }
+          // 切换到新会话
+          currentSessionId.value = json.data.sessionId
+          messages.value = []
+          welcomeMessage.value = ''
+          // 刷新列表
+          await fetchSessionList()
+        }
+      }
+    } catch {
+      // 忽略
+    }
+  }
+
+  // 加载历史会话
+  async function loadSession(sessionId: string) {
+    if (sessionId === currentSessionId.value) return
+    isLoadingSession.value = true
+
+    try {
+      // 保存当前会话消息到 IndexedDB
+      if (currentSessionId.value && messages.value.length > 0) {
+        await saveLocalMessages()
+      }
+
+      // 先尝试从 IndexedDB 缓存加载
+      const cached = await chatDB.getSession(sessionId)
+      if (cached && cached.messages.length > 0) {
+        currentSessionId.value = sessionId
+        messages.value = cached.messages
+        welcomeMessage.value = ''
+
+        // 后台静默调用 session/load 恢复 Kiro 上下文(不等待回放结果)
+        fetch(`${getBaseUrl()}/ai/sessions/${sessionId}/load`, {
+          method: 'POST',
+          headers: {
+            'Content-Type': 'application/json',
+            Authorization: `Bearer ${userStore.token}`,
+          },
+        })
+        await fetchSessionList()
+      } else {
+        // 无缓存:调用 session/load 获取回放消息
+        const resp = await fetch(`${getBaseUrl()}/ai/sessions/${sessionId}/load`, {
+          method: 'POST',
+          headers: {
+            'Content-Type': 'application/json',
+            Authorization: `Bearer ${userStore.token}`,
+          },
+        })
+        if (resp.ok) {
+          const json = await resp.json()
+          if (json.data?.sessionId) {
+            currentSessionId.value = json.data.sessionId
+            welcomeMessage.value = ''
+
+            // 将回放消息转为 ChatMessage 格式
+            if (json.data.replayMessages && json.data.replayMessages.length > 0) {
+              messages.value = json.data.replayMessages.map((m: any, i: number) => ({
+                id: m.messageId || `replay_${i}`,
+                role: m.role,
+                content: m.content,
+                timestamp: Date.now() - (json.data.replayMessages.length - i) * 1000,
+              }))
+              // 保存到缓存
+              await saveLocalMessages()
+            } else {
+              messages.value = []
+            }
+
+            await fetchSessionList()
+          }
+        }
+      }
+    } catch {
+      // 忽略
+    } finally {
+      isLoadingSession.value = false
+    }
+  }
+
+  // 删除会话
+  async function deleteSession(sessionId: string) {
+    try {
+      const resp = await fetch(`${getBaseUrl()}/ai/sessions/${sessionId}`, {
+        method: 'DELETE',
+        headers: { Authorization: `Bearer ${userStore.token}` },
+      })
+      if (resp.ok) {
+        // 清除 IndexedDB 中的消息
+        await chatDB.deleteSession(sessionId)
+        // 如果删的是当前会话,清空消息
+        if (sessionId === currentSessionId.value) {
+          messages.value = []
+          currentSessionId.value = ''
+        }
+        // 刷新列表
+        await fetchSessionList()
+      }
+    } catch {
+      // 忽略
+    }
+  }
+
+  // ============ IndexedDB 本地消息管理 ============
+
+  async function saveLocalMessages() {
+    if (!currentSessionId.value || messages.value.length === 0) return
+    const title = getSessionTitle()
+    await chatDB.saveSession(currentSessionId.value, title, toRaw(messages.value))
+  }
+
+  async function loadLocalMessages(sessionId: string) {
+    const session = await chatDB.getSession(sessionId)
+    if (session) {
+      messages.value = session.messages
+    } else {
+      messages.value = []
+    }
+  }
+
+  function getSessionTitle(): string {
+    const firstUserMsg = messages.value.find((m) => m.role === 'user')
+    if (firstUserMsg) {
+      const text = firstUserMsg.content
+      return text.length > 30 ? `${text.slice(0, 30)}...` : text
+    }
+    return '新对话'
+  }
+
+  // ============ 消息发送 ============
+
+  function generateId(): string {
+    return `${Date.now()}-${Math.random().toString(36).slice(2)}`
+  }
+
+  async function sendMessage(content: string) {
+    if (!content.trim() || isStreaming.value) return
+
+    // 添加用户消息
+    messages.value.push({
+      id: generateId(),
+      role: 'user',
+      content: content.trim(),
+      timestamp: Date.now(),
+    })
+
+    // 添加 AI 消息占位
+    messages.value.push({
+      id: generateId(),
+      role: 'assistant',
+      content: '',
+      toolCalls: [],
+      timestamp: Date.now(),
+    })
+    const aiMessage: ChatMessage = messages.value.at(-1)!
+
+    isStreaming.value = true
+
+    try {
+      const response = await fetch(`${getBaseUrl()}/ai/chat`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+          Authorization: `Bearer ${userStore.token}`,
+        },
+        body: JSON.stringify({
+          message: content.trim(),
+          sessionId: currentSessionId.value,
+        }),
+      })
+
+      if (!response.ok) {
+        aiMessage.content = `错误: ${response.status} ${response.statusText}`
+        isStreaming.value = false
+        return
+      }
+
+      const reader = response.body?.getReader()
+      if (!reader) {
+        aiMessage.content = '错误: 无法读取响应流'
+        isStreaming.value = false
+        return
+      }
+
+      const decoder = new TextDecoder()
+      let buffer = ''
+
+      while (true) {
+        const { done, value } = await reader.read()
+        if (done) break
+
+        buffer += decoder.decode(value, { stream: true })
+
+        // 按 \n\n 分割 SSE 事件
+        const parts = buffer.split('\n\n')
+        buffer = parts.pop() || ''
+
+        for (const part of parts) {
+          if (!part.trim()) continue
+          parseSSEEvent(part, aiMessage)
+        }
+      }
+
+      // 处理剩余 buffer
+      if (buffer.trim()) {
+        parseSSEEvent(buffer, aiMessage)
+      }
+    } catch (err: any) {
+      aiMessage.content += `\n\n[连接错误: ${err.message || '未知错误'}]`
+    } finally {
+      isStreaming.value = false
+      // 每次消息完成后保存到 IndexedDB
+      await saveLocalMessages()
+    }
+  }
+
+  function parseSSEEvent(raw: string, aiMessage: ChatMessage) {
+    const lines = raw.split('\n')
+    let eventType = ''
+    let data = ''
+
+    for (const line of lines) {
+      if (line.startsWith('event: ')) {
+        eventType = line.slice(7)
+      } else if (line.startsWith('data: ')) {
+        data = line.slice(6)
+      }
+    }
+
+    if (!data) return
+
+    try {
+      const payload = JSON.parse(data)
+
+      switch (eventType || payload.type) {
+        case 'text':
+          aiMessage.content += payload.content || ''
+          break
+        case 'tool_call':
+          if (!aiMessage.toolCalls) aiMessage.toolCalls = []
+          aiMessage.toolCalls.push({
+            name: payload.name || '',
+            status: 'running',
+          })
+          break
+        case 'tool_update': {
+          const tc = aiMessage.toolCalls?.find((t: ToolCallInfo) => t.name === payload.name)
+          if (tc) tc.status = payload.status || 'completed'
+          break
+        }
+        case 'permission': {
+          const permData =
+            typeof payload.content === 'string' ? JSON.parse(payload.content) : payload.content
+          aiMessage.permission = {
+            requestId: permData.requestId,
+            title: permData.title,
+            options: permData.options,
+            resolved: false,
+          }
+          break
+        }
+        case 'file_created': {
+          const fileData =
+            typeof payload.content === 'string' ? JSON.parse(payload.content) : payload.content
+          if (!aiMessage.files) aiMessage.files = []
+          aiMessage.files.push({
+            key: fileData.key,
+            fileName: fileData.fileName,
+            size: fileData.size,
+          })
+          break
+        }
+        case 'done':
+          if (payload.sessionId) {
+            currentSessionId.value = payload.sessionId
+          }
+          break
+        case 'welcome':
+          welcomeMessage.value = payload.content || ''
+          break
+        case 'error':
+          aiMessage.content += `\n\n[错误: ${payload.content || '未知'}]`
+          break
+      }
+    } catch {
+      // 非 JSON 数据忽略
+    }
+  }
+
+  async function resolvePermission(optionId: string) {
+    try {
+      await fetch(`${getBaseUrl()}/ai/permission`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+          Authorization: `Bearer ${userStore.token}`,
+        },
+        body: JSON.stringify({ optionId }),
+      })
+      const lastMsg = messages.value[messages.value.length - 1]
+      if (lastMsg?.permission) {
+        lastMsg.permission.resolved = true
+      }
+    } catch {
+      // 忽略
+    }
+  }
+
+  async function cancelChat() {
+    try {
+      await fetch(`${getBaseUrl()}/ai/cancel`, {
+        method: 'POST',
+        headers: { Authorization: `Bearer ${userStore.token}` },
+      })
+    } catch {
+      // 忽略
+    }
+  }
+
+  function clearMessages() {
+    messages.value = []
+    if (currentSessionId.value) {
+      chatDB.deleteSession(currentSessionId.value)
+    }
+  }
+
+  return {
+    messages,
+    isStreaming,
+    currentSessionId,
+    welcomeMessage,
+    sessionList,
+    isLoadingSession,
+    sendMessage,
+    cancelChat,
+    clearMessages,
+    resolvePermission,
+    fetchSession,
+    fetchSessionList,
+    createNewSession,
+    loadSession,
+    deleteSession,
+  }
+}

+ 4 - 2
src/layout/header/Actions/index.vue

@@ -1,6 +1,7 @@
 <script setup lang="ts">
 import { ButtonAnimation } from '@/components'
 
+import AiChatAction from './AiChat/index.vue'
 import ConfigureDrawer from './component/ConfigureDrawer.vue'
 import FullScreen from './component/FullScreen.vue'
 import Signout from './component/SignOut.vue'
@@ -12,9 +13,10 @@ defineOptions({
 </script>
 <template>
   <div class="flex items-center gap-x-3">
-    <ButtonAnimation :animation="false" tag="a" href="https://github.com/tenianon/lithe-admin" target="_blank">
+    <AiChatAction />
+    <!-- <ButtonAnimation :animation="false" tag="a" href="https://github.com/tenianon/lithe-admin" target="_blank">
       <span class="i-mdi:github size-5" />
-    </ButtonAnimation>
+    </ButtonAnimation> -->
     <FullScreen />
     <ThemeDropdown />
     <ConfigureDrawer />

+ 2 - 2
src/views/site-info-manage/index.vue

@@ -390,7 +390,7 @@ function createColumns(): DataTableColumns<SiteInfo> {
   // 给 SN 列添加点击打开功率流弹窗的 render
   const snColumn = result.find((col) => 'key' in col && col.key === 'sn')
   if (snColumn && 'key' in snColumn) {
-    (snColumn as DataTableBaseColumn<SiteInfo>).render = (rowData) => {
+    ;(snColumn as DataTableBaseColumn<SiteInfo>).render = (rowData) => {
       return h(
         NButton,
         {
@@ -431,7 +431,7 @@ function createColumns(): DataTableColumns<SiteInfo> {
     minWidth: 100,
     fixed: 'right',
     render(row) {
-      return h('div', { class: 'flex justify-center gap-4 p-x-4' }, [
+      return h('div', { class: 'flex justify-center gap-4 p-x-4 flex-nowrap' }, [
         h(
           NButton,
           {