# Snow CLI 使用文档——SSE 服务模式

欢迎使用 Snow CLI！在终端中进行 Agentic 编程。

## 快速体验

想要快速体验 SSE 客户端？我们提供了一个完整的浏览器测试客户端：

**位置**：`source/test/sse-client/index.html`

直接在浏览器中打开该文件，连接到 SSE 服务器即可开始测试。

## 什么是 SSE 服务模式

SSE（Server-Sent Events）服务模式允许您将 Snow CLI 作为后端服务运行，为外部应用程序提供 AI 能力。它非常适合：

- Web 应用集成
- 移动应用后端
- 第三方工具集成
- 微服务架构
- 自定义聊天界面

## 基础用法

### 启动 SSE 服务器

#### 基础启动

```bash
# 使用默认端口 3000（前台运行）
snow --sse

# 指定端口
snow --sse --sse-port 8080

# 指定工作目录
snow --sse --work-dir /path/to/project

# 自定义交互超时时长（默认 300000ms 即 5 分钟）
snow --sse --sse-timeout 600000  # 设置为 10 分钟

# 组合使用
snow --sse --sse-port 8080 --work-dir /path/to/project --sse-timeout 600000
```

#### 后台守护进程模式

如果不想让终端被占用，可以使用守护进程模式在后台运行：

```bash
# 启动后台守护进程（默认端口 3000）
snow --sse-daemon

# 指定不同端口（支持多开）
snow --sse-daemon --sse-port 3000
snow --sse-daemon --sse-port 8080
snow --sse-daemon --sse-port 9000

# 指定完整参数
snow --sse-daemon --sse-port 8080 --work-dir /path/to/project --sse-timeout 600000

# 查看所有守护进程状态
snow --sse-status

# 停止守护进程（三种方式）
snow --sse-stop                    # 停止默认端口3000的守护进程
snow --sse-stop --sse-port 8080    # 通过端口号停止
snow --sse-stop 12345              # 通过PID停止
```

守护进程特性：

- 支持多实例运行（不同端口）
- 在后台运行，不占用终端
- 每个端口独立的日志文件：`~/.snow/sse-logs/port-<端口>.log`
- 每个端口独立的 PID 文件：`~/.snow/sse-daemons/port-<端口>.pid`
- 支持通过端口或 PID 停止进程
- 查看状态时显示所有运行的守护进程

#### 启动时启用 YOLO 模式

虽然 SSE 服务器本身不使用 `--yolo` 参数，但您可以通过以下方式实现类似效果：

**方式一：客户端消息携带 yoloMode**

这是推荐的方式，灵活控制每个请求是否使用 YOLO 模式：

```javascript
// 发送消息时指定 YOLO 模式
await fetch('http://localhost:3000/message', {
	method: 'POST',
	headers: {'Content-Type': 'application/json'},
	body: JSON.stringify({
		type: 'chat',
		content: '你的问题',
		yoloMode: true, // 启用 YOLO 模式
	}),
});
```

**方式二：配置权限自动批准列表**

将常用工具添加到项目的权限配置文件中，实现默认自动批准：

```bash
# 编辑项目权限配置
vi .snow/permissions.json
```

```json
{
	"alwaysApprovedTools": [
		"filesystem-read",
		"filesystem-edit",
		"filesystem-create",
		"codebase-search",
		"ace-search",
		"notebook-add"
	]
}
```

这样，列表中的工具会自动批准，无需每次确认。

**注意事项**：

- SSE 服务器启动时不支持 `--yolo` 参数
- YOLO 模式需要通过客户端消息的 `yoloMode` 字段启用
- 或者通过配置 `.snow/permissions.json` 实现工具自动批准
- 敏感命令即使在 YOLO 模式下也需要确认

### 服务器信息

启动后，终端会显示美观的服务器状态界面：

```
✓ SSE 服务器已启动
端口: 3000 | 工作目录: /Users/xxx/project | ● 运行中

可用端点:
  http://localhost:3000/events
  POST http://localhost:3000/message
  POST http://localhost:3000/session/create
  POST http://localhost:3000/session/load
  GET http://localhost:3000/session/list
  GET http://localhost:3000/session/rollback-points?sessionId={sessionId}
  DELETE http://localhost:3000/session/{sessionId}
  GET http://localhost:3000/health

运行日志:
[14:30:45] SSE 服务已启动在端口 3000
[14:30:50] 创建新 session: abc-123

按 Ctrl+C 停止服务器
```

## API 端点

### 1. SSE 事件流连接

**端点**: `GET /events`

建立 SSE 连接，接收实时事件流。

#### JavaScript 示例

```javascript
const eventSource = new EventSource('http://localhost:3000/events');

eventSource.onmessage = event => {
	const data = JSON.parse(event.data);
	console.log('收到事件:', data);

	switch (data.type) {
		case 'connected':
			console.log('连接成功，连接ID:', data.data.connectionId);
			break;

		case 'message':
			if (data.data.streaming) {
				console.log('AI 正在回复:', data.data.content);
			} else if (data.data.role === 'user') {
				console.log('用户消息:', data.data.content);
			}
			break;

		case 'tool_confirmation_request':
			// 需要用户确认工具执行
			handleToolConfirmation(data);
			break;

		case 'complete':
			console.log('对话完成');
			break;
	}
};
```

### 2. 发送消息

**端点**: `POST /message`

**Content-Type**: `application/json`

#### 发送普通文本消息

```javascript
async function sendMessage(content) {
	const response = await fetch('http://localhost:3000/message', {
		method: 'POST',
		headers: {
			'Content-Type': 'application/json',
		},
		body: JSON.stringify({
			type: 'chat',
			content: content,
		}),
	});

	return await response.json();
}

// 使用示例
await sendMessage('帮我创建一个 React 组件');
```

#### 带 Session 的连续对话

```javascript
async function continueConversation(content, sessionId) {
	const response = await fetch('http://localhost:3000/message', {
		method: 'POST',
		headers: {
			'Content-Type': 'application/json',
		},
		body: JSON.stringify({
			type: 'chat',
			content: content,
			sessionId: sessionId, // 使用 session ID 继续对话
		}),
	});

	return await response.json();
}

// Session ID 会在 complete 事件中返回
eventSource.onmessage = event => {
	const data = JSON.parse(event.data);
	if (data.type === 'complete') {
		const sessionId = data.data.sessionId;
		console.log('Session ID:', sessionId);
	}
};
```

#### 发送图片消息

```javascript
async function sendImageMessage(content, imageFile) {
	// 将图片转换为 Base64 Data URI
	const reader = new FileReader();
	const imageData = await new Promise((resolve, reject) => {
		reader.onload = e => resolve(e.target.result);
		reader.onerror = reject;
		reader.readAsDataURL(imageFile);
	});

	const response = await fetch('http://localhost:3000/message', {
		method: 'POST',
		headers: {
			'Content-Type': 'application/json',
		},
		body: JSON.stringify({
			type: 'chat',
			content: content || '请分析这张图片',
			images: [
				{
					data: imageData, // 完整的 data URI，如 data:image/png;base64,iVBORw0KG...
					mimeType: imageFile.type, // 如 image/png, image/jpeg
				},
			],
		}),
	});

	return await response.json();
}

// 使用示例
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', async e => {
	const file = e.target.files[0];
	if (file && file.type.startsWith('image/')) {
		await sendImageMessage('这是什么？', file);
	}
});
```

#### 中断正在执行的任务

```javascript
async function abortTask(sessionId) {
	const response = await fetch('http://localhost:3000/message', {
		method: 'POST',
		headers: {
			'Content-Type': 'application/json',
		},
		body: JSON.stringify({
			type: 'abort',
			sessionId: sessionId,
		}),
	});

	return await response.json();
}

// 监听中断确认
eventSource.onmessage = event => {
	const data = JSON.parse(event.data);
	if (data.type === 'complete' && data.data.cancelled) {
		console.log('任务已被用户中断');
	}
};
```

#### 启用 YOLO 模式

```javascript
async function sendWithYolo(content) {
	const response = await fetch('http://localhost:3000/message', {
		method: 'POST',
		headers: {
			'Content-Type': 'application/json',
		},
		body: JSON.stringify({
			type: 'chat',
			content: content,
			yoloMode: true, // 自动批准所有非敏感工具
		}),
	});

	return await response.json();
}
```

### 3. 会话管理

#### 创建新会话

**端点**: `POST /session/create`

**Content-Type**: `application/json`

创建一个新的对话会话，返回会话信息并自动绑定到当前连接。

```javascript
async function createSession() {
	const response = await fetch('http://localhost:3000/session/create', {
		method: 'POST',
		headers: {
			'Content-Type': 'application/json',
		},
		body: JSON.stringify({
			connectionId: 'conn_xxx', // 可选，指定连接ID
		}),
	});

	const data = await response.json();
	console.log('会话ID:', data.session.id);
	console.log('创建时间:', data.session.createdAt);
	return data.session;
}
```

#### 加载已有会话

**端点**: `POST /session/load`

**Content-Type**: `application/json`

加载一个已保存的会话，恢复对话上下文。

