Skip to content
Merged
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
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,36 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).

## [0.7.3] - 2026-07-01

### Added
- 工具结果外置到磁盘,配合 `tool.recall` 按需回取原始内容,降低长对话上下文占用
- `thought.read` 工具,读取想法正文与元数据,并带隐私过滤
- web_research skill 增强为方案调研(solution research)能力
- Agent loop 提取并固定任务上下文为 system message,循环告警时重锚定到已固定目标
- 单条工具结果截断阈值 + 模型上下文窗口自动推断
- 流式工具结果前置摘要,并增强 SSE 流健壮性

### Changed
- RESEARCH 提示词改为保存完整调研结果(含来源与分析),而非精简摘要;并优先使用 skill 而非裸工具
- 上下文摘要触发更早,改为累积合并
- ContextGuard 对工具结果降级而非直接删除;以 `summarized_up_to` 作为预摘要缓存键
- 提高 web_research 超时以匹配更高的工具预算
- 澄清 `note.create` 与 `thought.create` 的工具描述差异

### Fixed
- 聊天窗口仅在吸附底部时自动滚动,向上翻阅历史时不再被流式刷新打断
- Agent loop 采用取消感知的指数退避重试
- 每个工具独立超时,避免 skill 子轮次被提前终止
- 在 OpenAI API 边界映射工具名,规避函数名不允许点号的限制

### Security
- 阻止 `note.read` 通过软链接逃逸出工作区
- 链接推荐与主题网络图谱均过滤 kf-private 私有笔记

### Removed
- 清理死代码:provider 层的 `is_remote` 与 `create_provider_by_id`

## [0.6.0] - 2026-05-25

### Added
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "knowforge",
"private": true,
"version": "0.7.2",
"version": "0.7.3",
"license": "Apache-2.0",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "knowforge"
version = "0.7.2"
version = "0.7.3"
description = "Knowforge desktop app"
authors = ["caichangqing"]
edition = "2024"
Expand Down
57 changes: 55 additions & 2 deletions src-tauri/src/link_recommendation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,9 +406,10 @@ fn read_note_markdown_for_overlap(vault_root: &Path, rel_key: &str, chunks: &[Do
if let Ok(abs) = crate::join_under_root(vault_root, &norm) {
if abs.is_file() {
if let Ok(s) = fs::read_to_string(&abs) {
if !note_privacy::markdown_treat_as_kf_private(&s) {
return s;
if note_privacy::markdown_treat_as_kf_private(&s) {
return String::new();
}
return s;
}
}
}
Expand Down Expand Up @@ -507,6 +508,10 @@ pub fn suggest_related_notes(

let mut out: Vec<LinkRecommendation> = Vec::new();
for (target_rel_path, sim) in scored.into_iter().take(max_results) {
let target_abs = vault_root.join(&target_rel_path);
if note_privacy::peek_kf_private_from_md_file(&target_abs) {
continue;
}
let target_md = read_note_markdown_for_overlap(vault_root, &target_rel_path, &all_chunks);
let target_plain = prepare_plain_for_overlap(&target_md);
let target_tf = term_frequencies(&target_plain);
Expand Down Expand Up @@ -946,4 +951,52 @@ mod tests {
.unwrap();
assert!(c[0].reason.is_none());
}

#[test]
fn suggest_related_filters_private_candidate() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
fs::write(root.join("cur.md"), "x\n").unwrap();
fs::write(
root.join("private.md"),
"---\nkf-private: true\n---\nsecret\n",
)
.unwrap();
fs::write(root.join("public.md"), "p\n").unwrap();

let conn = semantic_index::open_embedding_db(root).expect("open embedding db");
let hi = vec![1.0_f32, 0.0, 0.0];
let near = vec![0.95_f32, 0.05, 0.0];
semantic_index::upsert_doc_chunk(&conn, "cur.md#0", "cur.md", 0, "t", &hi, "m").unwrap();
semantic_index::upsert_doc_chunk(&conn, "private.md#0", "private.md", 0, "t", &near, "m")
.unwrap();
semantic_index::upsert_doc_chunk(&conn, "public.md#0", "public.md", 0, "t", &near, "m")
.unwrap();

let tconn = Connection::open_in_memory().unwrap();
let ec = semantic_index::EmbeddingCache::new();
let rec = suggest_related_notes(root, "cur.md", &conn, &tconn, 5, None, &ec).unwrap();
assert!(
rec.iter().all(|r| r.target_rel_path != "private.md"),
"private candidate must not appear: {rec:?}"
);
assert!(rec.iter().any(|r| r.target_rel_path == "public.md"));
}

#[test]
fn read_note_markdown_for_overlap_returns_empty_for_private() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
fs::write(
root.join("secret.md"),
"---\nkf-private: true\n---\nContent that must not leak\n",
)
.unwrap();

let result = super::read_note_markdown_for_overlap(root, "secret.md", &[]);
assert!(
result.is_empty(),
"private note must return empty string, got: {result}"
);
}
}
Loading