mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 00:40:45 +08:00
feat: 路径约定体系 & 自优化管道落地
This commit is contained in:
parent
08df5d580a
commit
df30b5902f
37 changed files with 239 additions and 324 deletions
31
skills/_workspace/public/path-convention/SKILL.md
Normal file
31
skills/_workspace/public/path-convention/SKILL.md
Normal 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/` 以外的技能目录
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
```
|
||||
|
||||
## 快速配置
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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¤t_weather=true"
|
||||
```
|
||||
|
||||
Tool fallback:
|
||||
```text
|
||||
web_fetch_clean url="https://api.open-meteo.com/v1/forecast?latitude=51.5&longitude=-0.12¤t_weather=true"
|
||||
```
|
||||
|
||||
Find coordinates for a city, then query. Returns JSON with temp, windspeed, weathercode.
|
||||
|
||||
Docs: https://open-meteo.com/en/docs
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue