Assistant Workspace V2: - Markdown indexing engine (incremental manifest + chunks JSONL) - Keyword retrieval with CJK bigram support and hotset boosting - Directory taxonomy inference and evolution suggestions - Workspace config management (.assistant/config.json) - Organize API: capture, classify, move, archive, suggest-evolution Workspace switching: - Inspect API (GET /api/workspace/inspect) for pre-switch validation - Two-phase switching with confirmation dialogs (5 workspace statuses) - Atomic save: init/reset runs before setSetting(), rollback on failure - Auto-navigate to new/reused session after switch - Mismatch banner in ChatView (only for former assistant sessions) - Debounced path validation with visual indicators Security hardening: - Completion fence (onboarding-complete/checkin-complete) only processes when session.workingDirectory === assistant_workspace_path (frontend) AND session.working_directory === workspacePath (backend double-check) - assertContained() in workspace-organizer: rejects absolute paths, ~/, ../ traversal, and symlink escapes for captureNote/moveFile - organize API validates all path params at entry (validateRelativePath) - captureDefault sanitized to relative path on onboarding write - R_OK + W_OK validation on GET settings, session creation, and PUT Bug fixes: - hookTriggeredSessionId now read before auto-trigger; allows retry if session has no messages (previous trigger failed silently) - workspace-switched event listener matches oldPath (not just != newPath) so normal project chats never show the mismatch banner - Daily check-in card hidden when onboarding not complete - Refresh docs now calls generateRootDocs() + generateDirectoryDocs() - Banner "Open" button reuses latest session (checkin mode) instead of always creating new onboarding session Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
258 lines
16 KiB
Markdown
258 lines
16 KiB
Markdown
# 助理工作区 — 交接文档
|
||
|
||
## 概述
|
||
|
||
助理工作区让用户指定一个目录存放 AI 人格/记忆文件(soul.md、user.md、memory.md、claude.md),在助理项目会话中自动注入系统提示词。附带对话式引导问卷(13 题)和每日轻量问询(3 题)。V2 新增索引/检索/分类/组织能力。
|
||
|
||
## 目录结构
|
||
|
||
```
|
||
workspace/
|
||
├── claude.md # 执行规则
|
||
├── soul.md # 人格风格
|
||
├── user.md # 用户画像
|
||
├── memory.md # 长期记忆(append-only,去重保护)
|
||
├── README.ai.md # 自动生成的目录概览
|
||
├── PATH.ai.md # 自动生成的路径索引
|
||
├── Inbox/ # 默认捕获目录
|
||
├── memory/
|
||
│ └── daily/ # 每日记忆(episodic,按日期命名)
|
||
│ ├── 2026-03-05.md
|
||
│ └── archive.md # 过期日记的摘要归档
|
||
└── .assistant/
|
||
├── state.json # 状态持久化(schemaVersion: 2)
|
||
├── config.json # 工作区配置(组织方式、归档策略、索引参数)
|
||
├── taxonomy.json # 分类体系(目录角色推断 + 演化建议)
|
||
└── index/
|
||
├── manifest.jsonl # 文件级元数据索引
|
||
├── chunks.jsonl # Markdown 分块索引
|
||
└── hotset.json # 高频访问文件(pinned + frequent)
|
||
```
|
||
|
||
## 核心模块
|
||
|
||
### `src/lib/assistant-workspace.ts`(V1 + V2)
|
||
|
||
| 函数 | 职责 |
|
||
|------|------|
|
||
| `validateWorkspace(dir)` | 检查目录和文件状态 |
|
||
| `initializeWorkspace(dir)` | 补建缺失模板 + state.json + V2 目录 |
|
||
| `loadWorkspaceFiles(dir)` | 读核心文件 + 每日记忆 + 根文档 |
|
||
| `assembleWorkspacePrompt(files, retrievalResults?)` | XML 标签拼接,按预算分配(总上限 40000 chars) |
|
||
| `loadState(dir)` / `saveState(dir, state)` | 读写 `.assistant/state.json` |
|
||
| `needsDailyCheckIn(state)` | 判断是否需要每日问询 |
|
||
| `generateDirectoryDocs(dir)` | 扫描子目录生成 README.ai.md + PATH.ai.md |
|
||
| `generateRootDocs(dir)` | 生成根级 README.ai.md + PATH.ai.md |
|
||
| `ensureDailyDir(dir)` | 确保 `memory/daily/` 目录存在 |
|
||
| `writeDailyMemory(dir, date, content)` | 写每日记忆文件 |
|
||
| `loadDailyMemories(dir, count)` | 加载最近 N 天的每日记忆 |
|
||
| `migrateStateV1ToV2(dir)` | V1→V2 自动迁移 |
|
||
|
||
### `src/lib/workspace-config.ts`(V2 新增)
|
||
|
||
工作区配置管理。`loadConfig()` 读取 `.assistant/config.json`,与 `DEFAULT_CONFIG` 深合并。配置项包括:
|
||
|
||
- `organizationStyle`:project / time / topic / mixed
|
||
- `captureDefault`:新信息默认存放目录
|
||
- `archivePolicy`:归档天数、是否自动归档
|
||
- `ignore`:索引忽略的 glob 模式
|
||
- `index`:分块大小、最大深度、文件大小上限
|
||
|
||
### `src/lib/workspace-taxonomy.ts`(V2 新增)
|
||
|
||
分类体系推断。从目录名映射已知角色(notes/project/journal/archive/inbox 等),按 confidence 评分。`classifyPath()` 按最长路径前缀匹配分类。
|
||
|
||
### `src/lib/workspace-indexer.ts`(V2 新增)
|
||
|
||
Markdown 索引引擎。
|
||
|
||
| 函数 | 职责 |
|
||
|------|------|
|
||
| `indexWorkspace(dir, { force? })` | **增量**索引:比较 mtime 跳过未变文件,仅重索引 stale 文件 |
|
||
| `indexFile(dir, filePath)` | 单文件索引:frontmatter 解析 + 分块 + 分类 |
|
||
| `chunkMarkdown(content, size, overlap)` | 按 heading 切 section,大 section 滑窗分块 |
|
||
| `extractMarkdownMeta(content)` | 提取 title/tags/headings/aliases |
|
||
| `loadManifest(dir)` / `loadChunks(dir)` | 读取 JSONL 索引文件 |
|
||
| `getIndexStats(dir)` | 统计文件数/分块数/stale 数 |
|
||
|
||
**增量索引机制**:每次聊天开始时自动调用(在检索之前),对未变文件复用已有 manifest/chunks 条目,仅写入有变化时才更新文件。UI "重新索引"按钮走 `force: true` 全量模式。
|
||
|
||
### `src/lib/workspace-retrieval.ts`(V2 新增)
|
||
|
||
关键词检索引擎。
|
||
|
||
| 函数 | 职责 |
|
||
|------|------|
|
||
| `parseQuery(query)` | 分词:英文按空白/标点 + 停用词过滤;**CJK 文本按 bigram + unigram** |
|
||
| `scoreManifest(entry, keywords)` | 标题 10 / 标签 8 / 别名 7 / 标题 5 / 路径 3 |
|
||
| `scoreChunk(chunk, keywords)` | 正文出现次数 + heading 2x |
|
||
| `searchWorkspace(dir, query, { limit })` | 综合评分 + **hotset 加权**(pinned +5,frequent +log2(count)) |
|
||
| `loadHotset(dir)` / `updateHotset(dir, paths)` | 高频文件管理 |
|
||
|
||
**CJK 支持**:`parseQuery()` 检测 CJK 字符范围(U+4E00–U+9FFF 等),CJK 连续 run 生成 unigram + bigram,英文 run 按空白切分 + 停用词过滤。确保中文查询能命中标题/标签/正文中的中文内容。
|
||
|
||
### `src/lib/workspace-organizer.ts`(V2 新增)
|
||
|
||
| 函数 | 职责 |
|
||
|------|------|
|
||
| `assertContained(dir, relativePath)` | 路径边界校验:拒绝绝对路径、`~`、`../` 穿越、符号链接逃逸 |
|
||
| `captureNote(dir, title, content)` | 将笔记写入默认捕获目录(经 `assertContained` 校验) |
|
||
| `classifyAndSuggest(dir, filePath)` | 基于 taxonomy 建议分类和目标路径 |
|
||
| `moveFile(dir, from, to)` | 移动文件(经 `assertContained` 校验) |
|
||
| `archiveDailyMemories(dir)` | 超过 retention 天数的日记摘要归档 + 删除原文件 |
|
||
| `promoteDailyToLongTerm(dir, date)` | 提取 "Candidate Long-Term Memory" 追加到 memory.md(**幂等**) |
|
||
| `suggestTaxonomyEvolution(dir)` | 分析文件分布,建议新建/归档分类 |
|
||
|
||
**memory.md 去重保护**:
|
||
- `promoteDailyToLongTerm()` 检查日记文件的 `<!-- promoted -->` 标记 + memory.md 的 `## Promoted from {date}` 标题,双重防重复
|
||
- check-in 路由的 inline promotion 也检查 memory.md 中是否已存在同日标题
|
||
|
||
### Prompt 注入(`src/app/api/chat/route.ts`)
|
||
|
||
仅当 `session.working_directory === getSetting('assistant_workspace_path')` 时注入:
|
||
1. **增量索引**(在检索前执行,确保当前轮拿到最新内容)
|
||
2. **检索**:对用户消息调用 `searchWorkspace()` 取相关上下文 + 更新 hotset
|
||
3. **工作区文件内容**(`<assistant-workspace>` XML,含核心文件 + 每日记忆 + 检索结果)
|
||
4. **引导/问询指令**(`<assistant-project-task>`)
|
||
|
||
普通项目聊天完全不受影响。
|
||
|
||
### 自动触发链路
|
||
|
||
1. **Mount 触发**:`ChatView` mount 时 500ms 延迟检测,使用 `startStream()` 直接发送(非 `sendMessage`)
|
||
2. **Focus 兜底**:`MessageInput` onFocus 首次触发,防 mount 未生效
|
||
3. **防重复**:`assistantTriggerFiredRef`(组件级 ref)防止同一 mount 周期内重复触发;`state.hookTriggeredSessionId` 按 session 粒度防止跨页面重复(但若 session 无消息则视为上次失败,允许重试)
|
||
4. **autoTrigger 标志**:触发时携带 `autoTrigger: true`,后端跳过保存用户消息和标题更新,实现"AI 先说话"体验
|
||
5. **hookTriggeredSessionId 清理**:引导/问询完成后前端调用 `POST /api/workspace/hook-triggered` 发送 `{ sessionId: '__clear__' }` 清除标记,确保下次进入可再次触发
|
||
6. **最新会话校验**:每日问询仅在该工作区最新会话中触发,旧会话打开不会劫持问询(通过 `GET /api/workspace/latest-session` 校验)
|
||
|
||
### 确定性落盘
|
||
|
||
AI 在对话中输出 `onboarding-complete` / `checkin-complete` 代码块 → 前端 `detectAssistantCompletion()` 解析 → 调 POST 对应端点 → 后端 AI 生成文件内容(失败回退原始答案)→ 清除 `hookTriggeredSessionId`。
|
||
|
||
**安全绑定**:`detectAssistantCompletion()` 在处理 fence 前先校验 `workingDirectory === assistant_workspace_path`,确保普通项目聊天或旧助理会话即使输出同样的 fence 也不会写入工作区文件。前端同时把 `sessionId` 传给后端,后端再次校验 `session.working_directory === workspacePath`(双重校验)。
|
||
|
||
**注意**:`onboarding-complete` / `checkin-complete` 是自定义 fence 语言标记,不是真正的编程语言。Shiki / Streamdown 渲染时会降级为纯文本(已通过 `supportsLanguage()` 前置检查静默处理)。
|
||
|
||
### 每日 check-in 数据流(V2 改进)
|
||
|
||
1. 用户回答 3 个问题 → 前端解析 `checkin-complete` JSON
|
||
2. 后端并行调 AI 生成:每日记忆条目 + 长期记忆提升候选 + user.md 更新
|
||
3. 每日记忆写入 `memory/daily/{date}.md`(episodic,不破坏长期记忆)
|
||
4. 长期记忆提升:仅追加新的稳定事实到 memory.md(日期去重)
|
||
5. 自动归档:过期日记摘要 → `archive.md`,7 天前的日记尝试 promote(幂等)
|
||
|
||
## API 端点
|
||
|
||
| 路由 | 方法 | 职责 |
|
||
|------|------|------|
|
||
| `/api/settings/workspace` | GET | 返回路径 + 文件状态 + state + **taxonomy**;路径无效时返回 `valid: false` + `reason`(path_not_found / not_a_directory / not_readable / not_writable) |
|
||
| `/api/settings/workspace` | PUT | 原子保存路径:先执行 initialize / resetOnboarding 副作用,全部成功后才 `setSetting()`;支持 `{ path, initialize?, resetOnboarding? }` |
|
||
| `/api/workspace/inspect` | GET | 切换前预检:`?path=xxx` → 返回 exists / isDirectory / readable / writable / hasAssistantData / workspaceStatus(empty / normal_directory / existing_workspace / partial_workspace / invalid)+ summary |
|
||
| `/api/workspace/session` | POST | 创建或复用助理会话(onboarding 新建,checkin 复用);创建前校验路径存在性 + R_OK + W_OK |
|
||
| `/api/workspace/onboarding` | POST | 接收 13 题答案 + sessionId → AI 生成 soul.md + user.md + claude.md + memory.md + config.json + taxonomy.json;校验 session 归属 |
|
||
| `/api/workspace/checkin` | POST | 接收答案 + sessionId → 写每日记忆 + 增量更新 memory.md + user.md;校验 session 归属 |
|
||
| `/api/workspace/hook-triggered` | POST | 设置/清除 hookTriggeredSessionId |
|
||
| `/api/workspace/latest-session` | GET | 返回指定 workingDirectory 的最新会话 ID |
|
||
| `/api/workspace/docs` | POST | 刷新根目录 + 子目录文档(`generateRootDocs` + `generateDirectoryDocs`) |
|
||
| `/api/workspace/index` | GET | 返回索引统计(文件数/分块数/stale 数) |
|
||
| `/api/workspace/index` | POST | **强制全量重建索引** |
|
||
| `/api/workspace/search` | GET | 关键词检索(`?q=xxx&limit=5`) |
|
||
| `/api/workspace/organize` | POST | 组织操作:capture / classify / move / archive / suggest-evolution;路径参数强制相对路径 + containment 校验 |
|
||
|
||
## 状态字段(`.assistant/state.json`)
|
||
|
||
```typescript
|
||
interface AssistantWorkspaceState {
|
||
onboardingComplete: boolean;
|
||
lastCheckInDate: string | null; // "YYYY-MM-DD"
|
||
schemaVersion: number; // 当前 2
|
||
hookTriggeredSessionId?: string; // 防重复触发
|
||
}
|
||
```
|
||
|
||
## 工作区切换流程
|
||
|
||
切换助理工作区路径是一个多步流程,确保数据安全和用户确认:
|
||
|
||
### 1. 路径输入与实时校验
|
||
|
||
- 输入框 debounce 500ms 后触发路径格式校验(绿 ✓ / 红 ✕ / 加载 spinner)
|
||
- 不存在的路径视为"可创建",不阻断保存
|
||
|
||
### 2. Inspect 预检
|
||
|
||
点击保存时,前端先调 `GET /api/workspace/inspect?path=xxx`,获取目标路径的 `workspaceStatus`:
|
||
|
||
| 状态 | 含义 | 确认弹窗行为 |
|
||
|------|------|-------------|
|
||
| `not_found` | 路径不存在 | 提示"将创建目录并初始化"→ `executeSave(initialize=true)` |
|
||
| `empty` | 空目录 | 提示"将初始化为工作区"→ `executeSave(initialize=true)` |
|
||
| `normal_directory` | 非空普通目录 | 提示"将在此初始化工作区"→ `executeSave(initialize=true)` |
|
||
| `existing_workspace` | 已有完整工作区 | 提供"继续接管"(reuse 模式)或"重新引导"(reset onboarding) |
|
||
| `partial_workspace` | 有 .assistant 但缺文件 | 提供"继续接管"或"重新初始化" |
|
||
|
||
### 3. 原子保存
|
||
|
||
`PUT /api/settings/workspace` 先执行所有副作用(`initializeWorkspace()` / `saveState(onboardingComplete: false)`),全部成功后才调 `setSetting()` 持久化路径。任一步骤失败则路径设置不变。
|
||
|
||
### 4. 自动导航
|
||
|
||
保存成功后:
|
||
- 前端根据 `navigateMode` 创建会话(`'new'` → onboarding 模式新建,`'reuse'` → checkin 模式复用最新会话)
|
||
- 派发 `assistant-workspace-switched` 自定义事件
|
||
- 自动跳转到新会话
|
||
|
||
### 5. 切换横幅
|
||
|
||
`ChatView` 监听 `assistant-workspace-switched` 事件,并检测当前会话的 `workingDirectory` 是否与 `assistant_workspace_path` 一致。不一致时显示黄色横幅,提供"打开新助理项目"按钮。
|
||
|
||
### 无效路径兜底
|
||
|
||
GET settings 返回 `valid: false` + `reason` 时,设置页面隐藏引导/问询/tab 面板,显示红色警告横幅。用户需修改路径后才能继续。
|
||
|
||
## 设置 UI(`AssistantWorkspaceSection.tsx`)
|
||
|
||
底部 tab 面板(仅当 `workspace.valid !== false` 时显示):
|
||
|
||
| Tab | 内容 |
|
||
|-----|------|
|
||
| 文件状态 | 4 个核心文件存在/大小 + 刷新目录文档按钮 |
|
||
| 分类体系 | 从 taxonomy.json 读取的分类列表 |
|
||
| 文件索引 | 索引统计(文件数/分块数/过期数/上次索引时间)+ 重新索引按钮 |
|
||
| 组织管理 | 归档旧记忆按钮 |
|
||
|
||
## 关键约束
|
||
|
||
- 引导与问询互斥:onboarding 完成当天不触发 daily check-in
|
||
- 每日问询复用最后一条会话,不新建
|
||
- `hookTriggeredSessionId` 按 session 粒度防重复,换 session 可重新触发;完成后自动清除;若 session 无消息(上次触发失败)则允许重试
|
||
- `autoTrigger` 贯穿 ChatView → stream-session-manager → chat/route.ts,跳过用户消息保存和标题更新
|
||
- 每日问询不受 `messages.length > 0` 限制,可在有历史消息的复用会话中触发
|
||
- 文件截断策略:每文件 8000 chars(head 6000 + truncated marker + tail 1800)
|
||
- **索引在检索前执行**:确保当前轮对话能看到最新文件内容
|
||
- **增量索引**:聊天入口的索引调用仅处理 stale 文件,大文库不会拖慢首轮响应
|
||
- **memory.md 不可被覆写**:所有写入都是 append,promotion 有日期去重保护
|
||
- **hotset 影响排序**:pinned 文件 +5 分,frequent 文件 +log2(count) 分(上限 +4)
|
||
- **路径校验 R_OK + W_OK**:GET settings、session 创建均要求读写权限,只读目录返回 `not_writable` 错误
|
||
- **切换原子性**:PUT settings 先执行副作用再持久化,失败不留脏状态
|
||
- **切换横幅**:`ChatView` 通过 `assistant-workspace-switched` 事件 + workingDirectory 比对检测不匹配;仅对旧助理会话显示(普通项目聊天不受影响);事件监听器匹配 `oldPath`,挂载时通过 inspect 检查 `hasAssistantData`
|
||
- **Completion fence 安全绑定**:前端校验 `workingDirectory === assistant_workspace_path` + 后端校验 `session.working_directory === workspacePath`,双重防止非助理会话写入工作区
|
||
- **Organize 路径边界**:`assertContained()` 拒绝绝对路径 / `~` / `../` / 符号链接逃逸;API 入口 `validateRelativePath()` 预检;`captureDefault` 写入 config 时强制相对路径
|
||
- **每日问询入口**:设置页中仅当 `onboardingComplete === true` 时显示每日问询卡片
|
||
- **刷新文档**:`/api/workspace/docs` 同时调用 `generateRootDocs()` + `generateDirectoryDocs()`
|
||
|
||
## V1 → V2 变更摘要
|
||
|
||
| 维度 | V1 | V2 |
|
||
|------|----|----|
|
||
| Schema | `schemaVersion: 1` | `schemaVersion: 2`,自动迁移 |
|
||
| 引导问卷 | 10 题 | 13 题(新增组织方式、默认收件箱、归档策略) |
|
||
| 引导产物 | soul.md + user.md | + claude.md + memory.md + config.json + taxonomy.json |
|
||
| 记忆模型 | 每日 check-in 覆写 memory.md | 每日记忆写入 `memory/daily/` + memory.md 仅追加 |
|
||
| 索引 | 无 | Markdown 分块索引(manifest + chunks JSONL) |
|
||
| 检索 | 无 | 关键词检索 + CJK bigram + hotset 加权 |
|
||
| 分类 | 无 | 目录角色推断 + 演化建议 |
|
||
| 组织 | 无 | 捕获/分类/移动/归档/长期记忆晋升 |
|
||
| 设置 UI | 文件状态 | + 分类体系 / 文件索引 / 组织管理 tab |
|