Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 58 additions & 6 deletions docs/feishu-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,10 @@ idle ──▶ starting ──▶ connected
| `handleCommand(text)` | 分发 `/new`、`/model`、`/help` 等斜杠命令 |
| `cmdNew(messageId, chatId)` | 清除会话映射和本聊天的模型 override,立即新建会话 |
| `cmdModel(messageId, chatId, args)` | 无参数列出模型,有参数切换本聊天的模型 |
| `cmdProject(messageId, chatId, arg)` | 查看/切换/隐藏项目,完整对齐微信端逻辑 |
| `buildModelList()` | 调用 `Provider.list()` 展平成编号列表,供 `/model` 使用 |
| `resolveModel(chatId)` | 三级模型解析:per-chat override → 连接快照 → undefined |
| `getProjects()` | 调用 `Project.recentList()` 并过滤根目录,与微信端 `GET /project/recent` 数据一致 |
| `replyText(messageId, text)` | 通过飞书 REST API 回复消息 |
| `stop()` | 断开 WebSocket,清理客户端和所有模型状态 |
| `clearSession()` | 删除本地配置和会话映射文件 |
Expand Down Expand Up @@ -157,6 +159,7 @@ private async _doStart(config: FeishuConfig, model: ModelRef | null): Promise<vo
|------|------|
| `config.json` | App ID 和 App Secret |
| `sessions.json` | 飞书聊天 → Aether 会话 ID 映射 |
| `hidden_projects.json` | 隐藏项目目录 → 隐藏时间戳 |

存储路径按平台:
- Windows: `%APPDATA%\opencode\feishu\`
Expand Down Expand Up @@ -266,6 +269,53 @@ export const [feishuStatus, setFeishuStatus] = createSignal<FeishuStatus>("idle"
- `/new` 只清除本聊天的 override,连接快照保持不变
- `/stop` 清除所有模型状态

## 项目切换逻辑

### 命令

| 命令 | 行为 |
|------|------|
| `/project` | 显示前 10 个非隐藏项目,当前项目标 `◀` |
| `/project list` | 显示全部项目,隐藏项目标 `[已隐藏]` |
| `/project n` | 切换到第 n 个项目,自动复用/新建该项目的会话 |
| `/project hide n` | 隐藏第 n 个项目;该项目有新活动后自动恢复 |

### 数据来源

`getProjects()` 直接调用 `Project.recentList()`,这与微信端调用的 `GET /project/recent` HTTP 接口是**同一个函数**,因此两端看到的项目列表完全一致。

### 每聊天目录(`_chatDirs`)

`/project n` 切换后,该 chatId 的目标目录保存在 `_chatDirs[chatId]`。后续每条消息都在该目录的 Instance 上下文中执行:

```typescript
const effectiveDir = this._chatDirs[chatId] ?? Instance.directory
await Instance.provide({
directory: effectiveDir,
fn: async () => {
// Session 查找/创建、SessionPrompt.prompt() 均在此上下文内运行
},
})
```

这样 `Session.create()` 和 AI 回复都归属于正确的项目,而不是连接时的默认目录。

### 隐藏项目

- 隐藏状态持久化到 `hidden_projects.json`,重连后保留
- **取消隐藏双机制**:
- 飞书消息直接触发:`handleMessage` 中若本条消息的目标目录在 `_hiddenDirs` 中,立即删除
- GlobalBus 监听:连接后注册 `GlobalBus.on("event", listener)`,web 端对该目录有任何活动时自动取消隐藏
- `/project n` 切换到已隐藏的项目时也会自动取消隐藏
- `autoUnhide()` 作为兜底:每条消息和 `/project` 命令时运行,基于 `time.activity` 时间戳对比

### 状态字段

| 字段 | 类型 | 生命周期 |
|------|------|---------|
| `_chatDirs` | `Record<chatId, directory>` | `/project n` 设置,`stop()` 时清除 |
| `_hiddenDirs` | `Record<directory, timestamp>` | `/project hide n` 设置,持久化,重连保留 |

### 三级解析(`resolveModel(chatId)`)

```
Expand Down Expand Up @@ -350,10 +400,12 @@ SDK 使用方式:

| 日期 | 修改内容 | 原因 |
|------|---------|------|
| 2026-04-06 11:25 | 事件回调从 `await` 改为 `void`(非阻塞) | 飞书服务器在回调未及时返回时会重发消息,导致用户收到重复回复 |
| 2026-04-06 11:39 | 事件回调从 `await` 改为 `void`(非阻塞) | 飞书服务器在回调未及时返回时会重发消息,导致用户收到重复回复 |
| 2026-04-06 11:39 | `handleMessage` 的 catch 中增加 `replyText` 错误反馈 | 报错时飞书用户无任何提示,现在会收到错误信息 |
| 2026-04-06 16:03 | 首次发消息优先复用最近会话,而非总是新建 | 飞书每条消息都新建会话,web 端体验混乱 |
| 2026-04-06 16:15 | 每条 AI 回复顶部追加项目和会话标题(`📁 项目名\n💬 会话名\n───`) | 用户无法感知当前处于哪个项目/会话 |
| 2026-04-06 16:25 | `/new` 改为立即创建新会话(`Session.create`),不再等到下一条消息才新建 | web 端侧边栏需实时出现新会话,旧实现只清除映射延迟到下条消息才生效 |
| 2026-04-06 18:30 | 新增模型选择逻辑:连接时前端传模型 + per-chat override + `/model` 命令 | 飞书端应沿用 web UI 当前选中的模型,连接后 web UI 切换模型不应影响飞书端 |
| 2026-04-06 19:00 | `/model` 列表格式改为按 provider 分组,参考微信端风格 | 原格式不直观,统一两端体验 |
| 2026-04-06 16:05 | 首次发消息优先复用最近会话,而非总是新建 | 飞书每条消息都新建会话,web 端体验混乱 |
| 2026-04-06 16:25 | 每条 AI 回复顶部追加项目和会话标题(`📁 项目名\n💬 会话名\n───`) | 用户无法感知当前处于哪个项目/会话 |
| 2026-04-06 17:09 | `/new` 改为立即创建新会话(`Session.create`),不再等到下一条消息才新建 | web 端侧边栏需实时出现新会话,旧实现只清除映射延迟到下条消息才生效 |
| 2026-04-06 18:54 | 新增模型选择逻辑:连接时前端传模型 + per-chat override + `/model` 命令 | 飞书端应沿用 web UI 当前选中的模型,连接后 web UI 切换模型不应影响飞书端 |
| 2026-04-06 19:17 | `/model` 列表格式改为按 provider 分组,参考微信端风格 | 原格式不直观,统一两端体验 |
| 2026-04-06 19:17 | 新增 `/project` 命令:查看/切换/隐藏项目,数据源与微信端一致 | 多项目场景下需在飞书端切换工作目录;切换后消息自动在对应项目上下文中执行 |
| 2026-04-06 19:17 | 取消隐藏改为双机制:飞书消息直接判断 + GlobalBus 监听 web 端活动 | `time.activity` 只在项目初始化时更新,不反映 session 消息,原方案对活跃项目无效 |
31 changes: 31 additions & 0 deletions docs/feishu-setup-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,33 @@ Aether 支持通过飞书进行对话,体验与微信连接一致:
| `/new` | 开始新对话(清除当前会话上下文和本聊天的模型设置) |
| `/model` | 列出所有可用模型,当前选中标 ★ |
| `/model <n>` | 将本聊天切换到第 n 号模型 |
| `/project` | 查看项目列表(前 10 个,当前标 ◀) |
| `/project list` | 查看全部项目(含隐藏) |
| `/project <n>` | 切换到第 n 个项目 |
| `/project hide <n>` | 隐藏第 n 个项目(有新活动后自动恢复) |
| `/help` | 显示帮助信息 |

### 项目切换

发送 `/project` 查看项目列表:

```
📂 当前项目:opencode

📂 项目列表:

1. opencode ◀
/home/user/code/openresearch/opencode
2. my-app
/home/user/code/my-app

💡 /project n 切换 | /project list 查看全部 | /project hide n 隐藏
```

发送 `/project 2` 切换到第 2 个项目,之后所有消息都在该项目上下文中执行,回复头部也会显示对应项目名。

> **提示**:项目列表与微信端完全一致(同一数据来源),隐藏状态跨重连保留。

### 模型切换

发送 `/model` 查看可用模型列表:
Expand Down Expand Up @@ -209,6 +234,12 @@ packages/app/src/
2. 确认已开通 `im:message` 权限
3. 确认在飞书中直接给机器人发消息(群聊中 @机器人 也支持,但需开通群聊权限)

### 项目不对 / 回复的不是我想要的项目

1. 发送 `/project` 查看当前项目和列表
2. 发送 `/project n` 切换到目标项目
3. 切换后发的消息会在新项目上下文中执行

### 模型不对

1. 连接前先在 web UI 底栏切换到目标模型,再点击连接
Expand Down
Loading
Loading