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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ Memory is the backbone of the **Save** capability. We build on [**PowerMem**](ht
- First end-to-end memory-backed skill: a daily package download digest with period-over-period deltas.
- Memory-adjacent observability — per-session spend, scheduled cost digests, and models.dev pricing.

**Next (v0.4.0)**: full seekdb hybrid retrieval and a self-maintaining LLM Wiki module — persistent wiki pages that compound with every ingest, with Ebbinghaus decay and freshness-weighted ranking keeping content alive. See the [v0.4.0 milestone](https://github.com/openmaster-ai/clawmaster/milestone/1) for tracked work.
**Next (v0.4.0)**: full seekdb hybrid retrieval and a self-maintaining LLM Knowledge module — persistent knowledge pages that compound with every ingest, with Ebbinghaus decay and freshness-weighted ranking keeping content alive. See the [v0.4.0 milestone](https://github.com/openmaster-ai/clawmaster/milestone/1) for tracked work.

## Product Tour

Expand Down Expand Up @@ -182,7 +182,7 @@ Six core capabilities — each moves from infrastructure toward real daily use:
|---|---|---|---|---|
| 1 | **Setup** | Available | Guided wizard, 6+ LLM providers with key validation, 6 channel types (Feishu / WeChat / Discord / Slack / Telegram / WhatsApp), profile switching | One-click environment migration ([#1](https://github.com/openmaster-ai/clawmaster/issues/1)), Windows + WSL2 first-class support |
| 2 | **Observe** | Available | ClawProbe-backed dashboard, per-session cost and token tracking, gateway health monitoring | Historical spend analytics, anomaly alerts, multi-profile comparison |
| 3 | **Save** | In progress | Managed PowerMem runtime + OpenClaw bridge, local workspace import, first memory-backed skill — see [Memory Highlights](#memory-highlights) | Full seekdb hybrid retrieval, self-maintaining LLM Wiki — see [v0.4.0 milestone](https://github.com/openmaster-ai/clawmaster/milestone/1) |
| 3 | **Save** | In progress | Managed PowerMem runtime + OpenClaw bridge, local workspace import, first memory-backed skill — see [Memory Highlights](#memory-highlights) | Full seekdb hybrid retrieval, self-maintaining LLM Knowledge module — see [v0.4.0 milestone](https://github.com/openmaster-ai/clawmaster/milestone/1) |
| 4 | **Apply** | In progress | PaddleOCR pipeline (upload → parse → structured markdown), layout-aware extraction | Photo → flashcard automation, invoice extraction templates, more scenario-first guided workflows |
| 5 | **Build** | Planned | Plugin/skill install and toggle, MCP server management, skill security auditing | Visual agent composer for skill chaining, LangChain Deep Agents integration, conversational agent builder |
| 6 | **Guard** | Planned | Skill Guard security scanning (dimension/severity/risk scoring), basic capability gating | API key vault (encrypted at rest), per-profile spend caps, RBAC for team deployments |
Expand Down
4 changes: 2 additions & 2 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ npm run tauri:dev # 桌面应用
- 首个端到端记忆驱动技能:每日 npm 包下载摘要,支持周期同比对比。
- 记忆相关的可观测:按会话花费、定时费用摘要、models.dev 定价。

**下一步(v0.4.0)**:完整的 seekdb 混合检索,以及自维护的 LLM Wiki 模块 —— 每次投入都会让 Wiki 页面自动交叉链接并积累,艾宾浩斯衰减与新鲜度加权让内容保持“活着”。具体进展见 [v0.4.0 里程碑](https://github.com/openmaster-ai/clawmaster/milestone/1)。
**下一步(v0.4.0)**:完整的 seekdb 混合检索,以及自维护的 LLM 知识库模块 —— 每次写入都会让知识页面自动交叉链接并积累,艾宾浩斯衰减与新鲜度加权让内容保持"活着"。具体进展见 [v0.4.0 里程碑](https://github.com/openmaster-ai/clawmaster/milestone/1)。

## 产品功能总览

Expand Down Expand Up @@ -182,7 +182,7 @@ npm run tauri:dev # 桌面应用
|---|---|---|---|---|
| 1 | **能接管** | 可用 | 引导式向导、6+ LLM 供应商并校验 Key、6 种频道(飞书 / 微信 / Discord / Slack / Telegram / WhatsApp)、Profile 切换 | 一键环境迁移、Windows + WSL2 一等支持 |
| 2 | **能观测** | 可用 | 基于 ClawProbe 的面板、按会话的费用与 Token 追踪、网关健康监控 | 历史花费分析、异常告警、多 Profile 对比 |
| 3 | **能省钱** | 进行中 | 托管 PowerMem 运行时 + OpenClaw 桥接、本地工作区导入、首个记忆驱动技能 —— 详见[记忆亮点](#记忆亮点) | 完整 seekdb 混合检索、自维护 LLM Wiki —— 详见 [v0.4.0 里程碑](https://github.com/openmaster-ai/clawmaster/milestone/1) |
| 3 | **能省钱** | 进行中 | 托管 PowerMem 运行时 + OpenClaw 桥接、本地工作区导入、首个记忆驱动技能 —— 详见[记忆亮点](#记忆亮点) | 完整 seekdb 混合检索、自维护 LLM 知识库模块 —— 详见 [v0.4.0 里程碑](https://github.com/openmaster-ai/clawmaster/milestone/1) |
| 4 | **能应用** | 进行中 | PaddleOCR 流水线(上传 → 解析 → 结构化 Markdown)、版面感知提取 | 拍照 → 闪卡自动生成、发票提取模板、更多场景优先的引导式工作流 |
| 5 | **能构建** | 规划中 | 插件 / 技能安装与开关、MCP 服务管理、技能安全审计 | 可视化智能体编排器、LangChain Deep Agents 集成、对话式智能体构建 |
| 6 | **能守护** | 规划中 | Skill Guard 安全扫描(维度 / 严重性 / 风险评分)、基础能力门控 | API Key 加密保险箱、按 Profile 的花费上限、团队部署 RBAC |
Expand Down
4 changes: 2 additions & 2 deletions README_JP.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ npm run tauri:dev # デスクトップアプリ
- 管理対象メモリで動く初のエンドツーエンドスキル:npm ダウンロード日次ダイジェストと期間比較。
- メモリ近傍の可観測性:セッション単位のコスト、スケジュール済みコストダイジェスト、models.dev 価格情報。

**次(v0.4.0)**:完全な seekdb ハイブリッド検索と、自己保守する LLM Wiki モジュール —— 取り込みごとに交差リンクされて積み上がる永続ページ、エビングハウス減衰と新鮮度重み付けで内容を生かし続けます。詳細は [v0.4.0 マイルストーン](https://github.com/openmaster-ai/clawmaster/milestone/1) を参照。
**次(v0.4.0)**:完全な seekdb ハイブリッド検索と、自己保守する LLM ナレッジモジュール —— 取り込みごとに交差リンクされて積み上がる永続ページ、エビングハウス減衰と新鮮度重み付けで内容を生かし続けます。詳細は [v0.4.0 マイルストーン](https://github.com/openmaster-ai/clawmaster/milestone/1) を参照。

## プロダクト機能ツアー

Expand Down Expand Up @@ -182,7 +182,7 @@ npm run tauri:dev # デスクトップアプリ
|---|---|---|---|---|
| 1 | **能管理** | 利用可能 | ガイド付きウィザード、6+ LLM プロバイダー(キー検証付き)、6 チャンネル(Feishu / WeChat / Discord / Slack / Telegram / WhatsApp)、Profile 切り替え | ワンクリック環境移行、Windows + WSL2 ファーストクラスサポート |
| 2 | **能観測** | 利用可能 | ClawProbe ベースのダッシュボード、セッションごとのコスト・トークン追跡、ゲートウェイヘルス監視 | 履歴コスト分析、異常アラート、マルチ Profile 比較 |
| 3 | **能節約** | 開発中 | 管理対象 PowerMem ランタイム + OpenClaw ブリッジ、ワークスペースインポート、初のメモリ駆動スキル —— 詳細は[メモリハイライト](#メモリハイライト) | 完全な seekdb ハイブリッド検索、自己保守 LLM Wiki —— 詳細は [v0.4.0 マイルストーン](https://github.com/openmaster-ai/clawmaster/milestone/1) |
| 3 | **能節約** | 開発中 | 管理対象 PowerMem ランタイム + OpenClaw ブリッジ、ワークスペースインポート、初のメモリ駆動スキル —— 詳細は[メモリハイライト](#メモリハイライト) | 完全な seekdb ハイブリッド検索、自己保守 LLM ナレッジモジュール —— 詳細は [v0.4.0 マイルストーン](https://github.com/openmaster-ai/clawmaster/milestone/1) |
| 4 | **能活用** | 開発中 | PaddleOCR パイプライン(アップロード → 解析 → 構造化 Markdown)、レイアウト認識抽出 | 写真 → フラッシュカード自動生成、請求書抽出テンプレート、シナリオ優先のガイド付きワークフロー |
| 5 | **能構築** | 計画中 | プラグイン / スキルのインストール・切り替え、MCP サーバー管理、スキルセキュリティ監査 | ビジュアルエージェントコンポーザー、LangChain Deep Agents 統合、対話型エージェントビルダー |
| 6 | **能守護** | 計画中 | Skill Guard セキュリティスキャン(次元 / 重大度 / リスクスコア)、基本的な機能ゲーティング | API キー暗号化ボールト、Profile ごとの支出上限、チームデプロイ向け RBAC |
Expand Down
260 changes: 260 additions & 0 deletions packages/backend/src/openclawVaultPaths.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
import assert from 'node:assert/strict'
import fs from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import test from 'node:test'
import {
ensureWikiVaultStructure,
resolveOpenclawStateDirForVault,
resolveOpenclawStateDirFromManagedDataRoot,
resolveWikiVaultLayout,
resolveWikiVaultRoot,
WIKI_SCHEMA_MARKDOWN,
} from './openclawVaultPaths.js'

function emptyEnv(): Record<string, string | undefined> {
return {}
}

test('resolveWikiVaultRoot honors explicit vault root override before all other inputs', () => {
const vaultRoot = resolveWikiVaultRoot({
vaultRootOverride: '/tmp/explicit-vault',
env: { OPENCLAW_STATE_DIR: '/tmp/other', CLAWMASTER_WIKI_ROOT: '/tmp/env-override' },
homeDir: '/home/testuser',
profileSelection: { kind: 'named', name: 'work' },
dataRootOverride: '/home/testuser/.clawmaster/data/named/work',
})
assert.equal(vaultRoot, '/tmp/explicit-vault')
})

test('resolveWikiVaultRoot honors CLAWMASTER_WIKI_ROOT env when no explicit override is supplied', () => {
const vaultRoot = resolveWikiVaultRoot({
env: { CLAWMASTER_WIKI_ROOT: '/tmp/env-vault' },
homeDir: '/home/testuser',
})
assert.equal(vaultRoot, '/tmp/env-vault')
})

test('resolveWikiVaultRoot prefers OPENCLAW_STATE_DIR over profile selection', () => {
const vaultRoot = resolveWikiVaultRoot({
env: { OPENCLAW_STATE_DIR: '/opt/openclaw-state' },
homeDir: '/home/testuser',
profileSelection: { kind: 'named', name: 'work' },
platform: 'linux',
})
assert.equal(vaultRoot, path.posix.join('/opt/openclaw-state', 'wiki'))
})

test('resolveWikiVaultRoot derives a named-profile state dir from the managed data root', () => {
const vaultRoot = resolveWikiVaultRoot({
env: emptyEnv(),
homeDir: '/home/testuser',
platform: 'linux',
dataRootOverride: '/home/testuser/.clawmaster/data/named/work',
})
assert.equal(vaultRoot, path.posix.join('/home/testuser/.openclaw-work', 'wiki'))
})

test('resolveWikiVaultRoot derives a dev state dir from the managed data root', () => {
const vaultRoot = resolveWikiVaultRoot({
env: emptyEnv(),
homeDir: '/home/testuser',
platform: 'linux',
dataRootOverride: '/home/testuser/.clawmaster/data/dev',
})
assert.equal(vaultRoot, path.posix.join('/home/testuser/.openclaw-dev', 'wiki'))
})

test('resolveWikiVaultRoot derives a default state dir from the managed data root', () => {
const vaultRoot = resolveWikiVaultRoot({
env: emptyEnv(),
homeDir: '/home/testuser',
platform: 'linux',
dataRootOverride: '/home/testuser/.clawmaster/data/default',
})
assert.equal(vaultRoot, path.posix.join('/home/testuser/.openclaw', 'wiki'))
})

test('resolveWikiVaultRoot falls back to profile selection + home when no overrides are supplied', () => {
const defaultRoot = resolveWikiVaultRoot({
env: emptyEnv(),
homeDir: '/home/testuser',
platform: 'linux',
profileSelection: { kind: 'default' },
})
assert.equal(defaultRoot, path.posix.join('/home/testuser/.openclaw', 'wiki'))

const devRoot = resolveWikiVaultRoot({
env: emptyEnv(),
homeDir: '/home/testuser',
platform: 'linux',
profileSelection: { kind: 'dev' },
})
assert.equal(devRoot, path.posix.join('/home/testuser/.openclaw-dev', 'wiki'))

const namedRoot = resolveWikiVaultRoot({
env: emptyEnv(),
homeDir: '/home/testuser',
platform: 'linux',
profileSelection: { kind: 'named', name: 'work' },
})
assert.equal(namedRoot, path.posix.join('/home/testuser/.openclaw-work', 'wiki'))
})

test('resolveWikiVaultRoot uses windows path semantics when a windows data root is supplied', () => {
const vaultRoot = resolveWikiVaultRoot({
env: emptyEnv(),
platform: 'win32',
dataRootOverride: 'C:\\Users\\dev\\.clawmaster\\data\\default',
})
assert.equal(vaultRoot, path.win32.join('C:\\Users\\dev\\.openclaw', 'wiki'))
})

test('resolveWikiVaultRoot prefers /home/<user> over /mnt/c/users/<user> when WSL data root is supplied', () => {
const vaultRoot = resolveWikiVaultRoot({
env: { HOME: '/home/wsluser' },
platform: 'linux',
dataRootOverride: '/mnt/c/users/dev/.clawmaster/data/default',
})
assert.equal(vaultRoot, path.posix.join('/home/wsluser/.openclaw', 'wiki'))
})

test('resolveOpenclawStateDirFromManagedDataRoot returns null for unrecognised managed data roots', () => {
assert.equal(
resolveOpenclawStateDirFromManagedDataRoot('/tmp/something/else', {}),
null,
)
assert.equal(
resolveOpenclawStateDirFromManagedDataRoot('', {}),
null,
)
})

test('resolveOpenclawStateDirForVault keeps OPENCLAW_STATE_DIR precedence over dataRootOverride', () => {
const stateDir = resolveOpenclawStateDirForVault({
env: { OPENCLAW_STATE_DIR: '/opt/override' },
homeDir: '/home/testuser',
dataRootOverride: '/home/testuser/.clawmaster/data/named/work',
})
assert.equal(stateDir, '/opt/override')
})

test('resolveWikiVaultLayout returns all derived paths below the resolved vault root', () => {
const layout = resolveWikiVaultLayout({
vaultRootOverride: '/tmp/vault',
platform: 'linux',
})
assert.equal(layout.vaultRoot, '/tmp/vault')
assert.equal(layout.rawRoot, path.posix.join('/tmp/vault', 'raw'))
assert.equal(layout.pagesRoot, path.posix.join('/tmp/vault', 'pages'))
assert.equal(layout.metaRoot, path.posix.join('/tmp/vault', '.meta'))
assert.equal(layout.indexPath, path.posix.join('/tmp/vault', 'index.md'))
assert.equal(layout.logPath, path.posix.join('/tmp/vault', 'log.md'))
assert.equal(layout.schemaPath, path.posix.join('/tmp/vault', 'SCHEMA.md'))
assert.equal(layout.freshnessPath, path.posix.join('/tmp/vault/.meta', 'freshness.json'))
assert.equal(layout.conflictsPath, path.posix.join('/tmp/vault/.meta', 'conflicts.json'))
assert.equal(layout.ingestStatePath, path.posix.join('/tmp/vault/.meta', 'ingest-state.json'))
})

test('ensureWikiVaultStructure creates directories, meta files, and the shared schema markdown', async () => {
const vaultRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'clawmaster-vault-'))
try {
const layout = resolveWikiVaultLayout({ vaultRootOverride: vaultRoot })
await ensureWikiVaultStructure(layout)

for (const subdir of ['raw', '.meta', 'pages/sources', 'pages/entities', 'pages/concepts', 'pages/synthesis', 'pages/processes']) {
const stat = await fs.stat(path.join(vaultRoot, subdir))
assert.ok(stat.isDirectory(), `${subdir} should be a directory`)
}

const schema = await fs.readFile(layout.schemaPath, 'utf8')
assert.equal(schema, `${WIKI_SCHEMA_MARKDOWN}\n`)

const freshness = await fs.readFile(layout.freshnessPath, 'utf8')
assert.equal(freshness.trim(), '{}')

const conflicts = await fs.readFile(layout.conflictsPath, 'utf8')
assert.equal(conflicts.trim(), '[]')

const state = JSON.parse(await fs.readFile(layout.ingestStatePath, 'utf8')) as {
version: number
sources: Record<string, unknown>
}
assert.equal(state.version, 1)
assert.deepEqual(state.sources, {})
} finally {
await fs.rm(vaultRoot, { recursive: true, force: true })
}
})

test('ensureWikiVaultStructure is idempotent and does not overwrite existing content', async () => {
const vaultRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'clawmaster-vault-idempotent-'))
try {
const layout = resolveWikiVaultLayout({ vaultRootOverride: vaultRoot })
await ensureWikiVaultStructure(layout)

const customSchema = '# Custom schema (user edited)\n'
await fs.writeFile(layout.schemaPath, customSchema, 'utf8')
await fs.writeFile(layout.logPath, '# User custom log\n\n- a personal log entry\n', 'utf8')
await fs.writeFile(layout.freshnessPath, '{"foo":1}\n', 'utf8')

await ensureWikiVaultStructure(layout)

assert.equal(await fs.readFile(layout.schemaPath, 'utf8'), customSchema)
assert.match(await fs.readFile(layout.logPath, 'utf8'), /personal log entry/)
assert.equal(JSON.parse(await fs.readFile(layout.freshnessPath, 'utf8')).foo, 1)
} finally {
await fs.rm(vaultRoot, { recursive: true, force: true })
}
})

test('resolveWikiVaultLayout uses dataRootOverride when vaultRootOverride is absent', () => {
const layout = resolveWikiVaultLayout({
env: emptyEnv(),
platform: 'linux',
dataRootOverride: '/home/testuser/.clawmaster/data/named/work',
})
assert.equal(layout.vaultRoot, path.posix.join('/home/testuser/.openclaw-work', 'wiki'))
assert.equal(layout.pagesRoot, path.posix.join('/home/testuser/.openclaw-work', 'wiki', 'pages'))
assert.equal(layout.ingestStatePath, path.posix.join('/home/testuser/.openclaw-work', 'wiki', '.meta', 'ingest-state.json'))
})

test('resolveWikiVaultRoot matches the plugin-side resolution for representative managed data roots (parity)', () => {
const cases = [
{
platform: 'linux' as const,
homeDir: '/home/testuser',
dataRootOverride: '/home/testuser/.clawmaster/data/default',
expected: '/home/testuser/.openclaw/wiki',
},
{
platform: 'linux' as const,
homeDir: '/home/testuser',
dataRootOverride: '/home/testuser/.clawmaster/data/named/work',
expected: '/home/testuser/.openclaw-work/wiki',
},
{
platform: 'linux' as const,
homeDir: '/home/testuser',
dataRootOverride: '/home/testuser/.clawmaster/data/dev',
expected: '/home/testuser/.openclaw-dev/wiki',
},
]

for (const { platform, homeDir, dataRootOverride, expected } of cases) {
const backendResolution = resolveWikiVaultRoot({
env: emptyEnv(),
platform,
homeDir,
dataRootOverride,
})
assert.equal(backendResolution, expected)

// Plugin resolution today is `resolveOpenclawWorkspaceDir(ctx)` joined with '../wiki'.
// Proving the derived-state-dir matches our shared helper is enough to guarantee
// the plugin's join(..., '..', 'wiki') lands on the same vault root without
// creating a circular plugin→backend import in production code.
const stateDir = resolveOpenclawStateDirFromManagedDataRoot(dataRootOverride, {})
assert.equal(stateDir, expected.replace(/\/wiki$/, ''))
}
})
Loading
Loading