mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-19 01:43:17 +08:00
兼容 memory-wiki 输入路径并强调相对路径
- memory-wiki 的 wiki_root 路径解析兼容:相对路径 / data/wiki 前缀 / wiki_root 下绝对路径 - 系统提示词强调:模型传相对 wiki 根路径(相对 data/wiki),避免直接给 data/wiki/... 或绝对路径 - 修复并回归:memory-wiki 工具动态加载模块 import-time 错误(sys.modules 注入) - chat.js 增强会话滚动跟随(异步渲染补滚) Made-with: Cursor
This commit is contained in:
parent
27b8698f9b
commit
25180921e7
4 changed files with 100 additions and 11 deletions
|
|
@ -2357,16 +2357,30 @@ async function renderChatUi() {
|
||||||
bindChatMermaidViewer(messagesEl);
|
bindChatMermaidViewer(messagesEl);
|
||||||
const AUTO_SCROLL_BOTTOM_GAP_PX = 56;
|
const AUTO_SCROLL_BOTTOM_GAP_PX = 56;
|
||||||
let shouldFollowMessages = true;
|
let shouldFollowMessages = true;
|
||||||
|
let autoScrollRaf = 0;
|
||||||
|
let autoScrollTimer = 0;
|
||||||
const isNearBottom = () => {
|
const isNearBottom = () => {
|
||||||
const remaining = messagesEl.scrollHeight - (messagesEl.scrollTop + messagesEl.clientHeight);
|
const remaining = messagesEl.scrollHeight - (messagesEl.scrollTop + messagesEl.clientHeight);
|
||||||
return remaining <= AUTO_SCROLL_BOTTOM_GAP_PX;
|
return remaining <= AUTO_SCROLL_BOTTOM_GAP_PX;
|
||||||
};
|
};
|
||||||
const scrollMessagesToBottom = (force = false) => {
|
const scheduleFollowScroll = (force = false) => {
|
||||||
if (!force && !shouldFollowMessages) return;
|
if (!force && !shouldFollowMessages) return;
|
||||||
messagesEl.scrollTop = messagesEl.scrollHeight;
|
if (autoScrollRaf) cancelAnimationFrame(autoScrollRaf);
|
||||||
requestAnimationFrame(() => {
|
if (autoScrollTimer) clearTimeout(autoScrollTimer);
|
||||||
if (force || shouldFollowMessages) messagesEl.scrollTop = messagesEl.scrollHeight;
|
autoScrollRaf = requestAnimationFrame(() => {
|
||||||
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||||
|
autoScrollRaf = requestAnimationFrame(() => {
|
||||||
|
if (force || shouldFollowMessages) messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
// Some async markdown/image/UI post-processing happens after RAF.
|
||||||
|
autoScrollTimer = setTimeout(() => {
|
||||||
|
if (force || shouldFollowMessages) messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||||
|
autoScrollTimer = 0;
|
||||||
|
}, 60);
|
||||||
|
};
|
||||||
|
const scrollMessagesToBottom = (force = false) => {
|
||||||
|
scheduleFollowScroll(force);
|
||||||
};
|
};
|
||||||
messagesEl.addEventListener("scroll", () => {
|
messagesEl.addEventListener("scroll", () => {
|
||||||
shouldFollowMessages = isNearBottom();
|
shouldFollowMessages = isNearBottom();
|
||||||
|
|
@ -2380,6 +2394,14 @@ async function renderChatUi() {
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
const messagesMutationObserver = new MutationObserver(() => {
|
||||||
|
scheduleFollowScroll(false);
|
||||||
|
});
|
||||||
|
messagesMutationObserver.observe(messagesEl, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
characterData: true,
|
||||||
|
});
|
||||||
const textarea = el("textarea", {
|
const textarea = el("textarea", {
|
||||||
class: "chat-composer__field",
|
class: "chat-composer__field",
|
||||||
rows: "1",
|
rows: "1",
|
||||||
|
|
|
||||||
|
|
@ -59,16 +59,61 @@ def _resolve_runtime(api: Any) -> WikiRuntime:
|
||||||
|
|
||||||
|
|
||||||
def _safe_path(rt: WikiRuntime, rel_path: str) -> Path:
|
def _safe_path(rt: WikiRuntime, rel_path: str) -> Path:
|
||||||
rp = str(rel_path or "").strip().replace("\\", "/")
|
raw = str(rel_path or "").strip()
|
||||||
rp = rp.lstrip("./")
|
raw = raw.replace("\\", "/")
|
||||||
if not rp:
|
raw = raw.lstrip("./")
|
||||||
|
if not raw:
|
||||||
raise ValueError("path_required")
|
raise ValueError("path_required")
|
||||||
p = (rt.wiki_root / rp).resolve()
|
|
||||||
root = rt.wiki_root.resolve()
|
root = rt.wiki_root.resolve()
|
||||||
if p != root and root not in p.parents:
|
|
||||||
raise ValueError("path_outside_wiki_root")
|
# 1) Absolute path: accept if it's under wiki_root.
|
||||||
|
try:
|
||||||
|
cand = Path(raw)
|
||||||
|
if cand.is_absolute():
|
||||||
|
p = cand.resolve()
|
||||||
|
if p.suffix.lower() != ".md":
|
||||||
|
raise ValueError("only_markdown_supported")
|
||||||
|
if p != root and root not in p.parents:
|
||||||
|
raise ValueError("path_outside_wiki_root")
|
||||||
|
return p
|
||||||
|
except Exception:
|
||||||
|
# fall through to relative/normalized handling
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 2) Relative path: normalize common "data/wiki/..." prefixes to wiki_root-relative paths.
|
||||||
|
rp = raw.lstrip("/")
|
||||||
|
|
||||||
|
prefixes: list[str] = []
|
||||||
|
# Legacy/default prefix.
|
||||||
|
prefixes.append("data/wiki/")
|
||||||
|
|
||||||
|
# If wiki_root is configured under project root, allow stripping its relative prefix too.
|
||||||
|
try:
|
||||||
|
root_rel = rt.wiki_root.resolve().relative_to(_project_root().resolve()).as_posix()
|
||||||
|
if root_rel:
|
||||||
|
prefixes.append(str(root_rel).lstrip("/") + "/")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Allow stripping the wiki_root folder name (e.g., "wiki/xxx.md") if user passes it.
|
||||||
|
prefixes.append(f"{root.name}/")
|
||||||
|
|
||||||
|
rp_lower = rp.lower()
|
||||||
|
stripped = rp
|
||||||
|
for pref in prefixes:
|
||||||
|
pref_l = str(pref or "").strip().lower()
|
||||||
|
if not pref_l:
|
||||||
|
continue
|
||||||
|
if rp_lower.startswith(pref_l):
|
||||||
|
stripped = rp[len(pref) :]
|
||||||
|
break
|
||||||
|
|
||||||
|
p = (rt.wiki_root / stripped).resolve()
|
||||||
if p.suffix.lower() != ".md":
|
if p.suffix.lower() != ".md":
|
||||||
raise ValueError("only_markdown_supported")
|
raise ValueError("only_markdown_supported")
|
||||||
|
if p != root and root not in p.parents:
|
||||||
|
raise ValueError("path_outside_wiki_root")
|
||||||
return p
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ def _unified_skill_policy_guidance() -> str:
|
||||||
"- 不要为了“列出技能”而去读取 SKILL.md。只有在你确实需要某个技能的详细使用说明时,才读取对应 SKILL.md。\n"
|
"- 不要为了“列出技能”而去读取 SKILL.md。只有在你确实需要某个技能的详细使用说明时,才读取对应 SKILL.md。\n"
|
||||||
"- 当你需要技能细节时,请按目录中给出的 path 读取对应的 SKILL.md。\n"
|
"- 当你需要技能细节时,请按目录中给出的 path 读取对应的 SKILL.md。\n"
|
||||||
"- 当对话涉及长期记忆、用户身份/偏好、项目背景延续、复发问题沉淀时,优先启用 wiki-first-autonomy 技能,并优先使用 memory_wiki_search/memory_wiki_get 检索上下文,再执行与回复。\n"
|
"- 当对话涉及长期记忆、用户身份/偏好、项目背景延续、复发问题沉淀时,优先启用 wiki-first-autonomy 技能,并优先使用 memory_wiki_search/memory_wiki_get 检索上下文,再执行与回复。\n"
|
||||||
"- memory wiki 默认根路径为 `data/wiki`(例如 `data/wiki/improvement/learnings.md`);未被用户明确覆盖时,统一按该路径读写与检索。\n"
|
"- memory wiki 请仅传相对 wiki 根的路径(相对 `data/wiki`),例如 `improvement/learnings.md`;不要传 `data/wiki/...` 或绝对路径。\n"
|
||||||
"- 当新增事实会影响后续决策时,完成当前任务后使用 memory_wiki_apply 写入结构化记忆,并用 memory_wiki_lint 做质量检查。\n"
|
"- 当新增事实会影响后续决策时,完成当前任务后使用 memory_wiki_apply 写入结构化记忆,并用 memory_wiki_lint 做质量检查。\n"
|
||||||
"- 技能包由说明文档和可选文件组成。运行时不会自动执行技能 `scripts/` 目录下的文件;\n"
|
"- 技能包由说明文档和可选文件组成。运行时不会自动执行技能 `scripts/` 目录下的文件;\n"
|
||||||
"- internal hooks 是独立系统,也不会自动执行这些脚本。\n"
|
"- internal hooks 是独立系统,也不会自动执行这些脚本。\n"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from oclaw.platform.config.paths import PROJECT_ROOT
|
||||||
from oclaw.runtime.tools.experts.memory.wiki_tools import (
|
from oclaw.runtime.tools.experts.memory.wiki_tools import (
|
||||||
memory_wiki_apply_tool,
|
memory_wiki_apply_tool,
|
||||||
memory_wiki_get_tool,
|
memory_wiki_get_tool,
|
||||||
|
|
@ -35,3 +38,22 @@ def test_memory_wiki_tools_handlers_load_without_dataclass_module_error() -> Non
|
||||||
assert "AttributeError" not in str(apply_res.get("error", ""))
|
assert "AttributeError" not in str(apply_res.get("error", ""))
|
||||||
assert "__dict__" not in str(apply_res.get("error", ""))
|
assert "__dict__" not in str(apply_res.get("error", ""))
|
||||||
|
|
||||||
|
|
||||||
|
def test_memory_wiki_tools_accept_relative_prefixed_and_absolute_paths() -> None:
|
||||||
|
wiki_root = (PROJECT_ROOT / "data" / "wiki").resolve()
|
||||||
|
rel = Path("improvement") / "learnings.md"
|
||||||
|
abs_path = str((wiki_root / rel).resolve())
|
||||||
|
prefixed_rel = str(Path("data/wiki") / rel)
|
||||||
|
|
||||||
|
# Relative
|
||||||
|
r1 = memory_wiki_get_tool().handler({"path": str(rel)})
|
||||||
|
assert r1.get("ok") is True
|
||||||
|
|
||||||
|
# Prefixed by "data/wiki/..."
|
||||||
|
r2 = memory_wiki_get_tool().handler({"path": prefixed_rel})
|
||||||
|
assert r2.get("ok") is True
|
||||||
|
|
||||||
|
# Absolute path under wiki_root
|
||||||
|
r3 = memory_wiki_get_tool().handler({"path": abs_path})
|
||||||
|
assert r3.get("ok") is True
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue