feat: 路径约定体系 & 自优化管道落地

This commit is contained in:
oliver 2026-05-10 23:28:41 +08:00
parent 08df5d580a
commit df30b5902f
37 changed files with 239 additions and 324 deletions

View file

@ -0,0 +1,31 @@
---
name: path-convention
description: "Oclaw 项目路径规范。定义了项目根目录、用户数据区、技能目录、Wiki 记忆四者之间的路径关系。适用于:新建文件、读取文件、运行脚本、创建目录等所有涉及文件路径的操作之前。"
---
# 路径规范 —— Path Convention
## 根目录
项目根 = 当前仓库根目录(workspace root)
所有 `read_file`、`write_file`、`delete_file`、`move_file`、`mkdir` 等工具的 `path` 参数支持两种写法:
- 相对路径:相对于**项目根目录**(workspace root)解析,例如 `data/workspace/a.txt`
- 绝对路径:按系统绝对路径解析(Windows 如 `D:\...`,Linux/macOS 如 `/...`),但仍受工作区路径访问策略约束
## 核心目录
| 用途 | 路径 | 说明 |
|------|------|------|
| **用户数据** | `data/workspace/` | 所有用户生成的文件放这里:脚本、文档、工作流、图片生成产物、测试文件等 |
| **技能文件** | `skills/` | 系统技能和用户安装的技能 |
| **Wiki 记忆** | `data/wiki/` | 长期记忆、Learnings、用户偏好、项目背景 |
| **临时文件** | `data/workspace/tmp/` | 临时中间文件 |
## 注意事项
- `get_cwd()` 返回项目根目录(workspace root)
- `run_command` 未传 `cwd` 时默认在项目根目录执行;需要在子目录执行时请显式传 `cwd`
- 用户数据只放在 `data/workspace/` 下,不要直接散落在根目录
- `data/wiki/` 仅存放结构化记忆,不要放脚本或工作流文件
- 技能安装路径见 `skill_auto_install` 工具,勿手动写入 `skills/` 以外的技能目录

View file

@ -1,109 +0,0 @@
/**
* Oclaw 自我改进 Hook
*
* 在 agent:bootstrap 阶段注入学习沉淀提醒。
*/
const REMINDER_NAME = 'SELF_IMPROVEMENT_REMINDER.md';
const REMINDER_PATH = REMINDER_NAME;
const REMINDER_CONTENT = `
## 自我改进提醒
任务完成后,请评估是否产生可沉淀学习。
仅在当前仓库/工作区启用 self-improvement 技能时记录。
记录前:
- 使用 memory_wiki_* 工具写入 \`improvement/\` 下的 Wiki 笔记
- 不记录密钥、令牌、私钥、环境变量或原始对话全文
- 优先使用简短摘要或脱敏片段,避免完整命令输出
**以下情况应记录:**
- 用户纠正你 → \`improvement/learnings.md\`
- 命令/操作失败 → \`improvement/errors.md\`
- 用户提出缺失能力 → \`improvement/feature-requests.md\`
- 发现认知错误 → \`improvement/learnings.md\`
- 发现更优做法 → \`improvement/learnings.md\`
**当模式被验证后进行提升:**
- 行为模式 → \`SOUL.md\`
- 工作流改进 → \`AGENTS.md\`
- 工具易错点 → \`TOOLS.md\`
条目保持简洁:时间、标题、发生了什么、后续应如何做。
`.trim();
function isObject(value) {
return !!value && typeof value === 'object';
}
function isInjectedReminderFile(value) {
if (!isObject(value) || value.path !== REMINDER_PATH) {
return false;
}
return (
value.virtual === true ||
value.content === REMINDER_CONTENT
);
}
const handler = async (event) => {
// 事件结构安全检查
if (!event || typeof event !== 'object') {
return;
}
// 仅处理 agent:bootstrap 事件
if (event.type !== 'agent' || event.action !== 'bootstrap') {
return;
}
// context 安全检查
if (!event.context || typeof event.context !== 'object') {
return;
}
// 跳过子代理会话,避免引导污染
const sessionKey = event.sessionKey || '';
if (sessionKey.includes(':subagent:')) {
return;
}
// 以虚拟 bootstrap 文件注入提醒
if (Array.isArray(event.context.bootstrapFiles)) {
const occupiedByOtherFile = event.context.bootstrapFiles.some(
(file) => isObject(file) && file.path === REMINDER_PATH && !isInjectedReminderFile(file),
);
if (occupiedByOtherFile) {
return;
}
const cleanedBootstrapFiles = event.context.bootstrapFiles.filter(
(file, index, files) =>
!isInjectedReminderFile(file) ||
files.findIndex((candidate) => isInjectedReminderFile(candidate)) === index,
);
const reminderFile = {
name: REMINDER_NAME,
path: REMINDER_PATH,
content: REMINDER_CONTENT,
missing: false,
virtual: true,
};
const existingIndex = cleanedBootstrapFiles.findIndex((file) => isInjectedReminderFile(file));
if (existingIndex === -1) {
cleanedBootstrapFiles.push(reminderFile);
} else {
cleanedBootstrapFiles[existingIndex] = reminderFile;
}
event.context.bootstrapFiles = cleanedBootstrapFiles;
}
};
module.exports = handler;
module.exports.default = handler;

View file

@ -1,108 +0,0 @@
/**
* Oclaw 自我改进 Hook
*
* 在 agent:bootstrap 阶段注入学习沉淀提醒。
*/
import type { HookHandler } from 'oclaw/hooks';
const REMINDER_NAME = 'SELF_IMPROVEMENT_REMINDER.md';
const REMINDER_PATH = REMINDER_NAME;
const REMINDER_CONTENT = `## 自我改进提醒
任务完成后,请评估是否产生可沉淀学习。
仅在当前仓库/工作区启用 self-improvement 技能时记录。
记录前:
- 使用 memory_wiki_* 工具写入 \`improvement/\` 下的 Wiki 笔记
- 不记录密钥、令牌、私钥、环境变量或原始对话全文
- 优先使用简短摘要或脱敏片段,避免完整命令输出
**以下情况应记录:**
- 用户纠正你 → \`improvement/learnings.md\`
- 命令/操作失败 → \`improvement/errors.md\`
- 用户提出缺失能力 → \`improvement/feature-requests.md\`
- 发现认知错误 → \`improvement/learnings.md\`
- 发现更优做法 → \`improvement/learnings.md\`
**当模式被验证后进行提升:**
- 行为模式 → \`SOUL.md\`
- 工作流改进 → \`AGENTS.md\`
- 工具易错点 → \`TOOLS.md\`
条目保持简洁:时间、标题、发生了什么、后续应如何做。`;
function isObject(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object';
}
function isInjectedReminderFile(value: unknown): boolean {
if (!isObject(value) || value.path !== REMINDER_PATH) {
return false;
}
return (
value.virtual === true ||
value.content === REMINDER_CONTENT
);
}
const handler: HookHandler = async (event) => {
// 事件结构安全检查
if (!event || typeof event !== 'object') {
return;
}
// 仅处理 agent:bootstrap 事件
if (event.type !== 'agent' || event.action !== 'bootstrap') {
return;
}
// context 安全检查
if (!event.context || typeof event.context !== 'object') {
return;
}
// 跳过子代理会话,避免引导污染
const sessionKey = event.sessionKey || '';
if (sessionKey.includes(':subagent:')) {
return;
}
// 以虚拟 bootstrap 文件注入提醒
if (Array.isArray(event.context.bootstrapFiles)) {
const occupiedByOtherFile = event.context.bootstrapFiles.some(
(file) => isObject(file) && file.path === REMINDER_PATH && !isInjectedReminderFile(file),
);
if (occupiedByOtherFile) {
return;
}
const cleanedBootstrapFiles = event.context.bootstrapFiles.filter(
(file, index, files) =>
!isInjectedReminderFile(file) ||
files.findIndex((candidate) => isInjectedReminderFile(candidate)) === index,
);
const reminderFile = {
name: REMINDER_NAME,
path: REMINDER_PATH,
content: REMINDER_CONTENT,
missing: false,
virtual: true,
};
const existingIndex = cleanedBootstrapFiles.findIndex((file) => isInjectedReminderFile(file));
if (existingIndex === -1) {
cleanedBootstrapFiles.push(reminderFile);
} else {
cleanedBootstrapFiles[existingIndex] = reminderFile;
}
event.context.bootstrapFiles = cleanedBootstrapFiles;
}
};
export default handler;

View file