```javascript
async function loadSession(sessionId) {
	const response = await fetch('http://localhost:3000/session/load', {
		method: 'POST',
		headers: {
			'Content-Type': 'application/json',
		},
		body: JSON.stringify({
			sessionId: sessionId,
			connectionId: 'conn_xxx', // 可选，指定连接ID
		}),
	});

	const data = await response.json();
	if (data.success) {
		console.log('会话已加载:', data.session.id);
		console.log('消息数量:', data.session.messages.length);
		return data.session;
	} else {
		console.error('加载失败:', data.error);
	}
}
```

#### 获取会话列表

**端点**: `GET /session/list`

**查询参数**:

- `page`: 页码，从 0 开始（可选，默认 0）
- `pageSize`: 每页数量（可选，默认 20，最大 200）
- `q`: 搜索关键词（可选，搜索会话中的消息内容）

获取所有已保存的会话列表，支持分页和搜索。

```javascript
async function listSessions(page = 0, pageSize = 20, searchQuery = '') {
	const params = new URLSearchParams({
		page: page.toString(),
		pageSize: pageSize.toString(),
	});

	if (searchQuery) {
		params.append('q', searchQuery);
	}

	const response = await fetch(`http://localhost:3000/session/list?${params}`);
	const data = await response.json();

	console.log('总会话数:', data.total);
	console.log('当前页:', data.page);
	console.log('每页数量:', data.pageSize);
	console.log('会话列表:', data.sessions);

	// 会话列表示例
	// data.sessions = [
	//   {
	//     id: 'abc-123',
	//     createdAt: '2025-12-30T10:00:00.000Z',
	//     updatedAt: '2025-12-30T10:30:00.000Z',
	//     messageCount: 10,
	//     firstMessage: '帮我创建一个函数'
	//   },
	//   ...
	// ]

	return data;
}
```

#### 获取回滚点列表

**端点**: `GET /session/rollback-points`

**查询参数**:

- `sessionId`: 会话 ID（必填）

返回指定会话中可用于回滚的用户消息列表（demo 使用）。

```javascript
async function getRollbackPoints(sessionId) {
	const params = new URLSearchParams({sessionId});
	const response = await fetch(
		`http://localhost:3000/session/rollback-points?${params.toString()}`,
	);
	const data = await response.json();

	// 返回示例（关键字段）:
	// {
	//   success: true,
	//   sessionId: 'abc-123',
	//   points: [
	//     {
	//       messageIndex: 0,
	//       role: 'user',
	//       timestamp: 1730000000000,
	//       summary: '...',
	//       hasSnapshot: true,
	//       snapshot: {timestamp: 1730000000000, fileCount: 12},
	//       filesToRollbackCount: 5
	//     }
	//   ]
	// }
	return data;
}
```

#### 删除会话

**端点**: `DELETE /session/{sessionId}`

删除指定的会话及其所有数据。

```javascript
async function deleteSession(sessionId) {
	const response = await fetch(`http://localhost:3000/session/${sessionId}`, {
		method: 'DELETE',
	});

	const data = await response.json();
	if (data.success) {
		console.log('会话已删除:', data.deleted);
	}
	return data;
}
```

### 4. 健康检查

**端点**: `GET /health`

检查服务器状态和当前连接数。

```javascript
async function checkHealth() {
	const response = await fetch('http://localhost:3000/health');
	const data = await response.json();
	console.log('状态:', data.status);
	console.log('连接数:', data.connections);
}
```

## 事件类型说明

### connected

连接成功事件。

```javascript
{
  type: 'connected',
  data: {
    connectionId: 'conn_1234567890'
  },
  timestamp: '2025-12-30T15:30:00.000Z'
}
```

### message

消息事件（用户或 AI）。

```javascript
// 用户消息
{
  type: 'message',
  data: {
    role: 'user',
    content: '帮我创建一个函数'
  }
}

// AI 流式响应
{
  type: 'message',
  data: {
    role: 'assistant',
    content: '当然，我来帮你...',
    streaming: true
  }
}

// AI 最终响应
{
  type: 'message',
  data: {
    role: 'assistant',
    content: '完整的回复内容',
    streaming: false
  }
}
```

### tool_call

工具调用事件。

```javascript
{
  type: 'tool_call',
  data: {
    name: 'filesystem-create',
    arguments: {
      filePath: 'example.js',
      content: '...'
    }
  }
}
```

### tool_confirmation_request

请求确认工具执行。

```javascript
{
  type: 'tool_confirmation_request',
  data: {
    toolCall: {
      function: {
        name: 'terminal-execute',
        arguments: '{"command":"rm -rf node_modules"}'
      }
    },
    isSensitive: true,  // 是否为敏感命令
    sensitiveInfo: {
      pattern: 'rm -rf',
      description: '删除文件或目录'
    },
    availableOptions: [
      {value: 'approve', label: 'Approve once'},
      {value: 'approve_always', label: 'Always approve'},  // 非敏感命令才有
      {value: 'reject_with_reply', label: 'Reject with reply'},
      {value: 'reject', label: 'Reject and end session'}
    ]
  },
  requestId: 'req_1234567890'
}
```

### tool_result

工具执行结果。

```javascript
{
  type: 'tool_result',
  data: {
    content: '执行成功',
    status: 'success'
  }
}
```

### user_question_request

AI 询问用户问题。

```javascript
{
  type: 'user_question_request',
  data: {
    question: '请选择一个选项',
    options: ['选项1', '选项2', '选项3'],
    multiSelect: false
  },
  requestId: 'req_1234567890'
}
```

### usage

Token 使用情况。

```javascript
{
  type: 'usage',
  data: {
    prompt_tokens: 150,
    completion_tokens: 200,
    total_tokens: 350
  }
}
```

### error

错误信息。

```javascript
{
  type: 'error',
  data: {
    message: '错误描述',
    stack: '错误堆栈（可选）'
  }
}
```

### complete

对话完成。

```javascript
{
  type: 'complete',
  data: {
    usage: {
      input_tokens: 150,
      output_tokens: 200
    },
    tokenCount: 350,
    sessionId: 'abc-123-def-456',  // 会话 ID
    cancelled: false  // 是否被用户取消（可选）
  }
}
```

### abort

任务中断请求（客户端主动发送）。

```javascript
// 客户端发送中断请求
await fetch('http://localhost:3000/message', {
  method: 'POST',
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({
    type: 'abort',
    sessionId: 'abc-123-def-456'
  })
});

// 服务器响应中断确认
{
  type: 'message',
  data: {
    role: 'assistant',
    content: 'Task has been aborted'
  },
  timestamp: '2025-12-30T15:30:00.000Z'
}

// 随后发送完成事件
{
  type: 'complete',
  data: {
    usage: {input_tokens: 0, output_tokens: 0},
    tokenCount: 0,
    sessionId: 'abc-123-def-456',
    cancelled: true
  }
}
```

## 工具确认流程

### 确认请求响应

当收到 `tool_confirmation_request` 事件时，需要发送确认响应：

```javascript
async function handleToolConfirmation(event) {
	const toolCall = event.data.toolCall;
	const options = event.data.availableOptions;

	// 显示工具信息给用户
	console.log('工具:', toolCall.function.name);
	console.log('参数:', toolCall.function.arguments);
	console.log('可用选项:', options);

	// 用户选择后发送响应
	const response = await fetch('http://localhost:3000/message', {
		method: 'POST',
		headers: {
			'Content-Type': 'application/json',
		},
		body: JSON.stringify({
			type: 'tool_confirmation_response',
			requestId: event.requestId,
			response: 'approve', // 或 'approve_always', 'reject', {type: 'reject_with_reply', reason: '...'}
		}),
	});

	return await response.json();
}
```

### 确认选项说明

| 选项       | 值                                            | 说明                     | 适用场景     |
| ---------- | --------------------------------------------- | ------------------------ | ------------ |
| 批准一次   | `'approve'`                                   | 仅批准这一次执行         | 所有工具     |
| 总是批准   | `'approve_always'`                            | 批准并添加到自动批准列表 | 仅非敏感命令 |
| 拒绝并回复 | `{type: 'reject_with_reply', reason: '原因'}` | 拒绝并告诉 AI 原因       | 所有工具     |
| 拒绝并结束 | `'reject'`                                    | 拒绝并结束会话           | 所有工具     |

### 敏感命令检测

系统会自动检测敏感命令（如 `rm -rf`、`sudo` 等），敏感命令：

- 不会显示"总是批准"选项
- 即使在 YOLO 模式下也需要确认
- 会显示警告信息和匹配的命令模式

关于敏感命令配置，请参考：[敏感命令配置](./06.敏感命令配置.md)

## 用户问题响应

当收到 `user_question_request` 事件时：

```javascript
async function handleUserQuestion(event) {
	const question = event.data.question;
	const options = event.data.options;
	const multiSelect = event.data.multiSelect;

	// 显示问题和选项给用户
	console.log('问题:', question);
	console.log('选项:', options);

	// 用户选择后发送响应
	const response = await fetch('http://localhost:3000/message', {
		method: 'POST',
		headers: {
			'Content-Type': 'application/json',
		},
		body: JSON.stringify({
			type: 'user_question_response',
			requestId: event.requestId,
			response: {
				selected: multiSelect ? ['选项1', '选项2'] : '选项1',
				customInput: '', // 可选的自定义输入
			},
		}),
	});

	return await response.json();
}
```

## 权限配置

### 自动批准列表

SSE 服务器会自动读取项目根目录的权限配置文件：

**位置**: `.snow/permissions.json`

```json
{
	"alwaysApprovedTools": [
		"filesystem-read",
		"codebase-search",
		"filesystem-edit",
		"notebook-add",
		"filesystem-create"
	]
}
```

### 权限继承规则

1. **项目级配置**：服务器启动时读取工作目录下的 `.snow/permissions.json`
2. **自动批准**：列表中的工具会自动执行，不需要用户确认
3. **敏感命令优先**：即使在自动批准列表中，敏感命令仍需确认
4. **动态更新**：用户选择"总是批准"时，工具会自动添加到配置文件

### 配置示例

```json
{
	"alwaysApprovedTools": [
		"filesystem-read", // 读取文件
		"filesystem-edit", // Hashline 编辑
		"filesystem-create", // 创建文件
		"codebase-search", // 代码搜索
		"ace-search", // 统一 ACE 代码搜索（通过 action 选择 semantic_search / find_definition / find_references / file_outline / text_search）
		"notebook-add" // 添加笔记
	]
}
```

## YOLO 模式

### 启用 YOLO 模式

在发送消息时携带 `yoloMode` 参数：

```javascript
const response = await fetch('http://localhost:3000/message', {
	method: 'POST',
	headers: {'Content-Type': 'application/json'},
	body: JSON.stringify({
		type: 'chat',
		content: '你的问题',
		yoloMode: true, // 启用 YOLO 模式
	}),
});
```

### YOLO 模式特性

- **自动批准**：非敏感命令自动执行
- **敏感命令例外**：敏感命令仍需确认
- **快速响应**：减少交互等待时间
- **适合自动化**：脚本和自动化场景

### 安全考虑

即使启用 YOLO 模式：

1. 敏感命令仍需确认
2. 不在权限列表中的工具首次需要确认
3. 可以随时通过拒绝来中止执行

## 完整示例

### JavaScript 客户端

```javascript
class SnowAIClient {
	constructor(baseUrl = 'http://localhost:3000') {
		this.baseUrl = baseUrl;
		this.eventSource = null;
		this.sessionId = null;
	}

	// 连接到 SSE 服务器
	connect() {
		return new Promise((resolve, reject) => {
			this.eventSource = new EventSource(`${this.baseUrl}/events`);

			this.eventSource.onopen = () => {
				console.log('已连接到 Snow AI');
				resolve();
			};

			this.eventSource.onerror = error => {
				console.error('连接错误:', error);
				reject(error);
			};

			this.eventSource.onmessage = event => {
				this.handleEvent(JSON.parse(event.data));
			};
		});
	}

	// 处理事件
	handleEvent(event) {
		console.log('[事件]', event.type);

		switch (event.type) {
			case 'tool_confirmation_request':
				this.handleToolConfirmation(event);
				break;

			case 'user_question_request':
				this.handleUserQuestion(event);
				break;

			case 'message':
				if (event.data.streaming) {
					process.stdout.write(event.data.content);
				}
				break;

			case 'complete':
				this.sessionId = event.data.sessionId;
				console.log('\n对话完成，Session ID:', this.sessionId);
				break;
		}
	}

	// 处理工具确认
	async handleToolConfirmation(event) {
		const options = event.data.availableOptions;

		// 这里可以实现自定义的确认逻辑
		// 示例：自动批准非敏感命令
		const decision = event.data.isSensitive ? 'reject' : 'approve';

		await this.sendToolConfirmation(event.requestId, decision);
	}

	// 处理用户问题
	async handleUserQuestion(event) {
		// 这里可以实现自定义的选择逻辑
		const selected = event.data.options[0];

		await this.sendUserQuestionResponse(event.requestId, {
			selected: selected,
		});
	}

	// 发送消息
	async sendMessage(content, yoloMode = false) {
		const payload = {
			type: 'chat',
			content: content,
		};

		if (this.sessionId) {
			payload.sessionId = this.sessionId;
		}

		if (yoloMode) {
			payload.yoloMode = true;
		}

		const response = await fetch(`${this.baseUrl}/message`, {
			method: 'POST',
			headers: {'Content-Type': 'application/json'},
			body: JSON.stringify(payload),
		});

		return await response.json();
	}

	// 发送工具确认响应
	async sendToolConfirmation(requestId, decision) {
		const response = await fetch(`${this.baseUrl}/message`, {
			method: 'POST',
			headers: {'Content-Type': 'application/json'},
			body: JSON.stringify({
				type: 'tool_confirmation_response',
				requestId: requestId,
				response: decision,
			}),
		});

		return await response.json();
	}

	// 发送用户问题响应
	async sendUserQuestionResponse(requestId, answer) {
		const response = await fetch(`${this.baseUrl}/message`, {
			method: 'POST',
			headers: {'Content-Type': 'application/json'},
			body: JSON.stringify({
				type: 'user_question_response',
				requestId: requestId,
				response: answer,
			}),
		});

		return await response.json();
	}

	// 断开连接
	disconnect() {
		if (this.eventSource) {
			this.eventSource.close();
			this.eventSource = null;
		}
	}
}

// 使用示例
async function main() {
	const client = new SnowAIClient();

	// 连接
	await client.connect();

	// 发送消息（启用 YOLO 模式）
	await client.sendMessage('帮我创建一个 TypeScript 函数', true);

	// 等待响应处理（通过事件监听器）
}

main();
```

### Python 客户端

```python
import requests
import json
import sseclient

class SnowAIClient:
    def __init__(self, base_url='http://localhost:3000'):
        self.base_url = base_url
        self.session = requests.Session()
        self.session_id = None

    def connect(self):
        """连接到 SSE 服务器"""
        response = self.session.get(
            f'{self.base_url}/events',
            stream=True,
            headers={'Accept': 'text/event-stream'}
        )
        client = sseclient.SSEClient(response)

        for event in client.events():
            data = json.loads(event.data)
            self.handle_event(data)

    def handle_event(self, event):
        """处理事件"""
        print(f"[事件] {event['type']}")

        if event['type'] == 'tool_confirmation_request':
            self.handle_tool_confirmation(event)
        elif event['type'] == 'user_question_request':
            self.handle_user_question(event)
        elif event['type'] == 'complete':
            self.session_id = event['data']['sessionId']
            print(f"Session ID: {self.session_id}")

    def handle_tool_confirmation(self, event):
        """处理工具确认"""
        # 自动批准非敏感命令
        decision = 'reject' if event['data']['isSensitive'] else 'approve'
        self.send_tool_confirmation_response(event['requestId'], decision)

    def handle_user_question(self, event):
        """处理用户问题"""
        selected = event['data']['options'][0]
        self.send_user_question_response(event['requestId'], {'selected': selected})

    def send_message(self, content, yolo_mode=False):
        """发送消息"""
        payload = {
            'type': 'chat',
            'content': content,
        }

        if self.session_id:
            payload['sessionId'] = self.session_id

        if yolo_mode:
            payload['yoloMode'] = True

        response = self.session.post(
            f'{self.base_url}/message',
            json=payload
        )
        return response.json()

    def send_tool_confirmation_response(self, request_id, decision):
        """发送工具确认响应"""
        response = self.session.post(
            f'{self.base_url}/message',
            json={
                'type': 'tool_confirmation_response',
                'requestId': request_id,
                'response': decision
            }
        )
        return response.json()

    def send_user_question_response(self, request_id, answer):
        """发送用户问题响应"""
        response = self.session.post(
            f'{self.base_url}/message',
            json={
                'type': 'user_question_response',
                'requestId': request_id,
                'response': answer
            }
        )
        return response.json()

# 使用示例
if __name__ == '__main__':
    client = SnowAIClient()

    # 发送消息（启用 YOLO 模式）
    client.send_message('帮我创建一个 Python 函数', yolo_mode=True)

    # 监听事件
    client.connect()
```

## 使用场景

### Web 应用集成

将 Snow AI 集成到您的 Web 应用中，提供智能编程助手功能：

```javascript
// React 组件示例
import {useState, useEffect, useRef} from 'react';

function AIAssistantChat() {
	const [connected, setConnected] = useState(false);
	const [messages, setMessages] = useState([]);
	const [sessionId, setSessionId] = useState(null);
	const eventSourceRef = useRef(null);

	// 连接到 SSE 服务器
	useEffect(() => {
		const eventSource = new EventSource('http://localhost:3000/events');
		eventSourceRef.current = eventSource;

		eventSource.onopen = () => {
			setConnected(true);
			console.log('已连接到 Snow AI');
		};

		eventSource.onmessage = event => {
			const data = JSON.parse(event.data);
			handleSSEEvent(data);
		};

		eventSource.onerror = () => {
			setConnected(false);
			console.error('连接断开');
		};

		return () => {
			eventSource.close();
		};
	}, []);

	// 处理 SSE 事件
	const handleSSEEvent = data => {
		switch (data.type) {
			case 'message':
				if (data.data.role === 'assistant') {
					if (data.data.streaming) {
						// 流式更新最后一条消息
						setMessages(prev => {
							const newMessages = [...prev];
							if (
								newMessages.length > 0 &&
								newMessages[newMessages.length - 1].role === 'assistant'
							) {
								newMessages[newMessages.length - 1].content = data.data.content;
							} else {
								newMessages.push({
									role: 'assistant',
									content: data.data.content,
								});
							}
							return newMessages;
						});
					}
				}
				break;

			case 'complete':
				setSessionId(data.data.sessionId);
				console.log('对话完成');
				break;

			case 'tool_confirmation_request':
				// 显示工具确认对话框
				handleToolConfirmation(data);
				break;

			case 'error':
				console.error('错误:', data.data.message);
				break;
		}
	};

	// 发送消息
	const sendMessage = async text => {
		const newMessage = {role: 'user', content: text};
		setMessages(prev => [...prev, newMessage]);

		const payload = {
			type: 'chat',
			content: text,
			yoloMode: true, // 自动批准安全工具
		};

		if (sessionId) {
			payload.sessionId = sessionId;
		}

		await fetch('http://localhost:3000/message', {
			method: 'POST',
			headers: {'Content-Type': 'application/json'},
			body: JSON.stringify(payload),
		});
	};

	// 处理工具确认
	const handleToolConfirmation = async event => {
		const confirmed = window.confirm(
			`AI 想要执行工具: ${event.data.toolCall.function.name}\n是否允许？`,
		);

		await fetch('http://localhost:3000/message', {
			method: 'POST',
			headers: {'Content-Type': 'application/json'},
			body: JSON.stringify({
				type: 'tool_confirmation_response',
				requestId: event.requestId,
				response: confirmed ? 'approve' : 'reject',
			}),
		});
	};

	return (
		<div>
			<div>状态: {connected ? '已连接' : '未连接'}</div>
			<div>
				{messages.map((msg, i) => (
					<div key={i}>
						<strong>{msg.role}:</strong> {msg.content}
					</div>
				))}
			</div>
			<input
				type="text"
				onKeyPress={e => {
					if (e.key === 'Enter') {
						sendMessage(e.target.value);
						e.target.value = '';
					}
				}}
			/>
		</div>
	);
}
```

### 移动应用后端

为移动应用提供 AI 能力：

```javascript
// Express 中间件
app.post('/api/ai/chat', async (req, res) => {
	const {message, sessionId} = req.body;

	// 转发到 Snow AI
	const response = await fetch('http://localhost:3000/message', {
		method: 'POST',
		headers: {'Content-Type': 'application/json'},
		body: JSON.stringify({
			type: 'chat',
			content: message,
			sessionId: sessionId,
			yoloMode: true,
		}),
	});

	res.json(await response.json());
});
```

### 微服务架构

作为 AI 微服务：

```javascript
// Kubernetes 部署
apiVersion: apps/v1
kind: Deployment
metadata:
  name: snow-ai-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: snow-ai
  template:
    metadata:
      labels:
        app: snow-ai
    spec:
      containers:
      - name: snow-ai
        image: snow-ai:latest
        command: ["snow", "--sse", "--sse-port", "3000"]
        ports:
        - containerPort: 3000
```

## 测试客户端

Snow CLI 提供了一个完整的 HTML 测试客户端：

**位置**: `sse-test-client.html`

### 功能特性

- 实时 SSE 事件监听
- 美观的聊天界面
- 事件日志查看
- YOLO 模式开关
- 工具确认 UI（包含完整的选项展示）
- Session 管理
- 连接状态显示

### 使用方法

1. 启动 SSE 服务器：

   ```bash
   snow --sse
   ```

2. 在浏览器中打开 `sse-test-client.html`

3. 点击"连接"按钮

4. 开始聊天测试

## 最佳实践

### 1. 错误处理

```javascript
// 完善的错误处理
eventSource.onerror = error => {
	console.error('SSE 连接错误:', error);

	// 自动重连
	setTimeout(() => {
		console.log('尝试重新连接...');
		connect();
	}, 5000);
};
```

### 2. 超时处理

```javascript
// 为交互请求设置超时
const TIMEOUT = 300000; // 5 分钟（默认值，可通过 --sse-timeout 参数调整）

function waitForResponse(requestId) {
	return new Promise((resolve, reject) => {
		const timeout = setTimeout(() => {
			reject(new Error('交互超时'));
		}, TIMEOUT);

		// 监听响应
		// 收到响应后 clearTimeout(timeout)
	});
}
```

### 3. Session 管理

```javascript
// 持久化 Session ID
localStorage.setItem('snow-session-id', sessionId);

// 恢复 Session
const savedSessionId = localStorage.getItem('snow-session-id');
if (savedSessionId) {
	await client.sendMessage('继续之前的对话', false, savedSessionId);
}
```

### 4. 安全考虑

```javascript
// 验证和清理用户输入
function sanitizeInput(input) {
	// 移除危险字符
	return input.replace(/[<>]/g, '');
}

// 在生产环境中添加认证
const response = await fetch('http://localhost:3000/message', {
	headers: {
		'Content-Type': 'application/json',
		Authorization: `Bearer ${apiToken}`,
	},
	// ...
});
```

## 限制和注意事项

### 不支持的功能

1. **交互式 UI**：

   - 无法使用 Ink 终端界面
   - 不支持快捷键

2. **Plan 模式**：

   - 不支持交互式计划审批
   - 所有操作立即执行

3. **本地文件访问限制**：
   - 只能访问服务器工作目录下的文件
   - 不能访问客户端本地文件

### 性能注意事项

1. **连接数限制**：

   - 建议单个服务器不超过 100 个并发连接
   - 考虑负载均衡

2. **Session 大小**：

   - 长会话会增加内存使用
   - 定期清理旧 Session

3. **网络带宽**：
   - 流式输出会持续占用连接
   - 考虑消息大小限制

### 安全注意事项

1. **认证和授权**：

   - 生产环境必须添加认证
   - 实施访问控制

2. **API 密钥保护**：

   - 不要在客户端暴露 API 密钥
   - 使用服务器端配置

3. **命令执行风险**：
   - 审查所有工具调用
   - 限制敏感操作

## 常见问题

**Q: SSE 服务器和无头模式有什么区别？**

A: SSE 服务器是持续运行的后端服务，支持多个客户端连接。无头模式是单次执行模式，执行完成后自动退出。SSE 适合 Web 应用集成，无头模式适合脚本自动化。

**Q: 如何在 SSE 模式下使用不同的 API 配置？**

A: SSE 服务器读取工作目录下的配置文件。可以通过 `--work-dir` 参数指定不同的项目目录，每个目录有独立的配置。

**Q: 可以同时运行多个 SSE 服务器吗？**

A: 可以，但需要使用不同的端口。例如：

```bash
snow --sse --sse-port 3000
snow --sse --sse-port 3001 --work-dir /另一个项目
```

**Q: Session 会过期吗？**

A: Session 不会过期，会永久保存在 `~/.snow/sessions/` 目录下。但是非常长的 Session 会增加 Token 消耗。

**Q: 如何处理工具确认超时？**

A: 工具确认默认有 5 分钟（300000ms）超时。如果超时，会自动拒绝执行并返回错误。建议在客户端实现自动处理或提示用户。

您可以通过 `--sse-timeout` 参数自定义超时时长：

```bash
# 设置为 10 分钟（600000ms）
snow --sse --sse-timeout 600000

# 设置为 30 秒（30000ms）
snow --sse --sse-timeout 30000
```

**Q: YOLO 模式会执行所有命令吗？**

A: 不会。敏感命令即使在 YOLO 模式下也需要确认。YOLO 模式只自动批准安全的、在权限列表中的工具。

**Q: 如何调试 SSE 连接问题？**

A:

1. 检查服务器日志（终端显示）
2. 使用浏览器开发工具查看网络请求
3. 使用 `sse-test-client.html` 测试
4. 检查防火墙和端口占用

**Q: 可以在 Docker 中运行 SSE 服务器吗？**

A: 可以。示例 Dockerfile：

```dockerfile
FROM node:18
RUN npm install -g snow-ai
EXPOSE 3000
CMD ["snow", "--sse", "--sse-port", "3000"]
```

## 配置文件位置

SSE 服务器使用的配置文件：

- **API 配置**: `~/.snow/profiles.json`
- **权限配置**: `<工作目录>/.snow/permissions.json`
- **敏感命令**: `~/.snow/sensitive-commands.json`
- **Session 存储**: `~/.snow/sessions/<项目名>/<日期>/`

配置方法请参考：[首次配置](./02.首次配置.md)

## 相关功能

- [无头模式](./12.无头模式.md) - 命令行快速对话
- [敏感命令配置](./06.敏感命令配置.md) - 配置需要确认的危险命令
- [异步任务管理](./15.异步任务管理.md) - 后台任务管理
- [启动参数说明](./19.启动参数说明.md) - 所有启动参数详解
