diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..0f2662d
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,60 @@
+# 在 PR / master 推送时做依赖安装、前端与 Rust 侧编译检查,避免明显破坏合并进主分支。
+name: CI
+
+on:
+ push:
+ branches: [master]
+ pull_request:
+ branches: [master]
+
+concurrency:
+ group: ci-${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Bun
+ uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: latest
+
+ - name: Install dependencies
+ run: bun install --frozen-lockfile
+
+ # 保证 CHANGELOG 与关于页数据源一致(脚本见 scripts/sync-changelog.mjs)
+ - name: Verify changelog sync
+ run: |
+ bun run sync:changelog
+ git diff --exit-code apps/desktop/src/data/app-changelog.md
+
+ - name: Build sidecar
+ run: cd packages/sidecar && bun run build
+
+ - name: Build desktop frontend
+ run: cd apps/desktop && bun run build
+
+ # Tauri 在 Linux 上链接需系统库,否则 cargo check 会失败
+ - name: Install Linux build dependencies (Tauri)
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y \
+ libwebkit2gtk-4.1-dev \
+ build-essential \
+ curl \
+ libssl-dev \
+ libgtk-3-dev \
+ libayatana-appindicator3-dev \
+ librsvg2-dev \
+ patchelf
+
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Cargo check (desktop)
+ run: cd apps/desktop/src-tauri && cargo check --all-targets
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..689c49d
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,23 @@
+# 推送形如 v0.1.2 的 tag 时自动创建 GitHub Release(含自动生成的提交说明)。
+# 若后续需要上传各平台安装包,可在此 job 中追加 matrix 与 tauri build。
+name: Release
+
+on:
+ push:
+ tags:
+ - 'v[0-9]+.[0-9]+.[0-9]+'
+
+permissions:
+ contents: write
+
+jobs:
+ github-release:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v2
+ with:
+ generate_release_notes: true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index acbb947..76ca9dd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,38 +1,65 @@
# 寻迹(XunJi)更新日志
+> 面向使用者的更新说明。实现细节见 `docs/CHANGELOG.md`。
+
+## 0.1.2(2026-03-22 – 2026-03-23)
+
+### 新增
+
+- **CodeBuddy**:支持从 CodeBuddy 导入对话记录,与本机扩展数据位置一致(Mac / Linux 常见路径见设置说明)。需在 **设置 → 数据源** 中打开「CodeBuddy」;默认关闭,避免未安装时白白扫盘。
+- **配置迁移**:若你用过旧版配置,应用启动时会**自动补全** CodeBuddy 相关选项,并把过时的扫描路径迁到当前扩展目录。
+
+### 体验改进
+
+- **CodeBuddy 对话更好认**:导入后,列表和详情会尽量显示与 CodeBuddy 里**相同的会话标题**,不必先「分析」也能分辨是哪一场对话。
+- **项目分组更直观**:按你当时在本机打开的项目文件夹来归类;若文件夹名字是一长串数字或看不懂的编码,会尽量用**上一层文件夹的名字**显示,减少整屏「乱码」分组。
+- **对话回放更清晰**:回看 CodeBuddy 对话时,AI **调用工具**、**思考过程**的展示更整齐,阅读起来更自然。
+
+### 修正与调整
+
+- **CodeBuddy**:存储位置已按**实际产品**对齐;此前若按旧路径采集会对不上,现已修正。
+- **老版本 CodeBuddy 存的对话**也能正确识别项目与标题,减少「只能看到内部编号」的情况。
+- **修复**部分会话在侧边栏只显示一长串内部 ID、**看不到真实项目名**的问题(与 CodeBuddy 保存消息的方式差异有关)。
+
+### 其他
+
+- 未做「提炼笔记」的会话,列表上会优先显示上面采集到的**会话标题**;详情页标题也会用上;工具名称在回放里显示更稳定。
+
+---
+
## 0.1.1(2026-03-22)
-### 新增与优化
+### 新增
-- **会话整理**:在对话记录侧栏可进入「会话整理」,在目录树中多选数据源、主机或项目后,一次性清理所选范围内的会话记录;清理的是应用内数据,**不会删除**本机上的原始对话文件;若一次选了多个范围,同一对话只会被删一次
-- **关于**:在设置中可查看应用版本、运行环境与本次更新说明
-- **导出**:笔记可导出为 Markdown,支持单条、多选或整库导出;整库导出不受当前列表筛选影响
-- **会话与笔记**:列表与详情可查看会话路径与会话标识;列表标题优先展示首条用户消息摘要
-- **分析**:分析失败时可看到原因;分析队列中可查看任务详情
-- **阅读**:笔记正文 Markdown 显示效果优化
-- **体验**:搜索与同步等相关提示与文案优化
+- **会话整理**:在对话记录侧栏可进入「会话整理」,在目录树里多选数据源、本机或项目后,一次性清理**应用里**的记录;**不会删除**你电脑上的原始对话文件;多选时同一条对话只删一次。
+- **关于**:在设置中可查看版本、运行环境和更新说明。
+- **导出**:笔记可导出为 Markdown,支持单条、多选或整库;整库导出不受当前列表筛选影响。
+- **会话与笔记**:列表与详情可查看来源路径和会话标识;列表标题优先用首条用户消息的摘要。
+- **分析**:失败时会提示原因;可在分析队列里查看任务详情。
+- **阅读**:笔记正文在界面上的阅读体验优化。
+- **体验**:搜索、同步等提示与文案优化。
-### 修复与调整
+### 修正与调整
-- 侧栏点选「未关联项目」时,列表能正确显示该分组下的会话
-- 批量删除会话仅通过「会话整理」勾选分组后进行,避免误操作
+- **侧栏**:选择「未关联项目」时,列表能正确显示该分组下的会话。
+- **删除方式**:批量删除会话需通过「会话整理」勾选分组后进行,降低误删风险。
---
## 0.1.0
-**发布说明**:首个正式版本,可在本机完成从采集对话、整理列表、提炼笔记到知识库浏览与搜索的完整流程。
+**说明**:首个正式版本,可在本机完成从**采集对话 → 整理列表 → 提炼笔记 → 知识库浏览与搜索**的完整流程。
### 新增
-- **应用**:桌面端应用,支持浅色与深色外观
-- **采集**:支持从 Claude Code、Cursor 等来源导入对话到本地
-- **整理**:对话列表、会话详情(含笔记与对话回放)、按数据源分组的侧栏目录
-- **提炼**:对会话做价值判断并生成结构化笔记,接入本地模型或命令行工具
-- **知识库**:笔记列表、全文搜索、标签与类型筛选
-- **其他**:同步与设置、分析任务队列
+- **应用**:桌面端,支持浅色与深色外观。
+- **采集**:支持从 Claude Code、Cursor 等来源导入对话。
+- **整理**:对话列表、会话详情(含笔记与对话回放)、按数据源分组的侧栏。
+- **提炼**:对会话做价值判断并生成结构化笔记,可接入本机模型或命令行工具。
+- **知识库**:笔记列表、全文搜索、标签与类型筛选。
+- **其他**:同步与设置、分析任务队列。
### 已知限制
-- 部分导出入口仍为占位或能力受限;个别数据源仅可配置名称与图标,尚不能采集
-- 同步需手动触发;与其他工具的深度联动仍在完善中
+- 部分导出能力仍在完善;个别数据源暂时只能改名称或图标,尚不能采集。
+- 同步需手动触发;与其他工具的深度联动仍在加强中。
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index 567b830..77f0f44 100644
--- a/apps/desktop/package.json
+++ b/apps/desktop/package.json
@@ -1,7 +1,7 @@
{
"name": "@xunji/desktop",
"private": true,
- "version": "0.1.1",
+ "version": "0.1.2",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml
index 8764d59..f0cf002 100644
--- a/apps/desktop/src-tauri/Cargo.toml
+++ b/apps/desktop/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "xunji-desktop"
-version = "0.1.1"
+version = "0.1.2"
description = "寻迹 - AI 编程知识管理平台"
authors = ["XunJi"]
edition = "2021"
diff --git a/apps/desktop/src-tauri/src/collector/claude_code.rs b/apps/desktop/src-tauri/src/collector/claude_code.rs
index 256a4b1..48c92fd 100644
--- a/apps/desktop/src-tauri/src/collector/claude_code.rs
+++ b/apps/desktop/src-tauri/src/collector/claude_code.rs
@@ -133,6 +133,7 @@ impl ClaudeCodeCollector {
session_id,
project_path: final_project_path,
project_name: final_project_name,
+ analysis_title: None,
messages: parse_result.messages,
raw_path: file_path.to_string_lossy().to_string(),
created_at,
diff --git a/apps/desktop/src-tauri/src/collector/codebuddy.rs b/apps/desktop/src-tauri/src/collector/codebuddy.rs
new file mode 100644
index 0000000..4d76975
--- /dev/null
+++ b/apps/desktop/src-tauri/src/collector/codebuddy.rs
@@ -0,0 +1,631 @@
+//! CodeBuddy 产品会话采集(与问渠 `scanner/codebuddy.py` 路径一致)。
+//!
+//! ```text
+//! 扫描根目录(默认):
+//! macOS ~/Library/Application Support/CodeBuddyExtension/Data
+//! Linux ~/.local/share/CodeBuddyExtension
+//!
+//! 在根目录下递归查找「会话叶目录」:
+//! 含 index.json(且存在顶层 messages 数组)+ messages/*.json
+//! (工作区目录仅有 conversations 列表、无 messages,会自动排除)
+//!
+//! 单条消息:messages/{id}.json 内 message 为 JSON 字符串,解析后取 content(字符串 / text 块数组 / 旧版单字符数组,见 `docs/CodeBuddy 会话记录提取逻辑文档.md`)。
+//! ```
+//!
+//! source_id 仍为 `codebuddy-cli`,与现有 config.toml / 侧栏 id 兼容。
+
+use std::collections::HashSet;
+use std::fs;
+use std::path::{Path, PathBuf};
+
+use chrono::{DateTime, Utc};
+use serde_json::Value;
+use walkdir::WalkDir;
+
+use super::normalizer::{NormalizedMessage, NormalizedSession};
+
+const SOURCE_ID: &str = "codebuddy-cli";
+
+/// CodeBuddy(扩展)采集器
+pub struct CodeBuddyCollector {
+ /// 来自配置的扫描根目录(已展开 ~),与问渠 LOCALHOST_CANDIDATES 一致
+ scan_dirs: Vec,
+}
+
+impl CodeBuddyCollector {
+ pub fn new(scan_dirs: Vec) -> Self {
+ Self { scan_dirs }
+ }
+
+ /// 扫描所有配置根目录下的会话目录,产出标准化会话列表
+ pub fn collect(&self) -> Vec {
+ log::info!(
+ "CodeBuddy 采集开始,解析后的扫描根目录共 {} 个: {:?}",
+ self.scan_dirs.len(),
+ self.scan_dirs
+ );
+ let mut sessions = Vec::new();
+ let mut seen = HashSet::::new();
+
+ for root in &self.scan_dirs {
+ if !root.is_dir() {
+ log::info!("CodeBuddy:扫描根目录不存在,跳过: {}", root.display());
+ continue;
+ }
+
+ let mut leaf_candidates = 0u32;
+ let mut parsed_ok = 0u32;
+ for entry in WalkDir::new(root)
+ .max_depth(32)
+ .follow_links(false)
+ .into_iter()
+ .filter_map(|e| e.ok())
+ {
+ let path = entry.path();
+ if !path.is_dir() {
+ continue;
+ }
+ if !is_session_leaf_dir(path) {
+ continue;
+ }
+ leaf_candidates += 1;
+ let key = path.to_string_lossy().to_string();
+ if !seen.insert(key) {
+ continue;
+ }
+
+ match parse_codebuddy_session_dir(path) {
+ Ok(Some(s)) => {
+ sessions.push(s);
+ parsed_ok += 1;
+ }
+ Ok(None) => {}
+ Err(e) => log::warn!("解析 CodeBuddy 会话目录失败: {} — {}", path.display(), e),
+ }
+ }
+ log::info!(
+ "CodeBuddy:根 {} — 叶目录 {} 个,成功解析 {} 个会话",
+ root.display(),
+ leaf_candidates,
+ parsed_ok
+ );
+ }
+
+ log::info!("CodeBuddy 扩展采集完成,共 {} 个会话", sessions.len());
+ sessions
+ }
+}
+
+/// 会话叶目录:含 index.json(顶层 `messages` 数组非空)且存在 messages 子目录
+fn is_session_leaf_dir(path: &Path) -> bool {
+ let index_path = path.join("index.json");
+ let msgs_dir = path.join("messages");
+ if !index_path.is_file() || !msgs_dir.is_dir() {
+ return false;
+ }
+ let Ok(text) = fs::read_to_string(&index_path) else {
+ return false;
+ };
+ let Ok(v) = serde_json::from_str::(&text) else {
+ return false;
+ };
+ match v.get("messages").and_then(|m| m.as_array()) {
+ Some(arr) if !arr.is_empty() => true,
+ _ => false,
+ }
+}
+
+/// 解析单条 message 文件外层 JSON 中的 `message` 字段(JSON 字符串或已解析对象)
+fn parse_message_inner(raw: &Value) -> Value {
+ match raw.get("message") {
+ Some(Value::String(s)) => serde_json::from_str(s).unwrap_or(Value::Null),
+ Some(v @ Value::Object(_)) => v.clone(),
+ _ => Value::Null,
+ }
+}
+
+/// 从首条 **用户** 消息的 inner `content` 文本中提取工作区路径,返回 (绝对路径, 目录名)
+///
+/// ```text
+/// messages/{id}.json → raw["message"] → inner_content_to_text(字符串 / text 块数组 / 旧版单字符数组)
+/// → Workspace Folder: 或 Workspace Path: → Path::file_name → "proj"
+/// ```
+fn try_extract_project_from_first_user_message(
+ session_dir: &Path,
+ msg_list: &[Value],
+) -> Option<(String, String)> {
+ for meta in msg_list {
+ if meta["role"].as_str() != Some("user") {
+ continue;
+ }
+ let id = match meta["id"].as_str() {
+ Some(s) if !s.is_empty() && !s.starts_with('.') => s,
+ _ => continue,
+ };
+ let msg_file = session_dir.join("messages").join(format!("{}.json", id));
+ if !msg_file.is_file() {
+ continue;
+ }
+ let text = fs::read_to_string(&msg_file).ok()?;
+ let raw: Value = serde_json::from_str(&text).ok()?;
+ let inner = parse_message_inner(&raw);
+ let folder = extract_workspace_folder_from_inner(&inner)?;
+ let name = Path::new(&folder)
+ .file_name()
+ .map(|n| n.to_string_lossy().to_string())
+ .filter(|s| !s.is_empty())?;
+ return Some((folder, name));
+ }
+ None
+}
+
+/// 将内层 `message` 的 `content` 展平为一段可搜索文本(与 `docs/CodeBuddy 会话记录提取逻辑文档.md` 一致)。
+///
+/// - **字符串**:整段 payload(新版常见)
+/// - **对象数组**:`[{ "type": "text", "text": "..." }, ...]`(新版 block)
+/// - **字符串数组**:`["<","u","s",...]` 单字符片段 `join("")`(旧版 ~2025-11)
+fn inner_content_to_text(inner: &Value) -> String {
+ if let Some(s) = inner.get("content").and_then(|c| c.as_str()) {
+ return s.to_string();
+ }
+ let Some(arr) = inner.get("content").and_then(|c| c.as_array()) else {
+ return String::new();
+ };
+ if arr.is_empty() {
+ return String::new();
+ }
+ // 新版:首项为对象 → 取 type==text 的 text(及 assistant 侧 reasoning/tool 在各自函数中处理)
+ if matches!(arr.first(), Some(Value::Object(_))) {
+ let mut buf = String::new();
+ for b in arr {
+ if b.get("type").and_then(|t| t.as_str()) == Some("text") {
+ if let Some(t) = b.get("text").and_then(|x| x.as_str()) {
+ buf.push_str(t);
+ }
+ }
+ }
+ return buf;
+ }
+ // 旧版:JSON 字符串数组,每元素多为单字符,等价于 JS `content.join("")`
+ if arr.iter().all(|v| v.as_str().is_some()) {
+ return arr.iter().filter_map(|v| v.as_str()).collect();
+ }
+ String::new()
+}
+
+/// 从 inner 展平文本后解析工作区绝对路径(先 `Workspace Folder:`,再兼容旧版 `Workspace Path:`)。
+fn extract_workspace_folder_from_inner(inner: &Value) -> Option {
+ let text = inner_content_to_text(inner);
+ extract_workspace_folder_from_text_blob(&text)
+}
+
+/// 工作区目录 `index.json` 中 `conversations[].name`,用于列表/详情标题(与 IDE 侧会话列表一致)
+fn try_conversation_title_from_workspace_index(
+ workspace_dir: &Path,
+ conversation_id: &str,
+) -> Option {
+ let p = workspace_dir.join("index.json");
+ let text = fs::read_to_string(&p).ok()?;
+ let v: Value = serde_json::from_str(&text).ok()?;
+ let arr = v.get("conversations")?.as_array()?;
+ for c in arr {
+ if c.get("id").and_then(|x| x.as_str()) == Some(conversation_id) {
+ let name = c.get("name").and_then(|x| x.as_str())?.trim();
+ if name.is_empty() {
+ return None;
+ }
+ return Some(truncate_chars(name, 200));
+ }
+ }
+ None
+}
+
+fn truncate_chars(s: &str, max_chars: usize) -> String {
+ let count = s.chars().count();
+ if count <= max_chars {
+ return s.to_string();
+ }
+ s.chars().take(max_chars).collect::() + "…"
+}
+
+/// 目录名看起来像日期戳或长 hex(无辨识度)时视为「不友好」
+fn is_ugly_dir_label(s: &str) -> bool {
+ let t = s.trim();
+ if t.is_empty() {
+ return true;
+ }
+ if t.chars().all(|c| c.is_ascii_digit()) && t.len() >= 8 {
+ return true;
+ }
+ if t.len() >= 16 && t.chars().all(|c| c.is_ascii_hexdigit()) {
+ return true;
+ }
+ false
+}
+
+/// 侧栏分组:Workspace Folder 末级为纯数字/长 hex 时,尝试用上一级目录名(如 `…/wenqu/20251201` → wenqu)
+fn friendly_project_folder_name(workspace_abs: &str) -> String {
+ let p = Path::new(workspace_abs);
+ let base = p
+ .file_name()
+ .map(|s| s.to_string_lossy().to_string())
+ .unwrap_or_default();
+ if is_ugly_dir_label(&base) {
+ if let Some(parent) = p.parent() {
+ if let Some(pname) = parent.file_name() {
+ let pn = pname.to_string_lossy().to_string();
+ if !pn.is_empty() && !is_ugly_dir_label(&pn) {
+ return pn;
+ }
+ }
+ }
+ }
+ base
+}
+
+/// 在一段文本中查找 `key`(如 `Workspace Folder:`)后的绝对路径,截断到换行或 `<`。
+fn extract_line_after_workspace_key(s: &str, key: &str) -> Option {
+ let idx = s.find(key)?;
+ let rest = s[idx + key.len()..].trim_start();
+ let end = rest
+ .find('\n')
+ .or_else(|| rest.find('\r'))
+ .or_else(|| rest.find('<'))
+ .unwrap_or(rest.len());
+ let path = rest[..end].trim().trim_end_matches('`').trim();
+ if path.is_empty() {
+ return None;
+ }
+ Some(path.to_string())
+}
+
+/// 优先 `Workspace Folder:`(新版),否则 `Workspace Path:`(旧版,见文档 §5、§6)
+fn extract_workspace_folder_from_text_blob(s: &str) -> Option {
+ extract_line_after_workspace_key(s, "Workspace Folder:")
+ .or_else(|| extract_line_after_workspace_key(s, "Workspace Path:"))
+}
+
+fn parse_codebuddy_session_dir(session_dir: &Path) -> Result
- 当前版本已接入采集的数据源:Claude Code(claude-code,JSONL)
- 与 Cursor(cursor,本地库)。
+ 当前版本已接入采集的数据源:Claude Code(claude-code,JSONL)、
+ Cursor(cursor,本地库)、
+ CodeBuddy(codebuddy-cli,CodeBuddyExtension 下会话目录,与问渠路径一致;默认关闭,需在设置中启用)。
+ 启用后请先点击保存,再点击顶部同步;侧栏若选了某一数据源筛选,请点「全部对话」或对应 CodeBuddy 节点以免被过滤。
若配置中存在其他 id,侧栏仍可显示品牌名,但同步时会跳过,直至后续版本接入采集器。
@@ -497,7 +500,7 @@ const extraArgsStr = computed({
:value="source.enabled"
size="small"
class="ml-3 mt-0.5"
- @update:value="toggleSource(source)"
+ @update:value="(v: boolean) => setSourceEnabled(source, v)"
/>
diff --git a/apps/desktop/src/data/app-changelog.md b/apps/desktop/src/data/app-changelog.md
index acbb947..76ca9dd 100644
--- a/apps/desktop/src/data/app-changelog.md
+++ b/apps/desktop/src/data/app-changelog.md
@@ -1,38 +1,65 @@
# 寻迹(XunJi)更新日志
+> 面向使用者的更新说明。实现细节见 `docs/CHANGELOG.md`。
+
+## 0.1.2(2026-03-22 – 2026-03-23)
+
+### 新增
+
+- **CodeBuddy**:支持从 CodeBuddy 导入对话记录,与本机扩展数据位置一致(Mac / Linux 常见路径见设置说明)。需在 **设置 → 数据源** 中打开「CodeBuddy」;默认关闭,避免未安装时白白扫盘。
+- **配置迁移**:若你用过旧版配置,应用启动时会**自动补全** CodeBuddy 相关选项,并把过时的扫描路径迁到当前扩展目录。
+
+### 体验改进
+
+- **CodeBuddy 对话更好认**:导入后,列表和详情会尽量显示与 CodeBuddy 里**相同的会话标题**,不必先「分析」也能分辨是哪一场对话。
+- **项目分组更直观**:按你当时在本机打开的项目文件夹来归类;若文件夹名字是一长串数字或看不懂的编码,会尽量用**上一层文件夹的名字**显示,减少整屏「乱码」分组。
+- **对话回放更清晰**:回看 CodeBuddy 对话时,AI **调用工具**、**思考过程**的展示更整齐,阅读起来更自然。
+
+### 修正与调整
+
+- **CodeBuddy**:存储位置已按**实际产品**对齐;此前若按旧路径采集会对不上,现已修正。
+- **老版本 CodeBuddy 存的对话**也能正确识别项目与标题,减少「只能看到内部编号」的情况。
+- **修复**部分会话在侧边栏只显示一长串内部 ID、**看不到真实项目名**的问题(与 CodeBuddy 保存消息的方式差异有关)。
+
+### 其他
+
+- 未做「提炼笔记」的会话,列表上会优先显示上面采集到的**会话标题**;详情页标题也会用上;工具名称在回放里显示更稳定。
+
+---
+
## 0.1.1(2026-03-22)
-### 新增与优化
+### 新增
-- **会话整理**:在对话记录侧栏可进入「会话整理」,在目录树中多选数据源、主机或项目后,一次性清理所选范围内的会话记录;清理的是应用内数据,**不会删除**本机上的原始对话文件;若一次选了多个范围,同一对话只会被删一次
-- **关于**:在设置中可查看应用版本、运行环境与本次更新说明
-- **导出**:笔记可导出为 Markdown,支持单条、多选或整库导出;整库导出不受当前列表筛选影响
-- **会话与笔记**:列表与详情可查看会话路径与会话标识;列表标题优先展示首条用户消息摘要
-- **分析**:分析失败时可看到原因;分析队列中可查看任务详情
-- **阅读**:笔记正文 Markdown 显示效果优化
-- **体验**:搜索与同步等相关提示与文案优化
+- **会话整理**:在对话记录侧栏可进入「会话整理」,在目录树里多选数据源、本机或项目后,一次性清理**应用里**的记录;**不会删除**你电脑上的原始对话文件;多选时同一条对话只删一次。
+- **关于**:在设置中可查看版本、运行环境和更新说明。
+- **导出**:笔记可导出为 Markdown,支持单条、多选或整库;整库导出不受当前列表筛选影响。
+- **会话与笔记**:列表与详情可查看来源路径和会话标识;列表标题优先用首条用户消息的摘要。
+- **分析**:失败时会提示原因;可在分析队列里查看任务详情。
+- **阅读**:笔记正文在界面上的阅读体验优化。
+- **体验**:搜索、同步等提示与文案优化。
-### 修复与调整
+### 修正与调整
-- 侧栏点选「未关联项目」时,列表能正确显示该分组下的会话
-- 批量删除会话仅通过「会话整理」勾选分组后进行,避免误操作
+- **侧栏**:选择「未关联项目」时,列表能正确显示该分组下的会话。
+- **删除方式**:批量删除会话需通过「会话整理」勾选分组后进行,降低误删风险。
---
## 0.1.0
-**发布说明**:首个正式版本,可在本机完成从采集对话、整理列表、提炼笔记到知识库浏览与搜索的完整流程。
+**说明**:首个正式版本,可在本机完成从**采集对话 → 整理列表 → 提炼笔记 → 知识库浏览与搜索**的完整流程。
### 新增
-- **应用**:桌面端应用,支持浅色与深色外观
-- **采集**:支持从 Claude Code、Cursor 等来源导入对话到本地
-- **整理**:对话列表、会话详情(含笔记与对话回放)、按数据源分组的侧栏目录
-- **提炼**:对会话做价值判断并生成结构化笔记,接入本地模型或命令行工具
-- **知识库**:笔记列表、全文搜索、标签与类型筛选
-- **其他**:同步与设置、分析任务队列
+- **应用**:桌面端,支持浅色与深色外观。
+- **采集**:支持从 Claude Code、Cursor 等来源导入对话。
+- **整理**:对话列表、会话详情(含笔记与对话回放)、按数据源分组的侧栏。
+- **提炼**:对会话做价值判断并生成结构化笔记,可接入本机模型或命令行工具。
+- **知识库**:笔记列表、全文搜索、标签与类型筛选。
+- **其他**:同步与设置、分析任务队列。
### 已知限制
-- 部分导出入口仍为占位或能力受限;个别数据源仅可配置名称与图标,尚不能采集
-- 同步需手动触发;与其他工具的深度联动仍在完善中
+- 部分导出能力仍在完善;个别数据源暂时只能改名称或图标,尚不能采集。
+- 同步需手动触发;与其他工具的深度联动仍在加强中。
diff --git a/apps/desktop/src/types/index.ts b/apps/desktop/src/types/index.ts
index 4d1ad93..81ff841 100644
--- a/apps/desktop/src/types/index.ts
+++ b/apps/desktop/src/types/index.ts
@@ -76,6 +76,8 @@ export interface Session {
sourceHost: string
projectPath: string | null
projectName: string | null
+ /** 采集侧可读标题(如 CodeBuddy 工作区 index 中的会话 name),未提炼笔记前可用于详情/列表 */
+ analysisTitle?: string | null
messageCount: number
contentHash: string | null
rawPath: string | null
diff --git a/apps/desktop/src/views/SessionDetailView.vue b/apps/desktop/src/views/SessionDetailView.vue
index 9215f4d..f343096 100644
--- a/apps/desktop/src/views/SessionDetailView.vue
+++ b/apps/desktop/src/views/SessionDetailView.vue
@@ -100,7 +100,11 @@ async function onExportMarkdown() {
function analyze() {
analyzeError.value = null
if (session.value) session.value.status = 'analyzing'
- const title = session.value?.projectName ?? session.value?.projectPath ?? props.sessionId
+ const title =
+ session.value?.analysisTitle
+ ?? session.value?.projectName
+ ?? session.value?.projectPath
+ ?? props.sessionId
queue.enqueue(props.sessionId, title, {
onLowValue: (result) => {
if (session.value) {
@@ -192,7 +196,7 @@ const valueColors: Record = {
- {{ session.projectName || session.projectPath || '未命名项目' }}
+ {{ session.analysisTitle || session.projectName || session.projectPath || '未命名项目' }}
{{ statusLabel }}
本文档与仓库根目录 `CHANGELOG.md` 的版本一一对应,保留**实现向**表述(字段名、路径、数据结构等),供开发、排查与文档对照使用。
+> **面向最终用户的说明请以根目录 `CHANGELOG.md` 为准。**
+
+---
+
+## 0.1.2(2026-03-22 – 2026-03-23)
+
+### 新增与优化
+
+- **采集**:支持 **CodeBuddy** 对话记录导入,扫描路径与问渠 `scanner/codebuddy.py` 一致:macOS `~/Library/Application Support/CodeBuddyExtension/Data`、Linux `~/.local/share/CodeBuddyExtension`,递归识别其下含 `index.json` 与 `messages/` 的会话目录;需在设置 → 数据源中开启「CodeBuddy」,默认关闭以免未安装时无效扫盘
+- **配置**:旧版配置文件在启动时会自动补充 CodeBuddy 数据源项(若尚无);若仍仅为旧默认 `~/.codebuddy`,启动时会迁移为上述 CodeBuddyExtension 根目录
+- **CodeBuddy 采集与展示**
+ - 从工作区目录 `index.json` 的 `conversations[].name` 写入会话展示标题(`analysis_title`),与 IDE 会话列表一致;列表/详情在未提炼笔记前即可显示可读标题
+ - 侧栏「项目」分组名:由 `Workspace Folder` / `Workspace Path` 推导;若路径末级为纯数字或长 hex 等无辨识度目录名,则尝试使用上一级目录名
+ - 助手消息中的工具调用与思考过程统一为 `[Tool: …]`、`…`,便于对话回放与 Markdown 渲染
+- **旧版 CodeBuddy 消息**:兼容 `content` 为**单字符字符串数组**(拼接后与新版等价)、以及旧版字段名 **`Workspace Path:`**(与新版 `Workspace Folder:` 并存,优先匹配 Folder)
+- **修复**:首条用户消息中 `content` 为**整段字符串**(非 text 块数组)时,可正确解析工作区路径,避免侧栏项目名退化为 `history` 下哈希目录名
+
+### 修正
+
+- **CodeBuddy 路径**:此前按 `~/.codebuddy/projects/*.jsonl` 采集与产品实际会话存储不符,已改为扩展数据目录(见上)
+
+### 其他
+
+- 会话列表未分析时优先展示上述采集标题;会话详情顶栏支持 `analysisTitle`;对话回放中工具标签匹配更宽松(含下划线等工具名)
+
+---
+
+## 0.1.1(2026-03-22)
+
+### 新增与优化
+
+- **会话整理**:在对话记录侧栏可进入「会话整理」,在目录树中多选数据源、主机或项目后,一次性清理所选范围内的会话记录;清理的是应用内数据,**不会删除**本机上的原始对话文件;若一次选了多个范围,同一对话只会被删一次
+- **关于**:在设置中可查看应用版本、运行环境与本次更新说明
+- **导出**:笔记可导出为 Markdown,支持单条、多选或整库导出;整库导出不受当前列表筛选影响
+- **会话与笔记**:列表与详情可查看会话路径与会话标识;列表标题优先展示首条用户消息摘要
+- **分析**:分析失败时可看到原因;分析队列中可查看任务详情
+- **阅读**:笔记正文 Markdown 显示效果优化
+- **体验**:搜索与同步等相关提示与文案优化
+
+### 修复与调整
+
+- 侧栏点选「未关联项目」时,列表能正确显示该分组下的会话
+- 批量删除会话仅通过「会话整理」勾选分组后进行,避免误操作
+
+---
+
+## 0.1.0
+
+**发布说明**:首个正式版本,可在本机完成从采集对话、整理列表、提炼笔记到知识库浏览与搜索的完整流程。
+
+### 新增
+
+- **应用**:桌面端应用,支持浅色与深色外观
+- **采集**:支持从 Claude Code、Cursor 等来源导入对话到本地
+- **整理**:对话列表、会话详情(含笔记与对话回放)、按数据源分组的侧栏目录
+- **提炼**:对会话做价值判断并生成结构化笔记,接入本地模型或命令行工具
+- **知识库**:笔记列表、全文搜索、标签与类型筛选
+- **其他**:同步与设置、分析任务队列
+
+### 已知限制
+
+- 部分导出入口仍为占位或能力受限;个别数据源仅可配置名称与图标,尚不能采集
+- 同步需手动触发;与其他工具的深度联动仍在完善中
diff --git "a/docs/CodeBuddy \344\274\232\350\257\235\350\256\260\345\275\225\346\217\220\345\217\226\351\200\273\350\276\221.md" "b/docs/CodeBuddy \344\274\232\350\257\235\350\256\260\345\275\225\346\217\220\345\217\226\351\200\273\350\276\221.md"
new file mode 100644
index 0000000..4bedebf
--- /dev/null
+++ "b/docs/CodeBuddy \344\274\232\350\257\235\350\256\260\345\275\225\346\217\220\345\217\226\351\200\273\350\276\221.md"
@@ -0,0 +1,344 @@
+# CodeBuddy 会话记录提取逻辑
+
+> 本文档详细说明 CodeBuddy IDE 的本地会话记录文件结构,以及如何从中提取**项目名(project_name)**、**工作区路径(workspace_folder)** 等关键信息。
+> 适用于需要在非 Python 技术栈中复用此逻辑的场景。
+
+---
+
+## 1. 存储位置
+
+CodeBuddy 的会话数据存储在以下路径:
+
+| 平台 | 路径 |
+|------|------|
+| **macOS** | `~/Library/Application Support/CodeBuddyExtension/Data/{profile_id}/CodeBuddyIDE/{profile_id}/history/` |
+| **Windows** | `%APPDATA%/CodeBuddyExtension/Data/{profile_id}/CodeBuddyIDE/{profile_id}/history/` |
+| **Linux** | `~/.config/CodeBuddyExtension/Data/{profile_id}/CodeBuddyIDE/{profile_id}/history/` |
+
+其中 `{profile_id}` 是一个 UUID 格式的字符串,如 `363d8e66-2fd1-4b66-9de2-072ff116f842`。
+
+---
+
+## 2. 目录结构
+
+```
+{history_dir}/
+ └── {workspace_id}/ # 32 位 hex hash 字符串
+ ├── index.json # workspace 级索引(含会话列表)
+ └── {conversation_id}/ # 32 位 hex 字符串
+ ├── index.json # 会话级索引(含消息列表)
+ └── messages/
+ └── {msg_id}.json # 单条消息文件
+```
+
+### 各级说明
+
+| 层级 | 说明 | 示例 |
+|------|------|------|
+| `{workspace_id}/` | 一个工作区(项目),hash 标识 | `389bc7591f797f51a238cf478d48407c` |
+| `{conversation_id}/` | 一次对话会话 | `3f161540dfa14ccf948c7f46b53bdc2c` |
+| `messages/{msg_id}.json` | 对话中的单条消息 | `d50d0df94d114d9cbd6eab11f32853f2.json` |
+
+---
+
+## 3. 索引文件格式
+
+### 3.1 Workspace 级 `index.json`
+
+```json
+{
+ "version": 1,
+ "conversations": [
+ {
+ "id": "c44ebb41a3f24265a5484b3116b9a96d",
+ "name": "activity-service-php 项目分析",
+ "createdAt": 1731036781123,
+ "lastMessageAt": 1731036890456
+ }
+ ]
+}
+```
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `conversations[].id` | string | 会话 ID,对应子目录名 |
+| `conversations[].name` | string | 会话标题(由 AI 自动生成) |
+| `conversations[].createdAt` | number | 创建时间戳(毫秒) |
+| `conversations[].lastMessageAt` | number | 最后消息时间戳(毫秒) |
+
+### 3.2 Conversation 级 `index.json`
+
+```json
+{
+ "version": 1,
+ "messages": [
+ {
+ "id": "d50d0df94d114d9cbd6eab11f32853f2",
+ "role": "user",
+ "type": "say",
+ "isComplete": true
+ },
+ {
+ "id": "a1b2c3d4e5f6...",
+ "role": "assistant",
+ "type": "say",
+ "isComplete": true
+ }
+ ]
+}
+```
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `messages[].id` | string | 消息 ID,对应 `messages/` 下的文件名(不含 `.json`) |
+| `messages[].role` | string | `"user"` 或 `"assistant"` |
+| `messages[].type` | string | 消息类型,通常是 `"say"` |
+| `messages[].isComplete` | boolean | 消息是否完整 |
+
+---
+
+## 4. 消息文件格式(两次 JSON 解析)
+
+消息文件 `messages/{msg_id}.json` 需要**两次 JSON 解析**:
+
+### 第一层:文件本身
+
+```json
+{
+ "role": "user",
+ "message": "",
+ "id": "d50d0df94d114d9cbd6eab11f32853f2",
+ "references": [],
+ "extra": {}
+}
+```
+
+> ⚠️ `message` 字段是一个 **JSON 字符串**(string),不是对象!需要二次 `JSON.parse()`。
+
+### 第二层:`message` 字段解析后
+
+```json
+{
+ "role": "user",
+ "content": [...]
+}
+```
+
+> ⚠️ `content` 在新旧版本中格式不同!详见下一节。
+
+---
+
+## 5. Content 字段的两种格式(新版 vs 旧版)
+
+### 5.1 新版格式(约 2025-12 之后)
+
+```json
+{
+ "content": [
+ {
+ "type": "text",
+ "text": "\nOS Version: darwin\nShell: zsh\nWorkspace Folder: /Users/steve/Codes/workspace/ssv/ssv-ai/wenqu\nCurrent date: Sunday, Mar 22, 2026\n\n\n\n你好\n"
+ }
+ ]
+}
+```
+
+- `content` 是**对象数组**,每个元素有 `type` 和 `text` 字段
+- 工作区字段名为 `Workspace Folder`
+
+### 5.2 旧版格式(约 2025-11 及之前)
+
+```json
+{
+ "content": ["<", "u", "s", "e", "r", "_", "i", "n", "f", "o", ">", "\n", ...]
+}
+```
+
+- `content` 是**单字符字符串数组**,每个元素是一个字符
+- 需要 `join("")` 拼接成完整文本
+- 工作区字段名为 `Workspace Path`
+
+### 5.3 如何判断版本
+
+```
+if content[0] 是 string 且 length == 1:
+ → 旧版(单字符数组)
+else if content[0] 是 object 且有 type 字段:
+ → 新版(block 对象数组)
+```
+
+---
+
+## 6. 项目名提取逻辑
+
+### 6.1 核心原理
+
+项目名**不是**存在某个配置或元数据文件里的,而是从**每条会话的第一条 user 消息内容**中,通过正则匹配提取出来的。
+
+CodeBuddy 在每次用户发消息时,会自动在消息中注入一段 `` 标签,其中包含 `Workspace Folder`(或旧版的 `Workspace Path`)。
+
+### 6.2 提取伪代码
+
+```
+function extractWorkspaceFolder(msgFilePath):
+ // ═══ Step 1: 两次 JSON 解析 ═══
+ raw = JSON.parse(readFile(msgFilePath))
+ msgBody = JSON.parse(raw.message || "{}")
+
+ if msgBody.role != "user":
+ return null
+
+ content = msgBody.content
+
+ // ═══ Step 2: 从 content 提取文本(兼容两种格式)═══
+ text = ""
+
+ if content is array AND content.length > 0:
+ firstElement = content[0]
+
+ if typeof firstElement == "string" AND firstElement.length <= 1:
+ // 旧版:单字符数组 → 拼接
+ text = content.join("")
+
+ else if typeof firstElement == "object" AND firstElement.type == "text":
+ // 新版:block 对象数组 → 遍历取 text
+ for block in content:
+ if block.type == "text":
+ text += block.text
+
+ // ═══ Step 3: 正则提取 Workspace 路径(兼容两个字段名)═══
+ match = regex(text, /Workspace Folder:\s*(.+)/)
+ if !match:
+ match = regex(text, /Workspace Path:\s*(.+)/)
+
+ if match:
+ return match.group(1).trim()
+
+ return null
+
+
+function extractProjectName(workspaceFolder):
+ if workspaceFolder:
+ return basename(workspaceFolder) // 取路径最后一级目录名
+ return "CodeBuddy" // 默认值
+```
+
+### 6.3 遍历整个 history 的伪代码
+
+```
+function extractAllSessions(historyDir):
+ results = []
+
+ for workspaceDir in listDirs(historyDir):
+ // 读取 workspace 级 index.json(可选,含会话名称等元数据)
+ wsIndex = JSON.parse(readFile(workspaceDir + "/index.json"))
+ convMeta = {}
+ for c in wsIndex.conversations:
+ convMeta[c.id] = c // { id, name, createdAt, lastMessageAt }
+
+ for convDir in listDirs(workspaceDir):
+ // 跳过 index.json 文件
+ if convDir is not directory:
+ continue
+
+ // 读取 conversation 级 index.json
+ convIndex = JSON.parse(readFile(convDir + "/index.json"))
+ msgList = convIndex.messages
+
+ // 找到第一条 role="user" 的消息,提取 workspace folder
+ workspaceFolder = null
+ for msgMeta in msgList:
+ if msgMeta.role != "user":
+ continue
+ msgFile = convDir + "/messages/" + msgMeta.id + ".json"
+ workspaceFolder = extractWorkspaceFolder(msgFile)
+ if workspaceFolder:
+ break
+
+ projectName = extractProjectName(workspaceFolder)
+ convName = convMeta[convDir.name]?.name || ""
+
+ results.push({
+ workspaceId: workspaceDir.name,
+ conversationId: convDir.name,
+ workspaceFolder: workspaceFolder,
+ projectName: projectName,
+ convName: convName,
+ createdAt: convMeta[convDir.name]?.createdAt,
+ lastMessageAt: convMeta[convDir.name]?.lastMessageAt
+ })
+
+ return results
+```
+
+---
+
+## 7. 新旧版本差异速查表
+
+| 项目 | 旧版(~2025-11 及之前) | 新版(~2025-12 之后) |
+|------|------------------------|---------------------|
+| `content` 格式 | `["<","u","s","e","r",...]` 单字符数组 | `[{"type":"text","text":"..."}]` 对象数组 |
+| `content` 处理方式 | `content.join("")` 拼接 | 遍历取 `block.text` |
+| 工作区字段名 | `Workspace Path:` | `Workspace Folder:` |
+| 日期格式 | `2025-11-07` | `Sunday, Mar 22, 2026` |
+
+---
+
+## 8. 实际数据示例
+
+以本机实际数据为例,提取多个 workspace 的项目名:
+
+| workspace_id(前12位) | 消息中的 Workspace 路径 | 提取出的项目名 |
+|------------------------|------------------------|---------------|
+| `1694a20c0947` | `/Users/steve/资料/学习资料/KM 文章/知识库` | **知识库** |
+| `1f2b3bada274` | `/Users/steve/资料/学习资料/AI/AI编程资料包` | **AI编程资料包** |
+| `337f7f20cfdc` | `/Users/steve/CodeBuddy/AutoPublishStory` | **AutoPublishStory** |
+| `389bc7591f79` | `/Users/steve/Codes/workspace/ssv/ssv-ai/wenqu` | **wenqu** |
+| `4879786f00cd` | `/Users/steve/杂七杂八/tech4good_docs` | **tech4good_docs** |
+| `538ffc8b3206` | `/Users/steve/Codes/workspace/ssv/ssv-ai/统计/ssv_docs` | **ssv_docs** |
+
+---
+
+## 9. 注意事项
+
+1. **两次 JSON 解析**:消息文件的 `message` 字段本身是 JSON 字符串,必须解析两次。
+2. **只取第一条 user 消息**:`Workspace Folder` 通常在第一条 user 消息中注入,找到后即停止搜索。
+3. **部分会话可能没有 Workspace Folder**:例如没有打开项目目录时的会话,此时 fallback 为 `"CodeBuddy"`。
+4. **字符编码**:路径可能包含中文等 Unicode 字符,注意编码处理。
+5. **跨平台路径**:macOS/Linux 用 `/`,Windows 用 `\`,提取 basename 时需适配。
+
+---
+
+## 10. 数据流总结
+
+```
+消息文件 messages/{id}.json
+ │
+ │ 第一次 JSON.parse → raw 对象
+ │
+ ▼
+raw.message(JSON 字符串)
+ │
+ │ 第二次 JSON.parse → msgBody 对象
+ │
+ ▼
+msgBody.content(数组)
+ │
+ │ 判断 content[0] 的类型
+ │ ├── string(len=1) → 旧版,join("") 拼接
+ │ └── object → 新版,取 block.text
+ │
+ ▼
+完整文本
+ │
+ │ 正则匹配:Workspace Folder: (.+)
+ │ 或:Workspace Path: (.+)
+ │
+ ▼
+workspaceFolder = "/Users/steve/.../wenqu"
+ │
+ │ basename()
+ │
+ ▼
+projectName = "wenqu"
+```
diff --git "a/docs/PRD/\345\257\273\350\277\271\357\274\210XunJi\357\274\211\344\272\247\345\223\201\351\234\200\346\261\202\346\226\207\346\241\243 PRD.md" "b/docs/PRD/\345\257\273\350\277\271\357\274\210XunJi\357\274\211\344\272\247\345\223\201\351\234\200\346\261\202\346\226\207\346\241\243 PRD.md"
index 7b32161..2596926 100644
--- "a/docs/PRD/\345\257\273\350\277\271\357\274\210XunJi\357\274\211\344\272\247\345\223\201\351\234\200\346\261\202\346\226\207\346\241\243 PRD.md"
+++ "b/docs/PRD/\345\257\273\350\277\271\357\274\210XunJi\357\274\211\344\272\247\345\223\201\351\234\200\346\261\202\346\226\207\346\241\243 PRD.md"
@@ -59,8 +59,8 @@
|--------|--------|---------|------|---------|
| P0 | **Claude Code(内网版)** | `~/.claude-internal/projects/{路径hash}/{session-uuid}.jsonl`
全局历史:`~/.claude-internal/history.jsonl`
子代理:`sessions/{id}/subagents/agent-*.jsonl` | JSONL | ✅ 低 |
| P0 | **Cursor** | 全局:`~/Library/Application Support/Cursor/User/globalStorage/state.vscdb`(~208MB)
工作区:`workspaceStorage//state.vscdb`(约 196 个)
Plan 文件:`~/.cursor/plans/*.plan.md` | SQLite + Lexical 富文本 | ⚠️ 高 |
-| P1 | **CodeBuddy CLI** | `~/.codebuddy/projects/{项目名}/{session-uuid}.jsonl` | JSONL | ✅ 低 |
-| P1 | **CodeBuddy JetBrains 插件** | 完整会话:`~/Library/Application Support/CodeBuddyExtension/Data/{USER_ID}/CodeBuddyIDE/{USER_ID}/history/`
索引库:`~/Library/Application Support/com.tencent.copilot/copilot_index.db`
问题历史:`~/Library/Application Support/JetBrains/{IDE版本}/options/tencent-copilot-question-history.xml` | JSON + SQLite + XML | ✅ 中等 |
+| P1 | **CodeBuddy(扩展 / IDE,与问渠扫描一致)** | 根目录:macOS `~/Library/Application Support/CodeBuddyExtension/Data/`,Linux `~/.local/share/CodeBuddyExtension/`(见问渠 `scanner/codebuddy.py`)。会话落盘在目录树中名为 `history` 的路径之下:`.../history/{workspace_id}/{conversation_id}/`,含 `index.json`(消息索引)与 `messages/*.json`(单条消息)。**不作为**主会话来源:`~/.codebuddy/projects/*.jsonl`(CLI JSONL,与扩展存储不同) | JSON | ✅ 中等 |
+| P1 | **CodeBuddy 补充数据** | 索引库:`~/Library/Application Support/com.tencent.copilot/copilot_index.db`
问题历史:`~/Library/Application Support/JetBrains/{IDE版本}/options/tencent-copilot-question-history.xml` | SQLite + XML | ✅ 中等 |
| P2 | **Windsurf** | `~/Library/Application Support/Windsurf/User/workspaceStorage/` | SQLite | 中 |
| P2 | **GitHub Copilot** | VSCode workspaceStorage | SQLite | 中 |
@@ -94,7 +94,9 @@
- Plan 文件结构清晰:YAML frontmatter 含 `name`、`overview`、`todos`(含 status/dependencies)
- ❌ 无 token 统计,时间戳为毫秒时间戳
-**CodeBuddy CLI**
+**CodeBuddy CLI(独立 JSONL 流,与扩展会话目录不同)**
+
+> 寻迹当前版本对 CodeBuddy 的采集实现为 **CodeBuddyExtension** 根目录下的会话目录(`index.json` + `messages/`,见上表);下表为 CLI 侧 JSONL 行格式,供对照,**非**当前采集主路径。
```json
{
diff --git "a/docs/plans/2026-03-20-\345\257\273\350\277\271\346\225\264\344\275\223\346\236\266\346\236\204\350\256\276\350\256\241.md" "b/docs/plans/2026-03-20-\345\257\273\350\277\271\346\225\264\344\275\223\346\236\266\346\236\204\350\256\276\350\256\241.md"
index b7d8bdd..2c9db12 100644
--- "a/docs/plans/2026-03-20-\345\257\273\350\277\271\346\225\264\344\275\223\346\236\266\346\236\204\350\256\276\350\256\241.md"
+++ "b/docs/plans/2026-03-20-\345\257\273\350\277\271\346\225\264\344\275\223\346\236\266\346\236\204\350\256\276\350\256\241.md"
@@ -103,7 +103,7 @@ xunji/
│ │ │ │ ├── mod.rs
│ │ │ │ ├── claude_code.rs
│ │ │ │ ├── cursor.rs
-│ │ │ │ ├── codebuddy_cli.rs
+│ │ │ │ ├── codebuddy.rs
│ │ │ │ ├── codebuddy_jetbrains.rs
│ │ │ │ ├── watcher.rs
│ │ │ │ └── normalizer.rs
diff --git a/docs/plans/2026-03-22-v0.1.2-tasks.md b/docs/plans/2026-03-22-v0.1.2-tasks.md
new file mode 100644
index 0000000..55614c4
--- /dev/null
+++ b/docs/plans/2026-03-22-v0.1.2-tasks.md
@@ -0,0 +1,28 @@
+# 寻迹 v0.1.2 任务跟踪
+
+> **关联**:在 v0.1.1 基础上增量发布;核心为 **CodeBuddy CLI 会话采集**。
+> **PRD**:[`docs/PRD/寻迹(XunJi)产品需求文档 PRD.md`](../PRD/寻迹(XunJi)产品需求文档 PRD.md)(CodeBuddy CLI JSONL)
+
+---
+
+## 任务
+
+| ID | 任务 | 状态 | 说明 |
+|----|------|------|------|
+| **T01** | CodeBuddy:问渠路径(CodeBuddyExtension → 会话 `index.json` + `messages/`)解析,接入 `scheduler` | 已完成 | `codebuddy.rs`、`scheduler.rs` |
+| **T02** | 默认配置 + 升级合并 `codebuddy-cli`(默认关闭采集) | 已完成 | `config.rs` |
+| **T03** | 设置页:已接入采集器说明含 CodeBuddy CLI | 已完成 | `SettingsModal.vue` |
+| **T04** | 版本号 0.1.2、CHANGELOG | 已完成 | 各 `package.json`、`Cargo.toml`、`tauri.conf.json` |
+
+## 变更记录
+
+| 日期 | 说明 |
+|------|------|
+| 2026-03-22 | 初版:CLI 采集与配置合并 |
+
+---
+
+## 与 v0.2.0 的关系
+
+- **CodeBuddy CLI** 已从 v0.2.0 计划中的「数据采集」提前在本版本交付。
+- **CodeBuddy JetBrains**(多路径 / SQLite / XML)仍归后续版本(v0.2.x 或独立小版本)。
diff --git "a/docs/\346\225\260\346\215\256\345\272\223/\345\257\273\350\277\271 SQLite \346\225\260\346\215\256\346\250\241\345\236\213.md" "b/docs/\346\225\260\346\215\256\345\272\223/\345\257\273\350\277\271 SQLite \346\225\260\346\215\256\346\250\241\345\236\213.md"
new file mode 100644
index 0000000..e4b3e2b
--- /dev/null
+++ "b/docs/\346\225\260\346\215\256\345\272\223/\345\257\273\350\277\271 SQLite \346\225\260\346\215\256\346\250\241\345\236\213.md"
@@ -0,0 +1,245 @@
+# 寻迹(XunJi)SQLite 数据模型与表关系
+
+本文档描述桌面端(Tauri)本地库当前 Schema,依据 `apps/desktop/src-tauri/src/storage/migrations.rs` 中的迁移逻辑整理,并用 Mermaid 辅助说明实体关系。
+
+## 1. 概述
+
+| 项目 | 说明 |
+|------|------|
+| 引擎 | SQLite,连接时开启 `PRAGMA foreign_keys=ON`、`journal_mode=WAL` |
+| 默认路径 | `~/.xunji/db/xunji.db`(见应用配置) |
+| 版本 | `PRAGMA user_version`,当前迁移最高为 **v6** |
+| 权威来源 | `storage/migrations.rs`(DDL)+ `storage/models.rs`(Rust 结构体与业务语义) |
+
+**外键策略**:仅在 `messages`、`cards`、`card_tags`、`categories`(自关联)、`token_usage` 上声明 SQLite `FOREIGN KEY`。`sessions.source_id`、`sync_log.source_id`、`cards.category_id` 等在库中为**逻辑关联**,应用层负责一致性,便于数据源与分类的灵活演进。
+
+---
+
+## 2. 实体关系图(ER)
+
+下列图中标注「逻辑」的连线在数据库中**没有** `FOREIGN KEY` 约束。
+
+**图例**:字段类型在图中使用 Mermaid 支持的写法(`string` / `int` / `float`),与 SQLite 的 `TEXT` / `INTEGER` / `REAL` 一一对应;字段行尾双引号内为「中文说明 + 英文字段名」。
+**说明**:部分预览器对 `erDiagram` 的 `实体["别名"] { ... }` 或 SQL 风格类型名解析不兼容,会导致实体框只显示 “(no attributes)”。下图使用**纯表名 + 标准类型**,以保证属性能渲染;中文表名见下表。
+
+| 表名 `identifier` | 中文含义 |
+|---------------------|----------|
+| `sources` | 数据源 |
+| `sessions` | 会话 |
+| `messages` | 对话消息 |
+| `cards` | 知识卡片 |
+| `tags` | 标签 |
+| `card_tags` | 卡片与标签关联(多对多) |
+| `categories` | 分类 |
+| `sync_log` | 同步日志 |
+| `token_usage` | Token 用量 / 计费流水 |
+
+```mermaid
+erDiagram
+ sources {
+ string id PK "主键 id"
+ string name "显示名称 name"
+ int enabled "是否启用 enabled"
+ string scan_paths "扫描目录列表 JSON scan_paths"
+ string last_sync "上次同步时间 last_sync"
+ string config "扩展配置 JSON config"
+ }
+
+ sessions {
+ string id PK "主键 id(应用内 UUID)"
+ string source_id "数据源 id source_id, 逻辑关联 sources.id"
+ string session_id "数据源侧会话标识 session_id"
+ string source_host "来源主机 source_host, 去重维度之一"
+ string project_path "项目路径 project_path"
+ string project_name "项目名称 project_name"
+ int message_count "消息条数 message_count"
+ string content_hash "内容摘要哈希 content_hash"
+ string raw_path "原始文件路径 raw_path"
+ string created_at "创建时间 created_at"
+ string updated_at "更新时间 updated_at"
+ string status "分析状态 status"
+ string value "价值等级 value"
+ int has_updates "是否有新消息 has_updates"
+ string analyzed_at "分析完成时间 analyzed_at"
+ string error_message "分析错误信息 error_message"
+ string analysis_note "低价值说明 analysis_note (v3)"
+ string analysis_title "列表展示标题 analysis_title (v4)"
+ string analysis_type "分析类型徽章 analysis_type (v4)"
+ }
+
+ messages {
+ string id PK "主键 id"
+ string session_id "所属会话 id session_id, FK sessions.id"
+ string role "角色 role"
+ string content "正文 content"
+ string timestamp "时间戳 timestamp"
+ int tokens_in "输入 token tokens_in"
+ int tokens_out "输出 token tokens_out"
+ int seq_order "会话内顺序 seq_order"
+ }
+
+ cards {
+ string id PK "主键 id"
+ string session_id "来源会话 id session_id, FK sessions.id"
+ string title "标题 title"
+ string type "知识类型 type"
+ string value "价值等级 value"
+ string summary "一句话摘要 summary"
+ string note "笔记正文 Markdown note"
+ string category_id "分类 id category_id, 逻辑 categories.id"
+ string memory "Memory 规则 memory"
+ string skill "Skill 定义 skill"
+ string source_name "数据源名称冗余 source_name"
+ string project_name "项目名称冗余 project_name"
+ int prompt_tokens "提炼提示 token prompt_tokens"
+ int completion_tokens "提炼补全 token completion_tokens"
+ float cost_yuan "费用(元) cost_yuan"
+ string feedback "用户反馈 feedback"
+ string created_at "创建时间 created_at"
+ string updated_at "更新时间 updated_at"
+ string tech_stack "技术栈逗号串 tech_stack (v5)"
+ }
+
+ tags {
+ string id PK "主键 id"
+ string name "标签名 name, 唯一"
+ string type "标签来源 type"
+ int count "引用计数 count"
+ }
+
+ card_tags {
+ string card_id "卡片 id card_id, FK cards.id"
+ string tag_id "标签 id tag_id, FK tags.id"
+ }
+
+ categories {
+ string id PK "主键 id"
+ string name "分类名称 name"
+ string parent_id "父分类 id parent_id, FK categories.id"
+ int sort_order "排序 sort_order"
+ }
+
+ sync_log {
+ string id PK "主键 id"
+ string source_id "数据源 id source_id, 逻辑 sources.id"
+ string started_at "开始时间 started_at"
+ string finished_at "结束时间 finished_at"
+ int sessions_found "扫描到会话数 sessions_found"
+ int sessions_new "新导入会话数 sessions_new"
+ int sessions_updated "检测到更新数 sessions_updated"
+ string status "任务状态 status"
+ }
+
+ token_usage {
+ string id PK "主键 id"
+ string card_id "关联卡片 id card_id, 可空, FK cards.id"
+ string provider "模型提供商 provider"
+ string model "模型名 model"
+ int prompt_tokens "提示 token prompt_tokens"
+ int completion_tokens "补全 token completion_tokens"
+ float cost_yuan "费用(元) cost_yuan"
+ string created_at "记录时间 created_at"
+ }
+
+ sources ||--o{ sessions : "按数据源归类(逻辑关联 source_id)"
+ sources ||--o{ sync_log : "同步任务归属(逻辑关联 source_id)"
+ sessions ||--o{ messages : "会话包含多条消息(一对多)"
+ sessions ||--o{ cards : "会话可有多张卡片(一对多)"
+ cards ||--o{ card_tags : "卡片到关联表"
+ card_tags }o--|| tags : "关联表到标签"
+ categories ||--o| categories : "父子分类(自关联 parent_id)"
+ cards }o--o| categories : "卡片归属分类(逻辑 category_id)"
+ cards ||--o{ token_usage : "提炼计费流水(card_id 可空)"
+```
+
+**字段名说明**:`tags` 表中业务列在 SQLite 里名为 `type`;Mermaid 属性名若使用 `type` 易与语法关键字冲突,图中写作 `tag_type`,并在注释中标明「列名 type」。
+
+---
+
+## 3. 关系说明(业务视角)
+
+```text
+sources(数据源)
+ │ source_id(逻辑)
+ ├── sessions(会话,去重键:session_id + source_host UNIQUE)
+ │ ├── messages(对话消息)
+ │ └── cards(知识卡片)
+ │ ├── card_tags ── tags(标签,多对多)
+ │ └── token_usage(提炼费用,可关联卡片)
+ └── sync_log(同步任务日志)
+
+categories(分类树,自引用 parent_id)
+
+cards_fts(FTS5 虚拟表,见下节,非业务实体表)
+```
+
+- **会话去重**:同一数据源侧 `session_id` 在不同 `source_host` 下可共存;组合 `(session_id, source_host)` 唯一。
+- **卡片与标签**:标签名在 `tags.name` 上唯一;`card_tags` 为联合主键 `(card_id, tag_id)`。
+- **全文搜索**:`cards_fts` 为 FTS5 独立索引表,字段 `title, summary, note, tags`;`tags` 列内容由 `card_tags` + `tags` 拼接后写入,与 `cards` 表无 `content=` 回链(见迁移 v2 说明)。
+
+---
+
+## 4. 各表字段概要
+
+### 4.1 `sources`
+
+AI IDE 数据源配置(名称、启用、扫描路径 JSON、上次同步时间、扩展配置 JSON)。
+
+### 4.2 `sessions`
+
+一条记录对应一次采集到的 AI 对话(如一个 JSONL 文件)。含分析流水线字段:`status`、`value`、`analyzed_at`、`error_message`;v3/v4 起增加低价值会话展示用 `analysis_note`、`analysis_title`、`analysis_type`。
+
+### 4.3 `messages`
+
+会话内消息,`seq_order` 为会话内顺序;`role` 如 user / assistant / tool 等。
+
+### 4.4 `cards`
+
+由 LLM 从会话提炼的笔记;`type` / `value` 等为业务枚举字符串;`tech_stack`(v5)为逗号分隔技术栈字符串;展示用标签来自关联查询,非 `cards` 表列。
+
+### 4.5 `tags` / `card_tags`
+
+标签字典与卡片—标签多对多桥表。
+
+### 4.6 `categories`
+
+分类树,`parent_id` 指向同表(可为空表示根)。
+
+### 4.7 `sync_log`
+
+某次同步任务的统计与状态,按 `source_id` 与数据源对应(逻辑关联)。
+
+### 4.8 `token_usage`
+
+LLM 调用计费流水;`card_id` 可空,声明外键指向 `cards(id)`。
+
+### 4.9 `cards_fts`(虚拟表)
+
+FTS5,用于卡片侧栏/列表等全文检索;应用层在卡片写入、更新、删除及标签变更时维护与 `cards` 一致。
+
+---
+
+## 5. 迁移版本与 Schema 增量
+
+| 版本 | 内容摘要 |
+|------|----------|
+| v1 | 创建全部业务表、`cards_fts`、索引 |
+| v2 | 重建 `cards_fts` 为独立 FTS5,并从现有 `cards` + 标签关联回填 |
+| v3 | `sessions.analysis_note` |
+| v4 | `sessions.analysis_title`、`sessions.analysis_type` |
+| v5 | `cards.tech_stack` |
+| v6 | 数据修复:解码 `sessions` 中仍含 URL 编码的 `project_path` / `project_name` |
+
+执行顺序见 `run()`:按 `user_version` 递增直至当前最高版本。
+
+---
+
+## 6. 代码索引
+
+| 模块 | 路径 |
+|------|------|
+| 迁移 DDL | `apps/desktop/src-tauri/src/storage/migrations.rs` |
+| 模型与注释 | `apps/desktop/src-tauri/src/storage/models.rs` |
+| 连接与错误类型 | `apps/desktop/src-tauri/src/storage/db.rs` |
+
+文档随迁移变更时请同步更新本文与 `CHANGELOG`(若项目约定记录 Schema 变更)。
diff --git "a/docs/\350\270\251\345\235\221/\346\241\214\351\235\242\345\272\224\347\224\250-sessions-\351\207\215\345\220\214\346\255\245\350\246\206\347\233\226analysis_title.md" "b/docs/\350\270\251\345\235\221/\346\241\214\351\235\242\345\272\224\347\224\250-sessions-\351\207\215\345\220\214\346\255\245\350\246\206\347\233\226analysis_title.md"
new file mode 100644
index 0000000..9211f73
--- /dev/null
+++ "b/docs/\350\270\251\345\235\221/\346\241\214\351\235\242\345\272\224\347\224\250-sessions-\351\207\215\345\220\214\346\255\245\350\246\206\347\233\226analysis_title.md"
@@ -0,0 +1,38 @@
+# 踩坑:增量重同步把 `sessions.analysis_title` 写成 NULL
+
+## 问题/背景
+
+会话经「低/无价值」分析后,由 `update_session_analysis_meta` 写入 `analysis_title` 等字段;随后若该会话在 Claude Code / Cursor 侧有新消息并触发采集重同步,列表里标题会消失。
+
+## 现象
+
+- 分析完成后列表有展示标题,同步一次后标题变空或恢复为默认逻辑。
+- 数据库中该行的 `analysis_title` 变为 `NULL`。
+
+## 根因
+
+```text
+dedup_and_write → message_count 增加
+ → update_session_resync_metadata(..., analysis_title = session.analysis_title)
+ NormalizedSession.analysis_title 对 claude-code / cursor 恒为 None
+ → SQL: UPDATE ... SET analysis_title = ?4 绑定 NULL
+ → 覆盖掉此前 analysis 步骤写入的标题
+```
+
+原先仅调用 `update_session_message_count` 时不会碰 `analysis_title`,引入「重同步元数据」后未区分「采集未提供」与「显式清空」。
+
+## 解决方法
+
+在 `update_session_resync_metadata` 的 `UPDATE` 中使用:
+
+`analysis_title = COALESCE(?4, analysis_title)`
+
+- 采集端传入非 NULL(如 CodeBuddy 工作区 index 中的名称)时仍更新;
+- 传入 NULL 时保留库内已有 `analysis_title`。
+
+实现位置:`apps/desktop/src-tauri/src/storage/sessions.rs` 中 `update_session_resync_metadata`。
+
+## 后续如何避免
+
+- 凡「可选字段」若语义为「未提供则不改」,在 SQL 层用 `COALESCE(新值, 列)` 或拆成两条 `UPDATE`,避免把 `NULL` 绑进无条件 `SET col = ?`。
+- 回归:`cargo test resync_metadata`(`storage::sessions` 内单元测试);或手工先跑分析写入标题,再触发该会话增量同步,确认标题仍在。
diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json
index a2f12d2..8947fb1 100644
--- a/packages/mcp-server/package.json
+++ b/packages/mcp-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@xunji/mcp-server",
- "version": "0.1.1",
+ "version": "0.1.3",
"private": true,
"type": "module",
"scripts": {
diff --git a/packages/shared/package.json b/packages/shared/package.json
index 02079ff..bad2103 100644
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -1,6 +1,6 @@
{
"name": "@xunji/shared",
- "version": "0.1.1",
+ "version": "0.1.3",
"private": true,
"type": "module",
"main": "./src/index.ts",
diff --git a/packages/sidecar/package.json b/packages/sidecar/package.json
index f989105..a5eaded 100644
--- a/packages/sidecar/package.json
+++ b/packages/sidecar/package.json
@@ -1,6 +1,6 @@
{
"name": "@xunji/sidecar",
- "version": "0.1.1",
+ "version": "0.1.3",
"private": true,
"type": "module",
"scripts": {