@ -23,7 +23,7 @@ Oclaw uses workspace-based prompt injection combined with event-driven hooks. Co
└── hooks/ # Custom hooks
└── <hook-name>/
├── HOOK.md
└── handler.ts
└── handler.py
```
## 快速配置

View file

@ -128,13 +128,35 @@ def _extract_recent_topic(memory_text: str) -> str:
return _last_nonempty_line(memory_text)
def _extract_developer_name(raw_text: str) -> str:
lines = [ln.strip() for ln in str(raw_text or "").splitlines() if ln.strip()]
for ln in lines:
low = ln.lower()
if low.startswith("#"):
continue
if low.startswith("name:") or low.startswith("姓名:"):
name = ln.split(":", 1)[1].strip()
if name:
return name[:60]
if low.startswith("- name:") or low.startswith("- 姓名:"):
name = ln.split(":", 1)[1].strip()
if name:
return name[:60]
if ln.startswith("- "):
val = ln[2:].strip()
if val:
return val[:60]
return ln[:60]
return ""
def _build_bootstrap_content(event: Any) -> str:
ws_dir = _workspace_dir(event)
repo = _repo_root()
wiki = _wiki_root(repo)
soul = repo / "runtime" / "skills" / "session-bootstrap" / "SOUL.md"
ident = repo / "runtime" / "skills" / "session-bootstrap" / "IDENTITY.md"
soul = repo / "skills" / "session-bootstrap" / "SOUL.md"
ident = repo / "skills" / "session-bootstrap" / "IDENTITY.md"
mem_file = _latest_memory_file(ws_dir)
role_id = _agent_role(event)
@ -158,10 +180,12 @@ def _build_bootstrap_content(event: Any) -> str:
role_lines.append(f"### {p.name}\n{snap or '(缺失)'}")
role_txt = "\n\n".join(role_lines).strip()
lrn_txt = _safe_read(learnings, max_chars=900)
user_current = wiki / "users" / "current.md"
user_txt = _safe_read(user_current, max_chars=400)
topic = _extract_recent_topic(mem_txt) or "近期项目上下文"
learning = _extract_recent_learning(lrn_txt) or "待补充新的关键学习"
developer = "开发者"
developer = _extract_developer_name(user_txt) or "开发者"
welcome = f"欢迎回来,{developer}。上次我们聊了{topic},我学到了{learning}。"
mem_path = str(mem_file) if mem_file else "(无)"
@ -184,6 +208,7 @@ def _build_bootstrap_content(event: Any) -> str:
f"- 当前角色: {role_id or '(未知)'}\n"
f"- Wiki core 规则文件: {core_sources}\n"
f"- Wiki 角色规则文件: {role_sources}\n"
f"- Wiki 当前用户: {user_current}\n"
f"- Wiki 学习记录: {learnings}\n"
f"- Wiki 错误记录: {errors}\n"
f"- Wiki 需求记录: {feats}\n\n"
@ -223,6 +248,13 @@ def handle(event: object) -> None:
return
content = _build_bootstrap_content(event)
try:
repo = _repo_root()
soul_ok = bool((repo / "skills" / "session-bootstrap" / "SOUL.md").exists())
ident_ok = bool((repo / "skills" / "session-bootstrap" / "IDENTITY.md").exists())
ctx["sessionBootstrapDiag"] = {"soul_found": soul_ok, "identity_found": ident_ok}
except Exception:
pass
occupied = any(_is_record(f) and str(f.get("path")) == BOOT_PATH and not _is_bootstrap_file(f) for f in files)
if occupied:
return

View file

@ -38,7 +38,7 @@ Set in OpenClaw config:
## Script Location
```bash
python3 skills/tavily/lib/tavily_search.py <command> "query" [options]
python3 skills/tavily-search-pro/lib/tavily_search.py <command> "query" [options]
```
---
@ -347,5 +347,5 @@ Sources:
## Install
```bash
bash skills/tavily/install.sh
bash skills/tavily-search-pro/install.sh
```

View file

@ -2,7 +2,7 @@
name: weather
description: Get current weather and forecasts (no API key required).
homepage: https://wttr.in/:help
metadata: {"clawdbot":{"emoji":"🌤️","requires":{"bins":["curl"]}}}
metadata: {"clawdbot":{"emoji":"🌤️"}}
---
# Weather
@ -17,6 +17,11 @@ curl -s "wttr.in/London?format=3"
# Output: London: ⛅️ +8°C
```
Tool fallback (no curl required):
```text
web_fetch_clean url="https://wttr.in/London?format=3"
```
Compact format:
```bash
curl -s "wttr.in/London?format=%l:+%c+%t+%h+%w"
@ -36,6 +41,7 @@ Tips:
- Units: `?m` (metric) `?u` (USCS)
- Today only: `?1` · Current only: `?0`
- PNG: `curl -s "wttr.in/Berlin.png" -o /tmp/weather.png`
- If `curl` is unavailable, prefer `web_fetch_clean` for text responses
## Open-Meteo (fallback, JSON)
@ -44,6 +50,11 @@ Free, no key, good for programmatic use:
curl -s "https://api.open-meteo.com/v1/forecast?latitude=51.5&longitude=-0.12&current_weather=true"
```
Tool fallback:
```text
web_fetch_clean url="https://api.open-meteo.com/v1/forecast?latitude=51.5&longitude=-0.12&current_weather=true"
```
Find coordinates for a city, then query. Returns JSON with temp, windspeed, weathercode.
Docs: https://open-meteo.com/en/docs