diff --git a/.cargo/config.toml b/.cargo/config.toml index 1e4c80007..5402c0999 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,3 +3,14 @@ [target.'cfg(all())'] rustflags = ["--cfg", "ruma_identifiers_storage=\"Arc\""] +## Override the bundle/package ID that Makepad uses by default with Robrix's ID. +[env] +MAKEPAD_BUNDLE_IDENTIFIER = { value = "rs.robius.robrix", force = true } +MAKEPAD_BUNDLE_NAME = { value = "Robrix", force = true } +MAKEPAD_APP_ICON_32 = { value = "resources/icon_32.png", relative = true } +MAKEPAD_APP_ICON_64 = { value = "resources/icon_64.png", relative = true } +MAKEPAD_APP_ICON_128 = { value = "resources/icon_128.png", relative = true } +MAKEPAD_APP_ICON_256 = { value = "resources/icon_256.png", relative = true } +MAKEPAD_APP_ICON_512 = { value = "resources/icon_512.png", relative = true } +MAKEPAD_APP_ICON_1024 = { value = "resources/icon_1024.png", relative = true } +MAKEPAD_APP_ICON_ICO = { value = "resources/icon.ico", relative = true } diff --git a/.claude/skills/file-issue/SKILL.md b/.claude/skills/file-issue/SKILL.md new file mode 100644 index 000000000..5af60e80b --- /dev/null +++ b/.claude/skills/file-issue/SKILL.md @@ -0,0 +1,116 @@ +--- +name: file-issue +description: Document a bug/fix locally in issues/ and create a matching GitHub issue +allowed-tools: + - Bash(ls:*) + - Bash(mkdir:*) + - Bash(gh:*) + - Glob + - Grep + - Read + - Write +when_to_use: | + Use when the user wants to document a discovered bug, applied fix, and remaining issues + as both a local issue file and a GitHub issue. Typically invoked after a debugging/fix session. + Examples: "file an issue for this", "record this bug", "create issue", "file-issue" +--- + +# File Issue + +Document a bug discovery and fix as a local issue file in `issues/` and a matching GitHub issue. +All output is written in English regardless of conversation language. + +## Goal + +Produce two artifacts: +1. A detailed local issue document at `issues/NNN-slug.md` +2. A GitHub issue with a summary version + +## Steps + +### 1. Scan for next issue number + +Check if `issues/` directory exists in the project root. Create it if missing. +List existing files to determine the next sequential number (e.g., if `001-*` exists, next is `002`). + +**Success criteria**: Know the next issue number (zero-padded to 3 digits) and confirmed `issues/` dir exists. + +### 2. Gather context from conversation + +Extract from the current conversation: +- **Summary**: One-line description of the bug +- **Severity**: Critical / High / Medium / Low +- **Symptoms**: What the user observed (UI behavior, error messages, logs) +- **Root Cause**: Technical explanation of why it happens +- **Reproduction**: Steps to reproduce +- **Fix Applied**: What was changed and why (include code snippets if relevant) +- **Remaining Issues**: Known limitations, follow-up work, upstream bugs +- **Files Changed**: List of modified files +- **Test Verification**: Before/after comparison table + +Generate a kebab-case slug from the summary (e.g., `dock-load-state-drawlist-corruption`). + +**Success criteria**: All template sections populated with specific, accurate details from the session. + +### 3. Write local issue document + +Write to `issues/NNN-slug.md` using this template: + +```markdown +# Issue #NNN: {Summary} + +**Date:** {YYYY-MM-DD} +**Severity:** {Critical|High|Medium|Low} +**Status:** Fixed (workaround applied) | Fixed | Open +**Affected component:** {file path(s)} + +## Summary +{One paragraph} + +## Symptoms +{Bullet list of what the user observed} + +## Root Cause +{Technical explanation with code snippets} + +## Reproduction +{Numbered steps} + +## Fix Applied +{Description + key code changes} + +## Remaining Issues +{Numbered list of known limitations and follow-up work} + +## Files Changed +{Bullet list} + +## Test Verification +{Before/after table} +``` + +**Success criteria**: File written, all sections filled, no placeholder text remaining. + +### 4. Create GitHub issue + +Detect the repo with `gh repo view --json nameWithOwner`. +Create a GitHub issue via `gh issue create` with: +- Title: same as local doc summary (concise, under 80 chars) +- Label: `bug` +- Body: condensed version with Summary, Symptoms, Root Cause, Fix Applied, Remaining Issues (as checklist), and Environment section +- Reference the local doc path in the body + +**Rules**: +- Use a HEREDOC for the body to preserve formatting +- Remaining Issues should be `- [ ]` checklist items +- Include a link/reference to the local issue doc + +**Success criteria**: GitHub issue created, URL returned. + +### 5. Report results + +Tell the user: +- Local issue doc path +- GitHub issue URL (in `owner/repo#number` format for clickable link) + +**Success criteria**: Both paths reported in a concise summary. \ No newline at end of file diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index 7aee76ee6..ee14edd5f 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -85,7 +85,9 @@ jobs: libwayland-dev libxkbcommon-dev - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + rustflags: "" - name: Cache Rust dependencies uses: Swatinem/rust-cache@v2 @@ -93,8 +95,6 @@ jobs: key: ubuntu-build-${{ hashFiles('Cargo.lock') }} - name: Build - env: - RUSTFLAGS: "-D warnings" run: | cargo build --profile fast @@ -116,7 +116,9 @@ jobs: - uses: actions/checkout@v4 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + rustflags: "" - name: Cache Rust dependencies uses: Swatinem/rust-cache@v2 @@ -124,8 +126,6 @@ jobs: key: macos-${{ matrix.arch }}-build-${{ hashFiles('Cargo.lock') }} - name: Build - env: - RUSTFLAGS: "-D warnings" run: | cargo build --profile fast @@ -140,7 +140,9 @@ jobs: - uses: actions/checkout@v4 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + rustflags: "" - name: Cache Rust dependencies uses: Swatinem/rust-cache@v2 @@ -148,8 +150,6 @@ jobs: key: windows-build-${{ hashFiles('Cargo.lock') }} - name: Build - env: - RUSTFLAGS: "-D warnings" run: | cargo build --profile fast @@ -165,10 +165,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install Rust stable and nightly - uses: dtolnay/rust-toolchain@stable + - name: Install Rust (pinned via rust-toolchain.toml) + uses: actions-rust-lang/setup-rust-toolchain@v1 with: - toolchain: stable + rustflags: "" - name: Install Rust nightly run: | @@ -192,7 +192,6 @@ jobs: - name: Build for iOS targets env: - RUSTFLAGS: "-D warnings" AWS_LC_SYS_CMAKE_BUILDER: 1 run: | # Install iOS targets @@ -217,7 +216,9 @@ jobs: - uses: actions/checkout@v4 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + rustflags: "" - name: Setup cargo-makepad uses: ./.github/actions/setup-cargo-makepad @@ -233,7 +234,6 @@ jobs: - name: Build Android APK env: - RUSTFLAGS: "-D warnings" AWS_LC_SYS_CMAKE_BUILDER: 1 run: | cargo makepad android build -p robrix \ @@ -251,7 +251,9 @@ jobs: - uses: actions/checkout@v4 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + rustflags: "" - name: Setup cargo-makepad uses: ./.github/actions/setup-cargo-makepad @@ -267,7 +269,6 @@ jobs: - name: Build Android APK env: - RUSTFLAGS: "-D warnings" AWS_LC_SYS_CMAKE_BUILDER: 1 run: | cargo makepad android build -p robrix \ @@ -288,7 +289,9 @@ jobs: - uses: actions/checkout@v4 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + rustflags: "" - name: Cache Rust dependencies uses: Swatinem/rust-cache@v2 @@ -308,7 +311,6 @@ jobs: - name: Build Android APK env: - RUSTFLAGS: "-D warnings" AWS_LC_SYS_CMAKE_BUILDER: 1 CMAKE_GENERATOR: Ninja run: | diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c0dd613dc..32ab9dbfc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -40,8 +40,6 @@ env: CARGO_TERM_COLOR: always CARGO_INCREMENTAL: "0" # Disable incremental compilation in CI RUST_BACKTRACE: 1 - # Enable warnings as errors for strict checks - RUSTFLAGS: "-D warnings" jobs: clippy: @@ -50,8 +48,9 @@ jobs: runs-on: macos-14 ## avoids having to install Linux deps steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable + - uses: actions-rust-lang/setup-rust-toolchain@v1 with: + rustflags: "" components: rustfmt, clippy - uses: Swatinem/rust-cache@v2 with: @@ -67,3 +66,24 @@ jobs: - uses: actions/checkout@v4 - name: Check for typos uses: crate-ci/typos@master + + check_patches: + if: github.event.pull_request.draft == false + name: check patches + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Forbid local path patches in Cargo.toml + # Local `path = ...` in a [patch.*] block is useful for upstream + # debugging but breaks CI and every teammate's checkout when + # accidentally committed. Upstream patches must use `git = ...`. + run: | + awk ' + /^[[:space:]]*#/ {next} + /^[[:space:]]*\[patch/ {in_patch=1; next} + /^[[:space:]]*\[/ {in_patch=0} + in_patch && /path[[:space:]]*=/ { + print "::error file=Cargo.toml,line=" NR "::local path patch found: " $0 + exit 1 + } + ' Cargo.toml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6e1394630..9187c8c94 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,6 +85,79 @@ concurrency: cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} jobs: + clippy_report: + name: Clippy Gate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + libssl-dev \ + libsqlite3-dev \ + pkg-config \ + llvm \ + clang \ + libclang-dev \ + libxcursor-dev \ + libx11-dev \ + libasound2-dev \ + libpulse-dev \ + libwayland-dev libxkbcommon-dev + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + rustflags: "" + components: clippy + + - uses: Swatinem/rust-cache@v2 + with: + key: release-clippy-report + + - name: Run clippy + shell: bash + run: | + set -o pipefail + cargo clippy --workspace --all-features 2>&1 | tee clippy.log + + - name: Summarize and enforce gate + if: always() + shell: bash + run: | + echo "## Clippy Gate for $(git rev-parse --short HEAD)" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + # Count warnings via the #[warn(...)] annotation line that clippy + # emits for each flagged lint. This is more accurate than counting + # "^warning:" header lines because cargo itself can emit meta-warnings + # (e.g. "warning: skipping duplicate package ...") that are not lints. + warn_count=$(grep -cE '#\[warn\(' clippy.log || true) + error_count=$(grep -cE '^error:' clippy.log || true) + echo "- Warnings: ${warn_count:-0}" >> $GITHUB_STEP_SUMMARY + echo "- Errors: ${error_count:-0}" >> $GITHUB_STEP_SUMMARY + if [[ "${warn_count:-0}" -gt 0 ]]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Warning categories" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + grep -oE '#\[warn\([^)]+\)\]' clippy.log | sed -E 's/#\[warn\((.*)\)\]/\1/' | sort | uniq -c | sort -rn >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + fi + # Hard gate: any warning or error here fails this job, which + # cascades through create_release.needs to block every build + # and publish step. Release cannot ship with dirty clippy state. + if [[ "${warn_count:-0}" -gt 0 || "${error_count:-0}" -gt 0 ]]; then + echo "::error::Clippy gate failed: ${warn_count:-0} warning(s), ${error_count:-0} error(s). See summary above." + exit 1 + fi + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: clippy-log + path: clippy.log + retention-days: 30 + check_tag_version: name: Check Release Tag and Cargo.toml Version Consistency runs-on: ubuntu-latest @@ -167,9 +240,9 @@ jobs: create_release: name: Create Release - needs: [check_tag_version, determine_matrices] + needs: [check_tag_version, determine_matrices, clippy_report] if: >- - ${{ always() && ( + ${{ always() && needs.clippy_report.result == 'success' && ( (github.event_name == 'push' && needs.check_tag_version.result == 'success') || (github.event_name == 'workflow_dispatch' && github.event.inputs.create_release == 'true' && @@ -265,16 +338,24 @@ jobs: libwayland-dev libxkbcommon-dev - name: Install Rust Stable - uses: dtolnay/rust-toolchain@stable + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + rustflags: "" - name: Package (desktop) uses: project-robius/makepad-packaging-action@v1 env: CARGO_PACKAGER_SIGNING_KEY: ${{ secrets.CARGO_PACKAGER_SIGNING_KEY }} CARGO_PACKAGER_SIGNING_PASSWORD: ${{ secrets.CARGO_PACKAGER_SIGNING_PASSWORD }} + CARGO_PACKAGER_SIGN_PRIVATE_KEY: ${{ secrets.CARGO_PACKAGER_SIGN_PRIVATE_KEY || secrets.CARGO_PACKAGER_SIGNING_KEY }} + CARGO_PACKAGER_SIGN_PRIVATE_KEY_PASSWORD: ${{ secrets.CARGO_PACKAGER_SIGN_PRIVATE_KEY_PASSWORD || secrets.CARGO_PACKAGER_SIGNING_PASSWORD }} + ROBRIX_UPDATER_PUBKEY: ${{ secrets.CARGO_PACKAGER_SIGN_PUBLIC_KEY }} + ROBRIX_UPDATER_ENDPOINT: https://github.com/${{ github.repository }}/releases/latest/download/latest.json with: github_token: ${{ secrets.ROBRIX_RELEASE }} packager_formats: deb + uploadUpdaterJson: true + uploadUpdaterSignatures: true releaseId: ${{ needs.create_release.outputs.release_id }} for_macos: @@ -289,22 +370,30 @@ jobs: - uses: actions/checkout@v4 - name: Install Rust Stable - uses: dtolnay/rust-toolchain@stable + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + rustflags: "" - name: Package (macos) uses: project-robius/makepad-packaging-action@v1 env: CARGO_PACKAGER_SIGNING_KEY: ${{ secrets.CARGO_PACKAGER_SIGNING_KEY }} CARGO_PACKAGER_SIGNING_PASSWORD: ${{ secrets.CARGO_PACKAGER_SIGNING_PASSWORD }} - APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} - APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + CARGO_PACKAGER_SIGN_PRIVATE_KEY: ${{ secrets.CARGO_PACKAGER_SIGN_PRIVATE_KEY || secrets.CARGO_PACKAGER_SIGNING_KEY }} + CARGO_PACKAGER_SIGN_PRIVATE_KEY_PASSWORD: ${{ secrets.CARGO_PACKAGER_SIGN_PRIVATE_KEY_PASSWORD || secrets.CARGO_PACKAGER_SIGNING_PASSWORD }} + ROBRIX_UPDATER_PUBKEY: ${{ secrets.CARGO_PACKAGER_SIGN_PUBLIC_KEY }} + ROBRIX_UPDATER_ENDPOINT: https://github.com/${{ github.repository }}/releases/latest/download/latest.json + # APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + # APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} APP_STORE_CONNECT_API_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }} APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }} APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }} with: github_token: ${{ secrets.ROBRIX_RELEASE }} packager_formats: dmg - enable_macos_notarization: true + enable_macos_notarization: false + uploadUpdaterJson: true + uploadUpdaterSignatures: true releaseId: ${{ needs.create_release.outputs.release_id }} for_windows: @@ -319,16 +408,24 @@ jobs: - uses: actions/checkout@v4 - name: Install Rust Stable - uses: dtolnay/rust-toolchain@stable + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + rustflags: "" - name: Package (windows) uses: project-robius/makepad-packaging-action@v1 env: CARGO_PACKAGER_SIGNING_KEY: ${{ secrets.CARGO_PACKAGER_SIGNING_KEY }} CARGO_PACKAGER_SIGNING_PASSWORD: ${{ secrets.CARGO_PACKAGER_SIGNING_PASSWORD }} + CARGO_PACKAGER_SIGN_PRIVATE_KEY: ${{ secrets.CARGO_PACKAGER_SIGN_PRIVATE_KEY || secrets.CARGO_PACKAGER_SIGNING_KEY }} + CARGO_PACKAGER_SIGN_PRIVATE_KEY_PASSWORD: ${{ secrets.CARGO_PACKAGER_SIGN_PRIVATE_KEY_PASSWORD || secrets.CARGO_PACKAGER_SIGNING_PASSWORD }} + ROBRIX_UPDATER_PUBKEY: ${{ secrets.CARGO_PACKAGER_SIGN_PUBLIC_KEY }} + ROBRIX_UPDATER_ENDPOINT: https://github.com/${{ github.repository }}/releases/latest/download/latest.json with: github_token: ${{ secrets.ROBRIX_RELEASE }} packager_formats: nsis + uploadUpdaterJson: true + uploadUpdaterSignatures: true releaseId: ${{ needs.create_release.outputs.release_id }} for_android: @@ -340,7 +437,9 @@ jobs: - uses: actions/checkout@v4 - name: Install Rust Stable - uses: dtolnay/rust-toolchain@stable + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + rustflags: "" - name: Package (android) uses: project-robius/makepad-packaging-action@v1 @@ -367,7 +466,9 @@ jobs: - uses: actions/checkout@v4 - name: Install Rust Stable - uses: dtolnay/rust-toolchain@stable + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + rustflags: "" - name: Package (iOS) uses: project-robius/makepad-packaging-action@v1 diff --git a/.gitignore b/.gitignore index 1f891a019..edd23fd14 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,8 @@ .vscode .DS_Store -CLAUDE.md proxychains.conf + +## agent-chat demo runtime logs / machine-specific registry +roadmap/agentchat-demo/.demo-logs/ +roadmap/agentchat-demo/projects.json diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 000000000..f9156ad46 --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1 @@ +## 2026-04-15 - Prevent HTML Injection in Media Captions\n**Vulnerability:** XSS / HTML Injection in media captions. The `formatted_caption()` was used indiscriminately, injecting `fb.body` into `show_html` regardless of the `fb.format` type. If a malicious client sent a custom format or plaintext with HTML tags, it would be executed by the UI.\n**Learning:** The `FormattedBody` structure from Matrix (via Ruma) must have its `.format` field explicitly checked (e.g., `MessageFormat::Html`) before treating its `.body` as safe HTML, as native UI renders rely on this explicit contract.\n**Prevention:** Always use `.filter(|fb| fb.format == MessageFormat::Html)` when extracting HTML from a `FormattedBody`, and strictly fallback to `htmlize::escape_text` for plain text representations. diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 000000000..9aba04cae --- /dev/null +++ b/.typos.toml @@ -0,0 +1,2 @@ +[default.extend-words] +PN = "PN" diff --git a/AGENTS.md b/AGENTS.md index 9a393de6f..8842d383c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,732 +1,105 @@ +# Robrix2 — Agent Instructions -# Makepad Project Guide +Keep this file short. Use it for project rules and working guidance. Use the codebase, `CLAUDE.md`, and Makepad 2.0 skills as the detailed reference. -## Important: When Converting Syntax +## Required Reading -**Always search for existing usage patterns in the NEW crates (widgets, code_editor, studio) before making syntax changes.** The old `widgets` and `live_design!` syntax is deprecated. When unsure about the correct syntax for something, grep for similar usage in `widgets/src/` to find the correct pattern. +Before starting work, read these documents: -```bash -# Example: find how texture declarations work in new system -grep -r "texture_2d" widgets/src/ -``` - -**Critical: Always use `Name: value` syntax, never `Name = value`.** The old `Key = Value` syntax no longer works. For named widget instances, use `name := Type{...}` syntax. - -## Running UI Programs - -```bash -RUST_BACKTRACE=1 cargo run -p makepad-example-splash --release & PID=$!; sleep 15; kill $PID 2>/dev/null; echo "Process $PID killed" -``` - -## Cargo.toml Setup - -```toml -[package] -name = "makepad-example-myapp" -version = "0.1.0" -edition = "2021" - -[dependencies] -makepad-widgets = { path = "../../widgets" } -``` - - -## Widgets DSL (script_mod!) - -The new DSL uses `script_mod!` macro with runtime script evaluation instead of the old `live_design!` compile-time macros. - -### Imports and App Setup - -```rust -use makepad_widgets::*; - -app_main!(App); - -script_mod!{ - use mod.prelude.widgets.* - - load_all_resources() do #(App::script_component(vm)){ - ui: Root{ - main_window := Window{ - window.inner_size: vec2(800, 600) - body +: { - // UI content here - } - } - } - } -} - -impl App { - fn run(vm: &mut ScriptVm) -> Self { - crate::makepad_widgets::script_mod(vm); // Register all widgets - // Platform-specific initialization goes here (e.g., vm.cx().start_stdin_service() for macos) - App::from_script_mod(vm, self::script_mod) - } -} - -#[derive(Script, ScriptHook)] -pub struct App { - #[live] ui: WidgetRef, -} - -impl MatchEvent for App { - fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) { - // Handle widget actions - } -} - -impl AppMain for App { - fn handle_event(&mut self, cx: &mut Cx, event: &Event) { - self.match_event(cx, event); - self.ui.handle_event(cx, event, &mut Scope::empty()); - } -} -``` - -### Available Widgets (widgets/src/lib.rs) - -Core: `View`, `SolidView`, `RoundedView`, `ScrollXView`, `ScrollYView`, `ScrollXYView` -Text: `Label`, `H1`, `H2`, `H3`, `LinkLabel`, `TextInput` -Buttons: `Button`, `ButtonFlat`, `ButtonFlatter` -Toggles: `CheckBox`, `Toggle`, `RadioButton` -Input: `Slider`, `DropDown` -Layout: `Splitter`, `FoldButton`, `FoldHeader`, `Hr` -Lists: `PortalList` -Navigation: `StackNavigation`, `ExpandablePanel` -Overlays: `Modal`, `Tooltip`, `PopupNotification` -Dock: `Dock`, `DockSplitter`, `DockTabs`, `DockTab` -Media: `Image`, `Icon`, `LoadingSpinner` -Special: `FileTree`, `PageFlip`, `CachedWidget` -Window: `Window`, `Root` -Markup: `Html`, `Markdown` (feature-gated) - -### Widget Definition Pattern - -```rust -// Rust struct -#[derive(Script, ScriptHook, Widget)] -pub struct MyWidget { - #[source] source: ScriptObjectRef, // Required for script integration - #[walk] walk: Walk, - #[layout] layout: Layout, - #[redraw] #[live] draw_bg: DrawQuad, - #[live] draw_text: DrawText, - #[rust] my_state: i32, // Runtime-only field -} - -// For widgets with animations, add Animator derive: -#[derive(Script, ScriptHook, Widget, Animator)] -pub struct AnimatedWidget { - #[source] source: ScriptObjectRef, - #[apply_default] animator: Animator, - // ... -} -``` - -### Script Module Structure - -```rust -script_mod!{ - use mod.prelude.widgets_internal.* // For internal widget definitions - use mod.widgets.* // Access other widgets - - // Register base widget (connects Rust struct to script) - mod.widgets.MyWidgetBase = #(MyWidget::register_widget(vm)) - - // Create styled variant with defaults - mod.widgets.MyWidget = set_type_default() do mod.widgets.MyWidgetBase{ - width: Fill - height: Fit - padding: theme.space_2 - - draw_bg +: { - color: theme.color_bg_app - } - } -} -``` - -### Key Syntax Differences (Old vs New) - -| Old (live_design!) | New (script_mod!) | -|-------------------|-------------------| -| `` | `mod.widgets.BaseWidget{ }` | -| `{{StructName}}` | `#(Struct::register_widget(vm))` | -| `(THEME_COLOR_X)` | `theme.color_x` | -| `` | `theme.font_regular` | -| `instance hover: 0.0` | `hover: instance(0.0)` | -| `uniform color: #fff` | `color: uniform(#fff)` | -| `draw_bg: { }` (replace) | `draw_bg +: { }` (merge) | -| `default: off` | `default: @off` | -| `fn pixel(self)` | `pixel: fn()` | -| `item.apply_over(cx, live!{...})` | `script_apply_eval!(cx, item, {...})` | - -### Runtime Property Updates with script_apply_eval! - -Use `script_apply_eval!` macro to dynamically update widget properties at runtime: -```rust -// Old system (live! macro with apply_over) -item.apply_over(cx, live!{ - height: (height) - draw_bg: {is_even: (if is_even {1.0} else {0.0})} -}); - -// New system (script_apply_eval! macro) -script_apply_eval!(cx, item, { - height: #(height) - draw_bg +: {is_even: #(if is_even {1.0} else {0.0})} -}); - -// For colors, use #(color) syntax -let color = self.color_focus; -script_apply_eval!(cx, item, { - draw_bg +: { - color: #(color) - } -}); -``` - -Note: In `script_apply_eval!`, use `#(expr)` for Rust expression interpolation instead of `(expr)`. - -### Theme Access - -Always use `theme.` prefix: -```rust -color: theme.color_bg_app -padding: theme.space_2 -font_size: theme.font_size_p -text_style: theme.font_regular -``` - -### Property Merging with `+:` - -The `+:` operator merges with parent instead of replacing: -```rust -mod.widgets.MyButton = mod.widgets.Button{ - draw_bg +: { - color: #f00 // Only overrides color, keeps other draw_bg properties - } -} -``` - -### Shader Instance vs Uniform - -- `instance(value)` - Per-draw-call value (can vary per widget instance) -- `uniform(value)` - Shared across all instances using same shader - -```rust -draw_bg +: { - hover: instance(0.0) // Each button has its own hover state - color: uniform(theme.color_x) // Shared base color - color_hover: instance(theme.color_y) // Per-instance if color varies -} -``` - -### Animator Definition - -```rust -animator: Animator{ - hover: { - default: @off - off: AnimatorState{ - from: {all: Forward {duration: 0.1}} - apply: { - draw_bg: {hover: 0.0} - draw_text: {hover: 0.0} - } - } - on: AnimatorState{ - from: {all: Snap} // Instant transition - apply: { - draw_bg: {hover: 1.0} - draw_text: {hover: 1.0} - } - } - } -} -``` - -### Shader Functions - -```rust -draw_bg +: { - pixel: fn() { - let sdf = Sdf2d.viewport(self.pos * self.rect_size) - sdf.box(0.0, 0.0, self.rect_size.x, self.rect_size.y, 4.0) - sdf.fill(self.color.mix(self.color_hover, self.hover)) - return sdf.result - } -} -``` - -Note: Use `.method()` not `::method()` in shaders. - -### Color Mixing (Method Chaining) - -```rust -// Old nested style (avoid) -mix(mix(mix(color1, color2, hover), color3, down), color4, focus) - -// New chained style (preferred) -color1.mix(color2, hover).mix(color3, down).mix(color4, focus) -``` +1. [DESIGN.md](DESIGN.md) — architecture overview, module organization, technology stack +2. [specs/project.spec.md](specs/project.spec.md) — project constraints, decisions, forbidden actions +3. [CLAUDE.md](CLAUDE.md) — project workflow rules and Makepad 2.0 guidance +4. [MAKEPAD.md](MAKEPAD.md) — Makepad 2.0 skill routing and design judgment entry point -### App Structure Pattern - -```rust -script_mod!{ - use mod.prelude.widgets.* - - load_all_resources() do #(App::script_component(vm)){ - ui: Root{ - main_window := Window{ - window.inner_size: vec2(1000, 700) - body +: { - // Your UI here - MyWidget{} - } - } - } - } -} - -impl App { - fn run(vm: &mut ScriptVm) -> Self { - crate::makepad_widgets::script_mod(vm); - // Platform-specific initialization (e.g., vm.cx().start_stdin_service() for macos) - App::from_script_mod(vm, self::script_mod) - } -} - -#[derive(Script, ScriptHook)] -pub struct App { - #[live] ui: WidgetRef, -} - -impl MatchEvent for App { - fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) { - if self.ui.button(ids!(my_button)).clicked(actions) { - log!("Button clicked!"); - } - } -} - -impl AppMain for App { - fn handle_event(&mut self, cx: &mut Cx, event: &Event) { - self.match_event(cx, event); - self.ui.handle_event(cx, event, &mut Scope::empty()); - } -} -``` - -### Widget ID References - -Use `:=` for named widget instances: -```rust -// In DSL -my_button := Button{text: "Click"} - -// In Rust code -self.ui.button(ids!(my_button)).clicked(actions) -``` - -### Template Definitions in Dock - -Templates inside Dock are local; use `let` bindings at script level for reusable components: -```rust -script_mod!{ - // Reusable at script level - let MyPanel = SolidView{ - width: Fill - height: Fill - // ... - } - - // Use directly - body +: { - MyPanel{} // Works because it's a let binding - } -} -``` - -### Custom Draw Widget Example - -```rust -#[derive(Script, ScriptHook, Widget)] -pub struct CustomDraw { - #[walk] walk: Walk, - #[layout] layout: Layout, - #[redraw] #[live] draw_quad: DrawQuad, - #[rust] area: Area, -} - -impl Widget for CustomDraw { - fn draw_walk(&mut self, cx: &mut Cx2d, _scope: &mut Scope, walk: Walk) -> DrawStep { - cx.begin_turtle(walk, self.layout); - let rect = cx.turtle().rect(); - self.draw_quad.draw_abs(cx, rect); - cx.end_turtle_with_area(&mut self.area); - DrawStep::done() - } - - fn handle_event(&mut self, _cx: &mut Cx, _event: &Event, _scope: &mut Scope) {} -} -``` - -### Script Object Storage: map vs vec - -In script objects, properties are stored in two different places: -- **`map`**: Contains `key: value` pairs (regular properties) -- **`vec`**: Contains named template items (via `:=` syntax) - -This distinction is important when working with `on_after_apply` or inspecting script objects directly. - -### Templates in List Widgets (PortalList, FlatList) - -In list widgets, named IDs (using `:=`) define **templates** that are stored in the widget's `templates` HashMap. These are NOT regular properties - they go into the script object's vec and are collected via `on_after_apply`. - -```rust -// In script_mod! - defining templates for a list -my_list := PortalList { - // Regular properties (go into struct fields) - width: Fill - height: Fill - scroll_bar: mod.widgets.ScrollBar {} - - // Templates (named with :=) - stored in templates HashMap, NOT struct fields - Item := View { - height: 40 - title := Label { text: "Default" } - } - Header := View { - draw_bg: { color: #333 } - } -} -``` - -The templates are collected in `on_after_apply`: -```rust -impl ScriptHook for PortalList { - fn on_after_apply(&mut self, vm: &mut ScriptVm, apply: &Apply, scope: &mut Scope, value: ScriptValue) { - if let Some(obj) = value.as_object() { - vm.vec_with(obj, |_vm, vec| { - for kv in vec { - if let Some(id) = kv.key.as_id() { - self.templates.insert(id, kv.value); - } - } - }); - } - } -} -``` - -Then used during drawing: -```rust -while let Some(item_id) = list.next_visible_item(cx) { - let item = list.item(cx, item_id, id!(Item)); - item.label(ids!(title)).set_text(cx, &format!("Item {}", item_id)); - item.draw_all(cx, &mut Scope::empty()); -} -``` - -**Key distinction**: Regular properties like `scroll_bar: mod.widgets.ScrollBar {}` are applied directly to struct fields. Template definitions like `Item := View {...}` are stored separately for dynamic instantiation. - -### PortalList Usage - -```rust -#[derive(Script, ScriptHook, Widget)] -pub struct MyList { - #[deref] view: View, -} - -impl Widget for MyList { - fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { - while let Some(item) = self.view.draw_walk(cx, scope, walk).step() { - if let Some(mut list) = item.borrow_mut::() { - list.set_item_range(cx, 0, 100); // 100 items - - while let Some(item_id) = list.next_visible_item(cx) { - let item = list.item(cx, item_id, id!(Item)); - item.label(ids!(title)).set_text(cx, &format!("Item {}", item_id)); - item.draw_all(cx, &mut Scope::empty()); - } - } - } - DrawStep::done() - } -} -``` - -### FileTree Usage - -```rust -impl Widget for FileTreeDemo { - fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { - while self.file_tree.draw_walk(cx, scope, walk).is_step() { - self.file_tree.set_folder_is_open(cx, live_id!(root), true, Animate::No); - // Draw nodes recursively - self.draw_node(cx, live_id!(root)); - } - DrawStep::done() - } -} -``` - -### Registering Custom Draw Shaders - -For custom draw types with shader fields, use `script_shader`: - -```rust -script_mod!{ - use mod.prelude.widgets_internal.* - - // Register custom draw shader - set_type_default() do #(DrawMyShader::script_shader(vm)){ - ..mod.draw.DrawQuad // Inherit from DrawQuad - } - - // Register widget that uses it - mod.widgets.MyWidgetBase = #(MyWidget::register_widget(vm)) -} - -#[derive(Script, ScriptHook)] -#[repr(C)] -struct DrawMyShader { - #[deref] draw_super: DrawQuad, - #[live] my_param: f32, -} -``` - -### Registering Components (non-Widget) - -For structs that aren't full widgets but need script registration: - -```rust -script_mod!{ - // For components (not widgets) - mod.widgets.MyComponentBase = #(MyComponent::script_component(vm)) - - // For widgets (implements Widget trait) - mod.widgets.MyWidgetBase = #(MyWidget::register_widget(vm)) -} -``` +## Critical Rules -### Script Prelude Modules - -Two prelude modules available: -- `mod.prelude.widgets_internal.*` - For internal widget library development -- `mod.prelude.widgets.*` - For app development (includes all widgets) - -```rust -script_mod!{ - // App development - use widgets prelude - use mod.prelude.widgets.* - - // Or for widget library internals - use mod.prelude.widgets_internal.* - use mod.widgets.* -} -``` +### Do NOT run `cargo fmt` or `rustfmt` -### Default Enum Values - -For enums with a `None` variant that need `Default`, use standard Rust `#[default]` attribute instead of `DefaultNone` derive: - -```rust -// Correct - use #[default] attribute on the None variant -#[derive(Clone, Copy, Debug, PartialEq, Default)] -pub enum MyAction { - SomeAction, - AnotherAction, - #[default] - None, -} - -// Wrong - don't use DefaultNone derive -#[derive(Clone, Copy, Debug, PartialEq, DefaultNone)] // Don't do this -pub enum MyAction { - SomeAction, - None, -} -``` +This project does not use automatic Rust formatting. Do not run `cargo fmt`, `rustfmt`, or formatter wrappers. Formatting churn creates noisy diffs and breaks the repo's hand-maintained style. -### Multi-Module Script Registration Pattern - -When refactoring a multi-file project (like studio) from `live_design!` to `script_mod!`: - -1. **Each widget module** defines its own `script_mod!` that registers to `mod.widgets.*`: -```rust -// In studio_editor.rs -script_mod! { - use mod.prelude.widgets_internal.* - use mod.widgets.* - - mod.widgets.StudioCodeEditorBase = #(StudioCodeEditor::register_widget(vm)) - mod.widgets.StudioCodeEditor = set_type_default() do mod.widgets.StudioCodeEditorBase { - editor := CodeEditor {} - } -} -``` +### Do NOT commit or create PRs without user testing -2. **The lib.rs** aggregates all widget script_mods: -```rust -pub fn script_mod(vm: &mut ScriptVm) { - crate::module1::script_mod(vm); - crate::module2::script_mod(vm); - // ... all widget modules -} -``` +Present changes for testing first. Wait for user confirmation before committing or opening a PR. -3. **The app.rs** calls them in correct order: -```rust -impl App { - fn run(vm: &mut ScriptVm) -> Self { - crate::makepad_widgets::script_mod(vm); // Base widgets first - crate::script_mod(vm); // Your widget modules - crate::app_ui::script_mod(vm); // UI that uses the widgets - App::from_script_mod(vm, self::script_mod) - } -} -``` +### Makepad 2.0 only -4. **The app_ui.rs** can then use registered widgets: -```rust -script_mod! { - use mod.prelude.widgets.* - // Now StudioCodeEditor is available from mod.widgets - - let EditorContent = View { - editor := StudioCodeEditor {} - } -} -``` +- Use `script_mod!`, not `live_design!` +- Use `#[derive(Script, ScriptHook, Widget)]`, not `Live` / `LiveHook` +- Use `:=` for named children, not `=` +- Use `+:` to merge properties; bare `:` replaces +- Use `script_apply_eval!` for runtime updates, not `apply_over` + `live!` -### Cross-Module Sharing via `mod` Object - -**IMPORTANT**: `use crate.module.*` does NOT work in script_mod. The `crate.` prefix is not available. - -To share definitions between script_mod blocks in different files, store them in the `mod` object: - -```rust -// In app_ui.rs - export to mod.widgets namespace -script_mod! { - use mod.prelude.widgets.* - - // This makes AppUI available as mod.widgets.AppUI - mod.widgets.AppUI = Window{ - // ... - } -} - -// In app.rs - import via mod.widgets -script_mod! { - use mod.prelude.widgets.* - use mod.widgets.* // Now AppUI is in scope - - load_all_resources() do #(App::script_component(vm)){ - ui: Root{ AppUI{} } - } -} -``` +### Converting syntax -The `mod` object is the only way to share data between script_mod blocks. +- Search the new crates first: `widgets`, `code_editor`, `studio` +- Prefer copying an existing Makepad 2.0 pattern over guessing syntax +- Always use `Name: value`, never `Name = value` +- Named widget instances use `name := Type{...}` -### Prelude Alias Syntax +### Dynamic widget state changes -When defining a prelude, use `name:mod.path` to create an alias: -```rust -mod.prelude.widgets = { - ..mod.std, // Spread all of mod.std into scope - theme:mod.theme, // Create 'theme' as alias for mod.theme - draw:mod.draw, // Create 'draw' as alias for mod.draw -} -``` +`script_apply_eval!` does not work on widgets created via `widget_ref_from_live_ptr()` because the backing `ScriptObject` is `ZERO`. For dynamic popup and list items, use Animator state plus shader instance variables instead. -Without the alias (just `mod.theme,`), the module is included but has no name - you can't access it! +### Async Matrix operations -### Let Bindings are Local +Always use `submit_async_request(MatrixRequest::*)`. Do not spawn raw tokio tasks for Matrix API calls from UI code. -`let` bindings in script_mod are LOCAL to that script_mod block. They cannot be: -- Accessed from other script_mod blocks -- Used as property values directly (e.g., `content +: MyLetBinding` won't work) +## Quick Makepad Notes -To use a `let` binding, instantiate it: `MyLetBinding{}` or store it in `mod.*` for cross-module access. +- `draw_bg +:` merges with the parent shader config; `draw_bg:` replaces it +- In `script_apply_eval!`, Rust expressions use `#(expr)` interpolation +- Runtime `script_apply_eval!` cannot rely on DSL constants like `Right`, `Fit`, or `Align` +- `Dock.load_state()` can corrupt DrawList references in this project -### Debug Logging with `~` +## Build & Test -Use `~expression` to log the value of an expression during script evaluation: -```rust -script_mod! { - ~mod.theme // Logs the theme object - ~mod.prelude.widgets // Logs what's in the prelude - ~some_variable // Logs a variable's value (or "not found" error) -} +```bash +cargo build +cargo run +cargo test ``` -### Common Pitfalls - -**Widget ID references**: Named widget instances use `:=` in the DSL and plain names in Rust id macros: -- DSL defines `code_block := View { ... }` → Rust uses `id!(code_block)` -- DSL defines `my_button := Button { ... }` → Rust uses `ids!(my_button)` - -1. **Missing `#[source]`**: All Script-derived structs need `#[source] source: ScriptObjectRef` - -2. **Template scope**: Templates defined inside Dock aren't available outside; use `let` at script level - -3. **Uniform vs Instance**: Use `instance()` for per-widget varying colors (like hover states on backgrounds) - -4. **Forgot `+:`**: Without `+:`, you replace the entire property instead of merging - -5. **Theme access**: Always `theme.color_x`, never `THEME_COLOR_X` or `(theme.color_x)` +## Key Entry Points -6. **Missing widget registration**: Call `crate::makepad_widgets::script_mod(vm)` in `App::run()` before your own `script_mod`. Note: the old `live_design!` system and its crates are archived under `old/` +- `src/app.rs` — root app and global state +- `src/sliding_sync.rs` — Matrix sync pipeline +- `src/home/room_screen.rs` — room timeline and input integration +- `src/shared/mentionable_text_input.rs` — `@mention` system -7. **Draw shader repr**: Custom draw shaders need `#[repr(C)]` for correct memory layout +## Specs -8. **DefaultNone derive**: Don't use `DefaultNone` derive - use standard `#[derive(Default)]` with `#[default]` attribute on the `None` variant +Task specs live in `specs/` and inherit from [specs/project.spec.md](specs/project.spec.md). -9. **Script_mod call order**: Widget modules must be registered BEFORE UI modules that use them. Always call `lib.rs::script_mod` before `app_ui::script_mod` +- `specs/task-mention-user.spec.md` — `@mention` autocomplete feature -10. **`pub` keyword invalid in script_mod**: Don't use `pub mod.widgets.X = ...`, just use `mod.widgets.X = ...`. Visibility is controlled by the Rust module system, not script_mod. +Use `agent-spec parse` and `agent-spec lint --min-score 0.7` when working on specs. -11. **Syntax for Inset/Align/Walk**: Use constructor syntax - `margin: Inset{left: 10}` not `margin: {left: 10}`, `align: Align{x: 0.5 y: 0.5}` not `align: {x: 0.5, y: 0.5}` +## Working Philosophy -12. **Cursor values**: Use `cursor: MouseCursor.Hand` not `cursor: Hand` or `cursor: @Hand` +You are an engineering collaborator on this project, not a standby assistant. Work in a direct, execution-first style: -13. **Resource paths**: Use `crate_resource("self://path")` not `dep("crate://self/path")` +- Finish concrete work before reporting back +- Report what you changed, why you changed it, and what tradeoffs you made +- Prefer complete, reviewable units over tentative partial steps +- Keep mid-work chatter low; use delivery reports for important context -14. **Texture declarations in shaders**: Use `tex: texture_2d(float)` not `tex: texture2d` +## What You Submit To -15. **Enums not exposed to script**: Some Rust enums like `PopupMenuPosition::BelowInput` may not be exposed to script. If you get "not found" errors on enum variants, just remove the property and use the default +In priority order: -17. **Shader `mod` vs `modf`**: The Makepad shader language uses `modf(a, b)` for float modulo, NOT `mod(a, b)`. Similarly, use `atan2(y, x)` not `atan(y, x)` for two-argument arctangent. `atan(x)` (single arg) is also available. `fract(x)` works as expected. +1. The task's completion criteria +2. The project's existing style and patterns +3. The user's explicit, unambiguous instructions -16. **Draw shader struct field ordering**: In `#[repr(C)]` draw shader structs that extend another draw shader via `#[deref]`, NEVER place `#[rust]` or other non-instance data AFTER `DrawVars` and the instance fields. The system uses an unsafe pointer trick in `DrawVars::as_slice()` that reads contiguously past the end of `dyn_instances` into the subsequent `#[live]` fields. Any non-instance data between `DrawVars` and the instance fields will corrupt the GPU instance buffer. Put all extra data (like `#[rust]`, `#[live]` non-instance fields such as resource handles, booleans, etc.) BEFORE the `#[deref]` field, and only `#[live]` instance fields (the ones that map to shader inputs) AFTER. - ```rust - // CORRECT - non-instance data before deref, instance fields after - #[derive(Script, ScriptHook)] - #[repr(C)] - pub struct MyDrawShader { - #[live] pub svg: Option, // non-instance, BEFORE deref - #[rust] my_state: bool, // non-instance, BEFORE deref - #[deref] pub draw_super: DrawVector, // contains DrawVars + base instance fields - #[live] pub tint: Vec4f, // instance field, AFTER deref - OK - } +Correctness outranks performative deference. Do the engineering work instead of offloading routine implementation choices back to the user. - // WRONG - rust data after instance fields breaks the memory layout - #[derive(Script, ScriptHook)] - #[repr(C)] - pub struct MyDrawShader { - #[deref] pub draw_super: DrawVector, - #[live] pub tint: Vec4f, // instance field - #[rust] my_state: bool, // BAD: sits between tint and the next shader's fields - } - ``` +## On Stopping to Ask -18. **Don't put comments or blank lines before the first real code in `script!`/`script_mod!`**: Rust's proc macro token stream strips comments entirely — they produce no tokens. This shifts error column/line info because the span tracking starts from the first actual token. Always start with real code (e.g., `use mod.std.assert`) immediately after the opening brace. +Stop and ask only when genuine ambiguity would likely produce output contrary to the user's intent. -19. **WARNING: Hex colors containing the letter `e` in `script_mod!`**: The Rust tokenizer interprets `e` or `E` in hex color literals as a scientific notation exponent, causing parse errors like `expected at least one digit in exponent`. For example, `#2ecc71` fails because `2e` looks like the start of `2e`. **Use the `#x` prefix** to escape this: write `#x2ecc71` instead of `#x2ecc71`. This applies to any hex color where a digit is immediately followed by `e`/`E` (e.g., `#1e1e2e`, `#4466ee`, `#7799ee`, `#bb99ee`). Colors without `e` (like `#ff4444`, `#44cc44`) work fine with plain `#`. +Do not stop just to ask about: -20. **Shader enums**: Prefer `match` on enum values with `_ =>` as the catch-all arm, not `if/else` chains over integer-like values. If enum `match` fails in shader compilation, treat it as a compiler bug: add or extend a `platform/script/test` case and fix the shader compiler path instead of rewriting shader logic to `if/else`. \ No newline at end of file +- Reversible implementation details +- Obvious next steps that are already part of the task +- Style choices you can resolve by reading the codebase +- Post-hoc "should I also do X" follow-ups when X is already implied by the task diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..46f865f37 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,97 @@ +# Robrix2 — Claude Code Instructions + +## Required Reading + +Before starting any task, read these documents: + +1. **[DESIGN.md](DESIGN.md)** — Architecture overview, module organization, technology stack +2. **[specs/project.spec.md](specs/project.spec.md)** — Project-level constraints, decisions, and forbidden actions +3. **[AGENTS.md](AGENTS.md)** — Makepad 2.0 patterns and DSL syntax reference +4. **[MAKEPAD.md](MAKEPAD.md)** — Makepad 2.0 skill routing and design judgment entry point; **For ALL Makepad questions, FIRST load `makepad-2.0-design-judgment`.** + +## Critical Rules + +### Do NOT run `cargo fmt` +This project does not use rustfmt. Formatting changes create noisy diffs and break existing code style. + +### Do NOT commit or create PRs without user testing +Always let the user test changes before committing. Present what's ready for testing, wait for confirmation. + +### Makepad 2.0 Syntax (NOT 1.x) +- Use `script_mod!` (NOT `live_design!`) +- Use `#[derive(Script, ScriptHook, Widget)]` (NOT `Live, LiveHook`) +- Use `:=` for named children (NOT `=`) +- Use `+:` to merge properties (NOT `:` which replaces) +- Use `script_apply_eval!` for runtime updates (NOT `apply_over` + `live!`) + +### Dynamic Widget State Changes +`script_apply_eval!` does NOT work on widgets created via `widget_ref_from_live_ptr()` (ScriptObject is ZERO). Use Animator + shader instance variables instead: + +```rust +// In DSL template: +draw_bg +: { selected: instance(0.0) } +animator: Animator { highlight: { ... apply: { draw_bg: { selected: 1.0 } } } } + +// In Rust: +view.animator_cut(cx, ids!(highlight.on)); +``` + +### Async Matrix Operations +Always use `submit_async_request(MatrixRequest::*)`. Do NOT spawn raw tokio tasks for Matrix API calls. + +## Makepad 2.0 Skills + +When working on Makepad UI code, **always invoke the relevant Makepad 2.0 skill** before writing or debugging: + +| Situation | Skill to Use | +|-----------|-------------| +| UI not rendering, widget invisible, click not working | `makepad-2.0-troubleshooting` (Pitfalls #1-#44) | +| DSL syntax questions, `script_mod!`, property system | `makepad-2.0-dsl` | +| Layout issues (width/height/flow/align) | `makepad-2.0-layout` | +| Hover effects, state transitions, animation | `makepad-2.0-animation` | +| Shader code, `draw_bg`, `Sdf2d`, pixel functions | `makepad-2.0-shaders` | +| Event handling, `handle_event`, actions, `script_apply_eval!` | `makepad-2.0-events` | +| Widget catalog (View, Button, Label, etc.) | `makepad-2.0-widgets` | +| Migrating from Makepad 1.x to 2.0 | `makepad-2.0-migration` | +| App structure, `app_main!`, `MatchEvent` | `makepad-2.0-app-structure` | +| Theme system, colors, fonts | `makepad-2.0-theme` | + +**Key pitfalls from this project** (in `makepad-2.0-troubleshooting`): +- **#40**: `script_apply_eval!` fails on dynamic widgets — use Animator instead +- **#41**: DSL constants (`Right`, `Fit`, `Align`) unavailable at runtime in `script_apply_eval!` +- **#42**: `Dock.load_state()` corrupts DrawList references +- **#43**: Named children: `=` vs `:=` in `script_mod!` +- **#44**: `draw_bg:` replaces vs `draw_bg +:` merges + +## Build & Test + +```bash +# Build +cargo build + +# Run +cargo run + +# Run with hot reload +cargo run -- --hot + +# Tests (limited — mostly manual testing) +cargo test +``` + +## Project Structure + +See [DESIGN.md](DESIGN.md) for full module organization. + +Key entry points: +- `src/app.rs` — Root app, global state +- `src/sliding_sync.rs` — Matrix client, sync +- `src/home/room_screen.rs` — Timeline rendering +- `src/shared/mentionable_text_input.rs` — @mention system + +## Specs + +Task specs live in `specs/` and inherit from `specs/project.spec.md`: +- `specs/task-mention-user.spec.md` — @mention autocomplete feature + +Use `agent-spec parse` and `agent-spec lint --min-score 0.7` to validate specs. diff --git a/Cargo.lock b/Cargo.lock index ee7593224..cf3fae809 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5,7 +5,7 @@ version = 4 [[package]] name = "ab_glyph_rasterizer" version = "0.1.8" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" [[package]] name = "accessory" @@ -69,6 +69,24 @@ dependencies = [ "memchr 2.7.6 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -169,6 +187,12 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + [[package]] name = "arc-swap" version = "1.7.1" @@ -181,6 +205,17 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae2ed21cd55021f05707a807a5fc85695dafb98832921f6cfa06db67ca5b869" +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "argon2" version = "0.5.3" @@ -224,6 +259,15 @@ dependencies = [ "serde", ] +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "as_variant" version = "1.3.0" @@ -239,6 +283,28 @@ dependencies = [ "libloading", ] +[[package]] +name = "ashpd" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.2", + "raw-window-handle", + "serde", + "serde_repr", + "url", + "wayland-backend 0.3.15", + "wayland-client 0.31.14", + "wayland-protocols 0.32.12", + "zbus", +] + [[package]] name = "askar-crypto" version = "0.3.7" @@ -334,6 +400,18 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f093eed78becd229346bf859eec0aa4dd7ddde0757287b2b4107a1f09c80002" +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-channel" version = "2.5.0" @@ -359,6 +437,49 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.1", +] + [[package]] name = "async-lock" version = "3.4.1" @@ -370,12 +491,52 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + [[package]] name = "async-once-cell" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a" +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "async-rx" version = "0.1.3" @@ -386,6 +547,24 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.1", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -408,6 +587,12 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.89" @@ -449,6 +634,49 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.17", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom 8.0.0", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7178fe5f7d460b13895ebb9dcb28a3a6216d2df2574a0806cb51b555d297f38" +dependencies = [ + "arrayvec", +] + [[package]] name = "aws-lc-rs" version = "1.14.1" @@ -598,6 +826,18 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.10.0" @@ -610,7 +850,7 @@ dependencies = [ [[package]] name = "bitflags" version = "2.10.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" [[package]] name = "bitmaps" @@ -618,6 +858,15 @@ version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6" +[[package]] +name = "bitstream-io" +version = "4.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +dependencies = [ + "no_std_io2", +] + [[package]] name = "blake2" version = "0.10.6" @@ -691,6 +940,19 @@ dependencies = [ "objc2", ] +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "bls12_381" version = "0.8.0" @@ -719,6 +981,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "built" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" + [[package]] name = "bumpalo" version = "3.19.0" @@ -728,7 +996,13 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" version = "1.25.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" [[package]] name = "byteorder" @@ -739,7 +1013,13 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "byteorder" version = "1.5.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" @@ -753,6 +1033,42 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" +[[package]] +name = "cargo-packager-updater" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eec09acab5c2227aba2e592d431708305bdeb6d507703f6cd8983fb57b6c5ef7" +dependencies = [ + "base64", + "cargo-packager-utils", + "ctor", + "dirs", + "dunce", + "flate2", + "http", + "log", + "minisign-verify", + "percent-encoding", + "reqwest 0.12.28", + "semver", + "serde", + "serde_json", + "tar", + "tempfile", + "thiserror 1.0.69", + "time", + "url", +] + +[[package]] +name = "cargo-packager-utils" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b43458dd2ee3cdab3f5b105acd80791383b730380c929018701313d7d299d4e8" +dependencies = [ + "ctor", +] + [[package]] name = "cbc" version = "0.1.2" @@ -786,7 +1102,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom", + "nom 7.1.3", ] [[package]] @@ -957,6 +1273,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.4" @@ -1121,6 +1443,25 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.12" @@ -1224,6 +1565,17 @@ dependencies = [ "cipher", ] +[[package]] +name = "ctrlc" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +dependencies = [ + "dispatch2", + "nix", + "windows-sys 0.61.1", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -1465,6 +1817,27 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", +] + [[package]] name = "dirs-sys" version = "0.5.0" @@ -1473,7 +1846,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.5.2", "windows-sys 0.61.1", ] @@ -1484,6 +1857,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "block2", + "libc", "objc2", ] @@ -1498,6 +1873,15 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -1607,6 +1991,53 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1655,6 +2086,27 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec 1.15.1 (registry+https://github.com/rust-lang/crates.io-index)", + "zune-inflate", +] + +[[package]] +name = "extended" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" + [[package]] name = "eyeball" version = "0.8.8" @@ -1725,6 +2177,21 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32 0.3.9", +] + [[package]] name = "ff" version = "0.13.1" @@ -1741,6 +2208,16 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + [[package]] name = "find-msvc-tools" version = "0.1.2" @@ -1941,9 +2418,9 @@ dependencies = [ [[package]] name = "fxhash" version = "0.2.1" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ - "byteorder 1.5.0 (git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix)", + "byteorder 1.5.0 (git+https://github.com/makepad/makepad?branch=dev)", ] [[package]] @@ -2024,6 +2501,16 @@ dependencies = [ "polyval", ] +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "glob" version = "0.3.3" @@ -2393,7 +2880,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.56.0", + "windows-core 0.61.2", ] [[package]] @@ -2524,6 +3011,40 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck 1.25.0 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + [[package]] name = "imbl" version = "6.1.0" @@ -2554,6 +3075,12 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8b35f3ad95576ac81603375dfe47a0450b70a368aa34d2b6e5bb0a0d7f02428" +[[package]] +name = "imgref" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40fac9d56ed6437b198fddba683305e8e2d651aa42647f00f5ae542e7f5c94a2" + [[package]] name = "impartial-ord" version = "1.0.6" @@ -2617,6 +3144,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -2777,11 +3315,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + [[package]] name = "libc" -version = "0.2.176" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] [[package]] name = "libloading" @@ -2790,7 +3344,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.53.4", ] [[package]] @@ -2871,6 +3425,15 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -2937,7 +3500,7 @@ dependencies = [ [[package]] name = "makepad-apple-sys" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "makepad-objc-sys", ] @@ -2945,12 +3508,12 @@ dependencies = [ [[package]] name = "makepad-byteorder-lite" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" [[package]] name = "makepad-code-editor" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "makepad-widgets", ] @@ -2958,7 +3521,7 @@ dependencies = [ [[package]] name = "makepad-derive-wasm-bridge" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "makepad-micro-proc-macro", ] @@ -2966,7 +3529,7 @@ dependencies = [ [[package]] name = "makepad-derive-widget" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "makepad-live-id", "makepad-micro-proc-macro", @@ -2975,10 +3538,11 @@ dependencies = [ [[package]] name = "makepad-draw" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "ab_glyph_rasterizer", "fxhash", + "makepad-gif", "makepad-live-id", "makepad-math", "makepad-platform", @@ -2989,15 +3553,15 @@ dependencies = [ "rustybuzz", "sdfer", "serde", - "unicode-bidi 0.3.18 (git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix)", + "unicode-bidi 0.3.18 (git+https://github.com/makepad/makepad?branch=dev)", "unicode-linebreak", - "unicode-segmentation 1.12.0 (git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix)", + "unicode-segmentation 1.12.0 (git+https://github.com/makepad/makepad?branch=dev)", ] [[package]] name = "makepad-error-log" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "makepad-micro-serde", ] @@ -3005,22 +3569,30 @@ dependencies = [ [[package]] name = "makepad-filesystem-watcher" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" [[package]] name = "makepad-futures" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" [[package]] name = "makepad-futures-legacy" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" + +[[package]] +name = "makepad-gif" +version = "0.1.0" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" +dependencies = [ + "weezl 0.1.12 (git+https://github.com/makepad/makepad?branch=dev)", +] [[package]] name = "makepad-html" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "makepad-live-id", ] @@ -3034,7 +3606,7 @@ checksum = "9775cbec5fa0647500c3e5de7c850280a88335d1d2d770e5aa2332b801ba7064" [[package]] name = "makepad-latex-math" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "ttf-parser", ] @@ -3042,7 +3614,7 @@ dependencies = [ [[package]] name = "makepad-live-id" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "makepad-live-id-macros", "serde", @@ -3051,7 +3623,7 @@ dependencies = [ [[package]] name = "makepad-live-id-macros" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "makepad-micro-proc-macro", ] @@ -3059,7 +3631,7 @@ dependencies = [ [[package]] name = "makepad-live-reload-core" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "makepad-filesystem-watcher", ] @@ -3067,7 +3639,7 @@ dependencies = [ [[package]] name = "makepad-math" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "makepad-micro-serde", ] @@ -3075,12 +3647,12 @@ dependencies = [ [[package]] name = "makepad-micro-proc-macro" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" [[package]] name = "makepad-micro-serde" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "makepad-live-id", "makepad-micro-serde-derive", @@ -3089,7 +3661,7 @@ dependencies = [ [[package]] name = "makepad-micro-serde-derive" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "makepad-micro-proc-macro", ] @@ -3097,7 +3669,7 @@ dependencies = [ [[package]] name = "makepad-network" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "makepad-apple-sys", "makepad-error-log", @@ -3111,15 +3683,16 @@ dependencies = [ [[package]] name = "makepad-objc-sys" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" [[package]] name = "makepad-platform" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "ash", - "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix)", + "bitflags 2.10.0 (git+https://github.com/makepad/makepad?branch=dev)", + "ctrlc", "hilog-sys", "makepad-android-state", "makepad-apple-sys", @@ -3140,10 +3713,10 @@ dependencies = [ "napi-derive-ohos", "napi-ohos", "ohos-sys", - "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix)", - "wayland-client", + "smallvec 1.15.1 (git+https://github.com/makepad/makepad?branch=dev)", + "wayland-client 0.31.12", "wayland-egl", - "wayland-protocols", + "wayland-protocols 0.32.10", "windows 0.62.2", "windows-core 0.62.2", "windows-targets 0.52.6", @@ -3152,12 +3725,12 @@ dependencies = [ [[package]] name = "makepad-regex" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" [[package]] name = "makepad-script" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "makepad-error-log", "makepad-html", @@ -3165,13 +3738,13 @@ dependencies = [ "makepad-math", "makepad-regex", "makepad-script-derive", - "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix)", + "smallvec 1.15.1 (git+https://github.com/makepad/makepad?branch=dev)", ] [[package]] name = "makepad-script-derive" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "makepad-micro-proc-macro", ] @@ -3179,7 +3752,7 @@ dependencies = [ [[package]] name = "makepad-script-std" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "makepad-network", "makepad-script", @@ -3188,14 +3761,14 @@ dependencies = [ [[package]] name = "makepad-shared-bytes" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" [[package]] name = "makepad-studio-protocol" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ - "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix)", + "bitflags 2.10.0 (git+https://github.com/makepad/makepad?branch=dev)", "makepad-error-log", "makepad-live-id", "makepad-micro-serde", @@ -3205,7 +3778,7 @@ dependencies = [ [[package]] name = "makepad-svg" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "makepad-html", "makepad-live-id", @@ -3214,7 +3787,7 @@ dependencies = [ [[package]] name = "makepad-tsdf" version = "0.1.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "makepad-math", "makepad-micro-serde", @@ -3223,7 +3796,7 @@ dependencies = [ [[package]] name = "makepad-wasm-bridge" version = "1.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "makepad-derive-wasm-bridge", "makepad-live-id", @@ -3232,7 +3805,7 @@ dependencies = [ [[package]] name = "makepad-webp" version = "0.2.4" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "makepad-byteorder-lite", ] @@ -3240,7 +3813,7 @@ dependencies = [ [[package]] name = "makepad-widgets" version = "2.0.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "makepad-derive-widget", "makepad-draw", @@ -3249,26 +3822,26 @@ dependencies = [ "pulldown-cmark 0.12.2", "serde", "ttf-parser", - "unicode-segmentation 1.12.0 (git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix)", + "unicode-segmentation 1.12.0 (git+https://github.com/makepad/makepad?branch=dev)", ] [[package]] name = "makepad-zune-core" version = "0.5.1" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" [[package]] name = "makepad-zune-inflate" version = "0.2.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ - "simd-adler32", + "simd-adler32 0.3.8", ] [[package]] name = "makepad-zune-jpeg" version = "0.5.12" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "makepad-zune-core", ] @@ -3276,7 +3849,7 @@ dependencies = [ [[package]] name = "makepad-zune-png" version = "0.5.1" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "makepad-zune-core", "makepad-zune-inflate", @@ -3644,6 +4217,16 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "md-5" version = "0.10.6" @@ -3663,7 +4246,16 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memchr" version = "2.7.6" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] [[package]] name = "mime" @@ -3677,12 +4269,28 @@ version = "0.1.54" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbf6f36070878c42c5233846cd3de24cf9016828fd47bc22957a687298bb21fc" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase 2.8.1", +] + [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "minisign-verify" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -3690,6 +4298,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32 0.3.9", ] [[package]] @@ -3703,6 +4312,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "multihash" version = "0.19.3" @@ -3817,6 +4436,27 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +dependencies = [ + "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr 2.7.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "nom" version = "7.1.3" @@ -3827,6 +4467,21 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr 2.7.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + [[package]] name = "nu-ansi-term" version = "0.50.1" @@ -3836,6 +4491,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.6" @@ -3858,6 +4523,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -3878,6 +4554,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3943,6 +4630,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" dependencies = [ "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "block2", "objc2", "objc2-foundation", ] @@ -4077,6 +4765,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "p256" version = "0.13.2" @@ -4228,6 +4926,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkcs1" version = "0.7.5" @@ -4255,6 +4964,39 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.1", +] + +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + [[package]] name = "poly1305" version = "0.8.0" @@ -4366,6 +5108,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4488a4a36b9a4ba6b9334a32a39971f77c1436ec82c38707bce707699cc3bbcb" +dependencies = [ + "quote", + "syn 2.0.106", +] + [[package]] name = "prost" version = "0.13.5" @@ -4392,10 +5153,10 @@ dependencies = [ [[package]] name = "pulldown-cmark" version = "0.12.2" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ - "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix)", - "memchr 2.7.6 (git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix)", + "bitflags 2.10.0 (git+https://github.com/makepad/makepad?branch=dev)", + "memchr 2.7.6 (git+https://github.com/makepad/makepad?branch=dev)", "unicase 2.9.0", ] @@ -4417,6 +5178,36 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck 1.25.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr 2.7.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "quinn" version = "0.11.9" @@ -4585,6 +5376,82 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93e7e49bb0bf967717f7bd674458b3d6b0c5f48ec7e3038166026a69fc22223" +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.14.0", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.2", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.17", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "readlock" version = "0.1.9" @@ -4609,6 +5476,17 @@ dependencies = [ "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -4678,6 +5556,7 @@ dependencies = [ "base64", "bytes", "encoding_rs", + "futures-channel", "futures-core", "futures-util", "h2", @@ -4765,6 +5644,36 @@ dependencies = [ "subtle", ] +[[package]] +name = "rfd" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +dependencies = [ + "ashpd", + "block2", + "dispatch2", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "pollster", + "raw-window-handle", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" + [[package]] name = "ring" version = "0.17.14" @@ -4815,9 +5724,9 @@ dependencies = [ [[package]] name = "robius-directories" version = "6.0.0" -source = "git+https://github.com/project-robius/robius#87ea5c1e155d618a5902cae477d9603abe3f64c4" +source = "git+https://github.com/Project-Robius-China/robius2.git#f05fdf168ee4407977f11d57d99a5bc34b0f92f1" dependencies = [ - "dirs-sys", + "dirs-sys 0.5.0", "jni", "robius-android-env", ] @@ -4825,7 +5734,7 @@ dependencies = [ [[package]] name = "robius-location" version = "0.2.0" -source = "git+https://github.com/project-robius/robius#87ea5c1e155d618a5902cae477d9603abe3f64c4" +source = "git+https://github.com/Project-Robius-China/robius2.git#f05fdf168ee4407977f11d57d99a5bc34b0f92f1" dependencies = [ "android-build", "cfg-if", @@ -4840,7 +5749,7 @@ dependencies = [ [[package]] name = "robius-open" version = "0.2.0" -source = "git+https://github.com/project-robius/robius#87ea5c1e155d618a5902cae477d9603abe3f64c4" +source = "git+https://github.com/Project-Robius-China/robius2.git#f05fdf168ee4407977f11d57d99a5bc34b0f92f1" dependencies = [ "block2", "cfg-if", @@ -4865,13 +5774,14 @@ dependencies = [ [[package]] name = "robrix" -version = "0.0.1-pre-alpha-4" +version = "1.0.0-alpha.1" dependencies = [ "anyhow", "aws-lc-rs", "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "blurhash", "bytesize", + "cargo-packager-updater", "chrono", "clap", "crossbeam-channel", @@ -4881,6 +5791,7 @@ dependencies = [ "futures-util", "hashbrown 0.16.1", "htmlize", + "image", "imbl", "imghdr", "indexmap 2.13.0", @@ -4890,25 +5801,32 @@ dependencies = [ "matrix-sdk", "matrix-sdk-base", "matrix-sdk-ui", + "mime", + "mime_guess", "percent-encoding", "quinn", "rand 0.8.5", "rangemap", "reqwest 0.12.28", + "rfd", "robius-directories", "robius-location", "robius-open", "robius-use-makepad", "ruma", "sanitize-filename", + "semver", "serde", "serde_json", + "symphonia", "thiserror 2.0.17", "tokio", + "toml", "tracing-subscriber", "tsp_sdk", "unicode-segmentation 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)", "url", + "winresource", ] [[package]] @@ -5202,12 +6120,12 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rustybuzz" version = "0.18.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ - "bitflags 2.10.0 (git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix)", - "bytemuck", + "bitflags 2.10.0 (git+https://github.com/makepad/makepad?branch=dev)", + "bytemuck 1.25.0 (git+https://github.com/makepad/makepad?branch=dev)", "makepad-error-log", - "smallvec 1.15.1 (git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix)", + "smallvec 1.15.1 (git+https://github.com/makepad/makepad?branch=dev)", "ttf-parser", "unicode-bidi-mirroring", "unicode-ccc", @@ -5302,7 +6220,7 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sdfer" version = "0.2.1" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" [[package]] name = "sealed" @@ -5478,6 +6396,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "serde_spanned" version = "1.1.1" @@ -5601,7 +6530,22 @@ dependencies = [ [[package]] name = "simd-adler32" version = "0.3.8" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] [[package]] name = "siphasher" @@ -5627,7 +6571,7 @@ dependencies = [ [[package]] name = "smallvec" version = "1.15.1" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" [[package]] name = "socket2" @@ -5905,6 +6849,127 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "symphonia" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039" +dependencies = [ + "lazy_static", + "symphonia-bundle-flac", + "symphonia-bundle-mp3", + "symphonia-codec-alac", + "symphonia-codec-pcm", + "symphonia-core", + "symphonia-format-isomp4", + "symphonia-format-riff", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-flac" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91565e180aea25d9b80a910c546802526ffd0072d0b8974e3ebe59b686c9976" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-codec-alac" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8413fa754942ac16a73634c9dfd1500ed5c61430956b33728567f667fdd393ab" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af" +dependencies = [ + "arrayvec", + "bitflags 1.3.2", + "bytemuck 1.25.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-format-isomp4" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243739585d11f81daf8dac8d9f3d18cc7898f6c09a259675fc364b382c30e0a5" +dependencies = [ + "encoding_rs", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-riff" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d7c3df0e7d94efb68401d81906eae73c02b40d5ec1a141962c592d0f11a96f" +dependencies = [ + "extended", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-utils-xiph" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16" +dependencies = [ + "symphonia-core", + "symphonia-metadata", +] + [[package]] name = "syn" version = "1.0.109" @@ -5968,6 +7033,17 @@ dependencies = [ "libc", ] +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.23.0" @@ -6040,6 +7116,20 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", + "zune-jpeg", +] + [[package]] name = "time" version = "0.3.47" @@ -6190,10 +7280,12 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ + "indexmap 2.13.0", "serde_core", "serde_spanned", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", + "toml_writer", "winnow 1.0.1", ] @@ -6236,6 +7328,12 @@ dependencies = [ "winnow 1.0.1", ] +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + [[package]] name = "tower" version = "0.5.2" @@ -6400,7 +7498,7 @@ dependencies = [ [[package]] name = "ttf-parser" version = "0.24.1" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" [[package]] name = "tungstenite" @@ -6433,6 +7531,17 @@ version = "1.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8c1ae7cc0fdb8b842d65d127cb981574b0d2b249b74d1c7a2986863dc134f71" +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.1", +] + [[package]] name = "ulid" version = "1.2.1" @@ -6452,7 +7561,7 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicase" version = "2.9.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" [[package]] name = "unicode-bidi" @@ -6463,17 +7572,17 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-bidi" version = "0.3.18" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" [[package]] name = "unicode-bidi-mirroring" version = "0.3.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" [[package]] name = "unicode-ccc" version = "0.3.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" [[package]] name = "unicode-ident" @@ -6484,7 +7593,7 @@ checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-linebreak" version = "0.1.5" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" [[package]] name = "unicode-normalization" @@ -6504,12 +7613,12 @@ checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" [[package]] name = "unicode-properties" version = "0.1.4" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" [[package]] name = "unicode-script" version = "0.5.8" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" [[package]] name = "unicode-segmentation" @@ -6520,7 +7629,7 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-segmentation" version = "1.12.0" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" [[package]] name = "unicode-width" @@ -6610,6 +7719,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -6852,49 +7972,109 @@ dependencies = [ [[package]] name = "wayland-backend" version = "0.3.12" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "downcast-rs", "libc", "scoped-tls", "smallvec 1.15.1 (registry+https://github.com/rust-lang/crates.io-index)", - "wayland-sys", + "wayland-sys 0.31.8", +] + +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec 1.15.1 (registry+https://github.com/rust-lang/crates.io-index)", + "wayland-sys 0.31.11", ] [[package]] name = "wayland-client" version = "0.31.12" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc", - "wayland-backend", + "wayland-backend 0.3.12", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rustix", + "wayland-backend 0.3.15", + "wayland-scanner", ] [[package]] name = "wayland-egl" version = "0.32.9" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ - "wayland-backend", - "wayland-sys", + "wayland-backend 0.3.12", + "wayland-sys 0.31.8", ] [[package]] name = "wayland-protocols" version = "0.32.10" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", - "wayland-backend", - "wayland-client", + "wayland-backend 0.3.12", + "wayland-client 0.31.12", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" +dependencies = [ + "bitflags 2.10.0 (registry+https://github.com/rust-lang/crates.io-index)", + "wayland-backend 0.3.15", + "wayland-client 0.31.14", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", ] [[package]] name = "wayland-sys" version = "0.31.8" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" +dependencies = [ + "log", + "pkg-config", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" dependencies = [ + "dlib", "log", "pkg-config", ] @@ -6950,6 +8130,17 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "weezl" +version = "0.1.12" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" + [[package]] name = "whoami" version = "1.6.1" @@ -6972,7 +8163,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.1", ] [[package]] @@ -7001,7 +8192,7 @@ dependencies = [ [[package]] name = "windows" version = "0.62.2" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "windows-collections 0.3.2", "windows-core 0.62.2", @@ -7020,7 +8211,7 @@ dependencies = [ [[package]] name = "windows-collections" version = "0.3.2" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "windows-core 0.62.2", ] @@ -7053,7 +8244,7 @@ dependencies = [ [[package]] name = "windows-core" version = "0.62.2" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "windows-link 0.2.1", "windows-result 0.4.1", @@ -7074,7 +8265,7 @@ dependencies = [ [[package]] name = "windows-future" version = "0.3.2" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "windows-core 0.62.2", ] @@ -7138,7 +8329,7 @@ checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" [[package]] name = "windows-link" version = "0.2.1" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" [[package]] name = "windows-numerics" @@ -7182,7 +8373,7 @@ dependencies = [ [[package]] name = "windows-result" version = "0.4.1" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "windows-link 0.2.1", ] @@ -7199,7 +8390,7 @@ dependencies = [ [[package]] name = "windows-strings" version = "0.5.1" -source = "git+https://github.com/kevinaboos/makepad?branch=cargo_makepad_ndk_fix#5e6d7b3ff0f067572a16ce7fd6e6d57fc209a95b" +source = "git+https://github.com/makepad/makepad?branch=dev#3d18a137ca158d7b3b1437153bd5f5addf7f60f5" dependencies = [ "windows-link 0.2.1", ] @@ -7524,6 +8715,19 @@ name = "winnow" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +dependencies = [ + "memchr 2.7.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winresource" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087" +dependencies = [ + "toml", + "version_check", +] [[package]] name = "wit-bindgen" @@ -7637,12 +8841,28 @@ dependencies = [ "zeroize", ] +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "xxhash-rust" version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + [[package]] name = "yoke" version = "0.8.0" @@ -7667,6 +8887,67 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.1", + "winnow 1.0.1", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.106", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +dependencies = [ + "serde", + "winnow 1.0.1", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.8.27" @@ -7766,3 +9047,68 @@ name = "zmij" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32 0.3.9", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + +[[package]] +name = "zvariant" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" +dependencies = [ + "endi", + "enumflags2", + "serde", + "url", + "winnow 1.0.1", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.106", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.106", + "winnow 1.0.1", +] diff --git a/Cargo.toml b/Cargo.toml index 924a8448a..21243e050 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,23 +9,20 @@ keywords = ["matrix", "chat", "client", "robius", "makepad"] license = "MIT" readme = "README.md" categories = ["gui"] -repository = "https://github.com/project-robius/robrix" -version = "0.0.1-pre-alpha-4" +repository = "https://github.com/Project-Robius-China/robrix2" +version = "1.0.0-alpha.1" metadata.makepad-auto-version = "zqpv-Yj-K7WNVK2I8h5Okhho46Q=" [dependencies] -# makepad-widgets = { git = "https://github.com/makepad/makepad", branch = "dev", features = ["serde"] } -# makepad-code-editor = { git = "https://github.com/makepad/makepad", branch = "dev" } - -makepad-widgets = { git = "https://github.com/kevinaboos/makepad", branch = "cargo_makepad_ndk_fix", features = ["serde"] } -makepad-code-editor = { git = "https://github.com/kevinaboos/makepad", branch = "cargo_makepad_ndk_fix" } +makepad-widgets = { git = "https://github.com/makepad/makepad", branch = "dev", features = ["serde"] } +makepad-code-editor = { git = "https://github.com/makepad/makepad", branch = "dev" } ## Including this crate automatically configures all `robius-*` crates to work with Makepad. robius-use-makepad = "0.1.1" -robius-open = { git = "https://github.com/project-robius/robius" } -robius-directories = { git = "https://github.com/project-robius/robius" } -robius-location = { git = "https://github.com/project-robius/robius" } +robius-open = { git = "https://github.com/Project-Robius-China/robius2.git" } +robius-directories = { git = "https://github.com/Project-Robius-China/robius2.git" } +robius-location = { git = "https://github.com/Project-Robius-China/robius2.git" } anyhow = "1.0" @@ -43,8 +40,11 @@ futures-util = "0.3" hashbrown = { version = "0.16", features = ["raw-entry"] } htmlize = "1.0.5" indexmap = "2.6.0" +image = "0.25" imghdr = "0.7.0" linkify = "0.10.0" +mime = "0.3" +mime_guess = "2.0" matrix-sdk-base = { git = "https://github.com/project-robius/matrix-rust-sdk", branch = "space_room_suggested" } matrix-sdk = { git = "https://github.com/project-robius/matrix-rust-sdk", branch = "space_room_suggested", default-features = false, features = [ "e2e-encryption", @@ -72,12 +72,18 @@ rangemap = "1.5.0" sanitize-filename = "0.6" serde = "1.0" serde_json = "1.0" +symphonia = { version = "0.5", default-features = false, features = ["mp3", "wav", "aiff", "flac", "isomp4", "alac", "pcm"] } thiserror = "2.0.16" tokio = { version = "1.43.1", features = ["macros", "rt-multi-thread"] } tracing-subscriber = "0.3.17" unicode-segmentation = "1.11.0" +percent-encoding = "2.3" url = "2.5.0" +[target.'cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))'.dependencies] +rfd = "0.15" +cargo-packager-updater = "0.2" +semver = "1" ## Dependencies for TSP support. ## Commit "f0bc4625dcd729e07e4a36257df2f1d94c81cef4" is the most recent one without the invalid change to pin serde to 1.0.219. @@ -91,7 +97,7 @@ quinn = { version = "0.11", default-features = false, optional = true } ## We only include this such that we can specify the prebuilt-nasm feature, ## which is required to build this on Windows x86_64 without having to install NASM separately. aws-lc-rs = { version = "1.13", optional = true, features = ["prebuilt-nasm"] } -percent-encoding = { version = "2.3", optional = true } +percent-encoding = "2.3" ## The following reqwest 0.12 features were taken from the tsp_sdk's `Cargo.toml` file. ## This is only needed for the optional TSP feature; the matrix-sdk uses reqwest 0.13 internally, ## which is re-exported as `matrix_sdk::reqwest` and used by regular (non-TSP) Robrix code. @@ -108,7 +114,13 @@ reqwest = { version = "0.12", default-features = false, optional = true, feature [features] default = [] ## Enables experimental support for using TSP wallets. -tsp = ["dep:tsp_sdk", "dep:quinn", "dep:aws-lc-rs", "dep:percent-encoding", "dep:reqwest"] +tsp = ["dep:tsp_sdk", "dep:quinn", "dep:aws-lc-rs", "dep:reqwest"] + +## Enables experimental remote agent-chat support (the `/create-issue`, `/go`, +## `/review`, `/status` workflow slash-commands shown in rooms that contain a +## `*_coordinator` agent). Off by default; even when compiled in, it must also +## be turned on at runtime via Settings → Preferences → "Enable agent-chat support". +agent_chat = [] ## Hides the command prompt console on Windows. hide_windows_console = [] @@ -159,10 +171,21 @@ askar-storage = { git = "https://github.com/openwallet-foundation/askar.git" } [patch."https://github.com/ruma/ruma"] ruma = { git = "https://github.com/project-robius/ruma.git", branch = "tsp" } +[build-dependencies] +toml = "1" + +## On Windows, use the `winresource` crate to embed the app's icon and metadata into the executable. +[target.'cfg(windows)'.build-dependencies] +winresource = "0.1" + [package.metadata.docs.rs] all-features = true +[profile.release] +debug = false +strip = "symbols" + ## An optimized profile for development, with full debug info and assertions. [profile.debug-opt] @@ -191,18 +214,26 @@ strip = true debug-assertions = false +## Configuration for cargo makepad (`cargo makepad android build-aab`). +## The `[package.metadata.packager]` section below already sets the package ID +## and product name, so we just need a version code for Google Play Store builds. +[package.metadata.makepad.android] +## Specifying "auto" will set it to a format like 'YYYYMMDDHH'. +version_code = "auto" + + ## Configuration for `cargo packager` [package.metadata.packager] product_name = "Robrix" -identifier = "org.robius.robrix" +identifier = "rs.robius.robrix" category = "SocialNetworking" authors = [ "Project Robius ", "Kevin Boos ", ] -publisher = "robius" +publisher = "GOSIM Foundation" license_file = "LICENSE-MIT" -copyright = "Copyright 2023-202, Project Robius" +copyright = "Copyright 2023-2026, Project Robius" homepage = "https://github.com/project-robius" ### Note: there is an 80-character max for each line of the `long_description`. long_description = """ @@ -213,7 +244,15 @@ and the Project Robius app dev framework and platform abstractions Robrix runs on all major desktop and mobile platforms: macOS, Windows, Linux, Android, and iOS. """ -icons = ["./packaging/robrix_logo_alpha.png"] +icons = [ + "./resources/icon_32.png", + "./resources/icon_48.png", + "./resources/icon_64.png", + "./resources/icon_128.png", + "./resources/icon_256.png", + "./resources/icon_512.png", + "./resources/icon.ico", +] out_dir = "./dist" ## Here, we define the list of resource directories for both Makepad and Robrix. @@ -285,6 +324,8 @@ non_local_definitions = "forbid" unsafe_op_in_unsafe_fn = "forbid" unnameable_types = "warn" unused_import_braces = "warn" +unused = { level = "deny", priority = -1 } +dead_code = "deny" ## Configuration for clippy lints. [lints.clippy] diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 000000000..cc5628b9a --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,115 @@ +# Robrix Design Document + +## Overview + +Robrix is a multi-platform Matrix chat client written in pure Rust using the Makepad UI framework and Project Robius application development framework. It targets macOS, Windows, Linux, Android, iOS, and OpenHarmony. + +## Architecture + +### Three-Layer Architecture + +``` ++─────────────────────────────────────────────────────+ +│ UI Layer │ +│ Makepad script_mod! DSL, Widgets, MatchEvent │ +│ (app.rs, home/, shared/, room/, login/, settings/) │ ++─────────────────────────────────────────────────────+ + │ + Actions / Scope + │ ++─────────────────────────────────────────────────────+ +│ Matrix Protocol Layer │ +│ sliding_sync.rs — async client, auth, timelines │ +│ space_service_sync.rs — space hierarchy │ +│ submit_async_request() → tokio background tasks │ ++─────────────────────────────────────────────────────+ + │ + Cx::post_action / MPSC + │ ++─────────────────────────────────────────────────────+ +│ Persistence & Cache Layer │ +│ persistence/ — session, app state, window geometry │ +│ avatar_cache, media_cache, user_profile_cache │ +│ account_manager — multi-account switching │ ++─────────────────────────────────────────────────────+ +``` + +### Key Components + +| Component | File(s) | Responsibility | +|-----------|---------|----------------| +| App | `app.rs` | Root state, event dispatch, modal management | +| Sliding Sync | `sliding_sync.rs` | Matrix client lifecycle, room sync, timeline subscriptions | +| Room Screen | `home/room_screen.rs` | Timeline rendering, message display, pagination | +| Rooms List | `home/rooms_list.rs` | Room list with categories (invited, direct, regular) | +| Room Input Bar | `room/room_input_bar.rs` | Message composition, replies, mentions | +| Mentionable Text Input | `shared/mentionable_text_input.rs` | @mention autocomplete with background search | +| Command Text Input | `shared/command_text_input.rs` | Generic popup/autocomplete infrastructure | +| HTML/Plaintext | `shared/html_or_plaintext.rs` | Message rendering with Matrix HTML support | + +### Technology Stack + +- **UI Framework**: Makepad 2.0 (`script_mod!` DSL, `Script`/`ScriptHook` derives) +- **Matrix SDK**: `matrix-sdk` with sliding sync, E2E encryption, SQLite storage +- **Async Runtime**: Tokio +- **Serialization**: Serde (JSON for persistence, RON for legacy) + +### UI Patterns (Makepad 2.0) + +- **Widget DSL**: `script_mod!` blocks define widget trees with Splash syntax +- **Named children**: Use `:=` operator (NOT `=`) for addressable widgets +- **Property merge**: Use `+:` to extend inherited properties, `:` to replace +- **Event flow**: `handle_event` → `MatchEvent::handle_actions` → widget action queries +- **State changes on dynamic widgets**: Use Animator + shader instance variables (NOT `script_apply_eval!` which fails on `widget_ref_from_live_ptr()` widgets due to `ScriptObject::ZERO`) +- **Runtime property limits**: `script_apply_eval!` cannot use DSL constants (`Right`, `Fit`, `Align`) — bake into templates or use `#(rust_expr)` interpolation + +### Async Communication Pattern + +``` +UI Thread Background Thread + │ │ + ├── submit_async_request() ──────────►│ MatrixRequest::* + │ │ + │◄── Cx::post_action() ──────────────┤ Result action + │ │ + ├── handle_actions() processes result │ +``` + +### Persistence + +- Session data: `~/.local/share/org.robius.robrix//persistent_state/` +- App state: `latest_app_state.json` (dock layout, open rooms, selected room) +- Window geometry: `window_geom_state.json` + +## Module Organization + +``` +src/ +├── app.rs # Root app, modals, global state +├── sliding_sync.rs # Matrix client, sync, requests +├── space_service_sync.rs # Space hierarchy +├── cpu_worker.rs # Background CPU tasks +├── home/ # Main UI screens +│ ├── room_screen.rs # Timeline + message display +│ ├── rooms_list.rs # Room list sidebar +│ ├── main_desktop_ui.rs # Desktop dock layout +│ ├── home_screen.rs # Adaptive desktop/mobile +│ └── ... +├── shared/ # Reusable widgets +│ ├── mentionable_text_input.rs # @mention system +│ ├── command_text_input.rs # Popup autocomplete +│ ├── html_or_plaintext.rs # Message rendering +│ ├── avatar.rs # Avatar display +│ └── ... +├── room/ # Room-specific logic +│ ├── room_input_bar.rs # Message input +│ ├── member_search.rs # Member search algorithm +│ └── ... +├── login/ # Authentication +├── logout/ # Session cleanup +├── settings/ # User preferences +├── persistence/ # State storage +├── profile/ # User profiles +├── i18n.rs # Internationalization +└── utils.rs # Shared utilities +``` diff --git a/MAKEPAD.md b/MAKEPAD.md new file mode 100644 index 000000000..f5d40ad5e --- /dev/null +++ b/MAKEPAD.md @@ -0,0 +1,136 @@ +# Makepad 2.0 Skills - Claude Instructions + +## Design Judgment Anchors (Liberation Layer) + +These concept anchors provide design judgment for Makepad 2.0 architecture questions. Use them when facing "how should I organize state / split components / handle complex interactions" — questions without a single correct answer. The specific DSL, API, and widget patterns come from the compliance-layer skills below; this section is the liberation layer. + +### Data Flow +Reference **Elm Architecture** (Evan Czaplicki): + +- State is centralized, UI is a projection of state, events trigger updates +- Makepad's event handlers are Elm's `update` function +- If you find state scattered across components that need to observe each other — stop, lift the state to a common ancestor + +### Component Decomposition +Reference **Dan Abramov**'s presentational vs container distinction: + +- Presentational components: receive props, hold no state, no side effects +- Container components: hold state, handle events, coordinate children +- Use Makepad's delegation patterns to separate Widget rendering from business logic + +### Rendering Mindset +Reference **Casey Muratori** (Handmade Hero): + +- This is not a DOM, it's a GPU-rendered frame every tick +- Don't think "mutate the node", think "what does the next frame look like" +- `redraw(cx)` is not "mark node dirty" — it's "tell the GPU to repaint this region next frame" + +### Layout +Reference **CSS Flexbox** as a mental model (but simpler): + +- `Flow.Down` = flex-direction: column +- `Flow.Right` = flex-direction: row +- `align`, `spacing`, `padding`, `margin` — same semantics as CSS +- Difference: Makepad has no CSS cascade or inheritance. Each component's style is self-contained — **this is a feature, not a bug** + +### Animation and Shaders +Reference the **Shadertoy** community's "everything is math" mindset: + +- Makepad shader fields contain real GPU shader code, not CSS-equivalents +- `Sdf2d` is a signed distance field — describe shapes with math, not bitmaps +- Animation is a shader uniform changing over time, not a CSS transition +- When you want a rounded button, the answer is an SDF function, not `border-radius` + +### Cross-Platform Philosophy +Reference **Flutter**'s "own every pixel" philosophy: + +- Makepad draws everything itself, does not use native platform controls +- Benefit: pixel-perfect cross-platform consistency +- Cost: accessibility support is a known weakness +- Don't try to mimic native control appearance — embrace Makepad's own design language + +### When Anchors Conflict with Compliance-Layer Skills + +If a design judgment from these anchors contradicts the actual Makepad 2.0 API documented in a compliance-layer skill, **the compliance-layer skill wins**. Those skills are the external reality. Anchors help you navigate within that reality, not override it. + +--- + +## Entry Point + +**For ALL Makepad questions, FIRST load `makepad-2.0-design-judgment`.** +This is the liberation layer — it provides design judgment anchors and routes +to the correct compliance-layer skill. Then co-load the specific skill below. + +## Skill Routing + +For Makepad 2.0 questions, route based on keywords: + +| Keywords | Skill | +|----------|-------| +| architecture, design, "how should I", component split, state management | makepad-2.0-design-judgment | +| getting started, app structure, `app_main!`, `ScriptVm`, Cargo setup | makepad-2.0-app-structure | +| DSL syntax, `script_mod!`, property, colon syntax, `mod.widgets` | makepad-2.0-dsl | +| layout, width, height, Flow, Fill, Fit, Inset, spacing, align | makepad-2.0-layout | +| View, Button, Label, TextInput, PortalList, Dock, Modal, widget | makepad-2.0-widgets | +| event, action, `handle_event`, `on_click`, `on_render`, Hit, ids! | makepad-2.0-events | +| animation, animator, state, transition, Forward, Snap, Loop | makepad-2.0-animation | +| shader, `draw_bg`, Sdf2d, GPU, pixel fn, vertex fn, DrawQuad | makepad-2.0-shaders | +| splash, script, `script_mod!`, hot reload, streaming evaluation | makepad-2.0-splash | +| theme, color, font, dark mode, light mode, `mod.themes` | makepad-2.0-theme | +| vector, SVG, path, gradient, tween, DropShadow, Group transform | makepad-2.0-vector | +| performance, debug, profiling, GC, `new_batch`, ViewOptimize | makepad-2.0-performance | +| troubleshooting, error, bug, widget not showing, text invisible | makepad-2.0-troubleshooting | +| migration, 1.x to 2.0, `live_design` to `script_mod`, upgrade | makepad-2.0-migration | + +## Usage Examples + +### App Structure +``` +User: "How do I create a Makepad 2.0 app?" +-> Load: makepad-2.0-app-structure +-> Answer with app_main!, ScriptVm, from_script_mod, MatchEvent +``` + +### DSL / Splash +``` +User: "How does the new Makepad DSL work?" +-> Load: makepad-2.0-dsl +-> Answer with script_mod!, colon syntax, mod.widgets, let bindings +``` + +### Layout +``` +User: "How do I center a widget in Makepad 2.0?" +-> Load: makepad-2.0-layout +-> Answer with Flow.Down, align, Fill, Fit +``` + +### Migration +``` +User: "How do I migrate from Makepad 1.x to 2.0?" +-> Load: makepad-2.0-migration +-> Answer with live_design→script_mod, LiveHook→ScriptHook changes +``` + +## Default Project Settings + +When creating Makepad 2.0 projects: + +```toml +[package] +edition = "2024" + +[dependencies] +makepad-widgets = { git = "https://github.com/makepad/makepad", branch = "dev" } + +[features] +default = [] +nightly = ["makepad-widgets/nightly"] +``` + +## Legacy + +Makepad 1.x skills (including Robius and MolyKit patterns) are archived on the `v1/makepad-1.0` branch. + +## Source +- **Makepad**: https://github.com/makepad/makepad diff --git a/README.md b/README.md index cb1c8d6ca..c651a7c9b 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,9 @@ Robrix is a Matrix chat client written in Rust to exemplify the features of [Project Robius](https://github.com/project-robius), a framework for multi-platform application development in Rust. Robrix is written using the [Makepad UI toolkit](https://github.com/makepad/makepad/). +> [!TIP] +> **Want to deploy Palpo and Octos, then use Robrix to chat with an AI bot?** Check out the [Documentation](docs/README.md) for guides on running [Palpo](https://github.com/palpo-im/palpo) (Matrix homeserver) + [Octos](https://github.com/octos-org/octos) (AI bot), understanding the App Service architecture, using Robrix as the client, and enabling federation. Quick links: [Deployment](docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md) · [Architecture](docs/robrix-with-palpo-and-octos/02-how-robrix-palpo-octos-work-together.md) · [Usage](docs/robrix-with-palpo-and-octos/03-using-robrix-with-palpo-and-octos.md) · [Federation](docs/robrix-with-palpo-and-octos/04-federation-with-palpo.md) + Check out our most recent talks and presentations for more info: * Robrix: a complex, multi-platform app in Rust for secure chat using Matrix ([Rust China Conf 2025](https://rustcc.cn/2025conf/schedule.html)) * Videos: [YouTube link](https://www.youtube.com/watch?v=kB-JdmG5kE4), [BiliBili Link](https://www.bilibili.com/video/BV1XJnjzKEZQ) @@ -71,6 +74,17 @@ The following table shows which host systems can currently be used to build Robr cargo run --release ``` +> [!TIP] +> If you get a build error from `aws-lc-sys` about a **"COMPILER BUG DETECTED"** related to `memcmp` +> ([GCC #95189](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=95189)), +> your GCC version is too old (GCC 9 and earlier are affected). +> The easiest fix is to build using `clang` instead: +> ```sh +> CC=clang CXX=clang++ cargo run --release +> ``` +> Alternatively, upgrade GCC to version 10 or newer. + + ## Building & Running Robrix on Mobile: Android, iOS, iPadOS 1. Install the `cargo-makepad` build tool: @@ -119,6 +133,12 @@ The following table shows which host systems can currently be used to build Robr --app=robrix \ run-sim -p robrix --release ``` +> [!TIP] +> If you get errors from the simulator, update your simulator tooling: +> ```sh +> xcodebuild -downloadPlatform iOS +> ``` + #### Running on a real iOS device 4. Run the following command to show all provisioning profiles, signing identities, and device identifiers on your Mac. @@ -134,11 +154,21 @@ The following table shows which host systems can currently be used to build Robr   --profile= \   --cert= \ --device= \ -   --org=rs.robius \ +   --org=rs.robius \ --app=robrix \ run-device -p robrix –release ``` +#### Add iOS AppIcon assets to the built `.app` bundle +5. After building the iOS app bundle, compile and apply the AppIcon asset catalog: + ```sh + ./packaging/ios/apply_ios_app_icons.sh \ + ./target/makepad-apple-app/aarch64-apple-ios/release/robrix.app \ + 1 + ``` + * This step adds `Assets.car` and required icon metadata into `Info.plist`. + * If you already signed the app before this step, you must re-sign it afterwards. + # Feature status tracker These are generally sorted in order of priority. If you're interested in helping out with anything here, please reach out via a GitHub issue or on our Robius matrix channel. diff --git a/SPLASH.md b/SPLASH.md index 88af44bba..3c67bb28c 100644 --- a/SPLASH.md +++ b/SPLASH.md @@ -1,7 +1,7 @@ # Splash Script Manual (Terse AI Reference) -Splash is Makepad's UI scripting language. It is whitespace-delimited, but Robrix prefers either newlines or commas to separate properties, for readability's sake. -**Please always use newlines or commas to separate properties, not just whitespace.** +Splash is Makepad's UI scripting language. It is whitespace-delimited, but Robrix uses commas or newlines to separate properties for readability (commas are treated as whitespace by the tokenizer). +**Please use commas or newlines to separate properties, matching the surrounding code style.** **Do NOT use `Root{}` or `Window{}`** — those are host-level wrappers handled externally. Your output is the content inside a body/splash widget. @@ -1552,6 +1552,102 @@ Pow {begin: 0.0, end: 1.0} Bezier {cp0: 0.0, cp1: 0.0, cp2: 1.0, cp3: 1.0} ``` +## Runtime Property Updates (`script_apply_eval!`) + +From Rust code, use `script_apply_eval!` to patch widget properties at runtime: + +```rust +let color = vec4(1.0, 0.0, 0.0, 1.0); +let height = 36.0_f64; +script_apply_eval!(cx, my_widget, { + draw_bg +: { color: #(color) } + height: #(height) +}); +``` + +Use `#(rust_expr)` to interpolate Rust values into the script. + +### What works in `script_apply_eval!` +- Numeric values: `height: #(h)`, `spacing: 10` +- Hex colors: `draw_bg +: { color: #ff0000 }` +- Rust expression interpolation: `#(my_vec4_variable)` +- Property paths: `draw_bg +: { ... }`, `draw_text +: { ... }` + +### What does NOT work in `script_apply_eval!` + +**DSL constants are NOT available at runtime:** +```rust +// WRONG — these all fail with "variable not found in scope" +script_apply_eval!(cx, item, { + flow: Right // ❌ Right not found + height: Fit // ❌ Fit not found + align: Align{y: 0.5} // ❌ Align not found + cursor: MouseCursor.Hand // ❌ MouseCursor not found +}); + +// CORRECT — bake layout into DSL template, or use numeric values +script_apply_eval!(cx, item, { + height: #(36.0_f64) // ✅ numeric value via interpolation + draw_bg +: { color: #(c) } // ✅ color via interpolation +}); +``` + +**Dynamic widgets ignore `script_apply_eval!` entirely:** + +Widgets created via `widget_ref_from_live_ptr()` (or `cx.with_vm(|vm| WidgetRef::script_from_value(...))`) have `ScriptObject::ZERO` as their script source. All `script_apply_eval!` calls on them silently do nothing. + +To change visual state on dynamic widgets, use **Animator + shader instance variables**: + +``` +// In DSL template definition: +mod.widgets.MyItem = View { + show_bg: true + draw_bg +: { + color: #fff + selected: instance(0.0) + pixel: fn() { + let sdf = Sdf2d.viewport(self.pos * self.rect_size) + sdf.box(0. 0. self.rect_size.x self.rect_size.y 4.0) + let highlight = #x1E90FF30 + sdf.fill(Pal.premul(self.color.mix(highlight self.selected))) + return sdf.result + } + } + animator: Animator { + highlight: { + default: @off + off: AnimatorState { + from: { all: Forward { duration: 0.12 } } + apply: { draw_bg: { selected: 0.0 } } + } + on: AnimatorState { + from: { all: Forward { duration: 0.08 } } + apply: { draw_bg: { selected: 1.0 } } + } + } + } +} +``` + +```rust +// In Rust — toggle state via Animator (works on ALL widgets including dynamic ones): +let view = item.as_view(); +view.animator_cut(cx, ids!(highlight.on)); // highlight on +view.animator_cut(cx, ids!(highlight.off)); // highlight off +``` + +### `draw_bg:` vs `draw_bg +:` + +**Always use `+:` when modifying sub-properties:** +``` +draw_bg +: { color: #f00 } // ✅ merges — keeps border_radius, shader, etc. +draw_bg: { color: #f00 } // ❌ replaces — loses ALL other draw_bg properties +``` + +This applies in both DSL definitions and `script_apply_eval!`. + +--- + ## Theme Variables (prefix: `theme.`) ### Spacing diff --git a/build.rs b/build.rs new file mode 100644 index 000000000..e089da0bf --- /dev/null +++ b/build.rs @@ -0,0 +1,118 @@ +fn main() { + // Note: `#[cfg(windows)]` checks the *host* OS, not the *target*. + // We must check the target env at runtime to avoid running this + // when cross-compiling (e.g., building for Android on a Windows CI runner). + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + if target_os == "windows" { + #[cfg(windows)] + { + let mut res = winresource::WindowsResource::new(); + res.set_icon("resources/icon.ico"); + // Explicit VERSIONINFO fields. Without these, Windows shows + // "Unknown publisher" in the UAC/SmartScreen install prompt + // (CompanyName/LegalCopyright are empty by default), and the + // ProductName/FileDescription fall back to the lowercase crate + // name "robrix" instead of the product name. + res.set("CompanyName", "GOSIM Foundation"); + res.set("ProductName", "Robrix"); + res.set("FileDescription", "Robrix - Matrix chat client"); + res.set("LegalCopyright", "Copyright - 2023-2026 Project Robius"); + res.compile().expect("Failed to compile Windows resources"); + } + } + + // Get version info about Robrix, the matrix SDK, and testflight. + println!("cargo:rerun-if-changed=Cargo.lock"); + let (sdk_version, sdk_git_rev, sdk_url) = read_matrix_sdk_info(); + println!("cargo:rustc-env=MATRIX_SDK_VERSION={sdk_version}"); + println!("cargo:rustc-env=MATRIX_SDK_GIT_REV={sdk_git_rev}"); + println!("cargo:rustc-env=MATRIX_SDK_URL={sdk_url}"); + + let (robrix_git_rev, robrix_url) = read_robrix_git_info(); + println!("cargo:rustc-env=ROBRIX_GIT_COMMIT_HASH={robrix_git_rev}"); + println!("cargo:rustc-env=ROBRIX_GIT_COMMIT_URL={robrix_url}"); + + println!("cargo:rerun-if-env-changed=TESTFLIGHT_BUILD_NUMBER"); + let testflight_build = std::env::var("TESTFLIGHT_BUILD_NUMBER").unwrap_or_default(); + println!("cargo:rustc-env=TESTFLIGHT_BUILD_NUMBER={testflight_build}"); +} + +/// Returns Robrix's own current git commit info as a commit hash and a permalink. +fn read_robrix_git_info() -> (String, String) { + // Tell cargo to re-run when the git-tracked HEAD changes. + println!("cargo:rerun-if-changed=.git/HEAD"); + if let Ok(head) = std::fs::read_to_string(".git/HEAD") { + if let Some(branch_ref) = head.trim().strip_prefix("ref: ") { + println!("cargo:rerun-if-changed=.git/{branch_ref}"); + } + } + + let Ok(output) = std::process::Command::new("git") + .args(["rev-parse", "HEAD"]) + .output() + else { + return (String::new(), String::new()); + }; + if !output.status.success() { + return (String::new(), String::new()); + } + let full_sha = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if full_sha.len() < 8 { + return (String::new(), String::new()); + } + let short_rev: String = full_sha.chars().take(8).collect(); + let url = format!("https://github.com/project-robius/robrix/tree/{full_sha}"); + (short_rev, url) +} + +/// Parses Cargo.lock to find the resolved version of `matrix-sdk`. +/// +/// Returns `(version, short_git_rev, url)`. +fn read_matrix_sdk_info() -> (String, String, String) { + let Ok(lockfile_text) = std::fs::read_to_string("Cargo.lock") else { + return (String::new(), String::new(), String::new()); + }; + let Ok(lockfile) = toml::from_str::(&lockfile_text) else { + return (String::new(), String::new(), String::new()); + }; + + let Some(pkg) = lockfile + .get("package") + .and_then(|p| p.as_array()) + .and_then(|pkgs| { + pkgs.iter().find(|p| { + p.get("name").and_then(|n| n.as_str()) == Some("matrix-sdk") + }) + }) + else { + return (String::new(), String::new(), String::new()); + }; + + let version = pkg + .get("version") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let source = pkg.get("source").and_then(|s| s.as_str()).unwrap_or(""); + + // Git sources look like `git+?#`. + // The repo URL is the prefix before `?` or `#`; the commit is after `#`. + let (git_rev, url) = if let Some(rest) = source.strip_prefix("git+") { + let (left, full_commit) = rest.rsplit_once('#').unwrap_or((rest, "")); + let base = left.split_once('?').map_or(left, |(b, _)| b); + let short_rev: String = full_commit.chars().take(8).collect(); + let url = if full_commit.is_empty() { + base.to_string() + } else { + format!("{base}/tree/{full_commit}") + }; + (short_rev, url) + } else if !version.is_empty() { + // Registry/path/other sources: fall back to the crates.io URL. + (String::new(), format!("https://crates.io/crates/matrix-sdk/{version}")) + } else { + (String::new(), String::new()) + }; + + (version, git_rev, url) +} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..66cbbcef0 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,102 @@ +# Robrix Documentation + +Welcome to the Robrix documentation. Choose a guide based on your use case. + +--- + +## Robrix Only + +For users who want to use Robrix as a standalone Matrix client, connecting to matrix.org or any existing homeserver: + +| Guide | Goal | +|-------|------| +| [Getting Started with Robrix](robrix/getting-started-with-robrix.md) | **Install Robrix and start chatting.** Download or build Robrix, connect to a Matrix server, register an account, and join rooms. | + +> Chinese: [Robrix 快速开始](robrix/getting-started-with-robrix-zh.md) + +--- + +## Robrix + Palpo + Octos (AI Bot System) + +For users who want to deploy a complete AI chat system — running your own Matrix homeserver with AI bot capabilities, then using Robrix to chat with AI bots. + +**Default path is the 3-step lightweight mode** — Palpo + PostgreSQL run in Docker, Octos runs as a native host process, and you only edit `.env` between two scripts: + +```sh +./setup.sh # clones Palpo source + downloads Octos bundle +$EDITOR .env # set DEEPSEEK_API_KEY +./start.sh # Palpo (Docker) + Octos (native) +``` + +| Guide | Goal | +|-------|------| +| [1. Deploying Palpo and Octos](robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md) | **Get Palpo homeserver and Octos AI bot running (3-step lightweight mode).** `./setup.sh` downloads the upstream Octos bundle and clones Palpo source; edit `.env`; `./start.sh` brings up Palpo + PostgreSQL in Docker and Octos as a native process. | +| [2. Using Robrix with Palpo and Octos](robrix-with-palpo-and-octos/02-using-robrix-with-palpo-and-octos.md) | **Use Robrix to chat with AI bots on your Palpo server.** Step-by-step with screenshots: log in, create rooms, invite bots, have conversations, and manage bots through the BotFather system. | +| [3. How Robrix, Palpo, and Octos Work Together](robrix-with-palpo-and-octos/03-how-robrix-palpo-octos-work-together.md) | **Understand the Application Service mechanism.** Learn how Octos registers as a Matrix App Service on Palpo, how messages flow from Robrix through Palpo to the AI bot, and how the BotFather system manages multiple bots. | +| [4. Federation with Palpo](robrix-with-palpo-and-octos/04-federation-with-palpo.md) | **Enable cross-server communication.** Configure Palpo for Matrix federation so users on different servers can chat with each other and access your AI bots. | + +> Chinese: +> [1. 部署 Palpo 和 Octos](robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md) · +> [2. 在 Robrix 上使用 Palpo 和 Octos](robrix-with-palpo-and-octos/02-using-robrix-with-palpo-and-octos-zh.md) · +> [3. Robrix、Palpo、Octos 协作原理](robrix-with-palpo-and-octos/03-how-robrix-palpo-octos-work-together-zh.md) · +> [4. Palpo 联邦功能](robrix-with-palpo-and-octos/04-federation-with-palpo-zh.md) + +--- + +## Robrix + OpenClaw (AI Agent Framework) + +For users who want to connect OpenClaw AI agents to Matrix, then use Robrix to chat with them: + +| Guide | Goal | +|-------|------| +| [1. Deploying OpenClaw with Matrix](robrix-with-openclaw/01-deploying-openclaw-with-matrix.md) | **Get OpenClaw connected to a Matrix homeserver.** Create a bot account, configure the Matrix channel plugin, and verify the connection so Robrix can chat with OpenClaw agents. | +| [2. Using Robrix with OpenClaw](robrix-with-openclaw/02-using-robrix-with-openclaw.md) | **Use Robrix to chat with OpenClaw agents.** Start conversations via DM or rooms, understand feature compatibility, and learn the differences from the Octos workflow. | +| [3. How Robrix and OpenClaw Work Together](robrix-with-openclaw/03-how-robrix-and-openclaw-work-together.md) | **Understand the client-based integration model.** Learn how OpenClaw connects to Matrix as a regular client (vs. Octos's Appservice model), how messages flow, and how E2EE works. | + +> Chinese: +> [1. 部署 OpenClaw + Matrix](robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md) · +> [2. 在 Robrix 上使用 OpenClaw](robrix-with-openclaw/02-using-robrix-with-openclaw-zh.md) · +> [3. Robrix 与 OpenClaw 协作原理](robrix-with-openclaw/03-how-robrix-and-openclaw-work-together-zh.md) + +--- + +## Robrix + Hermes (AI Agent Framework) + +For users who want to connect [Hermes Agent](https://github.com/NousResearch/Hermes-Agent) to Matrix, then use Robrix to chat with it: + +| Guide | Goal | +|-------|------| +| [1. Deploying Hermes with Matrix](robrix-with-hermes/01-deploying-hermes-with-matrix.md) | **Get Hermes Agent connected to a Matrix homeserver.** Install Hermes, wire up an LLM, log the bot in as a regular Matrix user, and verify end-to-end chat from Robrix. Also covers the local-Palpo `server_name` gotcha and the `mautrix[encryption]` install snag. | + +> **Usage and architecture:** Hermes and OpenClaw share the same "AI agent as a regular Matrix client" integration model, so from Robrix's side the usage UX is identical and the architecture is the same. For those topics see the OpenClaw guides: [2. Using Robrix with OpenClaw](robrix-with-openclaw/02-using-robrix-with-openclaw.md) and [3. How Robrix and OpenClaw Work Together](robrix-with-openclaw/03-how-robrix-and-openclaw-work-together.md). + +> Chinese: +> [1. 部署 Hermes + Matrix](robrix-with-hermes/01-deploying-hermes-with-matrix-zh.md) · 使用方式与架构原理同 OpenClaw:[2. 在 Robrix 上使用 OpenClaw](robrix-with-openclaw/02-using-robrix-with-openclaw-zh.md) · [3. Robrix 与 OpenClaw 协作原理](robrix-with-openclaw/03-how-robrix-and-openclaw-work-together-zh.md) + +--- + +## Palpo and Octos Deployment Files + +The [`palpo-and-octos-deploy/`](../palpo-and-octos-deploy/) directory (at the repository root) holds the runnable lightweight-mode deployment — see its [README](../palpo-and-octos-deploy/README.md) for the 3-step Quickstart. + +``` +palpo-and-octos-deploy/ +├── README.md # 3-step Quickstart, supported platforms, disk budget +├── setup.sh # Auto-detects platform, clones Palpo, installs Octos bundle +├── start.sh # Lifecycle: start / stop / restart / status / logs +├── compose.yml # Palpo + PostgreSQL (Octos runs natively, not in compose) +├── palpo.Dockerfile # Palpo source build (unified cross-platform) +├── palpo.toml # Palpo homeserver config +├── .env.example # Environment variables template (DEEPSEEK_API_KEY, etc.) +├── appservices/ +│ └── octos-registration.yaml # Appservice registration (Palpo ↔ Octos, via host.docker.internal) +├── config/ +│ ├── botfather.json # Bot profile and LLM settings +│ └── octos.json # Octos global settings +├── octos-bin/ # Native Octos binary (installed by setup.sh, gitignored) +├── repos/ # Palpo source (cloned by setup.sh, gitignored) +├── logs/ # Octos stdout/stderr (gitignored) +└── data/ # Runtime data — Postgres + Palpo media (gitignored) +``` + +For always-on Octos with systemd / launchd / frpc tunnel, see Octos upstream docs: . diff --git a/docs/agent-chat-feature-flag.md b/docs/agent-chat-feature-flag.md new file mode 100644 index 000000000..703df85df --- /dev/null +++ b/docs/agent-chat-feature-flag.md @@ -0,0 +1,84 @@ +# Agent-chat workflow support — enabling & local verification + +Robrix can show **agent-chat workflow slash-commands** (`/create-issue`, `/go`, +`/review`, `/status`) in rooms that contain a coordinator agent (a member whose +name is `coordinator` or ends with `_coordinator`, e.g. `wf_coordinator`, +`alpha_coordinator`). These let you drive an [agent-chat](https://github.com/) AI +agent team through an `issue → spec → plan → implement → review` workflow from a +normal Matrix chat. + +The feature is **double-gated and off by default**, so default Robrix builds are +unchanged: + +1. **Compile-time** — Cargo feature `agent_chat`. Without it, none of the + workflow-command code is compiled in. +2. **Runtime** — a persisted toggle in **Settings → Preferences → + "Agent-chat support (experimental)"** (off by default), only shown in + `agent_chat`-feature builds. + +The commands only appear when **both** gates are on **and** the room contains a +`*_coordinator` member. + +--- + +## Build + +```bash +# Default build — feature OFF (no agent-chat workflow support compiled in): +cargo run + +# Agent-chat build — feature ON: +cargo run --features agent_chat +# release: +cargo build --release --features agent_chat +``` + +## Verify — lightweight (no agent-chat stack needed) + +These check the gating itself; they only need a Matrix login + one room whose +member list contains a `*_coordinator`-named user. + +| # | Build | Action | Expected | +|---|-------|--------|----------| +| 1 | `cargo run` (feature OFF) | open **Settings → Preferences** | **No** "Agent-chat support" toggle | +| 2 | `cargo run` (feature OFF) | in any room, type `/` | workflow commands **never** appear (upstream-native behavior) | +| 3 | `cargo run --features agent_chat` | open **Settings → Preferences** | "Agent-chat support (experimental)" toggle present, **off** by default | +| 4 | feature ON, toggle **off** | in a room with a `*_coordinator` member, type `/` | workflow commands **do not** appear | +| 5 | feature ON, toggle **on** | same room, type `/` | popup shows **`/create-issue` `/go` `/review` `/status`** under a "Workflow Commands" header | +| 6 | feature ON, toggle **on** | a room **without** any `*_coordinator` member, type `/` | workflow commands **do not** appear | + +The toggle state persists across restarts (stored in `AppPreferences`). + +## Verify — full end-to-end (drives a real agent team) + +To exercise the commands against real agents (the agent actually files the issue, +writes the spec, implements, etc.), follow the complete local setup in +[`docs/robrix-with-agentchat/README.md`](./robrix-with-agentchat/README.md) +(local Palpo homeserver + agent-chat + tmux + Claude Code). Build Robrix with +`--features agent_chat` and turn the Settings toggle on, then in a room +containing `wf_coordinator` send e.g.: + +``` +@wf_coordinator /create-issue Title | description… +``` + +## Automated checks + +```bash +cargo check # feature OFF (default / production) +cargo check --features agent_chat # feature ON +cargo test --lib # feature OFF +cargo test --lib --features agent_chat # feature ON (runs the cfg-gated workflow test) +``` +All four pass. (Note: `cargo test` also needs the unrelated upstream test fixes +tracked in `issues/011`; see that issue if you build the test target from a merge +base that lacks them.) + +## Where the gate lives + +| Concern | Location | +|---|---| +| Cargo feature | `Cargo.toml` `[features] agent_chat = []` | +| Workflow command table + coordinator match (cfg-gated) | `src/shared/mentionable_text_input.rs` (`WORKFLOW_SLASH_COMMANDS`, `name_is_workflow_coordinator`, the workflow branch of `update_slash_command_list`) | +| Runtime setting (persisted) | `src/settings/app_preferences.rs` (`agent_chat_enabled`), persisted via `src/app.rs` | +| Settings toggle UI + i18n | `src/settings/app_settings.rs`, `resources/i18n/{en,zh-CN}.json` | diff --git a/docs/images/login-screen.png b/docs/images/login-screen.png new file mode 100644 index 000000000..e6d7b9cec Binary files /dev/null and b/docs/images/login-screen.png differ diff --git a/docs/images/openclaw-bot-join-room.png b/docs/images/openclaw-bot-join-room.png new file mode 100644 index 000000000..4444422a8 Binary files /dev/null and b/docs/images/openclaw-bot-join-room.png differ diff --git a/docs/images/openclaw-bot-reply.png b/docs/images/openclaw-bot-reply.png new file mode 100644 index 000000000..479ac5eea Binary files /dev/null and b/docs/images/openclaw-bot-reply.png differ diff --git a/docs/images/openclaw-search-bot.png b/docs/images/openclaw-search-bot.png new file mode 100644 index 000000000..97cf34d42 Binary files /dev/null and b/docs/images/openclaw-search-bot.png differ diff --git a/docs/images/register-account.png b/docs/images/register-account.png new file mode 100644 index 000000000..87241a8eb Binary files /dev/null and b/docs/images/register-account.png differ diff --git a/docs/images/robrix-add-friend.png b/docs/images/robrix-add-friend.png new file mode 100644 index 000000000..718f82d27 Binary files /dev/null and b/docs/images/robrix-add-friend.png differ diff --git a/docs/images/robrix-appservice-settings.png b/docs/images/robrix-appservice-settings.png new file mode 100644 index 000000000..a8ae5b8a2 Binary files /dev/null and b/docs/images/robrix-appservice-settings.png differ diff --git a/docs/images/search-invite-bot.png b/docs/images/search-invite-bot.png new file mode 100644 index 000000000..c37fe942d Binary files /dev/null and b/docs/images/search-invite-bot.png differ diff --git a/docs/robrix-with-agentchat/README.md b/docs/robrix-with-agentchat/README.md new file mode 100644 index 000000000..0fe743e2d --- /dev/null +++ b/docs/robrix-with-agentchat/README.md @@ -0,0 +1,502 @@ +# Robrix × agent-chat:用聊天驱动多 Agent 工作流 + +> 在 **Robrix**(Matrix 客户端)里发一条聊天消息,就能驱动一支 AI agent 团队完成 +> `issue → spec → plan → implement → review` 的完整软件工作流——并且全程在你本地的 +> **Palpo** Matrix 服务器上运行,零云依赖。 +> +> 本文档从**环境搭建**到**使用方法**,用我们真实跑通的案例(让 agent 把一个 hello +> 种子做成一个最小 Makepad 2.0 计数器 app)作为示例,配实测截图。 + +--- + +## 目录 + +1. [这是什么 / 架构](#1-这是什么--架构) +2. [前置要求](#2-前置要求) +3. [环境搭建(一次性)](#3-环境搭建一次性) +4. [启动](#4-启动) +5. [使用方法(带真实案例)](#5-使用方法带真实案例) +6. [两个可视化看板](#6-两个可视化看板) +7. [产物](#7-产物) +8. [排障](#8-排障) +9. [它是怎么集成的(原理)](#9-它是怎么集成的原理) +10. [附录:文件清单 / 命令速查](#10-附录文件清单--命令速查) + +--- + +## 1. 这是什么 / 架构 + +三个独立项目通过 **Matrix 协议**(而非代码耦合)协作: + +| 组件 | 角色 | 本文档里的地址 | +|---|---|---| +| **Robrix** | Matrix 聊天客户端(Rust/Makepad)。你在这里下命令、看 agent 回帖。**零代码改动**。 | 连 `http://127.0.0.1:8128` | +| **Palpo** | 你本地的 Matrix 服务器(Rust),跑在 Docker(OrbStack)里。 | CS-API `http://127.0.0.1:8128` | +| **agent-chat** | 多 agent 协调系统(Node)。把 tmux 里的 Claude Code 会话变成 Matrix 用户(`@ac_*`),通过一个 bot 桥接到房间。**零代码改动**。 | backend `:8090` / dashboard `:8084` | + +``` + 你 (Robrix)─────┐ + ├──► Palpo (Matrix :8128) ◄── @agent-bridge (bot) + 3 个 agent ──────┘ │ + @ac_wf_coordinator / implementer / reviewer │ + ▼ + agent-chat backend(:8090) ──SSE──► push-relay ──tmux──► Claude Code agent + │ + agent-spec CLI · 你的目标仓库 +``` + +**关键设计原则:Robrix 和 agent-chat 的源码都不动。** 我们只在外围加自己的层: +agent-chat 的 `.env`、一个共享 skill、几个启动脚本、一个独立看板。这样两个上游 +项目都能照常 `git pull`。 + +**工作流的 5 个角色**(都由同一个 skill 按 agent 名字分支): + +| 步骤 | Agent | 做什么 | +|---|---|---| +| 1. 建 issue | `wf_coordinator` | 把你的需求写成 `issues/NNN-*.md` | +| 2. spec / 审批门 | `wf_coordinator` | 起草 spec、跑 `agent-spec` 校验,**停下来等你 `approve`** | +| 3. plan | `wf_coordinator` | 写执行计划 `docs/plans/NNN-*.md` | +| 4. 实现 | `wf_implementer` | 真改代码,跑 `cargo check` | +| 5. 对抗审查 | `wf_reviewer` | **独立**重新编译验证 + 审查逻辑,给 verdict | +| 6. 终审(跨 runtime) | `wf_final_reviewer` | **跑在 Codex(不是 Claude)** 上的独立终审:在第一审 approve 之后,自己再重跑一遍 build、逐条复核 spec、专挑第一审遗漏,才最终签发 | + +> **为什么终审用 Codex?** 第 5 步的 reviewer 和前面的 agent 都是 Claude——同模型容易有共同盲区。 +> 第 6 步换一个**不同的 runtime / 模型**(Codex)做最后一道门,等于「换一双眼睛」,是更强的对抗多样性。 +> 框架对两种 runtime 是对等的:`agentchat up-v1 codex` 即可;Codex agent 会自动加载同一份 +> `issue-workflow` skill、收到同样的 `[NOTIFICATION]`、用同一套 MCP 工具回话(已实测全链路打通)。 + +--- + +## 2. 前置要求 + +| 要求 | 说明 | +|---|---| +| **Docker**(OrbStack 或 Docker Desktop) | 跑 Palpo Matrix 服务器 | +| **Node.js ≥ 22** | agent-chat 后端 + 看板 | +| **tmux** | 每个 agent 跑在一个 tmux 会话里 | +| **Claude Code CLI**(已登录) | agent 的"大脑";已认证 | +| **Rust / cargo** | agent 写的代码要能编译(本案例是 Makepad app) | +| **`agent-spec` CLI** | spec 校验工具(`~/.cargo/bin/agent-spec`,`cargo install` 或随项目提供) | +| **Robrix** | Matrix 客户端,连本地 Palpo。`/` 工作流命令需用 `--features agent_chat` 构建并在设置里开启(见下方 ⚠️) | + +本仓库假定的本地路径(按需替换): + +| 项目 | 路径 | +|---|---| +| Robrix | `/Users/zhangalex/Work/Projects/FW/robius/robrix2` | +| agent-chat | `/Users/zhangalex/Work/Projects/consult/agent-chat` | +| Palpo 部署 | `robrix2/palpo-and-octos-deploy/` | +| Demo 脚手架 | `robrix2/roadmap/agentchat-demo/` | + +> ⚠️ **启用 Robrix 的 agent-chat 支持(必须,双重门控)** +> 本集成给 Robrix 加的 `/create-issue`、`/go`、`/review`、`/status` 工作流斜杠命令 +> **默认既不编译、也不启用**,需要两层都打开: +> 1. **编译期 Cargo feature**:用 `cargo run --features agent_chat`(或 +> `cargo build --release --features agent_chat`)构建 Robrix。不带这个 feature 时, +> 相关代码根本不进二进制。 +> 2. **运行时开关**:在 Robrix **Settings → Preferences → "Agent-chat 支持(实验性)"** +> 打开开关(持久化,默认关)。 +> +> 两者都满足后,在**含有 `*_coordinator` agent**(如 `wf_coordinator`)的房间里输入 `/` +> 才会弹出工作流命令。任一不满足时 Robrix 行为与上游一致(命令不出现)。 + +--- + +## 3. 环境搭建(一次性) + +### 3.1 启动 Palpo(Matrix 服务器) + +Palpo 用 Docker Compose 跑。compose 把容器的 `:8008` 映射到主机 `:8128`, +`server_name` 与 well-known 都是 `127.0.0.1:8128`,所以**主机上一律连 `:8128`**。 + +```bash +cd /Users/zhangalex/Work/Projects/FW/robius/robrix2/palpo-and-octos-deploy +docker compose up -d # 首次会编译 Palpo,几分钟 +# 验证(应返回 Matrix 版本列表): +curl http://127.0.0.1:8128/_matrix/client/versions +``` + +> Palpo 已开启开放注册(`allow_registration = true`),所以 demo 账号用 +> `m.login.dummy` 流程直接创建,不需要注册令牌。 + +### 3.2 准备 agent-chat 的 `.env` + +把 demo 提供的 env 模板并入 agent-chat 的 `.env`: + +```bash +cd /Users/zhangalex/Work/Projects/consult/agent-chat +# 若还没有 .env:从 .env.example 起步 +cp .env.example .env 2>/dev/null || true +# 参考 robrix2/roadmap/agentchat-demo/agent-chat.env.demo 填这几个值: +``` + +**你需要自己现编 3 个值**(都不是从哪儿"取"的): + +| 变量 | 是什么 | 怎么填 | +|---|---|---| +| `API_TOKEN` | agent-chat **后端**的访问令牌。**不是 LLM key!** 后端没它会拒绝启动。 | 任意非空串,如 `dev-token-change-me` | +| `MATRIX_BOT_PASSWORD` | 中继 bot `@agent-bridge` 的密码 | 自定一个密码 | +| `MATRIX_AGENT_PASSWORD_SECRET` | 派生 agent 账号密码的种子 | `openssl rand -hex 24` 的输出 | + +其余按下面填(本地默认即可): + +```ini +MATRIX_HOMESERVER=http://127.0.0.1:8128 +MATRIX_SERVER_NAME=127.0.0.1:8128 +MATRIX_BOT_USERNAME=agent-bridge +MATRIX_AGENT_PREFIX=ac_ +MATRIX_REG_TOKEN= # 留空(开放注册) +MATRIX_BRIDGE_SECRET= # 本地留空即可 +MATRIX_TRUST_MODE=audit # 被邀请即自动 join,最省事 +AGENTCHAT_AGENT_TOKEN_MODE=audit # ★ 关键:必须是 audit,否则 agent 收不到消息 +``` + +> **★ 最容易踩的坑**:`.env.example` 默认 `AGENTCHAT_AGENT_TOKEN_MODE=hard`, +> 这会让 backend 拒绝 agent 自己的 `check_inbox`(403)并拒绝 MCP 注册,导致 +> **agent 永远收不到命令**。本地 demo 必须改成 `audit`。 + +> `start-demo.sh` 会在启动时自动:装 npm 依赖、把 `.env` 导入环境(agent-chat 的 +> node 入口没有 dotenv)、剥离 `.env` 里含 `<>` 的占位行(否则会破坏 `source`)。 + +### 3.3 准备目标仓库(DEMO_REPO) + +agent 要在某个 git 仓库里干活——issue/spec/plan/代码都落在这里。本案例用一个**一次性 +沙盒**,主题是「把一个 hello 种子长成一个最小 Makepad 2.0 app」: + +```bash +mkdir -p ~/Work/agentchat-demo-sandbox && cd ~/Work/agentchat-demo-sandbox +git init +mkdir -p issues specs docs/plans src +# 一个秒编的 hello 种子(agent 会把它改成 Makepad app): +cat > src/main.rs <<'EOF' +fn main() { println!("makepad-demo seed — replace me via the issue workflow"); } +EOF +cat > Cargo.toml <<'EOF' +[package] +name = "makepad-demo" +version = "0.1.0" +edition = "2021" +[[bin]] +name = "makepad-demo" +path = "src/main.rs" +EOF +``` + +再放一个**项目契约** `specs/project.spec.md`(所有 task 继承它)。注意 agent-spec 的 +frontmatter **第一行直接是 `spec:`,没有开头的 `---`**: + +```bash +cat > specs/project.spec.md <<'EOF' +spec: project +name: "agentchat-demo-sandbox" +tags: [makepad, rust, gui, demo] +--- + +## Intent +Grow a hello-world seed (`src/main.rs`) into a minimal Makepad 2.0 desktop app, +one issue at a time, to exercise the robrix2 × agent-chat workflow end to end. + +## Decisions +- UI framework: Makepad 2.0 (`script_mod!`, `#[derive(Script, ScriptHook)]`, `app_main!`) — NOT 1.x `live_design!`. +- Dependency: `makepad-widgets` from the kevinaboos fork (branch `cargo_makepad_ndk_fix`), matching robrix2 so the build reuses the cargo git cache. + +## Constraints +- Changes scoped to `src/`, `Cargo.toml`, and workflow dirs (`issues/`, `specs/`, `docs/plans/`). +- Repo stays a valid cargo project: `cargo check` passes, or the reviewer records why a full build was skipped. + +## Completion criteria +- After each task the repo compiles (or the reviewer records build status). +- Each task ships an issue, a (project-inherited) spec, a plan, and the code change. +EOF +git add -A && git commit -m "sandbox skeleton" +``` + +这就是上面截图里编辑器打开的 `project.spec.md`: + +![项目契约 project.spec.md](images/editor-projectspec.png) + +--- + +## 4. 启动 + +一条命令拉起整套(backend + bridge + push-relay + dashboard + workflow-board + +3 个 agent),并自动预建 4 个 Matrix 账号、链接 skill: + +```bash +cd /Users/zhangalex/Work/Projects/FW/robius/robrix2/roadmap/agentchat-demo +DEMO_REPO=~/Work/agentchat-demo-sandbox ./start-demo.sh +``` + +启动脚本依次做(全部带容错): + +``` +Step -1 清场:杀掉旧的 backend/bridge/relay/dashboard + 旧 tmux 会话(避免端口冲突) +Step 0 确保 npm 依赖已装 +Step 1 预建账号:@agent-bridge + @ac_wf_coordinator/implementer/reviewer/final_reviewer(幂等) +Step 2 起 backend → 等 /health → 起 bridge + push-relay + dashboard + workflow-board +Step 3 把 issue-workflow skill 链接进 ~/.claude/skills 和 ~/.codex/skills +Step 4 起 4 个 agent:3 个 Claude(coordinator/implementer/reviewer)+ 1 个 Codex(final_reviewer) +Step 5 打印 Robrix 端操作指引 +``` + +启动后你会有(可选预检:`./preflight.sh`): + +| 服务 | 地址 | +|---|---| +| Palpo (Matrix) | http://127.0.0.1:8128 | +| agent-chat backend | http://127.0.0.1:8090 | +| **Agent Monitor**(agent-chat 自带看板) | http://127.0.0.1:8084 | +| **Workflow Board**(本 demo 的工作流看板) | http://127.0.0.1:8086 | + +--- + +## 5. 使用方法(带真实案例) + +### 5.0 在 Robrix 里建一个群,把 4 个 agent 拉进去 + +1. Robrix 登录 `http://127.0.0.1:8128`(你自己的人类账号,如 `@alex`)。 +2. 邀请中继 bot `@agent-bridge:127.0.0.1:8128` 进任意房间。 +3. 在该房间发 bridge 命令建群(`!` 前缀是 bridge 的命令): + ``` + !mkgroup demoboard wf_coordinator wf_implementer wf_reviewer wf_final_reviewer + ``` + bridge 会新建一个群房并把你和 3 个 agent 都拉进去(agent ~30s 内自动 join)。 + +> **路由规则(重要)**:群房间里,**只有被 `@提及` 的 agent 才会进它的 inbox**。 +> `@提及` 用 agent 的**短名**(`@wf_coordinator`),不是 `@ac_wf_coordinator` 这个 MXID。 + +### 5.1 建 issue + 审批门 —— `/create-issue` + +在群里发(务必 @提及 coordinator): + +``` +@wf_coordinator /create-issue Makepad计数器 | 把 src/main.rs 的 hello 种子改成一个最小 Makepad 2.0 桌面 app:一个窗口、一个计数 Label、一个 +1 按钮 +``` + +coordinator 收到后会:写 `issues/0001-makepad-counter.md`、把 spec 决策 baked 进 +issue、跑 `agent-spec` 校验,然后**停在审批门**等你 `approve`: + +![coordinator 建 issue + 等审批](images/chat-create-issue.png) + +> 它甚至主动指出"收到两条重复的 /create-issue,当作一个请求处理"——这种尽职是 +> Claude Code agent 的本色。 + +### 5.2 审批放行 —— `approve` + +审批通过同样要 @提及(裸 `approve` 不会进 inbox,见截图里第一次没 @ 的 `approve` +没反应,第二次 `@wf_coordinator approve` 才生效): + +``` +@wf_coordinator approve +``` + +coordinator 放行后:写 plan(`docs/plans/0001-*.md`)→ `send_message` 派给 +implementer → implementer 写代码 → 派给 reviewer 对抗审查。整条流水线在群里可见: + +![approve → plan → implement → review 全流水线](images/chat-approve-pipeline.png) + +逐条看(都是真实回帖): +- **coordinator**:`Issue 0001 approved → plan written, handed off to wf_implementer` +- **implementer**:`Implementation done (cargo check passed). Handed off to wf_reviewer` + ——它报告 `cargo check passed (exit 0, 14.28s)`,改了 `Cargo.toml / src/main.rs (78 lines) / Cargo.lock`,还**主动 flag 了一处 spec 偏差**(derive 数量)。 +- **reviewer**:`Review verdict: APPROVE ✅ (5/5 criteria)` ——它**没有轻信** implementer, + 自己重跑了 `cargo check`,还把 `src/main.rs` 跟 fork 里 build-tested 的官方 + `examples/counter` 逐行对比,确认只改了一处(按钮文字 `Increment → +1`)。 + +这就是「对抗审查」的精髓:reviewer 不信报告,自己验证。 + +### 5.3 盯 agent 实时干活(tmux) + +想看 agent 内部在想什么/做什么,attach 它的 tmux 会话: + +```bash +tmux attach -t wf_coordinator # Ctrl-b d 脱离 +tmux attach -t wf_implementer +tmux attach -t wf_reviewer +``` + +coordinator 的会话(写完 plan、派活、等 reviewer): + +![coordinator 的 tmux 会话](images/tmux-coordinator.png) + +### 5.4 如果命令"没反应"——`nudge.sh` + +push-relay 有个 **idle-gate**:agent 正忙时,新消息会进 inbox 但**不立即注入 tmux** +(避免打断正在干活的 agent),等它空闲才推。如果你发了命令但 agent 迟迟不动,手动 +催一下: + +```bash +cd /Users/zhangalex/Work/Projects/FW/robius/robrix2/roadmap/agentchat-demo +./nudge.sh wf_coordinator +``` + +它等价于 push-relay 该做的:往 agent 的 tmux 注入一个 `[NOTIFICATION]`,让它去 +`check_inbox()`。 + +--- + +## 6. 两个可视化看板 + +### 6.1 Agent Monitor —— `http://127.0.0.1:8084` + +agent-chat 自带的看板(`server.js`)。左侧 pending queue,中间实时监控选中 agent 的 +终端输出,右侧 reminders。可切换三个 agent,看它们各自在做什么: + +implementer(报告 `cargo check PASSES: exit 0 in 14.28s`、复用了 fork 缓存、74 个 +makepad crate): + +![Agent Monitor — implementer](images/monitor-implementer.png) + +coordinator(`Pipeline: issue ✅ → plan ✅ → implement ✅ → review (in progress)`): + +![Agent Monitor — coordinator](images/monitor-coordinator.png) + +reviewer(`Verdict: APPROVE ✅ (5/5 acceptance criteria)`,逐条列出它的对抗验证): + +![Agent Monitor — reviewer](images/monitor-reviewer.png) + +### 6.2 Workflow Board —— `http://127.0.0.1:8086` + +本 demo 自带的**独立**看板(`workflow-board.mjs`,零依赖,**不改 agent-chat 源码**)。 +它读 `DEMO_REPO` 的 `specs/issues/docs/plans`,渲染成状态着色的卡片,点开看完整内容, +每 5 秒自动刷新。五列:**Project · Issues · Specs · Plans · Agent notes**: + +![Workflow Board 总览](images/board-overview.png) + +点 issue 卡片 → 看完整 issue(含逐字中文需求、验收标准): + +![Workflow Board — issue 详情](images/board-issue.png) + +点 spec 卡片 → 看项目契约(Intent / Decisions / Constraints): + +![Workflow Board — project spec](images/board-project-spec.png) + +点 plan 卡片 → 看执行计划(Objective / Implementation steps): + +![Workflow Board — plan 详情](images/board-plan.png) + +> **关于 Specs 列**:本案例里 coordinator 把 spec 决策直接 baked 进了 issue,没单独 +> 生成 `task-*.spec.md`,所以 Specs 列显示的是**项目契约** `project.spec.md`(所有 +> task 继承它)。如果你想要"每个 issue 配一个独立 task spec",让 coordinator 多走 +> 一步 `agent-spec` 即可,board 会自动显示新出现的 `specs/task-NNN-*.spec.md`。 + +--- + +## 7. 产物 + +整个工作流的产物都在 `DEMO_REPO`(沙盒)里——这是"真的干了活"的铁证: + +``` +~/Work/agentchat-demo-sandbox/ +├── issues/0001-makepad-counter.md # coordinator 写的 issue +├── specs/project.spec.md # 项目契约(task 继承) +├── docs/plans/0001-makepad-counter.md # coordinator 写的执行计划 +├── docs/progress.md # agent 记的进度 +├── docs/agent-knowledge.md # agent 记的 Makepad 2.0 知识 +├── Cargo.toml # implementer 加了 makepad-widgets 依赖 +└── src/main.rs # implementer 写的 78 行 Makepad 2.0 计数器 +``` + +implementer 写出的 `src/main.rs`(真 Makepad **2.0** API:`script_mod!` / +`#[derive(Script, ScriptHook)]` / `app_main!` / `script_eval!`,不是 1.x): + +![editor — src/main.rs](images/editor-mainrs.png) + +亲眼看 GUI: + +```bash +cd ~/Work/agentchat-demo-sandbox +cargo run # 首次会编译 makepad-widgets,几分钟;之后弹出窗口 +git diff # 看全部改动(agent 守规矩,没擅自 commit) +``` + +--- + +## 8. 排障 + +| 现象 | 原因 | 解法 | +|---|---|---| +| 发命令后 agent **没反应** | push-relay idle-gate 把消息暂存了(agent 当时在忙);或群里没 @提及 | `./nudge.sh `;或确认命令 @了 coordinator | +| backend 起不来 / `/health` 超时 | 旧 backend 占着 `:8090`(端口冲突) | `start-demo.sh` 的 Step -1 会自动清场;或手动 `pkill -f backend-v2.js` | +| backend FATAL `missing required API_TOKEN` | node 不读 `.env`(无 dotenv) | `start-demo.sh` 会自动导入;手动跑时 `set -a; . .env; set +a` | +| `.env: line N: syntax error near ...` | `.env` 里有含 `<>` 的占位行 | `start-demo.sh` 会自动剥离;或手动删那行 | +| agent inbox 403 / `missing MCP process` | `AGENTCHAT_AGENT_TOKEN_MODE=hard` | 改成 `audit`,重启 backend | +| `!mkgroup` 失败 | `MATRIX_BRIDGE_SECRET` 两端不一致 | 本地留空即可(后端为空时跳过校验) | +| coordinator `post` 不进群 | 群名没学对 | 它从触发消息的 `group` 字段取名,确认在正确的群发命令 | + +日志位置:`agent-chat/.demo-logs/{backend,bridge,relay,dashboard,workflow-board}.log` + +完整逐项验收清单见 [`roadmap/agentchat-demo/CHECKLIST.md`](../../roadmap/agentchat-demo/CHECKLIST.md)。 + +--- + +## 9. 它是怎么集成的(原理) + +诚实地说清楚——这套**没有任何代码级耦合**,纯靠 Matrix 协议: + +- **Robrix** 就是个普通 Matrix 客户端,零改动。它把你的消息发到 Palpo,仅此而已。 +- **agent-chat** 用 `matrix-bot-sdk` 以 bot 身份登录 Palpo,把每个 tmux 里的 Claude + Code 会话注册成一个 Matrix 用户(`@ac_`)。它发的是**纯文本** `m.room.message`, + 不发任何自定义事件。 +- 两者在**同一个 Palpo 群房间**里相遇。人发命令、agent 回帖,全是普通聊天消息。 +- "工作流"逻辑全在 **agent-chat 侧的一个共享 skill**(`issue-workflow/SKILL.md`)+ + `agent-spec` CLI 里。skill 按 agent 的 `whoami` 名字分支出 coordinator/implementer/ + reviewer 三种行为。 +- agent 之间的协作走 agent-chat 后端的 `send_message`(按 agent 名路由,不经房间), + 所以 demo 里我们让 coordinator 用 `post(group)` 把进度回帖到房间,让你能看见。 + +唯一需要"写"的东西就是那个共享 skill(纯 Markdown);其余都是配置 + 现成 CLI + +独立看板。**Robrix 和 agent-chat 的源码自始至终没动过。** + +完整的集成分析见 [`roadmap/robrix-agentchat-demo-integration-zh.md`](../../roadmap/robrix-agentchat-demo-integration-zh.md)。 + +--- + +## 10. 附录:文件清单 / 命令速查 + +### Demo 脚手架(`roadmap/agentchat-demo/`) + +| 文件 | 作用 | +|---|---| +| `start-demo.sh` | 一键启动整套(清场→依赖→账号→服务→看板→agent) | +| `preflight.sh` | 启动前/后自检(Palpo、env、账号、skill、建群能力) | +| `register-accounts.mjs` | 用 Palpo 的 dummy 流程预建 bot + agent 账号(幂等) | +| `link-skill.sh` | 把 `issue-workflow` skill 链接进 agent 的 skill 目录 | +| `nudge.sh` | 催醒被 idle-gate 暂存了消息的 agent | +| `workflow-board.mjs` | 独立看板(:8086),展示 project/issues/specs/plans | +| `issue-workflow/SKILL.md` | **唯一的"新代码"**:按名字分支的共享 agent skill | +| `agent-chat.env.demo` | agent-chat `.env` 模板(本地 Palpo) | +| `CHECKLIST.md` | 完整验收清单(A 自动 / B 安装 / C 人工) | + +### 命令速查 + +```bash +# 启动整套 +cd robrix2/roadmap/agentchat-demo +DEMO_REPO=~/Work/agentchat-demo-sandbox ./start-demo.sh + +# 自检 +AC_DIR=/Users/zhangalex/Work/Projects/consult/agent-chat ./preflight.sh + +# Robrix 里(群房间): +# !mkgroup demoboard wf_coordinator wf_implementer wf_reviewer wf_final_reviewer +# @wf_coordinator /create-issue 标题 | 描述 +# @wf_coordinator approve +# @wf_coordinator /status + +# 盯 agent +tmux attach -t wf_coordinator +./nudge.sh wf_coordinator # 命令没反应时催一下 + +# 看板 +open http://127.0.0.1:8084 # Agent Monitor +open http://127.0.0.1:8086 # Workflow Board + +# 看产物 +ls ~/Work/agentchat-demo-sandbox/{issues,specs,docs/plans} +git -C ~/Work/agentchat-demo-sandbox diff + +# 停 +pkill -f 'backend-v2.js|bridge-matrix.js|push-relay.js|node server.js|workflow-board.mjs' +for s in wf_coordinator wf_implementer wf_reviewer; do tmux kill-session -t $s; done +``` diff --git a/docs/robrix-with-agentchat/images/board-issue.png b/docs/robrix-with-agentchat/images/board-issue.png new file mode 100644 index 000000000..76a478b9f Binary files /dev/null and b/docs/robrix-with-agentchat/images/board-issue.png differ diff --git a/docs/robrix-with-agentchat/images/board-overview.png b/docs/robrix-with-agentchat/images/board-overview.png new file mode 100644 index 000000000..c963250c7 Binary files /dev/null and b/docs/robrix-with-agentchat/images/board-overview.png differ diff --git a/docs/robrix-with-agentchat/images/board-plan.png b/docs/robrix-with-agentchat/images/board-plan.png new file mode 100644 index 000000000..65728d339 Binary files /dev/null and b/docs/robrix-with-agentchat/images/board-plan.png differ diff --git a/docs/robrix-with-agentchat/images/board-project-spec.png b/docs/robrix-with-agentchat/images/board-project-spec.png new file mode 100644 index 000000000..fc9928819 Binary files /dev/null and b/docs/robrix-with-agentchat/images/board-project-spec.png differ diff --git a/docs/robrix-with-agentchat/images/chat-approve-pipeline.png b/docs/robrix-with-agentchat/images/chat-approve-pipeline.png new file mode 100644 index 000000000..cd2f8ff7d Binary files /dev/null and b/docs/robrix-with-agentchat/images/chat-approve-pipeline.png differ diff --git a/docs/robrix-with-agentchat/images/chat-create-issue.png b/docs/robrix-with-agentchat/images/chat-create-issue.png new file mode 100644 index 000000000..ef1d93428 Binary files /dev/null and b/docs/robrix-with-agentchat/images/chat-create-issue.png differ diff --git a/docs/robrix-with-agentchat/images/editor-mainrs.png b/docs/robrix-with-agentchat/images/editor-mainrs.png new file mode 100644 index 000000000..036b9fa71 Binary files /dev/null and b/docs/robrix-with-agentchat/images/editor-mainrs.png differ diff --git a/docs/robrix-with-agentchat/images/editor-projectspec.png b/docs/robrix-with-agentchat/images/editor-projectspec.png new file mode 100644 index 000000000..f5e9bbdfc Binary files /dev/null and b/docs/robrix-with-agentchat/images/editor-projectspec.png differ diff --git a/docs/robrix-with-agentchat/images/monitor-coordinator.png b/docs/robrix-with-agentchat/images/monitor-coordinator.png new file mode 100644 index 000000000..66b30e402 Binary files /dev/null and b/docs/robrix-with-agentchat/images/monitor-coordinator.png differ diff --git a/docs/robrix-with-agentchat/images/monitor-implementer.png b/docs/robrix-with-agentchat/images/monitor-implementer.png new file mode 100644 index 000000000..640a9a1fe Binary files /dev/null and b/docs/robrix-with-agentchat/images/monitor-implementer.png differ diff --git a/docs/robrix-with-agentchat/images/monitor-reviewer.png b/docs/robrix-with-agentchat/images/monitor-reviewer.png new file mode 100644 index 000000000..c27822230 Binary files /dev/null and b/docs/robrix-with-agentchat/images/monitor-reviewer.png differ diff --git a/docs/robrix-with-agentchat/images/tmux-coordinator.png b/docs/robrix-with-agentchat/images/tmux-coordinator.png new file mode 100644 index 000000000..827203636 Binary files /dev/null and b/docs/robrix-with-agentchat/images/tmux-coordinator.png differ diff --git a/docs/robrix-with-hermes/01-deploying-hermes-with-matrix-zh.md b/docs/robrix-with-hermes/01-deploying-hermes-with-matrix-zh.md new file mode 100644 index 000000000..834d74780 --- /dev/null +++ b/docs/robrix-with-hermes/01-deploying-hermes-with-matrix-zh.md @@ -0,0 +1,279 @@ +# 部署指南:Hermes Agent + Matrix + +[English](01-deploying-hermes-with-matrix.md) + +> **目标:** 读完本指南,你将能在 Robrix 里和 [Hermes Agent](https://github.com/NousResearch/Hermes-Agent) 直接对话。 + +## 什么是 Hermes Agent? + +[Hermes Agent](https://github.com/NousResearch/Hermes-Agent) 是 [Nous Research](https://nousresearch.com/) 开源的自托管 AI 代理框架,特点是原生围绕 function calling 和工具调用设计。它可以对接多家 LLM(Nous Portal、OpenAI、Anthropic、Gemini、DeepSeek 等),并通过统一的消息网关接入 Matrix、Telegram、Discord、WhatsApp 等聊天平台。在本指南中,Hermes 通过其**内置的 Matrix 适配器**以**普通用户身份**登录 homeserver,不需要任何服务器端配置——和 OpenClaw 的接入方式属于同一类"作为普通 Matrix 客户端"的路线,两者都可以被 Robrix 直接看见并对话。 + +## 本指南的定位 + +本指南聚焦把 Hermes bot **在本地环境里完整跑起来**的路径:先装好 Hermes,再让它以普通用户身份登录你的 Matrix 服务器,最后在 Robrix 里和它交流。顺带把这对组合下**常见问题**讲清楚——比如本地 Palpo 的连接地址和账号后缀可能不一样。 + +Hermes 自身更深入的用法(完整的环境变量、Session Model、其他消息平台、加密进阶)官方文档讲是最佳指南,建议先阅读官方文档做准备 + +- **Hermes 官方文档:** [hermes-agent.nousresearch.com/docs](https://hermes-agent.nousresearch.com/docs/) +- **Matrix 适配器专章:** [messaging/matrix](https://hermes-agent.nousresearch.com/docs/user-guide/messaging/matrix) +- **Hermes GitHub:** [github.com/NousResearch/Hermes-Agent](https://github.com/NousResearch/Hermes-Agent) + +本指南基于 Hermes v0.11.0(2026 年 4 月)+ 本地 Palpo + macOS arm64 实测。Hermes 迭代较快,后续版本如果字段或命令和本文不一样,以官方文档为准。 + +--- + +## 目录 + +1. [前置条件](#1-%E5%89%8D%E7%BD%AE%E6%9D%A1%E4%BB%B6) +2. [安装并配置 Hermes](#2-%E5%AE%89%E8%A3%85%E5%B9%B6%E9%85%8D%E7%BD%AE-hermes) +3. [让 Hermes 登录 Matrix](#3-%E8%AE%A9-hermes-%E7%99%BB%E5%BD%95-matrix) +4. [在 Robrix 里测试](#4-%E5%9C%A8-robrix-%E9%87%8C%E6%B5%8B%E8%AF%95) +5. [遇到问题看哪里](#5-%E9%81%87%E5%88%B0%E9%97%AE%E9%A2%98%E7%9C%8B%E5%93%AA%E9%87%8C) +6. [延伸阅读](#6-%E5%BB%B6%E4%BC%B8%E9%98%85%E8%AF%BB) + +--- + +## 1. 前置条件 + +| 条件 | 说明 | +| --- | --- | +| **Matrix 服务器** | 本地 Palpo([部署指南](../robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md))、matrix.org、自建 Synapse 都行 | +| **Robrix** | [Robrix 快速开始](../robrix/getting-started-with-robrix-zh.md) | +| **两个 Matrix 账号** | 一个是你自己,另一个给 Hermes bot 用 | +| **一个 LLM API Key** | [DeepSeek](https://platform.deepseek.com/api_keys)、Nous Portal、OpenAI、Anthropic 等,任选 | + +Hermes 本身我们下一章现装,不用预装。 + +--- + +## 2. 安装并配置 Hermes + +这一章的目标:让 Hermes 跑起来并能调到 LLM。还不涉及 Matrix。 + +### 2.1 一键安装 + +```bash +curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash +``` + +安装脚本会下 Python 3.11、建 venv、装依赖、把 `hermes` 命令软链接到 `~/.local/bin/hermes`。装完后重开一个终端,或者 `source ~/.zshrc`(bash 用户换成 `~/.bashrc`)让 `hermes` 命令进 PATH。 + +安装遇到问题,看 [Hermes 官方安装文档](https://hermes-agent.nousresearch.com/docs/getting-started/installation) 或 [GitHub Issues](https://github.com/NousResearch/Hermes-Agent/issues)。 + +### 2.2 验证装好了 + +```bash +hermes --version # 期待: Hermes Agent v0.11.x +``` + +### 2.3 配一个 LLM(以 DeepSeek 为例) + +```bash +hermes setup +``` + +向导会让你贴 API key(`sk-xxxx` 格式,从 [DeepSeek 控制台](https://platform.deepseek.com/api_keys) 拿),写到 `~/.hermes/.env`。 + +其他 provider 的用法直接看 `hermes --help`。 + +### 2.4 可选:设默认模型 + +```bash +hermes model +``` + +不带参数会进交互菜单,列表里是 registry 识别到的模型(目前是 `deepseek-reasoner` 和 `deepseek-chat`),另外有一个 **Enter custom model name** 选项,可以手工输入任意模型 ID——DeepSeek 发新模型(比如 `deepseek-v4`)registry 还没跟上时从这个入口填就行。 + +![hermes model 交互菜单,箭头指向 Enter custom model name](images/hermes-model-menu.png) + +具体模型 ID 以 DeepSeek [模型与定价文档](https://api-docs.deepseek.com/zh-cn/quick_start/pricing) 为准——Hermes 不做白名单,你填什么就透传给 DeepSeek API。 + +到这里 §2 就告一段落。想在接 Matrix 之前先验一下 LLM 通不通,可以直接跑 `hermes agent`——看到下面这个 splash(Tools / Skills 列出来、左下角显示 `deepseek-reasoner`、底部能输入对话),说明 Hermes 本体加 LLM 这条链路已经没问题,可以进入 §3 把 Matrix 接进来。 + +![hermes agent 启动后的 splash screen:Hermes + LLM 已就绪](images/hermes-agent-ready.png) + +--- + +## 3. 让 Hermes 登录 Matrix + +Hermes 已经装好了,下面让它以一个普通用户的身份登录到你的 Matrix 服务器。 + +### 3.1 在 Robrix 里给 Hermes 建个账号 + +这一步就是在 Matrix 服务器上注册一个普通账号,取个名字给 Hermes 用。最简单的方式是直接在 Robrix 里做: + +![Robrix 的 Create Account 页,Homeserver URL 已填成本地 Palpo 的 `http://127.0.0.1:8128`](images/robrix-create-account-palpo.png) + +| 你的 Matrix 服务器 | 怎么注册 | +| --- | --- | +| 本地 Palpo | 在 Robrix 里填 `http://127.0.0.1:8128`,用注册页新建一个账号 | +| matrix.org | 在 Robrix 或 [Element Web](https://app.element.io) 里注册 | +| 自建 Synapse | Admin API 或服务器提供的注册页 | + +注册时记下 **用户名** 和 **密码**。 + +> **如果你用本地 Palpo,顺便看一下 §3.2 这个小坑——一句话说,你在客户端连 Palpo 时填的地址,和账号后缀里的那个地址,可能不是同一个。** 公网域名的部署不会遇到这个问题。 + +### 3.2 本地 Palpo 的一个小坑:连接地址 ≠ 账号后缀 + +这个坑不是 Hermes 引起的,是 Matrix-id 设计取舍,但因为 Hermes 的配置里**同时会问你"homeserver 地址"和"完整 user_id"**,你需要知道这两个值可能是不一样的。 + +概念上: + +- **homeserver 地址** = 你用来连接 Palpo 的 URL,比如 `http://127.0.0.1:8128` +- **账号后缀**(Matrix 管它叫 `server_name`)= 你的 Matrix ID 里冒号后面那一串字符,Palpo 启动时在配置里自己声明的"身份" + +三种常见情况: + +| Palpo 配置里的 `server_name` | 客户端连 Palpo 用的 URL | 注册出来的账号长这样 | 两个地址是否一致 | +| --- | --- | --- | --- | +| `127.0.0.1:8128` | `http://127.0.0.1:8128` | `@hermes-bot:127.0.0.1:8128` | ✓ 一致 | +| `192.168.1.28:8128`(LAN IP) | `http://127.0.0.1:8128` | `@hermes-bot:192.168.1.28:8128` | ✗ **不一致** | +| `matrix.example.com`(域名) | `https://matrix.example.com` | `@hermes-bot:matrix.example.com` | ✓ 一致 | + +**第二种最容易混淆**——你用 `127.0.0.1` 能连上 Palpo 并成功注册,但注册出来的账号其实是带 LAN IP 的。所以下一步配 Hermes 时: + +- 填 homeserver 地址 → 用 `http://127.0.0.1:8128`(你本机连得上的那个) +- 填 user ID → 用 `@hermes-bot:192.168.1.28:8128`(账号真正的样子) + +两个地址**故意写得不一样**是正常的。 + +> 不确定自己账号id?在 Robrix 登录后去 Profile / Settings 页面,显示的 `@xxx:yyy` 就是完整的。 + +![Robrix 设置页里的完整 User ID](images/robrix-user-id.png) + +### 3.3 跑 Matrix 配置向导 + +回到命令行: + +```bash +hermes gateway setup +``` + +选 Matrix 之后,向导会先印一段 Matrix 接入的背景说明(包含怎么用 Element 或 `curl` 拿 access token 的命令),然后问你两件事: + +- **Homeserver URL**:填你的 Matrix 服务器地址。本地 Palpo 填 `http://127.0.0.1:8128` 如果是其他服务,填上matrix服务的域名等 +- **Access token**:贴你的 access token;如果你想用"用户名 + 密码"登录,这里留空 + +如果你选了**留空走密码登录**,向导这一阶段就结束了,把 homeserver 写进了 `~/.hermes/.env`。剩下的用户名、密码、白名单等需要你自己打开 `~/.hermes/.env` 补上。本地 Palpo 场景下一个能跑的最小配置: + +```bash +# ~/.hermes/.env +MATRIX_HOMESERVER=http://127.0.0.1:8128 +MATRIX_USER_ID=@hermes-bot:192.168.1.28:8128 # 参考 §3.2 看 server_name 后缀 +MATRIX_PASSWORD=your-bot-password +MATRIX_ALLOWED_USERS=@your-personal-account:192.168.1.28:8128 +``` + +> **用的是 matrix.org?** 把 homeserver 换成 `https://matrix.org`、user_id 换成 `@hermes-bot:matrix.org`,其余一样。 + +是否要求 @mention、是否启用加密、主房间等其他可配置项,完整清单看 [Hermes Matrix 文档](https://hermes-agent.nousresearch.com/docs/user-guide/messaging/matrix)。 + +### 3.4 启动 gateway,以及装 Matrix 库的一个小插曲 + +启动: + +```bash +hermes gateway +``` + +**成功长这样**——日志里会出现: + +``` +┌─────────────────────────────────────────────────────────┐ +│ ⚕ Hermes Gateway Starting... │ +├─────────────────────────────────────────────────────────┤ +│ Messaging platforms + cron scheduler │ +│ Press Ctrl+C to stop │ +└─────────────────────────────────────────────────────────┘ + +load: 2.11 cmd: python3.11 46130 waiting 0.43u 0.09s +``` + +**大概率第一次会看到这条警告**: + +``` +WARNING Matrix: mautrix not installed. Run: pip install 'mautrix[encryption]' +``` + +意思是 Matrix 适配器需要的 Python 库还没装。Hermes 的 venv 是用 uv 建的(默认不带 pip),所以要这样装: + +```bash +uv pip install --python ~/.hermes/hermes-agent/venv/bin/python 'mautrix[encryption]' +``` + +装上之后重新 `hermes gateway`,应该就能看到上面那成功日志了。 + +> **装不上加密版?** `mautrix[encryption]` 里有个叫 `python-olm` 的依赖,在一些系统上(尤其 macOS 新版 + CMake 4+)目前装不太动。遇到这种情况先装明文版救急: +> +> ```bash +> uv pip install --python ~/.hermes/hermes-agent/venv/bin/python mautrix +> ``` +> +> 这条一定能装上,代价是加密房间里 Hermes 看不到消息。这是 mautrix-python 的上游依赖问题,去 [mautrix/python](https://github.com/mautrix/python/issues) 那边追更方便。明文版也不耽误你先跑通流程——测试时用非加密的公共房间就好(下一章会讲)。 + +### 3.5 一个常见问题 + +测试前先提前说一下。 + +**一定要把你自己加进** `MATRIX_ALLOWED_USERS` + +Hermes 默认只回答白名单里的人。**你本人**给 Hermes 发消息,如果你的 Matrix ID 不在 `MATRIX_ALLOWED_USERS` 里,Hermes 是不会有回应的。 + +向导里那个 Allowed users 填的就是这个。手写 `.env` 的话是: + +``` +MATRIX_ALLOWED_USERS=@your-personal-account:192.168.1.28:8128 +``` + +--- + +## 4. 在 Robrix 里测试 + +1. 用**你自己的账号**(不是 bot)登录 Robrix +2. 搜索刚才配置给hermers-agent用的matrix-id +3. 注意输入 bot 完整 ID,例如 `@hermes-bot:192.168.1.28:8128` +4. Bot 会自动接受邀请,几秒内出现在成员列表 +5. 发一条消息试试 +6. 等 LLM 回你(几秒到几十秒) + +能收到回复,就说明 Robrix 和 Hermes 已经端到端通了。 + +![在 Robrix 里和 Hermes Agent 对话](images/hermes-agent-reply.png) + +--- + +## 5. 遇到问题看哪里 + +下表按问题的来源分类,大多数问题都不在 Robrix 这一侧,所以"去哪里解决"一列才是重点。 + +| 症状 | 问题出在哪 | 去哪里解决 | +| --- | --- | --- | +| 安装脚本卡住、\`curl | bash\` 早期就失败 | Hermes 安装流程 | +| 启动报 `mautrix not installed` / `No adapter available` | Matrix 库没装 | §3.4 的 `uv pip install` 那条命令 | +| `python-olm` 装不上、CMake 报错、找不到 libolm | mautrix 上游依赖 | [mautrix/python](https://github.com/mautrix/python/issues);先用明文版救急(§3.4) | +| 启动报 LLM 鉴权错、模型不存在、余额不足 | LLM provider 侧 | 对应 provider 的控制台 | +| 启动报 `Matrix: connection refused` / 拒绝连接 | Matrix 服务器没开 | 确认 Palpo / 你的 homeserver 在线,地址填对没 | +| 启动报 `Matrix: login failed: M_FORBIDDEN` | 账号或密码不对 | 对照 §3.2 看一下 user_id 里的 `server_name` 是不是和 Palpo 声明的一致 | +| bot 收到消息不回(日志没错误) | 没加白名单 | §3.5——把你自己的 user_id 加进 `MATRIX_ALLOWED_USERS` | +| 私聊里 bot 收不到消息 | 加密库没装,明文模式不能解密 | §3.4——换非加密房间测,或装好加密版 mautrix | +| Robrix 搜不到 bot 账号 | 账号没注册成功 | 用 bot 账号去 Element Web 登一下验证它真的存在 | +| Hermes 其他奇怪行为(CLI 崩溃、gateway 状态态异常、tool 调用错乱) | Hermes 本身 | [Hermes 官方文档](https://hermes-agent.nousresearch.com/docs/) / GitHub Issues | + +> **一个粗略的判断方法**:问题出现在"Hermes 启动阶段"或"Hermes 打的日志里"的,优先去 Hermes 那边查;出现在"Robrix 看到/看不到消息"这一层的,再回本指南对照。 + +--- + +## 6. 延伸阅读 + +- **Hermes 官方文档:** [hermes-agent.nousresearch.com/docs](https://hermes-agent.nousresearch.com/docs/) +- **Hermes Matrix 适配器专章:** [messaging/matrix](https://hermes-agent.nousresearch.com/docs/user-guide/messaging/matrix)——完整的环境变量、Session Model、加密进阶、proactive messages 等 +- **Hermes GitHub:** [github.com/NousResearch/Hermes-Agent](https://github.com/NousResearch/Hermes-Agent) +- **Palpo 部署指南:** [01-deploying-palpo-and-octos-zh.md](../robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md) +- **OpenClaw 对照指南:** [01-deploying-openclaw-with-matrix-zh.md](../robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md)——OpenClaw 走的是同一种"普通 Matrix 用户"的接入模式,细节可以互相印证 +- **OpenClaw 使用指南(对 Hermes 也适用):** [02-using-robrix-with-openclaw-zh.md](../robrix-with-openclaw/02-using-robrix-with-openclaw-zh.md)——Robrix 这一侧对任何"以普通 Matrix 用户身份登录"的 bot 看到的 UX 都一样(DM 流程、把 bot 拉进房间、@mention 行为),所以这份也覆盖了 Hermes 的日常使用 +- **Robrix × OpenClaw 架构原理:** [03-how-robrix-and-openclaw-work-together-zh.md](../robrix-with-openclaw/03-how-robrix-and-openclaw-work-together-zh.md)——这份讲的是"AI agent 作为普通 Matrix 客户端"的通用原理,对 Hermes 完全适用 + +--- + +*本指南基于 Hermes Agent v0.11.0(2026 年 4 月)实测。Hermes 迭代较快,字段和命令可能变化,遇到和本文对不上的地方以 [Hermes 官方文档](https://hermes-agent.nousresearch.com/docs/) 为准。* \ No newline at end of file diff --git a/docs/robrix-with-hermes/01-deploying-hermes-with-matrix.md b/docs/robrix-with-hermes/01-deploying-hermes-with-matrix.md new file mode 100644 index 000000000..5611971e5 --- /dev/null +++ b/docs/robrix-with-hermes/01-deploying-hermes-with-matrix.md @@ -0,0 +1,279 @@ +# Deployment Guide: Hermes Agent + Matrix + +[中文版](01-deploying-hermes-with-matrix-zh.md) + +> **Goal:** After following this guide, you will be able to chat with [Hermes Agent](https://github.com/NousResearch/Hermes-Agent) directly from Robrix. + +## What is Hermes Agent? + +[Hermes Agent](https://github.com/NousResearch/Hermes-Agent) is a self-hosted AI agent framework open-sourced by [Nous Research](https://nousresearch.com/), designed natively around function calling and tool use. It talks to many LLM providers (Nous Portal, OpenAI, Anthropic, Gemini, DeepSeek, …) and connects to chat platforms like Matrix, Telegram, Discord, and WhatsApp through a unified messaging gateway. In this guide, Hermes uses its **built-in Matrix adapter** to log in to your homeserver **as a regular user**, requiring no server-side configuration — the same "regular Matrix client" integration model OpenClaw uses, which means Robrix can see and talk to it with no extra setup. + +## About This Guide + +This guide is focused on the path of getting a Hermes bot **fully running in a local environment**: install Hermes first, then have it log in to your Matrix server as a regular user, and finally chat with it from Robrix. Along the way we spell out the **common gotchas** of this specific combo — for example, the fact that the connection address and the account suffix don't always match when you're talking to a local Palpo. + +For deeper Hermes usage (full environment variable list, Session Model, other messaging platforms, advanced encryption), the official documentation is the best guide — we recommend reading it alongside this one, and skimming the official docs first is a good way to prepare: + +- **Hermes official docs:** [hermes-agent.nousresearch.com/docs](https://hermes-agent.nousresearch.com/docs/) +- **Matrix adapter reference:** [messaging/matrix](https://hermes-agent.nousresearch.com/docs/user-guide/messaging/matrix) +- **Hermes GitHub:** [github.com/NousResearch/Hermes-Agent](https://github.com/NousResearch/Hermes-Agent) + +This guide was tested against Hermes v0.11.0 (April 2026) + local Palpo + macOS arm64. Hermes iterates quickly; if field names or commands in a later version diverge from what's written here, trust the official docs. + +--- + +## Table of Contents + +1. [Prerequisites](#1-prerequisites) +2. [Install and Configure Hermes](#2-install-and-configure-hermes) +3. [Log Hermes Into Matrix](#3-log-hermes-into-matrix) +4. [Test From Robrix](#4-test-from-robrix) +5. [Where to Look When Things Go Wrong](#5-where-to-look-when-things-go-wrong) +6. [Further Reading](#6-further-reading) + +--- + +## 1. Prerequisites + +| Requirement | Notes | +| --- | --- | +| **Matrix server** | Local Palpo ([deployment guide](../robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md)), matrix.org, or your own Synapse — any works | +| **Robrix** | [Getting Started with Robrix](../robrix/getting-started-with-robrix.md) | +| **Two Matrix accounts** | One for yourself, one for the Hermes bot | +| **An LLM API key** | [DeepSeek](https://platform.deepseek.com/api_keys), Nous Portal, OpenAI, Anthropic, … take your pick | + +We'll install Hermes itself in the next section — no need to pre-install. + +--- + +## 2. Install and Configure Hermes + +Goal of this section: get Hermes running and talking to an LLM. No Matrix yet. + +### 2.1 One-shot install + +```bash +curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash +``` + +The install script downloads Python 3.11, creates a venv, installs dependencies, and symlinks the `hermes` command into `~/.local/bin/hermes`. Once it finishes, open a new terminal, or run `source ~/.zshrc` (bash users: `~/.bashrc`), so the `hermes` command lands on your PATH. + +If install goes sideways, check the [Hermes installation docs](https://hermes-agent.nousresearch.com/docs/getting-started/installation) or [GitHub Issues](https://github.com/NousResearch/Hermes-Agent/issues). + +### 2.2 Verify the install + +```bash +hermes --version # expected: Hermes Agent v0.11.x +``` + +### 2.3 Wire up an LLM (DeepSeek as example) + +```bash +hermes setup +``` + +The wizard asks you to paste an API key (`sk-xxxx` format, grab it from the [DeepSeek console](https://platform.deepseek.com/api_keys)) and writes it to `~/.hermes/.env`. + +For other providers, `hermes --help` has the per-provider usage. + +### 2.4 Optional: set a default model + +```bash +hermes model +``` + +Called with no argument it drops into an interactive menu. The listed models are the ones Hermes's registry currently knows about (at the moment: `deepseek-reasoner` and `deepseek-chat`), plus an **Enter custom model name** option that lets you type any model ID by hand — useful whenever DeepSeek ships a new model (say `deepseek-v4`) before the registry catches up. + +![hermes model interactive menu, arrow pointing to Enter custom model name](images/hermes-model-menu.png) + +For specific model IDs, defer to DeepSeek's [models & pricing page](https://api-docs.deepseek.com/quick_start/pricing) — Hermes doesn't maintain an allowlist; whatever you type gets passed straight to the DeepSeek API. + +That wraps up §2. If you want to sanity-check the LLM wiring before touching Matrix, just run `hermes agent` — the splash screen below (Tools / Skills listed, `deepseek-reasoner` in the lower-left, input prompt at the bottom) means the Hermes + LLM leg of the chain is working, and you're ready to move on to §3 to plug in Matrix. + +![hermes agent splash screen: Hermes + LLM ready](images/hermes-agent-ready.png) + +--- + +## 3. Log Hermes Into Matrix + +Hermes is installed. Next we get it to log in to your Matrix server as a regular user. + +### 3.1 Register a Matrix account for Hermes in Robrix + +This step is just "register a regular account on your Matrix server and pick a name for Hermes to use." The easiest way is to do it from inside Robrix: + +![Robrix's Create Account screen with `http://127.0.0.1:8128` filled in as the Homeserver URL (local Palpo case)](images/robrix-create-account-palpo.png) + +| Your Matrix server | How to register | +| --- | --- | +| Local Palpo | In Robrix, set the homeserver to `http://127.0.0.1:8128` and use the registration page to create a new account | +| matrix.org | Register through Robrix or [Element Web](https://app.element.io) | +| Self-hosted Synapse | Admin API, or whatever registration page your server provides | + +Write down the **username** and **password**. + +> **If you're on local Palpo, please also read §3.2 — a gotcha in one sentence: the address you use to connect to Palpo and the address that shows up in your account suffix may not be the same one.** This doesn't affect public-domain deployments. + +### 3.2 The local-Palpo gotcha: connection address ≠ account suffix + +This gotcha isn't a Hermes quirk — it's a consequence of how Matrix IDs are designed — but because the Hermes config asks you for both **the homeserver URL** and **your full user_id**, you need to know these two values can legitimately differ. + +Conceptually: + +- **Homeserver URL** = the URL you use to connect to Palpo, e.g. `http://127.0.0.1:8128` +- **Account suffix** (Matrix calls this `server_name`) = the string after the colon in your Matrix ID — the "identity" Palpo declares for itself in its config at startup + +Three common cases: + +| Palpo's `server_name` | The URL clients use to reach Palpo | The resulting account | Same address? | +| --- | --- | --- | --- | +| `127.0.0.1:8128` | `http://127.0.0.1:8128` | `@hermes-bot:127.0.0.1:8128` | ✓ yes | +| `192.168.1.28:8128` (LAN IP) | `http://127.0.0.1:8128` | `@hermes-bot:192.168.1.28:8128` | ✗ **no** | +| `matrix.example.com` (domain) | `https://matrix.example.com` | `@hermes-bot:matrix.example.com` | ✓ yes | + +**The second case is the one that trips people up** — you can reach Palpo on `127.0.0.1` and register successfully there, but the account that gets created actually has the LAN IP in its suffix. So when you configure Hermes next: + +- For the homeserver URL → use `http://127.0.0.1:8128` (the one your machine can reach) +- For the user ID → use `@hermes-bot:192.168.1.28:8128` (what the account actually is) + +The two addresses being **deliberately different** is normal here. + +> Not sure what your account ID is? After logging into Robrix, head to Profile / Settings — the `@xxx:yyy` shown there is the full ID. + +![Full User ID shown in Robrix Settings](images/robrix-user-id.png) + +### 3.3 Run the Matrix config wizard + +Back to the command line: + +```bash +hermes gateway setup +``` + +Pick Matrix. The wizard first prints a block of background info about the Matrix integration (including commands for fetching an access token via Element or `curl`), then asks you two things: + +- **Homeserver URL**: your Matrix server address. For local Palpo: `http://127.0.0.1:8128`. For other setups, use the domain of your Matrix service. +- **Access token**: paste your access token; if you'd rather log in with username + password, leave it empty + +If you **left the token empty and chose password login**, the wizard ends here with just the homeserver written to `~/.hermes/.env`. You fill in the remaining bits (username, password, allowlist, etc.) by editing `~/.hermes/.env` yourself. A minimum working config for local Palpo looks like: + +```bash +# ~/.hermes/.env +MATRIX_HOMESERVER=http://127.0.0.1:8128 +MATRIX_USER_ID=@hermes-bot:192.168.1.28:8128 # see §3.2 for the server_name suffix +MATRIX_PASSWORD=your-bot-password +MATRIX_ALLOWED_USERS=@your-personal-account:192.168.1.28:8128 +``` + +> **Using matrix.org?** Swap the homeserver for `https://matrix.org` and the user_id for `@hermes-bot:matrix.org`; everything else stays the same. + +For the rest of the knobs (require @mention, enable encryption, primary rooms, etc.), the [Hermes Matrix docs](https://hermes-agent.nousresearch.com/docs/user-guide/messaging/matrix) have the full list. + +### 3.4 Start the gateway — and a small detour about the Matrix library + +To start: + +```bash +hermes gateway +``` + +**A successful start looks like this** — the log should contain: + +``` +┌─────────────────────────────────────────────────────────┐ +│ ⚕ Hermes Gateway Starting... │ +├─────────────────────────────────────────────────────────┤ +│ Messaging platforms + cron scheduler │ +│ Press Ctrl+C to stop │ +└─────────────────────────────────────────────────────────┘ + +load: 2.11 cmd: python3.11 46130 waiting 0.43u 0.09s +``` + +**The first time, you'll most likely hit this warning**: + +``` +WARNING Matrix: mautrix not installed. Run: pip install 'mautrix[encryption]' +``` + +Which means the Python library the Matrix adapter needs isn't installed yet. Hermes's venv is built with uv (no pip by default), so install it like this: + +```bash +uv pip install --python ~/.hermes/hermes-agent/venv/bin/python 'mautrix[encryption]' +``` + +Re-run `hermes gateway` and you should see the success log above. + +> **Can't install the encryption variant?** `mautrix[encryption]` has a transitive dependency called `python-olm` that currently fails to build on some systems (notably newer macOS + CMake 4+). If you hit that, fall back to the plaintext variant to unblock yourself: +> +> ```bash +> uv pip install --python ~/.hermes/hermes-agent/venv/bin/python mautrix +> ``` +> +> This one always installs. The cost: Hermes won't see messages in encrypted rooms. This is an upstream mautrix-python dependency issue — [mautrix/python](https://github.com/mautrix/python/issues) is the better place to track it. The plaintext variant doesn't stop you from getting the rest of the flow working — just test in a non-encrypted public room (covered in the next section). + +### 3.5 A common pitfall + +Worth calling out up front before you test from Robrix. + +**Always put your own account into** `MATRIX_ALLOWED_USERS` + +By default Hermes only responds to people on its allowlist. If **you**, messaging Hermes from your personal account, aren't in `MATRIX_ALLOWED_USERS`, Hermes silently ignores you. + +That's what the wizard's "Allowed users" prompt populates. By hand in `.env`: + +``` +MATRIX_ALLOWED_USERS=@your-personal-account:192.168.1.28:8128 +``` + +--- + +## 4. Test From Robrix + +1. Log in to Robrix with **your own account** (not the bot's) +2. Search for the Matrix ID you just configured for the Hermes agent +3. Make sure you type the bot's full ID, e.g. `@hermes-bot:192.168.1.28:8128` +4. The bot auto-accepts the invite and appears in the member list within a few seconds +5. Send a message +6. Wait for the LLM reply (a few seconds to tens of seconds) + +If you get a reply back, Robrix and Hermes are talking end-to-end. + +![Chatting with Hermes Agent from Robrix](images/hermes-agent-reply.png) + +--- + +## 5. Where to Look When Things Go Wrong + +The table below groups issues by where they originate. Most problems aren't on the Robrix side, so the "where to fix" column is the important one. + +| Symptom | Where the issue is | Where to fix | +| --- | --- | --- | +| Install script hangs, `curl \| bash` fails early | Hermes install | Hermes installation docs | +| Start fails with `mautrix not installed` / `No adapter available` | Matrix library missing | The `uv pip install` line in §3.4 | +| `python-olm` won't install, CMake errors, missing libolm | mautrix upstream dependency | [mautrix/python](https://github.com/mautrix/python/issues); fall back to the plaintext variant (§3.4) | +| Start fails with LLM auth error, unknown model, insufficient balance | LLM provider side | Your provider's console | +| Start fails with `Matrix: connection refused` | Matrix server isn't reachable | Confirm Palpo / your homeserver is running and the address is correct | +| Start fails with `Matrix: login failed: M_FORBIDDEN` | Wrong account or password | Double-check §3.2 — make sure the `server_name` suffix in your user_id matches the one Palpo declares | +| Bot receives messages but never replies (no errors in log) | Missing from allowlist | §3.5 — add your user_id to `MATRIX_ALLOWED_USERS` | +| Bot doesn't receive messages in DMs | Encryption lib missing, plaintext mode can't decrypt | §3.4 — test in a non-encrypted room, or install the encryption variant of mautrix | +| Robrix can't find the bot account | Registration didn't succeed | Log in as the bot from Element Web to confirm the account actually exists | +| Other weird Hermes behavior (CLI crashes, odd gateway state, tool-call mess) | Hermes itself | [Hermes official docs](https://hermes-agent.nousresearch.com/docs/) / GitHub Issues | + +> **Rough rule of thumb**: anything that shows up "during Hermes startup" or "in Hermes's logs" — check on Hermes's side first. Anything at the "Robrix sees / doesn't see a message" layer — come back to this guide. + +--- + +## 6. Further Reading + +- **Hermes official docs:** [hermes-agent.nousresearch.com/docs](https://hermes-agent.nousresearch.com/docs/) +- **Hermes Matrix adapter reference:** [messaging/matrix](https://hermes-agent.nousresearch.com/docs/user-guide/messaging/matrix) — full environment variable list, Session Model, advanced encryption, proactive messages, etc. +- **Hermes GitHub:** [github.com/NousResearch/Hermes-Agent](https://github.com/NousResearch/Hermes-Agent) +- **Palpo deployment guide:** [01-deploying-palpo-and-octos.md](../robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md) +- **OpenClaw companion guide:** [01-deploying-openclaw-with-matrix.md](../robrix-with-openclaw/01-deploying-openclaw-with-matrix.md) — OpenClaw uses the same "regular Matrix user" integration model, so the two guides' details can be cross-checked against each other +- **OpenClaw usage guide (applies to Hermes too):** [02-using-robrix-with-openclaw.md](../robrix-with-openclaw/02-using-robrix-with-openclaw.md) — from Robrix's side the chat UX is identical for any bot that logs in as a regular Matrix user (DM flow, room invites, @mention behavior), so this covers how to actually use Hermes too +- **Robrix × OpenClaw architecture:** [03-how-robrix-and-openclaw-work-together.md](../robrix-with-openclaw/03-how-robrix-and-openclaw-work-together.md) — this explains the general "AI agent as a regular Matrix client" model, which applies to Hermes too + +--- + +*This guide was tested against Hermes Agent v0.11.0 (April 2026). Hermes iterates quickly — field names and commands may drift; when something here doesn't match what you see, trust the [Hermes official docs](https://hermes-agent.nousresearch.com/docs/).* diff --git a/docs/robrix-with-hermes/images/hermes-agent-ready.png b/docs/robrix-with-hermes/images/hermes-agent-ready.png new file mode 100644 index 000000000..27f1fc86e Binary files /dev/null and b/docs/robrix-with-hermes/images/hermes-agent-ready.png differ diff --git a/docs/robrix-with-hermes/images/hermes-agent-reply.png b/docs/robrix-with-hermes/images/hermes-agent-reply.png new file mode 100644 index 000000000..0d344af7a Binary files /dev/null and b/docs/robrix-with-hermes/images/hermes-agent-reply.png differ diff --git a/docs/robrix-with-hermes/images/hermes-model-menu.png b/docs/robrix-with-hermes/images/hermes-model-menu.png new file mode 100644 index 000000000..4ecb623da Binary files /dev/null and b/docs/robrix-with-hermes/images/hermes-model-menu.png differ diff --git a/docs/robrix-with-hermes/images/robrix-create-account-palpo.png b/docs/robrix-with-hermes/images/robrix-create-account-palpo.png new file mode 100644 index 000000000..c75f9ee77 Binary files /dev/null and b/docs/robrix-with-hermes/images/robrix-create-account-palpo.png differ diff --git a/docs/robrix-with-hermes/images/robrix-user-id.png b/docs/robrix-with-hermes/images/robrix-user-id.png new file mode 100644 index 000000000..2126b733d Binary files /dev/null and b/docs/robrix-with-hermes/images/robrix-user-id.png differ diff --git a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md new file mode 100644 index 000000000..2fab781ce --- /dev/null +++ b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix-zh.md @@ -0,0 +1,380 @@ +# 部署指南:OpenClaw + Matrix + +[English](01-deploying-openclaw-with-matrix.md) + +> **目标:** 完成本指南后,你将拥有一个连接到 Matrix 服务器的 OpenClaw AI 代理。之后你可以使用 Robrix(或任何 Matrix 客户端)与 OpenClaw 驱动的 AI 代理对话。 + +## 什么是 OpenClaw? + +[OpenClaw](https://github.com/openclaw/openclaw) 是一个开源的自托管 AI 助手平台(前身为 MoltBot,2026 年初更名)。它支持多种 LLM(DeepSeek、OpenAI、Anthropic 等),并通过频道插件接入 Matrix、Telegram、Discord 等聊天平台。在本指南中,OpenClaw 通过 **Matrix 频道插件**以**普通用户身份**登录服务器,不需要任何服务器端配置。与 Octos 的 Application Service 模式不同,OpenClaw 的接入方式更简单——详见[架构原理](03-how-robrix-and-openclaw-work-together-zh.md)。 + +本指南将逐步引导你完成 OpenClaw 与 Matrix 的部署:从创建 Matrix Bot 账号,到配置 OpenClaw Matrix 频道插件,再到端到端验证连接。 + +> **想快速体验?** 跳转到 [快速开始](#2-快速开始)。 +> +> **想了解 OpenClaw 如何连接 Matrix?** 参见 [架构原理](03-how-robrix-and-openclaw-work-together-zh.md)。 + +> **关于 OpenClaw:** OpenClaw 目前仍在快速迭代中,CLI 和插件系统存在不少 bug(例如 `channels add` 向导可能崩溃)。本指南给出的是我们**实测验证过**的配置方式——直接编辑配置文件,跳过不稳定的 CLI 向导。如果你遇到本指南未覆盖的问题,请查阅 [OpenClaw 官方文档](https://docs.openclaw.ai/) 和 [GitHub Issues](https://github.com/openclaw/openclaw/issues)。 + +--- + +## 目录 + +1. [前置条件](#1-前置条件) +2. [快速开始](#2-快速开始) +3. [创建 Matrix Bot 账号](#3-创建-matrix-bot-账号) +4. [安装 OpenClaw 并初始化配置目录](#4-安装-openclaw-并初始化配置目录) +5. [编写配置文件](#5-编写配置文件) +6. [启动并验证](#6-启动并验证) +7. [故障排查](#7-故障排查) +8. [生产环境配置](#8-生产环境配置) +9. [延伸阅读](#9-延伸阅读) + +--- + +## 1. 前置条件 + +| 条件 | 说明 | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| **两个 Matrix 账号** | 一个作为你自己使用的账号,另一个作为 OpenClaw Bot 使用的账号 | +| **Node.js** | v22.16+ 或 v24+(推荐) | +| **LLM API Key** | 例如[DeepSeek](https://platform.deepseek.com/)(有免费额度)、OpenAI、Anthropic 等 | +| **Matrix 服务器** | 本地 Palpo(推荐,参见[Palpo 部署指南](../robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md))或公共服务器 matrix.org | +| **Robrix** | 参见[Robrix 快速开始](../robrix/getting-started-with-robrix-zh.md) | + +--- + +## 2. 快速开始 + +``` +1. 注册一个 Matrix Bot 账号(记住用户名和密码) +2. 安装 OpenClaw → 运行 openclaw config → 编辑 ~/.openclaw/openclaw.json +3. 运行 openclaw gateway start +4. 在 Robrix 中用另一个账号给 Bot 发消息 +``` + +详细步骤见下文。 + +--- + +## 3. 创建 Matrix Bot 账号 + +Bot 账号就是一个**普通的 Matrix 账号**。OpenClaw 会用它的用户名和密码自动登录,不需要你手动获取 Access Token。 + +| 服务器 | 注册方式 | 说明 | +| ---------------------- | ----------------------------------------------------- | ---------------------------------------------- | +| **本地 Palpo** | 在 Robrix 中注册 | 连接 `http://127.0.0.1:8128`,注册一个新账号 | +| **matrix.org** | 在 Robrix 或[Element Web](https://app.element.io) 中注册 | 公共服务器,免费,注册即用 | +| **自建 Synapse** | 通过 Admin API 或 Web 注册 | 生产环境推荐 | + +注册时记住: + +- **用户名**(例如 `chalice`) +- **密码** + +--- + +## 4. 安装 OpenClaw 并初始化配置目录 + +### 4.1 安装 + +```bash +npm install -g openclaw@latest +openclaw --version # 验证安装 +``` + +### 4.2 初始化配置目录 + +```bash +openclaw config +``` + +> **这个命令会报错——这是正常的,忽略即可。** 重要的是它已经在 `~/.openclaw/` 下创建了配置目录。后续所有配置都在这个目录中进行。 + +> **为什么不用 `openclaw channels add` 向导?** OpenClaw v2026.4.7 的 CLI 向导存在多个 bug(Telegram 插件路径错误导致向导崩溃、参数不完整等)。**直接编辑配置文件是唯一可靠的方式。** + +--- + +## 5. 编写配置文件 + +编辑 `~/.openclaw/openclaw.json`。下面提供两种场景的完整配置。 + +### 5.1 连接本地 Palpo(推荐) + +```json +{ + "commands": { + "native": "auto", + "nativeSkills": "auto" + }, + "models": { + "providers": { + "deepseek": { + "baseUrl": "https://api.deepseek.com/v1", + "apiKey": "sk-你的DeepSeek密钥", + "api": "openai-completions", + "models": [ + { + "id": "deepseek-chat", + "name": "DeepSeek Chat", + "contextWindow": 164000, + "maxTokens": 8192 + } + ] + } + } + }, + "channels": { + "matrix": { + "enabled": true, + "homeserver": "http://127.0.0.1:8128", + "network": { + "dangerouslyAllowPrivateNetwork": true + }, + "userId": "@chalice:127.0.0.1:8128", + "password": "你的密码", + "deviceName": "OpenClaw Bot", + "encryption": true, + "autoJoin": "always", + "dm": { + "policy": "open" + } + } + }, + "plugins": { + "entries": { + "matrix": { + "enabled": true + } + } + }, + "gateway": { + "mode": "local" + } +} +``` + +### 5.2 连接公共服务器 matrix.org + +```json +{ + "commands": { + "native": "auto", + "nativeSkills": "auto" + }, + "models": { + "providers": { + "deepseek": { + "baseUrl": "https://api.deepseek.com/v1", + "apiKey": "sk-你的DeepSeek密钥", + "api": "openai-completions", + "models": [ + { + "id": "deepseek-chat", + "name": "DeepSeek Chat", + "contextWindow": 164000, + "maxTokens": 8192 + } + ] + } + } + }, + "channels": { + "matrix": { + "enabled": true, + "homeserver": "https://matrix.org", + "userId": "@your-bot:matrix.org", + "password": "你的密码", + "deviceName": "OpenClaw Bot", + "encryption": true, + "autoJoin": "always", + "dm": { + "policy": "open" + } + } + }, + "plugins": { + "entries": { + "matrix": { + "enabled": true + } + } + }, + "gateway": { + "mode": "local" + } +} +``` + +### 5.3 配置项详解 + +#### `gateway` 配置 + +| 字段 | 值 | 重点说明 | +| -------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `mode` | `"local"` | **必填。** 没有这个字段 gateway 会拒绝启动,报 "missing gateway.mode" 错误。`"local"` 指的是 OpenClaw gateway 本身运行在本地(只监听 127.0.0.1),与 LLM 是否远程无关——DeepSeek API 调用仍然走公网。 | + +#### `models.providers` 配置 + +| 字段 | 值 | 重点说明 | +| ----------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `baseUrl` | `"https://api.deepseek.com/v1"` | **必须带 `/v1` 后缀。** DeepSeek 使用 OpenAI 兼容 API。 | +| `apiKey` | `"sk-xxx"` | **直接写明文密钥。** 不要用 `${ENV_VAR}` 格式——macOS LaunchAgent 服务读不到终端的环境变量。写完后 `chmod 600 ~/.openclaw/openclaw.json` 保护文件权限。 | +| `api` | `"openai-completions"` | **不是 `type`。** 网上很多教程写 `"type"` 是错的,正确字段名是 `"api"`。 | +| `contextWindow` | `164000` | **必须设大。** OpenClaw 系统提示词占 16K+ token,默认 4096 会直接报错。DeepSeek Chat 支持 164K。 | +| `maxTokens` | `8192` | 单次回复最大 token 数。 | + +> **注意 `providers` 的格式:** `providers` 是一个对象(provider 名称作为 key),不是数组。`models` 是数组。 + +#### `channels.matrix` 配置 + +| 字段 | 值 | 重点说明 | +| ------------------------------------------ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `enabled` | `true` | 启用 Matrix 频道。 | +| `homeserver` | `"http://127.0.0.1:8128"` | **本地 Palpo 必须用 `http`**,不是 `https`(Palpo 默认没有 TLS)。matrix.org 用 `https`。 | +| `network.dangerouslyAllowPrivateNetwork` | `true` | **仅本地/内网部署需要。** OpenClaw 默认阻止连接私有 IP(127.0.0.1、10.x、192.168.x),这是防 SSRF 的安全措施。连公共服务器(matrix.org)不需要此项。 | +| `userId` | `"@chalice:127.0.0.1:8128"` | **必须是完整 Matrix ID 格式** `@用户名:服务器`。 | +| `password` | `"你的密码"` | 密码认证——OpenClaw 自动登录并缓存 token 到 `~/.openclaw/credentials/matrix/`。也支持 Access Token 认证(将 `password` 替换为 `accessToken`),详见 [OpenClaw Matrix 插件文档](https://docs.openclaw.ai/channels/matrix)。 | +| `encryption` | `true` | **强烈建议开启。** Matrix DM 默认启用 E2EE。如果不开,Bot 收到加密消息无法解密,表现为"发了消息但没回复"。 | +| `autoJoin` | `"always"` | 测试阶段接受所有邀请。生产环境改为 `"allowlist"`。 | +| `dm.policy` | `"open"` | 测试阶段允许所有私聊。生产环境改为 `"allowlist"`。 | + +#### `plugins` 配置 + +| 字段 | 值 | 重点说明 | +| ---------------------------------- | -------- | ------------------------ | +| `plugins.entries.matrix.enabled` | `true` | 确保 Matrix 插件已启用。 | + +### 5.4 本地 Palpo vs 公共 matrix.org 的差异 + +| 配置项 | 本地 Palpo | 公共 matrix.org | +| ------------------------------------------ | -------------------------- | ------------------------------------------- | +| `homeserver` | `http://127.0.0.1:8128` | `https://matrix.org` | +| `network.dangerouslyAllowPrivateNetwork` | **需要** `true` | **不需要**(删除整个 `network` 块) | +| `userId` 格式 | `@用户名:127.0.0.1:8128` | `@用户名:matrix.org` | +| TLS | 无(`http`) | 有(`https`) | +| 注册方式 | Robrix 连接 Palpo 注册 | Element Web 或 Robrix 注册 | + +> **从本地 Palpo 切换到 matrix.org:** 本指南以 Palpo 为例,但同样的配置可以直接用于 matrix.org 或任何标准 Matrix 服务器。只需修改 `openclaw.json` 中的 3 处: +> +> 1. `homeserver`:`http://127.0.0.1:8128` → `https://matrix.org` +> 2. `userId`:`@用户名:127.0.0.1:8128` → `@用户名:matrix.org` +> 3. 删除整个 `"network": { "dangerouslyAllowPrivateNetwork": true }` 块(公网服务器不需要) +> +> 其他配置(LLM、加密、autoJoin 等)**完全不变**。改完后 `openclaw gateway restart` 即可。 +> +> 如果你使用其他自建 Matrix 服务器(如 Synapse、Dendrite),同样只需要修改这 3 处,将域名和协议替换为你的服务器地址即可。 + +--- + +## 6. 启动并验证 + +### 6.1 启动 Gateway + +```bash +openclaw gateway start +``` + +### 6.2 检查日志 + +```bash +tail -20 ~/.openclaw/logs/gateway.log +``` + +确认看到以下关键日志: + +``` +[gateway] agent model: deepseek/deepseek-chat ← LLM 配置正确 +[gateway] ready (6 plugins, 0.3s) ← Gateway 就绪 +[matrix] [default] starting provider (http://...) ← Matrix 开始连接 +matrix: logged in as @chalice:127.0.0.1:8128 ← 登录成功 +matrix: device is verified by its owner and ready for encrypted rooms ← 加密就绪 +``` + +> **提示:** OpenClaw 启动后需要一些时间完成 Matrix 登录和加密设备初始化。如果日志中只看到 `starting provider` 而没有 `logged in`,请耐心等待几秒到十几秒,登录完成后才能接收消息。 + +### 6.3 在 Robrix 中测试 + +1. **启动 Robrix**,用你的**个人账号**登录 +2. **搜索 Bot**:点搜索图标,输入 Bot 的 Matrix ID(如 `@chalice:127.0.0.1:8128`),切到 **People** 标签(Bot 是普通用户,必须在 People 中搜索) +3. **发起私聊**:选择 Bot,进入对话 +4. **发送消息**,耐心等待回复(LLM 生成回复需要几秒到十几秒) + +OpenClaw Bot(chalice)在 Robrix 中成功回复消息 + +> **重要:** 如果你在 OpenClaw 加密设备创建之前发送过消息,那些历史消息**永远无法解密**(这是 Matrix E2EE 的正常行为)。必须发送**新消息**才能触发回复。 + +--- + +## 7. 故障排查 + +| 现象 | 原因 | 解决方案 | +| ------------------------------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| `channels add` 向导崩溃报 ENOENT | v2026.4.7 Telegram 插件路径 bug | 跳过向导,直接编辑 `~/.openclaw/openclaw.json` | +| Gateway 拒绝启动:"missing gateway.mode" | 配置文件缺少 `gateway` 配置节 | 添加 `"gateway": {"mode": "local"}` | +| "Blocked hostname or private/internal/special-use IP address" | OpenClaw 默认阻止连接私有 IP | 添加 `"network": {"dangerouslyAllowPrivateNetwork": true}` | +| Matrix 连接失败,反复重试 | `homeserver` 使用了 `https` 但本地 Palpo 没有 TLS | 改为 `http://127.0.0.1:8128` | +| 启动报 "Invalid input: expected record, received array" | `providers` 格式写成了数组 | `providers` 是对象(key-value),不是数组 | +| 启动报 "Unrecognized key: type" | 字段名写错 | 用 `"api"` 而不是 `"type"` | +| "missing env var DEEPSEEK_API_KEY" | 环境变量对 LaunchAgent 不可见 | API key 直接写进配置文件 | +| 消息发出但 Bot 不回复(无错误) | DM 默认加密,但 OpenClaw 没开 | 添加 `"encryption": true` | +| "encrypted event received without encryption enabled" | 同上 | 添加 `"encryption": true` | +| "This message was sent before this device logged in" | 历史消息无法解密 | 正常现象。发送**新消息**即可 | +| Bot 回复为空或报错 | LLM API Key 无效或余额不足 | 检查 DeepSeek API Key 和账户余额 | +| Robrix 搜索不到 Bot | Bot 账号未注册成功 | 确认 Bot 账号存在(在 Element Web 中验证) | +| 其他 OpenClaw 问题 | — | 查阅[OpenClaw 官方文档](https://docs.openclaw.ai/) 和 [GitHub Issues](https://github.com/openclaw/openclaw/issues) | + +> **重要说明:** 本指南仅覆盖我们实测验证过的配置流程(OpenClaw v2026.4.7)。OpenClaw 本身仍在快速迭代中,其 CLI、插件系统、Gateway 行为可能在后续版本中发生变化。如果你遇到本指南中未列出的 OpenClaw 问题(如 CLI 报错、插件加载失败、Gateway 行为异常等),这些属于 OpenClaw 自身的问题,请参考以下资源: +> +> - [OpenClaw 官方文档](https://docs.openclaw.ai/) — 最新配置参考 +> - [OpenClaw Matrix 频道插件文档](https://docs.openclaw.ai/channels/matrix) — Matrix 插件专项 +> - [OpenClaw GitHub Issues](https://github.com/openclaw/openclaw/issues) — 已知问题和社区讨论 +> +> Robrix 作为标准 Matrix 客户端,与 OpenClaw 之间通过 Matrix 协议通信,两者完全解耦。Robrix 侧无需任何特殊配置。 + +--- + +## 8. 生产环境配置 + +测试通过后,收紧权限。修改 `channels.matrix` 中的以下字段: + +```json +{ + "autoJoin": "allowlist", + "autoJoinAllowlist": ["!room-id:your-server"], + "dm": { + "policy": "allowlist", + "allowFrom": ["@admin:your-server"], + "sessionScope": "per-room" + }, + "groupPolicy": "allowlist", + "groupAllowFrom": ["@admin:your-server"], + "groups": { + "!room-id:your-server": { + "requireMention": true + } + } +} +``` + +| 字段 | 测试值 | 生产值 | 说明 | +| ------------------ | ------------ | --------------- | ------------------------ | +| `autoJoin` | `"always"` | `"allowlist"` | 只加入白名单中的房间 | +| `dm.policy` | `"open"` | `"allowlist"` | 只接受白名单用户的私聊 | +| `groupPolicy` | — | `"allowlist"` | 群聊中限制谁可以触发 Bot | +| `requireMention` | — | `true` | 群聊中必须 @Bot 才响应 | + +--- + +## 9. 延伸阅读 + +- **OpenClaw 文档:** [docs.openclaw.ai](https://docs.openclaw.ai/) — OpenClaw 完整文档。 +- **OpenClaw Matrix 插件:** [docs.openclaw.ai/channels/matrix](https://docs.openclaw.ai/channels/matrix) — 官方 Matrix 频道插件参考。 +- **OpenClaw GitHub:** [github.com/openclaw/openclaw](https://github.com/openclaw/openclaw) — 源码、Issues 和最新发布。 +- **Palpo 部署指南:** [01-deploying-palpo-and-octos-zh.md](../robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md) — 如何部署本地 Palpo 服务器。 +- **架构原理:** [03-how-robrix-and-openclaw-work-together-zh.md](03-how-robrix-and-openclaw-work-together-zh.md) — OpenClaw 如何连接 Matrix,以及与 Octos AppService 模式的对比。 +- **使用指南:** [02-using-robrix-with-openclaw-zh.md](02-using-robrix-with-openclaw-zh.md) — 如何使用 Robrix 与 OpenClaw 代理对话。 + +--- + +*本指南基于 2026 年 4 月的实测结果编写(OpenClaw v2026.4.7 + Palpo)。OpenClaw 正在快速迭代中,如遇到问题请以 [官方文档](https://docs.openclaw.ai/) 为准。* diff --git a/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md new file mode 100644 index 000000000..bad16f4f2 --- /dev/null +++ b/docs/robrix-with-openclaw/01-deploying-openclaw-with-matrix.md @@ -0,0 +1,380 @@ +# Deployment Guide: OpenClaw + Matrix + +[中文版](01-deploying-openclaw-with-matrix-zh.md) + +> **Goal:** After following this guide, you will have OpenClaw running as an AI agent connected to a Matrix homeserver. You can then use Robrix (or any Matrix client) to chat with OpenClaw-powered AI agents. + +## What is OpenClaw? + +[OpenClaw](https://github.com/openclaw/openclaw) is an open-source, self-hosted AI assistant platform (formerly MoltBot, renamed in early 2026). It supports multiple LLMs (DeepSeek, OpenAI, Anthropic, etc.) and connects to Matrix, Telegram, Discord, and other chat platforms via channel plugins. In this guide, OpenClaw logs in to the Matrix server as a **regular user** through its **Matrix channel plugin**, requiring no server-side configuration. This is simpler than Octos's Application Service approach -- see [Architecture Guide](03-how-robrix-and-openclaw-work-together.md) for details. + +This guide walks you through deploying OpenClaw with Matrix step by step: from creating a Matrix bot account, to configuring the OpenClaw Matrix channel plugin, to verifying the connection end-to-end. + +> **Just want to try it quickly?** Jump to [Quick Start](#2-quick-start). +> +> **Want to understand HOW OpenClaw connects to Matrix?** See [Architecture](03-how-robrix-and-openclaw-work-together.md) for the full explanation. + +> **About OpenClaw:** OpenClaw is under rapid development and its CLI and plugin system have a number of bugs (e.g., the `channels add` wizard may crash). This guide documents a configuration approach we have **tested and verified** -- editing the config file directly, bypassing the unstable CLI wizards. If you encounter issues not covered here, consult the [OpenClaw official documentation](https://docs.openclaw.ai/) and [GitHub Issues](https://github.com/openclaw/openclaw/issues). + +--- + +## Table of Contents + +1. [Prerequisites](#1-prerequisites) +2. [Quick Start](#2-quick-start) +3. [Creating a Matrix Bot Account](#3-creating-a-matrix-bot-account) +4. [Installing OpenClaw and Initializing the Config Directory](#4-installing-openclaw-and-initializing-the-config-directory) +5. [Writing the Configuration File](#5-writing-the-configuration-file) +6. [Starting and Verifying](#6-starting-and-verifying) +7. [Troubleshooting](#7-troubleshooting) +8. [Production Configuration](#8-production-configuration) +9. [Further Reading](#9-further-reading) + +--- + +## 1. Prerequisites + +| Requirement | Notes | +| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| **Two Matrix accounts** | One for yourself, one for the OpenClaw bot | +| **Node.js** | v22.16+ or v24+ (recommended) | +| **LLM API Key** | e.g.,[DeepSeek](https://platform.deepseek.com/) (free tier available), OpenAI, Anthropic, etc. | +| **Matrix homeserver** | Local Palpo (recommended, see[Palpo Deployment Guide](../robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md)) or public server matrix.org | +| **Robrix** | See[Getting Started with Robrix](../robrix/getting-started-with-robrix.md) | + +--- + +## 2. Quick Start + +``` +1. Register a Matrix bot account (remember the username and password) +2. Install OpenClaw → run openclaw config → edit ~/.openclaw/openclaw.json +3. Run openclaw gateway start +4. In Robrix, message the bot from another account +``` + +See below for detailed steps. + +--- + +## 3. Creating a Matrix Bot Account + +A bot account is just a **regular Matrix account**. OpenClaw logs in automatically using the username and password -- you do not need to manually obtain an Access Token. + +| Server | How to register | Notes | +| ----------------------------- | ------------------------------------------------------- | --------------------------------------------------------------- | +| **Local Palpo** | Register in Robrix | Connect to `http://127.0.0.1:8128` and register a new account | +| **matrix.org** | Register in Robrix or[Element Web](https://app.element.io) | Public server, free, instant | +| **Self-hosted Synapse** | Via Admin API or web registration | Recommended for production | + +When registering, remember: + +- **Username** (e.g., `chalice`) +- **Password** + +--- + +## 4. Installing OpenClaw and Initializing the Config Directory + +### 4.1 Install + +```bash +npm install -g openclaw@latest +openclaw --version # verify installation +``` + +### 4.2 Initialize the config directory + +```bash +openclaw config +``` + +> **This command will output an error -- that is expected and can be safely ignored.** The important thing is that it creates the config directory structure under `~/.openclaw/`. All subsequent configuration is done in this directory. + +> **Why not use the `openclaw channels add` wizard?** OpenClaw v2026.4.7's CLI wizard has multiple bugs (Telegram plugin path error crashes the wizard, incomplete parameters, etc.). **Editing the config file directly is the only reliable approach.** + +--- + +## 5. Writing the Configuration File + +Edit `~/.openclaw/openclaw.json`. Two complete configurations are provided below for different scenarios. + +### 5.1 Connecting to Local Palpo (Recommended) + +```json +{ + "commands": { + "native": "auto", + "nativeSkills": "auto" + }, + "models": { + "providers": { + "deepseek": { + "baseUrl": "https://api.deepseek.com/v1", + "apiKey": "sk-your-deepseek-key", + "api": "openai-completions", + "models": [ + { + "id": "deepseek-chat", + "name": "DeepSeek Chat", + "contextWindow": 164000, + "maxTokens": 8192 + } + ] + } + } + }, + "channels": { + "matrix": { + "enabled": true, + "homeserver": "http://127.0.0.1:8128", + "network": { + "dangerouslyAllowPrivateNetwork": true + }, + "userId": "@chalice:127.0.0.1:8128", + "password": "your-password", + "deviceName": "OpenClaw Bot", + "encryption": true, + "autoJoin": "always", + "dm": { + "policy": "open" + } + } + }, + "plugins": { + "entries": { + "matrix": { + "enabled": true + } + } + }, + "gateway": { + "mode": "local" + } +} +``` + +### 5.2 Connecting to Public matrix.org + +```json +{ + "commands": { + "native": "auto", + "nativeSkills": "auto" + }, + "models": { + "providers": { + "deepseek": { + "baseUrl": "https://api.deepseek.com/v1", + "apiKey": "sk-your-deepseek-key", + "api": "openai-completions", + "models": [ + { + "id": "deepseek-chat", + "name": "DeepSeek Chat", + "contextWindow": 164000, + "maxTokens": 8192 + } + ] + } + } + }, + "channels": { + "matrix": { + "enabled": true, + "homeserver": "https://matrix.org", + "userId": "@your-bot:matrix.org", + "password": "your-password", + "deviceName": "OpenClaw Bot", + "encryption": true, + "autoJoin": "always", + "dm": { + "policy": "open" + } + } + }, + "plugins": { + "entries": { + "matrix": { + "enabled": true + } + } + }, + "gateway": { + "mode": "local" + } +} +``` + +### 5.3 Configuration Details + +#### `gateway` Configuration + +| Field | Value | Key Notes | +| -------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `mode` | `"local"` | **Required.** Without this field, gateway refuses to start with "missing gateway.mode" error. `"local"` means the OpenClaw gateway itself runs locally (listens on 127.0.0.1 only) -- this is unrelated to whether the LLM is remote. DeepSeek API calls still go over the internet. | + +#### `models.providers` Configuration + +| Field | Value | Key Notes | +| ----------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `baseUrl` | `"https://api.deepseek.com/v1"` | **Must include the `/v1` suffix.** DeepSeek uses an OpenAI-compatible API. | +| `apiKey` | `"sk-xxx"` | **Write the key directly as plaintext.** Do not use `${ENV_VAR}` format -- macOS LaunchAgent services cannot read terminal environment variables. After writing, run `chmod 600 ~/.openclaw/openclaw.json` to protect file permissions. | +| `api` | `"openai-completions"` | **Not `type`.** Many online tutorials incorrectly use `"type"` -- the correct field name is `"api"`. | +| `contextWindow` | `164000` | **Must be set high.** OpenClaw's system prompt alone takes 16K+ tokens; the default 4096 will cause errors. DeepSeek Chat supports 164K. | +| `maxTokens` | `8192` | Maximum tokens per reply. | + +> **Note on `providers` format:** `providers` is an object (provider name as key), not an array. `models` inside a provider IS an array. + +#### `channels.matrix` Configuration + +| Field | Value | Key Notes | +| ------------------------------------------ | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `enabled` | `true` | Enable the Matrix channel. | +| `homeserver` | `"http://127.0.0.1:8128"` | **Local Palpo must use `http`**, not `https` (Palpo has no TLS by default). matrix.org uses `https`. | +| `network.dangerouslyAllowPrivateNetwork` | `true` | **Only needed for local/LAN deployments.** OpenClaw blocks private IPs (127.0.0.1, 10.x, 192.168.x) by default as an anti-SSRF security measure. Not needed when connecting to public servers like matrix.org. | +| `userId` | `"@chalice:127.0.0.1:8128"` | **Must be the full Matrix ID format** `@username:server`. | +| `password` | `"your-password"` | Password authentication -- OpenClaw logs in automatically and caches the token at `~/.openclaw/credentials/matrix/`. Access Token authentication is also supported (replace `password` with `accessToken`) -- see [OpenClaw Matrix Plugin Docs](https://docs.openclaw.ai/channels/matrix). | +| `encryption` | `true` | **Strongly recommended.** Matrix DMs enable E2EE by default. Without this, the bot receives encrypted messages it cannot decrypt, resulting in "message sent but no reply". | +| `autoJoin` | `"always"` | Accept all invites during testing. Change to `"allowlist"` in production. | +| `dm.policy` | `"open"` | Allow all DMs during testing. Change to `"allowlist"` in production. | + +#### `plugins` Configuration + +| Field | Value | Key Notes | +| ---------------------------------- | -------- | ------------------------------------ | +| `plugins.entries.matrix.enabled` | `true` | Ensure the Matrix plugin is enabled. | + +### 5.4 Local Palpo vs Public matrix.org Differences + +| Setting | Local Palpo | Public matrix.org | +| ------------------------------------------ | ------------------------------------- | ---------------------------------------------------------- | +| `homeserver` | `http://127.0.0.1:8128` | `https://matrix.org` | +| `network.dangerouslyAllowPrivateNetwork` | **Required** `true` | **Not needed** (remove the entire `network` block) | +| `userId` format | `@username:127.0.0.1:8128` | `@username:matrix.org` | +| TLS | None (`http`) | Yes (`https`) | +| Registration | Register in Robrix connected to Palpo | Register via Element Web or Robrix | + +> **Switching from local Palpo to matrix.org:** This guide uses Palpo as the example, but the same configuration works on matrix.org or any standard Matrix server. You only need to change 3 things in `openclaw.json`: +> +> 1. `homeserver`: `http://127.0.0.1:8128` → `https://matrix.org` +> 2. `userId`: `@username:127.0.0.1:8128` → `@username:matrix.org` +> 3. Remove the entire `"network": { "dangerouslyAllowPrivateNetwork": true }` block (not needed for public servers) +> +> Everything else (LLM, encryption, autoJoin, etc.) **stays exactly the same**. After editing, run `openclaw gateway restart`. +> +> If you use another self-hosted Matrix server (e.g., Synapse, Dendrite), the same 3 changes apply -- just replace with your server's domain and protocol. + +--- + +## 6. Starting and Verifying + +### 6.1 Start the Gateway + +```bash +openclaw gateway start +``` + +### 6.2 Check the Logs + +```bash +tail -20 ~/.openclaw/logs/gateway.log +``` + +Confirm you see these key log lines: + +``` +[gateway] agent model: deepseek/deepseek-chat ← LLM config is correct +[gateway] ready (6 plugins, 0.3s) ← Gateway is ready +[matrix] [default] starting provider (http://...) ← Matrix connecting +matrix: logged in as @chalice:127.0.0.1:8128 ← Login successful +matrix: device is verified by its owner and ready for encrypted rooms ← Encryption ready +``` + +> **Tip:** OpenClaw needs some time to complete the Matrix login and encryption device initialization after startup. If you only see `starting provider` but not `logged in` in the logs, wait a few seconds -- messages can only be received after login completes. + +### 6.3 Test in Robrix + +1. **Launch Robrix** and log in with your **personal account** +2. **Search for the bot**: Click the search icon, type the bot's Matrix ID (e.g., `@chalice:127.0.0.1:8128`), switch to the **People** tab (the bot is a regular user, so you must search under People) +3. **Start a DM**: Select the bot to enter a conversation +4. **Send a message** and wait patiently for a reply (LLM responses take a few seconds to tens of seconds) + +OpenClaw Bot (chalice) successfully replying in Robrix + +> **Important:** If you sent messages before OpenClaw's encryption device was created, those historical messages **can never be decrypted** (this is normal Matrix E2EE behavior). You must send a **new message** to trigger a reply. + +--- + +## 7. Troubleshooting + +| Symptom | Cause | Solution | +| ------------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| `channels add` wizard crashes with ENOENT | v2026.4.7 Telegram plugin path bug | Skip the wizard, edit `~/.openclaw/openclaw.json` directly | +| Gateway refuses to start: "missing gateway.mode" | Config file missing `gateway` section | Add `"gateway": {"mode": "local"}` | +| "Blocked hostname or private/internal/special-use IP address" | OpenClaw blocks private IPs by default | Add `"network": {"dangerouslyAllowPrivateNetwork": true}` | +| Matrix connection fails, keeps retrying | `homeserver` uses `https` but local Palpo has no TLS | Change to `http://127.0.0.1:8128` | +| "Invalid input: expected record, received array" | `providers` format is wrong | `providers` must be an object (key-value), not an array | +| "Unrecognized key: type" | Wrong field name | Use `"api"` instead of `"type"` | +| "missing env var DEEPSEEK_API_KEY" | Environment variable not visible to LaunchAgent | Write API key directly in the config file | +| Message sent but bot does not reply (no error) | DM is encrypted but OpenClaw has encryption disabled | Add `"encryption": true` | +| "encrypted event received without encryption enabled" | Same as above | Add `"encryption": true` | +| "This message was sent before this device logged in" | Historical messages cannot be decrypted | Normal behavior. Send a**new message** | +| Bot replies are empty or error | LLM API key invalid or insufficient balance | Check DeepSeek API key and account balance | +| Robrix cannot find the bot | Bot account not registered | Confirm the bot account exists (verify in Element Web) | +| Other OpenClaw issues | — | Consult[OpenClaw docs](https://docs.openclaw.ai/) and [GitHub Issues](https://github.com/openclaw/openclaw/issues) | + +> **Important note:** This guide only covers the configuration workflow we have tested and verified (OpenClaw v2026.4.7). OpenClaw is still under rapid development -- its CLI, plugin system, and gateway behavior may change in future versions. If you encounter OpenClaw issues not listed above (CLI errors, plugin loading failures, gateway behavior anomalies, etc.), these are OpenClaw-side issues. Please refer to: +> +> - [OpenClaw Official Documentation](https://docs.openclaw.ai/) -- latest configuration reference +> - [OpenClaw Matrix Channel Plugin Docs](https://docs.openclaw.ai/channels/matrix) -- Matrix plugin specifics +> - [OpenClaw GitHub Issues](https://github.com/openclaw/openclaw/issues) -- known issues and community discussions +> +> Robrix, as a standard Matrix client, communicates with OpenClaw through the Matrix protocol. The two are fully decoupled -- no special configuration is needed on the Robrix side. + +--- + +## 8. Production Configuration + +After testing, tighten permissions. Modify these fields in `channels.matrix`: + +```json +{ + "autoJoin": "allowlist", + "autoJoinAllowlist": ["!room-id:your-server"], + "dm": { + "policy": "allowlist", + "allowFrom": ["@admin:your-server"], + "sessionScope": "per-room" + }, + "groupPolicy": "allowlist", + "groupAllowFrom": ["@admin:your-server"], + "groups": { + "!room-id:your-server": { + "requireMention": true + } + } +} +``` + +| Field | Testing | Production | Purpose | +| ------------------ | ------------ | --------------- | ------------------------------------------- | +| `autoJoin` | `"always"` | `"allowlist"` | Only join allowlisted rooms | +| `dm.policy` | `"open"` | `"allowlist"` | Only accept DMs from allowlisted users | +| `groupPolicy` | — | `"allowlist"` | Restrict who can trigger the bot in groups | +| `requireMention` | — | `true` | In group chats, require @mention to respond | + +--- + +## 9. Further Reading + +- **OpenClaw Documentation:** [docs.openclaw.ai](https://docs.openclaw.ai/) -- full OpenClaw documentation. +- **OpenClaw Matrix Plugin:** [docs.openclaw.ai/channels/matrix](https://docs.openclaw.ai/channels/matrix) -- official Matrix channel plugin reference. +- **OpenClaw GitHub:** [github.com/openclaw/openclaw](https://github.com/openclaw/openclaw) -- source code, issues, and latest releases. +- **Palpo Deployment Guide:** [01-deploying-palpo-and-octos.md](../robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md) -- how to deploy a local Palpo homeserver. +- **Architecture Guide:** [03-how-robrix-and-openclaw-work-together.md](03-how-robrix-and-openclaw-work-together.md) -- how OpenClaw connects to Matrix, and comparison with the Octos AppService model. +- **Usage Guide:** [02-using-robrix-with-openclaw.md](02-using-robrix-with-openclaw.md) -- how to use Robrix to chat with OpenClaw agents. + +--- + +*This guide is based on tested results from April 2026 (OpenClaw v2026.4.7 + Palpo). OpenClaw is under rapid development -- if you encounter issues, refer to the [official documentation](https://docs.openclaw.ai/) for the latest information.* diff --git a/docs/robrix-with-openclaw/02-using-robrix-with-openclaw-zh.md b/docs/robrix-with-openclaw/02-using-robrix-with-openclaw-zh.md new file mode 100644 index 000000000..82a9f9ac4 --- /dev/null +++ b/docs/robrix-with-openclaw/02-using-robrix-with-openclaw-zh.md @@ -0,0 +1,125 @@ +# 使用指南:Robrix + OpenClaw + +[English](02-using-robrix-with-openclaw.md) + +> **目标:** 完成本指南后,你将知道如何使用 Robrix 与 OpenClaw AI 代理对话 —— 包括发起会话、使用私聊和房间、以及了解 OpenClaw 功能在 Robrix 中的表现。 + +本指南假设你已经完成了 [部署指南](01-deploying-openclaw-with-matrix-zh.md) 中的配置,OpenClaw gateway 正在运行。 + +**快速索引** + +| 你想做什么 | 跳转到 | +|---|---| +| 与 Bot 发起私聊 | [第 2 节](#2-发起私聊) | +| 邀请 Bot 进入房间 | [第 3 节](#3-在房间中使用) | +| 了解功能兼容性 | [第 4 节](#4-openclaw-功能在-robrix-中的表现) | +| 与 Octos 工作流对比 | [第 5 节](#5-与-octos-工作流的区别) | + +--- + +## 1. 开始之前 + +确认以下条件: + +- [ ] OpenClaw gateway 正在运行(`openclaw gateway status` 显示 `running`) +- [ ] 日志中显示 `matrix: logged in as @bot-name:server` +- [ ] 你有另一个 Matrix 账号(个人账号)用于和 Bot 对话 +- [ ] Robrix 已安装并能连接到同一个 Matrix 服务器 + +--- + +## 2. 发起私聊 + +### 2.1 搜索 Bot + +1. 打开 Robrix,用你的**个人账号**登录 +2. 点击顶部的**搜索图标** +3. 输入 Bot 的完整 Matrix ID,例如 `@chalice:127.0.0.1:8128` +4. 切换到 **People** 标签页(Bot 是普通用户身份,必须在 People 中搜索) + +在 Robrix 中搜索 OpenClaw Bot + +### 2.2 发送第一条消息 + +1. 选择 Bot,进入对话 +2. 输入消息(例如 "你好"),按回车 +3. 等待 Bot 回复 + +> **提示:** LLM 生成回复需要时间,特别是较长的回答可能需要等待几秒到十几秒。请耐心等待,不要重复发送消息。 + +OpenClaw Bot(chalice)成功回复消息 + +> **注意:** 如果 Bot 刚刚部署完成,你之前发送的消息可能无法被解密(因为那些消息的加密密钥没有分发给 Bot 的设备)。这是正常的 Matrix E2EE 行为——发送**新消息**即可。 + +### 2.3 多轮对话 + +OpenClaw 会保持对话上下文。你可以连续提问,Bot 会记住之前的对话内容。上下文窗口大小取决于 LLM 配置(DeepSeek Chat 支持 164K token)。 + +--- + +## 3. 在房间中使用 + +除了私聊,你还可以邀请 Bot 进入群聊房间。 + +### 3.1 创建房间并邀请 Bot + +1. 在 Robrix 中创建一个新房间 +2. 邀请 Bot(输入 Bot 的 Matrix ID) +3. Bot 会自动加入(因为配置了 `autoJoin: "always"`) + +OpenClaw Bot(chalice)接受邀请加入房间,和普通用户一样 + +> 可以看到 chalice 以普通用户身份接受了邀请加入房间——这就是客户端模式的特点,Bot 和普通用户没有任何区别。 + +### 3.2 在房间中对话 + +- **默认行为:** Bot 会回复房间内的所有消息 +- **如果配置了 `requireMention: true`:** 需要在消息中 @Bot 才会触发回复 + +在房间中与 OpenClaw Bot 对话 + +--- + +## 4. OpenClaw 功能在 Robrix 中的表现 + +| OpenClaw 功能 | Robrix 表现 | 说明 | +|--------------|-------------|------| +| **文字消息** | 完全支持 | 标准 Matrix 消息,无兼容性问题 | +| **多轮上下文** | 完全支持 | OpenClaw 自动维护对话历史 | +| **E2EE 加密** | 完全支持 | 消息在传输中全程加密 | + +--- + +## 5. 与 Octos 工作流的区别 + +如果你之前使用过 Robrix + Palpo + Octos,以下是主要区别: + +| | OpenClaw | Octos | +|---|---|---| +| **Bot 管理** | 无 BotFather 系统。一个 OpenClaw 实例 = 一个 Bot。 | BotFather 可以动态创建多个子 Bot | +| **创建新 Bot** | 部署一个新的 OpenClaw 实例 | 在聊天中输入 `/createbot` 命令 | +| **Bot 发现** | 需要知道 Bot 的 Matrix ID | 可以用 `/listbots` 查看所有可用 Bot | +| **访问控制** | 通过 OpenClaw 的 `dm.policy` 配置 | 通过 AppService 命名空间和 `allowed_senders` | +| **服务器端设置** | 无需任何设置 | 需要注册 AppService YAML | +| **Robrix Bot 设置面板** | 不使用 | 用于配置 BotFather 和创建子 Bot | + +> 想深入了解两种模式的技术差异?参见 [架构原理](03-how-robrix-and-openclaw-work-together-zh.md)。 + +--- + +## 6. 使用技巧 + +- **私聊 vs 房间**:私聊更适合个人助手场景,Bot 回复所有消息。房间适合团队协作,可以配置 `requireMention` 避免 Bot 过度回复。 +- **切换 LLM**:修改 `~/.openclaw/openclaw.json` 中的 `models.providers` 配置,然后 `openclaw gateway restart`。 +- **Bot 不响应?** 常见原因:LLM API Key 过期、加密设备未验证、`autoJoin` 配置问题。查看 [部署指南 - 故障排查](01-deploying-openclaw-with-matrix-zh.md#7-故障排查)。 + +--- + +## 接下来 + +- [部署指南](01-deploying-openclaw-with-matrix-zh.md) — 配置和部署 OpenClaw + Matrix +- [架构原理](03-how-robrix-and-openclaw-work-together-zh.md) — 了解 OpenClaw 客户端模式 vs Octos AppService 模式 + +--- + +*本指南基于 2026 年 4 月的使用方式编写。最新更新请参见各项目仓库。* diff --git a/docs/robrix-with-openclaw/02-using-robrix-with-openclaw.md b/docs/robrix-with-openclaw/02-using-robrix-with-openclaw.md new file mode 100644 index 000000000..b2ac11d5c --- /dev/null +++ b/docs/robrix-with-openclaw/02-using-robrix-with-openclaw.md @@ -0,0 +1,125 @@ +# Usage Guide: Robrix + OpenClaw + +[中文版](02-using-robrix-with-openclaw-zh.md) + +> **Goal:** After following this guide, you will know how to use Robrix to chat with OpenClaw AI agents -- including starting conversations, using DMs and rooms, and understanding how OpenClaw features appear in Robrix. + +This guide assumes you have completed the [Deployment Guide](01-deploying-openclaw-with-matrix.md) and the OpenClaw gateway is running. + +**Quick Reference** + +| What you want to do | Go to | +|---|---| +| Start a DM with the bot | [Section 2](#2-starting-a-dm) | +| Invite the bot to a room | [Section 3](#3-using-the-bot-in-rooms) | +| Understand feature compatibility | [Section 4](#4-openclaw-features-in-robrix) | +| Compare with Octos workflow | [Section 5](#5-differences-from-octos-workflow) | + +--- + +## 1. Before You Start + +Confirm the following: + +- [ ] OpenClaw gateway is running (`openclaw gateway status` shows `running`) +- [ ] Logs show `matrix: logged in as @bot-name:server` +- [ ] You have another Matrix account (your personal account) to chat with the bot +- [ ] Robrix is installed and can connect to the same Matrix server + +--- + +## 2. Starting a DM + +### 2.1 Search for the Bot + +1. Open Robrix and log in with your **personal account** +2. Click the **search icon** at the top +3. Type the bot's full Matrix ID, e.g., `@chalice:127.0.0.1:8128` +4. Switch to the **People** tab (the bot is a regular user, so you must search under People) + +Searching for OpenClaw bot in Robrix + +### 2.2 Send the First Message + +1. Select the bot to enter the conversation +2. Type a message (e.g., "Hello"), press Enter +3. Wait for the bot to reply + +> **Tip:** LLM responses take time to generate, especially for longer answers (a few seconds to tens of seconds). Please be patient and avoid sending duplicate messages. + +OpenClaw Bot (chalice) successfully replying + +> **Note:** If the bot was just deployed, messages you sent earlier may not be decryptable (because those messages' encryption keys were not distributed to the bot's device). This is normal Matrix E2EE behavior -- send a **new message** instead. + +### 2.3 Multi-Turn Conversation + +OpenClaw maintains conversation context. You can ask follow-up questions and the bot will remember previous messages. The context window size depends on the LLM configuration (DeepSeek Chat supports 164K tokens). + +--- + +## 3. Using the Bot in Rooms + +In addition to DMs, you can invite the bot to group chat rooms. + +### 3.1 Create a Room and Invite the Bot + +1. Create a new room in Robrix +2. Invite the bot (type the bot's Matrix ID) +3. The bot joins automatically (because `autoJoin: "always"` is configured) + +OpenClaw Bot (chalice) accepts invitation and joins the room, just like a regular user + +> Notice that chalice accepts the invitation and joins the room as a regular user -- this is a key characteristic of the client mode. The bot is indistinguishable from any other user. + +### 3.2 Chat in the Room + +- **Default behavior:** The bot responds to all messages in the room +- **If `requireMention: true` is configured:** You need to @mention the bot to trigger a reply + +Chatting with OpenClaw Bot in a room + +--- + +## 4. OpenClaw Features in Robrix + +| OpenClaw Feature | Robrix Support | Notes | +|------------------|---------------|-------| +| **Text messages** | Fully supported | Standard Matrix messages, no compatibility issues | +| **Multi-turn context** | Fully supported | OpenClaw automatically maintains conversation history | +| **E2EE encryption** | Fully supported | Messages are encrypted end-to-end | + +--- + +## 5. Differences from Octos Workflow + +If you have previously used Robrix + Palpo + Octos, here are the key differences: + +| | OpenClaw | Octos | +|---|---|---| +| **Bot management** | No BotFather system. One OpenClaw instance = one bot. | BotFather can dynamically create multiple child bots | +| **Creating new bots** | Deploy a new OpenClaw instance | Type `/createbot` command in chat | +| **Bot discovery** | Need to know the bot's Matrix ID | Use `/listbots` to see all available bots | +| **Access control** | Via OpenClaw's `dm.policy` configuration | Via AppService namespaces and `allowed_senders` | +| **Server-side setup** | None required | Must register AppService YAML | +| **Robrix Bot Settings panel** | Not used | Used to configure BotFather and create child bots | + +> Want to understand the technical differences in depth? See [Architecture Guide](03-how-robrix-and-openclaw-work-together.md). + +--- + +## 6. Tips + +- **DM vs Room**: DMs are better for personal assistant use cases -- the bot replies to all messages. Rooms are better for team collaboration; configure `requireMention` to prevent excessive replies. +- **Switching LLMs**: Edit the `models.providers` section in `~/.openclaw/openclaw.json`, then run `openclaw gateway restart`. +- **Bot not responding?** Common causes: expired LLM API key, unverified encryption device, `autoJoin` configuration issues. See [Deployment Guide - Troubleshooting](01-deploying-openclaw-with-matrix.md#7-troubleshooting). + +--- + +## What's Next + +- [Deployment Guide](01-deploying-openclaw-with-matrix.md) -- set up and configure OpenClaw with Matrix +- [Architecture Guide](03-how-robrix-and-openclaw-work-together.md) -- understand OpenClaw client mode vs Octos AppService mode + +--- + +*This guide covers usage as of April 2026. For the latest updates, see the respective project repositories.* diff --git a/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together-zh.md b/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together-zh.md new file mode 100644 index 000000000..b22091904 --- /dev/null +++ b/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together-zh.md @@ -0,0 +1,213 @@ +# 架构原理:Robrix 与 OpenClaw 如何协作 + +[English](03-how-robrix-and-openclaw-work-together.md) + +> **目标:** 阅读本文档后,你将理解 OpenClaw 如何以普通客户端身份连接 Matrix,消息如何在 Robrix、Matrix 服务器和 OpenClaw AI 代理之间流转,以及这与 Octos 使用的 Application Service 模式有何本质区别。 + +本文档解释 Robrix + OpenClaw 系统背后的**机制**。如需部署,请参见 [部署指南](01-deploying-openclaw-with-matrix-zh.md)。如需使用,请参见 [使用指南](02-using-robrix-with-openclaw-zh.md)。 + +--- + +## 目录 + +1. [两个项目概览](#1-两个项目概览) +2. [OpenClaw 如何连接 Matrix](#2-openclaw-如何连接-matrix) +3. [消息生命周期](#3-消息生命周期) +4. [客户端模式 vs Application Service 模式](#4-客户端模式-vs-application-service-模式) +5. [端到端加密(E2EE)](#5-端到端加密e2ee) +6. [延伸阅读](#6-延伸阅读) + +--- + +## 1. 两个项目概览 + +| 项目 | 角色 | 作用 | +|------|------|------| +| [**Robrix**](https://github.com/Project-Robius-China/robrix2) | Matrix 客户端 | 使用 Rust + [Makepad](https://github.com/makepad/makepad/) 构建的跨平台 Matrix 聊天客户端。这是用户界面——你在这里读写消息。 | +| [**OpenClaw**](https://github.com/openclaw/openclaw) | AI 代理框架 | 开源 AI 助手平台,通过 Matrix 频道插件以**普通客户端**身份登录 Matrix 服务器。接收用户消息,调用 LLM 生成回复,再发送回房间。 | + +两个项目完全独立。OpenClaw 不是专为 Robrix 设计的,也不是专为 Matrix 设计的——它支持 Telegram、Discord、Slack 等多种频道。Matrix 只是其中之一。 + +--- + +## 2. OpenClaw 如何连接 Matrix + +OpenClaw 通过 **Client-Server API** 连接 Matrix,方式和 Robrix 一样——它就是一个普通的 Matrix 客户端。 + +### 连接流程 + +``` +1. OpenClaw 启动时,用 userId + password 调用 POST /_matrix/client/v3/login +2. 服务器返回 access_token,OpenClaw 缓存到 ~/.openclaw/credentials/ +3. OpenClaw 开始 Sliding Sync 循环,持续拉取新事件 +4. 收到 m.room.message 事件时,提取消息内容,调用 LLM +5. LLM 返回回复后,OpenClaw 通过 PUT /_matrix/client/v3/rooms/{roomId}/send/ 发送回复 +``` + +### 关键特征 + +- **登录方式**:用户名 + 密码(和普通用户一样) +- **消息获取**:通过 Sync 主动拉取(不是服务器推送) +- **权限级别**:和普通用户完全一样(受速率限制、需要被邀请才能加入房间) +- **底层 SDK**:OpenClaw 的 Matrix 插件基于 [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk)(官方 JavaScript SDK) + +--- + +## 3. 消息生命周期 + +### 数据流图 + +``` +用户在 Robrix 中输入 "你好" + | + v ++-----------------+ +| 1. Robrix 发送 | PUT /_matrix/client/v3/rooms/{roomId}/send/m.room.message +| 通过 CS API | -> Palpo (http://127.0.0.1:8128) ++--------+--------+ + | + v ++-----------------+ +| 2. Palpo 存储 | 事件写入 PostgreSQL +| 事件 | 房间状态更新 ++--------+--------+ + | + v ++-----------------+ +| 3. OpenClaw | 通过 Sliding Sync 拉取到新事件 +| 收到消息 | (OpenClaw 是普通客户端,主动同步) ++--------+--------+ + | + v ++-----------------+ +| 4. OpenClaw | POST https://api.deepseek.com/v1/chat/completions +| 调用 LLM | 携带对话历史作为上下文 ++--------+--------+ + | + v ++-----------------+ +| 5. OpenClaw | PUT /_matrix/client/v3/rooms/{roomId}/send/m.room.message +| 发送回复 | -> Palpo (http://127.0.0.1:8128) +| 通过 CS API | 认证:Bearer {access_token} ++--------+--------+ + | + v ++-----------------+ +| 6. Palpo 存储 | Bot 的回复事件写入数据库 +| 并推送 | Sliding Sync 推送到 Robrix ++--------+--------+ + | + v +用户在 Robrix 中看到 AI 回复 +``` + +### 架构图 + +``` ++----------+ +----------+ +----------+ +-----+ +| Robrix | Client-Server API | Palpo | Client-Server API | OpenClaw | HTTPS | LLM | +| (客户端) | --------------------> | (服务器) | <------------------> | (AI Bot) | ------> | | +| | <-------------------- | | Sliding Sync | | <------ | | ++----------+ Sliding Sync +----------+ +----------+ +-----+ + 你的电脑 Docker :8128 你的电脑 外部 API +``` + +**关键观察:** + +- **Robrix 和 OpenClaw 对 Palpo 来说地位完全平等** —— 都是通过 Client-Server API 连接的普通客户端。 +- **OpenClaw 不依赖服务器端配置** —— 不需要修改 Palpo 的任何配置文件(对比 Octos 需要注册 AppService YAML)。 +- **只有 LLM 调用离开本机** —— Robrix ↔ Palpo ↔ OpenClaw 全部在本地网络,只有 DeepSeek API 调用走公网。 + +--- + +## 4. 客户端模式 vs Application Service 模式 + +Robrix 生态中有两种接入 AI Bot 的方式:OpenClaw 使用的**客户端模式**和 Octos 使用的 **Application Service 模式**。它们的核心区别如下: + +### 4.1 连接机制对比 + +| | OpenClaw(客户端模式) | Octos(Application Service 模式) | +|---|---|---| +| **连接方式** | 和普通用户一样,用密码登录 | 通过 YAML 注册文件在服务器端注册 | +| **消息获取** | **Sync 拉取**——OpenClaw 主动轮询服务器获取新事件 | **服务器推送**——Palpo 主动将事件推送到 Octos 的 HTTP 端点 | +| **认证方式** | access_token(用户级别) | as_token / hs_token(服务级别,双向认证) | +| **服务器端配置** | **无需任何配置**——Bot 就是一个普通用户 | **需要注册**——在 Palpo 的 `appservice_registration_dir` 放置 YAML 文件 | +| **用户命名空间** | 只有一个用户 ID | 可以声明排他的用户命名空间,动态创建子 Bot | +| **速率限制** | 受限(和普通用户一样) | 不受限(`rate_limited: false`) | + +### 4.2 能力对比 + +| 能力 | OpenClaw | Octos | +|------|----------|-------| +| 基本对话 | 支持 | 支持 | +| 多模型切换 | 支持(14+ LLM provider) | 支持 | +| E2EE 加密 | 支持(Rust crypto SDK) | 不需要(AppService 绕过加密) | +| 服务器端管理 | 不需要 | 需要管理员权限注册 AppService | +| 多频道(Telegram、Discord 等) | 支持 | 仅 Matrix | +| 对 homeserver 的要求 | 任何标准 Matrix 服务器 | 需要支持 AppService API | + +### 4.3 消息延迟 + +| 环节 | OpenClaw | Octos | +|------|----------|-------| +| 消息到达 Bot | Sync 间隔(通常 1-5 秒) | 即时推送(< 100ms) | +| LLM 响应 | 取决于 LLM provider | 取决于 LLM provider | +| Bot 发送回复 | 即时 | 即时 | + +> OpenClaw 使用 Sliding Sync 的长轮询模式,实际延迟通常在 1-2 秒,对于聊天场景几乎感觉不到。 + +### 4.4 部署复杂度 + +| | OpenClaw | Octos | +|---|---|---| +| 服务器端 | 无需配置 | 需要放置注册 YAML、配置 token | +| 客户端 | 安装 OpenClaw + 编辑一个 JSON 文件 | 需要 Docker Compose 编排三个服务 | +| Token 管理 | 密码自动登录,token 自动缓存 | 需要手动生成并同步 as_token / hs_token | +| 架构复杂度 | 简单(一个进程) | 复杂(Palpo + Octos + PostgreSQL) | + +### 4.5 什么时候用哪个? + +| 场景 | 推荐方案 | +|------|---------| +| 快速测试 AI 对话 | **OpenClaw** —— 5 分钟配好,不需要碰服务器 | +| 个人 AI 助手 | **OpenClaw** —— 简单、灵活、支持多频道 | +| 团队内多个专业 Bot | **Octos** —— BotFather 可以动态创建多个子 Bot | +| 需要服务器端管理 | **Octos** —— AppService 由管理员注册和控制 | +| 高并发消息 | **Octos** —— 服务器推送 + 无速率限制 | +| 跨平台 AI 代理(同时接入 Telegram/Discord) | **OpenClaw** —— 原生支持多频道 | + +--- + +## 5. 端到端加密(E2EE) + +### OpenClaw 如何处理加密 + +OpenClaw 的 Matrix 插件使用 matrix-js-sdk 的 **Rust crypto 路径**,实现了 Olm(一对一密钥交换)和 Megolm(群组加密)协议。 + +当 `"encryption": true` 配置启用后: + +1. **首次登录**:OpenClaw 创建加密设备,生成 cross-signing identity +2. **自动引导**:执行 secret storage bootstrap,设备被标记为 "verified by its owner" +3. **接收消息**:OpenClaw 解密 Megolm 加密的消息 +4. **发送回复**:回复自动加密发送 + +### 注意事项 + +- **历史消息不可解密** —— 在 OpenClaw 设备创建之前发送的消息,其 Megolm 会话密钥未分发给 OpenClaw,永远无法解密。 +- **vs Octos** —— Octos 作为 AppService 接收的是**服务器端解密后的明文事件**,不需要处理 E2EE。OpenClaw 作为客户端必须自己处理加密。 + +--- + +## 6. 延伸阅读 + +- **OpenClaw 文档:** [docs.openclaw.ai](https://docs.openclaw.ai/) — OpenClaw 完整文档。 +- **OpenClaw Matrix 插件:** [docs.openclaw.ai/channels/matrix](https://docs.openclaw.ai/channels/matrix) — 官方 Matrix 频道插件参考。 +- **Matrix Client-Server API 规范:** [spec.matrix.org -- Client-Server API](https://spec.matrix.org/latest/client-server-api/) — OpenClaw 使用的协议。 +- **Matrix Application Service API 规范:** [spec.matrix.org -- Application Service API](https://spec.matrix.org/latest/application-service-api/) — Octos 使用的协议。 +- **Octos 架构原理:** [03-how-robrix-palpo-octos-work-together-zh.md](../robrix-with-palpo-and-octos/03-how-robrix-palpo-octos-work-together-zh.md) — Octos AppService 模式的完整解析。 +- **部署指南:** [01-deploying-openclaw-with-matrix-zh.md](01-deploying-openclaw-with-matrix-zh.md) — 如何部署 OpenClaw + Matrix。 +- **使用指南:** [02-using-robrix-with-openclaw-zh.md](02-using-robrix-with-openclaw-zh.md) — 如何使用 Robrix 与 OpenClaw 代理对话。 + +--- + +*本文档基于 2026 年 4 月的实测结果编写。最新更新请参见各项目仓库。* diff --git a/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together.md b/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together.md new file mode 100644 index 000000000..0a9d8560d --- /dev/null +++ b/docs/robrix-with-openclaw/03-how-robrix-and-openclaw-work-together.md @@ -0,0 +1,213 @@ +# Architecture: How Robrix and OpenClaw Work Together + +[中文版](03-how-robrix-and-openclaw-work-together-zh.md) + +> **Goal:** After reading this document, you will understand how OpenClaw connects to Matrix as a regular client, how messages flow between Robrix, the Matrix homeserver, and the OpenClaw AI agent, and how this fundamentally differs from the Application Service model used by Octos. + +This document explains the **mechanisms** behind the Robrix + OpenClaw system. If you want to deploy it, see [Deployment Guide](01-deploying-openclaw-with-matrix.md). If you want to use it, see [Usage Guide](02-using-robrix-with-openclaw.md). + +--- + +## Table of Contents + +1. [Two Projects Overview](#1-two-projects-overview) +2. [How OpenClaw Connects to Matrix](#2-how-openclaw-connects-to-matrix) +3. [Message Lifecycle](#3-message-lifecycle) +4. [Client Mode vs Application Service Mode](#4-client-mode-vs-application-service-mode) +5. [End-to-End Encryption (E2EE)](#5-end-to-end-encryption-e2ee) +6. [Further Reading](#6-further-reading) + +--- + +## 1. Two Projects Overview + +| Project | Role | What it does | +|---------|------|--------------| +| [**Robrix**](https://github.com/Project-Robius-China/robrix2) | Matrix Client | A cross-platform Matrix chat client built with Rust + [Makepad](https://github.com/makepad/makepad/). This is the user interface -- where you read and send messages. | +| [**OpenClaw**](https://github.com/openclaw/openclaw) | AI Agent Framework | An open-source AI assistant platform that connects to Matrix via its channel plugin as a **regular client**. Receives user messages, calls an LLM to generate replies, and sends them back to the room. | + +Both projects are completely independent. OpenClaw is not designed specifically for Robrix or for Matrix -- it supports Telegram, Discord, Slack, and other channels. Matrix is just one of them. + +--- + +## 2. How OpenClaw Connects to Matrix + +OpenClaw connects to Matrix via the **Client-Server API**, the same way Robrix does -- it is simply a regular Matrix client. + +### Connection Flow + +``` +1. On startup, OpenClaw calls POST /_matrix/client/v3/login with userId + password +2. Server returns an access_token, OpenClaw caches it at ~/.openclaw/credentials/ +3. OpenClaw starts a Sliding Sync loop, continuously pulling new events +4. When an m.room.message event arrives, it extracts the content and calls the LLM +5. After the LLM responds, OpenClaw sends the reply via PUT /_matrix/client/v3/rooms/{roomId}/send/ +``` + +### Key Characteristics + +- **Authentication**: Username + password (same as any regular user) +- **Message retrieval**: Via Sync (actively pulls from server, not pushed by server) +- **Permission level**: Identical to a regular user (rate-limited, must be invited to join rooms) +- **Underlying SDK**: OpenClaw's Matrix plugin uses [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk) (official JavaScript SDK) + +--- + +## 3. Message Lifecycle + +### Data Flow Diagram + +``` +User types "Hello" in Robrix + | + v ++-----------------+ +| 1. Robrix sends | PUT /_matrix/client/v3/rooms/{roomId}/send/m.room.message +| via CS API | -> Palpo (http://127.0.0.1:8128) ++--------+--------+ + | + v ++-----------------+ +| 2. Palpo stores | Event saved to PostgreSQL +| the event | Room state updated ++--------+--------+ + | + v ++-----------------+ +| 3. OpenClaw | Receives new event via Sliding Sync +| gets message | (OpenClaw is a regular client, actively syncing) ++--------+--------+ + | + v ++-----------------+ +| 4. OpenClaw | POST https://api.deepseek.com/v1/chat/completions +| calls LLM | With conversation history as context ++--------+--------+ + | + v ++-----------------+ +| 5. OpenClaw | PUT /_matrix/client/v3/rooms/{roomId}/send/m.room.message +| sends reply | -> Palpo (http://127.0.0.1:8128) +| via CS API | Auth: Bearer {access_token} ++--------+--------+ + | + v ++-----------------+ +| 6. Palpo stores | Bot's reply event saved +| & delivers | Sliding Sync pushes to Robrix ++--------+--------+ + | + v +User sees AI reply in Robrix +``` + +### Architecture Diagram + +``` ++----------+ +----------+ +----------+ +-----+ +| Robrix | Client-Server API | Palpo | Client-Server API | OpenClaw | HTTPS | LLM | +| (Client) | --------------------> | (Server) | <------------------> | (AI Bot) | ------> | | +| | <-------------------- | | Sliding Sync | | <------ | | ++----------+ Sliding Sync +----------+ +----------+ +-----+ + Your machine Docker :8128 Your machine External +``` + +**Key observations:** + +- **Robrix and OpenClaw are equal peers to Palpo** -- both connect via the Client-Server API as regular clients. +- **OpenClaw requires no server-side configuration** -- no need to modify any Palpo config files (compare with Octos which requires AppService YAML registration). +- **Only the LLM call leaves local network** -- Robrix ↔ Palpo ↔ OpenClaw all stay on localhost; only the DeepSeek API call goes to the internet. + +--- + +## 4. Client Mode vs Application Service Mode + +The Robrix ecosystem offers two ways to integrate AI bots: OpenClaw's **client mode** and Octos's **Application Service mode**. Their core differences are: + +### 4.1 Connection Mechanism Comparison + +| | OpenClaw (Client Mode) | Octos (Application Service Mode) | +|---|---|---| +| **Connection** | Logs in with password, same as a regular user | Registered on the server via a YAML registration file | +| **Message retrieval** | **Sync pull** -- OpenClaw actively polls the server for new events | **Server push** -- Palpo actively pushes events to Octos's HTTP endpoint | +| **Authentication** | access_token (user-level) | as_token / hs_token (service-level, mutual authentication) | +| **Server-side config** | **None required** -- the bot is just a regular user | **Registration required** -- place YAML file in Palpo's `appservice_registration_dir` | +| **User namespaces** | Only one user ID | Can claim exclusive user namespaces, dynamically create child bots | +| **Rate limiting** | Subject to limits (same as regular users) | Exempt (`rate_limited: false`) | + +### 4.2 Capability Comparison + +| Capability | OpenClaw | Octos | +|------------|----------|-------| +| Basic conversation | Yes | Yes | +| Multiple LLM providers | Yes (14+) | Yes | +| E2EE encryption | Yes (Rust crypto SDK) | Not needed (AppService bypasses encryption) | +| Server-side administration | Not needed | Requires admin access to register AppService | +| Multi-channel (Telegram, Discord, etc.) | Yes | Matrix only | +| Homeserver requirements | Any standard Matrix server | Must support Application Service API | + +### 4.3 Message Latency + +| Stage | OpenClaw | Octos | +|-------|----------|-------| +| Message reaches bot | Sync interval (typically 1-5 seconds) | Instant push (< 100ms) | +| LLM response | Depends on LLM provider | Depends on LLM provider | +| Bot sends reply | Instant | Instant | + +> OpenClaw uses Sliding Sync's long-polling mode, so actual latency is typically 1-2 seconds -- barely noticeable in a chat context. + +### 4.4 Deployment Complexity + +| | OpenClaw | Octos | +|---|---|---| +| Server side | No configuration needed | Requires registration YAML, token configuration | +| Client side | Install OpenClaw + edit one JSON file | Requires Docker Compose orchestrating three services | +| Token management | Password auto-login, token auto-cached | Must manually generate and synchronize as_token / hs_token | +| Architecture complexity | Simple (single process) | Complex (Palpo + Octos + PostgreSQL) | + +### 4.5 When to Use Which? + +| Scenario | Recommended | +|----------|-------------| +| Quick-test AI conversation | **OpenClaw** -- 5 minutes to configure, no server changes needed | +| Personal AI assistant | **OpenClaw** -- simple, flexible, multi-channel support | +| Team with multiple specialized bots | **Octos** -- BotFather can dynamically create child bots | +| Server-side administration needed | **Octos** -- AppService is registered and controlled by admins | +| High-concurrency messages | **Octos** -- server push + no rate limits | +| Cross-platform AI agent (Telegram/Discord simultaneously) | **OpenClaw** -- native multi-channel support | + +--- + +## 5. End-to-End Encryption (E2EE) + +### How OpenClaw Handles Encryption + +OpenClaw's Matrix plugin uses matrix-js-sdk's **Rust crypto path**, implementing the Olm (one-to-one key exchange) and Megolm (group encryption) protocols. + +When `"encryption": true` is configured: + +1. **First login**: OpenClaw creates an encryption device and generates a cross-signing identity +2. **Auto-bootstrap**: Executes secret storage bootstrap; device is marked "verified by its owner" +3. **Receiving messages**: OpenClaw decrypts Megolm-encrypted messages +4. **Sending replies**: Replies are automatically encrypted + +### Important Notes + +- **Historical messages cannot be decrypted** -- Messages sent before OpenClaw's device was created did not have their Megolm session keys distributed to OpenClaw. They can never be decrypted. +- **vs Octos** -- Octos, as an AppService, receives **server-side decrypted plaintext events**. It does not need to handle E2EE at all. OpenClaw, as a client, must handle encryption itself. + +--- + +## 6. Further Reading + +- **OpenClaw Documentation:** [docs.openclaw.ai](https://docs.openclaw.ai/) -- full OpenClaw documentation. +- **OpenClaw Matrix Plugin:** [docs.openclaw.ai/channels/matrix](https://docs.openclaw.ai/channels/matrix) -- official Matrix channel plugin reference. +- **Matrix Client-Server API Spec:** [spec.matrix.org -- Client-Server API](https://spec.matrix.org/latest/client-server-api/) -- the protocol OpenClaw uses. +- **Matrix Application Service API Spec:** [spec.matrix.org -- Application Service API](https://spec.matrix.org/latest/application-service-api/) -- the protocol Octos uses. +- **Octos Architecture Guide:** [03-how-robrix-palpo-octos-work-together.md](../robrix-with-palpo-and-octos/03-how-robrix-palpo-octos-work-together.md) -- full explanation of the Octos AppService model. +- **Deployment Guide:** [01-deploying-openclaw-with-matrix.md](01-deploying-openclaw-with-matrix.md) -- how to deploy OpenClaw with Matrix. +- **Usage Guide:** [02-using-robrix-with-openclaw.md](02-using-robrix-with-openclaw.md) -- how to use Robrix to chat with OpenClaw agents. + +--- + +*This document is based on tested results from April 2026. For the latest updates, see the respective project repositories.* diff --git a/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md b/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md new file mode 100644 index 000000000..fd4d76c16 --- /dev/null +++ b/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md @@ -0,0 +1,554 @@ +# 部署指南:Robrix + Palpo + Octos + +[English Version](01-deploying-palpo-and-octos.md) + +> **目标:** 按照本指南操作后,你的机器上将通过 Docker Compose 运行 Palpo(Matrix 主服务器)、Octos(AI 机器人)和 PostgreSQL。Robrix 将能连接到你的 Palpo 服务器,你可以与 Octos AI 机器人对话。 + +本指南带你一步步部署后端服务:从克隆源码,到配置各组件,再到验证一切正常运行。 + +> **只想快速试试?** 跳到 [快速开始](#2-快速开始) — 5 步即可运行。 +> +> **想了解每个配置背后的原理?** 参阅 [架构原理](03-how-robrix-palpo-octos-work-together-zh.md) 了解完整解释。 + +--- + +## 目录 + +1. [前提条件](#1-前提条件) +2. [快速开始](#2-快速开始) +3. [配置详解](#3-配置详解) +4. [端到端验证](#4-端到端验证) +5. [故障排除](#5-故障排除) +6. [延伸阅读](#6-延伸阅读) + +--- + +## 1. 前提条件 + +开始之前,请确保你已具备以下条件: + +| 需求 | 版本 | 备注 | +|------|------|------| +| **Docker** + **Docker Compose** | v2+ | 运行 `docker compose version` 检查。Docker Desktop 自带 Compose v2。 | +| **Git** | 任意 | 用于克隆源码仓库。 | +| **一个 LLM API Key** | -- | 如 [DeepSeek](https://platform.deepseek.com/)(有免费额度)、OpenAI、Anthropic 等。 | +| **Robrix** | 最新版 | 参阅 [Robrix 快速开始](../robrix/getting-started-with-robrix-zh.md) 了解下载或构建方式。 | + +> **注意:** Palpo 和 Octos 都在 Docker 内从源码构建。你不需要在宿主机上安装 Rust 或任何其他工具链。 + +### 硬盘预算 + +首次 `docker compose up --build` 是占用峰值。之后磁盘上大部分空间是一次性 build cache,可以安全清理 —— 见 [§5.5 清理 Docker 缓存](#55-清理-docker-缓存)。 + +| 项目 | 大小 | 说明 | +|---|---|---| +| Palpo 镜像(本地构建) | ~214 MB | `debian:bookworm` runtime | +| Octos 镜像(本地构建) | ~1.7 GB | 捆绑 skills runtime(ffmpeg、LibreOffice 等) | +| Postgres 镜像 | ~476 MB | `postgres:17` | +| **镜像合计** | **~2.4 GB** | 长期占用 | +| Build cache(首次构建) | ~4.75 GB | 100% 可回收 | +| 运行数据(`./data/`,24h 稳态) | ~50-120 MB | 随消息量缓慢增长 | +| **峰值(刚首次 build 完)** | **~7.2 GB** | | +| **稳态(`docker builder prune -af` 后)** | **~2.5 GB** | | + +> 256 GB SSD 完全够用 —— 只要偶尔运行一下 `docker builder prune -af` 即可。源码反复修改后的重复构建会累积几 GB 的缓存,这部分就是清理命令要回收的对象。 + +--- + +## 2. 快速开始 + +5 步在本地跑通所有服务。 + +### 步骤 1:克隆仓库 + +```bash +git clone https://github.com/Project-Robius-China/robrix2.git +cd robrix2/palpo-and-octos-deploy +``` + +### 步骤 2:运行初始化脚本 + +```bash +./setup.sh +``` + +此脚本会: +- 将 Palpo 源码仓库克隆到 `repos/palpo/`(从 GitHub 浅克隆) +- 将 Octos 源码仓库克隆到 `repos/octos/`(从 GitHub 浅克隆) +- 从 `.env.example` 创建 `.env` 文件 + +两个服务均在 Docker 内从源码构建,以支持所有架构(x86_64、ARM64/Apple Silicon 等)。 + +> **文件存放位置说明:** 运行 `setup.sh` 和 `docker compose up` 后,`palpo-and-octos-deploy/` 目录会包含: +> - `repos/` — Palpo 和 Octos 的源码(Docker 用来构建镜像) +> - `data/` — 运行时数据(PostgreSQL 数据库、Octos 会话、媒体文件) +> - `.env` — 你的环境变量(API Key 等) +> +> 这些目录已在 `.gitignore` 中列出,**不会**被提交到版本库。 + +### 步骤 3:设置 API Key + +编辑 `.env`,将 `your-api-key-here` 替换为你的实际 API Key: + +``` +DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx +``` + +> **使用 DeepSeek?多一步配置。** DeepSeek 单次响应的 token 上限是 **8192**,而 Octos 内置默认值是 **16384**——首次发消息就会报 `400 Bad Request: Invalid max_tokens value`。**在运行 `docker compose up` 之前**,打开 `config/botfather.json`,在 `config.gateway` 块内添加 `"max_output_tokens": 8000`(完整示例见 [3.5 节](#35-octos-机器人配置configbotfatherjson))。其他提供商(Moonshot、OpenAI、Anthropic)的上限更高,不需要这行。背景说明:[5.3 机器人问题](#53-机器人问题)。 + +### 步骤 4:启动服务 + +```bash +docker compose up -d +``` + +> **重要:** 首次运行会从源码编译 Palpo 和 Octos,这可能需要 **10--30 分钟**(取决于你的机器性能和网络速度)。Palpo 需要编译其 Rust 代码;Octos 还额外需要下载 Node.js、Chromium 等技能插件的运行时工具。后续启动会使用缓存镜像,几秒内完成。 + +检查运行状态: + +```bash +docker compose ps +``` + +你应该看到三个服务(`palpo_postgres`、`palpo`、`octos`)都处于 `running` 状态。 + +### 步骤 5:用 Robrix 连接 + +1. **打开 Robrix**(还没有?参阅 [Robrix 快速开始](../robrix/getting-started-with-robrix-zh.md)) + +2. **设置服务器地址**:在登录界面,在 **Homeserver URL** 输入框中输入 `http://127.0.0.1:8128` + +3. **注册新账号**:输入用户名和密码,点击 **Sign up** + +4. **在 设置 → Labs 里绑定机器人**(首次使用必做 —— 这个开关启用 AppService 功能,Robrix 才能和 Octos 通信;不开 bot 不会回复): + + ![Robrix AppService 设置](../images/robrix-appservice-settings.png) + + - 点击左下角 **⚙** 设置图标 + - 切到 **Labs** 标签页 + - 打开 **Enabled** 开关 + - 填入: + - **BotFather User ID**:`@octosbot:127.0.0.1:8128` + - **Octos Service**:`http://127.0.0.1:8010` + - 点 **Save**,再点 **Check Now** —— 应该显示绿色的 **Reachable** 字样 + +5. **与 AI 机器人对话**:登录后,创建一个房间并邀请机器人: + - 点击房间中的邀请按钮 + - 输入 `@octosbot:127.0.0.1:8128` + - 等待机器人加入房间(你应该能看到加入事件) + - 发送一条消息——AI 机器人应该会回复! + +**完成!** 你现在拥有了一个可工作的 Robrix + Palpo + Octos 系统。继续阅读了解配置详情,或跳到 [故障排除](#5-故障排除) 解决问题。 + +--- + +## 3. 配置详解 + +本节解释 `palpo-and-octos-deploy/` 目录中的每个配置文件。快速开始已经让你跑起来了——当你需要自定义时再来这里查阅。 + +> **注意:** 想了解架构以及每个组件为何如此配置,请参阅 [架构原理](03-how-robrix-palpo-octos-work-together-zh.md)。 + +### 3.1 目录结构 + +``` +palpo-and-octos-deploy/ +├── compose.yml # Docker Compose — 编排所有服务 +├── setup.sh # 一次性初始化脚本 +├── .env.example # 环境变量模板 +├── palpo.toml # Palpo 主服务器配置 +├── palpo.Dockerfile # Palpo Docker 构建(多阶段,release 模式) +├── appservices/ +│ └── octos-registration.yaml # 应用服务注册文件(连接 Palpo <-> Octos) +├── config/ +│ ├── botfather.json # Octos 机器人配置(LLM + Matrix 通道) +│ └── octos.json # Octos 全局设置 +├── repos/ # 源码(由 setup.sh 创建,已 gitignore) +│ ├── palpo/ # Palpo 主服务器源码 +│ └── octos/ # Octos 机器人源码 +├── data/ # 持久化数据(运行时自动创建,已 gitignore) +│ ├── pgsql/ # PostgreSQL 数据库文件 +│ ├── octos/ # Octos 运行时数据 +│ └── media/ # Palpo 媒体存储 +``` + +### 3.2 令牌生成 + +应用服务注册文件和 Octos 机器人配置共享两个密钥令牌,用于双向认证。示例文件中已预填开发用令牌,但**在生产环境中你必须重新生成**: + +```bash +openssl rand -hex 32 # → 用作 as_token +openssl rand -hex 32 # → 用作 hs_token +``` + +这两个值必须在 `palpo-and-octos-deploy/appservices/octos-registration.yaml` 和 `palpo-and-octos-deploy/config/botfather.json` 中完全一致。如果不匹配,机器人将无法工作。详见 [3.8 令牌匹配检查清单](#38-令牌匹配检查清单)。 + +### 3.3 应用服务注册文件(`appservices/octos-registration.yaml`) + +此文件告诉 Palpo 关于 Octos 的信息——Octos 管理哪些用户命名空间,以及将事件发送到哪里。 + +```yaml +id: octos-matrix-appservice +url: "http://octos:8009" + +as_token: "d1f46062a08e4833b18286d95c5e09a5f3e4a1b2c3d4e5f6a7b8c9d0e1f2a3b4" +hs_token: "e2a57173b19f5944c29397ea6d6f1ab6a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9" + +sender_localpart: octosbot +rate_limited: false + +namespaces: + users: + - exclusive: true + regex: "@octosbot_.*:127\\.0\\.0\\.1:8128" + - exclusive: true + regex: "@octosbot:127\\.0\\.0\\.1:8128" + aliases: [] + rooms: [] +``` + +| 字段 | 说明 | +|------|------| +| `id` | 此应用服务注册的唯一标识符。 | +| `url` | Palpo 发送事件的目标地址。使用 Docker 服务名 `octos`(不是 `localhost`),因为两个容器在同一个 Docker 网络中。 | +| `as_token` | Octos 调用 Palpo API 时使用的令牌。**必须**与 `botfather.json` 匹配。 | +| `hs_token` | Palpo 向 Octos 推送事件时使用的令牌。**必须**与 `botfather.json` 匹配。 | +| `sender_localpart` | 机器人的 Matrix 本地用户名。最终变为 `@octosbot:127.0.0.1:8128`。 | +| `rate_limited` | 设为 `false`,让机器人回复不受速率限制。 | +| `namespaces.users` | 此应用服务管理的用户 ID 正则匹配模式。包含机器人本身(`@octosbot:...`)和动态创建的子机器人(`@octosbot_*:...`)。 | + +### 3.4 Palpo 配置(`palpo.toml`) + +```toml +server_name = "127.0.0.1:8128" + +allow_registration = true +yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true +enable_admin_room = true + +appservice_registration_dir = "/var/palpo/appservices" + +# HTTP 监听器(Client-Server API) +[[listeners]] +address = "0.0.0.0:8008" + +[logger] +format = "pretty" + +[db] +url = "postgres://palpo:palpo_dev_password@palpo_postgres:5432/palpo" +pool_size = 10 + +[well_known] +server = "127.0.0.1:8128" +client = "http://127.0.0.1:8128" +``` + +| 字段 | 说明 | +|------|------| +| `server_name` | 所有 Matrix ID 的域名部分(如 `@user:127.0.0.1:8128`)。 | +| `allow_registration` | 是否允许新用户注册。设为 `true` 以便 Robrix 用户创建账号。 | +| `yes_i_am_very_very_sure_...` | 当 `allow_registration = true` 时必填的安全确认。 | +| `enable_admin_room` | 启用服务器管理员房间。 | +| `appservice_registration_dir` | Palpo 启动时自动加载此目录下所有 `.yaml` 文件。Octos 就是通过这种方式被发现的。 | +| `[[listeners]]` | 网络监听器。每个条目定义一个 Palpo 监听的地址。 | +| `[logger]` | 日志格式。`"pretty"` 用于开发,`"json"` 用于生产。 | +| `[db]` | PostgreSQL 连接配置。`palpo_postgres` 是 Docker 服务名。密码必须与 `compose.yml` 中的 `POSTGRES_PASSWORD` 匹配。 | +| `[well_known]` | 用于客户端发现服务器。必须与外部可访问的地址匹配。 | + +> **注意:** `server_name` 值 `"127.0.0.1:8128"` 仅用于本地开发。生产环境部署时,请替换为你的实际域名(如 `"chat.example.com"`)。更改 `server_name` 时,你还需要同步更新 `octos-registration.yaml`(正则表达式部分)和 `botfather.json`(`server_name` 字段)。 + +> **重要:** 在这个本地 Docker 示例里,Matrix 身份统一使用 `127.0.0.1:8128`。因此 `server_name`、应用服务正则和机器人用户 ID 都必须写成 `127.0.0.1:8128`。只有容器之间通信时才使用 `palpo:8008`、`octos:8009` 这类 Docker 服务名。 + +### 3.5 Octos 机器人配置(`config/botfather.json`) + +此文件定义机器人的身份、LLM 提供商和 Matrix 通道配置。 + +```json +{ + "id": "botfather", + "name": "BotFather", + "enabled": true, + "config": { + "provider": "deepseek", + "model": "deepseek-chat", + "api_key_env": "DEEPSEEK_API_KEY", + "admin_mode": true, + "channels": [ + { + "type": "matrix", + "homeserver": "http://palpo:8008", + "as_token": "d1f46062a08e4833b18286d95c5e09a5f3e4a1b2c3d4e5f6a7b8c9d0e1f2a3b4", + "hs_token": "e2a57173b19f5944c29397ea6d6f1ab6a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9", + "server_name": "127.0.0.1:8128", + "sender_localpart": "octosbot", + "user_prefix": "octosbot_", + "port": 8009, + "allowed_senders": [] + } + ], + "gateway": { + "max_history": 50, + "queue_mode": "followup", + "max_output_tokens": 8000 + } + }, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" +} +``` + +> **重要:** `created_at` 和 `updated_at` 字段是 Octos **必需的**。如果缺少这两个字段,Octos 会跳过该 profile,机器人将无法启动。 + +**LLM 提供商设置:** + +| 字段 | 说明 | +|------|------| +| `provider` | LLM 提供商名称。Octos 支持 `deepseek`、`openai`、`anthropic` 等[多种提供商](https://octos-org.github.io/octos/)。 | +| `model` | 模型标识符(如 `deepseek-chat`、`gpt-4o`、`claude-sonnet-4-20250514`)。 | +| `api_key_env` | 存放 API Key 的环境变量名称。 | +| `admin_mode` | 启用 BotFather 管理命令(`/createbot`、`/deletebot`、`/listbots`)。从 Robrix 或聊天中创建和管理子机器人时必须开启。 | + +**Matrix 通道设置:** + +| 字段 | 说明 | +|------|------| +| `type` | 必须为 `"matrix"`。 | +| `homeserver` | Palpo 的内部 URL。使用 Docker 服务名 `palpo`,不是 `localhost`。 | +| `as_token` / `hs_token` | 必须与应用服务注册 YAML 文件匹配。 | +| `server_name` | Matrix 域名。必须与 `palpo.toml` 中的 `server_name` 一致。 | +| `sender_localpart` | 机器人用户名。必须与注册文件一致。 | +| `user_prefix` | 动态创建的子机器人用户 ID 前缀(如 `octosbot_translator`)。 | +| `port` | Octos 监听 Palpo 应用服务事件的端口。 | +| `allowed_senders` | 允许与机器人对话的 Matrix 用户 ID。空数组 `[]` = 所有人都可以对话。 | + +> **重要:** `homeserver` 是 Octos 访问 Palpo 时使用的 Docker 内部 URL;`server_name` 是写进 Matrix 用户 ID 的域名部分。两者相关但不能混用。详见 [架构原理](03-how-robrix-palpo-octos-work-together-zh.md)。 + +**Gateway 设置:** + +| 字段 | 说明 | +|------|------| +| `max_history` | 作为 LLM 上下文发送的最大历史消息数量。 | +| `queue_mode` | Octos 处理传入消息的方式。`followup` 将新消息排队并顺序处理。 | +| `max_output_tokens` | 可选。覆盖 Octos 内置的 chat `max_tokens` 默认值(16384)。**使用 DeepSeek 时必填**(单次响应上限 8192)——详见 [5.3 机器人问题](#53-机器人问题)。 | + +**切换 LLM 提供商(以 OpenAI 替代 DeepSeek 为例):** + +1. 在 `botfather.json` 中修改:`"provider": "openai"`、`"model": "gpt-4o"`、`"api_key_env": "OPENAI_API_KEY"` +2. 在 `.env` 中修改:`OPENAI_API_KEY=sk-xxxxxxxx` +3. 在 `compose.yml` 的 `octos` 服务 `environment` 中添加:`OPENAI_API_KEY: ${OPENAI_API_KEY}` + +Octos 支持 14+ 种提供商——完整列表见 [Octos Book](https://octos-org.github.io/octos/)。 + +### 3.6 Octos 全局设置(`config/octos.json`) + +此文件配置 Octos 的核心运行路径和日志级别。 + +```json +{ + "profiles_dir": "/root/.octos/profiles", + "data_dir": "/root/.octos", + "log_level": "debug" +} +``` + +| 字段 | 说明 | +|------|------| +| `profiles_dir` | Octos 加载机器人配置文件(如 `botfather.json`)的目录。通过 Docker 卷映射自 `./config/`。 | +| `data_dir` | Octos 运行时数据(会话、记忆)的根目录。映射自 `./data/octos/`。 | +| `log_level` | Octos 日志详细程度。开发环境用 `debug`,生产环境用 `info`。 | + +> **注意:** 这些是容器内部路径。`compose.yml` 中的 Docker 卷映射会将它们连接到宿主机目录。 + +### 3.7 Docker Compose(`compose.yml`) + +提供的 `compose.yml` 启动三个服务: + +| 服务 | 镜像 | 暴露端口 | 用途 | +|------|------|----------|------| +| `palpo_postgres` | `postgres:17` | *(无,仅内部)* | Palpo 的数据库 | +| `palpo` | 从源码构建 | `8128:8008` | Matrix 主服务器 | +| `octos` | 从源码构建 | `8009:8009`、`8010:8080` | AI 机器人应用服务 | + +**端口映射说明:** + +- `8128` — Robrix 连接此端口(Client-Server API) +- `8009` — Palpo 向 Octos 推送事件(Appservice API,同时暴露到宿主机供调试) +- `8010` — Octos 管理控制面板(可选,用于监控) + +**持久化卷:** + +| 卷 | 用途 | +|----|------| +| `./data/pgsql` | PostgreSQL 数据。`docker compose down` 后保留。 | +| `./data/octos` | Octos 运行时数据(会话、记忆)。 | +| `./data/media` | 通过 Matrix 上传的媒体文件(图片、文件)。 | + +**环境变量(`.env`):** + +| 变量 | 必填 | 默认值 | 说明 | +|------|------|--------|------| +| `DEEPSEEK_API_KEY` | **是** | -- | 你的 LLM API Key | +| `DB_PASSWORD` | 否 | `palpo_dev_password` | PostgreSQL 密码 | +| `RUST_LOG` | 否 | `octos=debug,info` | 日志详细程度 | + +### 3.8 令牌匹配检查清单 + +最常见的配置错误是令牌不匹配。以下值在两个文件中**必须完全一致**: + +| 值 | 在 `octos-registration.yaml` 中 | 在 `botfather.json` 中 | +|----|----------------------------------|------------------------| +| `as_token` | `as_token: "d1f4..."` | `"as_token": "d1f4..."` | +| `hs_token` | `hs_token: "e2a5..."` | `"hs_token": "e2a5..."` | +| `sender_localpart` | `sender_localpart: octosbot` | `"sender_localpart": "octosbot"` | +| `server_name` | regex: `@octosbot:127\\.0\\.0\\.1:8128` | `"server_name": "127.0.0.1:8128"` | + +如果有任何不匹配,机器人将不会响应消息。提交 bug 报告前请先检查! + +--- + +## 4. 端到端验证 + +部署完成后,按照以下检查清单确认一切正常。 + +### 服务健康检查 + +```bash +# 检查所有容器是否运行 +docker compose ps + +# 检查 Palpo 日志是否有启动错误 +docker compose logs palpo | tail -20 + +# 检查 Octos 日志——寻找 "appservice listening" 或类似信息 +docker compose logs octos | tail -20 + +# 验证 Palpo 是否响应 Matrix API +curl -s http://127.0.0.1:8128/_matrix/client/versions | head -5 +``` + +### 客户端连接检查清单 + +- [ ] Robrix 能连接到 `http://127.0.0.1:8128` +- [ ] 能注册新账号 +- [ ] 登录后房间列表能加载(新账号可能为空) +- [ ] 能创建新房间 + +### 机器人交互检查清单 + +- [ ] 能邀请 `@octosbot:127.0.0.1:8128` 到房间 +- [ ] 机器人加入房间(如果没有,检查 `docker compose logs octos`) +- [ ] 发送消息后机器人回复 +- [ ] 回复内容合理(确认 LLM 连接正常) + +### 日志检查顺序(跟随数据流) + +如果某步失败,按照数据在系统中流动的顺序检查日志: + +```bash +# 1. Palpo 是否收到了 Robrix 的消息? +docker compose logs palpo --since 1m + +# 2. Palpo 是否将事件转发给了 Octos? +docker compose logs palpo --since 1m | grep -i appservice + +# 3. Octos 是否收到并处理了事件? +docker compose logs octos --since 1m + +# 4. Octos 是否成功调用了 LLM? +docker compose logs octos --since 1m | grep -i -E "deepseek|llm|provider" +``` + +--- + +## 5. 故障排除 + +### 5.1 服务启动问题 + +| 症状 | 原因 | 解决方法 | +|------|------|----------| +| `palpo_postgres` 无法启动 | 端口 5432 已被占用,或数据损坏 | 检查 `docker compose logs palpo_postgres`。删除 `data/pgsql/` 重新开始。 | +| `palpo` 构建失败 | 网络问题或源码获取失败 | 确保 Docker 能访问 `github.com`。检查 `docker compose logs palpo` 查看构建错误。 | +| `palpo` 启动时崩溃 | `palpo.toml` 语法错误或数据库连接失败 | 检查日志。确保 `palpo_postgres` 先正常运行。验证数据库密码一致。 | +| `octos` 构建失败 | 缺少 Dockerfile 或网络问题 | 确保 Docker 能访问 `github.com`。运行 `./setup.sh` 确认仓库已克隆。 | +| `octos` 启动但日志有错误 | `botfather.json` 无效或缺少 API Key | 检查 JSON 语法。验证 `.env` 中已设置 `DEEPSEEK_API_KEY`。 | + +### 5.2 Robrix 连接问题 + +| 症状 | 原因 | 解决方法 | +|------|------|----------| +| "无法连接到服务器" | Homeserver URL 错误或 Palpo 未运行 | 确认 Palpo 正在运行(`docker compose ps`)。确认 URL 为 `http://127.0.0.1:8128`。 | +| 登录成功但没有房间 | 新账号的正常现象 | 创建一个新房间。加入或创建后房间会出现在列表中。 | +| 注册失败 | `palpo.toml` 中 `allow_registration = false` | 检查 `palpo.toml`。确保 `allow_registration = true`。 | +| "Homeserver 不支持 Sliding Sync" | Palpo 版本过旧 | 重新构建 Palpo:`docker compose build --no-cache palpo`。 | +| 连接超时 | 防火墙阻止了端口 8128 | 检查防火墙规则。macOS 上在系统设置中允许传入连接。 | + +### 5.3 机器人问题 + +| 症状 | 原因 | 解决方法 | +|------|------|----------| +| 机器人不响应消息 | 注册文件和配置文件之间令牌不匹配 | 验证 [令牌匹配检查清单](#38-令牌匹配检查清单)。 | +| Palpo 日志中出现 `Connection refused` | Octos 未运行,或注册 YAML 中 `url` 错误 | 确保 Octos 正在运行。`url` 必须使用 Docker 服务名(`http://octos:8009`),不能用 `localhost`。 | +| `User ID not in namespace` | `sender_localpart` 与 `namespaces.users` 正则不匹配 | 更新 `octos-registration.yaml` 中的正则表达式,包含机器人的完整用户 ID 模式。 | +| 机器人加入房间但回复空消息 | LLM API Key 无效或额度不足 | 检查 `docker compose logs octos` 中的 API 错误。验证 API Key 和账户余额。 | +| Octos 日志出现 `400 Bad Request: "Invalid max_tokens value, the valid range of max_tokens is [1, 8192]"`(DeepSeek) | Octos 默认的 chat `max_tokens` 是 16384,超出 DeepSeek 单次响应上限(8192)。 | 在 `config/botfather.json` 的 `config.gateway` 内添加 `"max_output_tokens": 8000`(参考 [3.5 节](#35-octos-机器人配置configbotfatherjson)),然后 `docker compose restart octos`。无需重新构建镜像。 | +| 部分用户的消息被忽略 | `botfather.json` 中的 `allowed_senders` 过滤 | 设 `allowed_senders` 为 `[]` 允许所有人,或添加用户的 Matrix ID。 | +| 机器人配置未加载 | `botfather.json` 缺少 `created_at` / `updated_at` | 这两个字段是必需的。按 [3.5 节](#35-octos-机器人配置configbotfatherjson) 示例添加。 | + +### 5.4 常用调试命令 + +```bash +# 实时查看所有服务日志 +docker compose logs -f + +# 查看特定服务的日志 +docker compose logs -f palpo +docker compose logs -f octos + +# 重启单个服务(如修改 botfather.json 后) +docker compose restart octos + +# 重新构建单个服务(如更新源码后) +docker compose build --no-cache palpo +docker compose up -d palpo + +# 检查 Palpo 的 Client-Server API +curl http://127.0.0.1:8128/_matrix/client/versions + +# 完全重置(警告:删除所有数据,包括账号和消息) +docker compose down -v +rm -rf data/ +docker compose up -d +``` + +### 5.5 清理 Docker 缓存 + +磁盘占用里的大头通常是 **build cache**,不是运行数据。缓存可以放心清 —— 代价只是下次构建慢一点。 + +```bash +# 查看 Docker 实际占用 +docker system df + +# 只清构建缓存(安全;下次构建会复用 registry 层缓存,仅 Rust 编译产物重来) +docker builder prune -af + +# 核选项:停容器 + 清悬挂镜像 + 未用网络 + 卷 +docker compose down +docker system prune -af --volumes +``` + +如果你已经迭代源码一段时间,`docker system df` 显示几十 GB 的可回收缓存是正常现象 —— Cargo 的 target 目录缓存在 builder 层里,反复 `cargo build` 会积累。一次 `docker builder prune -af` 即可清零。 + +--- + +## 6. 延伸阅读 + +- **Octos 完整文档:** [octos-org.github.io/octos](https://octos-org.github.io/octos/) — 覆盖所有 LLM 提供商、通道、技能、记忆系统和高级配置。 +- **Octos Matrix Appservice 指南:** [octos-org/octos#171](https://github.com/octos-org/octos/pull/171) — 本文档参考的原始 Palpo + Octos 集成指南。 +- **Palpo:** [github.com/palpo-im/palpo](https://github.com/palpo-im/palpo) — Palpo 主服务器文档。 +- **Robrix:** [Project-Robius-China/robrix2](https://github.com/Project-Robius-China/robrix2) — Robrix 客户端、构建说明和功能追踪。 +- **Matrix Appservice 规范:** [spec.matrix.org — Application Service API](https://spec.matrix.org/latest/application-service-api/) — 应用服务的 Matrix 协议规范。 +- **架构原理:** [03-how-robrix-palpo-octos-work-together-zh.md](03-how-robrix-palpo-octos-work-together-zh.md) — 应用服务机制如何运作、消息生命周期和 BotFather 系统。 + +--- + +*本指南内容截至 2026 年 4 月。最新更新请查看各项目的仓库。* diff --git a/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md b/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md new file mode 100644 index 000000000..c1813db79 --- /dev/null +++ b/docs/robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md @@ -0,0 +1,554 @@ +# Deployment Guide: Robrix + Palpo + Octos + +[中文版](01-deploying-palpo-and-octos-zh.md) + +> **Goal:** After following this guide, you will have Palpo (Matrix homeserver), Octos (AI bot), and PostgreSQL running via Docker Compose on your machine. Robrix will be able to connect to your Palpo server, and you can chat with the Octos AI bot. + +This guide walks you through deploying the backend services step by step: from cloning the source code, to configuring each component, to verifying everything works end-to-end. + +> **Just want to try it quickly?** Jump to [Quick Start](#2-quick-start) -- 5 steps to get running. +> +> **Want to understand WHY things are configured this way?** See [Architecture](03-how-robrix-palpo-octos-work-together.md) for the full explanation. + +--- + +## Table of Contents + +1. [Prerequisites](#1-prerequisites) +2. [Quick Start](#2-quick-start) +3. [Configuration Details](#3-configuration-details) +4. [End-to-End Verification](#4-end-to-end-verification) +5. [Troubleshooting](#5-troubleshooting) +6. [Further Reading](#6-further-reading) + +--- + +## 1. Prerequisites + +Before starting, make sure you have: + +| Requirement | Version | Notes | +|-------------|---------|-------| +| **Docker** + **Docker Compose** | v2+ | `docker compose version` to check. Docker Desktop includes Compose v2. | +| **Git** | Any | For cloning source repos. | +| **An LLM API key** | -- | e.g., [DeepSeek](https://platform.deepseek.com/) (free tier available), OpenAI, Anthropic, etc. | +| **Robrix** | Latest | See [Getting Started with Robrix](../robrix/getting-started-with-robrix.md) for download or build instructions. | + +> **Note:** Palpo and Octos are both built from source inside Docker. You do not need to install Rust or any other toolchain on your host machine. + +### Disk Budget + +First-time `docker compose up --build` is the peak. Most of what shows up on disk after that is one-time build cache that can be safely pruned -- see [§5.5 Cleaning up Docker Cache](#55-cleaning-up-docker-cache). + +| Item | Size | Notes | +|---|---|---| +| Palpo image (local build) | ~214 MB | `debian:bookworm` runtime | +| Octos image (local build) | ~1.7 GB | bundles skills runtime (ffmpeg, LibreOffice, etc.) | +| Postgres image | ~476 MB | `postgres:17` | +| **Container images total** | **~2.4 GB** | permanent footprint | +| Build cache (first build) | ~4.75 GB | 100% reclaimable | +| Runtime data (`./data/`, 24h steady) | ~50-120 MB | grows slowly with messages | +| **Peak (right after first build)** | **~7.2 GB** | | +| **Steady state (after `docker builder prune -af`)** | **~2.5 GB** | | + +> A 256 GB SSD is fine as long as you run `docker builder prune -af` occasionally. Rebuilding after source-code changes piles up several GB of cache each time -- that cache is what the cleanup commands reclaim. + +--- + +## 2. Quick Start + +Get everything running locally in 5 steps. + +### Step 1: Clone the Repo + +```bash +git clone https://github.com/Project-Robius-China/robrix2.git +cd robrix2/palpo-and-octos-deploy +``` + +### Step 2: Run the Setup Script + +```bash +./setup.sh +``` + +This script: +- Clones the Palpo source repo into `repos/palpo/` (shallow clone from GitHub) +- Clones the Octos source repo into `repos/octos/` (shallow clone from GitHub) +- Creates your `.env` file from `.env.example` + +Both services are built from source inside Docker to support all architectures (x86_64, ARM64/Apple Silicon, etc.). + +> **Where do files go?** After running `setup.sh` and `docker compose up`, the `palpo-and-octos-deploy/` directory will contain: +> - `repos/` — Palpo and Octos source code (used by Docker to build images) +> - `data/` — runtime data (PostgreSQL database, Octos sessions, media files) +> - `.env` — your environment variables (API key, etc.) +> +> These directories are listed in `.gitignore` and will **not** be committed to the repository. + +### Step 3: Set Your API Key + +Edit `.env` and replace `your-api-key-here` with your actual API key: + +``` +DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx +``` + +> **Using DeepSeek? One extra config step is required.** DeepSeek caps per-response tokens at **8192**, but Octos's built-in default is **16384** — your first message will fail with `400 Bad Request: Invalid max_tokens value`. **Before** running `docker compose up`, open `config/botfather.json` and add `"max_output_tokens": 8000` inside the `config.gateway` block (full example in [3.5](#35-octos-bot-profile-configbotfatherjson)). Other providers (Moonshot, OpenAI, Anthropic) have higher caps and do not need this line. Background: [5.3 Bot Issues](#53-bot-issues). + +### Step 4: Start the Services + +```bash +docker compose up -d +``` + +> **Important:** The first run builds both Palpo and Octos from source, which can take **10--30 minutes** depending on your machine and network speed. Palpo compiles its Rust codebase; Octos additionally downloads runtime tools (Node.js, Chromium) for its skill plugins. Subsequent runs use cached images and start in seconds. + +Check that everything is running: + +```bash +docker compose ps +``` + +You should see three services (`palpo_postgres`, `palpo`, `octos`) all in `running` state. + +### Step 5: Connect with Robrix + +1. **Open Robrix** (see [Getting Started with Robrix](../robrix/getting-started-with-robrix.md) if you don't have it yet) + +2. **Set the homeserver**: In the login screen, enter `http://127.0.0.1:8128` in the **Homeserver URL** field + +3. **Register a new account**: Enter a username and password, then click **Sign up** + +4. **Bind the bot in Settings → Labs** (first-run only -- this switch turns on the AppService features that let Robrix talk to Octos; without it the bot won't respond): + + ![Robrix AppService settings](../images/robrix-appservice-settings.png) + + - Click the **⚙** Settings icon (bottom-left) + - Open the **Labs** tab + - Toggle **Enabled** on + - Fill in: + - **BotFather User ID**: `@octosbot:127.0.0.1:8128` + - **Octos Service**: `http://127.0.0.1:8010` + - Click **Save**, then **Check Now** -- it should show a green **Reachable** indicator + +5. **Talk to the AI bot**: After logging in, create a room and invite the bot: + - Click the invite button in the room + - Enter `@octosbot:127.0.0.1:8128` + - Wait a moment for the bot to join the room (you should see a join event) + - Send a message -- the AI bot should reply! + +**That's it!** You now have a working Robrix + Palpo + Octos setup. Read on for configuration details, or jump to [Troubleshooting](#5-troubleshooting) if something isn't working. + +--- + +## 3. Configuration Details + +This section explains every configuration file in the `palpo-and-octos-deploy/` directory. You already have a working setup from the Quick Start -- come here when you want to customize. + +> **Note:** To understand the architecture and WHY each component is configured this way, see [Architecture](03-how-robrix-palpo-octos-work-together.md). + +### 3.1 Directory Layout + +``` +palpo-and-octos-deploy/ +├── compose.yml # Docker Compose -- orchestrates all services +├── setup.sh # One-time setup script +├── .env.example # Environment variables template +├── palpo.toml # Palpo homeserver configuration +├── palpo.Dockerfile # Palpo Docker build (multi-stage, release) +├── appservices/ +│ └── octos-registration.yaml # Appservice registration (links Palpo <-> Octos) +├── config/ +│ ├── botfather.json # Octos bot profile (LLM + Matrix channel config) +│ └── octos.json # Octos global settings +├── repos/ # Source code (created by setup.sh, gitignored) +│ ├── palpo/ # Palpo homeserver source +│ └── octos/ # Octos bot source +├── data/ # Persistent data (created at runtime, gitignored) +│ ├── pgsql/ # PostgreSQL database files +│ ├── octos/ # Octos runtime data +│ └── media/ # Palpo media storage +``` + +### 3.2 Token Generation + +The Appservice registration and the Octos bot profile share two secret tokens for mutual authentication. The example files come with pre-filled development tokens, but **you must generate new tokens for production**: + +```bash +openssl rand -hex 32 # -> use as as_token +openssl rand -hex 32 # -> use as hs_token +``` + +These two values must be identical in `palpo-and-octos-deploy/appservices/octos-registration.yaml` and `palpo-and-octos-deploy/config/botfather.json`. If they don't match, the bot will not work. See [3.8 Token Matching Checklist](#38-token-matching-checklist). + +### 3.3 Appservice Registration (`appservices/octos-registration.yaml`) + +This file tells Palpo about Octos -- which user namespaces Octos manages and where to send events. + +```yaml +id: octos-matrix-appservice +url: "http://octos:8009" + +as_token: "d1f46062a08e4833b18286d95c5e09a5f3e4a1b2c3d4e5f6a7b8c9d0e1f2a3b4" +hs_token: "e2a57173b19f5944c29397ea6d6f1ab6a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9" + +sender_localpart: octosbot +rate_limited: false + +namespaces: + users: + - exclusive: true + regex: "@octosbot_.*:127\\.0\\.0\\.1:8128" + - exclusive: true + regex: "@octosbot:127\\.0\\.0\\.1:8128" + aliases: [] + rooms: [] +``` + +| Field | Description | +|-------|-------------| +| `id` | A unique identifier for this appservice registration. | +| `url` | Where Palpo sends events. Uses the Docker service name `octos` (not `localhost`), because both containers share the same Docker network. | +| `as_token` | Token that Octos uses when calling Palpo's API. **Must match** `botfather.json`. | +| `hs_token` | Token that Palpo uses when pushing events to Octos. **Must match** `botfather.json`. | +| `sender_localpart` | The bot's Matrix local username. Becomes `@octosbot:127.0.0.1:8128`. | +| `rate_limited` | Set to `false` so the bot can respond without rate limits. | +| `namespaces.users` | Regex patterns for user IDs that this appservice owns. Include the bot itself (`@octosbot:...`) and any dynamically-created bot users (`@octosbot_*:...`). | + +### 3.4 Palpo Configuration (`palpo.toml`) + +```toml +server_name = "127.0.0.1:8128" + +allow_registration = true +yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true +enable_admin_room = true + +appservice_registration_dir = "/var/palpo/appservices" + +# HTTP listener (Client-Server API) +[[listeners]] +address = "0.0.0.0:8008" + +[logger] +format = "pretty" + +[db] +url = "postgres://palpo:palpo_dev_password@palpo_postgres:5432/palpo" +pool_size = 10 + +[well_known] +server = "127.0.0.1:8128" +client = "http://127.0.0.1:8128" +``` + +| Field | Description | +|-------|-------------| +| `server_name` | The domain part of all Matrix IDs (e.g., `@user:127.0.0.1:8128`). | +| `allow_registration` | Whether new users can register. Set to `true` for Robrix users to create accounts. | +| `yes_i_am_very_very_sure_...` | Required safety confirmation when `allow_registration = true`. | +| `enable_admin_room` | Enables the server admin room for management. | +| `appservice_registration_dir` | Palpo loads all `.yaml` files from this directory on startup. This is how it discovers Octos. | +| `[[listeners]]` | Network listeners. Each entry defines an address Palpo listens on. | +| `[logger]` | Log format. `"pretty"` for development, `"json"` for production. | +| `[db]` | PostgreSQL connection. `palpo_postgres` is the Docker service name. The password must match `POSTGRES_PASSWORD` in `compose.yml`. | +| `[well_known]` | Used by clients for server discovery. Must match externally-reachable addresses. | + +> **Note:** The `server_name` `"127.0.0.1:8128"` is for local development only. For production deployment, replace it with your actual domain name (e.g., `"chat.example.com"`). When you change `server_name`, you must also update it in `octos-registration.yaml` (the regex patterns) and `botfather.json` (`server_name` field). + +> **Important:** In this local Docker setup, the Matrix identity is `127.0.0.1:8128`, so `server_name`, the appservice regex, and bot user IDs must all use `127.0.0.1:8128`. Only container-to-container traffic uses Docker service names like `palpo:8008` or `octos:8009`. + +### 3.5 Octos Bot Profile (`config/botfather.json`) + +This file defines the bot's identity, LLM provider, and Matrix channel configuration. + +```json +{ + "id": "botfather", + "name": "BotFather", + "enabled": true, + "config": { + "provider": "deepseek", + "model": "deepseek-chat", + "api_key_env": "DEEPSEEK_API_KEY", + "admin_mode": true, + "channels": [ + { + "type": "matrix", + "homeserver": "http://palpo:8008", + "as_token": "d1f46062a08e4833b18286d95c5e09a5f3e4a1b2c3d4e5f6a7b8c9d0e1f2a3b4", + "hs_token": "e2a57173b19f5944c29397ea6d6f1ab6a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9", + "server_name": "127.0.0.1:8128", + "sender_localpart": "octosbot", + "user_prefix": "octosbot_", + "port": 8009, + "allowed_senders": [] + } + ], + "gateway": { + "max_history": 50, + "queue_mode": "followup", + "max_output_tokens": 8000 + } + }, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z" +} +``` + +> **Important:** The `created_at` and `updated_at` fields are **required** by Octos. If they are missing, Octos will skip this profile and the bot will never start. + +**LLM Provider settings:** + +| Field | Description | +|-------|-------------| +| `provider` | LLM provider name. Octos supports `deepseek`, `openai`, `anthropic`, and [more](https://octos-org.github.io/octos/). | +| `model` | Model identifier (e.g., `deepseek-chat`, `gpt-4o`, `claude-sonnet-4-20250514`). | +| `api_key_env` | Name of the environment variable holding your API key. | +| `admin_mode` | Enables BotFather management commands (`/createbot`, `/deletebot`, `/listbots`). Required for creating and managing child bots from Robrix or chat. | + +**Matrix channel settings:** + +| Field | Description | +|-------|-------------| +| `type` | Must be `"matrix"`. | +| `homeserver` | Palpo's internal URL. Uses Docker service name `palpo`, not `localhost`. | +| `as_token` / `hs_token` | Must match the appservice registration YAML. | +| `server_name` | The Matrix domain. Must match `server_name` in `palpo.toml`. | +| `sender_localpart` | Bot username. Must match the registration file. | +| `user_prefix` | Prefix for dynamically-created bot users (e.g., `octosbot_translator`). | +| `port` | Port Octos listens on for Appservice events from Palpo. | +| `allowed_senders` | Matrix user IDs allowed to talk to the bot. Empty `[]` = everyone. | + +> **Important:** `homeserver` is the internal Docker URL Octos uses to call Palpo. `server_name` is the Matrix domain embedded in user IDs. They are related but not interchangeable. See [Architecture](03-how-robrix-palpo-octos-work-together.md) for why. + +**Gateway settings:** + +| Field | Description | +|-------|-------------| +| `max_history` | Maximum number of messages to include as context for the LLM. | +| `queue_mode` | How Octos handles incoming messages. `followup` queues new messages and processes them sequentially. | +| `max_output_tokens` | Optional. Overrides Octos's built-in chat `max_tokens` default (16384). **Required when using DeepSeek** (per-response cap is 8192) — see [5.3 Bot Issues](#53-bot-issues). | + +**Switching LLM Provider (example: OpenAI instead of DeepSeek):** + +1. In `botfather.json`, change: `"provider": "openai"`, `"model": "gpt-4o"`, `"api_key_env": "OPENAI_API_KEY"` +2. In `.env`, change: `OPENAI_API_KEY=sk-xxxxxxxx` +3. In `compose.yml`, add to the `octos` service's `environment`: `OPENAI_API_KEY: ${OPENAI_API_KEY}` + +Octos supports 14+ providers — see [Octos Book](https://octos-org.github.io/octos/) for the full list. + +### 3.6 Octos Global Settings (`config/octos.json`) + +This file configures Octos's core runtime paths and logging. + +```json +{ + "profiles_dir": "/root/.octos/profiles", + "data_dir": "/root/.octos", + "log_level": "debug" +} +``` + +| Field | Description | +|-------|-------------| +| `profiles_dir` | Directory where Octos loads bot profiles (like `botfather.json`). Mapped via Docker volume from `./config/`. | +| `data_dir` | Root directory for Octos runtime data (sessions, memory). Mapped from `./data/octos/`. | +| `log_level` | Octos log verbosity. Use `debug` for development, `info` for production. | + +> **Note:** These are container-internal paths. The Docker volume mappings in `compose.yml` connect them to the host directories. + +### 3.7 Docker Compose (`compose.yml`) + +The provided `compose.yml` starts three services: + +| Service | Image | Exposed Ports | Purpose | +|---------|-------|--------------|---------| +| `palpo_postgres` | `postgres:17` | *(none, internal only)* | Database for Palpo | +| `palpo` | Built from source | `8128:8008` | Matrix homeserver | +| `octos` | Built from source | `8009:8009`, `8010:8080` | AI bot appservice | + +**Port mapping explanation:** + +- `8128` -- Robrix connects here (Client-Server API) +- `8009` -- Palpo pushes events to Octos here (Appservice API, also exposed to host for debugging) +- `8010` -- Octos admin dashboard (optional, for monitoring) + +**Persistent volumes:** + +| Volume | Purpose | +|--------|---------| +| `./data/pgsql` | PostgreSQL data. Survives `docker compose down`. | +| `./data/octos` | Octos runtime data (sessions, memory). | +| `./data/media` | Media files uploaded through Matrix (images, files). | + +**Environment variables (`.env`):** + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `DEEPSEEK_API_KEY` | **Yes** | -- | Your LLM API key | +| `DB_PASSWORD` | No | `palpo_dev_password` | PostgreSQL password | +| `RUST_LOG` | No | `octos=debug,info` | Log verbosity | + +### 3.8 Token Matching Checklist + +The most common configuration error is a token mismatch. These values **must be identical** across files: + +| Value | In `octos-registration.yaml` | In `botfather.json` | +|-------|------------------------------|---------------------| +| `as_token` | `as_token: "d1f4..."` | `"as_token": "d1f4..."` | +| `hs_token` | `hs_token: "e2a5..."` | `"hs_token": "e2a5..."` | +| `sender_localpart` | `sender_localpart: octosbot` | `"sender_localpart": "octosbot"` | +| `server_name` | regex: `@octosbot:127\\.0\\.0\\.1:8128` | `"server_name": "127.0.0.1:8128"` | + +If any of these don't match, the bot will not respond to messages. Double-check before filing a bug report! + +--- + +## 4. End-to-End Verification + +After setting up, run through this checklist to confirm everything works. + +### Service Health Check + +```bash +# Check all containers are running +docker compose ps + +# Check Palpo logs for startup errors +docker compose logs palpo | tail -20 + +# Check Octos logs -- look for "appservice listening" or similar +docker compose logs octos | tail -20 + +# Verify Palpo is responding to the Matrix API +curl -s http://127.0.0.1:8128/_matrix/client/versions | head -5 +``` + +### Client Connectivity Checklist + +- [ ] Robrix can connect to `http://127.0.0.1:8128` +- [ ] You can register a new account +- [ ] After login, the room list loads (may be empty for a fresh account) +- [ ] You can create a new room + +### Bot Interaction Checklist + +- [ ] You can invite `@octosbot:127.0.0.1:8128` to a room +- [ ] The bot joins the room (check `docker compose logs octos` if it doesn't) +- [ ] Sending a message triggers a response from the bot +- [ ] The response content makes sense (confirms LLM connection works) + +### Log Checking Order (Follow the Data Flow) + +If something fails, check the logs in the order that data flows through the system: + +```bash +# 1. Is Palpo receiving messages from Robrix? +docker compose logs palpo --since 1m + +# 2. Is Palpo forwarding events to Octos? +docker compose logs palpo --since 1m | grep -i appservice + +# 3. Is Octos receiving and processing events? +docker compose logs octos --since 1m + +# 4. Is Octos successfully calling the LLM? +docker compose logs octos --since 1m | grep -i -E "deepseek|llm|provider" +``` + +--- + +## 5. Troubleshooting + +### 5.1 Service Startup Issues + +| Symptom | Cause | Fix | +|---------|-------|-----| +| `palpo_postgres` won't start | Port 5432 already in use, or corrupt data | Check `docker compose logs palpo_postgres`. Remove `data/pgsql/` to start fresh. | +| `palpo` build fails | Network issue or missing source | Ensure Docker can reach `github.com`. Check `docker compose logs palpo` for build errors. | +| `palpo` crashes on startup | Bad `palpo.toml` syntax or DB connection failure | Check logs. Ensure `palpo_postgres` is healthy first. Verify DB password matches. | +| `octos` build fails | Missing Dockerfile or network issue | Ensure Docker can reach `github.com`. Run `./setup.sh` to verify repos are cloned. | +| `octos` starts but logs show errors | Invalid `botfather.json` or missing API key | Check JSON syntax. Verify `DEEPSEEK_API_KEY` is set in `.env`. | + +### 5.2 Robrix Connection Issues + +| Symptom | Cause | Fix | +|---------|-------|-----| +| "Cannot connect to server" | Wrong homeserver URL or Palpo not running | Verify Palpo is running (`docker compose ps`). Confirm URL is `http://127.0.0.1:8128`. | +| Login succeeds but no rooms appear | Normal for a fresh account | Create a new room. Rooms appear as you join or create them. | +| Registration fails | `allow_registration = false` in `palpo.toml` | Check `palpo.toml`. Ensure `allow_registration = true`. | +| "Homeserver does not support Sliding Sync" | Palpo version too old | Rebuild Palpo: `docker compose build --no-cache palpo`. | +| Connection times out | Firewall blocking port 8128 | Check firewall rules. On macOS, allow incoming connections in System Settings. | + +### 5.3 Bot Issues + +| Symptom | Cause | Fix | +|---------|-------|-----| +| Bot does not respond to messages | Token mismatch between registration and profile | Verify the [Token Matching Checklist](#38-token-matching-checklist). | +| `Connection refused` in Palpo logs | Octos not running, or wrong `url` in registration YAML | Ensure Octos is running. The `url` must use Docker service name (`http://octos:8009`), not `localhost`. | +| `User ID not in namespace` | `sender_localpart` doesn't match `namespaces.users` regex | Update the regex in `octos-registration.yaml` to include the bot's full user ID pattern. | +| Bot joins room but gives empty replies | LLM API key invalid or quota exceeded | Check `docker compose logs octos` for API errors. Verify your API key and account balance. | +| Octos logs show `400 Bad Request: "Invalid max_tokens value, the valid range of max_tokens is [1, 8192]"` (DeepSeek) | Octos's default chat `max_tokens` is 16384, which exceeds DeepSeek's per-response cap (8192). | In `config/botfather.json`, add `"max_output_tokens": 8000` inside `config.gateway` (see [3.5](#35-octos-bot-profile-configbotfatherjson)), then `docker compose restart octos`. No rebuild needed. | +| Messages from some users are ignored | `allowed_senders` filtering in `botfather.json` | Set `allowed_senders` to `[]` to allow everyone, or add the user's Matrix ID. | +| Bot profile not loading | Missing `created_at` / `updated_at` in `botfather.json` | These fields are required. Add them as shown in section [3.5](#35-octos-bot-profile-configbotfatherjson). | + +### 5.4 Useful Debug Commands + +```bash +# View real-time logs for all services +docker compose logs -f + +# View logs for a specific service +docker compose logs -f palpo +docker compose logs -f octos + +# Restart a single service (e.g., after editing botfather.json) +docker compose restart octos + +# Rebuild a single service (e.g., after updating source) +docker compose build --no-cache palpo +docker compose up -d palpo + +# Check Palpo's Client-Server API +curl http://127.0.0.1:8128/_matrix/client/versions + +# Full reset (WARNING: deletes all data including accounts and messages) +docker compose down -v +rm -rf data/ +docker compose up -d +``` + +### 5.5 Cleaning up Docker Cache + +The big number on disk is usually **build cache**, not running data. Cache is safe to drop -- the only cost is a slower next rebuild. + +```bash +# See what Docker is using +docker system df + +# Drop build cache only (safe; next build re-uses the registry layer cache) +docker builder prune -af + +# Nuclear: also remove stopped containers, dangling images, unused networks, and volumes +docker compose down +docker system prune -af --volumes +``` + +If you've been iterating on source code for a while and `docker system df` shows tens of GB of reclaimable cache, that's expected -- Cargo's target directory is cached inside the builder layer, so repeated `cargo build` across edits piles up. One `docker builder prune -af` zeros it. + +--- + +## 6. Further Reading + +- **Octos Documentation (full):** [octos-org.github.io/octos](https://octos-org.github.io/octos/) -- covers all LLM providers, channels, skills, memory system, and advanced configuration. +- **Octos Matrix Appservice Guide:** [octos-org/octos#171](https://github.com/octos-org/octos/pull/171) -- the original Palpo + Octos integration guide this document builds upon. +- **Palpo:** [github.com/palpo-im/palpo](https://github.com/palpo-im/palpo) -- Palpo homeserver documentation. +- **Robrix:** [Project-Robius-China/robrix2](https://github.com/Project-Robius-China/robrix2) -- Robrix client, build instructions, and feature tracker. +- **Matrix Appservice Spec:** [spec.matrix.org -- Application Service API](https://spec.matrix.org/latest/application-service-api/) -- the Matrix protocol specification for application services. +- **Architecture Guide:** [03-how-robrix-palpo-octos-work-together.md](03-how-robrix-palpo-octos-work-together.md) -- how the Appservice mechanism works, message lifecycle, and BotFather system. + +--- + +*This guide covers the deployment as of April 2026. For the latest updates, see the respective project repositories.* diff --git a/docs/robrix-with-palpo-and-octos/02-using-robrix-with-palpo-and-octos-zh.md b/docs/robrix-with-palpo-and-octos/02-using-robrix-with-palpo-and-octos-zh.md new file mode 100644 index 000000000..e25303513 --- /dev/null +++ b/docs/robrix-with-palpo-and-octos/02-using-robrix-with-palpo-and-octos-zh.md @@ -0,0 +1,287 @@ +# 使用指南:Robrix + Palpo + Octos + +[English Version](02-using-robrix-with-palpo-and-octos.md) + +> **目标:** 按照本指南操作后,你将掌握如何使用 Robrix 连接 Palpo 服务器、注册账号、创建房间、邀请 AI 机器人、进行对话,以及通过 BotFather 系统管理机器人——全部配有分步截图演示。 + +本指南逐步介绍如何使用 Robrix 客户端连接 Palpo 服务器,并与 Octos AI 机器人进行对话。每个步骤都包含具体的操作说明。 + +**快速索引** + +| 你想做什么 | 跳转到 | +| ------------------------- | --------------------------------- | +| 连接到服务器 | [第 2 节](#2-连接到-palpo) | +| 创建账号 | [第 3 节](#3-注册账号) | +| 与 AI 机器人聊天 | [第 5 节](#5-与-ai-机器人聊天) | +| 创建专业化机器人 | [第 6 节](#6-机器人管理高级功能) | +| 机器人命令与公开/私有设置 | [第 7 节](#7-octos-机器人命令与行为) | + +--- + +## 1. 开始之前 + +请确认: + +- **Palpo 和 Octos 已经启动并运行。** 请参照 [部署指南](01-deploying-palpo-and-octos-zh.md) 完成所有服务的搭建。 +- **Robrix 已安装就绪。** 请参照 [快速入门](../robrix/getting-started-with-robrix-zh.md) 获取构建或下载说明。 + +> **注意:** 本指南假设使用本地部署,`server_name = 127.0.0.1:8128`。如果你使用远程部署,请将相关地址替换为你的实际服务器地址。 + +--- + +## 2. 连接到 Palpo + +打开 Robrix 后会显示登录界面。默认情况下,Robrix 连接到 `matrix.org`。你需要将其指向你自己的 Palpo 服务器。 + +1. 在登录界面的 **底部** 找到 **Homeserver URL** 输入框。 +2. 输入 `http://127.0.0.1:8128`(本地部署)。 +3. 如果是远程服务器,输入 `https://your.server.name` 或 `http://服务器IP:8128`。 + +Robrix 登录界面 — 在底部输入 Homeserver URL + +> **注意:** 如果 Homeserver URL 留空,Robrix 会默认连接到 `matrix.org`。你必须填写此字段才能连接到自己的 Palpo 服务器。 + +--- + +## 3. 注册账号 + +在 Palpo 服务器上创建新账号: + +1. 输入你想要的 **用户名**(例如 `alice`)。 +2. 输入 **密码**。 +3. 在 **确认密码** 字段再次输入相同的密码。 +4. 输入 **Homeserver URL**:`http://127.0.0.1:8128`。 +5. 点击 **Sign up(注册)**。 + +注册账号 — 输入用户名、密码和 Homeserver URL + +> **注意:** 服务器必须启用注册功能。请确保 `palpo.toml` 中设置了 `allow_registration = true`。详见 [部署指南 -- 配置部分](01-deploying-palpo-and-octos-zh.md)。 + +--- + +## 4. 登录 + +如果你已有账号: + +1. 输入 **用户名** 和 **密码**。 +2. 输入 **Homeserver URL**:`http://127.0.0.1:8128` +3. 点击 **Log in(登录)**。 + +登录后会看到房间列表。新账号的房间列表是空的。 + + + +--- + +## 5. 与 AI 机器人聊天 + +这是主要的使用流程:创建房间、邀请机器人、开始对话。 + +### 5.1 创建新房间 + +1. 点击房间列表区域的 **创建房间** 按钮("+" 图标)。 +2. 为房间命名,例如 "AI Chat"。 +3. 房间创建完成,你会自动进入该房间。 + + + +### 5.2 邀请机器人 + +1. 点击房间列表顶部的 **搜索图标**(下图中的 **①**)。 +2. 在搜索对话框中输入机器人的完整 Matrix ID:`@octosbot:127.0.0.1:8128`。 +3. 点击 **People** 标签页(**②**),将搜索结果过滤为用户和机器人(而非 Rooms 或 Spaces)。 +4. 从搜索结果中选择机器人,即可开始直接对话或邀请它加入房间。 +5. 机器人会自动加入。这是通过 Application Service 机制实现的,不需要在机器人端手动接受邀请。 + +搜索机器人:点击搜索图标(1),输入机器人 ID,然后点击 People(2)找到它 + +> **这个机器人名字是怎么来的?** BotFather 的 Matrix ID 由两个配置值组合而成: +> +> | 组成部分 | 值 | 配置位置 | +> | ------------------------ | -------------------------------------- | --------------------------------------------------------------------------- | +> | 用户名(localpart) | `octosbot` | `octos-registration.yaml` 和 `botfather.json` 中的 `sender_localpart` | +> | 服务器域名 | `127.0.0.1:8128` | `palpo.toml` 中的 `server_name` | +> | **完整 Matrix ID** | **`@octosbot:127.0.0.1:8128`** | | +> | 显示名称(房间内显示) | `BotFather` | `botfather.json` 中的 `name` | +> +> 通过 `/createbot` 创建的子机器人遵循类似规则。`botfather.json` 中的 `user_prefix` 字段(默认值:`octosbot_`)会自动拼接在你指定的用户名前面: +> +> `/createbot weather Weather Bot` → Matrix ID:`@octosbot_weather:127.0.0.1:8128` +> +> 如果你在生产环境中更改了 `server_name`,所有机器人 ID 都会随之改变。你还需要同步更新 `octos-registration.yaml` 中的命名空间正则表达式。 + +### 5.3 开始聊天 + +1. 在房间底部的输入框中输入消息。 +2. 按 **Enter** 或点击 **发送**。 +3. 机器人通过配置的 LLM 处理你的消息并回复。 +4. 你会看到流式动画效果,回复内容会实时逐步显示。 + + + +> 机器人的响应时间取决于 LLM 提供商和模型。DeepSeek 通常在几秒内响应,较大的模型可能需要更长时间。 + +**对话示例:** + +``` +你: 什么是 Matrix 协议? +机器人:Matrix 是一个去中心化实时通信的开放标准。它提供 HTTP API + 用于创建和管理聊天室、发送消息以及在联邦服务器之间同步状态…… +``` + +### 5.4 替代方式:加入已有的机器人房间 + +如果其他人已经创建了包含机器人的房间并邀请了你,或者存在公开房间: + +1. 点击 **加入房间**。 +2. 输入房间别名(例如 `#ai-chat:127.0.0.1:8128`)或房间 ID。 +3. 即可直接与机器人聊天。 + + + +--- + +## 6. 机器人管理(高级功能) + +Octos 支持"BotFather"模式:主机器人(`@octosbot`)可以创建**子机器人**,每个子机器人拥有自己的个性和系统提示词。这对于构建专业化的 AI 助手非常有用。 + +如需深入了解其工作原理,请参阅 [架构指南](03-how-robrix-palpo-octos-work-together-zh.md)。 + +### 6.1 在 Robrix 中启用 App Service 支持 + +在管理机器人之前,需要先在 Robrix 中启用该功能: + +1. 打开 Robrix 的 **设置**(齿轮图标)。 +2. 导航到 **Bot Settings(机器人设置)**。 +3. 将 **Enable App Service** 开关打开。 +4. 输入 **BotFather User ID**:`@octosbot:127.0.0.1:8128`。 +5. 点击 **Save(保存)**。 + + + +### 6.2 创建子机器人 + +启用 BotFather 后,你可以创建专业化的子机器人: + +1. 从机器人管理面板打开 **Create Bot(创建机器人)** 对话框。 +2. 填写以下字段: + - **Username(用户名)** -- 仅限小写字母、数字和下划线(例如 `translator_bot`)。 + - **Display Name(显示名称)** -- 在房间中显示的名称(例如 "翻译机器人")。 + - **System Prompt(系统提示词)** -- 定义机器人行为的指令。示例: + - `"你是一个翻译助手。将所有消息翻译成中文。"` + - `"你是一个编程助手。帮助用户编写和调试代码。"` + - `"你是一个写作教练。检查文本的清晰度和语法。"` +3. 点击 **Create Bot(创建机器人)**。 + +子机器人会以 `@octosbot_<用户名>:127.0.0.1:8128` 的格式注册。以上面的例子为例,ID 为 `@octosbot_translator_bot:127.0.0.1:8128`。 + + + +### 6.3 使用子机器人 + +创建子机器人后,使用方式与主机器人相同: + +1. 创建新房间或使用已有房间。 +2. 通过完整的 Matrix ID 邀请子机器人(例如 `@octosbot_translator_bot:127.0.0.1:8128`)。 +3. 与它对话。机器人会按照你定义的系统提示词来响应。 + + + +--- + +## 7. Octos 机器人命令与行为 + +Octos 机器人支持在聊天房间中直接输入少量斜杠命令。本节只保留最主要的 BotFather 管理命令,以及子机器人的公开/私有可见性说明。 + +### 7.1 BotFather 管理命令 + +这些命令只对 BotFather 机器人(`@octosbot`)有效,子机器人不会响应。 + +| 命令 | 说明 | 示例 | +| --------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| `/createbot <用户名> <显示名> [选项]` | 创建子机器人。选项:`--public` 或 `--private`(默认),`--prompt "..."` 设置系统提示词。 | `/createbot weather Weather Bot --public --prompt "You are a weather assistant"` | +| `/deletebot ` | 删除子机器人。只有创建者(或管理员)可以删除。 | `/deletebot @octosbot_weather:127.0.0.1:8128` | +| `/listbots` | 列出所有公开机器人以及你自己创建的私有机器人。 | `/listbots` | +| `/bothelp` | 显示机器人管理命令的帮助信息。 | `/bothelp` | + +> **注意:** 你也可以通过 Robrix 的 UI 创建机器人(第 6.2 节),它提供了表单形式的替代方案。 + +### 7.2 BotFather 与子机器人的区别 + +BotFather 和子机器人的角色不同: + +| | BotFather(`@octosbot`) | 子机器人(`@octosbot_<名称>`) | +| ---------------------------- | ----------------------------------------------------- | ---------------------------------- | +| **角色** | 管理入口 + 通用 AI 聊天 | 专业化 AI 助手 | +| **管理命令** | 支持(`/createbot`、`/deletebot`、`/listbots`) | 不支持 | +| **自定义系统提示词** | 使用默认提示词 | 拥有独立的专用提示词 | +| **能否创建其他机器人** | 能 | 不能 | +| **Matrix 用户 ID** | `@octosbot:server_name` | `@octosbot_<用户名>:server_name` | + +**何时使用哪个:** + +- 使用 **BotFather** 进行通用 AI 对话,以及管理(创建/删除)其他机器人。 +- 使用**子机器人**来完成特定任务(翻译、编程辅助、文字审阅等),它们拥有固定的系统提示词。 + +### 7.3 公开与私有机器人 + +创建子机器人时,你可以设置其**可见性**: + +- **私有(默认):** 只有创建者可以邀请和使用此机器人。其他用户通过 `/listbots` 看不到它,如果尝试邀请它,机器人会短暂加入房间、发送拒绝消息,然后离开。 +- **公开:** 服务器上的任何用户都可以通过 `/listbots` 发现此机器人,将其邀请到房间并与之对话。 + +**创建私有机器人(默认):** + +``` +/createbot myhelper My Helper --prompt "You are my personal assistant" +``` + +**创建公开机器人:** + +``` +/createbot translator Translator Bot --public --prompt "Translate all messages to English" +``` + +**谁可以删除机器人:** + +- 机器人的**创建者**(所有者)可以随时删除它。 +- **管理员**(`botfather.json` 中 `allowed_senders` 列表中的用户)可以删除任何机器人,作为紧急覆盖权限。 + +> **提示:** 建议先创建私有机器人供个人使用。只在你想让服务器上其他用户也能使用时,才将机器人设为公开。 + +--- + +## 8. 使用技巧 + +- **在一个房间中使用多个机器人。** 你可以在同一个房间中邀请多个机器人,每个机器人根据自己的系统提示词独立响应。这对于对比不同模型的输出或构建多智能体工作流很有用。 +- **私密对话。** 创建一个私密房间,只邀请一个机器人,进行不受其他用户或机器人干扰的一对一聊天。 +- **更换 LLM 提供商。** LLM 后端在 `botfather.json` 中配置(或通过环境变量设置)。你可以在 DeepSeek、OpenAI、Anthropic 等提供商之间切换。详见 [部署指南 -- 配置部分](01-deploying-palpo-and-octos-zh.md)。 +- **机器人没有响应?** 常见原因: + + - Octos 服务未运行。 + - LLM API 密钥缺失或无效。 + - 机器人未被正确邀请到房间。 + - 请查看部署指南中的 [故障排查部分](01-deploying-palpo-and-octos-zh.md#5-故障排除)。 +- **Server name 不匹配。** 所有 Matrix ID(用户、机器人、房间)必须使用与 Palpo 配置相同的 `server_name`。如果机器人 ID 与服务器名称不匹配,邀请会失败。 + +--- + +## 9. 常用 Matrix ID 参考 + +本地部署(`server_name = 127.0.0.1:8128`)下的常用 ID: + +| 项目 | Matrix ID | +| -------------------------- | ------------------------------------------- | +| 你的用户账号 | `@你的用户名:127.0.0.1:8128` | +| 主 AI 机器人(BotFather) | `@octosbot:127.0.0.1:8128` | +| 子机器人(例如翻译机器人) | `@octosbot_translator_bot:127.0.0.1:8128` | +| 房间别名 | `#房间名:127.0.0.1:8128` | + +远程部署时,将 `127.0.0.1:8128` 替换为你配置的 `server_name`。 + +--- + +## 接下来 + +- [部署指南](01-deploying-palpo-and-octos-zh.md) -- 搭建和配置服务 +- [架构指南](03-how-robrix-palpo-octos-work-together-zh.md) -- 了解各组件如何协同工作 diff --git a/docs/robrix-with-palpo-and-octos/02-using-robrix-with-palpo-and-octos.md b/docs/robrix-with-palpo-and-octos/02-using-robrix-with-palpo-and-octos.md new file mode 100644 index 000000000..dc3937b2b --- /dev/null +++ b/docs/robrix-with-palpo-and-octos/02-using-robrix-with-palpo-and-octos.md @@ -0,0 +1,287 @@ +# Usage Guide: Robrix + Palpo + Octos + +[中文版](02-using-robrix-with-palpo-and-octos-zh.md) + +> **Goal:** After following this guide, you will know how to use Robrix to connect to your Palpo server, register an account, create rooms, invite AI bots, have conversations, and manage bots through the BotFather system — all demonstrated with step-by-step screenshots. + +This guide walks you through using Robrix as a Matrix client connected to a Palpo homeserver with Octos AI bots. Every step includes what to click and what to type. + +**Quick Reference** + +| What you want to do | Go to | +|---|---| +| Connect to your server | [Section 2](#2-connecting-to-palpo) | +| Create an account | [Section 3](#3-registration) | +| Chat with the AI bot | [Section 5](#5-chatting-with-the-ai-bot) | +| Create specialized bots | [Section 6](#6-bot-management-advanced) | +| Bot commands and public/private bots | [Section 7](#7-octos-bot-commands-and-behavior) | + +--- + +## 1. Before You Start + +Make sure: + +- **Palpo and Octos are running.** Follow the [Deployment Guide](01-deploying-palpo-and-octos.md) to set up all services. +- **Robrix is installed and ready.** See [Getting Started](../robrix/getting-started-with-robrix.md) for build or download instructions. + +> **Note:** This guide assumes a local deployment with `server_name = 127.0.0.1:8128`. Replace this with your actual server name if you deployed remotely. + +--- + +## 2. Connecting to Palpo + +When you open Robrix, the login screen appears. By default, Robrix connects to `matrix.org`. You need to point it to your Palpo server instead. + +1. Look at the **bottom** of the login screen for the **Homeserver URL** field. +2. Enter `http://127.0.0.1:8128` for a local deployment. +3. For a remote server, enter `https://your.server.name` or `http://server-ip:8128`. + +Robrix login screen — enter your Homeserver URL at the bottom + +> **Note:** If the Homeserver URL field is left empty, Robrix connects to `matrix.org` by default. You must fill it in to reach your own Palpo server. + +--- + +## 3. Registration + +To create a new account on your Palpo server: + +1. Enter your desired **Username** (e.g., `alice`). +2. Enter a **Password**. +3. Enter the same password again in the **Confirm password** field. +4. Enter the **Homeserver URL**: `http://127.0.0.1:8128`. +5. Click **Sign up**. + +Register account — enter username, password, and Homeserver URL + +> **Note:** Registration must be enabled on the server. Make sure `allow_registration = true` is set in your `palpo.toml`. See [Deployment Guide -- Configuration](01-deploying-palpo-and-octos.md) for details. + +--- + +## 4. Login + +If you already have an account: + +1. Enter your **Username** and **Password**. +2. Enter the **Homeserver URL**: `http://127.0.0.1:8128`. +3. Click **Log in**. + +After logging in, you will see the room list. For a new account, this list is empty. + + + +--- + +## 5. Chatting with the AI Bot + +This is the main workflow: create a room, invite the bot, and start a conversation. + +### 5.1 Create a New Room + +1. Click the **create room** button (the "+" icon in the room list area). +2. Give the room a name, for example "AI Chat". +3. The room is created and you enter it automatically. + + + +### 5.2 Invite the Bot + +1. Click the **search icon** (**①** in the screenshot below) at the top of the room list. +2. In the search dialog, type the bot's full Matrix ID: `@octosbot:127.0.0.1:8128`. +3. Click the **People** tab (**②**) to filter results to users and bots (instead of Rooms or Spaces). +4. Select the bot from the search results to start a direct conversation or invite it to a room. +5. The bot joins automatically. This is handled by the Application Service mechanism -- no manual acceptance is needed on the bot side. + +Search for the bot: click the search icon (1), type the bot ID, then click People (2) to find it + +> **How is this bot name determined?** The BotFather's Matrix ID is assembled from two config values: +> +> | Part | Value | Configured in | +> |------|-------|---------------| +> | Username (localpart) | `octosbot` | `sender_localpart` in `octos-registration.yaml` and `botfather.json` | +> | Server domain | `127.0.0.1:8128` | `server_name` in `palpo.toml` | +> | **Full Matrix ID** | **`@octosbot:127.0.0.1:8128`** | | +> | Display name (shown in rooms) | `BotFather` | `name` in `botfather.json` | +> +> Child bots created via `/createbot` follow a similar pattern. The `user_prefix` field in `botfather.json` (default: `octosbot_`) is automatically prepended to the username you specify: +> +> `/createbot weather Weather Bot` → Matrix ID: `@octosbot_weather:127.0.0.1:8128` +> +> If you change `server_name` in production, all bot IDs change accordingly. You must also update the namespace regex in `octos-registration.yaml` to match. + +### 5.3 Start Chatting + +1. Type a message in the input box at the bottom of the room. +2. Press **Enter** or click **Send**. +3. The bot processes your message through the configured LLM and replies. +4. You will see a streaming animation as the response arrives in real time. + + + +> The bot's response time depends on the LLM provider and model. DeepSeek typically responds within a few seconds. Larger models may take longer. + +**Example conversation:** + +``` +You: What is the Matrix protocol? +Bot: Matrix is an open standard for decentralized, real-time communication. + It provides HTTP APIs for creating and managing chat rooms, sending + messages, and synchronizing state across federated servers... +``` + +### 5.4 Alternative: Join an Existing Bot Room + +If someone else has already created a room with the bot and invited you, or if a public room exists: + +1. Click **Join Room**. +2. Enter the room alias (e.g., `#ai-chat:127.0.0.1:8128`) or the room ID. +3. You can start chatting with the bot right away. + + + +--- + +## 6. Bot Management (Advanced) + +Octos supports a "BotFather" pattern: the main bot (`@octosbot`) can create **child bots**, each with its own personality and system prompt. This is useful for building specialized assistants. + +For a deeper understanding of how this works, see the [Architecture Guide](03-how-robrix-palpo-octos-work-together.md). + +### 6.1 Enable App Service Support in Robrix + +Before managing bots, enable the feature in Robrix: + +1. Open **Settings** in Robrix (gear icon). +2. Navigate to **Bot Settings**. +3. Toggle **Enable App Service** to on. +4. Enter the **BotFather User ID**: `@octosbot:127.0.0.1:8128`. +5. Click **Save**. + + + +### 6.2 Create Child Bots + +With BotFather enabled, you can create specialized bots: + +1. Open the **Create Bot** dialog from the bot management panel. +2. Fill in the following fields: + - **Username** -- lowercase letters, digits, and underscores only (e.g., `translator_bot`). + - **Display Name** -- a human-readable name shown in rooms (e.g., "Translator Bot"). + - **System Prompt** -- instructions that define the bot's behavior. Examples: + - `"You are a translator. Translate all messages to English."` + - `"You are a coding assistant. Help users write and debug code."` + - `"You are a writing coach. Review text for clarity and grammar."` +3. Click **Create Bot**. + +The child bot is registered as `@octosbot_:127.0.0.1:8128`. For the example above, it would be `@octosbot_translator_bot:127.0.0.1:8128`. + + + +### 6.3 Using Child Bots + +After creating a child bot, use it like the main bot: + +1. Create a new room or use an existing one. +2. Invite the child bot by its full Matrix ID (e.g., `@octosbot_translator_bot:127.0.0.1:8128`). +3. Chat with it. The bot follows the system prompt you defined. + + + +--- + +## 7. Octos Bot Commands and Behavior + +Octos bots support a small set of slash commands that you type directly in the chat room. In this guide, we focus on the main BotFather management commands and the public/private visibility model for child bots. + +### 7.1 BotFather Management Commands + +These commands only work when sent to the BotFather bot (`@octosbot`). Child bots do not respond to them. + +| Command | Description | Example | +|---------|-------------|---------| +| `/createbot [flags]` | Create a new child bot. Flags: `--public` or `--private` (default), `--prompt "..."` for system prompt. | `/createbot weather Weather Bot --public --prompt "You are a weather assistant"` | +| `/deletebot ` | Delete a child bot. Only the bot's creator (or the operator) can delete it. | `/deletebot @octosbot_weather:127.0.0.1:8128` | +| `/listbots` | List all public bots plus your own private bots. | `/listbots` | +| `/bothelp` | Show help text for bot management commands. | `/bothelp` | + +> **Note:** You can also create bots through Robrix's UI (Section 6.2), which provides a form-based alternative to these slash commands. + +### 7.2 BotFather vs Child Bots + +BotFather and child bots serve different roles: + +| | BotFather (`@octosbot`) | Child Bot (`@octosbot_`) | +|---|---|---| +| **Role** | Management gateway + general AI chat | Specialized AI assistant | +| **Bot management commands** | Yes (`/createbot`, `/deletebot`, `/listbots`) | No | +| **Custom system prompt** | Uses default prompt | Has its own dedicated prompt | +| **Can create other bots** | Yes | No | +| **Matrix user ID** | `@octosbot:server_name` | `@octosbot_:server_name` | + +**When to use which:** +- Use **BotFather** for general-purpose AI chat and for managing (creating/deleting) other bots. +- Use **child bots** when you need a dedicated assistant for a specific task (translation, coding help, writing review, etc.) with a fixed system prompt. + +### 7.3 Public vs Private Bots + +When creating a child bot, you can set its **visibility**: + +- **Private (default):** Only the creator can invite and chat with this bot. Other users cannot discover it via `/listbots`, and if they try to invite it, the bot will join briefly, send a rejection message, then leave the room. +- **Public:** Any user on the server can discover the bot via `/listbots`, invite it to rooms, and chat with it. + +**Creating a private bot (default):** +``` +/createbot myhelper My Helper --prompt "You are my personal assistant" +``` + +**Creating a public bot:** +``` +/createbot translator Translator Bot --public --prompt "Translate all messages to English" +``` + +**Who can delete a bot:** +- The **creator** (owner) of the bot can always delete it. +- The **operator** (anyone in `allowed_senders` in `botfather.json`) can delete any bot as an override. + +> **Tip:** Start with private bots for personal use. Make a bot public only when you want other users on the server to use it. + +--- + +## 8. Tips + +- **Multiple bots in one room.** You can invite several bots into the same room. Each bot responds independently based on its own system prompt. This is useful for comparing outputs or building multi-agent workflows. + +- **Private conversations.** Create a private room and invite only one bot for focused 1-on-1 chats without noise from other users or bots. + +- **Change the LLM provider.** The LLM backend is configured in `botfather.json` (or via environment variables). You can switch between DeepSeek, OpenAI, Anthropic, and other providers. See the [Deployment Guide -- Configuration](01-deploying-palpo-and-octos.md) for details. + +- **Bot not responding?** Common causes: + - The Octos service is not running. + - The LLM API key is missing or invalid. + - The bot was not properly invited to the room. + - Check the [Troubleshooting section](01-deploying-palpo-and-octos.md#5-troubleshooting) in the Deployment Guide. + +- **Server name mismatch.** All Matrix IDs (users, bots, rooms) must use the same `server_name` that Palpo is configured with. If your bot ID does not match the server name, the invitation will fail. + +--- + +## 9. Common Matrix IDs Reference + +For a local deployment with `server_name = 127.0.0.1:8128`: + +| Item | Matrix ID | +|---|---| +| Your user account | `@yourusername:127.0.0.1:8128` | +| Main AI bot (BotFather) | `@octosbot:127.0.0.1:8128` | +| A child bot (e.g., translator) | `@octosbot_translator_bot:127.0.0.1:8128` | +| A room alias | `#room-name:127.0.0.1:8128` | + +For remote deployments, replace `127.0.0.1:8128` with your configured `server_name`. + +--- + +## What's Next + +- [Deployment Guide](01-deploying-palpo-and-octos.md) -- set up and configure services +- [Architecture Guide](03-how-robrix-palpo-octos-work-together.md) -- understand how the components work together diff --git a/docs/robrix-with-palpo-and-octos/03-how-robrix-palpo-octos-work-together-zh.md b/docs/robrix-with-palpo-and-octos/03-how-robrix-palpo-octos-work-together-zh.md new file mode 100644 index 000000000..28ed43ea1 --- /dev/null +++ b/docs/robrix-with-palpo-and-octos/03-how-robrix-palpo-octos-work-together-zh.md @@ -0,0 +1,321 @@ +# 架构原理:Robrix + Palpo + Octos 如何协同工作 + +[English Version](03-how-robrix-palpo-octos-work-together.md) + +> **目标:** 阅读本指南后,你将理解 Matrix Application Service(应用服务)机制如何运作,Octos 如何作为 App Service 注册到 Palpo 以接收和回复消息,以及消息从 Robrix 经过 Palpo 到达 AI 机器人再返回的完整生命周期。 + +本文档解释 Robrix + Palpo + Octos 系统背后的**工作机制**。如需部署请参阅 [01-deploying-palpo-and-octos-zh.md](01-deploying-palpo-and-octos-zh.md)。如需使用指南请参阅 [02-using-robrix-with-palpo-and-octos-zh.md](02-using-robrix-with-palpo-and-octos-zh.md)。 + +--- + +## 目录 + +1. [三个项目概览](#1-三个项目概览) +2. [Matrix 协议基础](#2-matrix-协议基础) +3. [Application Service 机制](#3-application-service-机制) +4. [消息生命周期](#4-消息生命周期) +5. [端口与协议](#5-端口与协议) +6. [BotFather 系统](#6-botfather-系统) +7. [延伸阅读](#7-延伸阅读) + +--- + +## 1. 三个项目概览 + +| 项目 | 角色 | 功能说明 | +|------|------|----------| +| [**Robrix**](https://github.com/Project-Robius-China/robrix2) | Matrix 客户端 | 使用 Rust 和 [Makepad](https://github.com/makepad/makepad/) 编写的跨平台 Matrix 聊天客户端。支持 macOS、Linux、Windows、Android 和 iOS 原生运行。这是用户直接交互的应用程序——在这里阅读和发送消息。 | +| [**Palpo**](https://github.com/palpo-im/palpo) | Matrix 服务器 | Rust 原生的 Matrix 主服务器(homeserver)。使用 PostgreSQL 存储用户账号、房间和消息。负责在客户端(Robrix)和应用服务(Octos)之间路由事件。可以把它理解为系统的"中央邮局"。 | +| [**Octos**](https://github.com/octos-org/octos) | AI 机器人(应用服务) | Rust 原生的 AI 代理平台,以 [Matrix Application Service](https://spec.matrix.org/latest/application-service-api/) 的形式运行。从 Palpo 接收消息,将其转发给 LLM(DeepSeek、OpenAI、Anthropic 等),然后将 AI 的回复发布到房间中。 | + +三个项目各自独立且完全开源。组合在一起,它们构成一个完整的 AI 聊天系统:用户通过原生聊天界面与 AI 机器人交互,所有通信都通过符合标准的 Matrix 服务器进行路由。 + +--- + +## 2. Matrix 协议基础 + +在深入架构之前,先了解理解本系统所需的 Matrix 协议核心概念。 + +### 主服务器(Homeserver) + +主服务器是 Matrix 的骨干。它存储用户账号、房间状态和消息历史。每个用户恰好属于一个主服务器——例如,`@alice:example.com` 属于 `example.com` 上的主服务器。在我们的系统中,Palpo 就是主服务器。 + +### 房间(Room) + +房间是一个共享的对话空间。当你发送消息时,消息是发送到房间的,而不是直接发给另一个用户。房间中的所有参与者都能看到消息。房间中可以包含任意组合的真实用户和机器人。 + +### 事件(Event) + +Matrix 中的一切都是**事件**。一条消息是事件(`m.room.message`)。加入房间是事件(`m.room.member`)。修改房间名称也是事件。事件是最基本的数据单元——它们是不可变的、有序的,构成了房间的完整历史记录。 + +### 客户端-服务器 API(Client-Server API) + +这是客户端(如 Robrix)与其主服务器(Palpo)之间的通信方式。Client-Server API 用于: + +- 登录和注册账号 +- 发送消息(`PUT /_matrix/client/v3/rooms/{roomId}/send/...`) +- 同步房间状态和消息历史 +- 管理房间(创建、加入、邀请) + +Robrix 完全通过此 API 与 Palpo 通信。Octos 在发送机器人回复时也使用此 API。 + +### 服务器间 API(Federation) + +这是主服务器之间相互通信的方式。如果 `@alice:server-a.com` 在一个包含 `@bob:server-b.com` 的房间中发送消息,两个主服务器会通过联邦协议(Federation)通信来传递事件。这正是 Matrix 成为去中心化协议的关键所在。详见 [04-federation-with-palpo-zh.md](04-federation-with-palpo-zh.md)。 + +### 滑动同步(Sliding Sync) + +传统的 Matrix 同步会在启动时下载完整的房间状态,在移动设备或受限设备上可能很慢。**滑动同步(Sliding Sync)** 是 Matrix 规范中定义的一种优化同步机制,只发送客户端当前需要的数据——就像在房间列表上滑动一个窗口。Robrix 要求主服务器支持 Sliding Sync。Palpo 原生支持这一特性。 + +--- + +## 3. Application Service 机制 + +本节是架构文档的核心。理解 Application Service(应用服务)机制是理解 Octos 如何接入 Palpo 的关键。 + +### 3.1 什么是 Matrix Application Service? + +Matrix Application Service 是一种在主服务器上拥有**特殊权限**的程序。与使用用户名和密码登录的普通客户端不同,应用服务: + +- **通过 YAML 注册文件向主服务器注册**(而不是通过 Client-Server API) +- **声明独占的用户命名空间** -- 拥有一系列用户 ID,并可以代表其中任何一个用户行事 +- **从主服务器接收推送的事件** -- 无需轮询或同步 +- **不受速率限制** -- 可以按需要的任何速度发送消息 +- **可以动态创建虚拟用户**,无需经过常规注册流程 + +这是为桥接(将 Matrix 连接到 Telegram、Slack 等)和机器人设计的机制。Octos 使用它来运行 AI 机器人。 + +> Matrix 规范参考:[Application Service API](https://spec.matrix.org/latest/application-service-api/) + +### 3.2 注册文件:Palpo 如何发现 Octos + +启动时,Palpo 从 `palpo.toml` 中 `appservice_registration_dir` 指定的目录读取所有 `.yaml` 文件。每个文件代表一个已注册的应用服务。 + +注册文件(`appservices/octos-registration.yaml`)包含: + +```yaml +id: octos-matrix-appservice +url: "http://octos:8009" + +as_token: "" +hs_token: "" + +sender_localpart: octosbot +rate_limited: false + +namespaces: + users: + - exclusive: true + regex: "@octosbot_.*:127\\.0\\.0\\.1:8128" + - exclusive: true + regex: "@octosbot:127\\.0\\.0\\.1:8128" +``` + +各字段说明: + +| 字段 | 用途 | +|------|------| +| `id` | 此应用服务的唯一名称。Palpo 用它来跟踪事件投递状态。 | +| `url` | Palpo 发送事件的 HTTP 端点。Octos 作为 Docker 容器,与 Palpo 在同一个内部网络,所以 Palpo 通过服务名 `octos` 直接访问它。 | +| `as_token` | Octos 调用 Palpo API 时出示的令牌。证明"我是已注册的应用服务"。 | +| `hs_token` | Palpo 向 Octos 推送事件时出示的令牌。证明"我是你注册时对应的主服务器"。 | +| `sender_localpart` | 主机器人的用户名。与 `server_name` 组合后变成 `@octosbot:127.0.0.1:8128`。 | +| `namespaces.users` | 此应用服务独占的用户 ID 正则表达式模式。 | + +这是一种**双向信任关系**:Octos 用 `as_token` 向 Palpo 认证,Palpo 用 `hs_token` 向 Octos 认证。双方必须持有相同的令牌对,分别配置在两个文件中:注册 YAML 文件(给 Palpo 读取)和 `botfather.json`(给 Octos 读取)。如果不匹配,系统将无法工作。请参阅部署指南中的[令牌匹配检查清单](01-deploying-palpo-and-octos-zh.md#38-令牌匹配检查清单)。 + +### 3.3 用户命名空间:机器人身份 + +`namespaces.users` 部分告诉 Palpo 哪些用户 ID 属于 Octos。正则表达式模式声明了特定的范围: + +- **`@octosbot:127.0.0.1:8128`** -- 主机器人,也称为 **BotFather**。这是用户的入口点。 +- **`@octosbot_.*:127.0.0.1:8128`** -- 动态创建的子机器人(例如 `@octosbot_translator:127.0.0.1:8128`)。`.*` 通配符意味着 Octos 可以创建任何带 `octosbot_` 前缀的用户 ID。 + +设置 `exclusive: true` 意味着**没有其他实体可以创建或声明这些用户 ID**。如果普通用户尝试注册为 `@octosbot:127.0.0.1:8128`,Palpo 会拒绝该请求。 + +命名空间机制也是 Palpo 决定是否通知 Octos 的依据。当有人邀请 `@octosbot:127.0.0.1:8128` 加入房间时,Palpo 检查其已注册的应用服务,发现此用户 ID 匹配 Octos 的命名空间,于是将邀请事件推送给 Octos。 + +### 3.4 事件推送流程 + +应用服务协议是**推送式**的,不是拉取式的。应用服务不需要同步或轮询——主服务器主动向它发送事件。 + +当一条消息到达一个包含应用服务用户的房间时: + +1. **Palpo 检查其应用服务注册表。** 它查看哪些应用服务用户是该房间的成员。如果 `@octosbot:127.0.0.1:8128` 在房间中,Palpo 就知道需要通知 Octos。 + +2. **Palpo 向 Octos 发送 HTTP PUT 请求。** 请求发送到 `{url}/transactions/{txnId}`——在我们的场景中是 `http://octos:8009/transactions/{txnId}`。请求体包含事件数据(发送者、房间 ID、消息内容等),Palpo 附带 `hs_token` 进行认证。 + +3. **Octos 处理事件。** 它接收事件,识别房间和发送者,并决定如何响应。对于 AI 机器人来说,这意味着调用配置的 LLM。 + +4. **Octos 通过 Palpo 的 Client-Server API 发送回复。** Octos 没有直接连接到 Robrix 的通道。它以机器人用户的身份通过 Palpo 发送消息,就像其他任何客户端一样,使用 `as_token` 进行认证。 + +这种推送模型非常高效:Octos 不会浪费资源进行轮询,事件以最小延迟传递。 + +--- + +## 4. 消息生命周期 + +以下是一条消息在系统中的完整旅程,从你在 Robrix 中输入到 AI 机器人的回复出现在屏幕上。 + +### 逐步数据流 + +``` +用户在 Robrix 中输入 "Hello" + | + v ++-----------------+ +| 1. Robrix 发送 | PUT /_matrix/client/v3/rooms/{roomId}/send/m.room.message +| (CS API) | -> http://127.0.0.1:8128 (Palpo) ++--------+--------+ + | + v ++-----------------+ +| 2. Palpo 存储 | 事件保存到 PostgreSQL +| 事件 | 房间状态更新 ++--------+--------+ + | + v ++-----------------+ +| 3. Palpo 推送 | PUT /transactions/{txnId} -> http://octos:8009 +| 到 Octos | (Appservice API;Octos 在同一个 Docker 网络中) ++--------+--------+ + | + v ++-----------------+ +| 4. Octos 调用 | POST /v1/chat/completions -> DeepSeek API +| LLM | (或其他配置的提供商) ++--------+--------+ + | + v ++-----------------+ +| 5. Octos 发送 | PUT /_matrix/client/v3/rooms/{roomId}/send/m.room.message +| 回复 | -> http://palpo:8008 (内部 Docker 网络中的 Palpo 服务) +| (CS API) | Auth: Bearer {as_token} ++--------+--------+ + | + v ++-----------------+ +| 6. Palpo 存储 | 机器人回复事件已保存 +| 并投递 | Sliding Sync 推送到 Robrix ++--------+--------+ + | + v +用户在 Robrix 中看到 AI 回复 +``` + +### 每一步发生了什么 + +**步骤 1 -- Robrix 发送消息。** 当你点击发送时,Robrix 向 Palpo 的 Client-Server API 发起 HTTP PUT 请求。请求包含房间 ID、事件类型(`m.room.message`)和消息内容。Robrix 连接到 `http://127.0.0.1:8128`,即 Palpo 暴露在主机上的端口。 + +**步骤 2 -- Palpo 存储事件。** Palpo 接收消息,分配一个事件 ID,并将其持久化到 PostgreSQL。房间状态更新以反映新消息。 + +**步骤 3 -- Palpo 将事件推送给 Octos。** Palpo 检查其应用服务注册表,发现 `@octosbot:127.0.0.1:8128` 是该房间的成员。它通过 HTTP PUT 请求将事件发送到 Octos 的应用服务端点(`http://octos:8009`),路径为 `/transactions/{txnId}`。Octos 作为 Docker 容器,与 Palpo 在同一个内部网络,所以 Palpo 通过服务名 `octos` 解析到它——流量不会离开 Docker 网络。 + +**步骤 4 -- Octos 调用 LLM。** Octos 接收事件,提取消息内容,并调用配置的 LLM 提供商(例如 DeepSeek 的 `/v1/chat/completions` 端点)。它会包含对话历史作为上下文。 + +**步骤 5 -- Octos 发送回复。** LLM 响应后,Octos 以机器人用户(`@octosbot:127.0.0.1:8128`)的身份,通过 Palpo 的 Client-Server API 发送回复。它使用 `as_token` 进行认证。Octos 通过内部 Docker 网络连接 Palpo 的地址是 `http://palpo:8008`——服务名 `palpo` 直接解析到 Palpo 容器的 8008 端口。 + +**步骤 6 -- Palpo 将回复投递给 Robrix。** Palpo 存储机器人的回复事件,并将其包含在 Robrix 的下一次 Sliding Sync 响应中。Robrix 接收事件并在对话中显示 AI 机器人的消息。 + +### 架构图 + +``` ++----------+ +----------+ +----------+ +-----+ +| Robrix | Client-Server API | Palpo | Appservice API | Octos | HTTPS | LLM | +| (客户端) | --------------------> | (服务器) | --------------------> | (机器人) | ------> | | +| | <-------------------- | | <------------------- | | <------ | | ++----------+ Sliding Sync +----------+ Client-Server API +----------+ +-----+ + 你的机器 Docker :8128 Docker :8009 外部服务 +``` + +关键观察: + +- **Robrix 从不直接与 Octos 通信。** 所有通信都通过 Palpo 中转。Robrix 甚至不知道 Octos 的存在——它只看到房间中的机器人用户。 +- **两条不同的路径,同一个 API。** Robrix 和 Octos 都使用 Client-Server API 与 Palpo 通信,但 Octos 使用 `as_token`(应用服务凭证)而非普通用户会话进行认证。 +- **内部流量与外部流量。** Robrix 通过主机端口(`127.0.0.1:8128`)连接到 Palpo。Palpo 和 Octos 都作为 Docker 容器运行在同一个内部网络:Palpo 通过 `octos:8009` 调用 Octos,Octos 反向通过 `palpo:8008` 调用 Palpo。只有 LLM API 调用走公网。 + +--- + +## 5. 端口与协议 + +| 连接 | 协议 | 默认端口 | 方向 | 说明 | +|------|------|----------|------|------| +| Robrix -> Palpo | Client-Server API (Sliding Sync) | 8128 (主机) -> 8008 (容器) | 双向 | Robrix 唯一需要的端口。暴露在主机上。 | +| Palpo -> Octos | Appservice API | 8009 (内部) | Palpo 推送事件 | Octos 监听在 Docker 网络内;Palpo 通过 `octos:8009` 访问。 | +| Octos -> Palpo | Client-Server API | 8008 (内部) | Octos 发送回复 | Octos 通过 `palpo:8008` 经由内部 Docker 网络连接 Palpo。通过 `as_token` 认证。 | +| Octos 管理面板 | HTTP | 8080 (主机) | 入站 | 可选的管理 UI,用于监控 Octos(暴露到主机以便浏览器访问)。 | +| Octos -> LLM | HTTPS | 443 (出站) | 出站 | 对 LLM 提供商的外部 API 调用。 | + +**为什么 Palpo 有两个不同的端口(8008 vs. 8128)?** Palpo 在容器内监听 8008 端口。Docker 将宿主机端口 8128 映射到容器端口 8008,这样在宿主机上原生运行的 Robrix 就能连接 `127.0.0.1:8128`。Octos 也运行在 Docker 内,直接在内部网络通过 `palpo:8008` 连接 Palpo——不需要端口映射。反过来,Palpo 通过 `octos:8009` 访问 Octos,利用 Docker 的服务名解析机制。 + +--- + +## 6. BotFather 系统 + +Octos 实现了 **BotFather** 模式,通过单个应用服务管理多个 AI 机器人。 + +### 父机器人与子机器人 + +**BotFather** 是主机器人(`@octosbot:server_name`)。它是入口点——用户邀请 BotFather 加入房间即可开始交互。但 BotFather 还可以创建**子机器人**,每个子机器人拥有不同的个性和用途。 + +``` +BotFather (@octosbot:127.0.0.1:8128) + | + +-- 翻译机器人 (@octosbot_translator:127.0.0.1:8128) + | 系统提示词: "你是一个翻译。将所有消息翻译成中文。" + | + +-- 代码审查员 (@octosbot_reviewer:127.0.0.1:8128) + | 系统提示词: "你是一个代码审查员。检查代码中的错误和风格问题。" + | + +-- 写作助手 (@octosbot_writer:127.0.0.1:8128) + 系统提示词: "你是一个写作助手。帮助改善文字的清晰度和语气。" +``` + +### 子机器人的工作原理 + +每个子机器人都有自己的: + +- **显示名称** -- 在聊天中显示的可读名称(例如:"翻译机器人") +- **系统提示词(System Prompt)** -- 定义机器人个性和行为的指令 +- **用户 ID** -- 使用 `octosbot_` 前缀生成(例如:`@octosbot_translator:127.0.0.1:8128`) + +子机器人在运行时动态创建。它们不需要单独的注册文件或独立的进程。所有子机器人都在同一个 Octos 应用服务实例中运行。 + +### 为什么这能工作:命名空间的关联 + +还记得注册文件中的命名空间正则表达式吗? + +```yaml +namespaces: + users: + - exclusive: true + regex: "@octosbot_.*:127\\.0\\.0\\.1:8128" +``` + +这个通配符模式正是使动态创建子机器人成为可能的原因。当 Octos 创建新的子机器人(如 `@octosbot_translator:127.0.0.1:8128`)时,Palpo 检查已注册的命名空间,确认该用户 ID 在 Octos 的独占范围内,然后允许创建。无需额外配置。 + +### 从 Robrix 管理机器人 + +Robrix 内置了通过 BotFather 系统创建和管理子机器人的 UI。在 Robrix 的**机器人设置**面板中,你可以: + +1. 启用应用服务支持并配置 BotFather 用户 ID +2. 创建新的子机器人,自定义用户名、显示名称和系统提示词 +3. 查看和管理现有机器人 + +详细的操作步骤请参阅使用指南中的[机器人管理](02-using-robrix-with-palpo-and-octos-zh.md)部分。 + +--- + +## 7. 延伸阅读 + +- **Matrix Application Service 规范:** [spec.matrix.org -- Application Service API](https://spec.matrix.org/latest/application-service-api/) -- 应用服务的官方协议规范。 +- **Octos 文档:** [octos-org.github.io/octos](https://octos-org.github.io/octos/) -- Octos 的完整文档,包括全部 14 种 LLM 提供商、频道、技能和记忆系统。 +- **Palpo GitHub:** [github.com/palpo-im/palpo](https://github.com/palpo-im/palpo) -- Palpo 主服务器文档和源代码。 +- **Robrix GitHub:** [Project-Robius-China/robrix2](https://github.com/Project-Robius-China/robrix2) -- Robrix 客户端源代码和功能跟踪。 +- **Matrix 规范 (Client-Server API):** [spec.matrix.org -- Client-Server API](https://spec.matrix.org/latest/client-server-api/) -- 完整的 Client-Server API 规范,包括 Sliding Sync。 +- **部署指南:** [01-deploying-palpo-and-octos-zh.md](01-deploying-palpo-and-octos-zh.md) -- 如何部署和配置系统。 +- **使用指南:** [02-using-robrix-with-palpo-and-octos-zh.md](02-using-robrix-with-palpo-and-octos-zh.md) -- 如何使用 Robrix 与 AI 机器人交互的分步指南。 + +--- + +*本文档描述的是截至 2026 年 4 月的架构。如需最新更新,请参阅各项目的代码仓库。* diff --git a/docs/robrix-with-palpo-and-octos/03-how-robrix-palpo-octos-work-together.md b/docs/robrix-with-palpo-and-octos/03-how-robrix-palpo-octos-work-together.md new file mode 100644 index 000000000..5b46ff525 --- /dev/null +++ b/docs/robrix-with-palpo-and-octos/03-how-robrix-palpo-octos-work-together.md @@ -0,0 +1,321 @@ +# Architecture: How Robrix + Palpo + Octos Work Together + +[中文版](03-how-robrix-palpo-octos-work-together-zh.md) + +> **Goal:** After reading this guide, you will understand how the Matrix Application Service mechanism works, how Octos registers as an App Service on Palpo to receive and respond to messages, and how the complete message lifecycle flows from Robrix through Palpo to the AI bot and back. + +This document explains the **mechanisms** behind the Robrix + Palpo + Octos system. If you want to deploy it, see [01-deploying-palpo-and-octos.md](01-deploying-palpo-and-octos.md). If you want to use it, see [02-using-robrix-with-palpo-and-octos.md](02-using-robrix-with-palpo-and-octos.md). + +--- + +## Table of Contents + +1. [Three Projects Overview](#1-three-projects-overview) +2. [Matrix Protocol Basics](#2-matrix-protocol-basics) +3. [Application Service Mechanism](#3-application-service-mechanism) +4. [Message Lifecycle](#4-message-lifecycle) +5. [Ports and Protocols](#5-ports-and-protocols) +6. [BotFather System](#6-botfather-system) +7. [Further Reading](#7-further-reading) + +--- + +## 1. Three Projects Overview + +| Project | Role | What it does | +|---------|------|--------------| +| [**Robrix**](https://github.com/Project-Robius-China/robrix2) | Matrix Client | A cross-platform Matrix chat client written in Rust using [Makepad](https://github.com/makepad/makepad/). Runs natively on macOS, Linux, Windows, Android, and iOS. This is the user-facing application -- where you read and send messages. | +| [**Palpo**](https://github.com/palpo-im/palpo) | Matrix Homeserver | A Rust-native Matrix homeserver. Stores user accounts, rooms, and messages in PostgreSQL. Routes events between clients (Robrix) and application services (Octos). Think of it as the central post office. | +| [**Octos**](https://github.com/octos-org/octos) | AI Bot (Appservice) | A Rust-native AI agent platform that runs as a [Matrix Application Service](https://spec.matrix.org/latest/application-service-api/). Receives messages from Palpo, forwards them to an LLM (DeepSeek, OpenAI, Anthropic, etc.), and posts the AI reply back into the room. | + +Each project is independent and open-source. Together, they form a complete AI chat system where users interact with AI bots through a native chat interface, with all communication routed through a standards-compliant Matrix homeserver. + +--- + +## 2. Matrix Protocol Basics + +Before diving into the architecture, here are the Matrix protocol concepts you need to understand. + +### Homeserver + +A homeserver is the backbone of Matrix. It stores user accounts, room state, and message history. Every user belongs to exactly one homeserver -- for example, `@alice:example.com` belongs to the homeserver at `example.com`. In our system, Palpo is the homeserver. + +### Room + +A room is a shared conversation space. When you send a message, it is sent to a room, not directly to another user. All participants in the room see the message. Rooms can contain any mix of human users and bots. + +### Event + +Everything in Matrix is an **event**. A message is an event (`m.room.message`). Joining a room is an event (`m.room.member`). Changing a room's name is an event. Events are the fundamental unit of data -- they are immutable, ordered, and form the room's history. + +### Client-Server API + +This is how clients (like Robrix) communicate with their homeserver (Palpo). The Client-Server API is used for: + +- Logging in and registering accounts +- Sending messages (`PUT /_matrix/client/v3/rooms/{roomId}/send/...`) +- Syncing room state and message history +- Managing rooms (creating, joining, inviting) + +Robrix talks to Palpo exclusively through this API. Octos also uses it when sending bot replies back through Palpo. + +### Server-Server API (Federation) + +This is how homeservers talk to each other. If `@alice:server-a.com` sends a message in a room that `@bob:server-b.com` is in, the two homeservers communicate via federation to deliver the event. This is what makes Matrix a decentralized protocol. See [04-federation-with-palpo.md](04-federation-with-palpo.md) for details. + +### Sliding Sync + +Traditional Matrix sync downloads the entire room state on startup, which can be slow on mobile or constrained devices. **Sliding Sync** is an optimized sync mechanism (defined in the Matrix spec) that only sends the data the client currently needs -- like a sliding window over your room list. Robrix requires Sliding Sync support from the homeserver. Palpo supports it natively. + +--- + +## 3. Application Service Mechanism + +This section is the core of the architecture. Understanding the Application Service (appservice) mechanism is the key to understanding how Octos connects to Palpo. + +### 3.1 What is a Matrix Application Service? + +A Matrix Application Service is a special kind of program that has **elevated privileges** on a homeserver. Unlike a regular client that logs in with a username and password, an appservice: + +- **Registers with the homeserver** via a YAML registration file (not through the Client-Server API) +- **Claims exclusive user namespaces** -- it owns a range of user IDs and can act as any of them +- **Receives pushed events** from the homeserver -- it does not need to poll or sync +- **Is not rate-limited** -- it can send messages at whatever speed it needs +- **Can create virtual users** dynamically, without going through the registration flow + +This is the mechanism designed for bridges (connecting Matrix to Telegram, Slack, etc.) and bots. Octos uses it to run AI bots. + +> Matrix spec reference: [Application Service API](https://spec.matrix.org/latest/application-service-api/) + +### 3.2 Registration File: How Palpo Discovers Octos + +On startup, Palpo reads all `.yaml` files from the directory specified by `appservice_registration_dir` in `palpo.toml`. Each file represents one registered appservice. + +The registration file (`appservices/octos-registration.yaml`) contains: + +```yaml +id: octos-matrix-appservice +url: "http://octos:8009" + +as_token: "" +hs_token: "" + +sender_localpart: octosbot +rate_limited: false + +namespaces: + users: + - exclusive: true + regex: "@octosbot_.*:127\\.0\\.0\\.1:8128" + - exclusive: true + regex: "@octosbot:127\\.0\\.0\\.1:8128" +``` + +Here is what each field does: + +| Field | Purpose | +|-------|---------| +| `id` | A unique name for this appservice. Palpo uses it to track event delivery. | +| `url` | The HTTP endpoint where Palpo sends events. Octos runs as a Docker container on the same internal network as Palpo, so Palpo reaches it by the service name `octos`. | +| `as_token` | The token Octos presents when calling Palpo's API. Proves "I am the registered appservice." | +| `hs_token` | The token Palpo presents when pushing events to Octos. Proves "I am the homeserver you registered with." | +| `sender_localpart` | The main bot's username. Combined with `server_name`, it becomes `@octosbot:127.0.0.1:8128`. | +| `namespaces.users` | Regex patterns for user IDs that this appservice exclusively owns. | + +This is a **mutual trust relationship**: Octos authenticates to Palpo with `as_token`, and Palpo authenticates to Octos with `hs_token`. Both sides must have the same token pair, configured in two files: the registration YAML (for Palpo) and `botfather.json` (for Octos). If they do not match, nothing works. See the [Token Matching Checklist](01-deploying-palpo-and-octos.md#38-token-matching-checklist) in the deployment guide. + +### 3.3 User Namespaces: Bot Identity + +The `namespaces.users` section is how Palpo knows which user IDs belong to Octos. The regex patterns claim specific ranges: + +- **`@octosbot:127.0.0.1:8128`** -- The main bot, also called the **BotFather**. This is the entry point for users. +- **`@octosbot_.*:127.0.0.1:8128`** -- Child bots created dynamically (e.g., `@octosbot_translator:127.0.0.1:8128`). The `.*` wildcard means Octos can create any user ID with the `octosbot_` prefix. + +Setting `exclusive: true` means **no other entity can create or claim these user IDs**. If a regular user tries to register as `@octosbot:127.0.0.1:8128`, Palpo will reject the request. + +This namespace mechanism is also how Palpo decides to notify Octos. When someone invites `@octosbot:127.0.0.1:8128` to a room, Palpo checks its registered appservices, finds that this user ID matches Octos's namespace, and pushes the invite event to Octos. + +### 3.4 Event Push Flow + +The appservice protocol is **push-based**, not pull-based. The appservice does not sync or poll -- the homeserver sends events to it. + +When a message arrives in a room where an appservice user is present: + +1. **Palpo checks its appservice registry.** It looks at which appservice users are members of the room. If `@octosbot:127.0.0.1:8128` is in the room, Palpo knows Octos needs to be notified. + +2. **Palpo sends an HTTP PUT to Octos.** The request goes to `{url}/transactions/{txnId}` -- in our case, `http://octos:8009/transactions/{txnId}`. The body contains the event data (sender, room ID, message content, etc.), and Palpo includes `hs_token` for authentication. + +3. **Octos processes the event.** It receives the event, identifies the room and sender, and decides how to respond. For an AI bot, this means calling the configured LLM. + +4. **Octos sends its reply via Palpo's Client-Server API.** Octos does not have its own connection to Robrix. Instead, it acts as the bot user and sends a message through Palpo, just like any other client. It authenticates with `as_token`. + +This push model is efficient: Octos does not waste resources polling, and events are delivered with minimal latency. + +--- + +## 4. Message Lifecycle + +Here is the complete journey of a message through the system, from the moment you type it in Robrix to the moment the AI bot's reply appears on your screen. + +### Step-by-Step Data Flow + +``` +User types "Hello" in Robrix + | + v ++-----------------+ +| 1. Robrix sends | PUT /_matrix/client/v3/rooms/{roomId}/send/m.room.message +| via CS API | -> http://127.0.0.1:8128 (Palpo) ++--------+--------+ + | + v ++-----------------+ +| 2. Palpo stores | Event saved to PostgreSQL +| the event | Room state updated ++--------+--------+ + | + v ++-----------------+ +| 3. Palpo pushes | PUT /transactions/{txnId} -> http://octos:8009 +| to Octos | (Appservice API; Octos runs in the same Docker network) ++--------+--------+ + | + v ++-----------------+ +| 4. Octos calls | POST /v1/chat/completions -> DeepSeek API +| the LLM | (or other configured provider) ++--------+--------+ + | + v ++-----------------+ +| 5. Octos sends | PUT /_matrix/client/v3/rooms/{roomId}/send/m.room.message +| reply via | -> http://palpo:8008 (Palpo service on the internal Docker network) +| CS API | Auth: Bearer {as_token} ++--------+--------+ + | + v ++-----------------+ +| 6. Palpo stores | Bot's reply event saved +| & delivers | Sliding Sync pushes to Robrix ++--------+--------+ + | + v +User sees AI reply in Robrix +``` + +### What Happens at Each Step + +**Step 1 -- Robrix sends the message.** When you hit send, Robrix makes an HTTP PUT request to Palpo's Client-Server API. The request includes the room ID, the event type (`m.room.message`), and the message content. Robrix connects to `http://127.0.0.1:8128`, which is Palpo's port exposed on the host machine. + +**Step 2 -- Palpo stores the event.** Palpo receives the message, assigns it an event ID, and persists it to PostgreSQL. The room's state is updated to reflect the new message. + +**Step 3 -- Palpo pushes the event to Octos.** Palpo checks its appservice registry and sees that `@octosbot:127.0.0.1:8128` is a member of this room. It sends the event to Octos's appservice endpoint (`http://octos:8009`) via an HTTP PUT to `/transactions/{txnId}`. Octos runs as a Docker container on the same internal network as Palpo, so Palpo resolves it by the service name `octos` -- no traffic leaves the Docker network. + +**Step 4 -- Octos calls the LLM.** Octos receives the event, extracts the message content, and calls the configured LLM provider (e.g., DeepSeek's `/v1/chat/completions` endpoint). It includes conversation history for context. + +**Step 5 -- Octos sends the reply.** Once the LLM responds, Octos sends the reply back through Palpo's Client-Server API, acting as the bot user (`@octosbot:127.0.0.1:8128`). It authenticates with `as_token`. Octos connects to Palpo at `http://palpo:8008` over the internal Docker network -- the service name `palpo` resolves to Palpo's container on port 8008. + +**Step 6 -- Palpo delivers the reply to Robrix.** Palpo stores the bot's reply event and includes it in Robrix's next Sliding Sync response. Robrix receives the event and displays the AI bot's message in the conversation. + +### Architecture Diagram + +``` ++----------+ +----------+ +----------+ +-----+ +| Robrix | Client-Server API | Palpo | Appservice API | Octos | HTTPS | LLM | +| (Client) | --------------------> | (Server) | --------------------> | (Bot) | ------> | | +| | <-------------------- | | <------------------- | | <------ | | ++----------+ Sliding Sync +----------+ Client-Server API +----------+ +-----+ + Your machine Docker :8128 Docker :8009 External +``` + +Key observations: + +- **Robrix never talks directly to Octos.** All communication goes through Palpo. Robrix does not even know Octos exists -- it just sees bot users in rooms. +- **Two different paths, same API.** Both Robrix and Octos use the Client-Server API to talk to Palpo, but Octos authenticates with `as_token` (appservice credential) instead of a regular user session. +- **Internal vs. external traffic.** Robrix connects to Palpo via the host port (`127.0.0.1:8128`). Palpo and Octos both run as Docker containers on the same internal network: Palpo calls Octos via `octos:8009`, and Octos calls back into Palpo via `palpo:8008`. Only the LLM API call goes to the public internet. + +--- + +## 5. Ports and Protocols + +| Connection | Protocol | Default Port | Direction | Notes | +|-----------|----------|-------------|-----------|-------| +| Robrix -> Palpo | Client-Server API (Sliding Sync) | 8128 (host) -> 8008 (container) | Bidirectional | The only port Robrix needs. Exposed on the host machine. | +| Palpo -> Octos | Appservice API | 8009 (internal) | Palpo pushes events | Octos listens inside the Docker network; Palpo reaches it via `octos:8009`. | +| Octos -> Palpo | Client-Server API | 8008 (internal) | Octos sends replies | Octos connects to Palpo via `palpo:8008` over the internal Docker network. Auth via `as_token`. | +| Octos Dashboard | HTTP | 8080 (host) | Inbound | Optional admin UI for monitoring Octos (exposed on the host for browser access). | +| Octos -> LLM | HTTPS | 443 (outbound) | Outbound | External API call to the LLM provider. | + +**Why two different ports for Palpo (8008 vs. 8128)?** Inside the Docker container, Palpo listens on port 8008. Docker maps host port 8128 to container port 8008 so Robrix (running natively on your machine) can connect to `127.0.0.1:8128`. Octos, which also runs inside Docker, connects directly to Palpo on the internal network via `palpo:8008` -- no port mapping needed. Palpo, in turn, reaches Octos at `octos:8009` using Docker's service name resolution. + +--- + +## 6. BotFather System + +Octos implements a **BotFather** pattern for managing multiple AI bots through a single appservice. + +### Parent and Child Bots + +The **BotFather** is the main bot (`@octosbot:server_name`). It is the entry point -- users invite BotFather to a room to start interacting. But BotFather can also create **child bots**, each with a different personality and purpose. + +``` +BotFather (@octosbot:127.0.0.1:8128) + | + +-- Translator Bot (@octosbot_translator:127.0.0.1:8128) + | System prompt: "You are a translator. Translate all messages to English." + | + +-- Code Reviewer (@octosbot_reviewer:127.0.0.1:8128) + | System prompt: "You are a code reviewer. Review code for bugs and style." + | + +-- Writing Assistant (@octosbot_writer:127.0.0.1:8128) + System prompt: "You are a writing assistant. Help improve clarity and tone." +``` + +### How Child Bots Work + +Each child bot has its own: + +- **Display name** -- A human-readable name shown in the chat (e.g., "Translator Bot") +- **System prompt** -- Instructions that define the bot's personality and behavior +- **User ID** -- Generated with the `octosbot_` prefix (e.g., `@octosbot_translator:127.0.0.1:8128`) + +Child bots are created dynamically at runtime. They do not need separate registration files or separate processes. They all run within the single Octos appservice instance. + +### Why This Works: The Namespace Connection + +Remember the namespace regex in the registration file? + +```yaml +namespaces: + users: + - exclusive: true + regex: "@octosbot_.*:127\\.0\\.0\\.1:8128" +``` + +This wildcard pattern is what makes dynamic child bot creation possible. When Octos creates a new child bot like `@octosbot_translator:127.0.0.1:8128`, Palpo checks the registered namespaces, confirms that this user ID is within Octos's exclusive range, and allows it. No additional configuration is needed. + +### Managing Bots from Robrix + +Robrix has a built-in UI for creating and managing child bots through the BotFather system. From Robrix's **Bot Settings** panel, you can: + +1. Enable appservice support and configure the BotFather user ID +2. Create new child bots with a custom username, display name, and system prompt +3. View and manage existing bots + +For step-by-step instructions, see the [Bot Management](02-using-robrix-with-palpo-and-octos.md) section in the usage guide. + +--- + +## 7. Further Reading + +- **Matrix Application Service Spec:** [spec.matrix.org -- Application Service API](https://spec.matrix.org/latest/application-service-api/) -- The official protocol specification for appservices. +- **Octos Book:** [octos-org.github.io/octos](https://octos-org.github.io/octos/) -- Full documentation for Octos, including all 14 LLM providers, channels, skills, and memory. +- **Palpo GitHub:** [github.com/palpo-im/palpo](https://github.com/palpo-im/palpo) -- Palpo homeserver documentation and source. +- **Robrix GitHub:** [Project-Robius-China/robrix2](https://github.com/Project-Robius-China/robrix2) -- Robrix client source and feature tracker. +- **Matrix Spec (Client-Server API):** [spec.matrix.org -- Client-Server API](https://spec.matrix.org/latest/client-server-api/) -- The full Client-Server API specification, including Sliding Sync. +- **Deployment Guide:** [01-deploying-palpo-and-octos.md](01-deploying-palpo-and-octos.md) -- How to deploy and configure the system. +- **Usage Guide:** [02-using-robrix-with-palpo-and-octos.md](02-using-robrix-with-palpo-and-octos.md) -- How to use Robrix with AI bots, step by step. + +--- + +*This document describes the architecture as of April 2026. For the latest updates, see the respective project repositories.* diff --git a/docs/robrix-with-palpo-and-octos/04-federation-with-palpo-zh.md b/docs/robrix-with-palpo-and-octos/04-federation-with-palpo-zh.md new file mode 100644 index 000000000..6f2468976 --- /dev/null +++ b/docs/robrix-with-palpo-and-octos/04-federation-with-palpo-zh.md @@ -0,0 +1,790 @@ +# 联邦功能(本地双节点测试) + +[English Version](04-federation-with-palpo.md) + +> **目标:** 按照本指南操作后,你将在本机运行两个相互联邦的 Palpo 节点:节点 1 上部署 Octos AI 机器人,节点 2 上注册普通用户,然后在 Robrix 中以节点 2 的用户身份**跨服务器**与节点 1 上的机器人聊天 -- 完全不需要公网域名或真实证书。 + +--- + +## 🚀 快速开始(5 条命令跑起来) + +本仓库已经提供了一套**开箱即用的联邦配置**,在 `palpo-and-octos-deploy/federation/` 目录下。你不需要自己写任何配置文件。 + +### 前提 + +- 已安装 Docker + Docker Compose +- 已按 [01-deploying-palpo-and-octos-zh.md](01-deploying-palpo-and-octos-zh.md) clone 过 `repos/palpo` 和 `repos/octos` +- 如果单节点部署在跑,先停掉它:`cd palpo-and-octos-deploy && docker compose down` + +### 运行 + +```bash +cd palpo-and-octos-deploy/federation + +# 1. 生成两节点自签证书(一次性) +./gen-certs.sh + +# 2. 设置 API key +cp .env.example .env +$EDITOR .env # 填 DEEPSEEK_API_KEY + +# 3. 构建并启动全部 5 个服务 +docker compose up -d --build + +# 4. 观察状态(palpo-1 / palpo-2 要变 healthy) +docker compose ps +``` + +### 服务端点对照(后续步骤会用到) + +两个 palpo 容器各自对外暴露**两组端口**:客户端 API(给 Robrix 用的 HTTP)和联邦 API(两节点之间握手用的 TLS)。下面的表把 URL 和 Matrix 身份字符串一起列出来——两者**不是一回事**,新用户最容易混淆这一点。 + +| 服务 | 客户端 API URL(Robrix 里 Homeserver 填这个) | server_name(MXID 里用这个) | 联邦 API(节点之间握手) | 用途 | +|------|------------------------------------------|--------------------------|---------------------|------| +| **palpo-1** | `http://localhost:6001` | `palpo-1:8448` | `https://localhost:6401` | 跑 Octos bot,bot 账号 `@bot:palpo-1:8448` | +| **palpo-2** | `http://localhost:6002` | `palpo-2:8448` | `https://localhost:6402` | 跑普通用户,稍后注册 `@alice:palpo-2:8448` | + +> **记住这条规则**:Robrix 登录 / curl 打 HTTP 用 **左边的 URL**;MXID 里出现的 `palpo-X:8448` 是**身份标识**,不是 URL,不要混着填。 + +### 注册 palpo-2 上的用户(必需) + +全新环境下 palpo-2 上还没有任何用户。在 Robrix 登录界面点 **Sign up(注册)**,用下面的值注册 alice: + +| 字段 | 值 | +|------|-----| +| Username | `alice` | +| Password | `test1234` | +| **Homeserver** | `http://localhost:6002` | + +完整截图和步骤见 [02-using-robrix-with-palpo-and-octos-zh.md 第 3 节 注册账号](02-using-robrix-with-palpo-and-octos-zh.md#3-注册账号)。 + +### 验证联邦通路(可选,用 curl) + +想在打开 Robrix 之前先确认联邦握手没问题,可以用 alice 跨服务器查一次 bot 的 profile(这会触发 palpo-2 → palpo-1 的联邦调用): + +```bash +# alice 登录拿 token +TOKEN=$(curl -s -X POST http://localhost:6002/_matrix/client/v3/login \ + -H "Content-Type: application/json" \ + -d '{"type":"m.login.password","identifier":{"type":"m.id.user","user":"alice"},"password":"test1234"}' \ + | jq -r .access_token) + +# 用 alice 查 palpo-1 上 bot 的 profile(会触发联邦调用) +curl -s "http://localhost:6002/_matrix/client/v3/profile/@bot:palpo-1:8448" \ + -H "Authorization: Bearer $TOKEN" +# 返回非 404 就说明联邦通路 OK +``` + +### 在 Robrix 里跨联邦和 bot 聊天 + +用上一步注册的 alice 账号登录 Robrix: + +| 字段 | 值 | +|------|-----| +| Username | `@alice:palpo-2:8448` | +| Password | `test1234` | +| **Homeserver** | `http://localhost:6002` | + +登录后,点击左侧导航栏的 **+** 按钮打开 **Add/Explore Rooms and Spaces** 页面。**Matrix 的用户目录搜索只覆盖本服务器**,所以你搜不到 palpo-1 上的 bot -- 要走中间的 **Add a friend** 这条路: + +![Robrix Add a friend 面板](../images/robrix-add-friend.png) + +1. 在 **Add a friend** 输入框里填入 bot 的完整 MXID:`@bot:palpo-1:8448` +2. 点击 **Add friend** 按钮 -- Robrix 会通过 palpo-2 → palpo-1 的联邦通道创建 DM 房间 +3. 进入新创建的房间,发送 `hello`,等 bot 回复 + +> **为什么必须从 "Add a friend" 走,而不是搜索?** +> +> Matrix 的用户目录搜索(`/user_directory/search`)**只索引本服务器已知的用户**。palpo-2 刚起来只有 alice 自己,**不认识任何 palpo-1 上的用户**,所以不管在搜索框里怎么搜都找不到 `@bot`。 +> +> "Add a friend" 直接调用 `/createRoom` + `invite`:palpo-2 收到邀请请求后会**主动发起联邦请求**去 palpo-1 验证这个 MXID 存在、是否可邀请 -- 这是跨联邦建立 DM 的**唯一正确入口**。 +> +> 这个限制不是 Robrix 特有的 -- Element、SchildiChat 等所有 Matrix 客户端跨联邦聊天都得这样:输入完整 MXID + 发起邀请,而不是搜索。 + +**如果 bot 回复了消息,说明:联邦握手、AppService 转发、Octos bot 回复这三条链路全部打通了。** + +--- + +## 📚 本文档的后续内容 / 进阶阅读 + +上面的快速开始足以完成测试。下面是这套配置**为什么能工作**的详细解释,遇到问题时查阅。 + +| 你想做什么 | 看哪里 | +|------|------| +| 只想跑起来,遇到报错时查问题 | [第 8 节 故障排查](#8-故障排查) | +| 理解为什么要这么配(架构原理) | [第 2 节 架构](#2-本地双节点架构) + [第 7 节 消息流](#7-消息流详解) | +| 想改配置(不同端口、不同 bot 名字等) | [第 4 节 配置文件说明](#4-配置文件说明) | +| 要部署到真实服务器 | [05-federation-production-deployment-zh.md](05-federation-production-deployment-zh.md)(高级内容) | +| 单节点部署(无联邦) | [01-deploying-palpo-and-octos-zh.md](01-deploying-palpo-and-octos-zh.md) | +| 在 Robrix 里使用 Palpo + Octos | [02-using-robrix-with-palpo-and-octos-zh.md](02-using-robrix-with-palpo-and-octos-zh.md) | + +--- + +## 目录(进阶内容) + +1. [什么是 Matrix 联邦?](#1-什么是-matrix-联邦) +2. [本地双节点架构](#2-本地双节点架构) +3. [文件结构](#3-文件结构) +4. [配置文件说明](#4-配置文件说明) +5. [启动细节](#5-启动细节) +6. [备选方案:API 层测试(CI / 无头脚本)](#6-备选方案api-层测试ci--无头脚本) +7. [消息流详解](#7-消息流详解) +8. [故障排查](#8-故障排查) +9. [下一步](#9-下一步) + +--- + +## 1. 什么是 Matrix 联邦? + +Matrix 是一个**去中心化**的通信协议。每个组织都可以运行自己的服务器,联邦机制允许不同服务器上的用户无缝通信,类似电子邮件: + +- `@alice:server-a.com` 可以和 `@bob:server-b.com` 直接聊天 +- 每个服务器独立存储自己用户的数据 +- 消息在参与对话的所有服务器之间复制同步 +- 任意一台服务器宕机不影响其他服务器 + +Matrix 客户端连接的 API 分两类: + +| API | 端口(默认) | 用途 | +|-----|-------------|------| +| **Client-Server API (C-S)** | 443(或 8008) | 客户端(Robrix、Element)与自己的 homeserver 通信 | +| **Server-Server API (联邦)** | 8448 | 两个 homeserver 之间互相通信 | + +本地部署指南里只用了 C-S API,服务器是隔离的。**联邦的关键是多开一个端口 8448,并且用 TLS 加密。** + +--- + +## 2. 本地双节点架构 + +本文档使用 `palpo-and-octos-deploy/federation/` 目录下的双节点 Docker 环境。两个 Palpo 节点通过 Docker 内部网络(`palpo-federation`)互相发现,不需要公网域名。 + +``` +┌──── Docker 网络 "palpo-federation" ────────────────────┐ +│ │ +│ ┌──────────────────────┐ ┌──────────────────────┐│ +│ │ palpo-1 │ │ palpo-2 ││ +│ │ server_name: │ │ server_name: ││ +│ │ palpo-1:8448 │◄───►│ palpo-2:8448 ││ +│ │ │联邦 │ ││ +│ │ 8008 → host:6001 │8448 │ 8008 → host:6002 ││ +│ │ 8448 → host:6401 │ │ 8448 → host:6402 ││ +│ │ (TLS self-signed) │ │ (TLS self-signed) ││ +│ └──────────┬───────────┘ └──────────────────────┘│ +│ │ │ +│ │ AppService (HTTP transaction) │ +│ ▼ │ +│ ┌──────────────────────┐ │ +│ │ octos │ │ +│ │ bot MXID: │ │ +│ │ @bot:palpo-1:8448 │ │ +│ │ 监听 8009 │ │ +│ └──────────────────────┘ │ +│ │ +│ (postgres 数据库略去) │ +└─────────────────────────────────────────────────────────┘ + + Robrix (host 上) + ↓ 连接到 localhost:6002 + 登录 @alice:palpo-2:8448 + ↓ 发 DM 给 @bot:palpo-1:8448 + (通过联邦跨服务器送达) +``` + +### 端口分配 + +| 服务 | 容器端口 | Host 暴露端口 | 用途 | +|------|---------|-------------|------| +| palpo-1 | 8008 | 6001 | Client-Server API(Robrix / curl 直连) | +| palpo-1 | 8448 | 6401 | 联邦 API(外部调试观察) | +| palpo-2 | 8008 | 6002 | Client-Server API | +| palpo-2 | 8448 | 6402 | 联邦 API | +| octos | 8009 | 8009 | AppService transaction 接收 | + +容器之间通过 Docker 网络别名(`palpo-1`、`palpo-2`、`octos`)直接通信,不走 host 暴露端口。 + +--- + +## 3. 文件结构 + +完整部署目录(git 里的 + 运行时生成的): + +``` +palpo-and-octos-deploy/federation/ +├── docker-compose.yml # 5 个服务:2 palpo + 2 postgres + octos +├── palpo.Dockerfile # Alpine 版 Palpo 构建+运行时镜像 +├── gen-certs.sh # 生成自签 TLS 证书到 certs/ +├── .env.example # 模板:DEEPSEEK_API_KEY、RUST_LOG +├── .gitignore # 排除 certs/ data/ nodes/*/media/ +├── README.md # 目录内速查说明 +├── config/ +│ ├── botfather.json # Octos bot 配置文件(渠道 + LLM + admin_mode) +│ └── octos.json # Octos 运行时:profiles_dir、data_dir、log_level +├── nodes/ +│ ├── node1/ +│ │ ├── palpo.toml # server_name = "palpo-1:8448" +│ │ ├── appservices/ +│ │ │ └── octos.yaml # AppService 注册(octos 的 namespace) +│ │ └── media/ # [运行时] 上传的媒体,持久化 +│ └── node2/ +│ ├── palpo.toml # server_name = "palpo-2:8448" +│ └── media/ # [运行时] +├── certs/ # [运行时] gen-certs.sh 生成 +│ ├── node1.crt / node1.key +│ └── node2.crt / node2.key +└── data/ # [运行时] postgres + octos 持久化 + ├── pg-1/ # palpo-1 的 postgres 数据 + ├── pg-2/ # palpo-2 的 postgres 数据 + └── octos/ # Octos 状态(挂载为 /root/.octos) +``` + +> `[运行时]` 标记的目录被 `.gitignore` 排除 — 跑 `./gen-certs.sh` 和 `docker compose up` 之后才会出现。 + +--- + +## 4. 配置文件说明 + +> **本节目的:** 快速开始用的配置文件已经在 `palpo-and-octos-deploy/federation/` 里写好。下面的内容是**解释每个文件里的关键字段在做什么**,方便你需要改配置时知道该改哪里,以及出错时明白哪里容易坑。 + +### 4.1 生成自签证书(`./gen-certs.sh` 做了什么) + +`gen-certs.sh` 脚本等价于下面这段 openssl 命令: + +```bash +# 为 palpo-1 生成证书,CN 必须匹配 server_name 的主机部分 +openssl req -x509 -nodes -newkey rsa:2048 -days 365 \ + -keyout certs/node1.key -out certs/node1.crt \ + -subj "/CN=palpo-1" \ + -addext "subjectAltName=DNS:palpo-1" + +# 为 palpo-2 生成同样的证书 +openssl req -x509 -nodes -newkey rsa:2048 -days 365 \ + -keyout certs/node2.key -out certs/node2.crt \ + -subj "/CN=palpo-2" \ + -addext "subjectAltName=DNS:palpo-2" +``` + +关键点:**CN 和 subjectAltName 必须匹配 `palpo.toml` 里 `server_name` 的主机部分**(这里是 `palpo-1` / `palpo-2`),否则 TLS 握手会因 hostname 不匹配失败。 + +### 4.2 `docker-compose.yml` + +> 📁 **实际文件:** [`palpo-and-octos-deploy/federation/docker-compose.yml`](../../palpo-and-octos-deploy/federation/docker-compose.yml) -- 下面展示的是关键结构,完整内容请直接看文件。 + +```yaml +services: + # ── Node 1:含 Octos AppService ────────────────────── + palpo-1: + build: + context: .. # 使用 palpo-and-octos-deploy/repos/palpo + dockerfile: federation/palpo.Dockerfile + image: palpo-federation:local-dev + container_name: palpo-1 + depends_on: + palpo-pg-1: { condition: service_healthy } + volumes: + - ./nodes/node1/palpo.toml:/var/palpo/palpo.toml:ro + - ./nodes/node1/media:/var/palpo/media + - ./nodes/node1/appservices:/var/palpo/appservices:ro + - ./certs/node1.crt:/var/palpo/certs/node1.crt:ro + - ./certs/node1.key:/var/palpo/certs/node1.key:ro + environment: + PALPO_CONFIG: /var/palpo/palpo.toml + RUST_LOG: palpo=debug,palpo_core=info + ports: + - "6001:8008" # C-S API + - "6401:8448" # 联邦 API + networks: + federation: { aliases: [palpo-1] } + + palpo-pg-1: + image: postgres:16-alpine + container_name: palpo-pg-1 + environment: + POSTGRES_DB: palpo_node_1 + POSTGRES_USER: palpo + POSTGRES_PASSWORD: palpo + volumes: [pg-1-data:/var/lib/postgresql/data] + networks: [federation] + healthcheck: + test: [CMD-SHELL, pg_isready -U palpo] + interval: 5s + retries: 10 + + # ── Node 2:普通用户(alice 注册到这里)──────────── + palpo-2: + build: # 构建规格和 palpo-1 一样;Docker 层缓存 + context: .. # 让第二次构建基本是 no-op + dockerfile: federation/palpo.Dockerfile + image: palpo-federation:local-dev # 和 palpo-1 共用同一个 tag + container_name: palpo-2 + depends_on: + palpo-pg-2: { condition: service_healthy } + volumes: + - ./nodes/node2/palpo.toml:/var/palpo/palpo.toml:ro + - ./nodes/node2/media:/var/palpo/media + - ./certs/node2.crt:/var/palpo/certs/node2.crt:ro + - ./certs/node2.key:/var/palpo/certs/node2.key:ro + environment: + PALPO_CONFIG: /var/palpo/palpo.toml + RUST_LOG: palpo=debug,palpo_core=info + ports: + - "6002:8008" + - "6402:8448" + networks: + federation: { aliases: [palpo-2] } + + palpo-pg-2: + image: postgres:16-alpine + container_name: palpo-pg-2 + environment: + POSTGRES_DB: palpo_node_2 + POSTGRES_USER: palpo + POSTGRES_PASSWORD: palpo + volumes: [pg-2-data:/var/lib/postgresql/data] + networks: [federation] + healthcheck: + test: [CMD-SHELL, pg_isready -U palpo] + interval: 5s + retries: 10 + + # ── Octos AppService(只对接 palpo-1)───────────────── + octos: + build: + context: ../repos/octos # 本地 Octos 源码(由父目录 ./setup.sh 克隆) + dockerfile: Dockerfile + image: octos-federation:local-dev + container_name: octos + depends_on: [palpo-1] + volumes: + - ./data/octos:/root/.octos # 持久化状态 + - ./config/botfather.json:/root/.octos/profiles/botfather.json:ro # Octos 启动时加载的 bot profile + - ./config/octos.json:/config/octos.json:ro # 运行时配置(profiles_dir 等) + environment: + DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY} + RUST_LOG: ${RUST_LOG:-octos=debug,info} + command: ["serve", "--host", "0.0.0.0", "--port", "8080", "--config", "/config/octos.json"] + ports: + - "8009:8009" # AppService transaction 接收 + - "8010:8080" # Octos 面板 / admin API + networks: + federation: { aliases: [octos] } + +networks: + federation: + name: palpo-federation + +volumes: + pg-1-data: + pg-2-data: +``` + +> **关于 Octos 的位置:** 和单节点部署(`palpo-and-octos-deploy/`)一样,本方案把 Octos 也放在 docker 网络里,AppService URL 使用服务名 `http://octos:8009`。这比"Octos 跑在 host 上 + `host.docker.internal`"更简单,也更接近生产部署模式。 + +### 4.3 `nodes/node1/palpo.toml` + +> 📁 **实际文件:** [`palpo-and-octos-deploy/federation/nodes/node1/palpo.toml`](../../palpo-and-octos-deploy/federation/nodes/node1/palpo.toml) + +```toml +# ── palpo-1: 用 Docker 网络别名当 server_name ── +server_name = "palpo-1:8448" + +allow_registration = true +yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true +enable_admin_room = true + +# ── 本地测试关键:允许自签证书 ── +allow_invalid_tls_certificates = true + +appservice_registration_dir = "/var/palpo/appservices" + +# Client-Server API(明文 HTTP,给 Robrix / curl 用) +[[listeners]] +address = "0.0.0.0:8008" + +# 联邦 API(TLS,给 palpo-2 用) +[[listeners]] +address = "0.0.0.0:8448" +[listeners.tls] +cert = "/var/palpo/certs/node1.crt" +key = "/var/palpo/certs/node1.key" + +[logger] +format = "pretty" + +[db] +url = "postgres://palpo:palpo@palpo-pg-1:5432/palpo_node_1" +pool_size = 10 + +# ── 开启联邦 ── +[federation] +enable = true + +# well-known:供 host 上的客户端发现(Robrix 用 C-S 连接时) +[well_known] +server = "localhost:6401" +client = "http://localhost:6001" +``` + +### 4.4 `nodes/node2/palpo.toml` + +> 📁 **实际文件:** [`palpo-and-octos-deploy/federation/nodes/node2/palpo.toml`](../../palpo-and-octos-deploy/federation/nodes/node2/palpo.toml) + +和 node1 几乎一样,只需要改 server_name、端口、数据库、证书路径: + +```toml +server_name = "palpo-2:8448" + +allow_registration = true +yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true +enable_admin_room = true +allow_invalid_tls_certificates = true + +[[listeners]] +address = "0.0.0.0:8008" + +[[listeners]] +address = "0.0.0.0:8448" +[listeners.tls] +cert = "/var/palpo/certs/node2.crt" +key = "/var/palpo/certs/node2.key" + +[logger] +format = "pretty" + +[db] +url = "postgres://palpo:palpo@palpo-pg-2:5432/palpo_node_2" +pool_size = 10 + +[federation] +enable = true + +[well_known] +server = "localhost:6402" +client = "http://localhost:6002" +``` + +> **注意:** node2 上**没有** `appservice_registration_dir`,因为本地测试里 Octos 只注册在 node1。 + +### 4.5 `nodes/node1/appservices/octos.yaml` + +> 📁 **实际文件:** [`palpo-and-octos-deploy/federation/nodes/node1/appservices/octos.yaml`](../../palpo-and-octos-deploy/federation/nodes/node1/appservices/octos.yaml) + +这是 Palpo-1 侧的 AppService 注册文件,告诉 Palpo:"凡是匹配 `@bot_*:palpo-1:8448` 或 `@bot:palpo-1:8448` 的消息都转发给 Octos"。 + +```yaml +id: octos-matrix-appservice +url: "http://octos:8009" # Docker 网络里 octos 服务名 +as_token: "436682e5f10a0113775779eb8fedf702a095254a95e229c7d20f085b9082903b" +hs_token: "ef642609a1a5b2eda1486a6bada6411f4e861691a7500b10ff26b5b2e16573fd" +sender_localpart: bot +rate_limited: false +namespaces: + users: + - exclusive: true + regex: "@bot:palpo-1:8448" + - exclusive: true + regex: "@bot_.*:palpo-1:8448" + aliases: [] + rooms: [] +``` + +> **生成自己的 token:** 上面的 `as_token` / `hs_token` 仅用于演示。生产环境用 `openssl rand -hex 32` 为每个 token 生成独立的随机值。本地测试可以直接复用上面的示例值。 + +### 4.6 `config/botfather.json` 和 `config/octos.json` + +> 📁 **实际文件:** +> - [`palpo-and-octos-deploy/federation/config/botfather.json`](../../palpo-and-octos-deploy/federation/config/botfather.json) -- Octos bot profile(LLM 设置 + 把 bot 绑到 palpo-1 的 Matrix 渠道) +> - [`palpo-and-octos-deploy/federation/config/octos.json`](../../palpo-and-octos-deploy/federation/config/octos.json) -- Octos 运行时配置(profiles / data 目录等) + +`botfather.json` 是一个 **Octos bot profile**。Octos 启动时从 `profiles_dir` 加载它,用来把 bot 连到某个 LLM 后端 + 某个 Matrix homeserver: + +```json +{ + "id": "botfather", + "name": "BotFather", + "enabled": true, + "config": { + "provider": "deepseek", + "model": "deepseek-chat", + "api_key_env": "DEEPSEEK_API_KEY", + "admin_mode": true, + "channels": [ + { + "type": "matrix", + "homeserver": "http://palpo-1:8008", + "as_token": "436682e5f10a0113775779eb8fedf702a095254a95e229c7d20f085b9082903b", + "hs_token": "ef642609a1a5b2eda1486a6bada6411f4e861691a7500b10ff26b5b2e16573fd", + "server_name": "palpo-1:8448", + "sender_localpart": "bot", + "user_prefix": "bot_", + "port": 8009, + "allowed_senders": [] + } + ], + "gateway": { + "max_history": 50, + "queue_mode": "followup" + } + } +} +``` + +关键字段: + +- **`provider` / `model` / `api_key_env`** — LLM 后端;换成其他 Octos 支持的 provider 时同时改 `.env` 里的 API key 环境变量 +- **`admin_mode: true`** — 开启 Octos 的 admin 指令(和 #86 一致) +- **`channels[0].homeserver` vs `channels[0].server_name`** — 两个不同概念: + - `homeserver = "http://palpo-1:8008"` — Octos 调 Client-Server API 的 HTTP URL + - `server_name = "palpo-1:8448"` — Matrix 身份;必须和 palpo-1 的 `palpo.toml` 一致 +- **`as_token` / `hs_token`** — 必须和 `nodes/node1/appservices/octos.yaml` **完全一致**,否则 palpo-1 拒绝 AppService 连接 +- **`allowed_senders: []`** — 空数组表示所有人(包括联邦用户)都能 DM bot +- **`gateway.queue_mode: "followup"`** — Octos 并发对话的处理策略(`followup` 按房间维度串行回复) + +`octos.json` 要简单得多 — 只告诉 Octos 到哪里找 profiles 和 data: + +```json +{ + "profiles_dir": "/root/.octos/profiles", + "data_dir": "/root/.octos", + "log_level": "debug" +} +``` + +docker-compose 把 `config/botfather.json` 挂载进 `/root/.octos/profiles/`,Octos 启动时就自动发现它了。 + +--- + +## 5. 启动细节 + +> 快速开始已经覆盖基本启动流程。本节补充一些观察点和常见现象。 + +### 5.1 期望的启动顺序 + +``` +1. palpo-pg-1 / palpo-pg-2 启动并通过 pg_isready 健康检查 +2. palpo-1 / palpo-2 连接 postgres,监听 8008 + 8448 +3. palpo-1 加载 /var/palpo/appservices/octos.yaml +4. octos 向 palpo-1 登录为 @bot:palpo-1:8448 +``` + +### 5.2 健康检查和日志 + +```bash +# 5 个容器状态 +docker compose ps +# palpo-pg-1 / palpo-pg-2 → healthy +# palpo-1 / palpo-2 → healthy +# octos → running + +# palpo-2 成功联系 palpo-1(应该看到联邦握手) +docker compose logs palpo-2 | grep -i "palpo-1" + +# Octos 登录成功 +docker compose logs octos | grep -i "bot\|logged in" +``` + +### 5.3 首次构建时间 + +palpo 和 octos 都从源码编译,首次 `up -d --build` 可能花 5-10 分钟。之后重启只用 1-2 秒(除非改了源码)。构建产物走 Docker BuildKit 缓存,不会重复编译 crate。 + +### 5.4 磁盘占用与清理 + +联邦模式会跑 **两份** palpo 镜像、**两份** postgres 和 **一份** octos,占用比单机大: + +| 阶段 | 大小 | +|---|---| +| 镜像(稳态,两份 palpo 共享图层) | ~3 GB | +| Build cache(首次构建峰值) | ~5 GB(可回收) | +| 运行数据(`data/node1` + `data/node2`) | 每节点 ~50-100 MB | + +`docker system df` 显示可回收缓存太多时清理: + +```bash +docker builder prune -af # 清构建缓存(安全) +docker compose down -v # 停容器 + 清数据卷 +docker system prune -af --volumes # 核选项:所有 Docker 相关内容 +``` + +完整说明(为什么缓存会涨、应该选哪条命令)见 [01 §5.5 清理 Docker 缓存](01-deploying-palpo-and-octos-zh.md#55-清理-docker-缓存)。 + +--- + +## 6. 备选方案:API 层测试(CI / 无头脚本) + +> **本节等价于 Quick Start,但用纯 HTTP 调用替代 Robrix UI。** 适合 CI 流水线、命令行复现问题、或想看清 Robrix 在底层到底发了哪些请求。如果只是交互式跑一遍 demo,[Quick Start](#-快速开始5-条命令跑起来) 已经够了,本节可以跳过。 + +### 6.1 用 curl 在 palpo-2 上注册 alice + +等价于在 Robrix 里点 **Sign up** — 走的是 `palpo.toml` 里 `allow_registration = true` 开启的无需鉴权的 `m.login.dummy` 注册流: + +```bash +curl -X POST http://localhost:6002/_matrix/client/v3/register \ + -H "Content-Type: application/json" \ + -d '{ + "username": "alice", + "password": "test1234", + "auth": {"type": "m.login.dummy"} + }' +``` + +预期返回: + +```json +{ + "user_id": "@alice:palpo-2:8448", + "access_token": "...", + "home_server": "palpo-2:8448", + ... +} +``` + +### 6.2 用 curl 验证 palpo-2 和 palpo-1 之间的联邦 + +从 palpo-2 查 palpo-1 上 bot 的 profile — 这会触发一次 server-to-server 握手,在连 UI 之前就能证明联邦通道通畅: + +```bash +# 1) 用 alice 登录拿 token +TOKEN=$(curl -s -X POST http://localhost:6002/_matrix/client/v3/login \ + -H "Content-Type: application/json" \ + -d '{ + "type":"m.login.password", + "identifier":{"type":"m.id.user","user":"alice"}, + "password":"test1234" + }' | jq -r .access_token) + +# 2) 通过 palpo-2 查 palpo-1 上 bot 的 profile(触发联邦) +curl -s "http://localhost:6002/_matrix/client/v3/profile/@bot:palpo-1:8448" \ + -H "Authorization: Bearer $TOKEN" +``` + +**预期结果:** 返回 `{"displayname": "...", "avatar_url": "..."}` 或空对象 `{}`。返回 `404` 说明联邦没通 — 看[第 8 节](#8-故障排查)。 + +### 6.3 和 bot 聊天 + +只要 6.1 注册好了 alice,6.2 证实联邦可用,后面的 DM 创建 + 聊天流程和 Quick Start 完全一致。跟着 [在 Robrix 里跨联邦和 bot 聊天](#在-robrix-里跨联邦和-bot-聊天) 走 UI 版就行(Add a friend → `@bot:palpo-1:8448` → 发 `hello`)。 + +如果 CI 里需要连 DM 创建也自动化,用 Matrix 的 [`POST /_matrix/client/v3/createRoom`](https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3createroom),带上 6.1 里拿到的 token,body 里填 `invite: ["@bot:palpo-1:8448"]` + `is_direct: true` — 这会触发跟 Robrix "Add a friend" 按钮一样的跨联邦 MXID 查找。 + +--- + +## 7. 消息流详解 + +当 alice 给 bot 发 `hello` 时,消息经历如下路径: + +``` +┌─────────────────┐ +│ Robrix (host) │ +│ @alice:palpo-2 │ +└────────┬────────┘ + │ PUT /_matrix/client/v3/rooms/{id}/send/m.room.message + │ 目标 http://localhost:6002 + ▼ +┌─────────────────────────────────────────────────────┐ +│ palpo-2 容器 │ +│ 看到消息事件里有 @bot:palpo-1:8448 │ +│ server_name 部分是 "palpo-1:8448" │ +│ 通过 Docker DNS 解析 palpo-1 → 容器 IP │ +└────────┬────────────────────────────────────────────┘ + │ PUT https://palpo-1:8448/_matrix/federation/v1/send/{txn} + │ TLS(自签证书,allow_invalid=true 跳过验证) + ▼ +┌─────────────────────────────────────────────────────┐ +│ palpo-1 容器(8448 TLS listener) │ +│ 接收联邦事件 │ +│ 检查 MXID 匹配 AppService namespace │ +│ @bot:palpo-1:8448 匹配 octos.yaml 正则 │ +└────────┬────────────────────────────────────────────┘ + │ PUT http://octos:8009/_matrix/app/v1/transactions/{txn} + │ Authorization: Bearer + ▼ +┌─────────────────────────────────────────────────────┐ +│ octos 容器 │ +│ 解析事件,识别 "hello" │ +│ 调用 DeepSeek API 生成回复 │ +└────────┬────────────────────────────────────────────┘ + │ PUT http://palpo-1:8008/_matrix/client/v3/rooms/{id}/send/... + │ Authorization: Bearer (bot 身份) + ▼ +┌─────────────────────────────────────────────────────┐ +│ palpo-1 → 联邦回 palpo-2 → alice 的 Robrix 收到回复 │ +└─────────────────────────────────────────────────────┘ +``` + +**关键观察:** + +1. Robrix 只认识 `localhost:6002`,它**感知不到**联邦的存在 -- 联邦是 palpo-2 内部完成的 +2. 消息在 `palpo-2 → palpo-1` 之间走 TLS 联邦端口 8448,这是 Matrix 规范要求的 +3. `palpo-1 → octos` 是 AppService HTTP,没有联邦概念 -- 对 palpo-1 来说 octos 就是本地的事件处理器 +4. Octos 回复走的是 palpo-1 的 C-S API(它有 `as_token` 伪装成 bot 的身份发消息),不走联邦 + +--- + +## 8. 故障排查 + +### 8.1 诊断清单 + +| 症状 | 可能原因 | 查什么 | +|------|---------|--------| +| `docker compose up` 起不来 | 端口被占用 | `lsof -i :6001 :6002 :6401 :6402 :8009` | +| **首次 up 成功之后,Robrix 注册报 "Account Creation Failed" / 请求 hang** | 对应的 palpo 容器没在服务(`Restarting (127)` 或 `Exited`) | `docker compose ps` — palpo 如果不是 `healthy`,跑 `docker compose logs palpo-2 \| tail -30`。常见根因:运行时镜像里缺 `libgcc`(Rust 的 stack unwinding 需要 `libgcc_s.so.1`;本仓库 `palpo.Dockerfile` 已经 apk install 了 `libgcc`,千万别删)、`palpo.toml` 里证书路径写错、postgres 还没 healthy 时 palpo 就启动了 | +| Step 6.2 profile 查询返回 404 | 联邦未通 | `docker compose logs palpo-2 \| grep -i "fed\|palpo-1"` | +| 机器人能收消息但不回 | Octos → palpo-1 连接异常 | `docker compose logs octos \| tail -50` | +| Robrix 登录报 "invalid homeserver" | Homeserver URL 填错 | 必须是 `http://localhost:6002`,不是 `palpo-2:8448` | +| 创建 DM 时提示 "user not found" | 联邦 profile lookup 失败 | 查 palpo-2 日志看 TLS 握手和证书验证 | +| 消息发出去但没到 | 联邦异步队列堵塞 | `docker compose logs palpo-2 \| grep -i "send_txn\|backoff"` | +| 和 bot 建 DM 时弹出 "Failed to join: it has already been joined" | `/createRoom` 和 sliding sync 的竞态 | 无害 — DM 其实已经建好了,关掉弹窗即可。已被 PR #83(开着)修复 | + +### 8.2 常用调试命令 + +```bash +# 查看全部服务日志(滚动) +docker compose logs -f + +# 只看联邦相关日志 +docker compose logs palpo-1 palpo-2 | grep -i "federation" + +# 从容器内部测试 palpo-1 能否联系 palpo-2 +docker compose exec palpo-1 curl -k https://palpo-2:8448/_matrix/federation/v1/version + +# 查看 palpo-1 上的 AppService 注册状态 +docker compose exec palpo-1 ls -la /var/palpo/appservices/ + +# 重启某个服务(不重启数据库) +docker compose restart palpo-1 octos + +# 完全清掉重来(会删除所有用户和房间数据!) +docker compose down -v +``` + +### 8.3 验证 Octos 注册成功 + +```bash +# palpo-1 应该在启动日志里报告 AppService 已加载 +docker compose logs palpo-1 | grep -i "appservice\|octos" + +# Octos 启动后应该能用 bot token 访问 palpo-1 +docker compose exec octos \ + curl -s -H "Authorization: Bearer 436682e5f10a0113775779eb8fedf702a095254a95e229c7d20f085b9082903b" \ + http://palpo-1:8008/_matrix/client/v3/account/whoami +# 期望:{"user_id":"@bot:palpo-1:8448",...} +``` + +--- + +## 9. 下一步 + +- **切到生产环境:** 本文档使用 Docker DNS 别名 + 自签证书,仅限本机测试。正式部署需要真实域名、Let's Encrypt 证书、反向代理等,参见 → [05-federation-production-deployment-zh.md](05-federation-production-deployment-zh.md) +- **和公共 Matrix 网络联邦:** 生产环境配置完成后,你可以和 `matrix.org` 等公共服务器互通。在公共房间里邀请你的 bot,或者让 `matrix.org` 用户主动来访。 +- **扩展 Octos 能力:** bot 支持多种 LLM 后端、自定义指令、知识库等,参见 Octos 项目文档。 + +--- + +## 延伸阅读 + +- **Matrix 联邦规范:** [spec.matrix.org/latest/server-server-api](https://spec.matrix.org/latest/server-server-api/) -- Server-Server API 协议细节 +- **AppService 规范:** [spec.matrix.org/latest/application-service-api](https://spec.matrix.org/latest/application-service-api/) -- AppService 通信协议 +- **Palpo GitHub:** [github.com/palpo-im/palpo](https://github.com/palpo-im/palpo) -- Palpo 源码和配置参考 +- **Matrix Federation Tester:** [federationtester.matrix.org](https://federationtester.matrix.org/) -- 在线联邦配置检查工具(仅对公网域名有效) + +--- + +*本指南基于 2026 年 4 月的 Palpo 和 Octos 版本。配置文件可能随上游更新而变化,以各项目仓库为准。* diff --git a/docs/robrix-with-palpo-and-octos/04-federation-with-palpo.md b/docs/robrix-with-palpo-and-octos/04-federation-with-palpo.md new file mode 100644 index 000000000..87288d00a --- /dev/null +++ b/docs/robrix-with-palpo-and-octos/04-federation-with-palpo.md @@ -0,0 +1,790 @@ +# Federation (Local Two-Node Testing) + +[中文版](04-federation-with-palpo-zh.md) + +> **Goal:** After following this guide, you will run two federated Palpo nodes on your local machine: node 1 hosts the Octos AI bot, and node 2 hosts a regular user account. You will then chat **across servers** from the node-2 user to the node-1 bot using Robrix -- all without a public domain or real TLS certificates. + +--- + +## 🚀 Quick Start (5 commands) + +This repository ships with a **ready-to-run federation config** under `palpo-and-octos-deploy/federation/`. You do not need to write any configuration files yourself. + +### Prerequisites + +- Docker + Docker Compose installed +- `repos/palpo` and `repos/octos` cloned as described in [01-deploying-palpo-and-octos.md](01-deploying-palpo-and-octos.md) +- If the single-node deployment is running, stop it first: `cd palpo-and-octos-deploy && docker compose down` + +### Run + +```bash +cd palpo-and-octos-deploy/federation + +# 1. Generate self-signed certs for both nodes (one-time) +./gen-certs.sh + +# 2. Set the API key +cp .env.example .env +$EDITOR .env # fill in DEEPSEEK_API_KEY + +# 3. Build and start all 5 services +docker compose up -d --build + +# 4. Watch status (palpo-1 / palpo-2 should become healthy) +docker compose ps +``` + +### Service endpoints (referenced in the steps below) + +Each palpo container exposes **two endpoints**: the Client-Server API (plain HTTP, used by Robrix) and the Federation API (TLS, used by the other palpo node). The table below lists URLs and Matrix identity strings side by side — they are **not the same thing**, and new users most often trip over this distinction. + +| Service | Client API URL (Robrix "Homeserver" field) | server_name (used in MXIDs) | Federation API (node-to-node) | Purpose | +|---------|-------------------------------------------|----------------------------|------------------------------|---------| +| **palpo-1** | `http://localhost:6001` | `palpo-1:8448` | `https://localhost:6401` | Hosts the Octos bot, account `@bot:palpo-1:8448` | +| **palpo-2** | `http://localhost:6002` | `palpo-2:8448` | `https://localhost:6402` | Hosts the regular user, you'll register `@alice:palpo-2:8448` below | + +> **Rule of thumb:** Robrix login / curl HTTP calls use **the URL on the left**; the `palpo-X:8448` that appears inside MXIDs is an **identity label**, not a URL — don't mix them up. + +### Register a user on palpo-2 (required) + +On a fresh environment palpo-2 has no users yet. Click **Sign up** on the Robrix login screen and register alice with: + +| Field | Value | +|-------|-------| +| Username | `alice` | +| Password | `test1234` | +| **Homeserver** | `http://localhost:6002` | + +For screenshots and the full registration flow, see [02-using-robrix-with-palpo-and-octos.md Section 3: Registration](02-using-robrix-with-palpo-and-octos.md#3-registration). + +### Verify federation works (optional, via curl) + +If you want to confirm the federation handshake before opening Robrix, log alice in and query the bot's profile. This triggers a cross-server call from palpo-2 to palpo-1: + +```bash +# Log alice in and grab a token +TOKEN=$(curl -s -X POST http://localhost:6002/_matrix/client/v3/login \ + -H "Content-Type: application/json" \ + -d '{"type":"m.login.password","identifier":{"type":"m.id.user","user":"alice"},"password":"test1234"}' \ + | jq -r .access_token) + +# Query the bot profile on palpo-1 via palpo-2 (triggers federation) +curl -s "http://localhost:6002/_matrix/client/v3/profile/@bot:palpo-1:8448" \ + -H "Authorization: Bearer $TOKEN" +# Non-404 response = federation works +``` + +### Chat with the bot in Robrix + +Log in to Robrix using the alice account you registered above: + +| Field | Value | +|-------|-------| +| Username | `@alice:palpo-2:8448` | +| Password | `test1234` | +| **Homeserver** | `http://localhost:6002` | + +After logging in, click the **+** button in the left nav bar to open **Add/Explore Rooms and Spaces**. **Matrix user-directory search is scoped to each homeserver**, so you *cannot* find the palpo-1 bot via the search box -- use the middle **Add a friend** panel instead: + +![Robrix Add a friend panel](../images/robrix-add-friend.png) + +1. In the **Add a friend** input, enter the bot's full MXID: `@bot:palpo-1:8448` +2. Click **Add friend** -- Robrix creates a DM room through the palpo-2 → palpo-1 federation channel +3. Open the new room, send `hello`, and wait for the bot to reply + +> **Why must you go through "Add a friend" instead of searching?** +> +> Matrix's user-directory search (`/user_directory/search`) **only indexes users known to the local homeserver**. palpo-2 was just started and only knows alice -- it has **no record of any palpo-1 user**, so the search box will never find `@bot` no matter what you type. +> +> "Add a friend" directly invokes `/createRoom` + `invite`: when palpo-2 receives the invite request, it **actively issues a federation request** to palpo-1 to verify the MXID exists and accepts invitations -- this is the **only correct entry point** for creating a cross-federation DM. +> +> This constraint is not Robrix-specific -- Element, SchildiChat, and every other Matrix client work the same way: full MXID + invite, never search. + +**A successful reply confirms all three legs work: federation handshake, AppService forwarding, and the Octos LLM backend.** + +--- + +## 📚 Further Reading in This Document + +The quick start above is enough to finish a test run. The rest of this document explains **why the config works** so you can adjust it or debug problems. + +| What you want to do | Where to look | +|---------------------|---------------| +| Just run it, but something broke | [Section 8: Troubleshooting](#8-troubleshooting) | +| Understand the architecture | [Section 2: Architecture](#2-local-two-node-architecture) + [Section 7: Message Flow](#7-message-flow-explained) | +| Modify the config (different ports, bot name, etc.) | [Section 4: Configuration Details](#4-configuration-details) | +| Deploy to a real server | [05-federation-production-deployment.md](05-federation-production-deployment.md) (advanced) | +| Single-node (no federation) | [01-deploying-palpo-and-octos.md](01-deploying-palpo-and-octos.md) | +| Use Palpo + Octos in Robrix | [02-using-robrix-with-palpo-and-octos.md](02-using-robrix-with-palpo-and-octos.md) | + +--- + +## Table of Contents (advanced reading) + +1. [What is Matrix Federation?](#1-what-is-matrix-federation) +2. [Local Two-Node Architecture](#2-local-two-node-architecture) +3. [File Layout](#3-file-layout) +4. [Configuration Details](#4-configuration-details) +5. [Startup Details](#5-startup-details) +6. [Alternative: API-level testing (for CI / headless scripting)](#6-alternative-api-level-testing-for-ci--headless-scripting) +7. [Message Flow Explained](#7-message-flow-explained) +8. [Troubleshooting](#8-troubleshooting) +9. [Next Steps](#9-next-steps) + +--- + +## 1. What is Matrix Federation? + +Matrix is a **decentralized** communication protocol. Each organization can run its own homeserver, and federation enables users on different homeservers to communicate seamlessly -- much like email: + +- `@alice:server-a.com` can chat with `@bob:server-b.com` directly +- Each server stores its own users' data independently +- Messages are replicated across all servers participating in a conversation +- If one server goes down, the others keep working + +Matrix clients connect via two distinct APIs: + +| API | Default port | Purpose | +|-----|--------------|---------| +| **Client-Server API (C-S)** | 443 (or 8008) | Client (Robrix, Element) talks to its own homeserver | +| **Server-Server API (Federation)** | 8448 | Two homeservers talk to each other | + +The local deployment guide only uses the C-S API -- servers are isolated. **Federation requires opening port 8448 as well, with TLS encryption.** + +--- + +## 2. Local Two-Node Architecture + +This document uses the dual-node Docker environment under `palpo-and-octos-deploy/federation/`. Two Palpo nodes discover each other over an internal Docker network (`palpo-federation`), with no public domain needed. + +``` +┌──── Docker network "palpo-federation" ─────────────────┐ +│ │ +│ ┌──────────────────────┐ ┌──────────────────────┐│ +│ │ palpo-1 │ │ palpo-2 ││ +│ │ server_name: │ │ server_name: ││ +│ │ palpo-1:8448 │◄───►│ palpo-2:8448 ││ +│ │ │ fed │ ││ +│ │ 8008 → host:6001 │8448 │ 8008 → host:6002 ││ +│ │ 8448 → host:6401 │ │ 8448 → host:6402 ││ +│ │ (TLS self-signed) │ │ (TLS self-signed) ││ +│ └──────────┬───────────┘ └──────────────────────┘│ +│ │ │ +│ │ AppService (HTTP transaction) │ +│ ▼ │ +│ ┌──────────────────────┐ │ +│ │ octos │ │ +│ │ bot MXID: │ │ +│ │ @bot:palpo-1:8448 │ │ +│ │ listens on 8009 │ │ +│ └──────────────────────┘ │ +│ │ +│ (postgres databases omitted) │ +└─────────────────────────────────────────────────────────┘ + + Robrix (on host) + ↓ connects to localhost:6002 + login as @alice:palpo-2:8448 + ↓ send DM to @bot:palpo-1:8448 + (delivered cross-server via federation) +``` + +### Port Allocation + +| Service | Container port | Host port | Purpose | +|---------|---------------|-----------|---------| +| palpo-1 | 8008 | 6001 | Client-Server API (Robrix / curl direct) | +| palpo-1 | 8448 | 6401 | Federation API (external debug observation) | +| palpo-2 | 8008 | 6002 | Client-Server API | +| palpo-2 | 8448 | 6402 | Federation API | +| octos | 8009 | 8009 | AppService transaction receiver | + +Containers communicate with each other through Docker network aliases (`palpo-1`, `palpo-2`, `octos`), not through host-exposed ports. + +--- + +## 3. File Layout + +Full deployment directory layout (checked into git + runtime-generated artifacts): + +``` +palpo-and-octos-deploy/federation/ +├── docker-compose.yml # 5 services: 2 palpo + 2 postgres + octos +├── palpo.Dockerfile # Alpine-based Palpo builder + runtime image +├── gen-certs.sh # Generates self-signed TLS certs into certs/ +├── .env.example # Template: DEEPSEEK_API_KEY, RUST_LOG +├── .gitignore # Excludes certs/, data/, nodes/*/media/ +├── README.md # Per-folder quick overview +├── config/ +│ ├── botfather.json # Octos bot profile (channel + LLM + admin_mode) +│ └── octos.json # Octos runtime: profiles_dir, data_dir, log_level +├── nodes/ +│ ├── node1/ +│ │ ├── palpo.toml # server_name = "palpo-1:8448" +│ │ ├── appservices/ +│ │ │ └── octos.yaml # AppService registration (octos namespace) +│ │ └── media/ # [runtime] uploaded media, persistent +│ └── node2/ +│ ├── palpo.toml # server_name = "palpo-2:8448" +│ └── media/ # [runtime] +├── certs/ # [runtime] generated by gen-certs.sh +│ ├── node1.crt / node1.key +│ └── node2.crt / node2.key +└── data/ # [runtime] postgres + octos persistence + ├── pg-1/ # palpo-1's postgres data + ├── pg-2/ # palpo-2's postgres data + └── octos/ # Octos state (mounted as /root/.octos) +``` + +> `[runtime]` folders are ignored by `.gitignore` — they appear after you run `./gen-certs.sh` and `docker compose up`. + +--- + +## 4. Configuration Details + +> **Purpose of this section:** The files used by the quick start are already in `palpo-and-octos-deploy/federation/`. This section explains **what the key fields do**, so you know what to change when customizing, and what's likely to trip you up. + +### 4.1 Self-Signed Certificates (what `./gen-certs.sh` does) + +The `gen-certs.sh` script runs effectively these commands: + +```bash +# Generate cert for palpo-1. CN must match the hostname part of server_name +openssl req -x509 -nodes -newkey rsa:2048 -days 365 \ + -keyout certs/node1.key -out certs/node1.crt \ + -subj "/CN=palpo-1" \ + -addext "subjectAltName=DNS:palpo-1" + +# Same for palpo-2 +openssl req -x509 -nodes -newkey rsa:2048 -days 365 \ + -keyout certs/node2.key -out certs/node2.crt \ + -subj "/CN=palpo-2" \ + -addext "subjectAltName=DNS:palpo-2" +``` + +Key point: **the CN and subjectAltName must match the hostname part of `server_name` in `palpo.toml`** (here `palpo-1` / `palpo-2`), otherwise the TLS handshake fails with a hostname mismatch. + +### 4.2 `docker-compose.yml` + +> 📁 **Actual file:** [`palpo-and-octos-deploy/federation/docker-compose.yml`](../../palpo-and-octos-deploy/federation/docker-compose.yml) -- the snippet below shows the key structure; see the file for the complete content. + +```yaml +services: + # ── Node 1: hosts Octos AppService ──────────────────── + palpo-1: + build: + context: .. # uses palpo-and-octos-deploy/repos/palpo + dockerfile: federation/palpo.Dockerfile + image: palpo-federation:local-dev + container_name: palpo-1 + depends_on: + palpo-pg-1: { condition: service_healthy } + volumes: + - ./nodes/node1/palpo.toml:/var/palpo/palpo.toml:ro + - ./nodes/node1/media:/var/palpo/media + - ./nodes/node1/appservices:/var/palpo/appservices:ro + - ./certs/node1.crt:/var/palpo/certs/node1.crt:ro + - ./certs/node1.key:/var/palpo/certs/node1.key:ro + environment: + PALPO_CONFIG: /var/palpo/palpo.toml + RUST_LOG: palpo=debug,palpo_core=info + ports: + - "6001:8008" # C-S API + - "6401:8448" # Federation API + networks: + federation: { aliases: [palpo-1] } + + palpo-pg-1: + image: postgres:16-alpine + container_name: palpo-pg-1 + environment: + POSTGRES_DB: palpo_node_1 + POSTGRES_USER: palpo + POSTGRES_PASSWORD: palpo + volumes: [pg-1-data:/var/lib/postgresql/data] + networks: [federation] + healthcheck: + test: [CMD-SHELL, pg_isready -U palpo] + interval: 5s + retries: 10 + + # ── Node 2: regular user (alice registered here) ────── + palpo-2: + build: # same build spec as palpo-1; Docker layer cache + context: .. # makes the second build a no-op + dockerfile: federation/palpo.Dockerfile + image: palpo-federation:local-dev # same image tag as palpo-1 + container_name: palpo-2 + depends_on: + palpo-pg-2: { condition: service_healthy } + volumes: + - ./nodes/node2/palpo.toml:/var/palpo/palpo.toml:ro + - ./nodes/node2/media:/var/palpo/media + - ./certs/node2.crt:/var/palpo/certs/node2.crt:ro + - ./certs/node2.key:/var/palpo/certs/node2.key:ro + environment: + PALPO_CONFIG: /var/palpo/palpo.toml + RUST_LOG: palpo=debug,palpo_core=info + ports: + - "6002:8008" + - "6402:8448" + networks: + federation: { aliases: [palpo-2] } + + palpo-pg-2: + image: postgres:16-alpine + container_name: palpo-pg-2 + environment: + POSTGRES_DB: palpo_node_2 + POSTGRES_USER: palpo + POSTGRES_PASSWORD: palpo + volumes: [pg-2-data:/var/lib/postgresql/data] + networks: [federation] + healthcheck: + test: [CMD-SHELL, pg_isready -U palpo] + interval: 5s + retries: 10 + + # ── Octos AppService (only connects to palpo-1) ─────── + octos: + build: + context: ../repos/octos # your local Octos source (cloned by parent ./setup.sh) + dockerfile: Dockerfile + image: octos-federation:local-dev + container_name: octos + depends_on: [palpo-1] + volumes: + - ./data/octos:/root/.octos # persistent state + - ./config/botfather.json:/root/.octos/profiles/botfather.json:ro # bot profile loaded by Octos + - ./config/octos.json:/config/octos.json:ro # runtime config (profiles_dir, etc.) + environment: + DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY} + RUST_LOG: ${RUST_LOG:-octos=debug,info} + command: ["serve", "--host", "0.0.0.0", "--port", "8080", "--config", "/config/octos.json"] + ports: + - "8009:8009" # AppService transaction receiver + - "8010:8080" # Octos dashboard / admin API + networks: + federation: { aliases: [octos] } + +networks: + federation: + name: palpo-federation + +volumes: + pg-1-data: + pg-2-data: +``` + +> **On the Octos location:** Just like the single-node deployment (`palpo-and-octos-deploy/`), this setup runs Octos inside the docker network. The AppService URL uses the service name `http://octos:8009`. This is simpler than "Octos on host with `host.docker.internal`" and closer to the production pattern. + +### 4.3 `nodes/node1/palpo.toml` + +> 📁 **Actual file:** [`palpo-and-octos-deploy/federation/nodes/node1/palpo.toml`](../../palpo-and-octos-deploy/federation/nodes/node1/palpo.toml) + +```toml +# ── palpo-1: use Docker network alias as server_name ── +server_name = "palpo-1:8448" + +allow_registration = true +yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true +enable_admin_room = true + +# ── Local testing: accept self-signed certs ── +allow_invalid_tls_certificates = true + +appservice_registration_dir = "/var/palpo/appservices" + +# Client-Server API (plain HTTP, for Robrix / curl) +[[listeners]] +address = "0.0.0.0:8008" + +# Federation API (TLS, for palpo-2) +[[listeners]] +address = "0.0.0.0:8448" +[listeners.tls] +cert = "/var/palpo/certs/node1.crt" +key = "/var/palpo/certs/node1.key" + +[logger] +format = "pretty" + +[db] +url = "postgres://palpo:palpo@palpo-pg-1:5432/palpo_node_1" +pool_size = 10 + +# ── Enable federation ── +[federation] +enable = true + +# well-known: for host-side clients (e.g., Robrix C-S connection) +[well_known] +server = "localhost:6401" +client = "http://localhost:6001" +``` + +### 4.4 `nodes/node2/palpo.toml` + +> 📁 **Actual file:** [`palpo-and-octos-deploy/federation/nodes/node2/palpo.toml`](../../palpo-and-octos-deploy/federation/nodes/node2/palpo.toml) + +Almost identical to node1 -- only `server_name`, ports, database, and cert paths change: + +```toml +server_name = "palpo-2:8448" + +allow_registration = true +yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = true +enable_admin_room = true +allow_invalid_tls_certificates = true + +[[listeners]] +address = "0.0.0.0:8008" + +[[listeners]] +address = "0.0.0.0:8448" +[listeners.tls] +cert = "/var/palpo/certs/node2.crt" +key = "/var/palpo/certs/node2.key" + +[logger] +format = "pretty" + +[db] +url = "postgres://palpo:palpo@palpo-pg-2:5432/palpo_node_2" +pool_size = 10 + +[federation] +enable = true + +[well_known] +server = "localhost:6402" +client = "http://localhost:6002" +``` + +> **Note:** node2 has **no** `appservice_registration_dir` because Octos is registered only on node1 in this test setup. + +### 4.5 `nodes/node1/appservices/octos.yaml` + +> 📁 **Actual file:** [`palpo-and-octos-deploy/federation/nodes/node1/appservices/octos.yaml`](../../palpo-and-octos-deploy/federation/nodes/node1/appservices/octos.yaml) + +This is the AppService registration file on palpo-1, telling Palpo: "any message matching `@bot_*:palpo-1:8448` or `@bot:palpo-1:8448` should be forwarded to Octos". + +```yaml +id: octos-matrix-appservice +url: "http://octos:8009" # Docker network service name +as_token: "436682e5f10a0113775779eb8fedf702a095254a95e229c7d20f085b9082903b" +hs_token: "ef642609a1a5b2eda1486a6bada6411f4e861691a7500b10ff26b5b2e16573fd" +sender_localpart: bot +rate_limited: false +namespaces: + users: + - exclusive: true + regex: "@bot:palpo-1:8448" + - exclusive: true + regex: "@bot_.*:palpo-1:8448" + aliases: [] + rooms: [] +``` + +> **Generate your own tokens:** The `as_token` / `hs_token` above are for demonstration only. For production, use `openssl rand -hex 32` to generate an independent random value per token. For local testing you can copy-paste the sample values as-is. + +### 4.6 `config/botfather.json` and `config/octos.json` + +> 📁 **Actual files:** +> - [`palpo-and-octos-deploy/federation/config/botfather.json`](../../palpo-and-octos-deploy/federation/config/botfather.json) -- Octos bot profile (LLM settings + Matrix channel that binds it to palpo-1) +> - [`palpo-and-octos-deploy/federation/config/octos.json`](../../palpo-and-octos-deploy/federation/config/octos.json) -- Octos runtime config (where to find profiles / data) + +`botfather.json` is an **Octos bot profile**. Octos loads it from `profiles_dir` at startup and uses it to wire the bot to an LLM backend and to a Matrix homeserver: + +```json +{ + "id": "botfather", + "name": "BotFather", + "enabled": true, + "config": { + "provider": "deepseek", + "model": "deepseek-chat", + "api_key_env": "DEEPSEEK_API_KEY", + "admin_mode": true, + "channels": [ + { + "type": "matrix", + "homeserver": "http://palpo-1:8008", + "as_token": "436682e5f10a0113775779eb8fedf702a095254a95e229c7d20f085b9082903b", + "hs_token": "ef642609a1a5b2eda1486a6bada6411f4e861691a7500b10ff26b5b2e16573fd", + "server_name": "palpo-1:8448", + "sender_localpart": "bot", + "user_prefix": "bot_", + "port": 8009, + "allowed_senders": [] + } + ], + "gateway": { + "max_history": 50, + "queue_mode": "followup" + } + } +} +``` + +Key fields: + +- **`provider` / `model` / `api_key_env`** -- LLM backend; swap for any other Octos-supported provider and update `.env` accordingly +- **`admin_mode: true`** -- unlocks Octos admin commands (matches #86) +- **`channels[0].homeserver` vs `channels[0].server_name`** -- two different concepts: + - `homeserver = "http://palpo-1:8008"` -- HTTP URL Octos uses to call the Client-Server API + - `server_name = "palpo-1:8448"` -- Matrix identity; must match palpo-1's `palpo.toml` +- **`as_token` / `hs_token`** -- must match `nodes/node1/appservices/octos.yaml` exactly, otherwise palpo-1 refuses the AppService connection +- **`allowed_senders: []`** -- empty means any user (including federated) can DM the bot +- **`gateway.queue_mode: "followup"`** -- how Octos queues concurrent conversations (`followup` keeps replies threaded per room) + +`octos.json` is much simpler -- it just tells the Octos daemon where to look: + +```json +{ + "profiles_dir": "/root/.octos/profiles", + "data_dir": "/root/.octos", + "log_level": "debug" +} +``` + +The compose file mounts `config/botfather.json` into `/root/.octos/profiles/`, so Octos discovers it automatically on startup. + +--- + +## 5. Startup Details + +> The Quick Start already covers the basic commands. This section adds what to watch for and common first-run observations. + +### 5.1 Expected Startup Sequence + +``` +1. palpo-pg-1 / palpo-pg-2 start and pass pg_isready +2. palpo-1 / palpo-2 connect to postgres, listen on 8008 + 8448 +3. palpo-1 loads /var/palpo/appservices/octos.yaml +4. octos logs into palpo-1 as @bot:palpo-1:8448 +``` + +### 5.2 Health Checks and Logs + +```bash +# 5 container statuses +docker compose ps +# palpo-pg-1 / palpo-pg-2 → healthy +# palpo-1 / palpo-2 → healthy +# octos → running + +# palpo-2 reaching palpo-1 (federation handshake) +docker compose logs palpo-2 | grep -i "palpo-1" + +# Octos login success +docker compose logs octos | grep -i "bot\|logged in" +``` + +### 5.3 First-Time Build Duration + +Both palpo and octos are compiled from source. The first `docker compose up -d --build` may take 5-10 minutes. Subsequent restarts are 1-2 seconds (unless source changes). Docker BuildKit caches Rust artifacts, so crates aren't recompiled every time. + +### 5.4 Disk Footprint & Cleanup + +Federation mode runs **two** palpo images, **two** postgres instances, and **one** octos, so the footprint is larger than single-node: + +| Stage | Size | +|---|---| +| Images (steady, layers shared across the two palpo instances) | ~3 GB | +| Build cache (first build peak) | ~5 GB (reclaimable) | +| Runtime data (`data/node1` + `data/node2`) | ~50-100 MB per node | + +Clean up when `docker system df` shows too much reclaimable cache: + +```bash +docker builder prune -af # drop build cache (safe) +docker compose down -v # stop + wipe data volumes +docker system prune -af --volumes # nuclear: everything Docker-related +``` + +See [01 §5.5 Cleaning up Docker Cache](01-deploying-palpo-and-octos.md#55-cleaning-up-docker-cache) for the full explanation of why cache grows and which command to pick. + +--- + +## 6. Alternative: API-level testing (for CI / headless scripting) + +> **This section mirrors the Quick Start via raw HTTP calls.** Use it when you want to script the workflow in CI, reproduce bugs from a terminal, or see exactly what Robrix is doing under the hood. If you just want to run through the demo interactively, the [Quick Start](#-quick-start-5-commands) covers the same ground via the Robrix UI and this section can be skipped. + +### 6.1 Register alice on palpo-2 (curl) + +Equivalent to clicking **Sign up** in Robrix — uses the unauthenticated `m.login.dummy` flow enabled by `allow_registration = true` in `palpo.toml`: + +```bash +curl -X POST http://localhost:6002/_matrix/client/v3/register \ + -H "Content-Type: application/json" \ + -d '{ + "username": "alice", + "password": "test1234", + "auth": {"type": "m.login.dummy"} + }' +``` + +Expected response: + +```json +{ + "user_id": "@alice:palpo-2:8448", + "access_token": "...", + "home_server": "palpo-2:8448", + ... +} +``` + +### 6.2 Verify federation between palpo-2 and palpo-1 (curl) + +Query the bot's profile on palpo-1 through palpo-2 — this triggers a server-to-server handshake and proves the federation channel works before you even open a UI client: + +```bash +# 1) Log in as alice to get a token +TOKEN=$(curl -s -X POST http://localhost:6002/_matrix/client/v3/login \ + -H "Content-Type: application/json" \ + -d '{ + "type":"m.login.password", + "identifier":{"type":"m.id.user","user":"alice"}, + "password":"test1234" + }' | jq -r .access_token) + +# 2) Query the bot's profile on palpo-1 (triggers federation) +curl -s "http://localhost:6002/_matrix/client/v3/profile/@bot:palpo-1:8448" \ + -H "Authorization: Bearer $TOKEN" +``` + +**Expected result:** returns `{"displayname": "...", "avatar_url": "..."}` or an empty object `{}`. A `404` means federation is broken — see [Section 8](#8-troubleshooting). + +### 6.3 Chat with the bot + +Once 6.1 has registered alice and 6.2 has confirmed federation works, the DM creation + chat flow is identical to the Quick Start. Follow [Chat with the bot in Robrix](#chat-with-the-bot-in-robrix) for the UI walkthrough (Add a friend → `@bot:palpo-1:8448` → send `hello`). + +If you need to automate the DM creation itself (for CI), use Matrix's [`POST /_matrix/client/v3/createRoom`](https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3createroom) with alice's token from 6.1 — `invite: ["@bot:palpo-1:8448"]` and `is_direct: true` will trigger the same cross-federation lookup that Robrix's "Add a friend" button does. + +--- + +## 7. Message Flow Explained + +When alice sends `hello` to the bot, the message takes this path: + +``` +┌─────────────────┐ +│ Robrix (host) │ +│ @alice:palpo-2 │ +└────────┬────────┘ + │ PUT /_matrix/client/v3/rooms/{id}/send/m.room.message + │ target http://localhost:6002 + ▼ +┌─────────────────────────────────────────────────────┐ +│ palpo-2 container │ +│ Sees event targeting @bot:palpo-1:8448 │ +│ server_name part is "palpo-1:8448" │ +│ Docker DNS resolves palpo-1 → container IP │ +└────────┬────────────────────────────────────────────┘ + │ PUT https://palpo-1:8448/_matrix/federation/v1/send/{txn} + │ TLS (self-signed, allow_invalid=true skips validation) + ▼ +┌─────────────────────────────────────────────────────┐ +│ palpo-1 container (8448 TLS listener) │ +│ Receives federation event │ +│ Checks MXID against AppService namespaces │ +│ @bot:palpo-1:8448 matches octos.yaml regex │ +└────────┬────────────────────────────────────────────┘ + │ PUT http://octos:8009/_matrix/app/v1/transactions/{txn} + │ Authorization: Bearer + ▼ +┌─────────────────────────────────────────────────────┐ +│ octos container │ +│ Parses event, recognizes "hello" │ +│ Calls DeepSeek API for a reply │ +└────────┬────────────────────────────────────────────┘ + │ PUT http://palpo-1:8008/_matrix/client/v3/rooms/{id}/send/... + │ Authorization: Bearer (acting as bot) + ▼ +┌─────────────────────────────────────────────────────┐ +│ palpo-1 → federation back to palpo-2 → alice sees │ +└─────────────────────────────────────────────────────┘ +``` + +**Key observations:** + +1. Robrix only knows about `localhost:6002` -- it is **unaware** of federation. Federation happens entirely inside palpo-2 +2. The `palpo-2 → palpo-1` hop goes through TLS on port 8448, as required by the Matrix spec +3. `palpo-1 → octos` is AppService HTTP, with no federation concept -- to palpo-1, octos is just a local event handler +4. Octos replies through palpo-1's C-S API (using `as_token` to impersonate the bot), not through federation + +--- + +## 8. Troubleshooting + +### 8.1 Diagnostic Checklist + +| Symptom | Likely cause | What to check | +|---------|--------------|---------------| +| `docker compose up` fails | Port conflict | `lsof -i :6001 :6002 :6401 :6402 :8009` | +| **Robrix register returns "Account Creation Failed" / request hangs on a fresh setup** | The target palpo container isn't actually serving (stuck in `Restarting (127)` or `Exited`) | `docker compose ps` — if palpo is not `healthy`, run `docker compose logs palpo-2 \| tail -30`. Common root causes: missing `libgcc` in the runtime image (Rust's stack-unwinding runtime needs `libgcc_s.so.1`; this is why `palpo.Dockerfile` apk-installs `libgcc` — don't remove it), wrong cert paths in `palpo.toml`, or postgres not yet healthy when palpo started | +| Step 6.2 profile query returns 404 | Federation broken | `docker compose logs palpo-2 \| grep -i "fed\|palpo-1"` | +| Bot receives message but doesn't reply | Octos → palpo-1 connection issue | `docker compose logs octos \| tail -50` | +| Robrix login: "invalid homeserver" | Homeserver URL wrong | Must be `http://localhost:6002`, not `palpo-2:8448` | +| "user not found" when creating DM | Federation profile lookup failed | Check palpo-2 logs for TLS handshake and cert validation | +| Message sent but never arrives | Federation async queue backoff | `docker compose logs palpo-2 \| grep -i "send_txn\|backoff"` | +| Spurious "Failed to join: it has already been joined" popup after creating a DM with the bot | Race condition between `/createRoom` and sliding sync | Benign — the DM is already created; just dismiss. Fixed by #83 (open). | + +### 8.2 Common Debug Commands + +```bash +# Follow all service logs +docker compose logs -f + +# Only federation-related logs +docker compose logs palpo-1 palpo-2 | grep -i "federation" + +# From inside palpo-1, test reaching palpo-2 +docker compose exec palpo-1 curl -k https://palpo-2:8448/_matrix/federation/v1/version + +# Check AppService registration on palpo-1 +docker compose exec palpo-1 ls -la /var/palpo/appservices/ + +# Restart a service (without restarting the database) +docker compose restart palpo-1 octos + +# Nuke everything (this wipes all user and room data!) +docker compose down -v +``` + +### 8.3 Verify Octos Is Registered + +```bash +# palpo-1 should have loaded the AppService at startup +docker compose logs palpo-1 | grep -i "appservice\|octos" + +# Octos should be able to use bot token against palpo-1 +docker compose exec octos \ + curl -s -H "Authorization: Bearer 436682e5f10a0113775779eb8fedf702a095254a95e229c7d20f085b9082903b" \ + http://palpo-1:8008/_matrix/client/v3/account/whoami +# Expected: {"user_id":"@bot:palpo-1:8448",...} +``` + +--- + +## 9. Next Steps + +- **Move to production:** This document uses Docker DNS aliases + self-signed certs, which only work on a single machine. For real deployment you need a real domain, Let's Encrypt certs, a reverse proxy, etc. → [05-federation-production-deployment.md](05-federation-production-deployment.md) +- **Federate with the public Matrix network:** Once production is set up, you can talk to `matrix.org` and other public servers. Invite your bot to public rooms, or let `matrix.org` users DM it. +- **Extend Octos capabilities:** The bot supports multiple LLM backends, custom commands, knowledge bases, etc. See the Octos project docs. + +--- + +## Further Reading + +- **Matrix Federation Spec:** [spec.matrix.org/latest/server-server-api](https://spec.matrix.org/latest/server-server-api/) -- Server-Server API protocol details +- **AppService Spec:** [spec.matrix.org/latest/application-service-api](https://spec.matrix.org/latest/application-service-api/) -- AppService communication protocol +- **Palpo GitHub:** [github.com/palpo-im/palpo](https://github.com/palpo-im/palpo) -- Palpo source and configuration reference +- **Matrix Federation Tester:** [federationtester.matrix.org](https://federationtester.matrix.org/) -- Online federation checker (public domains only) + +--- + +*This guide is based on Palpo and Octos as of April 2026. Configuration files may change with upstream updates; refer to each project's repository for the latest details.* diff --git a/docs/robrix-with-palpo-and-octos/05-federation-production-deployment-zh.md b/docs/robrix-with-palpo-and-octos/05-federation-production-deployment-zh.md new file mode 100644 index 000000000..4de44ba5b --- /dev/null +++ b/docs/robrix-with-palpo-and-octos/05-federation-production-deployment-zh.md @@ -0,0 +1,574 @@ +# 联邦功能(生产环境部署) + +> ### ⚠️ 高级内容 / 不需要本地跑就能看这篇 +> +> 本文档是**进阶内容**。如果你只想在本机跑起来一个可用的联邦环境(两个 Palpo + 一个 Octos bot),请看 [04-federation-with-palpo-zh.md](04-federation-with-palpo-zh.md) -- 那篇文档是**完全自足**的,不依赖本文档任何配置。 +> +> 本文档的目的是让你在**真实服务器**上对外提供服务时,知道哪些东西必须从"本地测试模式"换成"生产模式",以及为什么。 + +> **目标:** 按照本指南操作后,你的 Palpo 将部署在真实域名上,支持 Let's Encrypt TLS 证书和反向代理,可以和 `matrix.org` 等公共 Matrix 服务器互通。 + +本文档介绍**生产环境**的 Matrix 联邦部署 -- 真实域名、受信任 TLS 证书、反向代理、DNS 配置、安全加固。 + +--- + +## 🔀 本地测试 vs 生产部署:完整差异速查表 + +下面这张表是本文档的**核心价值** -- 把 04 篇里能跑的本地环境,逐项对照出生产部署需要做什么变化。 + +| 方面 | 本地测试(04 篇) | 生产部署(本篇) | 为什么变 | +|------|-----------------|-----------------|---------| +| **域名 / 主机名** | | | | +| `server_name` | `palpo-1:8448` / `palpo-2:8448` | `matrix.example.com` | 公网服务器要能被 DNS 解析 | +| 服务器如何互相找到 | Docker 网络 DNS 别名 | 真实 DNS A 记录 + (可选) SRV 记录 | Docker 别名只在容器内生效 | +| **TLS 证书** | | | | +| 证书类型 | 自签 `openssl req -x509` | 受信 CA(Let's Encrypt 等) | 远程服务器会拒绝自签证书 | +| 证书验证 | `allow_invalid_tls_certificates = true` | **不设** 或明确 `false` | 生产必须做证书验证 | +| 证书管理 | 手工生成(一次性) | Caddy 自动 / certbot 定期续签 | Let's Encrypt 证书 90 天过期 | +| **网络端口** | | | | +| C-S API 对外 | `localhost:6001` / `localhost:6002` | `https://matrix.example.com` (443) | 公网需标准端口 + HTTPS | +| 联邦 API 对外 | `localhost:6401` / `localhost:6402` | `matrix.example.com:8448` | 其他服务器通过 8448 或 443 + well-known 联系 | +| 反向代理 | 不用 | Caddy / Nginx(强烈推荐) | 集中管理 TLS、well-known、限流 | +| TLS 终止位置 | Palpo 自己(`[[listeners]] [listeners.tls]`) | Caddy / Nginx 终止,Palpo 只跑 HTTP 8008 | 反向代理统一处理证书 | +| **well-known 配置** | | | | +| `[well_known].server` | `localhost:6401`(方便 host 客户端调试) | `matrix.example.com:443` | 生产环境公告真实端点 | +| `[well_known].client` | `http://localhost:6001` | `https://matrix.example.com` | 生产强制 HTTPS | +| well-known 服务方 | Palpo 内置 | Caddy 直接响应(或 Palpo 后端) | Caddy 更灵活,不受 Palpo 重启影响 | +| **安全相关** | | | | +| `allow_registration` | `true`(方便测试) | `false`(先建账号再锁) | 防止随机注册刷账号 | +| `yes_i_am_very_very_sure...` | `true` | 移除或 `false` | 生产不应使用无条件注册 | +| 数据库密码 | `palpo:palpo`(固定弱密码) | `.env` 里的强随机密码 | 防止数据库暴露后被打爆 | +| API key(如 DeepSeek) | 直接写在 `compose.yml` / `config.json` | `.env` 环境变量,加 `.gitignore` | 避免误入 git 仓库 | +| 防火墙 | 无所谓(本机) | 只开 443 / 8448 | 内部端口不对外暴露 | +| **日志与运维** | | | | +| 日志格式 | `pretty`(给人看) | `json`(给日志系统收集) | 结构化日志方便告警 | +| `RUST_LOG` | `debug` | `info` 或 `warn` | 生产减少 I/O 开销 | +| 数据持久化 | Docker volume 足够 | 定期备份 Postgres + media | 生产数据丢失不可恢复 | +| **联邦访问控制** | | | | +| `[federation].enable` | `true` | `true` | 一致 | +| `[federation].allowed_servers` | 不设(全开) | 可设白名单限制联邦对象 | 内部服务器可能只允许特定伙伴 | +| `[federation].denied_servers` | 不设 | 可加黑名单屏蔽恶意服务器 | 用于防垃圾 / 封禁 | +| `trusted_servers` | 不设 | `["matrix.org"]` | 生产环境需要公证服务器帮验证远程 key | +| **Bot (Octos) 配置** | | | | +| `botfather.json` 的 `server_name` | `palpo-1:8448` | `matrix.example.com` | 生产用真实域名 | +| `botfather.json` 的 `homeserver` | `http://palpo-1:8008` | `http://palpo:8008`(仍是 Docker 内部名) | bot 通过 Docker 网络连 Palpo,不走公网 | +| AppService namespace regex | `@bot:palpo-1:8448` | `@octosbot:matrix\\.example\\.com` | 匹配真实 MXID 格式 | +| `allowed_senders` | `[]`(全开) | `[]` 或显式白名单 | 生产可限制谁能用 bot | + +> **重要原则:** 上面表格里,**只有 `server_name` 和几个安全相关的字段必须改**。像 `homeserver` 指向 Docker 内部名(`http://palpo:8008`)这种配置,在本地和生产**都一样** -- 因为 Octos 总是通过 Docker 网络连 Palpo,不需要走公网。 + +--- + +## 📚 本文档的范围 + +| 场景 | 本文档 | 其他文档 | +|------|--------|---------| +| **生产环境联邦** | ✅ 本文档 | -- | +| **本地测试联邦**(Docker DNS、自签证书) | ❌ | [04-federation-with-palpo-zh.md](04-federation-with-palpo-zh.md) | +| 单节点本地部署 | ❌ | [01-deploying-palpo-and-octos-zh.md](01-deploying-palpo-and-octos-zh.md) | + +> **前提条件:** 建议先在本地完成[第 04 篇](04-federation-with-palpo-zh.md)的双节点联邦测试,理解 `server_name`、`well-known`、联邦端口等概念之后再来部署生产环境。本文档假设你已经有能访问真实域名 DNS 的服务器和管理员权限。 + +--- + +## 目录 + +1. [生产部署的前提条件](#1-生产部署的前提条件) +2. [整体架构](#2-整体架构) +3. [域名与 DNS 配置](#3-域名与-dns-配置) +4. [反向代理(Caddy 示例)](#4-反向代理caddy-示例) +5. [`palpo.toml` 生产配置](#5-palpotoml-生产配置) +6. [Docker Compose 变更](#6-docker-compose-变更) +7. [AppService 注册更新](#7-appservice-注册更新) +8. [启动与验证](#8-启动与验证) +9. [使用联邦功能](#9-使用联邦功能) +10. [故障排查](#10-故障排查) +11. [延伸阅读](#11-延伸阅读) + +--- + +## 1. 生产部署的前提条件 + +与本地测试部署不同,生产环境联邦对基础设施有严格要求: + +| 需求 | 本地测试(04 篇) | 生产部署(本篇) | +|------|-----------------|-----------------| +| 域名 | 不需要(Docker DNS 别名) | **必需**(如 `matrix.example.com`) | +| TLS 证书 | 自签(`allow_invalid_tls_certificates = true`) | **必需**(Let's Encrypt 等受信 CA) | +| 端口 443 | 不需要 | **开放**(Client-Server API) | +| 端口 8448 | 仅 Docker 内部 | **开放**(Server-Server 联邦 API) | +| 反向代理 | 不需要 | **推荐**(Caddy / Nginx) | +| DNS 记录 | 不需要 | A 记录必需,SRV 记录可选 | +| 公网 IP | 不需要 | **必需** | + +> **⚠️ 自签名证书不能用于生产联邦。** 其他 Matrix 服务器会拒绝 TLS 连接,联邦消息无法投递。请使用 [Let's Encrypt](https://letsencrypt.org/) 等受信 CA 获取证书。 + +--- + +## 2. 整体架构 + +典型的生产拓扑: + +``` + Internet + │ + │ 443 / 8448 + ▼ + ┌───────────────┐ + │ Caddy │ ← 反向代理 + 自动 TLS + │ (host) │ + └───────┬───────┘ + │ localhost:8008 + ▼ +┌─────────── Docker 网络 ────────────┐ +│ ┌──────────────┐ │ +│ │ Palpo │ │ +│ │ server_name: │ │ +│ │ matrix. │ │ +│ │ example.com │ │ +│ └──────┬───────┘ │ +│ │ │ +│ ▼ AppService │ +│ ┌──────────────┐ ┌─────────┐ │ +│ │ Octos │ │ Postgres│ │ +│ └──────────────┘ └─────────┘ │ +└─────────────────────────────────────┘ +``` + +**关键设计:** + +1. Caddy 监听公网 443,负责 TLS 终止和 Let's Encrypt 证书自动续签 +2. Palpo 在 docker 内部只开 HTTP 8008,由 Caddy 代理 +3. 客户端(Robrix/Element)通过 HTTPS 连 `matrix.example.com` +4. 其他联邦服务器通过 HTTPS 连 `matrix.example.com:8448`(或 443 + well-known 委托) + +--- + +## 3. 域名与 DNS 配置 + +### 3.1 注册域名 + +注册一个域名(如 `example.com`),用子域名分配给 Matrix 服务器,例如 `matrix.example.com`。 + +### 3.2 创建 DNS A 记录 + +将子域名指向服务器的公网 IP: + +``` +matrix.example.com. IN A 203.0.113.10 +``` + +### 3.3 (可选)创建 SRV 记录 + +如果联邦 API 用的不是 8448 的默认端口,或者要让 `example.com` 的联邦跳转到 `matrix.example.com`,需要 SRV 记录: + +``` +_matrix-fed._tcp.example.com. IN SRV 10 0 8448 matrix.example.com. +``` + +> **什么时候不需要 SRV 记录?** 如果你在 `matrix.example.com:443` 上提供了 `/.well-known/matrix/server`,Matrix 客户端会通过 well-known 委托发现联邦端点,不依赖 SRV。大多数生产部署采用这种方式。 + +### 3.4 生产 DNS 参数(palpo.toml) + +在 Docker 网络中运行时,DNS 解析建议用 TCP: + +```toml +query_over_tcp_only = true # 容器网络中 UDP DNS 有时不稳定 +query_all_nameservers = true # 查所有配置的 DNS,避免单点失败 +ip_lookup_strategy = 5 # 5 = 先 IPv4 再 IPv6 +``` + +--- + +## 4. 反向代理(Caddy 示例) + +生产环境**强烈推荐**使用反向代理,原因: + +1. **自动 TLS** -- Caddy 内置 Let's Encrypt,自动申请和续签证书 +2. **well-known 端点管理** -- 直接用 Caddy 响应,不依赖 Palpo +3. **流量控制** -- 限流、日志、WAF 等都好挂载 + +### 4.1 Caddyfile + +```caddyfile +matrix.example.com { + # Matrix 客户端发现端点 + handle /.well-known/matrix/client { + header Access-Control-Allow-Origin "*" + respond `{"m.homeserver":{"base_url":"https://matrix.example.com"}}` + } + + # Matrix 联邦发现端点 + handle /.well-known/matrix/server { + respond `{"m.server":"matrix.example.com:443"}` + } + + # 其他请求代理给 Palpo + reverse_proxy localhost:8008 +} + +# 如果走非 443 端口的联邦,额外监听: +# matrix.example.com:8448 { +# reverse_proxy localhost:8008 +# } +``` + +### 4.2 禁用 Palpo 自带 TLS + +让 Caddy 独家处理 TLS,Palpo 内部只跑明文 HTTP: + +```toml +# palpo.toml +[tls] +enable = false +``` + +### 4.3 Nginx 替代方案 + +如果必须用 Nginx,需要单独用 `certbot` 管理证书: + +```bash +# 申请证书 +sudo certbot --nginx -d matrix.example.com + +# 证书路径 +# /etc/letsencrypt/live/matrix.example.com/fullchain.pem +# /etc/letsencrypt/live/matrix.example.com/privkey.pem +``` + +然后在 Nginx 配置里写 `ssl_certificate` / `ssl_certificate_key` 指令,并代理 `/` 到 `http://127.0.0.1:8008`,以及显式响应 `/.well-known/matrix/server` 和 `/.well-known/matrix/client`。 + +--- + +## 5. `palpo.toml` 生产配置 + +```toml +# ── 核心配置 ────────────────────────────────── +# 修改:真实域名 +server_name = "matrix.example.com" + +# 修改:生产环境关闭开放注册 +# 建议先创建管理员账号,再设为 false +allow_registration = false + +enable_admin_room = true +appservice_registration_dir = "/var/palpo/appservices" + +# 生产环境不要开自签证书豁免! +# allow_invalid_tls_certificates 保持默认 false + +# ── 监听器 ───────────────────────────────────── +# Caddy 在 443 代理到这里 +[[listeners]] +address = "0.0.0.0:8008" + +# ── 日志 ─────────────────────────────────────── +[logger] +format = "json" # 修改:生产用 JSON 格式,方便收集 +level = "info" + +# ── 数据库 ───────────────────────────────────── +[db] +url = "postgres://palpo:<强密码>@palpo_postgres:5432/palpo" +pool_size = 10 + +# ── well-known(Palpo 自己响应;如果 Caddy 代管可省略)─ +[well_known] +server = "matrix.example.com:443" +client = "https://matrix.example.com" + +# ── 联邦设置 ─────────────────────────────────── +[federation] +enable = true +allow_inbound_profile_lookup = true # 允许远程查本地用户 profile +# 可选访问控制: +# allowed_servers = ["matrix.org", "*.trusted.com"] +# denied_servers = ["evil.com"] + +# ── TLS(推荐关闭,让 Caddy 处理)─────────────── +[tls] +enable = false +# 如果不用反向代理: +# enable = true +# cert = "/path/to/fullchain.pem" +# key = "/path/to/privkey.pem" +# dual_protocol = false + +# ── 在线状态与输入提示(跨联邦实时指示)───────── +[presence] +allow_local = true +allow_incoming = true +allow_outgoing = true + +[typing] +allow_incoming = true +allow_outgoing = true +federation_timeout = 30000 + +# ── 受信服务器(key 验证公证人)───────────────── +trusted_servers = ["matrix.org"] + +# ── DNS 优化(容器内推荐)───────────────────── +query_over_tcp_only = true +query_all_nameservers = true +ip_lookup_strategy = 5 +``` + +### 5.1 `[federation]` 字段参考 + +| 字段 | 类型 | 默认 | 说明 | +|------|------|------|------| +| `enable` | bool | `true` | 联邦总开关 | +| `allow_loopback` | bool | `false` | 允许向自身发联邦请求,仅开发用 | +| `allow_device_name` | bool | `false` | 向联邦暴露设备名,隐私考虑建议关 | +| `allow_inbound_profile_lookup` | bool | `true` | 允许远程查本地用户 profile | +| `allowed_servers` | list | 无 | 允许列表,支持通配符 `*.example.com`,未设置则全部允许 | +| `denied_servers` | list | `[]` | 拒绝列表,**优先级高于** `allowed_servers` | + +### 5.2 受信服务器(Perspectives Key 验证) + +`trusted_servers` 充当**公证服务器**,帮助验证其他服务器的签名密钥。这是 [Perspectives 密钥验证](https://spec.matrix.org/latest/server-server-api/#querying-keys-through-another-server)机制。 + +```toml +trusted_servers = ["matrix.org"] +``` + +最常用的选择是 `matrix.org`,因为它是公共联邦的中心节点。 + +--- + +## 6. Docker Compose 变更 + +> **基线说明:** 本节的对比是针对**单节点部署**(`palpo-and-octos-deploy/compose.yml`,`server_name = "127.0.0.1:8128"`),不是 04 号文档的双节点联邦。因为生产环境拓扑几乎总是**单 homeserver + 对外联邦**,结构上更接近单节点而非 04 号的本地双节点模拟。如果你是从 04 号过来的,端口/服务名的本地列略过就好,重点关注**右列**(生产值)— 生产值本身是通用的。 + +相比本地 `compose.yml` 的关键变更: + +```yaml +services: + palpo: + # 镜像与构建部分和本地相同 + ports: + - "8008:8008" # Caddy 代理到此端口(不再暴露 8128) + volumes: + - ./palpo.toml:/var/palpo/palpo.toml:ro + - ./appservices:/var/palpo/appservices:ro + - ./data/media:/var/palpo/media + # 若 Palpo 直接处理 TLS(不推荐),挂载证书: + # - /etc/letsencrypt/live/matrix.example.com:/certs:ro + restart: unless-stopped + # ... 其余配置和本地一致 ... + + palpo_postgres: + environment: + POSTGRES_PASSWORD: ${DB_PASSWORD} # 改:从 .env 读强密码 + POSTGRES_USER: palpo + POSTGRES_DB: palpo + # ... + + octos: + environment: + DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY} + RUST_LOG: octos=info # 改:生产用 info 而非 debug + # ... +``` + +整体架构和本地部署一致 -- Postgres、Palpo、Octos -- 主要差异: + +| 本地 | 生产 | +|------|------| +| `server_name = "127.0.0.1:8128"` | `server_name = "matrix.example.com"` | +| 暴露端口 `8128:8008` | 暴露端口 `8008:8008`(由 Caddy 代理) | +| Palpo 自管 TLS(或不用 TLS) | Caddy 终止 TLS | +| `allow_registration = true` | `allow_registration = false` | +| 日志 `pretty` | 日志 `json` | +| API key 可直接写 yml | 从 `.env` 环境变量读 | + +--- + +## 7. AppService 注册更新 + +从 `127.0.0.1:8128` 切到真实域名时,需要更新以下文件。 + +### 7.1 AppService namespace 文件 + +不同的本地部署用不同的文件名: + +| 起点 | 路径 | +|------|------| +| 单节点(01 号文档) | `palpo-and-octos-deploy/appservices/octos-registration.yaml` | +| 双节点联邦(04 号文档) | `palpo-and-octos-deploy/federation/nodes/node1/appservices/octos.yaml` | + +不管用哪个,都要把 namespace regex 改成你的真实域名: + +```yaml +namespaces: + users: + - exclusive: true + regex: "@octosbot_.*:matrix\\.example\\.com" # 改:真实域名 + - exclusive: true + regex: "@octosbot:matrix\\.example\\.com" # 改:真实域名 +``` + +### 7.2 `config/botfather.json` + +```json +{ + "config": { + "channels": [{ + "type": "matrix", + "homeserver": "http://palpo:8008", + "server_name": "matrix.example.com", + "sender_localpart": "octosbot", + ... + }] + } +} +``` + +> **重要:** `homeserver` URL **保持** Docker 内部地址(`http://palpo:8008`),因为 Octos 通过 Docker 网络连接 Palpo -- 不需要走公网。只有 `server_name` 需要改为真实域名,因为那是对外的 Matrix 身份。 + +--- + +## 8. 启动与验证 + +### 8.1 启动服务 + +```bash +cd palpo-and-octos-deploy # 或你的生产部署目录 + +# 设置环境变量 +cp .env.example .env +vim .env # 填 DEEPSEEK_API_KEY、DB_PASSWORD 等 + +# 启动 +docker compose up -d + +# 查看状态 +docker compose ps +docker compose logs -f +``` + +### 8.2 测试 well-known 端点 + +```bash +# 服务器发现(其他联邦服务器用) +curl https://matrix.example.com/.well-known/matrix/server +# 期望:{"m.server":"matrix.example.com:443"} + +# 客户端发现(Robrix/Element 用) +curl https://matrix.example.com/.well-known/matrix/client +# 期望:{"m.homeserver":{"base_url":"https://matrix.example.com"}} +``` + +### 8.3 Matrix Federation Tester + +访问 [https://federationtester.matrix.org](https://federationtester.matrix.org) 输入你的域名(`matrix.example.com`)。会检查: + +- DNS 解析是否正确 +- TLS 证书是否受信任 +- well-known 端点是否响应 +- Server-Server API 是否可达 +- 签名密钥验证是否通过 + +所有检查通过才说明联邦配置完整。 + +--- + +## 9. 使用联邦功能 + +联邦生效后,你可以: + +### 9.1 加入其他服务器的房间 + +Robrix 里: + +1. 点左侧导航栏的 **+** 按钮打开 **Add/Explore Rooms and Spaces** 页面 +2. 在最底下的 **Join an existing room or space** 区域,输入目标房间别名(`#general:matrix.org`)、ID(`!...:matrix.org`)或 `matrix:` 链接,点 **Go** +3. 你的服务器通过联邦联系 `matrix.org` 并加入 +4. 来自所有参与服务器的消息实时同步 + +### 9.2 邀请其他服务器的用户 + +1. 打开你的房间,通过房间里的 invite 入口(右键房间或打开房间 info 面板,具体位置取决于 Robrix 版本) +2. 输入远程用户 MXID:`@friend:other-server.com` +3. 邀请通过联邦送到远程服务器 +4. 对方接受后加入你的房间 + +### 9.3 跨联邦 AI 机器人 + +联邦启用后,**其他服务器**的用户也能和你的 Octos 机器人交互: + +1. `matrix.org` 上的用户邀请 `@octosbot:matrix.example.com` 到他们的房间 +2. 邀请通过联邦送达你的服务器 +3. Octos 接受邀请并加入房间 +4. 机器人响应消息 -- 即使房间在远程服务器 + +> **注意:** 要让这个功能工作,`botfather.json` 里的 `allowed_senders` 必须是空数组 `[]`(允许所有人),或显式包含远程用户的 MXID(如 `@remoteuser:matrix.org`)。 + +--- + +## 10. 故障排查 + +### 10.1 常见问题 + +| 症状 | 原因 | 解决方法 | +|------|------|---------| +| 无法加入其他服务器房间 | 联邦未启用或端口被墙 | 检查 `[federation] enable = true`;确保防火墙开 443 和 8448 | +| "Unable to find signing key" | TLS 或 DNS 问题 | 证书必须是受信任 CA 签的(不能自签);查 DNS 解析 | +| well-known 返回 404 | 反向代理没转发 | 检查 Caddy/Nginx 配置里 `/.well-known/matrix/*` 的 handle | +| 远程用户看不到本地用户 profile | profile lookup 被禁 | 设 `allow_inbound_profile_lookup = true` | +| 连接远程服务器超时 | 出站防火墙或 DNS 问题 | 试 `query_over_tcp_only = true`;验证能否访问 8448 | +| 机器人不回复联邦用户 | `allowed_senders` 过滤 | 设为 `[]` 或加远程用户 MXID | +| Federation Tester 报 TLS 错 | 证书链不完整或过期 | 检查 fullchain.pem 包含中间证书;查证书有效期 | + +### 10.2 调试命令 + +```bash +# Palpo 联邦日志 +docker compose logs palpo | grep -i federation + +# Palpo 能否访问其他服务器 +docker compose exec palpo curl -sf https://matrix.org/.well-known/matrix/server + +# 从外部验证 well-known +curl -sf https://matrix.example.com/.well-known/matrix/server +curl -sf https://matrix.example.com/.well-known/matrix/client + +# 证书有效期检查 +openssl s_client -connect matrix.example.com:443 \ + -servername matrix.example.com < /dev/null 2>/dev/null \ + | openssl x509 -noout -dates + +# 测试 Server-Server API 版本 +curl -sf https://matrix.example.com:8448/_matrix/federation/v1/version +``` + +### 10.3 安全自检 + +生产环境启动前的清单: + +- [ ] `allow_registration = false`(或设置注册 token) +- [ ] `yes_i_am_very_very_sure...` 已移除或设为 false +- [ ] 数据库密码不是默认值 +- [ ] `.env` 文件不在 git 仓库里(加到 `.gitignore`) +- [ ] `allow_invalid_tls_certificates` 未设置或为 false +- [ ] TLS 证书来自受信 CA(非自签) +- [ ] Caddy/Nginx 的 HTTPS 重定向已启用 +- [ ] 防火墙只开 443 / 8448(不暴露 8008) +- [ ] 日志格式为 `json`,方便收集和告警 +- [ ] 数据卷配置了定期备份(尤其是 Postgres) + +--- + +## 11. 延伸阅读 + +- **Matrix 联邦规范:** [spec.matrix.org/latest/server-server-api](https://spec.matrix.org/latest/server-server-api/) -- 服务器间通信协议规范 +- **Matrix Federation Tester:** [federationtester.matrix.org](https://federationtester.matrix.org/) -- 联邦配置在线验证 +- **Palpo GitHub:** [github.com/palpo-im/palpo](https://github.com/palpo-im/palpo) -- Palpo 服务器源码 +- **Let's Encrypt:** [letsencrypt.org](https://letsencrypt.org/) -- 免费自动化 TLS 证书 +- **Caddy:** [caddyserver.com](https://caddyserver.com/) -- 内置自动 HTTPS 的反向代理 +- **Certbot:** [certbot.eff.org](https://certbot.eff.org/) -- Nginx + Let's Encrypt 工具 + +--- + +*本指南覆盖 2026 年 4 月的生产部署。具体配置项可能随上游更新变化,以各项目仓库为准。* diff --git a/docs/robrix-with-palpo-and-octos/05-federation-production-deployment.md b/docs/robrix-with-palpo-and-octos/05-federation-production-deployment.md new file mode 100644 index 000000000..5c95b7c90 --- /dev/null +++ b/docs/robrix-with-palpo-and-octos/05-federation-production-deployment.md @@ -0,0 +1,576 @@ +# Federation (Production Deployment) + +[中文版](05-federation-production-deployment-zh.md) + +> ### ⚠️ Advanced Content / Not Required to Run Locally +> +> This document is **advanced material**. If you just want to run a working federation environment on your local machine (two Palpo nodes + one Octos bot), see [04-federation-with-palpo.md](04-federation-with-palpo.md) -- that document is **fully self-contained** and does not depend on anything here. +> +> The purpose of this document is to tell you, when you deploy to a **real server** for outside-world use, which bits must change from "local testing mode" to "production mode" and why. + +> **Goal:** After following this guide, your Palpo server will be deployed on a real domain with Let's Encrypt TLS and a reverse proxy, capable of federating with public Matrix servers like `matrix.org`. + +This document covers **production** Matrix federation deployment -- real domain, trusted TLS certificates, reverse proxy, DNS setup, and security hardening. + +--- + +## 🔀 Local Testing vs. Production: Full Diff Table + +This table is the **core value** of this document -- for every piece of the working local environment in Doc 04, it spells out what has to change in production. + +| Aspect | Local testing (Doc 04) | Production (this doc) | Why | +|--------|------------------------|------------------------|-----| +| **Domain / hostname** | | | | +| `server_name` | `palpo-1:8448` / `palpo-2:8448` | `matrix.example.com` | Public servers must be DNS-resolvable | +| How servers find each other | Docker network DNS aliases | Real DNS A record + (optional) SRV record | Docker aliases only work inside containers | +| **TLS certificates** | | | | +| Cert type | Self-signed via `openssl req -x509` | Trusted CA (Let's Encrypt, etc.) | Remote servers reject self-signed certs | +| Cert validation | `allow_invalid_tls_certificates = true` | **Not set** or explicitly `false` | Production requires real validation | +| Cert management | Manual generation (one-time) | Caddy automatic / certbot periodic renewal | Let's Encrypt certs expire after 90 days | +| **Network ports** | | | | +| C-S API outward | `localhost:6001` / `localhost:6002` | `https://matrix.example.com` (443) | Public needs standard ports + HTTPS | +| Federation API outward | `localhost:6401` / `localhost:6402` | `matrix.example.com:8448` | Other servers reach via 8448 or 443 + well-known | +| Reverse proxy | Not used | Caddy / Nginx (strongly recommended) | Centralizes TLS, well-known, rate limits | +| TLS termination | Palpo itself (`[[listeners]] [listeners.tls]`) | Caddy / Nginx terminates; Palpo runs plain HTTP 8008 | Reverse proxy unifies cert management | +| **well-known configuration** | | | | +| `[well_known].server` | `localhost:6401` (convenient for host-side debugging) | `matrix.example.com:443` | Production advertises real endpoints | +| `[well_known].client` | `http://localhost:6001` | `https://matrix.example.com` | Production enforces HTTPS | +| well-known server | Served by Palpo built-in | Served directly by Caddy (or from Palpo backend) | Caddy is more flexible, independent of Palpo restarts | +| **Security-related** | | | | +| `allow_registration` | `true` (convenient for testing) | `false` (create accounts first, then lock) | Prevents account spam | +| `yes_i_am_very_very_sure...` | `true` | Remove or `false` | Production should never use unconditional registration | +| Database password | `palpo:palpo` (fixed weak password) | Strong random in `.env` | Prevents DB compromise on exposure | +| API keys (e.g., DeepSeek) | Written directly in `compose.yml` / `config.json` | `.env` environment variables, with `.gitignore` | Avoids accidental git check-in | +| Firewall | Doesn't matter (local) | Only open 443 / 8448 | Internal ports should not be public | +| **Logging and operations** | | | | +| Log format | `pretty` (human-readable) | `json` (machine-collectable) | Structured logs enable alerting | +| `RUST_LOG` | `debug` | `info` or `warn` | Lower I/O overhead in production | +| Data persistence | Docker volumes sufficient | Regular Postgres + media backups | Production data loss is catastrophic | +| **Federation access control** | | | | +| `[federation].enable` | `true` | `true` | Same | +| `[federation].allowed_servers` | Not set (wide open) | Optional allowlist for federation partners | Internal servers may only federate with specific peers | +| `[federation].denied_servers` | Not set | Optional blocklist for malicious servers | Used for spam/ban control | +| `trusted_servers` | Not set | `["matrix.org"]` | Production needs notary servers to help validate remote keys | +| **Bot (Octos) configuration** | | | | +| `botfather.json` `server_name` | `palpo-1:8448` | `matrix.example.com` | Production uses real domain | +| `botfather.json` `homeserver` | `http://palpo-1:8008` | `http://palpo:8008` (still Docker-internal name) | Bot connects to Palpo through Docker net, not public internet | +| AppService namespace regex | `@bot:palpo-1:8448` | `@octosbot:matrix\\.example\\.com` | Match real MXID format | +| `allowed_senders` | `[]` (wide open) | `[]` or explicit allowlist | Production may restrict who can use the bot | + +> **Important principle:** In the table above, **only `server_name` and a few security-related fields MUST change.** Settings like `homeserver` pointing to a Docker-internal name (`http://palpo:8008`) stay the **same** in both local and production -- because Octos always connects to Palpo via the Docker network, never through the public internet. + +--- + +## 📚 Scope of This Document + +| Scenario | This document | Other documents | +|----------|---------------|-----------------| +| **Production federation** | ✅ This document | -- | +| **Local federation testing** (Docker DNS, self-signed certs) | ❌ | [04-federation-with-palpo.md](04-federation-with-palpo.md) | +| Single-node local deployment | ❌ | [01-deploying-palpo-and-octos.md](01-deploying-palpo-and-octos.md) | + +> **Prerequisite:** It is recommended to finish the local dual-node federation test in [Document 04](04-federation-with-palpo.md) first. That way you will already understand concepts like `server_name`, `well-known`, and federation ports before deploying to production. This document assumes you have a server with administrator access and the ability to manage DNS for a real domain. + +--- + +## Table of Contents + +1. [Prerequisites for Production](#1-prerequisites-for-production) +2. [Overall Architecture](#2-overall-architecture) +3. [Domain and DNS Setup](#3-domain-and-dns-setup) +4. [Reverse Proxy (Caddy Example)](#4-reverse-proxy-caddy-example) +5. [Production `palpo.toml`](#5-production-palpotoml) +6. [Docker Compose Changes](#6-docker-compose-changes) +7. [AppService Registration Update](#7-appservice-registration-update) +8. [Launch and Verification](#8-launch-and-verification) +9. [Using Federation](#9-using-federation) +10. [Troubleshooting](#10-troubleshooting) +11. [Further Reading](#11-further-reading) + +--- + +## 1. Prerequisites for Production + +Production federation has stricter infrastructure requirements than local testing: + +| Requirement | Local testing (Doc 04) | Production (this doc) | +|-------------|------------------------|------------------------| +| Domain name | Not needed (Docker DNS alias) | **Required** (e.g., `matrix.example.com`) | +| TLS certificate | Self-signed (`allow_invalid_tls_certificates = true`) | **Required** (trusted CA like Let's Encrypt) | +| Port 443 | Not needed | **Open** (Client-Server API) | +| Port 8448 | Docker-internal only | **Open** (Server-Server Federation API) | +| Reverse proxy | Not needed | **Recommended** (Caddy / Nginx) | +| DNS records | Not needed | A record required, SRV record optional | +| Public IP | Not needed | **Required** | + +> **⚠️ Self-signed certificates will NOT work in production federation.** Other Matrix servers will reject the TLS connection and federation messages will fail to deliver. Use a trusted CA like [Let's Encrypt](https://letsencrypt.org/). + +--- + +## 2. Overall Architecture + +A typical production topology: + +``` + Internet + │ + │ 443 / 8448 + ▼ + ┌───────────────┐ + │ Caddy │ ← Reverse proxy + auto TLS + │ (host) │ + └───────┬───────┘ + │ localhost:8008 + ▼ +┌─────────── Docker network ─────────┐ +│ ┌──────────────┐ │ +│ │ Palpo │ │ +│ │ server_name: │ │ +│ │ matrix. │ │ +│ │ example.com │ │ +│ └──────┬───────┘ │ +│ │ │ +│ ▼ AppService │ +│ ┌──────────────┐ ┌─────────┐ │ +│ │ Octos │ │ Postgres│ │ +│ └──────────────┘ └─────────┘ │ +└─────────────────────────────────────┘ +``` + +**Key design choices:** + +1. Caddy listens on the public 443 port, handling TLS termination and automatic Let's Encrypt certificate renewal +2. Palpo only exposes plain HTTP 8008 internally and is proxied by Caddy +3. Clients (Robrix/Element) connect to `matrix.example.com` over HTTPS +4. Other federated servers connect to `matrix.example.com:8448` (or 443 + well-known delegation) + +--- + +## 3. Domain and DNS Setup + +### 3.1 Register a Domain + +Register a domain (e.g., `example.com`) and allocate a subdomain for Matrix, such as `matrix.example.com`. + +### 3.2 Create a DNS A Record + +Point the subdomain to your server's public IP: + +``` +matrix.example.com. IN A 203.0.113.10 +``` + +### 3.3 (Optional) Create an SRV Record + +You need an SRV record if federation runs on a non-default port, or if you want federation for `example.com` to route to `matrix.example.com`: + +``` +_matrix-fed._tcp.example.com. IN SRV 10 0 8448 matrix.example.com. +``` + +> **When can I skip SRV?** If you serve `/.well-known/matrix/server` on `matrix.example.com:443`, Matrix clients discover the federation endpoint through well-known delegation without SRV. Most production deployments use this approach. + +### 3.4 Production DNS Settings (palpo.toml) + +When running inside a Docker network, use TCP for DNS resolution: + +```toml +query_over_tcp_only = true # UDP DNS in container networks can be unreliable +query_all_nameservers = true # Query all configured DNS servers to avoid single point of failure +ip_lookup_strategy = 5 # 5 = try IPv4 first, then IPv6 +``` + +--- + +## 4. Reverse Proxy (Caddy Example) + +A reverse proxy is **strongly recommended** in production because: + +1. **Automatic TLS** -- Caddy has built-in Let's Encrypt, handling certificate issuance and renewal automatically +2. **well-known endpoint management** -- Caddy responds directly, without depending on Palpo +3. **Traffic control** -- rate limiting, logging, WAF integration all hang off nicely + +### 4.1 Caddyfile + +```caddyfile +matrix.example.com { + # Matrix client discovery endpoint + handle /.well-known/matrix/client { + header Access-Control-Allow-Origin "*" + respond `{"m.homeserver":{"base_url":"https://matrix.example.com"}}` + } + + # Matrix federation discovery endpoint + handle /.well-known/matrix/server { + respond `{"m.server":"matrix.example.com:443"}` + } + + # Everything else: proxy to Palpo + reverse_proxy localhost:8008 +} + +# If federation uses a non-443 port, add another block: +# matrix.example.com:8448 { +# reverse_proxy localhost:8008 +# } +``` + +### 4.2 Disable Palpo's Own TLS + +Let Caddy exclusively handle TLS; Palpo runs plain HTTP internally: + +```toml +# palpo.toml +[tls] +enable = false +``` + +### 4.3 Nginx Alternative + +If you must use Nginx, manage certificates separately with `certbot`: + +```bash +# Request certificate +sudo certbot --nginx -d matrix.example.com + +# Cert paths +# /etc/letsencrypt/live/matrix.example.com/fullchain.pem +# /etc/letsencrypt/live/matrix.example.com/privkey.pem +``` + +Then write `ssl_certificate` / `ssl_certificate_key` in your Nginx config and proxy `/` to `http://127.0.0.1:8008`, with explicit `location` blocks for `/.well-known/matrix/server` and `/.well-known/matrix/client`. + +--- + +## 5. Production `palpo.toml` + +```toml +# ── Core configuration ───────────────────────── +# CHANGED: real domain +server_name = "matrix.example.com" + +# CHANGED: disable open registration in production +# Create admin accounts first, then set to false +allow_registration = false + +enable_admin_room = true +appservice_registration_dir = "/var/palpo/appservices" + +# Do NOT enable self-signed cert bypass in production! +# allow_invalid_tls_certificates stays at default (false) + +# ── Listeners ────────────────────────────────── +# Caddy proxies port 443 to here +[[listeners]] +address = "0.0.0.0:8008" + +# ── Logging ──────────────────────────────────── +[logger] +format = "json" # CHANGED: JSON logs for production (easier to collect) +level = "info" + +# ── Database ─────────────────────────────────── +[db] +url = "postgres://palpo:@palpo_postgres:5432/palpo" +pool_size = 10 + +# ── well-known (Palpo serves it; can be omitted if Caddy handles it) ─ +[well_known] +server = "matrix.example.com:443" +client = "https://matrix.example.com" + +# ── Federation settings ──────────────────────── +[federation] +enable = true +allow_inbound_profile_lookup = true # Allow remote servers to query local user profiles +# Optional access control: +# allowed_servers = ["matrix.org", "*.trusted.com"] +# denied_servers = ["evil.com"] + +# ── TLS (recommended to disable, let Caddy handle it) ─ +[tls] +enable = false +# If not using a reverse proxy: +# enable = true +# cert = "/path/to/fullchain.pem" +# key = "/path/to/privkey.pem" +# dual_protocol = false + +# ── Presence and typing (cross-federation indicators) ─ +[presence] +allow_local = true +allow_incoming = true +allow_outgoing = true + +[typing] +allow_incoming = true +allow_outgoing = true +federation_timeout = 30000 + +# ── Trusted servers (Perspectives key notaries) ─ +trusted_servers = ["matrix.org"] + +# ── DNS tuning (recommended inside containers) ─ +query_over_tcp_only = true +query_all_nameservers = true +ip_lookup_strategy = 5 +``` + +### 5.1 `[federation]` Field Reference + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `enable` | bool | `true` | Master switch for federation | +| `allow_loopback` | bool | `false` | Allow federation requests to self (dev only) | +| `allow_device_name` | bool | `false` | Expose device names via federation; disable for privacy | +| `allow_inbound_profile_lookup` | bool | `true` | Allow remote servers to query local user profiles | +| `allowed_servers` | list | none | Allowlist, supports wildcards like `*.example.com`; not set = allow all | +| `denied_servers` | list | `[]` | Blocklist, **takes precedence over** `allowed_servers` | + +### 5.2 Trusted Servers (Perspectives Key Validation) + +`trusted_servers` act as **notary servers** that help validate other servers' signing keys. This is the [Perspectives key validation](https://spec.matrix.org/latest/server-server-api/#querying-keys-through-another-server) mechanism. + +```toml +trusted_servers = ["matrix.org"] +``` + +The most common choice is `matrix.org` since it is the central hub of the public federation. + +--- + +## 6. Docker Compose Changes + +> **Baseline note:** This section compares against the **single-node** deployment (`palpo-and-octos-deploy/compose.yml`, which uses `server_name = "127.0.0.1:8128"`), not the dual-node federation from Doc 04. Production topology is almost always a single homeserver with outward federation — structurally closer to single-node than to Doc 04's local-simulation topology. If you started from Doc 04, ignore the port/service-name specifics and focus on the **rightmost column** — production values apply identically. + +Key differences versus the local `compose.yml`: + +```yaml +services: + palpo: + # image / build section same as local + ports: + - "8008:8008" # Caddy proxies here (no more 8128 exposure) + volumes: + - ./palpo.toml:/var/palpo/palpo.toml:ro + - ./appservices:/var/palpo/appservices:ro + - ./data/media:/var/palpo/media + # If Palpo handles TLS directly (not recommended), mount certs: + # - /etc/letsencrypt/live/matrix.example.com:/certs:ro + restart: unless-stopped + # ... rest same as local ... + + palpo_postgres: + environment: + POSTGRES_PASSWORD: ${DB_PASSWORD} # CHANGED: read strong password from .env + POSTGRES_USER: palpo + POSTGRES_DB: palpo + # ... + + octos: + environment: + DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY} + RUST_LOG: octos=info # CHANGED: info rather than debug + # ... +``` + +Overall structure stays the same as the local deployment -- Postgres, Palpo, Octos. Key differences: + +| Local | Production | +|-------|------------| +| `server_name = "127.0.0.1:8128"` | `server_name = "matrix.example.com"` | +| Port `8128:8008` | Port `8008:8008` (proxied by Caddy) | +| Palpo self-manages TLS (or none) | Caddy terminates TLS | +| `allow_registration = true` | `allow_registration = false` | +| `pretty` logs | `json` logs | +| API keys inline in yml | Read from `.env` | + +--- + +## 7. AppService Registration Update + +When switching from `127.0.0.1:8128` to a real domain, update the following files. + +### 7.1 AppService namespace file + +Different local setups use different filenames: + +| Starting point | Path | +|---------------|------| +| Single-node (Doc 01) | `palpo-and-octos-deploy/appservices/octos-registration.yaml` | +| Dual-node federation (Doc 04) | `palpo-and-octos-deploy/federation/nodes/node1/appservices/octos.yaml` | + +Whichever you use, update the namespace regex to your real domain: + +```yaml +namespaces: + users: + - exclusive: true + regex: "@octosbot_.*:matrix\\.example\\.com" # CHANGED: real domain + - exclusive: true + regex: "@octosbot:matrix\\.example\\.com" # CHANGED: real domain +``` + +### 7.2 `config/botfather.json` + +```json +{ + "config": { + "channels": [{ + "type": "matrix", + "homeserver": "http://palpo:8008", + "server_name": "matrix.example.com", + "sender_localpart": "octosbot", + ... + }] + } +} +``` + +> **Important:** The `homeserver` URL **stays** as the Docker-internal address (`http://palpo:8008`) because Octos connects to Palpo through Docker networking -- it doesn't need to go through the public internet. Only `server_name` changes to the real domain, since that is the outward Matrix identity. + +--- + +## 8. Launch and Verification + +### 8.1 Starting the Services + +```bash +cd palpo-and-octos-deploy # or your production deploy directory + +# Set environment variables +cp .env.example .env +vim .env # fill in DEEPSEEK_API_KEY, DB_PASSWORD, etc. + +# Start +docker compose up -d + +# Check status +docker compose ps +docker compose logs -f +``` + +### 8.2 Test well-known Endpoints + +```bash +# Server discovery (used by other federated servers) +curl https://matrix.example.com/.well-known/matrix/server +# Expected: {"m.server":"matrix.example.com:443"} + +# Client discovery (used by Robrix/Element) +curl https://matrix.example.com/.well-known/matrix/client +# Expected: {"m.homeserver":{"base_url":"https://matrix.example.com"}} +``` + +### 8.3 Matrix Federation Tester + +Visit [https://federationtester.matrix.org](https://federationtester.matrix.org) and enter your domain (`matrix.example.com`). It checks: + +- DNS resolution correctness +- TLS certificate trust chain +- well-known endpoint responses +- Server-Server API reachability +- Signing key validation + +All checks must pass before federation is fully operational. + +--- + +## 9. Using Federation + +Once federation is up, you can: + +### 9.1 Join Rooms on Other Servers + +In Robrix: + +1. Click the **+** button in the left nav bar to open **Add/Explore Rooms and Spaces** +2. In the bottom **Join an existing room or space** section, enter a target room alias (`#general:matrix.org`), ID (`!...:matrix.org`), or a `matrix:` link, then click **Go** +3. Your server reaches `matrix.org` through federation and joins +4. Messages from all participating servers sync in real time + +### 9.2 Invite Users from Other Servers + +1. Open one of your rooms and use its invite action (right-click the room or open its info panel, depending on the Robrix version) +2. Enter the remote user's MXID: `@friend:other-server.com` +3. The invitation is sent via federation to the remote server +4. After they accept, they join your room + +### 9.3 Cross-Federation AI Bot + +With federation enabled, users from **other servers** can also interact with your Octos bot: + +1. A user on `matrix.org` invites `@octosbot:matrix.example.com` to their room +2. The invitation is delivered to your server via federation +3. Octos accepts and joins the room +4. The bot responds to messages -- even though the room lives on a remote server + +> **Note:** For this to work, `allowed_senders` in `botfather.json` must be an empty array `[]` (allow all users) or explicitly include the remote user's MXID (e.g., `@remoteuser:matrix.org`). + +--- + +## 10. Troubleshooting + +### 10.1 Common Issues + +| Symptom | Cause | Fix | +|---------|-------|-----| +| Can't join rooms on other servers | Federation disabled or port blocked | Check `[federation] enable = true`; open firewall ports 443 and 8448 | +| "Unable to find signing key" | TLS or DNS issue | Certs must be from a trusted CA (not self-signed); verify DNS | +| well-known returns 404 | Reverse proxy not forwarding | Check Caddy/Nginx handles `/.well-known/matrix/*` | +| Remote users can't see local user profiles | profile lookup disabled | Set `allow_inbound_profile_lookup = true` | +| Timeout connecting to remote server | Outbound firewall or DNS problem | Try `query_over_tcp_only = true`; verify reachability on 8448 | +| Bot doesn't reply to federated users | `allowed_senders` filter | Set to `[]` or add remote MXID explicitly | +| Federation Tester reports TLS error | Incomplete cert chain or expired | Ensure `fullchain.pem` includes intermediate certs; check expiry | + +### 10.2 Debug Commands + +```bash +# Palpo federation logs +docker compose logs palpo | grep -i federation + +# Can Palpo reach other servers? +docker compose exec palpo curl -sf https://matrix.org/.well-known/matrix/server + +# Verify well-known externally +curl -sf https://matrix.example.com/.well-known/matrix/server +curl -sf https://matrix.example.com/.well-known/matrix/client + +# Certificate expiry check +openssl s_client -connect matrix.example.com:443 \ + -servername matrix.example.com < /dev/null 2>/dev/null \ + | openssl x509 -noout -dates + +# Test Server-Server API version +curl -sf https://matrix.example.com:8448/_matrix/federation/v1/version +``` + +### 10.3 Security Checklist + +Before going live in production: + +- [ ] `allow_registration = false` (or registration token configured) +- [ ] `yes_i_am_very_very_sure...` removed or set to false +- [ ] Database password is not the default value +- [ ] `.env` file is in `.gitignore` (not committed) +- [ ] `allow_invalid_tls_certificates` is unset or false +- [ ] TLS certificate is from a trusted CA (not self-signed) +- [ ] Caddy/Nginx enforces HTTPS redirect +- [ ] Firewall only opens 443 / 8448 (not 8008) +- [ ] Logs are in `json` format (for collection and alerts) +- [ ] Data volumes have a regular backup policy (especially Postgres) + +--- + +## 11. Further Reading + +- **Matrix Federation Spec:** [spec.matrix.org/latest/server-server-api](https://spec.matrix.org/latest/server-server-api/) -- Server-Server protocol specification +- **Matrix Federation Tester:** [federationtester.matrix.org](https://federationtester.matrix.org/) -- Online federation validator +- **Palpo GitHub:** [github.com/palpo-im/palpo](https://github.com/palpo-im/palpo) -- Palpo server source +- **Let's Encrypt:** [letsencrypt.org](https://letsencrypt.org/) -- Free, automated TLS certificates +- **Caddy:** [caddyserver.com](https://caddyserver.com/) -- Reverse proxy with built-in auto-HTTPS +- **Certbot:** [certbot.eff.org](https://certbot.eff.org/) -- Nginx + Let's Encrypt tool + +--- + +*This guide covers production deployment as of April 2026. Specific configuration fields may change with upstream updates; refer to each project's repository for the latest details.* diff --git a/docs/robrix/getting-started-with-robrix-zh.md b/docs/robrix/getting-started-with-robrix-zh.md new file mode 100644 index 000000000..4035c6015 --- /dev/null +++ b/docs/robrix/getting-started-with-robrix-zh.md @@ -0,0 +1,72 @@ +# Robrix 快速开始 + +[English Version](getting-started-with-robrix.md) + +> **目标:** 按照本指南操作后,你将完成 Robrix 的安装和运行,连接到 Matrix 服务器,并可以开始聊天。 + +Robrix 是一个用 Rust 编写的跨平台 Matrix 聊天客户端,基于 [Makepad](https://github.com/makepad/makepad/) UI 框架。原生运行在 macOS、Linux、Windows、Android 和 iOS 上。 + +## 下载预编译版本(推荐) + +从 [Robrix 发布页面](https://github.com/Project-Robius-China/robrix2/releases) 下载最新版本。支持 macOS、Linux 和 Windows。 + +## 从源码构建 + +### 前提条件 + +- [Rust](https://www.rust-lang.org/tools/install)(最新稳定版) +- Linux 上需要安装系统依赖: + ```bash + sudo apt-get install libssl-dev libsqlite3-dev pkg-config libxcursor-dev libx11-dev libasound2-dev libpulse-dev libwayland-dev libxkbcommon-dev + ``` + +### 桌面端(macOS / Linux / Windows) + +```bash +git clone https://github.com/Project-Robius-China/robrix2.git +cd robrix2 +cargo run --release +``` + +### 移动端 + +Android 和 iOS 构建方法请参考 [Robrix README — 构建与运行](https://github.com/Project-Robius-China/robrix2#building--running-robrix-on-desktop)。 + +--- + +## 连接 Matrix 服务器 + +启动 Robrix 后,登录界面底部有一个 **Homeserver URL** 输入框。 + +Robrix 登录界面 + +- **留空** 默认连接 `matrix.org`(公共服务器) +- **输入自定义 URL** 连接其他 Matrix 兼容服务器: + - 本地 Palpo 实例:`http://127.0.0.1:8128` + - 远程服务器:`https://your.server.name` + +> **注意:** Robrix 要求主服务器支持 [Sliding Sync](https://spec.matrix.org/latest/client-server-api/#sliding-sync)。Palpo 原生支持此功能;其他服务器请查阅其文档。 + +## 注册或登录 + +**新账号(服务器允许注册时):** + +1. 输入**用户名**和**密码** +2. 确认密码 +3. 设置 **Homeserver URL** +4. 点击 **Sign up** + +**已有账号:** + +1. 输入**用户名**和**密码** +2. 设置 **Homeserver URL** +3. 点击 **Log in** + +登录后你会看到房间列表。你可以加入房间、创建新房间并开始聊天。 + +--- + +## 下一步? + +- **只是聊天?** 你已经准备好了——加入房间,和 Matrix 网络上的人交流。 +- **想要 AI 机器人?** 查看 [Robrix + Palpo + Octos 部署指南](../robrix-with-palpo-and-octos/01-deploying-palpo-and-octos-zh.md),搭建你自己的 AI 聊天系统。 diff --git a/docs/robrix/getting-started-with-robrix.md b/docs/robrix/getting-started-with-robrix.md new file mode 100644 index 000000000..745fb29e6 --- /dev/null +++ b/docs/robrix/getting-started-with-robrix.md @@ -0,0 +1,74 @@ +# Getting Started with Robrix + +[中文版](getting-started-with-robrix-zh.md) + +> **Goal:** After following this guide, you will have Robrix installed and running, connected to a Matrix server, and ready to chat. + +Robrix is a cross-platform Matrix chat client written in Rust using the [Makepad](https://github.com/makepad/makepad/) UI framework. It runs natively on macOS, Linux, Windows, Android, and iOS. + +--- + +## Download a Pre-built Release (Recommended) + +Download the latest version from the [Robrix Releases page](https://github.com/Project-Robius-China/robrix2/releases). Available for macOS, Linux, and Windows. + +## Build from Source + +### Prerequisites + +- [Rust](https://www.rust-lang.org/tools/install) (latest stable) +- On Linux, install system dependencies: + ```bash + sudo apt-get install libssl-dev libsqlite3-dev pkg-config libxcursor-dev libx11-dev libasound2-dev libpulse-dev libwayland-dev libxkbcommon-dev + ``` + +### Desktop (macOS / Linux / Windows) + +```bash +git clone https://github.com/Project-Robius-China/robrix2.git +cd robrix2 +cargo run --release +``` + +### Mobile + +For Android and iOS builds, see the [Robrix README — Building & Running](https://github.com/Project-Robius-China/robrix2#building--running-robrix-on-desktop). + +--- + +## Connect to a Matrix Server + +When you launch Robrix, you'll see the login screen with a **Homeserver URL** field at the bottom. + +Robrix login screen + +- **Leave it empty** to connect to `matrix.org` (the default public server) +- **Enter a custom URL** to connect to any Matrix-compatible server: + - Local Palpo instance: `http://127.0.0.1:8128` + - Remote server: `https://your.server.name` + +> **Note:** Robrix requires the homeserver to support [Sliding Sync](https://spec.matrix.org/latest/client-server-api/#sliding-sync). Palpo supports this natively; for other servers, check their documentation. + +## Register or Log In + +**New account (if the server allows registration):** + +1. Enter a **username** and **password** +2. Confirm the password +3. Set the **Homeserver URL** +4. Click **Sign up** + +**Existing account:** + +1. Enter your **username** and **password** +2. Set the **Homeserver URL** +3. Click **Log in** + +After login, you'll see your room list. From here you can join rooms, create new rooms, and start chatting. + +--- + +## What's Next? + +- **Just chatting?** You're all set — join rooms and talk to people on the Matrix network. +- **Want AI bots?** See the [Robrix + Palpo + Octos deployment guide](../robrix-with-palpo-and-octos/01-deploying-palpo-and-octos.md) to set up your own AI chat system. diff --git a/docs/superpowers/plans/2026-04-11-tg-bot-architecture-review.md b/docs/superpowers/plans/2026-04-11-tg-bot-architecture-review.md new file mode 100644 index 000000000..b16acc9ad --- /dev/null +++ b/docs/superpowers/plans/2026-04-11-tg-bot-architecture-review.md @@ -0,0 +1,208 @@ +# Codex TG Bot 架构方案审查 + +> **⚠️ SUPERSEDED (2026-04-13).** This architecture review shaped the Phase 2 +> explicit-target-chip design. The product direction has since moved to +> **mention/reply-first** (Phase 3) — see +> [`docs/superpowers/plans/2026-04-12-tg-bot-mention-reply-first-plan.md`](./2026-04-12-tg-bot-mention-reply-first-plan.md) +> and [`specs/task-tg-bot-mention-reply-first.spec.md`](../../../specs/task-tg-bot-mention-reply-first.spec.md). +> The type model (`ExplicitOverride`/`ResolvedTarget`), `is_known_or_likely_bot()`, +> and the reply-to-human fix survived the pivot; the persistent target chip +> UI and its switching menu did not. Kept for historical context. + +## Context + +Codex 提出将 Robrix2 的 bot 交互模型从"implicit room-bound bot routing"转向"explicit bot targeting",对标 Telegram。本文档审查该方案的技术可行性、架构合理性,以及 Codex 未覆盖的设计问题。 + +**架构原则:** Telegram-style bot UX on top of Matrix semantics。Robrix2 是 Matrix IM 客户端,不是 Telegram 客户端。底层保留 Matrix 的 room/user/message 模型,只在客户端 UX 层补上 Telegram 风格的显式、低摩擦 bot 交互体验。不应把 Telegram 的协议级 bot 概念(原生命令列表、BotFather、menu button)直接映射为 Robrix 的产品默认。 + +**修正说明:** Bot ID 不一致(`"bot"` vs `"octosbot"`)确实存在。实际部署配置 `palpo-and-octos-deploy/config/botfather.json:16` 用的是 `"octosbot"`,与 `src/app.rs:2069` 的默认值 `"bot"` 不匹配。Codex 在这点上是对的。但 `DEFAULT_BOTFATHER_LOCALPART` 是通用默认值,不应绑定到特定 appservice 实现。 + +--- + +## 一、核心论断审查 + +**Codex 论断:** Robrix2 是"implicit room-bound bot routing",Telegram 是"explicit bot targeting"。要对齐,应把 binding 降为内部实现,把显式 target 升为用户可见模型。 + +**审查结论:论断正确,方向正确。** + +代码证据: +- `resolve_target_user_id()` (`room_input_bar.rs:861-876`) — 三级优先级 explicit > reply > fallback,但两个 send path 都传 `None` 给 explicit,从未使用 +- `active_target_user_id` (`room_input_bar.rs:605`) — 状态字段存在但:(a) 从不被 UI 显示,(b) 发送后不清零,(c) 没有用户主动设置的入口 +- `bound_bot_user_id` 在 `RoomScreenProps` 中作为 fallback 默默路由消息 + +**一句话:路由管道已经预留了显式 target 的参数位,但状态模型、bot 判定、UI 层都还没接入——不止是 UI 接线。** + +--- + +## 二、P0/P1/P2 分层评估 + +### P0:基础修正 — 认可 + +| 项目 | 评估 | 备注 | +|------|------|------| +| bot/octosbot 配置对齐 | **应做但不改全局默认** | `botfather_user_id` 已是用户可配置项(`app.rs:2218`),应通过文档/preset 引导 Palpo+OctOS 用户配置,不应把 `DEFAULT_BOTFATHER_LOCALPART` 硬改为 `"octosbot"` | +| 改善绑定错误提示 | **应做** | 当前 "Bind BotFather..." 文案对用户不友好 | +| 改善自动检测 | **低优先** | `is_likely_bot_user_id()` 已有合理的启发式规则 | + +### P1:核心模型对齐 — 认可,但有设计缺口 + +**Codex 方案:** 输入框加 target chip(`To room` / `To @configured bot`) + +**技术可行性:非常高。** 代码库已有所有基础设施: + +| 已有基础 | 位置 | 复用方式 | +|----------|------|----------| +| 状态字段 `active_target_user_id` | `room_input_bar.rs:605` | 直接用 | +| 三级路由函数 `resolve_target_user_id()` | `room_input_bar.rs:861` | 已支持 explicit target 参数 | +| 上下文指示器 UI 模式 | `reply_preview.rs:77-123` `ReplyingPreview` | 完全照搬:Label + 取消按钮,位于输入框上方 | +| Bot 绑定状态 | `RoomScreenProps.bound_bot_user_id` | 决定默认 target | +| 状态持久化 | `RoomInputBarState` (`room_input_bar.rs:1596`) | 已保存 `active_target_user_id` | + +**Codex 未覆盖的 4 个设计问题:** + +1. **⚠️ Reply 不区分 bot 和人(Matrix 语义风险)** + - 当前 `reply_target_user_id` 直接取被回复消息的 sender(`room_input_bar.rs:1112-1115`),不检查是否为 bot + - 这个值传入 `resolve_target_user_id()` 后作为 `target_user_id`,最终在 `sliding_sync.rs:2726` 触发 `ensure_target_user_joined_room()` + - **问题:** 回复普通用户的消息,也会被当作 bot 定向处理。在 Matrix 语义下,reply 首先是原生回复关系,不应默认变成"把消息定向给被回复的人" + - **方案:** 必须在 resolver 中增加 bot 判定——只有 reply-to-bot 才参与 bot targeting,reply-to-human 只作为普通 Matrix reply + - **⚠️ Bot 判定不能只用 `is_likely_bot_user_id()`。** 该函数只覆盖 localpart 启发式和父 bot 精确匹配(`room_screen.rs:432`),不查 `known_bot_user_ids()`。代码中更完整的 bot 识别逻辑在 `detected_bot_binding_for_members()`(`room_screen.rs:360`),它先查 `resolved_bot_user_id()`,再查 `known_bot_user_ids()`,最后做启发式。但注意:该函数是"房间级绑定检测"(接受 `&[RoomMember]`),不是通用的"单个 user_id 是否是 bot"判定器 + - **正确方案:** 需要新建一个独立的 `is_known_or_likely_bot(user_id: &UserId, bot_settings: &BotSettingsState, current_user_id: Option<&UserId>) -> bool` 函数,合并 `known_bot_user_ids()` 精确查询、`resolved_bot_user_id(current_user_id)` 匹配和 `is_likely_bot_user_id()` 启发式。注意:`resolved_bot_user_id()` 需要 `current_user_id` 参数来将 localpart-only 的配置值解析为完整 MXID(`app.rs:2218`)。这是一个新函数,不是从 `detected_bot_binding_for_members()` 抽取——后者的职责是房间级绑定发现,不应被当作单 user_id 判定的前身 + - **⚠️ 依赖传递问题:** 该函数需要 `BotSettingsState` 和 `current_user_id`,但 `reply_target_user_id` 的决策点在 `room_input_bar` 的发送路径中(`room_input_bar.rs:1112`),而当前 `RoomScreenProps` 只带了 `bound_bot_user_id` 等少量 bot 上下文(`room_screen.rs:6480`)。P1 实现需要二选一: + - **方案 A:** 扩展 `RoomScreenProps`,把 `&BotSettingsState` 引用或 resolved parent bot user_id 传入 `room_input_bar`,让 bot 判定在发送路径中本地完成 + - **方案 B:** 将 reply-target 的 bot 判定上移到持有 `AppState` / `current_user_id` 的 `room_screen` 层,在传入 `room_input_bar` 之前就完成过滤 + - 推荐 **方案 A**(改动面更小,`RoomScreenProps` 已经是 bot 上下文的传递通道) + +2. **"To room" vs "无 target" 语义不同** + - 当前 `active_target_user_id = None` + `bound_bot_user_id = Some` → 消息发给 bot(fallback) + - 用户点击 "To room" 意味着**显式不发给 bot** + - 需要区分 "未设置 target"(用 fallback)和 "显式选择 room"(跳过 fallback) + - **方案:** 使用 `TargetSource` enum(见第 4 点),`ExplicitRoom` 表示用户主动选择发给房间,resolver 遇到此状态时跳过 fallback + +2. **Target 何时清零?** + - Codex 没说。当前 `active_target_user_id` 发送后不清零(sticky) + - Telegram 官方文档只明确了群里可以通过 reply 或 `/command@OtherBot` 与 bot 通信([Bot Features](https://core.telegram.org/bots/features#bot-to-bot-communication)),未规定"后续 target 是否自动清零" + - **这是产品决策,不是 Telegram parity 事实。** 推荐行为:reply target 发送后清零,显式 bot target 保持 sticky — 但这需要作为明确的设计选择记录,而非伪装成对标 Telegram 的既有行为 + +3. **首次进入 bot room 的默认状态 + target 来源模型** + - 绑定了 bot 的房间,首次进入时 target chip 应该显示什么? + - "To room" 但 fallback 实际发给 bot → **矛盾** + - "To @configured bot" 但用户没有主动选择 → **可能困惑** + - 当前 resolver 只区分"有某个 user_id"或"没有",丢失了 target 来源信息 + - **方案:** 拆分为"持久化的用户意图"和"运行时计算的 resolved target"两层: + + **持久化层(存入 `RoomInputBarState`):** + ``` + ExplicitOverride { + None, // 用户没有主动覆盖,使用房间默认行为 + Bot(bot_user_id), // 用户主动选择的 bot + Room, // 用户主动选择发给房间(跳过默认 bot) + } + ``` + + **运行时计算层(resolve 时根据上下文推导):** + ``` + ResolvedTarget { + NoTarget, // 普通 Matrix 房间,没有任何 bot target + RoomDefault(bot_user_id), // 来自 bound_bot_user_id + ExplicitOverride::None + ExplicitBot(bot_user_id), // 来自 ExplicitOverride::Bot + ExplicitRoom, // 来自 ExplicitOverride::Room + ReplyBot(bot_user_id), // 来自当前 replying_to + bot 判定 + } + ``` + + - **持久化什么:** 只持久化 `ExplicitOverride`(用户的显式意图)。`RoomDefault` 和 `ReplyBot` 是派生状态,在 resolve 时从 `bound_bot_user_id` 和 `replying_to` 实时计算。这避免了房间绑定变化、取消回复时的陈旧值问题 + - **运行时 resolve 逻辑:** + 1. 如果有 `replying_to` 且被回复者是 bot → `ReplyBot(bot_user_id)` + 2. 否则看 `ExplicitOverride`:`Bot(id)` → `ExplicitBot(id)`,`Room` → `ExplicitRoom` + 3. 否则(`ExplicitOverride::None`):如果有 `bound_bot_user_id` → `RoomDefault(bot)`,否则 → `NoTarget` + - **混合场景决策(ExplicitOverride::Bot + reply-to-human):** + - 用户已设置 `ExplicitOverride::Bot(octosbot)`,然后 reply 一个普通人的消息 + - reply-to-human 不触发 `ReplyBot`(步骤 1 的 bot 判定不通过),继续走步骤 2 + - resolve 结果:`ExplicitBot(octosbot)`,同时挂上对普通人消息的 Matrix reply 关系 + - **产品语义:** 消息定向发给 bot,同时在 Matrix 协议层是对那条人类消息的 reply。这是合理的——用户可能想让 bot 看到被引用的上下文 + - **UI 展示:** target chip 显示 "To @bot",reply preview 正常显示被引用的消息,两者独立 + - **UI 展示:** + - `NoTarget`:chip 不显示 + - `RoomDefault`:淡色 "Default: @bot" + - `ExplicitBot`:正常色 "To @bot" + - `ExplicitRoom`:chip 显示 "To room" + - `ReplyBot`:chip 显示 "Reply → @bot"(临时,取消 reply 即消失) + - **chip × 行为:** 清除 `ExplicitOverride` 回到 `None`,resolve 自动回退到 `RoomDefault`(有绑定 bot 时)或 `NoTarget`(无 bot 时) + - **产品决策:** 首次进入任何房间时 `ExplicitOverride` 为 `None`,resolve 根据是否有 `bound_bot_user_id` 决定显示 + +### P2:Telegram 化交互 — 方向对,优先级合理 + +| 项目 | 评估 | +|------|------| +| Menu button 替代 `/bot` | 可做,但 `/bot` 可保留给 power user | +| 命令分类(纯命令 send-on-select / 参数命令 insert) | 方向合理,但当前硬编码命令表(`mentionable_text_input.rs:188-195`)就是设计本身——spec 明确将"动态命令注册"列为 out of scope(`task-tg-bot-ui-alignment.spec.md:48`)。OctOS 的 slash 命令本质上也是"在聊天里输入的文本命令",不是客户端可发现的协议能力。不应在静态 `SlashCommand` 上堆字段固化,但也不应把"动态注册"默认为自然的下一步——那需要一个新的 Matrix-side 元数据/协议设计,属于独立的未来方向 | +| `/command@bot` 显式寻址 | 长期目标,需解析语法 + 多 bot room 支持 | + +--- + +## 三、架构判断 + +**Codex 的核心设计决策是正确的:** + +> "底层继续复用现在的 target_user_id 机制,不推翻现有发送链路" + +这是最合理的路径。`resolve_target_user_id()` 已经设计了三级优先级,需要: +1. 让 UI 能显示当前 resolved target +2. 让用户能主动设置 explicit target(当前两个 send path 都传 `None`) +3. 让用户能清除 target(切回 "To room") + +**⚠️ 不止是 UI 接线。** 当前 `resolve_target_user_id()` 的签名只接受 `Option`,能表达"某个 bot"或"没有显式 bot",但表达不了"显式发给 room、禁止 fallback"这个第三种状态(`room_input_bar.rs:861`)。需要两层模型(见第二节第 4 点): +- **持久化层:** 将 `active_target_user_id: Option` 改为 `ExplicitOverride { None, Bot(UserId), Room }`,只存用户显式意图 +- **运行时层:** `resolve_target()` 从 `ExplicitOverride` + `bound_bot_user_id` + `replying_to` 实时推导 `ResolvedTarget` +- 抽取 `is_known_or_likely_bot(user_id, bot_settings, current_user_id)`(见第二节第 1 点),供 resolve 时判断 reply target 是否为 bot + +--- + +## 四、Gap 总结 + +| Gap | 严重程度 | 需要决策 | +|-----|---------|---------| +| Reply 不区分 bot 和人 | **高** — 回复普通用户也触发 bot targeting | 需要统一 bot 判定函数(`known_bot_user_ids()` + `is_likely_bot_user_id()`),在 resolver 中检查 | +| "To room" vs "未设置 target" 的语义区分 | **高** — 不解决会导致 target chip 说谎 | 需要两层模型:持久化 `ExplicitOverride` + 运行时 `ResolvedTarget`,拆分用户意图和派生状态 | +| Target 清零时机 | **中** — 影响用户心智模型 | 这是产品决策,不是 TG parity 事实 | +| Target 跨导航持久化 | **已定义** | 持久化 `ExplicitOverride`(用户意图)。`ReplyBot` 不独立持久化,但因 `replying_to` 已持久化(`room_input_bar.rs:1595`)而间接恢复 | +| Reply 与 Target 所有权 | **高** — 必须定义 | 取消 reply → 清掉 ReplyBot;清掉 target → 保留 Matrix reply。ReplyBot 从 replying_to + bot 判定实时推导,不独立持久化 | +| 首次进入 bot room 的默认 target | **中** — 影响首次体验 | 建议用 `RoomDefault(bot)` 而非直接显示 "To @bot" | +| 静态命令表的定位 | **低** — spec 已明确 out of scope | 硬编码命令表就是当前设计,不应默认为"过渡层";动态注册需新的 Matrix-side 协议设计,属独立未来方向 | +| 文档中 `@octosbot` 硬编码 | **低** — 通用架构不应绑定特定 appservice | 改为 "configured bot" / "default bot",只在 OctOS 章节举例 | +| 用户如何主动切换 target | **高** — P1 必答题 | 无切换入口则 target chip 只是展示,不是交互模型。至少需定下一种:点 chip 弹出切换菜单 / reply bot 自动切换 / slash qualifier 选 target | +| 多 bot room 场景 | **低** — 当前不是主要场景 | P2 解决 | + +--- + +## 五、推荐的实施路径 + +认可 Codex 的 P0 → P1 → P2 分层,补充设计细节后可以写 spec: + +**P0(先做):** 改善绑定错误文案 + 为 Palpo+OctOS 部署提供 migration/preset 示例值。注意:`DEFAULT_BOTFATHER_LOCALPART` 是通用默认值,`botfather_user_id` 本身已是用户可配置项(`src/app.rs:2218`),UI 文案也定义为通用输入(`resources/i18n/en.json:274`)。不应把全局默认硬改为 `"octosbot"`,而应通过文档/示例引导 Palpo+OctOS 用户配置正确的值 + +**P1(核心):** Target 状态模型 + chip + 切换入口 +- 将 `active_target_user_id: Option` 重构为 `ExplicitOverride` enum(只持久化用户意图),运行时 resolve 为 `ResolvedTarget` +- 抽取 `is_known_or_likely_bot(user_id, bot_settings, current_user_id)` 统一 bot 判定(合并 `known_bot_user_ids()` + `resolved_bot_user_id()` + `is_likely_bot_user_id()`) +- 修复 reply-to-human 误触发 bot targeting(在 resolver 中加 bot 判定) +- 新增 `TargetIndicator` widget(参考 `ReplyingPreview` 的 UI 模式),区分来源显示 +- **⚠️ TargetIndicator 与 ReplyingPreview 的所有权关系:** reply 预览已有独立的显示/取消/恢复状态机(`show_replying_to()` at `room_input_bar.rs:1214`、`clear_replying_to()` at `:1267`、`on_editing_pane_hidden()` at `:1307`)。必须先定义: + - 取消 reply 是否同时清掉 `ReplyBot` target → **应该是**,因为 `ReplyBot` 的真相来源就是 `replying_to` + - 清掉 target chip 是否保留 Matrix reply → **应该是**,reply 是 Matrix 原生关系,target 是 Robrix UX 层 + - `ReplyBot` 不应作为独立状态持久化,而应在 resolve 时从 `replying_to` + bot 判定实时推导(见上方持久化层设计) +- **定义切换入口(P1 必答题):** 必须包含"主动选择 bot"的入口(否则 `ExplicitOverride::Bot` 永远不会被设置)。最小完整集: + - 点 chip 弹出切换菜单 → 可选择 bot 或 room(产生 `ExplicitOverride::Bot` / `ExplicitOverride::Room`) + - reply bot 消息 → 自动 resolve 为 `ReplyBot`(临时,取消 reply 即消失) + - chip 上的 × → 清除 `ExplicitOverride` 回到 `None`(默认行为) +- 在 send path 中接入 explicit target(当前传 `None` 的地方) + +**P2(增量):** 命令分类 + menu button + `/command@bot` + +## 六、验证方式 + +- 运行 `cargo run`,进入有 bot 的房间 +- 确认 target chip 正确显示当前消息目标,且区分来源(默认 bot vs 显式选择) +- 测试切换 target(To room ↔ To configured bot)后发送消息,验证路由正确性 +- 测试 reply-to-bot 时 target 自动切换为 `ReplyBot` +- **测试 reply-to-human 时不触发 bot targeting**(当前是 bug) +- 验证跨导航时 `ExplicitOverride` 被恢复;`ReplyBot` 会随 `replying_to` 一起恢复(`replying_to` 已持久化在 `RoomInputBarState`,`ReplyBot` 从中实时推导——不独立持久化,但因 `replying_to` 恢复而间接恢复) diff --git a/docs/superpowers/plans/2026-04-11-tg-bot-explicit-targeting-plan.md b/docs/superpowers/plans/2026-04-11-tg-bot-explicit-targeting-plan.md new file mode 100644 index 000000000..00ea23b3e --- /dev/null +++ b/docs/superpowers/plans/2026-04-11-tg-bot-explicit-targeting-plan.md @@ -0,0 +1,635 @@ +# Telegram Bot Explicit Target Model Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Implement an explicit bot target model in the room input bar so users can see and control whether a message goes to the room, the bound bot, or a reply-to-bot target, while preserving normal Matrix reply semantics. + +**Architecture:** Keep the existing Matrix send pipeline and `target_user_id` transport field, but replace the sticky `active_target_user_id` model with persisted `ExplicitOverride` plus runtime `ResolvedTarget`. `RoomScreen` precomputes bot-classification context and passes it through `RoomScreenProps`; `RoomInputBar` owns target resolution, chip/menu UI, persistence, and send-path integration. + +**Tech Stack:** Rust, Makepad 2.0 `script_mod!`, `matrix-sdk`/`ruma`, serde-backed UI state, `cargo test`, `cargo build`, `agent-spec`. + +**Repo rules for this plan:** +- Do not run `cargo fmt`. +- Do not commit during implementation until the user has manually tested the feature. +- `agent-spec lifecycle` is a whole-spec gate, not a per-selector tool. Use targeted `cargo test ` commands during each task, then run full lifecycle verification at the end. + +--- + +## File Map + +- `src/home/room_screen.rs` + - Add the new `is_known_or_likely_bot()` helper. + - Expand `RoomScreenProps` with precomputed bot-classification context. + - Resolve `resolved_parent_bot_user_id` from `AppState` when building room props. + - Add unit tests to the existing `#[cfg(test)] mod tests`. + +- `src/room/room_input_bar.rs` + - Replace `active_target_user_id` with persisted `ExplicitOverride`. + - Add `ResolvedTarget` and pure target-resolution helpers. + - Update both send paths to resolve `target_user_id` from `ResolvedTarget`. + - Add `TargetIndicator` DSL, chip/menu interaction, and formatting helpers. + - Keep `replying_to` as the single source of truth for reply state. + - Add unit tests to the existing `#[cfg(test)] mod tests`. + +- `src/room/reply_preview.rs` + - Only adjust spacing/layout if `TargetIndicator` and `ReplyingPreview` do not stack cleanly. + - Do not change reply lifecycle logic here. + +- `resources/i18n/en.json` +- `resources/i18n/zh-CN.json` + - Add target-chip and target-menu strings under the existing `room_input_bar.*` namespace. + +--- + +## Task 1: Bot Classification Context in `RoomScreen` + +**Files:** +- Modify: `src/home/room_screen.rs` (`detected_bot_binding_for_members()` near lines 360-429, `is_likely_bot_user_id()` near lines 432-448, `RoomScreenProps` construction near lines 3522-3575, struct definition near lines 6471-6482, test module near lines 9035+) +- Test: `src/home/room_screen.rs` + +- [ ] **Step 1: Add failing bot-detection tests in `room_screen.rs`** + +Add these exact spec-bound tests to the existing `#[cfg(test)] mod tests`: + +```rust +#[test] +fn test_bot_detection_configured_parent() { /* ... */ } + +#[test] +fn test_bot_detection_heuristic_fallback() { /* ... */ } + +#[test] +fn test_bot_detection_child_bot() { /* ... */ } + +#[test] +fn test_bot_detection_rejects_normal_user() { /* ... */ } +``` + +Focus them on the pure helper signature from the spec: + +```rust +is_known_or_likely_bot(user_id, resolved_parent_bot_user_id.as_deref(), &known_bot_user_ids) +``` + +- [ ] **Step 2: Run the new tests and confirm they fail** + +Run: + +```bash +cargo test test_bot_detection_ +``` + +Expected: +- FAIL because `is_known_or_likely_bot()` does not exist yet, or because current logic does not cover all three detection paths. + +- [ ] **Step 3: Implement `is_known_or_likely_bot()` without changing room-binding detection responsibilities** + +Add a new helper adjacent to `is_likely_bot_user_id()`: + +```rust +fn is_known_or_likely_bot( + user_id: &UserId, + resolved_parent_bot_user_id: Option<&UserId>, + known_bot_user_ids: &[OwnedUserId], +) -> bool { + known_bot_user_ids.iter().any(|known| known.as_str() == user_id.as_str()) + || resolved_parent_bot_user_id.is_some_and(|parent| parent == user_id) + || is_likely_bot_user_id(user_id, resolved_parent_bot_user_id) +} +``` + +Keep `detected_bot_binding_for_members()` as the room-level binding detector. Do not collapse it into the new helper. + +- [ ] **Step 4: Extend `RoomScreenProps` with precomputed bot context** + +Add these fields: + +```rust +pub resolved_parent_bot_user_id: Option, +pub known_bot_user_ids: Vec, +``` + +Populate them when constructing `RoomScreenProps` from `AppState`, next to the existing `bound_bot_user_id` logic: + +```rust +let resolved_parent_bot_user_id = app_state + .bot_settings + .resolved_bot_user_id(current_user_id().as_deref()) + .ok(); +let known_bot_user_ids = app_state.bot_settings.known_bot_user_ids(); +``` + +Dummy/fallback props should use `None` / `Vec::new()`. + +- [ ] **Step 5: Re-run the bot-detection tests** + +Run: + +```bash +cargo test test_bot_detection_ +``` + +Expected: +- PASS for all four detection tests. + +- [ ] **Step 6: Checkpoint the diff without committing** + +Run: + +```bash +git diff --stat -- src/home/room_screen.rs +``` + +Expected: +- Only `src/home/room_screen.rs` is touched for this task. + +--- + +## Task 2: Replace Sticky Target State with Explicit Model + +**Files:** +- Modify: `src/room/room_input_bar.rs` (`RoomInputBar` fields near lines 603-605, `resolve_target_user_id()` near lines 861-876, save/restore near lines 1588-1648, `RoomInputBarState` near lines 1770-1779, test module near lines 1797+) +- Test: `src/room/room_input_bar.rs` + +- [ ] **Step 1: Add failing pure-resolution tests** + +Add these spec-bound tests to `room_input_bar.rs`: + +```rust +#[test] +fn test_reply_to_human_no_bot_targeting() { /* ... */ } + +#[test] +fn test_reply_bot_overrides_explicit_room() { /* ... */ } + +#[test] +fn test_chip_dismiss_returns_to_room_default() { /* ... */ } + +#[test] +fn test_chip_dismiss_explicit_room_to_room_default() { /* ... */ } + +#[test] +fn test_chip_dismiss_no_bound_bot() { /* ... */ } +``` + +Implement them against pure helpers, not Makepad widget rendering. The goal is to lock the precedence chain before wiring UI. + +- [ ] **Step 2: Run the new tests and confirm they fail** + +Run: + +```bash +cargo test test_reply_to_human_no_bot_targeting +cargo test test_reply_bot_overrides_explicit_room +cargo test test_chip_dismiss_ +``` + +Expected: +- FAIL because the current state model is still `active_target_user_id: Option`. + +- [ ] **Step 3: Introduce `ExplicitOverride` and `ResolvedTarget`** + +Add pure enums: + +```rust +#[derive(Clone, Debug, Default, PartialEq, Eq)] +enum ExplicitOverride { + #[default] + None, + Bot(OwnedUserId), + Room, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum ResolvedTarget { + NoTarget, + RoomDefault(OwnedUserId), + ExplicitBot(OwnedUserId), + ExplicitRoom, + ReplyBot(OwnedUserId), +} +``` + +Also add helper functions: + +```rust +fn resolve_target( + explicit_override: &ExplicitOverride, + replying_to_sender: Option<&UserId>, + bound_bot_user_id: Option<&UserId>, + resolved_parent_bot_user_id: Option<&UserId>, + known_bot_user_ids: &[OwnedUserId], +) -> ResolvedTarget + +fn resolved_target_user_id(target: &ResolvedTarget) -> Option + +fn clear_explicit_override_result( + bound_bot_user_id: Option<&UserId>, +) -> ResolvedTarget +``` + +Design rule: +- `ReplyBot` is derived, never persisted. +- `ExplicitRoom` means “skip fallback bot”, not “no explicit state”. + +- [ ] **Step 4: Replace the persisted state slot** + +Rename/replace: + +```rust +#[rust] active_target_user_id: Option +``` + +with: + +```rust +#[rust] explicit_override: ExplicitOverride +``` + +Do the same in `RoomInputBarState` save/restore. Keep `replying_to` exactly as-is; only the target intent slot changes. + +- [ ] **Step 5: Re-run the precedence tests** + +Run: + +```bash +cargo test test_reply_to_human_no_bot_targeting +cargo test test_reply_bot_overrides_explicit_room +cargo test test_chip_dismiss_ +``` + +Expected: +- PASS for the pure precedence/dismiss tests. + +- [ ] **Step 6: Checkpoint the diff without committing** + +Run: + +```bash +git diff --stat -- src/room/room_input_bar.rs +``` + +Expected: +- Only `src/room/room_input_bar.rs` changes in this task. + +--- + +## Task 3: Wire the Send Paths and Restore Semantics + +**Files:** +- Modify: `src/room/room_input_bar.rs` (location send near lines 1037-1083, text send near lines 1095-1159, reply clear near lines 887-894, restore logic near lines 1641-1648) +- Test: `src/room/room_input_bar.rs` + +- [ ] **Step 1: Add failing integration-oriented state tests** + +Add these tests: + +```rust +#[test] +fn test_explicit_bot_with_reply_to_human() { /* ... */ } + +#[test] +fn test_cancel_reply_clears_reply_bot() { /* ... */ } + +#[test] +fn test_explicit_override_persists_navigation() { /* ... */ } + +#[test] +fn test_reply_bot_restores_with_replying_to() { /* ... */ } +``` + +Keep them deterministic by testing helper/state transitions directly where possible. Do not wait for full widget-render assertions. + +- [ ] **Step 2: Run the new tests and confirm they fail** + +Run: + +```bash +cargo test test_explicit_bot_with_reply_to_human +cargo test test_cancel_reply_clears_reply_bot +cargo test test_explicit_override_persists_navigation +cargo test test_reply_bot_restores_with_replying_to +``` + +Expected: +- FAIL because reply send paths still use `replying_to.sender()` directly and persistence still assumes sticky target state. + +- [ ] **Step 3: Add a bot-aware reply-target helper** + +Add a helper that derives reply targeting only when the replied-to sender is actually a bot: + +```rust +fn reply_bot_target_user_id( + &self, + room_screen_props: &RoomScreenProps, +) -> Option { + let reply_sender = self.replying_to + .as_ref() + .map(|(event_tl_item, _)| event_tl_item.sender()); + + match resolve_target( + &self.explicit_override, + reply_sender, + room_screen_props.bound_bot_user_id.as_deref(), + room_screen_props.resolved_parent_bot_user_id.as_deref(), + &room_screen_props.known_bot_user_ids, + ) { + ResolvedTarget::ReplyBot(user_id) => Some(user_id), + _ => None, + } +} +``` + +The pure resolver decides whether reply-to-human falls back to `ExplicitOverride` / `RoomDefault`. + +- [ ] **Step 4: Update both send paths** + +In both the location send path and text send path: +- compute `ResolvedTarget` once +- derive `target_user_id` from `resolved_target_user_id(&resolved_target)` +- keep Matrix `Reply` relation unchanged +- never set `target_user_id` to a human sender merely because the user clicked reply + +The current raw pattern: + +```rust +let reply_target_user_id = self.replying_to.as_ref().map(|(item, _)| item.sender().to_owned()); +``` + +should disappear from both send paths. + +- [ ] **Step 5: Keep reply and explicit target ownership separate** + +On cancel reply: +- `replying_to` becomes `None` +- `ReplyBot` disappears because it is derived +- `ExplicitOverride` remains unchanged + +On restore: +- `ExplicitOverride` comes from `RoomInputBarState` +- `ReplyBot` is re-derived if `replying_to` restores + +- [ ] **Step 6: Re-run the integration tests** + +Run: + +```bash +cargo test test_explicit_bot_with_reply_to_human +cargo test test_cancel_reply_clears_reply_bot +cargo test test_explicit_override_persists_navigation +cargo test test_reply_bot_restores_with_replying_to +``` + +Expected: +- PASS for send-path and restore semantics. + +- [ ] **Step 7: Build after send-path changes** + +Run: + +```bash +cargo build +``` + +Expected: +- PASS + +--- + +## Task 4: Add `TargetIndicator` UI, Menu Interaction, and i18n + +**Files:** +- Modify: `src/room/room_input_bar.rs` (DSL root near lines 175-220 and surrounding widget tree, event handling in `handle_actions()`, helper functions) +- Modify: `src/room/reply_preview.rs` only if vertical spacing becomes cramped +- Modify: `resources/i18n/en.json` +- Modify: `resources/i18n/zh-CN.json` +- Test: `src/room/room_input_bar.rs` + +- [ ] **Step 1: Add failing presentation tests** + +Add these spec-bound tests: + +```rust +#[test] +fn test_target_chip_room_default() { /* ... */ } + +#[test] +fn test_target_chip_hidden_no_bot() { /* ... */ } + +#[test] +fn test_explicit_bot_via_chip_menu() { /* ... */ } + +#[test] +fn test_explicit_room_via_chip_menu() { /* ... */ } +``` + +Do not make them depend on full Makepad rendering. Add a pure presentation helper so the tests can assert: +- visibility +- label text +- subdued-vs-normal style flag +- whether dismiss is shown + +- [ ] **Step 2: Run the presentation tests and confirm they fail** + +Run: + +```bash +cargo test test_target_chip_ +cargo test test_explicit_bot_via_chip_menu +cargo test test_explicit_room_via_chip_menu +``` + +Expected: +- FAIL because there is no target-chip presentation layer yet. + +- [ ] **Step 3: Add a pure presentation formatter** + +Introduce a helper such as: + +```rust +struct TargetChipPresentation { + visible: bool, + label: String, + subdued: bool, + dismissible: bool, +} + +fn format_target_chip_presentation( + app_language: AppLanguage, + resolved_target: &ResolvedTarget, + bot_display_name: Option<&str>, +) -> TargetChipPresentation +``` + +Formatting rules must match the spec exactly: +- `RoomDefault` → `"Default: {display_name}"` in subdued style +- `ExplicitBot` → `"To {display_name}"` +- `ExplicitRoom` → `"To room"` +- `ReplyBot` → `"Reply → {display_name}"` +- display name falls back to localpart if no room-member display name is available + +- [ ] **Step 4: Add `TargetIndicator` DSL to `RoomInputBar`** + +Insert the new UI above `replying_preview` in the widget tree so the target chip is the top-most context row: + +```rust +target_indicator := View { + visible: false + width: Fill + height: Fit + flow: Down + + target_chip_row := View { + flow: Right + align: Align{y: 0.5} + // chip label + dismiss + menu anchor + } + + target_menu_popup := RoundedView { + visible: false + // room option + bound bot option + } +} +``` + +Reuse the inline popup pattern already used by `emoji_picker_popup` / `translation_lang_wrapper`. Do not introduce a new global popup framework for this task. + +- [ ] **Step 5: Wire chip/menu actions in `handle_actions()`** + +Required interactions: +- clicking the chip toggles the target menu +- selecting the room option sets `ExplicitOverride::Room` +- selecting the bound bot option sets `ExplicitOverride::Bot(bound_bot_user_id.clone())` +- clicking `×` resets `ExplicitOverride::None` +- reply-to-bot does **not** mutate `ExplicitOverride`; it only changes runtime `ResolvedTarget` + +If the room has no bound bot, do not invent a multi-bot menu. Hide the bot option and fall back to the `NoTarget` / `To room` behavior from the resolver. + +- [ ] **Step 6: Add i18n keys** + +Add keys under the existing `room_input_bar.*` namespace, for example: + +```json +"room_input_bar.target.default": "Default: {display_name}", +"room_input_bar.target.to_bot": "To {display_name}", +"room_input_bar.target.to_room": "To room", +"room_input_bar.target.reply_bot": "Reply → {display_name}", +"room_input_bar.target.menu.bound_bot": "{display_name}", +"room_input_bar.target.menu.room": "To room" +``` + +Mirror them in `zh-CN.json`. + +- [ ] **Step 7: Adjust `reply_preview.rs` only if the stacked layout looks wrong** + +Allowed adjustment: +- padding / margin / spacing between the new `TargetIndicator` row and the existing `ReplyingPreview` + +Not allowed: +- changing the `ReplyingPreview` state machine +- moving reply state ownership into `reply_preview.rs` + +- [ ] **Step 8: Re-run the target-chip tests and build** + +Run: + +```bash +cargo test test_target_chip_ +cargo test test_explicit_bot_via_chip_menu +cargo test test_explicit_room_via_chip_menu +cargo build +``` + +Expected: +- PASS + +--- + +## Task 5: Full Verification and Manual Smoke Test + +**Files:** +- Modify: none expected unless verification uncovers defects in the files above +- Verify: `src/home/room_screen.rs`, `src/room/room_input_bar.rs`, `src/room/reply_preview.rs`, `resources/i18n/en.json`, `resources/i18n/zh-CN.json` + +- [ ] **Step 1: Run focused cargo test batches** + +Run: + +```bash +cargo test test_bot_detection_ +cargo test test_target_chip_ +cargo test test_reply_ +cargo test test_explicit_ +cargo test test_chip_dismiss_ +``` + +Expected: +- PASS across all new spec-bound unit tests. + +- [ ] **Step 2: Run a full build** + +Run: + +```bash +cargo build +``` + +Expected: +- PASS + +- [ ] **Step 3: Run full contract verification** + +Run: + +```bash +agent-spec lifecycle specs/task-tg-bot-explicit-targeting.spec.md --code . --format json +``` + +Expected: +- every scenario verdict is `pass` +- no boundary violations +- no spec quality regressions + +- [ ] **Step 4: Manual smoke test in the app** + +Run: + +```bash +cargo run +``` + +Manual checklist: +1. Open a room with a bound bot and confirm the chip shows `Default: `. +2. Click the chip and switch to `To room`; send a message and confirm `target_user_id` is absent. +3. Switch back to the bound bot; send a message and confirm `target_user_id` is the bot. +4. Reply to a human message and confirm the message is a Matrix reply but not targeted to that human. +5. Reply to a bot message and confirm the chip changes to `Reply → `. +6. Cancel the reply and confirm the chip falls back to the underlying explicit/default state. +7. Navigate away and back; confirm `ExplicitOverride` restores and `ReplyBot` only restores if `replying_to` restores. + +- [ ] **Step 5: Stop for user testing** + +Run: + +```bash +git status --short +git diff --stat +``` + +Expected: +- only the planned files changed +- no commit has been created yet + +Do not commit until the user tests the feature and explicitly approves the commit step. + +--- + +## Final Exit Criteria + +Before calling this implementation complete, all of the following must be true: + +- `cargo build` passes +- all spec-bound `cargo test` filters above pass +- `agent-spec lifecycle specs/task-tg-bot-explicit-targeting.spec.md --code . --format json` shows all scenarios as `pass` +- manual smoke test covers target chip visibility, chip menu switching, reply-to-human fallback, reply-to-bot temporary override, and navigation restore +- the worktree is ready for user testing, but **not yet committed** diff --git a/docs/superpowers/plans/2026-04-11-tg-bot-ui-alignment.md b/docs/superpowers/plans/2026-04-11-tg-bot-ui-alignment.md new file mode 100644 index 000000000..1da2751fd --- /dev/null +++ b/docs/superpowers/plans/2026-04-11-tg-bot-ui-alignment.md @@ -0,0 +1,363 @@ +# Telegram Bot UI Alignment Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add bot identity badges on message timeline and `/` slash command autocomplete menu to align Robrix bot UX with Telegram. + +**Architecture:** Two independent features that share no code: (1) a `bot_badge` Label widget added to the `Message` DSL template, controlled by `set_visible()` in `populate_message_view()`; (2) slash command autocomplete reusing the existing `CommandTextInput` trigger mechanism with a hardcoded command list. Both features are purely UI — no Matrix SDK or backend changes needed. + +**Tech Stack:** Makepad 2.0 `script_mod!` DSL, Rust, existing `CommandTextInput` widget, existing `is_likely_bot_user_id()` detection. + +**Spec:** `specs/task-tg-bot-ui-alignment.spec.md` + +--- + +## File Structure + +| File | Action | Responsibility | +|------|--------|----------------| +| `src/home/room_screen.rs` | Modify | Add `bot_badge` Label to `Message` DSL template; set visibility in `populate_message_view()` | +| `src/shared/mentionable_text_input.rs` | Modify | Add slash command detection, hardcoded command list, command item template | +| `resources/i18n/en.json` | Modify | Add slash command description strings | +| `resources/i18n/zh-CN.json` | Modify | Add slash command description strings (Chinese) | + +--- + +### Task 1: Add Bot Badge Widget to Message DSL Template + +**Files:** +- Modify: `src/home/room_screen.rs:665-682` (username_view in Message template) + +This task adds a hidden-by-default `bot_badge` Label inside the `username_view` of the `Message` template. The badge is a small rounded label with blue background and white "bot" text. + +- [ ] **Step 1: Add bot_badge to the Message template DSL** + +In `src/home/room_screen.rs`, inside the `username_view` (line ~665), add a `bot_badge` Label after the `username` Label. The badge should be hidden by default (`visible: false`). + +Find the existing `username_view` block: +``` +username_view := View { + flow: Right, + width: Fill, + height: Fit, + username := Label { + width: Fill, + flow: Right, // do not wrap + ... + text: "" + } +} +``` + +Change `username` width from `Fill` to `Fit` (so the badge can sit next to it), and add the badge: +``` +username_view := View { + flow: Right, + width: Fill, + height: Fit, + align: Align{y: 0.5} + username := Label { + width: Fit, + flow: Right, + padding: 0, + margin: Inset{bottom: 9.0, top: 20.0, right: 4.0,} + max_lines: 1 + text_overflow: Ellipsis + draw_text +: { + text_style: USERNAME_TEXT_STYLE {}, + color: (USERNAME_TEXT_COLOR) + } + text: "" + } + bot_badge := RoundedView { + visible: false + width: Fit + height: 16 + margin: Inset{top: 18.0, right: 6.0} + padding: Inset{left: 5, right: 5, top: 1, bottom: 1} + show_bg: true + draw_bg +: { + color: (COLOR_ACTIVE_PRIMARY) + border_radius: 3.0 + } + bot_badge_label := Label { + width: Fit + height: Fit + draw_text +: { + text_style: REGULAR_TEXT {font_size: 8.0} + color: #fff + } + text: "bot" + } + } +} +``` + +- [ ] **Step 2: Build to verify DSL compiles** + +Run: `cargo build 2>&1 | tail -5` +Expected: `Finished` with no errors + +- [ ] **Step 3: Commit** + +```bash +git add src/home/room_screen.rs +git commit -m "ui: add bot_badge widget to Message template (hidden by default)" +``` + +--- + +### Task 2: Show/Hide Bot Badge in populate_message_view() + +**Files:** +- Modify: `src/home/room_screen.rs:7360-7394` (username setting block in populate_message_view) + +This task adds bot detection logic that calls `set_visible()` on the badge after setting the username. + +- [ ] **Step 1: Add bot detection and badge visibility logic** + +In `src/home/room_screen.rs`, after the username is set (line ~7380 `username_label.set_text(cx, &username);`), add bot badge visibility logic. The detection uses the existing `is_likely_bot_user_id()` function (already defined in this file around line 383). + +Find the block: +```rust +username_label.set_text(cx, &username); +new_drawn_status.profile_drawn = profile_drawn; +``` + +Add after it: +```rust +// Show/hide the bot badge based on sender's user ID +let sender_is_bot = is_likely_bot_user_id(event_tl_item.sender()); +item.view(cx, ids!(content.username_view.bot_badge)).set_visible(cx, sender_is_bot); +``` + +Also in the `else` branch (server notice, line ~7383), ensure the badge is hidden: +```rust +item.view(cx, ids!(content.username_view.bot_badge)).set_visible(cx, false); +``` + +- [ ] **Step 2: Build to verify** + +Run: `cargo build 2>&1 | tail -5` +Expected: `Finished` with no errors + +- [ ] **Step 3: Manual test** + +Run: `cargo run` +- Open a room with a bot (e.g., `@octosbot:127.0.0.1:8128`) +- Bot messages should show a blue "bot" badge next to the username +- Your own messages should NOT show the badge +- Condensed messages (consecutive from same sender) should NOT show the badge (username_view is hidden in CondensedMessage) + +- [ ] **Step 4: Commit** + +```bash +git add src/home/room_screen.rs +git commit -m "ui: show bot badge on messages from bot users" +``` + +--- + +### Task 3: Add i18n Keys for Slash Commands + +**Files:** +- Modify: `resources/i18n/en.json` +- Modify: `resources/i18n/zh-CN.json` + +- [ ] **Step 1: Add English slash command descriptions** + +In `resources/i18n/en.json`, add these keys (in the appropriate alphabetical position): + +```json +"slash_command.createbot.description": "Create a new child bot", +"slash_command.deletebot.description": "Delete an existing bot", +"slash_command.listbots.description": "List all available bots", +"slash_command.bothelp.description": "Show bot management help", +"slash_command.header": "Bot Commands", +``` + +- [ ] **Step 2: Add Chinese slash command descriptions** + +In `resources/i18n/zh-CN.json`, add: + +```json +"slash_command.createbot.description": "创建一个新的子 Bot", +"slash_command.deletebot.description": "删除一个已有的 Bot", +"slash_command.listbots.description": "列出所有可用的 Bot", +"slash_command.bothelp.description": "显示 Bot 管理帮助", +"slash_command.header": "Bot 命令", +``` + +- [ ] **Step 3: Build to verify JSON is valid** + +Run: `cargo build 2>&1 | tail -5` +Expected: `Finished` with no errors + +- [ ] **Step 4: Commit** + +```bash +git add resources/i18n/en.json resources/i18n/zh-CN.json +git commit -m "i18n: add slash command description strings" +``` + +--- + +### Task 4: Add Slash Command Detection to MentionableTextInput + +**Files:** +- Modify: `src/shared/mentionable_text_input.rs` + +The existing `MentionableTextInput` uses `CommandTextInput` with trigger `"@"`. We need to detect when the user types `/` at the **start of the input** (position 0 or after a newline) and show a hardcoded list of bot commands. + +The approach: instead of modifying `CommandTextInput`'s single-trigger mechanism, we handle `/` detection in `MentionableTextInput`'s own `handle_event` / `handle_actions` by checking the text content. When `/` is detected at position 0, we populate the popup with command items instead of user items. + +- [ ] **Step 1: Add slash command data struct and constant list** + +In `src/shared/mentionable_text_input.rs`, add a struct and constant list after the imports: + +```rust +/// A bot slash command entry for the command autocomplete popup. +pub struct SlashCommand { + pub command: &'static str, + pub description_key: &'static str, +} + +/// Hardcoded BotFather slash commands. +const SLASH_COMMANDS: &[SlashCommand] = &[ + SlashCommand { command: "/createbot", description_key: "slash_command.createbot.description" }, + SlashCommand { command: "/deletebot", description_key: "slash_command.deletebot.description" }, + SlashCommand { command: "/listbots", description_key: "slash_command.listbots.description" }, + SlashCommand { command: "/bothelp", description_key: "slash_command.bothelp.description" }, +]; +``` + +- [ ] **Step 2: Add a slash command list item DSL template** + +In the `script_mod!` block, add a template for slash command items (similar to `UserListItem` but simpler — command name + description): + +``` +mod.widgets.SlashCommandListItem = { + ..mod.widgets.View + width: Fill + height: Fit + padding: Inset{left: 12, right: 12, top: 8, bottom: 8} + spacing: 4 + flow: Down + show_bg: true + draw_bg +: { + color: (COLOR_PRIMARY) + } + + command_label := Label { + width: Fit + height: Fit + draw_text +: { + text_style: REGULAR_TEXT {font_size: 11.0} + color: (COLOR_ACTIVE_PRIMARY) + } + } + description_label := Label { + width: Fill + height: Fit + draw_text +: { + text_style: REGULAR_TEXT {font_size: 9.5} + color: #888 + } + } +} +``` + +Also add a `#[live]` field to `MentionableTextInput` for this template: +```rust +#[live] +slash_command_list_item: Option, +``` + +- [ ] **Step 3: Add slash command state tracking** + +Add fields to `MentionableTextInput`: +```rust +/// Whether slash command popup is currently active (instead of @mention) +#[rust(false)] +slash_command_active: bool, +``` + +- [ ] **Step 4: Implement slash command detection and popup** + +In the `handle_event` or `handle_actions` method of `MentionableTextInput`, add detection logic: + +When the text input content changes: +1. Check if the text starts with `/` +2. If yes and no `@mention` search is active, extract the prefix after `/` +3. Filter `SLASH_COMMANDS` by prefix match +4. Populate the popup list with matching commands +5. Show the popup + +When a command item is selected: +1. Replace the input text with the selected command +2. Close the popup + +When the text no longer starts with `/`: +1. If `slash_command_active`, hide the popup and reset + +This is the most complex step. The implementation should hook into the existing `changed()` action handler where text changes are detected. + +- [ ] **Step 5: Build to verify** + +Run: `cargo build 2>&1 | tail -5` +Expected: `Finished` with no errors + +- [ ] **Step 6: Manual test** + +Run: `cargo run` +- Type `/` at the start of the message input +- A popup should appear with 4 commands: `/createbot`, `/deletebot`, `/listbots`, `/bothelp` +- Type `/list` — popup should filter to show only `/listbots` +- Type `/zzz` — popup should show empty or close +- Select a command — it should be inserted into the input +- Type a normal message (no `/`) — no popup should appear + +- [ ] **Step 7: Commit** + +```bash +git add src/shared/mentionable_text_input.rs +git commit -m "feat: add slash command autocomplete for bot commands" +``` + +--- + +### Task 5: Final Integration Test and Cleanup + +**Files:** +- All modified files + +- [ ] **Step 1: Full build** + +Run: `cargo build 2>&1 | tail -5` +Expected: `Finished` with no errors + +- [ ] **Step 2: End-to-end manual test** + +Run: `cargo run` + +Verify all scenarios from the spec: +1. Bot messages show blue "bot" badge next to username +2. User messages do NOT show bot badge +3. Consecutive bot messages (condensed view) do NOT show badge +4. Typing `/` shows command popup with 4 commands +5. Filtering works (type `/list` to narrow) +6. Selecting a command inserts it +7. Non-matching prefix (`/zzz`) shows empty/closes +8. Normal typing (no `/`) does not trigger popup + +- [ ] **Step 3: Verify against spec** + +Run: `agent-spec parse specs/task-tg-bot-ui-alignment.spec.md` +Review each scenario and confirm it passes manually. + +- [ ] **Step 4: Final commit if any cleanup needed** + +```bash +git add -A +git commit -m "feat: telegram bot UI alignment — bot badge and slash commands" +``` diff --git a/docs/superpowers/plans/2026-04-12-tg-bot-mention-reply-first-plan.md b/docs/superpowers/plans/2026-04-12-tg-bot-mention-reply-first-plan.md new file mode 100644 index 000000000..00c6b33fb --- /dev/null +++ b/docs/superpowers/plans/2026-04-12-tg-bot-mention-reply-first-plan.md @@ -0,0 +1,136 @@ +# Telegram Bot Mention/Reply-First Targeting Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Remove the always-visible target chip/popup and make bot-bound rooms default to room-first message routing, with bot interaction driven by `@mention` and reply-to-bot. + +**Architecture:** Keep the existing mention routing, reply-to-bot targeting, and `explicit_room` marker pipeline. Change the input bar’s default resolved target from `RoomDefault` to `ExplicitRoom` in bot-bound rooms, clear any persisted explicit target overrides from older builds, and hide the target chip/popup UI entirely so the main interaction path is text-first rather than mode-switch-first. + +**Tech Stack:** Rust, Makepad 2.0 `script_mod!`, `cargo test`, `cargo build`, `agent-spec`. + +--- + +## File Map + +- `src/room/room_input_bar.rs` + - Change default target resolution to room-first in bot-bound rooms. + - Remove visible target-chip behavior from the input bar. + - Stop restoring persisted explicit overrides from previous builds. + - Update unit tests from chip/menu semantics to mention/reply-first semantics. + +- `src/home/room_screen.rs` + - Remove or neutralize the now-unused target popup trigger path if needed for compile cleanliness. + +- `specs/task-tg-bot-mention-reply-first.spec.md` + - Verification contract for the new direction. + +--- + +### Task 1: Lock the New Routing Semantics with Failing Tests + +**Files:** +- Modify: `src/room/room_input_bar.rs` +- Test: `src/room/room_input_bar.rs` + +- [ ] **Step 1: Add failing tests for the new default behavior** + +Add tests for: +- `test_bot_bound_room_defaults_to_explicit_room` +- `test_reply_to_human_in_bot_bound_room_stays_explicit_room` +- `test_reply_to_bot_still_targets_bot` +- `test_persisted_explicit_override_is_ignored_on_restore` + +- [ ] **Step 2: Run the new tests and confirm they fail** + +Run: + +```bash +cargo test test_bot_bound_room_defaults_to_explicit_room +cargo test test_reply_to_human_in_bot_bound_room_stays_explicit_room +cargo test test_reply_to_bot_still_targets_bot +cargo test test_persisted_explicit_override_is_ignored_on_restore +``` + +- [ ] **Step 3: Update the target resolution helpers** + +Implement the minimal logic so that: +- `ExplicitOverride::None + bound_bot_user_id` resolves to `ExplicitRoom` +- `reply-to-bot` still resolves to `ReplyBot` +- persisted explicit overrides restore as `None` + +- [ ] **Step 4: Re-run the routing tests** + +Run the same four tests and confirm they pass. + +--- + +### Task 2: Remove the Visible Target UI from the Input Bar + +**Files:** +- Modify: `src/room/room_input_bar.rs` +- Modify: `src/home/room_screen.rs` + +- [ ] **Step 1: Add a failing UI-state test** + +Add: +- `test_target_chip_hidden_in_bot_bound_room` + +- [ ] **Step 2: Run the test and confirm it fails** + +Run: + +```bash +cargo test test_target_chip_hidden_in_bot_bound_room +``` + +- [ ] **Step 3: Hide the target indicator and stop exposing the popup** + +Make the minimal code changes so: +- `sync_target_indicator()` always hides the target chip +- the target chip no longer opens a popup in normal use +- any stale popup path in `RoomScreen` is neutralized or left dormant but unreachable + +- [ ] **Step 4: Re-run the UI-state test** + +Run: + +```bash +cargo test test_target_chip_hidden_in_bot_bound_room +``` + +--- + +### Task 3: Verify Mention/Reply Routing Still Works End-to-End + +**Files:** +- Modify: `src/room/room_input_bar.rs` + +- [ ] **Step 1: Keep and adapt the mention/reply regression tests** + +Ensure these tests still cover the final behavior: +- `test_message_bot_mention_keeps_explicit_room_marker` +- `test_text_mentions_known_bot_matches_localpart` +- `test_message_mentions_room_member_bot_with_empty_known_bot_list` +- `test_message_mentions_known_bot_prefers_structured_mentions` + +- [ ] **Step 2: Run the focused regression suite** + +Run: + +```bash +cargo test test_message_bot_mention_keeps_explicit_room_marker +cargo test test_text_mentions_known_bot_matches_localpart +cargo test test_message_mentions_room_member_bot_with_empty_known_bot_list +cargo test test_message_mentions_known_bot_prefers_structured_mentions +``` + +- [ ] **Step 3: Run the final verification gates** + +Run: + +```bash +cargo build +agent-spec parse specs/task-tg-bot-mention-reply-first.spec.md +agent-spec lint specs/task-tg-bot-mention-reply-first.spec.md --min-score 0.7 +``` + diff --git a/docs/superpowers/plans/2026-04-12-tg-bot-timeline-cards-plan.md b/docs/superpowers/plans/2026-04-12-tg-bot-timeline-cards-plan.md new file mode 100644 index 000000000..5ae1b6664 --- /dev/null +++ b/docs/superpowers/plans/2026-04-12-tg-bot-timeline-cards-plan.md @@ -0,0 +1,184 @@ +# Telegram Bot Timeline Cards Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Restyle bot-authored text messages in the room timeline into Telegram-inspired reply cards with a clear body card, lightweight status strip, and subdued metadata footer. + +**Architecture:** Keep the current mention/reply-first routing and current Matrix payloads untouched. Add a small bot-message parsing layer in `room_screen.rs` that recognizes Octos' existing status/provider/footer text format, then render bot text messages through a dedicated timeline card sub-structure while preserving `HtmlOrPlaintext` for the extracted main body. + +**Tech Stack:** Rust, Makepad 2.0 `script_mod!`, existing `HtmlOrPlaintext`, `cargo test`, `cargo build`, `agent-spec`. + +--- + +## File Map + +- `src/home/room_screen.rs` + - Add pure helpers for parsing bot timeline layers from existing Octos text output. + - Add bot card widgets / styling to `Message` and `CondensedMessage`. + - Wire parsed bot layers into timeline population for text and notice messages. + +- `src/shared/html_or_plaintext.rs` + - Only touch if the bot card body needs a small style or spacing hook that cannot live entirely in `room_screen.rs`. + +- `src/home/edited_indicator.rs` + - Only touch if edited indicator placement or visual weight needs a small adjustment to fit the new footer hierarchy. + +- `specs/task-tg-bot-timeline-cards.spec.md` + - Verification contract for the work. + +--- + +### Task 1: Lock Bot Timeline Parsing with Pure Failing Tests + +**Files:** +- Modify: `src/home/room_screen.rs` +- Test: `src/home/room_screen.rs` + +- [ ] **Step 1: Add parsing tests for the happy path and fallback cases** + +Add tests for: +- `test_parse_bot_timeline_layers_extracts_status_provider_body_and_footer` +- `test_parse_bot_timeline_layers_falls_back_for_unmatched_bot_text` +- `test_parse_bot_timeline_layers_ignores_regular_user_messages` +- `test_parse_bot_timeline_layers_prefers_safe_fallback_for_malformed_metadata` +- `test_parse_bot_timeline_layers_invalid_metadata_does_not_panic` + +- [ ] **Step 2: Run the focused parsing tests and confirm they fail** + +Run: + +```bash +cargo test test_parse_bot_timeline_layers_extracts_status_provider_body_and_footer +cargo test test_parse_bot_timeline_layers_falls_back_for_unmatched_bot_text +cargo test test_parse_bot_timeline_layers_ignores_regular_user_messages +cargo test test_parse_bot_timeline_layers_prefers_safe_fallback_for_malformed_metadata +cargo test test_parse_bot_timeline_layers_invalid_metadata_does_not_panic +``` + +- [ ] **Step 3: Add a minimal bot timeline layer parser** + +Implement small pure helpers in `src/home/room_screen.rs`: +- one type to hold parsed bot layers +- one function that only activates for bot senders +- conservative parsing for: + - optional top status line + - optional `via provider (model)` line + - optional trailing `_model · X in · Y out · Zs_` footer +- safe fallback to full-body rendering when the format is unmatched or malformed + +- [ ] **Step 4: Re-run the parsing tests** + +Run the same five tests and confirm they pass. + +--- + +### Task 2: Add the Bot Reply Card Structure to Timeline Widgets + +**Files:** +- Modify: `src/home/room_screen.rs` + +- [ ] **Step 1: Add rendering-state tests for card visibility and hierarchy** + +Add tests for: +- `test_bot_timeline_card_visible_for_bot_text_message` +- `test_bot_timeline_card_hidden_for_regular_user_message` +- `test_bot_status_strip_renders_above_body_and_not_inside_body` +- `test_bot_metadata_footer_renders_below_body` + +- [ ] **Step 2: Run the rendering-state tests and confirm they fail** + +Run: + +```bash +cargo test test_bot_timeline_card_visible_for_bot_text_message +cargo test test_bot_timeline_card_hidden_for_regular_user_message +cargo test test_bot_status_strip_renders_above_body_and_not_inside_body +cargo test test_bot_metadata_footer_renders_below_body +``` + +- [ ] **Step 3: Add bot-specific card widgets to the message templates** + +In `src/home/room_screen.rs`: +- extend `Message` with a bot-only card container around the message body +- add optional `status strip` and `metadata footer` regions +- keep the existing username row and `bot` badge +- keep the main reply body rendered through `HtmlOrPlaintext` +- make sure ordinary user messages still use the plain timeline path + +- [ ] **Step 4: Populate the new bot card subviews** + +Update the timeline population path so that: +- bot-authored text/notice messages use the parsed layers +- main body text is sent to `HtmlOrPlaintext` +- provider/footer text is routed to the lighter metadata views +- unmatched bot messages fall back cleanly without partial junk UI + +- [ ] **Step 5: Re-run the rendering-state tests** + +Run the same four tests and confirm they pass. + +--- + +### Task 3: Preserve Reply Preview, Condensed Layout, and Final Rendering Semantics + +**Files:** +- Modify: `src/home/room_screen.rs` +- Modify if needed: `src/shared/html_or_plaintext.rs` +- Modify if needed: `src/home/edited_indicator.rs` + +- [ ] **Step 1: Add regression tests for shared timeline behavior** + +Add tests for: +- `test_bot_timeline_card_body_uses_html_or_plaintext_rendering` +- `test_bot_timeline_card_preserves_reply_preview_and_condensed_layout` + +- [ ] **Step 2: Run the regression tests and confirm they fail** + +Run: + +```bash +cargo test test_bot_timeline_card_body_uses_html_or_plaintext_rendering +cargo test test_bot_timeline_card_preserves_reply_preview_and_condensed_layout +``` + +- [ ] **Step 3: Adjust spacing and supporting widget hooks only where needed** + +Make the minimal changes needed so that: +- reply preview still sits correctly above the bot card +- condensed bot messages still render a readable card body without restoring a full profile row +- edited indicator and footer do not visually compete +- `HtmlOrPlaintext` behavior remains unchanged for links, emphasis, and line breaks + +- [ ] **Step 4: Run the targeted regression suite** + +Run: + +```bash +cargo test test_bot_timeline_card_body_uses_html_or_plaintext_rendering +cargo test test_bot_timeline_card_preserves_reply_preview_and_condensed_layout +``` + +- [ ] **Step 5: Run the final verification gates** + +Run: + +```bash +cargo build +agent-spec parse specs/task-tg-bot-timeline-cards.spec.md +agent-spec lint specs/task-tg-bot-timeline-cards.spec.md --min-score 0.7 +``` + +- [ ] **Step 6: Manual GUI validation** + +Run: + +```bash +cargo run +``` + +Verify: +- bot replies read as distinct cards in mixed human/bot rooms +- status text is visually above the main reply, not inside it +- provider/model and token/latency text are visibly weaker than the answer +- long bot replies stay readable +- reply previews and condensed messages still align correctly diff --git a/docs/superpowers/plans/2026-04-13-app-service-octos-health-plan.md b/docs/superpowers/plans/2026-04-13-app-service-octos-health-plan.md new file mode 100644 index 000000000..a9997969a --- /dev/null +++ b/docs/superpowers/plans/2026-04-13-app-service-octos-health-plan.md @@ -0,0 +1,170 @@ +# App Service Octos Health Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Show an editable Octos Service URL plus a manual health-check status in `Settings > App Service`, defaulting to `http://127.0.0.1:8010`, without changing Matrix routing or bot behavior. + +**Architecture:** Persist the Octos Service base URL in `BotSettingsState`, validate it before save/probe, and keep the health-check status as widget-local UI state in `BotSettings`. The probe sequence uses `{configured_url}/health` with fallback to `{configured_url}/api/status`, and the result only updates local settings UI state. + +**Tech Stack:** Rust, Makepad 2.0 `script_mod!`, widget-local `cx.http_request` / `Event::NetworkResponses`, serde-persisted app state, existing `url` crate for URI validation. + +--- + +### Task 1: Add persisted Octos service URL + pure validation helpers + +**Files:** +- Modify: `src/app.rs` +- Test: `src/app.rs` + +- [ ] **Step 1: Write the failing state test** + +Add tests that construct default/custom `BotSettingsState` and assert: +- default service URL resolves to `http://127.0.0.1:8010` +- custom configured URL is preserved +- invalid URLs are rejected by validation helper +- health status defaults to `Unknown` +- checking flag defaults to false + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `cargo test app_service_health_defaults` +Expected: FAIL because the health state and helper do not exist yet + +- [ ] **Step 3: Add minimal state model** + +In `src/app.rs`: +- add persisted `octos_service_url` field to `BotSettingsState` +- default it to `http://127.0.0.1:8010` +- add helper to resolve empty values back to the default +- add URI validation helper for `http` / `https` + +- [ ] **Step 4: Run the test to verify it passes** + +Run: `cargo test app_service_health_defaults` +Expected: PASS + +### Task 2: Add manual health-check state machine + +**Files:** +- Modify: `src/settings/bot_settings.rs` +- Test: `src/settings/bot_settings.rs` + +- [ ] **Step 1: Write the failing behavior tests** + +Add focused tests for: +- successful `{configured_url}/health` result maps to `Reachable` +- `{configured_url}/health` failure with `/api/status` success still maps to `Reachable` +- both probe failures map to `Unreachable` +- duplicate `Check Now` presses while checking do not enqueue overlapping work + +- [ ] **Step 2: Run the tests to verify they fail** + +Run: +```bash +cargo test app_service_health_check_ +``` +Expected: FAIL because the state machine still assumes a fixed URL + +- [ ] **Step 3: Add request + action plumbing** + +In `src/settings/bot_settings.rs`: +- keep `OctosHealthStatus` / probe stage as widget-local state +- make the probe sequence depend on the currently configured base URL +- keep `Checking` / duplicate-click suppression local to the widget +- treat any HTTP 200 as reachable +- treat timeout/connect/status failures on both endpoints as unreachable + +- [ ] **Step 4: Run the tests to verify they pass** + +Run: +```bash +cargo test app_service_health_check_ +``` +Expected: PASS + +### Task 3: Render editable URL field, Save, and Check Now in BotSettings + +**Files:** +- Modify: `src/settings/bot_settings.rs` +- Modify: `resources/i18n/en.json` +- Modify: `resources/i18n/zh-CN.json` +- Test: `src/settings/bot_settings.rs` + +- [ ] **Step 1: Write the failing widget tests** + +Add tests covering: +- card shows editable URL with local default and `Unknown` +- invalid URL is rejected before probing +- opening settings does not auto-start a probe +- pressing `Check Now` sets status to `Checking` +- button is disabled or ignored while already checking + +- [ ] **Step 2: Run the tests to verify they fail** + +Run: +```bash +cargo test app_service_health_ui_ +``` +Expected: FAIL because the UI row does not exist yet + +- [ ] **Step 3: Implement the minimal UI** + +In `src/settings/bot_settings.rs`: +- add a compact status block under the existing app service toggle +- show: + - editable Octos Service input + - `Save` button + - status label + - `Check Now` button +- validate the URL before save or probe +- persist valid URL changes to `BotSettingsState` +- use widget-local `cx.http_request` probing only when not already checking +- keep all text/theme consistent with existing settings card patterns + +In i18n: +- add strings for service label, placeholder, save/check buttons, validation error, saved popup, and status labels + +- [ ] **Step 4: Run the tests to verify they pass** + +Run: +```bash +cargo test app_service_health_ui_ +``` +Expected: PASS + +### Task 4: Regression verification + +**Files:** +- Modify: `specs/task-app-service-octos-health.spec.md` only if implementation forced a wording correction + +- [ ] **Step 1: Run targeted behavior tests** + +Run: +```bash +cargo test app_service_health_defaults +cargo test app_service_health_check_ +cargo test app_service_health_ui_ +``` +Expected: PASS + +- [ ] **Step 2: Run full build** + +Run: +```bash +cargo build +``` +Expected: PASS + +- [ ] **Step 3: Manual verification** + +Check in app: +- `Settings > App Service` shows `Octos Service` +- the field defaults to `http://127.0.0.1:8010` +- editing and saving a valid remote URL persists it +- invalid URLs are rejected before probe +- initial status is `Unknown` +- opening settings alone does not trigger a check +- clicking `Check Now` moves to `Checking` +- with `{configured_url}` reachable, status becomes `Reachable` +- with `{configured_url}` unreachable, status becomes `Unreachable` +- App Service enable toggle and room bindings are unaffected diff --git a/docs/superpowers/plans/2026-04-13-mention-visible-display-text-plan.md b/docs/superpowers/plans/2026-04-13-mention-visible-display-text-plan.md new file mode 100644 index 000000000..4bb859e8f --- /dev/null +++ b/docs/superpowers/plans/2026-04-13-mention-visible-display-text-plan.md @@ -0,0 +1,272 @@ +# Visible Mention Display Text Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make selected `@mention` items render as friendly visible text in the composer, while still generating correct Matrix mention links and `Mentions` metadata when the message is sent. + +**Architecture:** Replace the current “possible mentions + final string scan” model with explicit tracked visible-mention spans owned by `MentionableTextInput`. Selection inserts visible text like `@Alice `, text edits reconcile or invalidate only the affected tracked spans, and send-time transformation resolves those spans into Matrix mention links for markdown and `/html` while leaving `/plain` untouched. + +**Tech Stack:** Rust, Makepad 2.0 `script_mod!`, `matrix-sdk`/`ruma` `Mentions`, unit tests in `src/shared/mentionable_text_input.rs`, `cargo test`, `cargo build`, `agent-spec`. + +--- + +## File Map + +- `src/shared/mentionable_text_input.rs` + - Replace `possible_mentions: BTreeMap` with a tracked visible-mention span model. + - Change popup selection insertion from raw markdown link text to visible mention text. + - Reconcile span state on `TextInputAction::Changed`. + - Resolve tracked spans into outgoing markdown/HTML mention links and `Mentions` metadata at send time. + - Host all new unit tests for span tracking, invalidation, and send transformation. + +- `src/home/editing_pane.rs` + - **No change expected for this task.** + - Keep historical edit-message handling out of scope unless implementation reveals an unavoidable shared helper extraction. + +- `specs/task-mention-visible-display-text.spec.md` + - Source-of-truth contract for behavior; update only if implementation reveals a true wording bug. + +## Key Implementation Decisions + +- Store tracked mentions as explicit byte spans over the current composer text, not as a superset map. +- Each tracked mention should carry: + - `user_id: OwnedUserId` + - `visible_text: String` (for example `@Alice`) + - `start: usize` + - `end: usize` +- Span bookkeeping should use byte indices because the existing text input utilities and cursor indices are byte-based. +- Reconciliation should be conservative: + - edits strictly before a span shift it + - edits strictly after a span leave it unchanged + - edits overlapping a span invalidate only that span +- `/plain` remains plain text and must not emit `Mentions`. +- Duplicate visible labels are valid because the stable key is `OwnedUserId + span`, not the label text. + +--- + +### Task 1: Replace Raw Markdown Insertion with Visible Mention Tokens + +**Files:** +- Modify: `src/shared/mentionable_text_input.rs` +- Test: `src/shared/mentionable_text_input.rs` + +- [ ] **Step 1: Write the failing selection tests** + +Add unit tests: +- `test_selecting_user_mention_inserts_visible_display_name` +- `test_selecting_user_mention_without_display_name_falls_back_to_localpart` + +Each test should assert: +- inserted composer text is `@DisplayName ` or `@localpart ` +- inserted composer text does not contain `matrix.to` +- cursor lands after the trailing space + +- [ ] **Step 2: Run the new selection tests and confirm they fail** + +Run: + +```bash +cargo test test_selecting_user_mention_inserts_visible_display_name +cargo test test_selecting_user_mention_without_display_name_falls_back_to_localpart +``` + +Expected: FAIL because `on_user_selected()` still inserts markdown link text. + +- [ ] **Step 3: Introduce the tracked visible-mention state model** + +In `src/shared/mentionable_text_input.rs`: +- add a small `TrackedVisibleMention` struct near the widget state definitions +- replace `possible_mentions: BTreeMap` with `tracked_visible_mentions: Vec` +- add a helper that derives visible mention text from popup selection: + - prefer display name + - fall back to MXID localpart + - always prefix with `@` + +- [ ] **Step 4: Change popup selection insertion to use visible mention text** + +Update `on_user_selected()` so that: +- selecting `@room` keeps existing `@room ` behavior +- selecting a user inserts visible text rather than markdown link syntax +- the inserted mention span is registered immediately with `start/end/user_id/visible_text` +- the cursor is moved to the end of the visible mention plus trailing space + +- [ ] **Step 5: Re-run the selection tests** + +Run: + +```bash +cargo test test_selecting_user_mention_inserts_visible_display_name +cargo test test_selecting_user_mention_without_display_name_falls_back_to_localpart +``` + +Expected: PASS. + +--- + +### Task 2: Reconcile and Invalidate Mention Spans on Composer Edits + +**Files:** +- Modify: `src/shared/mentionable_text_input.rs` +- Test: `src/shared/mentionable_text_input.rs` + +- [ ] **Step 1: Write the failing span-reconciliation tests** + +Add unit tests: +- `test_editing_inside_visible_mention_clears_tracking_for_that_mention` +- `test_duplicate_display_name_mentions_preserve_distinct_user_ids` + +Also add one focused helper test for unchanged spans shifting correctly when text is inserted before them, for example: +- `test_visible_mention_spans_shift_when_edit_happens_before_them` + +- [ ] **Step 2: Run the new reconciliation tests and confirm they fail** + +Run: + +```bash +cargo test test_editing_inside_visible_mention_clears_tracking_for_that_mention +cargo test test_duplicate_display_name_mentions_preserve_distinct_user_ids +cargo test test_visible_mention_spans_shift_when_edit_happens_before_them +``` + +Expected: FAIL because the widget currently has no span model or reconciliation helper. + +- [ ] **Step 3: Add a pure reconciliation helper** + +In `src/shared/mentionable_text_input.rs`: +- add a helper that compares `old_text` and `new_text` +- compute the changed byte window and net length delta +- return updated tracked spans by: + - shifting spans fully after the edit + - preserving spans fully before the edit + - dropping only spans overlapped by the edit + +Keep the helper pure so the tests can drive it directly. + +- [ ] **Step 4: Wire reconciliation into `handle_text_change()`** + +When text changes: +- reconcile `tracked_visible_mentions` against the new text before mention search logic continues +- clear all tracked mentions when the text becomes empty +- keep `possible_room_mention` semantics unchanged for `@room` + +- [ ] **Step 5: Re-run the reconciliation tests** + +Run: + +```bash +cargo test test_editing_inside_visible_mention_clears_tracking_for_that_mention +cargo test test_duplicate_display_name_mentions_preserve_distinct_user_ids +cargo test test_visible_mention_spans_shift_when_edit_happens_before_them +``` + +Expected: PASS. + +--- + +### Task 3: Resolve Visible Mentions into Outgoing Matrix Links and Metadata + +**Files:** +- Modify: `src/shared/mentionable_text_input.rs` +- Test: `src/shared/mentionable_text_input.rs` + +- [ ] **Step 1: Write the failing send-transformation tests** + +Add unit tests: +- `test_create_message_with_visible_mentions_emits_matrix_links_and_mentions` +- `test_html_message_with_visible_mentions_emits_anchor_and_mentions` +- `test_plain_mode_visible_mentions_remain_plain_text_without_mentions` + +These should assert: +- default markdown send path resolves visible mentions into Matrix user links +- `/html` send path emits `` using the visible label +- `/plain` keeps the visible label as plain text and emits no `Mentions` + +- [ ] **Step 2: Run the new send tests and confirm they fail** + +Run: + +```bash +cargo test test_create_message_with_visible_mentions_emits_matrix_links_and_mentions +cargo test test_html_message_with_visible_mentions_emits_anchor_and_mentions +cargo test test_plain_mode_visible_mentions_remain_plain_text_without_mentions +``` + +Expected: FAIL because send-time logic still scans for pre-rendered markdown links. + +- [ ] **Step 3: Add pure transformation helpers** + +In `src/shared/mentionable_text_input.rs`: +- add a helper that resolves the current plain composer text plus tracked spans into: + - transformed markdown text + - transformed HTML text + - `Mentions` +- make it preserve visible labels in the outgoing link text, for example `[@Alice](matrix.to...)` +- ensure duplicate visible labels resolve using span identity, not string matching + +- [ ] **Step 4: Update `create_message_with_mentions()` to use the helpers** + +Implement the minimal wiring: +- `/html`: build outgoing HTML from tracked spans, then attach `Mentions` +- default markdown: build outgoing markdown from tracked spans, then attach `Mentions` +- `/plain`: bypass mention resolution and return plain text without metadata +- remove or dead-code-eliminate the old `possible_mentions` scanning helpers + +- [ ] **Step 5: Re-run the send-transformation tests** + +Run: + +```bash +cargo test test_create_message_with_visible_mentions_emits_matrix_links_and_mentions +cargo test test_html_message_with_visible_mentions_emits_anchor_and_mentions +cargo test test_plain_mode_visible_mentions_remain_plain_text_without_mentions +``` + +Expected: PASS. + +--- + +### Task 4: Regression Verification and Manual Composer Checks + +**Files:** +- Modify: `specs/task-mention-visible-display-text.spec.md` only if implementation forces a wording correction + +- [ ] **Step 1: Run the focused mention test suite** + +Run: + +```bash +cargo test test_selecting_user_mention_inserts_visible_display_name +cargo test test_selecting_user_mention_without_display_name_falls_back_to_localpart +cargo test test_editing_inside_visible_mention_clears_tracking_for_that_mention +cargo test test_duplicate_display_name_mentions_preserve_distinct_user_ids +cargo test test_visible_mention_spans_shift_when_edit_happens_before_them +cargo test test_create_message_with_visible_mentions_emits_matrix_links_and_mentions +cargo test test_html_message_with_visible_mentions_emits_anchor_and_mentions +cargo test test_plain_mode_visible_mentions_remain_plain_text_without_mentions +``` + +Expected: PASS. + +- [ ] **Step 2: Run full compile verification** + +Run: + +```bash +cargo build +agent-spec parse specs/task-mention-visible-display-text.spec.md +agent-spec lint specs/task-mention-visible-display-text.spec.md --min-score 0.7 +``` + +Expected: PASS. + +- [ ] **Step 3: Manual verification in the app** + +Check in app: +- selecting a user from the `@mention` popup inserts `@DisplayName ` instead of raw markdown link syntax +- the input field never shows `matrix.to` after selection +- sending a default markdown message still produces a clickable Matrix mention in the timeline +- sending `/html` still produces a clickable Matrix mention in the timeline +- sending `/plain` leaves the visible mention as plain text with no structured mention behavior +- editing one selected mention token invalidates only that token, not neighboring mentions +- duplicate visible labels such as two `@Alex` mentions still notify both distinct users + diff --git a/docs/superpowers/plans/2026-04-13-tg-bot-action-buttons-plan.md b/docs/superpowers/plans/2026-04-13-tg-bot-action-buttons-plan.md new file mode 100644 index 000000000..ae2ffff13 --- /dev/null +++ b/docs/superpowers/plans/2026-04-13-tg-bot-action-buttons-plan.md @@ -0,0 +1,309 @@ +# TG Bot Action Buttons Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Render bot-supplied inline action buttons below timeline messages and send a one-shot action response back to the original bot when a user clicks one. + +**Architecture:** Implement Phase 4c with native Makepad widgets, not Splash-generated buttons. `room_screen.rs` will parse `org.octos.actions`, populate a fixed six-slot button row inside each message item, and submit a direct `MatrixRequest::SendMessage` with `target_user_id = original_sender`, `explicit_room = false`, `m.in_reply_to`, and `org.octos.action_response` custom fields. The input bar state is not consulted. + +**Tech Stack:** Makepad 2.0 `script_mod!` DSL, native `Button`/`Robrix*Button` widgets, Matrix `RoomMessageEventContent`, existing `submit_async_request(MatrixRequest::SendMessage)` send path, project i18n popup notifications. + +--- + +## File Map + +- Modify: `src/home/room_screen.rs` + - Add action-button DSL nodes to `Message` and `CondensedMessage` + - Parse `org.octos.actions` from original event JSON + - Populate per-message button rows + - Handle button clicks, local disable/re-enable, and one-shot action-response sending +- Modify: `resources/i18n/en.json` + - Add error/fallback strings for action response failure and accessibility label text +- Modify: `resources/i18n/zh-CN.json` + - Add Chinese translations for the same strings + +## Implementation Notes + +- Use a fixed maximum of 6 buttons in the DSL. Hide unused slots instead of creating dynamic widget trees. +- Keep action buttons as a sibling of `splash_card` / `message` / `link_preview_view`; do not change message body structure. +- Parse event JSON once per populate path. If malformed entries are present, skip them and log warnings instead of failing the whole message. +- For the click path, build the outgoing `RoomMessageEventContent` directly in `room_screen.rs`. Do not route through input-bar reply state or `ResolvedTarget`. +- Use client-local disabled state keyed by `(event_id, action_id)` so double-clicks are suppressed until send success/failure resolves. + +### Task 1: Parse and Render Action Rows + +**Files:** +- Modify: `src/home/room_screen.rs` +- Test: `src/home/room_screen.rs` (existing unit-test module) + +- [ ] **Step 1: Write failing parsing/render-state tests** + +Add tests for: +- `test_parse_octos_actions_skips_malformed_entries` +- `test_parse_octos_actions_truncates_after_six` +- `test_action_buttons_render_state_hidden_without_actions` +- `test_action_buttons_render_state_with_primary_secondary_danger` + +Each test should operate on small JSON fixtures and assert a compact parsed/render-state struct, not a whole widget tree. + +- [ ] **Step 2: Run the new tests to verify RED** + +Run: +```bash +cargo test parse_octos_actions --quiet +``` + +Expected: +- The new tests fail because action parsing/render-state helpers do not exist yet. + +- [ ] **Step 3: Add minimal action model + parser** + +In `src/home/room_screen.rs`, add focused helpers like: +```rust +#[derive(Debug, Clone, PartialEq, Eq)] +struct OctosActionButton { + id: String, + label: String, + style: OctosActionStyle, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum OctosActionStyle { + Primary, + Secondary, + Danger, +} +``` + +And a parser: +```rust +fn parse_octos_actions( + original_json: &Raw, +) -> Vec +``` + +Requirements: +- Read `content.org.octos.actions` +- Accept only entries with non-empty `id` and `label` +- Default unknown/missing style to `Secondary` +- Truncate after 6 entries +- Emit warnings for malformed/truncated entries + +- [ ] **Step 4: Add DSL slots for six action buttons** + +In both `Message` and `CondensedMessage` templates, add: +- `action_buttons := View { visible: false ... }` +- six named child buttons, e.g. `action_button_0` .. `action_button_5` + +Use existing project button widgets: +- `primary`: `RobrixPositiveIconButton` +- `secondary`: `Button` +- `danger`: `RobrixNegativeIconButton` + +Keep the container below `message` / `splash_card` and above `link_preview_view`. + +- [ ] **Step 5: Populate buttons during timeline rendering** + +Extend the existing message population path so text/notice messages also call a helper like: +```rust +populate_octos_action_buttons(cx, &item, event_tl_item.original_json(), event_id); +``` + +Requirements: +- Hide the entire container when no valid actions exist +- Show only the used button slots +- Set each visible button's text label and style +- Keep unused slots hidden + +- [ ] **Step 6: Run focused tests to verify GREEN** + +Run: +```bash +cargo test parse_octos_actions --quiet +``` + +Expected: +- All new parser/render-state tests pass. + +### Task 2: Send Action Responses with One-Shot Bot Targeting + +**Files:** +- Modify: `src/home/room_screen.rs` +- Test: `src/home/room_screen.rs` (existing unit-test module) + +- [ ] **Step 1: Write failing click/send contract tests** + +Add tests for: +- `test_build_action_response_targets_original_sender` +- `test_build_action_response_preserves_reply_relation_to_source_event` +- `test_click_action_button_disables_all_buttons_locally` +- `test_action_response_failure_reenables_buttons` + +Model these around a pure helper plus a small local-state map, not around live Makepad events. + +- [ ] **Step 2: Run the tests to verify RED** + +Run: +```bash +cargo test action_response --quiet +``` + +Expected: +- Tests fail because the helper/state does not exist yet. + +- [ ] **Step 3: Add action-response builder helper** + +Add a helper in `src/home/room_screen.rs`: +```rust +fn build_octos_action_response_message( + label: &str, + action_id: &str, + source_event_id: &EventId, +) -> RoomMessageEventContent +``` + +Requirements: +- Body fallback: `[Action: {label}]` +- Custom field: +```json +{ + "org.octos.action_response": { + "action_id": "...", + "source_event_id": "$event" + } +} +``` +- Reply relation: `m.in_reply_to` to the source event + +- [ ] **Step 4: Add local disabled-state tracking** + +Add a widget-local rust field in `RoomScreen` for disabled action keys, keyed by: +```rust +(OwnedEventId, String) +``` + +Add helpers to: +- mark all actions for one source event disabled on click +- clear them on send failure +- leave them disabled on send success + +- [ ] **Step 5: Wire action-button clicks to MatrixRequest::SendMessage** + +In the `RoomScreen` action handler: +- detect which action button was clicked and recover: + - source event id + - original sender user id + - selected `action_id` + - selected label +- immediately disable all buttons for that source event +- submit: +```rust +submit_async_request(MatrixRequest::SendMessage { + room_id, + timeline_kind, + message, + reply_to: None, + target_user_id: Some(original_sender.to_owned()), + explicit_room: false, +}); +``` + +Do not consult input-bar reply state or mention parsing. + +- [ ] **Step 6: Handle send failure popup + local re-enable** + +Hook the existing Matrix send error path for this action-response request so failures: +- show popup `Failed to send action response` +- re-enable buttons for the original event + +Keep success path silent. + +- [ ] **Step 7: Run focused tests to verify GREEN** + +Run: +```bash +cargo test action_response --quiet +``` + +Expected: +- All action-response tests pass. + +### Task 3: Integrate with Existing Message Types and Verify End-to-End Behavior + +**Files:** +- Modify: `src/home/room_screen.rs` +- Modify: `resources/i18n/en.json` +- Modify: `resources/i18n/zh-CN.json` +- Test: `src/home/room_screen.rs` + +- [ ] **Step 1: Add failing integration tests** + +Add tests for: +- `test_plain_message_without_actions_keeps_action_row_hidden` +- `test_splash_card_and_actions_coexist` +- `test_action_button_label_escaped` +- `test_unknown_style_falls_back_to_secondary` + +- [ ] **Step 2: Run the tests to verify RED** + +Run: +```bash +cargo test action_buttons --quiet +``` + +Expected: +- New integration tests fail until the final wiring is complete. + +- [ ] **Step 3: Finish mixed rendering cases** + +Ensure: +- plain messages without `org.octos.actions` render exactly as before +- `org.octos.splash_card` and `org.octos.actions` can both render on one message +- action labels are escaped before setting button text / fallback body text + +- [ ] **Step 4: Add i18n strings** + +Add: +- `room_screen.action_response_failed` +- `room_screen.action_button_prefix` + +English and Chinese only; no new locales. + +- [ ] **Step 5: Run targeted tests** + +Run: +```bash +cargo test action_buttons --quiet +``` + +Expected: +- All 4c-specific tests pass. + +- [ ] **Step 6: Run broader regression checks** + +Run: +```bash +cargo test room_screen --quiet +cargo build +agent-spec parse specs/task-tg-bot-action-buttons.spec.md +agent-spec lint specs/task-tg-bot-action-buttons.spec.md --min-score 0.7 +``` + +Expected: +- `cargo build` passes +- no existing bot timeline regressions +- spec parse/lint passes + +- [ ] **Step 7: Manual verification** + +Test in app: +1. Open a room with a bot message that includes `org.octos.actions`. +2. Confirm buttons render below the message body. +3. Click a button and confirm: + - buttons disable immediately + - one reply message is sent + - it routes to the original bot +4. Force a send failure and confirm: + - error popup appears + - buttons re-enable +5. Confirm a normal message without actions does not show any button row. diff --git a/docs/superpowers/plans/2026-04-13-tg-bot-phase4-backlog.md b/docs/superpowers/plans/2026-04-13-tg-bot-phase4-backlog.md new file mode 100644 index 000000000..e00a9c3be --- /dev/null +++ b/docs/superpowers/plans/2026-04-13-tg-bot-phase4-backlog.md @@ -0,0 +1,151 @@ +# TG Bot UI Alignment — Phase 4 Backlog + +> **Date:** 2026-04-13 +> **Status:** Active planning doc. Supersedes the Phase 2 explicit-target-chip +> direction. Written after Claude+Codex synced on the current state of the +> `tg-align` branch. + +## Where we are right now + +Phase 3 (`mention/reply-first`) is merged and is the current active design. +Phases 1 and 2 type model work (`ExplicitOverride`, `ResolvedTarget`, +`is_known_or_likely_bot()`) survived the pivot; the persistent target chip UI +did not. The relevant historical docs are marked superseded: + +- `specs/task-tg-bot-explicit-targeting.spec.md` (Phase 2) — SUPERSEDED +- `docs/superpowers/plans/2026-04-11-tg-bot-architecture-review.md` — SUPERSEDED + +The authoritative active spec for the current behavior is +`specs/task-tg-bot-mention-reply-first.spec.md`. + +### What actually shipped on `tg-align` + +| Commit | Scope | +|--------|-------| +| `86eb0626` | Octos health checks, room restore handling | +| `77a8ee4c` | Mention input display names + Event Source rendering (CJK) | +| `297b34da` | Splash card rendering for bot messages | +| `4ed6ac4d` | Clippy clone_on_copy fix for Cmd+Enter handler | +| `9e00e197` | Cmd/Ctrl+Enter sends messages in multiline input | +| `c8e448d2` | File download support for Matrix file messages | +| `1a339985` | Bot timeline card rendering polish | +| `bbaa4ef0` | Matrix bot UX → mention-first routing (Phase 3 pivot) | +| `7481a007` | Explicit Matrix bot targeting UX (Phase 2 foundation) | +| `98e38b50` | Phase 1 bot badge + slash commands (WIP merged forward) | + +Also on the OctOS side: `3370cb7` added bidirectional media support to the +Matrix appservice channel (file/image/audio/video upload+download). + +### Behavior summary + +- **Multi-member bot-bound rooms:** default to `ExplicitRoom` (room-first). + Plain messages go to the room; `explicit_room` flag suppresses OctOS fallback. +- **DMs (direct rooms):** default to `ExplicitBot(bound_bot_user_id)` so users + can chat with a bot without @mentioning it every line. +- **Reply-to-bot:** always resolves to `ReplyBot(bot_user_id)` regardless of + override state. +- **Reply-to-human:** never triggers bot targeting, even in a bot-bound room. +- **Persisted `ExplicitOverride`:** discarded on restore, since the chip UI + that would have let users correct it no longer exists. +- **Target chip UI:** hidden. The DSL scaffolding (`target_indicator`, + `TargetChipButton`, etc.) remains in `room_input_bar.rs` as dead code. + +## P0 — Historical residue to clean up + +These are mechanical cleanups. They do not change behavior, but leaving them +misleads reviewers and future agents into thinking the target chip is still +on the roadmap. + +| File | Action | +|------|--------| +| `src/room/room_input_bar.rs` | Delete `TargetChipPresentation` struct, `#[cfg(test)] fn format_target_chip_presentation`, `target_chip_button` / `target_chip_dismiss_button` / `target_indicator` DSL blocks, empty `sync_target_indicator()` stub | +| `src/shared/mentionable_text_input.rs:1404` | Replace `if false /* TODO: add is_direct_room to RoomScreenProps */` with `if room_props.is_direct_room` — the field already exists and is used by `resolve_target` | +| `specs/task-tg-bot-explicit-targeting.spec.md` | Already marked superseded in frontmatter banner | +| `docs/superpowers/plans/2026-04-11-tg-bot-architecture-review.md` | Already marked superseded in banner | + +## P1 — Pre-existing test failures (not blocking) + +Two `room_input_bar` tests fail both with and without any current change +on `tg-align`, so they are not regressions from Phase 3: + +- `test_message_bot_mention_suppresses_explicit_bot_target` +- `test_room_bot_mention_overrides_selected_explicit_bot` + +Both expect `routing_directives_for_message(ExplicitBot, mentions_bot=true)` +to return `(None, false)`, but the function currently returns `(None, true)` +because of `explicit_room = target_user_id.is_none()`. Either the test +expectation or the function semantics is wrong; the team should decide which +and reconcile. Low priority — no user-visible impact, since mention-first +flow already keeps the `explicit_room` flag on mention messages. + +## P2 — Next TG parity work (new specs to write) + +These are the three things Codex flagged as the real remaining TG alignment +work after the mention/reply-first pivot. Each will get its own spec. + +### Phase 4a: Bot menu button + pure command send-on-select + +- **Problem:** The current entry point for bot commands is the local `/bot` + shortcut text command. That is a power-user affordance, not a TG-style + menu button, and it does not match the user's mental model of "click the + bot icon to see what I can do." +- **Also:** Pure commands like `/listbots` and `/bothelp` currently insert + the command text into the input and wait for the user to press enter. + TG sends these immediately on select. +- **Spec file:** `specs/task-tg-bot-menu-button.spec.md` (Phase 4a) +- **Key design questions to lock down in the spec:** + - Where does the menu button live? (next to the input bar? inside it?) + - How is "pure" vs "parameterized" classified? (new field on + `SlashCommand`? a hardcoded list?) + - Does the menu button work in all bot-bound rooms or only DMs? + - Does clicking the menu button open the same command popup that typing + `/` already opens, or a distinct UI? + +### Phase 4b: `/command@bot` explicit addressing + +- **Problem:** In multi-bot rooms, `/listbots` is ambiguous — which bot does + it target? TG uses `/command@BotName` syntax to disambiguate. +- **Spec file:** `specs/task-tg-bot-command-at-addressing.spec.md` (Phase 4b) +- **Key design questions:** + - Parser: where does the `@bot` suffix parsing live? In + `mentionable_text_input` or a shared utility? + - Routing: does the parsed target become an `ExplicitBot` override, a + one-shot `target_user_id` on the outgoing message, or both? + - UI: does typing `/command@` trigger a bot-name autocomplete? + - Fallback: in a single-bot room, is the `@bot` suffix optional? + +### Phase 4c: Bot message action buttons / inline keyboard UX + +- **Problem:** Bots currently communicate only through plain text + the + new Splash card prototype. They have no way to present "click to confirm", + "click to retry file generation", or similar action affordances. The + real-world PPT regeneration incidents show this is painful. +- **Spec file:** `specs/task-tg-bot-action-buttons.spec.md` (Phase 4c) +- **Key design questions:** + - Transport: how does a bot attach action data to a Matrix message? + (new custom field like `org.octos.actions`? reuse Splash card for this?) + - Rendering: inline keyboard (buttons under the message) vs reply keyboard + (buttons replacing the input area) — we should start with inline. + - Click handler: when a user clicks a button, what gets sent back? A new + Matrix message with a `org.octos.action_response` field? A direct HTTP + callback to OctOS? + - Integration with Splash cards: the Splash renderer already exists; should + action buttons just be clickable widgets inside a Splash body, or a + separate concept? + +## Division of labor + +Based on Codex's handoff (2026-04-12T23:04:47Z session) and the fact that +Codex is more familiar with `room_input_bar.rs` DSL internals: + +| Domain | Owner | +|--------|-------| +| Spec/product direction, backlog maintenance | Claude | +| Cleanup of historical residue (P0) | Codex | +| Implementation of Phase 4a/4b/4c | Codex | +| Spec review after each phase ships | Claude (via mempal peek) | + +Claude's next action: write the three Phase 4 specs listed above, in order. + +Codex's next action (once Claude's spec lands): execute P0 cleanup, then +start Phase 4a implementation against the new spec. diff --git a/docs/superpowers/plans/2026-04-14-fix-mobile-appservice-persistence.md b/docs/superpowers/plans/2026-04-14-fix-mobile-appservice-persistence.md new file mode 100644 index 000000000..fd0043e8b --- /dev/null +++ b/docs/superpowers/plans/2026-04-14-fix-mobile-appservice-persistence.md @@ -0,0 +1,355 @@ +# Fix Mobile App Service Binding Persistence Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fix issue #94 by ensuring `RestoreAppStateFromPersistentState` is dispatched whenever a successfully loaded `AppState` contains meaningful persisted content, so non-dock persisted fields (`selected_room`, `bot_settings`, `app_language`, `translation`) survive force-quit + relaunch on mobile while fresh installs remain a no-op. + +**Architecture:** The persistence save path is already correct — the bug is primarily a single `if`-guard at `src/sliding_sync.rs::handle_load_app_state` that scopes the entire restore dispatch behind non-empty dock state. Replace that guard with a content-aware predicate; the existing restore match arm in `src/app.rs` already handles empty-dock state correctly via full `AppState` replacement. Also keep the Settings page hydrated from `Scope` so a page opened before the async restore action is processed updates once restored state arrives. Add serde round-trip tests, direct restore-gate tests, and UI hydrate predicate tests as regression guards. + +**Tech Stack:** Rust, Makepad 2.0, matrix-sdk, serde/serde_json, tokio. No new dependencies. + +**Spec:** `specs/task-fix-mobile-appservice-persistence.spec.md` (passes `agent-spec lint --min-score 0.7`; latest observed quality 93%). + +**Commit policy:** CLAUDE.md forbids committing before the user has tested. Android manual verification passed on 2026-04-29. All intermediate work is left uncommitted; a single commit at the end bundles the spec + plan + implementation + test + issue update. + +--- + +## File Structure + +| Path | Action | Responsibility | +|---|---|---| +| `specs/task-fix-mobile-appservice-persistence.spec.md` | already written | Task Contract; the source of truth for "what done looks like" | +| `docs/superpowers/plans/2026-04-14-fix-mobile-appservice-persistence.md` | this file | Implementation plan for the engineer | +| `src/sliding_sync.rs` | modify | Replace the dock-state guard in `handle_load_app_state` with a content-aware restore gate, update log message, add gate tests | +| `src/app.rs` | modify (tests only) | Add regression unit test inside existing `#[cfg(test)] mod tests` block (near line 2570+). No production code change in app.rs. | +| `src/settings/bot_settings.rs` | modify | Re-hydrate visible Settings UI from restored `AppState`; add predicate tests | +| `issues/009-mobile-appservice-binding-not-persisted.md` | modify | Append "Fix Applied" section documenting the one-line fix and the regression test | + +## Test Strategy + +- **Unit test** (machine-verifiable): `test_app_state_roundtrip_preserves_bot_settings_with_empty_dock` — constructs an `AppState` with populated `bot_settings` and empty dock; asserts serde_json round-trip preserves all three App Service fields. Runs under `cargo test -p robrix`. +- **Manual test** (user verification): Android force-quit + relaunch per issue #94 reproduction steps. User owns this step. + +## TDD Discipline + +The unit test is written FIRST, confirmed to FAIL against the current guarded code (proving it guards against the right bug), then the production fix is applied, then the test confirms GREEN. Red → Green → Commit. + +Actually, a subtle point: because the unit test operates on the `AppState` serde contract (not on `handle_load_app_state`), it would pass against the current buggy code too — serde is innocent; the bug is in the dispatch guard. So: +- The unit test is a **regression guard for the serde contract** — it protects against a future regression where someone adds `#[serde(skip)]` to `bot_settings` (which would silently break persistence the same way). +- The real "red test" is the **manual mobile reproduction** described in the spec. +- We still follow TDD order (test first) so the test exists before the code it guards. + +--- + +## Task 1: Regression Unit Test for Serde Round-Trip + +**Files:** +- Modify: `src/app.rs` — inside the existing `#[cfg(test)] mod tests` block (search for `mod tests` near line 2568 or `use super::{BotSettingsState, RoomBotBindingState, SavedDockState, SelectedRoom};` near line 2570) + +- [ ] **Step 1.1: Locate the test module and existing imports** + +Run: `grep -n "use super::" src/app.rs | head -5` + +Expected output: a line like `2570: use super::{BotSettingsState, RoomBotBindingState, SavedDockState, SelectedRoom};`. Note the exact line so the new test lands in the right scope. + +- [ ] **Step 1.2: Extend the test module's `use super::` import** + +Find the line `use super::{BotSettingsState, RoomBotBindingState, SavedDockState, SelectedRoom};` and add `AppState` to the imported items so the new test can construct an `AppState` directly. + +Before: +```rust +use super::{BotSettingsState, RoomBotBindingState, SavedDockState, SelectedRoom}; +``` + +After: +```rust +use super::{AppState, BotSettingsState, RoomBotBindingState, SavedDockState, SelectedRoom}; +``` + +- [ ] **Step 1.3: Add the regression test at the end of the test module** + +Locate the closing `}` of the `mod tests` block (the last `}` in the file that closes a `#[cfg(test)] mod tests {`). Insert the following test just before that closing brace. + +```rust + /// Regression test for issue #94: mobile app service binding must survive force-quit + relaunch. + /// + /// The production bug was in sliding_sync.rs's load-side guard, but this test protects + /// the underlying serde contract: if a future change adds `#[serde(skip)]` to + /// bot_settings (or reorders fields in a breaking way), this test fails before users hit + /// the bug on mobile. + #[test] + fn test_app_state_roundtrip_preserves_bot_settings_with_empty_dock() { + let mut state = AppState::default(); + state.bot_settings.enabled = true; + state.bot_settings.botfather_user_id = "@octosbot:example.com".to_string(); + state.bot_settings.octos_service_url = "http://192.168.5.12:8010".to_string(); + assert!(state.saved_dock_state_home.open_rooms.is_empty(), + "precondition: this test simulates the mobile / fresh-desktop case with empty dock"); + assert!(state.saved_dock_state_home.dock_items.is_empty(), + "precondition: this test simulates the mobile / fresh-desktop case with empty dock"); + + let serialized = serde_json::to_string(&state) + .expect("AppState must serialize via serde_json"); + let deserialized: AppState = serde_json::from_str(&serialized) + .expect("serialized AppState must deserialize back"); + + assert!(deserialized.bot_settings.enabled, + "bot_settings.enabled must survive the round-trip (issue #94 regression guard)"); + assert_eq!(deserialized.bot_settings.botfather_user_id, "@octosbot:example.com", + "botfather_user_id must survive the round-trip (issue #94 regression guard)"); + assert_eq!(deserialized.bot_settings.octos_service_url, "http://192.168.5.12:8010", + "octos_service_url must survive the round-trip (issue #94 regression guard)"); + } +``` + +- [ ] **Step 1.4: Confirm the test compiles and passes against current (buggy) code** + +Run: `cargo test -p robrix --lib test_app_state_roundtrip_preserves_bot_settings_with_empty_dock -- --nocapture` + +Expected: PASS. The serde layer is innocent — this test exists to catch future breakage, not to drive the current fix. Passing now is correct and expected. + +If the test FAILS at this step: STOP. Either (a) `AppState::default()` does not exist and the test module needs `use super::AppState` re-checked, or (b) a `#[serde(skip)]` was silently added to `bot_settings` in the past — investigate before proceeding. + +- [ ] **Step 1.5: Do NOT commit yet** + +CLAUDE.md forbids committing before user testing. The test stays staged-in-worktree until Task 4. + +--- + +## Task 2: Remove the Load-Side Guard in handle_load_app_state + +**Files:** +- Modify: `src/sliding_sync.rs` — function `handle_load_app_state` (~lines 4958-4990) + +- [ ] **Step 2.1: Read the current function in full to confirm the target lines** + +Run: `grep -n "fn handle_load_app_state" src/sliding_sync.rs` + +Expected: single hit at around line 4958. Confirm the match arm structure: +```rust +match load_app_state(&user_id).await { + Ok(app_state) => { + if !app_state.saved_dock_state_home.open_rooms.is_empty() + && !app_state.saved_dock_state_home.dock_items.is_empty() + { + log!("Loaded room panel state from app data directory. Restoring now..."); + Cx::post_action(AppStateAction::RestoreAppStateFromPersistentState(Box::new(app_state))); + } + } + Err(_e) => { ... } +} +``` + +- [ ] **Step 2.2: Replace the dock-only guard with a content-aware restore gate** + +Replace this block: + +```rust + Ok(app_state) => { + if !app_state.saved_dock_state_home.open_rooms.is_empty() + && !app_state.saved_dock_state_home.dock_items.is_empty() + { + log!("Loaded room panel state from app data directory. Restoring now..."); + Cx::post_action(AppStateAction::RestoreAppStateFromPersistentState(Box::new(app_state))); + } + } +``` + +With: + +```rust + Ok(app_state) => { + if should_restore_loaded_app_state(&app_state) { + log!("Loaded app state from persistent storage. Restoring now..."); + Cx::post_action(AppStateAction::RestoreAppStateFromPersistentState(Box::new(app_state))); + } + } +``` + +Add the helper above `handle_load_app_state`: + +```rust +fn should_restore_loaded_app_state(app_state: &crate::app::AppState) -> bool { + fn saved_dock_state_has_content(saved: &crate::app::SavedDockState) -> bool { + !saved.open_rooms.is_empty() + || !saved.dock_items.is_empty() + || !saved.room_order.is_empty() + || saved.selected_room.is_some() + } + + app_state.selected_room.is_some() + || saved_dock_state_has_content(&app_state.saved_dock_state_home) + || app_state.saved_dock_state_per_space.values().any(saved_dock_state_has_content) + || app_state.bot_settings != crate::app::BotSettingsState::default() + || app_state.app_language != crate::i18n::AppLanguage::default() + || app_state.translation != crate::room::translation::TranslationConfig::default() +} +``` + +Do not use the unconditional restore form below; it was rejected because `load_app_state` returns `Ok(AppState::default())` for fresh installs and corrupt-file fallback: + +```rust + Ok(app_state) => { + log!("Loaded app state from persistent storage. Restoring now..."); + Cx::post_action(AppStateAction::RestoreAppStateFromPersistentState(Box::new(app_state))); + } +``` + +Rationale: the restore match arm in `src/app.rs:1071-1095` already performs a full `AppState` replacement and dispatches `MainDesktopUiAction::LoadDockFromAppState` unconditionally. Empty dock state is already handled safely downstream, but the all-default state from a fresh install should remain a no-op. + +- [ ] **Step 2.3: Confirm no other fields in the `Err` arm reference the removed guard symbols** + +Run: `grep -n "saved_dock_state_home" src/sliding_sync.rs` + +Expected: zero matches inside `handle_load_app_state` after the edit. If any remain in the `Err` arm or surrounding code, re-read and only keep references that are unrelated to the guard. + +- [ ] **Step 2.4: `cargo build` to catch any compile errors from the edit** + +Run: `cargo build -p robrix 2>&1 | tail -30` + +Expected: clean build. If an unused-import warning fires for `AppStateAction` or similar, inspect — it likely means the edit collapsed the only remaining reference. Unlikely since the action is still dispatched. + +- [ ] **Step 2.5: Re-run the regression unit test to confirm no side-effects** + +Run: `cargo test -p robrix --lib test_app_state_roundtrip_preserves_bot_settings_with_empty_dock -- --nocapture` + +Expected: PASS. The test is orthogonal to this change (it tests serde, not sliding_sync) — this re-run is a sanity check that the edit didn't break the test module's compilation. + +- [ ] **Step 2.6: Do NOT commit yet** + +Same reason as Task 1 — wait for user testing. + +--- + +## Task 3: Verification Pipeline + +**Files:** none modified — verification only. + +- [ ] **Step 3.1: Full workspace build** + +Run: `cargo build 2>&1 | tail -20` + +Expected: clean build with no new warnings introduced by our edits. + +- [ ] **Step 3.2: Full test suite for the robrix package** + +Run: `cargo test -p robrix --lib 2>&1 | tail -40` + +Expected: all tests PASS, including our new `test_app_state_roundtrip_preserves_bot_settings_with_empty_dock`. Look specifically for the summary line `test result: ok. N passed; 0 failed`. + +If any test unrelated to our change FAILS, STOP — it may indicate pre-existing breakage on the branch. Run `git stash && cargo test -p robrix --lib` to confirm the test failed before our edits, then `git stash pop`. Report back rather than guessing. + +- [ ] **Step 3.3: agent-spec lifecycle against the task spec** + +Run: +```bash +agent-spec lifecycle specs/task-fix-mobile-appservice-persistence.spec.md \ + --code . \ + --change-scope worktree \ + --format json \ + --run-log-dir .agent-spec/runs 2>&1 | tail -60 +``` + +Expected behavior per scenario: +- `test_app_state_roundtrip_preserves_bot_settings_with_empty_dock` → verdict `pass` (cargo test bound) +- Six `manual_test_*` scenarios → verdict `skip` (no bound test; they are manual) + +`skip` is not a failure — it is a distinct verdict. The spec explicitly marks these with `Level: manual` to communicate that they need human verification. + +- [ ] **Step 3.4: agent-spec guard (repo-wide safety check)** + +Run: `agent-spec guard --spec-dir specs --code . --change-scope worktree 2>&1 | tail -30` + +Expected: no spec reports a boundary violation or failed scenario for our change set. This confirms the edits stay within `src/sliding_sync.rs` and `src/app.rs`'s test module per the spec's `Allowed Changes`. + +--- + +## Task 4: Update Issue Doc and Prepare for User Verification + +**Files:** +- Modify: `issues/009-mobile-appservice-binding-not-persisted.md` + +- [ ] **Step 4.1: Replace the `## Fix Applied` section** + +Find the line `## Fix Applied` followed by `None yet.` in `issues/009-mobile-appservice-binding-not-persisted.md` and replace that two-line block with: + +```markdown +## Fix Applied + +**Root cause confirmed**: `src/sliding_sync.rs::handle_load_app_state` gated the entire `RestoreAppStateFromPersistentState` dispatch behind a non-empty dock-state check: + +```rust +if !app_state.saved_dock_state_home.open_rooms.is_empty() + && !app_state.saved_dock_state_home.dock_items.is_empty() +{ ... Cx::post_action(RestoreAppStateFromPersistentState ...) ... } +``` + +On mobile there is no dock, so every restart silently dropped the loaded `selected_room`, `bot_settings`, `app_language`, and `translation`. Desktop masked the bug because dock state is almost always non-empty after first run. + +**Fix**: dispatch `RestoreAppStateFromPersistentState` whenever `load_app_state` succeeds with meaningful non-default persisted content. Pure default state from a fresh install remains a no-op. The restore match arm in `src/app.rs` already performs a full `AppState` replacement and dispatches `LoadDockFromAppState` — empty dock is safely handled downstream. + +**Settings UI hydration**: do not depend on mobile app swipe-away / force-quit to save state; those lifecycle events are not guaranteed. App Service state is saved immediately on Save / Check Now / toggle. `BotSettings` also re-hydrates from `Scope` when restored settings arrive after the page was already opened. + +**Regression guard**: `src/app.rs` unit tests assert the serde contract, `src/sliding_sync.rs` unit tests assert the restore gate behavior directly, and `src/settings/bot_settings.rs` unit tests assert the UI hydrate predicate. + +**Spec**: `specs/task-fix-mobile-appservice-persistence.spec.md` (agent-spec Task Contract, quality 93%). +``` + +- [x] **Step 4.2: Update Status header** + +At the top of the issue file, status now says Android is verified and the fix is ready for review, with iOS called out as not separately verified. + +- [ ] **Step 4.3: Stage the fix for user testing** + +Do NOT run `git commit`. Present the work to the user with: +- Files changed (`git status` output) +- Exactly how to verify on Android: + 1. `cargo makepad android run -p robrix --release` to Android emulator/device + 2. Login, go to Settings → Labs → App Service + 3. Enable, fill both fields, Save → success popup + Reachable + 4. `adb shell am force-stop dev.makepad.robrix` + 5. Relaunch → expect the fields populated and Check Now → Reachable + +- [x] **Step 4.4: Wait for user confirmation before committing** + +Per CLAUDE.md feedback memory `feedback_no_co_authored_by`, the final commit message must omit the `Co-Authored-By: Claude` trailer — the project's commit-msg hook rejects it. + +Per CLAUDE.md's "do NOT commit without user testing" rule, the final commit only runs after the user says it works on Android. User confirmed the Android force-quit + relaunch path works on 2026-04-29. + +When the user approves, stage and commit all artifacts together: + +```bash +git add \ + specs/task-fix-mobile-appservice-persistence.spec.md \ + docs/superpowers/plans/2026-04-14-fix-mobile-appservice-persistence.md \ + src/sliding_sync.rs \ + src/app.rs \ + issues/009-mobile-appservice-binding-not-persisted.md + +git commit -m "fix(persistence): restore non-dock app state on relaunch (#94) + +handle_load_app_state previously gated RestoreAppStateFromPersistentState +behind non-empty dock state, which silently dropped bot_settings, +app_language, and translation on every mobile relaunch. Unconditionally +dispatch the restore action when load_app_state succeeds; the restore +match arm already handles empty dock correctly. + +Add serde round-trip unit test as a regression guard for future +#[serde(skip)] additions that could re-introduce the same failure mode. + +Spec: specs/task-fix-mobile-appservice-persistence.spec.md +Plan: docs/superpowers/plans/2026-04-14-fix-mobile-appservice-persistence.md +Closes #94" +``` + +Do NOT push without the user's explicit instruction (see feedback_no_auto_merge). + +--- + +## Self-Review Checklist + +- [x] **Spec coverage**: every scenario in the spec has a task that implements it (Task 1 covers the unit-test scenario; Task 2 delivers the behavior the six manual scenarios verify; Task 3 runs `agent-spec lifecycle` to bind them). +- [x] **Placeholder scan**: no "TBD", no "implement later", no "add error handling as needed". Every code block shows exact before/after content. +- [x] **Type consistency**: `AppStateAction::RestoreAppStateFromPersistentState` and `Box::new(app_state)` are used identically across Task 2 and the restore match arm it relies on. The test imports `AppState, BotSettingsState, ...` and uses `.bot_settings.enabled` consistent with `src/app.rs:2061`. +- [x] **Commit policy respected**: one final commit at end-of-plan, gated on user Android verification, omitting `Co-Authored-By: Claude`. +- [x] **Boundaries respected**: edits live in `src/sliding_sync.rs` (production) and `src/app.rs` (tests only) plus doc/spec/issue updates — exactly matches the spec's `Allowed Changes`. diff --git a/docs/superpowers/plans/2026-04-14-octos-approval-policy-config-plan.md b/docs/superpowers/plans/2026-04-14-octos-approval-policy-config-plan.md new file mode 100644 index 000000000..10a949d18 --- /dev/null +++ b/docs/superpowers/plans/2026-04-14-octos-approval-policy-config-plan.md @@ -0,0 +1,329 @@ +# Octos Approval Policy Configuration Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a native `approval_policy` config in Octos so approval-required tool calls are driven by first-class configuration instead of external hook scripts. + +**Architecture:** Keep the existing approval-request protocol and runtime, but introduce a new config layer that decides when to enter that approval path. `tool_policy.deny` remains the hard-stop gate, `approval_policy` becomes the primary approval trigger for v1, and hooks remain available only for advanced deny/modify/logging cases. + +**Tech Stack:** Rust, serde config deserialization, Octos CLI config/runtime, Octos agent execution path, Matrix approval-request protocol, `cargo test`, `cargo build`, `agent-spec`. + +--- + +## File Map + +- `specs/task-octos-approval-policy-config.spec.md` + - Source-of-truth contract for config-driven approval rules. + +- `../../octos/crates/octos-cli/src/config.rs` + - Add config structs for `approval_policy` and validation hooks. + +- `../../octos/crates/octos-cli/src/commands/chat.rs` + - Pass native approval-policy config into the agent/session runtime. + +- `../../octos/crates/octos-cli/src/commands/gateway/profile_factory.rs` + - Ensure gateway-created child sessions inherit the resolved approval policy from profile config. + +- `../../octos/crates/octos-cli/src/session_actor.rs` + - No new protocol here if possible; only accept resolved pending approval drafts from the agent/runtime. + +- `../../octos/crates/octos-agent/src/approval.rs` + - Extend runtime approval types with config-driven rule matching and request shaping helpers. + +- `../../octos/crates/octos-agent/src/agent/execution.rs` + - Evaluate `tool_policy.deny` then `approval_policy` before tool execution. + +- `../../octos/book/src/configuration.md` +- `../../octos/book/src/advanced.md` + - Document the new `approval_policy` field and how it relates to hooks. + +## Key Decisions To Preserve + +- Do not move approval truth into Robrix. +- Do not require external Python/shell hooks for the default approval path. +- Keep v1 matching by tool name only. +- `tool_policy.deny` must override `approval_policy`. +- `approval_policy.rules` are first-match-wins. +- Empty `authorized_approvers` must fail closed at config validation time. +- Reuse the existing `org.octos.approval_request` / `approval_response` protocol; do not fork it. +- Do not add new cargo dependencies. + +### Task 1: Add Config Types and Validation + +**Files:** +- Modify: `../../octos/crates/octos-cli/src/config.rs` +- Test: `../../octos/crates/octos-cli/src/config.rs` + +- [ ] **Step 1: Write failing config tests** + +Add focused tests: +- `test_config_deserializes_approval_policy` +- `test_config_rejects_approval_policy_with_empty_authorized_approvers` +- `test_config_rejects_approval_policy_with_require_approval_false` +- `test_config_rejects_approval_policy_with_empty_tools` + +- [ ] **Step 2: Run the new config tests and confirm they fail** + +Run: + +```bash +cargo test -p octos-cli approval_policy --quiet +``` + +Expected: FAIL because `approval_policy` does not exist in config yet. + +- [ ] **Step 3: Add config structs** + +In `config.rs`, add: +- `ApprovalPolicyConfig` +- `ApprovalRuleConfig` +- `ApprovalPolicyDefault` +- `ApprovalPolicyRiskLevel` +- `ApprovalPolicyTimeoutBehavior` + +Use serde derives only; no new dependency. + +- [ ] **Step 4: Add config validation helpers** + +Validate: +- `rules[*].tools` non-empty +- `rules[*].authorized_approvers` non-empty +- `require_approval == true` +- `expires_in_secs > 0` +- `default == "allow"` + +Return descriptive config-load errors. + +- [ ] **Step 5: Re-run config tests** + +Run: + +```bash +cargo test -p octos-cli approval_policy --quiet +``` + +Expected: PASS. + +### Task 2: Add Approval Policy Matching Runtime + +**Files:** +- Modify: `../../octos/crates/octos-agent/src/approval.rs` +- Modify: `../../octos/crates/octos-agent/src/lib.rs` +- Test: `../../octos/crates/octos-agent/src/approval.rs` + +- [ ] **Step 1: Write failing approval-policy matching tests** + +Add focused tests: +- `test_approval_policy_first_match_wins` +- `test_approval_policy_non_matching_tool_returns_none` +- `test_approval_policy_generates_relative_expiry` +- `test_approval_policy_shell_call_emits_pending_approval` + +- [ ] **Step 2: Run the new approval matching tests and confirm they fail** + +Run: + +```bash +cargo test -p octos-agent approval_policy --quiet +``` + +Expected: FAIL because rule matching helpers do not exist yet. + +- [ ] **Step 3: Add runtime policy matcher** + +In `approval.rs`, add a small matcher that: +- takes tool name + current time + config rules +- returns `None` for no match +- returns a `PendingApprovalDraft` or equivalent request spec when matched +- computes `expires_at = created_at + expires_in_secs` + +- [ ] **Step 4: Keep digest generation in runtime** + +Ensure `tool_args_digest` is still computed from actual tool args at runtime, not from config. + +- [ ] **Step 5: Re-run approval matching tests** + +Run: + +```bash +cargo test -p octos-agent approval_policy --quiet +``` + +Expected: PASS. + +### Task 3: Wire Approval Policy into Agent Execution + +**Files:** +- Modify: `../../octos/crates/octos-agent/src/agent/execution.rs` +- Test: `../../octos/crates/octos-agent/src/agent/execution.rs` + +- [ ] **Step 1: Write failing execution-order tests** + +Add focused tests: +- `test_tool_policy_deny_overrides_approval_policy` +- `test_approval_policy_creates_pending_approval_before_tool_execution` +- `test_approval_policy_non_matching_tool_executes_normally` +- `test_approval_policy_does_not_require_hook_exit_3` + +- [ ] **Step 2: Run the new execution tests and confirm they fail** + +Run: + +```bash +cargo test -p octos-agent execution approval_policy --quiet +``` + +Expected: FAIL because execution path does not consult native `approval_policy` yet. + +- [ ] **Step 3: Evaluate deny before approval** + +In `execution.rs`: +- keep existing `tool_policy.deny` behavior first +- if denied, stop immediately and do not create pending approval + +- [ ] **Step 4: Evaluate approval policy before running the tool** + +If a rule matches: +- create pending approval draft +- do not execute the tool +- return the approval-request outcome + +If no rule matches: +- continue current execution path unchanged + +- [ ] **Step 5: Keep hook compatibility narrow** + +Do not require `before_tool_call` to emit approval JSON anymore for the common path. +Hooks may still: +- deny +- modify args +- log/observe + +- [ ] **Step 6: Re-run execution tests** + +Run: + +```bash +cargo test -p octos-agent execution approval_policy --quiet +``` + +Expected: PASS. + +### Task 4: Pass Approval Policy Through CLI/Gateway Config + +**Files:** +- Modify: `../../octos/crates/octos-cli/src/commands/chat.rs` +- Modify: `../../octos/crates/octos-cli/src/commands/gateway/profile_factory.rs` +- Potentially modify: `../../octos/crates/octos-cli/src/session_actor.rs` +- Test: corresponding unit tests near config/runtime wiring + +- [ ] **Step 1: Identify where agent/session runtime receives tool policy today** + +Read the current config-to-runtime wiring in `chat.rs` and gateway profile factory. + +- [ ] **Step 2: Add approval policy plumbing alongside tool policy** + +Pass resolved `approval_policy` into the runtime/agent config using the same pattern as `tool_policy`. + +- [ ] **Step 3: Add a gateway-focused failing test** + +Add: +- `test_gateway_runtime_emits_matrix_approval_request_from_policy` + +Use a minimal config/profile fixture that includes `approval_policy`. + +- [ ] **Step 4: Run the gateway test and confirm it fails** + +Run: + +```bash +cargo test -p octos-cli gateway approval_policy --quiet +``` + +Expected: FAIL until runtime wiring is complete. + +- [ ] **Step 5: Make the test pass** + +Keep `session_actor.rs` protocol behavior unchanged if possible; it should just receive pending approvals from the agent path exactly as before. + +- [ ] **Step 6: Re-run the gateway-focused tests** + +Run: + +```bash +cargo test -p octos-cli gateway approval_policy --quiet +``` + +Expected: PASS. + +### Task 5: Document Config-Driven Approval + +**Files:** +- Modify: `../../octos/book/src/configuration.md` +- Modify: `../../octos/book/src/advanced.md` + +- [ ] **Step 1: Document the new config block in `configuration.md`** + +Add: +- field description +- JSON example +- note that v1 matches by tool name only + +- [ ] **Step 2: Update `advanced.md` to reposition hooks** + +Clarify: +- hooks can still deny/modify/log +- native `approval_policy` is now the default approval mechanism +- exit code `3` remains available for advanced dynamic cases, but is no longer required for normal approval setup + +- [ ] **Step 3: Sanity-check docs for consistency** + +Search for conflicting statements about hooks being the only approval path. + +Run: + +```bash +rg -n "approval|before_tool_call|exit code 3" ../../octos/book/src -S +``` + +Expected: docs consistently describe native config-driven approval. + +### Task 6: Full Verification + +**Files:** +- No code changes; verification only + +- [ ] **Step 1: Run focused Octos config tests** + +```bash +cargo test -p octos-cli approval_policy --quiet +``` + +- [ ] **Step 2: Run focused Octos agent approval tests** + +```bash +cargo test -p octos-agent approval --quiet +``` + +- [ ] **Step 3: Run full Matrix bus regression** + +```bash +cargo test -p octos-bus --features matrix --quiet +``` + +- [ ] **Step 4: Build Octos** + +```bash +cargo build +``` + +- [ ] **Step 5: User test checklist** + +Verify manually: +- config with `approval_policy` and no hook still produces approval requests +- unauthorized local user sees disabled approval buttons in Robrix +- authorized user can approve/deny +- deny does not execute tool +- approve executes tool +- expired request emits timeout notice + diff --git a/docs/superpowers/plans/2026-04-14-tg-bot-approval-request-plan.md b/docs/superpowers/plans/2026-04-14-tg-bot-approval-request-plan.md new file mode 100644 index 000000000..790fb3041 --- /dev/null +++ b/docs/superpowers/plans/2026-04-14-tg-bot-approval-request-plan.md @@ -0,0 +1,491 @@ +# TG Bot Approval Request Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a cross-repo approval-request flow where Octos emits approval-required bot messages, Robrix renders inline approval buttons, and Octos revalidates, audits, and executes only after a valid approval response. + +**Architecture:** Keep the security boundary in Octos. Robrix only parses `org.octos.approval_request`, renders approval UI on top of the existing Phase 4c action-buttons path, and sends a one-shot targeted `org.octos.approval_response`. Octos owns pending approval storage, replay/expiry checks, approver revalidation, timeout notifications, and audit logging; the current `before_tool_call` hook boundary is the right interception point for approval-required tool calls. + +**Tech Stack:** Rust, Makepad 2.0 `script_mod!`, Matrix custom event content fields, `matrix-sdk` send path in `sliding_sync.rs`, Octos agent lifecycle hooks (`before_tool_call`), Octos Matrix bus, `cargo test`, `cargo build`, `agent-spec`. + +--- + +## File Map + +- `specs/task-tg-bot-approval-request.spec.md` + - Source-of-truth contract. Only update if implementation reveals a real wording bug. + +- `src/home/room_screen.rs` + - Parse `org.octos.approval_request` alongside existing `org.octos.actions`. + - Render approval title/summary/expiry state using the current action-buttons container. + - Disable approval buttons for unauthorized local users. + - Build action-button context for approval responses. + - Add Robrix-side unit tests for approval request parsing and UI state. + +- `src/sliding_sync.rs` + - Add a send-path helper for approval responses that sets: + - one-shot `target_user_id = original_sender` + - `org.octos.approval_response` + - `m.in_reply_to` to the source event + - Keep generic `org.octos.action_response` behavior intact. + +- `resources/i18n/en.json` +- `resources/i18n/zh-CN.json` + - Add fallback strings for disabled approval buttons, timeout state, and malformed-approval warnings if needed. + +- `../../octos/crates/octos-agent/src/approval.rs` (new) + - Own the runtime approval model: + - `PendingApproval` + - `ApprovalDecision` + - validation helpers for expiry / replay / approver authority / digest + - Provide a small in-memory pending store abstraction and audit event structs. + +- `../../octos/crates/octos-agent/src/lib.rs` + - Export the new approval module if needed by agent runtime tests. + +- `../../octos/crates/octos-agent/src/agent/execution.rs` + - Hook point for converting approval-required tool calls into pending approval requests instead of immediate execution. + - Reuse existing `before_tool_call` lifecycle semantics; do not invent a parallel policy path. + +- `../../octos/crates/octos-bus/src/matrix_channel.rs` + - Emit Matrix approval-request messages. + - Consume `org.octos.approval_response` messages from Matrix and forward them back into Octos approval handling. + - Ensure expired/replayed/unauthorized responses do not execute tools. + +- `../../octos/book/src/advanced.md` + - Document approval request behavior as an extension of the hook / policy model. + +## Key Implementation Decisions To Preserve + +- Do not move permission truth into Robrix. +- Do not add slash-command approval UX like `/approve 123`. +- Do not let `authorized_approvers` from the message bypass Octos-side revalidation. +- Robrix must treat `tool_args_digest` as opaque request data: copy only, never recompute. +- Robrix must render approval requests from the original event content, not `m.replace` / `m.new_content`. +- Octos must derive approver identity from the Matrix event `sender`, never from payload fields. +- Octos must bind pending approvals to the originating `room_id`; wrong-room responses are invalid. +- Octos must reject approval requests with empty `authorized_approvers` instead of emitting unusable approval UI. +- Do not replace generic Phase 4c actions; approval requests are an extension, not a fork. +- Do not commit until user testing completes on both the Robrix UI side and the Octos execution/audit side. + +--- + +### Task 1: Robrix Parses and Renders Approval Requests + +**Files:** +- Modify: `src/home/room_screen.rs` +- Modify: `resources/i18n/en.json` +- Modify: `resources/i18n/zh-CN.json` +- Test: `src/home/room_screen.rs` + +- [ ] **Step 1: Write failing parsing/render-state tests for approval requests** + +Add focused tests near the existing `org.octos.actions` tests: +- `test_parse_octos_approval_request_from_content` +- `test_approval_buttons_disabled_for_unauthorized_user` +- `test_generic_actions_without_approval_request_remain_supported` +- `test_malformed_approval_request_hides_buttons` +- `test_approval_request_ignores_m_replace_edits` + +Cover: +- valid `request_id`, `tool_args_digest`, `authorized_approvers`, `expires_at` +- missing required fields +- empty `authorized_approvers` +- local user present vs absent in `authorized_approvers` +- approval rendering sourced from the original event content only +- generic actions message still using the old path + +- [ ] **Step 2: Run the new Robrix parsing tests and confirm they fail** + +Run: + +```bash +cargo test parse_octos_approval_request --quiet +cargo test approval_buttons_disabled --quiet +cargo test malformed_approval_request --quiet +``` + +Expected: FAIL because `room_screen.rs` currently only understands `org.octos.actions`. + +- [ ] **Step 3: Add approval-request data structs and parser helpers** + +In `src/home/room_screen.rs`: +- add a small `OctosApprovalRequest` struct +- add parser helpers that: + - read `org.octos.approval_request` + - read it from original event content rather than latest edit content + - validate required fields + - return `None` on malformed input +- add an authorization helper: + - `local_user_can_approve(approval_request, current_user_id)` + +- [ ] **Step 4: Extend the action-button render state with approval metadata** + +Update the existing render-state computation so it can represent: +- generic actions +- approval request with enabled buttons +- approval request with disabled buttons +- malformed approval request with no buttons + +Do not introduce a second button container. Keep approval on the current Phase 4c surface. + +- [ ] **Step 5: Render approval title/summary state in the timeline** + +In `populate_message_view()` and the action-button population helpers: +- surface approval title / summary above the buttons when approval metadata exists +- disable buttons when the local user is not in `authorized_approvers` +- leave generic action-button messages unchanged + +- [ ] **Step 6: Re-run Robrix approval parsing/render tests** + +Run: + +```bash +cargo test parse_octos_approval_request --quiet +cargo test approval_buttons_disabled --quiet +cargo test generic_actions_without_approval_request --quiet +cargo test malformed_approval_request --quiet +``` + +Expected: PASS. + +--- + +### Task 2: Robrix Sends Structured Approval Responses + +**Files:** +- Modify: `src/home/room_screen.rs` +- Modify: `src/sliding_sync.rs` +- Test: `src/home/room_screen.rs` +- Test: `src/sliding_sync.rs` + +- [ ] **Step 1: Write failing send-path tests for approval responses** + +Add tests: +- `test_click_approve_builds_approval_response_payload` +- `test_click_deny_builds_approval_response_payload` +- `test_approval_response_routes_to_original_sender` +- `test_approval_response_copies_digest_without_recomputing` + +Assert: +- `org.octos.approval_response.request_id` +- `decision = approve|deny` +- `tool_args_digest` +- `m.in_reply_to` points to the source event +- `target_user_id` equals the original bot sender + +- [ ] **Step 2: Run the new send-path tests and confirm they fail** + +Run: + +```bash +cargo test approval_response --quiet +``` + +Expected: FAIL because button clicks currently only send generic `org.octos.action_response`. + +- [ ] **Step 3: Add an approval-response request type separate from generic actions** + +In `src/home/room_screen.rs`: +- add a dedicated approval-response request builder +- keep the current generic action-response builder untouched +- ensure approval clicks carry `request_id`, `decision`, and `tool_args_digest` +- ensure `tool_args_digest` is copied byte-for-byte from the request, with no client-side hashing or normalization + +- [ ] **Step 4: Add a dedicated send helper in `sliding_sync.rs`** + +Add the minimal send-path helper needed so approval clicks can: +- bypass input-bar reply/mention state +- set one-shot `target_user_id = original_sender` +- attach `org.octos.approval_response` +- attach `m.in_reply_to` + +Do not merge this into unrelated generic send code unless it clearly reduces duplication. + +- [ ] **Step 5: Wire approval button clicks to the new send helper** + +In the room-screen action-button click handler: +- detect `approve` / `deny` on messages carrying approval metadata +- call the new approval send helper +- preserve current disable-on-click behavior +- keep generic action buttons on the old send path + +- [ ] **Step 6: Re-run focused approval send tests** + +Run: + +```bash +cargo test approval_response --quiet +``` + +Expected: PASS. + +--- + +### Task 3: Octos Converts Approval-Required Tool Calls into Pending Requests + +**Files:** +- Create: `../../octos/crates/octos-agent/src/approval.rs` +- Modify: `../../octos/crates/octos-agent/src/lib.rs` +- Modify: `../../octos/crates/octos-agent/src/agent/execution.rs` +- Test: `../../octos/crates/octos-agent/src/approval.rs` +- Test: `../../octos/crates/octos-agent/src/agent/execution.rs` + +- [ ] **Step 1: Write failing Octos unit tests for pending approval state** + +Add focused tests such as: +- `test_create_pending_approval_with_digest_and_expiry` +- `test_pending_approval_rejects_duplicate_consume` +- `test_pending_approval_rejects_expired_request` +- `test_pending_approval_revalidates_authorized_approver` +- `test_pending_approval_rejects_empty_authorized_approvers` +- `test_pending_approval_rejects_wrong_room` + +- [ ] **Step 2: Run the new Octos approval-state tests and confirm they fail** + +Run from `../../octos`: + +```bash +cargo test -p octos-agent approval --quiet +``` + +Expected: FAIL because approval runtime types and store do not exist yet. + +- [ ] **Step 3: Create `approval.rs` with the minimal runtime model** + +Implement: +- `PendingApproval` +- `ApprovalDecision` +- helper for `tool_args_digest` +- in-memory pending store keyed by `request_id` +- store original `room_id` alongside `request_id` +- helper methods: + - `is_expired` + - `consume_once` + - `is_authorized_approver` + +Keep the storage abstraction small; v1 does not need durable persistence. + +- [ ] **Step 4: Hook approval-required tool calls in `agent/execution.rs`** + +Use the existing `before_tool_call` / policy boundary: +- when policy says “approval required”, do not execute the tool +- if policy resolves to an empty approver set, deny immediately instead of creating a pending request +- create a pending approval entry instead +- return control to the transport layer with enough metadata to send a Matrix approval request + +Do not invent a second policy engine alongside hooks/tool policy. + +- [ ] **Step 5: Re-run Octos approval-state tests** + +Run: + +```bash +cargo test -p octos-agent approval --quiet +``` + +Expected: PASS. + +--- + +### Task 4: Octos Emits Approval Requests and Consumes Approval Responses over Matrix + +**Files:** +- Modify: `../../octos/crates/octos-bus/src/matrix_channel.rs` +- Test: `../../octos/crates/octos-bus/src/matrix_channel.rs` + +- [ ] **Step 1: Write failing Matrix protocol tests** + +Add tests covering: +- `test_matrix_approval_request_event_contains_protocol_fields` +- `test_matrix_approval_response_executes_once` +- `test_duplicate_approval_response_is_rejected` +- `test_expired_approval_response_notifies_without_execution` +- `test_approval_response_revalidated_against_current_policy` +- `test_approval_response_uses_matrix_sender_identity` +- `test_approval_response_wrong_room_rejected` + +- [ ] **Step 2: Run the new Matrix approval tests and confirm they fail** + +Run from `../../octos`: + +```bash +cargo test -p octos-bus approval --features matrix --quiet +``` + +Expected: FAIL because Matrix channel currently has no approval protocol. + +- [ ] **Step 3: Emit approval-request Matrix messages** + +In `matrix_channel.rs`: +- add a helper to build `m.room.message` content with: + - `org.octos.approval_request` + - `org.octos.actions` for `approve` / `deny` +- include `request_id`, `tool_name`, `tool_args_digest`, `title`, `summary`, + `risk_level`, `authorized_approvers`, `expires_at`, `on_timeout` + +- [ ] **Step 4: Parse and validate `org.octos.approval_response`** + +When Matrix messages arrive: +- detect `org.octos.approval_response` +- validate required fields +- look up pending request by `request_id` +- derive approver identity from Matrix event `sender` +- reject if expired / already consumed / unauthorized / digest mismatch / wrong room +- only execute the tool on a valid, first-time `approve` +- mark as terminal on `deny` + +- [ ] **Step 5: Emit timeout and rejection notifications** + +Add minimal follow-up Matrix messages for: +- timeout (`on_timeout = notify`) +- duplicate response rejected +- unauthorized response rejected + +Keep wording simple; v1 is about safety and observability, not polished copy. + +- [ ] **Step 6: Re-run Matrix approval tests** + +Run: + +```bash +cargo test -p octos-bus approval --features matrix --quiet +``` + +Expected: PASS. + +--- + +### Task 5: Audit Logging and Documentation + +**Files:** +- Modify: `../../octos/crates/octos-agent/src/approval.rs` +- Modify: `../../octos/book/src/advanced.md` +- Test: `../../octos/crates/octos-agent/src/approval.rs` + +- [ ] **Step 1: Write failing audit-focused tests** + +Add tests such as: +- `test_approval_request_creation_is_audited` +- `test_approval_terminal_decision_is_audited` + +Assert that audit entries include: +- `request_id` +- `tool_name` +- `tool_args_digest` +- requester / approver +- execution outcome + +- [ ] **Step 2: Run the audit tests and confirm they fail** + +Run: + +```bash +cargo test -p octos-agent approval_audit --quiet +``` + +Expected: FAIL because approval audit events are not yet emitted. + +- [ ] **Step 3: Add minimal audit event recording** + +In `approval.rs`: +- add structured audit event types +- record request creation +- record terminal decision / execution outcome + +Prefer the smallest abstraction that can later be swapped to durable storage. + +- [ ] **Step 4: Document the approval-request flow** + +In `../../octos/book/src/advanced.md`: +- explain approval-required tool calls as an extension of `before_tool_call` +- document the Matrix fields at a high level +- state that Robrix is UI-only and Octos revalidates authority on receipt + +- [ ] **Step 5: Re-run audit tests** + +Run: + +```bash +cargo test -p octos-agent approval_audit --quiet +``` + +Expected: PASS. + +--- + +### Task 6: Cross-Repo Verification and User Test Checklist + +**Files:** +- No new files unless the spec or docs need wording correction + +- [ ] **Step 1: Run Robrix focused tests** + +Run from `robrix2`: + +```bash +cargo test approval_response --quiet +cargo test parse_octos_approval_request --quiet +cargo test approval_buttons_disabled --quiet +``` + +Expected: PASS. + +- [ ] **Step 2: Run Octos focused tests** + +Run from `../../octos`: + +```bash +cargo test -p octos-agent approval --quiet +cargo test -p octos-bus approval --features matrix --quiet +``` + +Expected: PASS. + +- [ ] **Step 3: Build both repos** + +Run: + +```bash +cargo build +``` + +from `robrix2`, then: + +```bash +cargo build -p octos-agent -p octos-bus +``` + +from `../../octos`. + +Expected: PASS. + +- [ ] **Step 4: Manual end-to-end test with local Octos + Robrix** + +Verify: +- Octos emits an approval request for a policy-gated action +- Robrix shows title, summary, and two buttons +- unauthorized local user sees disabled buttons +- edits to the approval request message do not alter approver UI +- authorized user can approve once +- duplicate clicks do not double-execute +- approval from the wrong room is rejected +- spoofed payload identity does not override Matrix sender identity +- expired request produces timeout notification instead of execution + +- [ ] **Step 5: User testing checkpoint (required before any commit)** + +Do not commit or open a PR until the user confirms: +- approval request UI looks correct +- approve/deny responses route to the correct bot +- denied / expired / duplicate flows are understandable +- Octos audit behavior matches expectation + +--- + +## Notes for the Implementer + +- Start with focused unit tests in both repos before touching behavior. +- Keep the first version narrow: `approve` / `deny`, `normal|critical`, `on_timeout = notify`. +- Reuse the existing Phase 4c action-button infrastructure in Robrix; do not create a second UI concept. +- If you need to choose between “clean abstraction” and “clear enforceable boundary”, prefer the latter. The important invariant is that approval authority never migrates into the client. diff --git a/docs/superpowers/plans/2026-04-16-dm-joined-room-details-churn-plan.md b/docs/superpowers/plans/2026-04-16-dm-joined-room-details-churn-plan.md new file mode 100644 index 000000000..cf9de0e1e --- /dev/null +++ b/docs/superpowers/plans/2026-04-16-dm-joined-room-details-churn-plan.md @@ -0,0 +1,609 @@ +# DM Rejoin — Stop JoinedRoomDetails Churn On Display Flip — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Stop `src/sliding_sync.rs::update_room` from tearing down `JoinedRoomDetails` when a still-Joined room's display eligibility flips, so the open `RoomScreen`'s singleton timeline receiver survives the `(is_direct=true, display_name=Empty)` transient state of a freshly-created DM. + +**Architecture:** Replace the two early-return branches that call `remove_room` / `add_new_room` on display flips with `RoomsListUpdate::HideRoom` / new `RoomsListUpdate::UnhideRoom` signals that only toggle visibility in the sidebar, and let the normal `UpdateRoomName` / `UpdateIsDirect` / avatar / power-level updates flow through in the same `update_room` invocation. + +**Tech Stack:** Rust, matrix-sdk sliding-sync, crossbeam_channel, Makepad 2.0 (unchanged in this task). + +--- + +## File Structure + +- **Modify** `src/sliding_sync.rs` + - Add `JoinedRoomDisplayFlip` enum + `classify_joined_room_display_flip` helper (near the existing `should_display_joined_room_entry` at line 1141). + - Delete `should_reuse_existing_joined_room_details` (line 1151) and its two unit tests (lines 1267–1282). + - Rewrite `update_room` lines 4819–4825 to emit `HideRoom` / `UnhideRoom` and fall through. + - Revert `add_new_room` (lines 5081–5142) back to a single unconditional timeline-build path (remove the reuse branch). +- **Modify** `src/home/rooms_list.rs` + - Add `UnhideRoom { room_id: OwnedRoomId }` variant to `RoomsListUpdate` (near `HideRoom`, line ~180). + - Add a handler arm for `UnhideRoom` that clears the hidden flag and re-evaluates displayed lists (next to the existing `HideRoom` arm at line 886). +- **Create** `specs/task-dm-joined-room-details-churn.spec.md` — already written. +- **Test:** unit tests live in the existing `#[cfg(test)] mod matrix_request_tests` block at `src/sliding_sync.rs` and a fresh `#[cfg(test)] mod` in `src/home/rooms_list.rs` (or a separate module beside it if needed). + +--- + +### Task 1: Add UnhideRoom variant + handler in rooms_list (TDD) + +**Files:** +- Modify: `src/home/rooms_list.rs` (add variant near line 180, add handler arm near line 886) + +- [ ] **Step 1: Read the current `RoomsListUpdate` variant list and the `HideRoom` handler** + +Run: inspect `src/home/rooms_list.rs` around lines 148–250 (enum) and lines 886–935 (HideRoom arm). Note the exact field layout of `HideRoom` and how it removes from `displayed_direct_rooms` / `displayed_regular_rooms`. + +- [ ] **Step 2: Write a failing test that asserts UnhideRoom restores a hidden direct room** + +Add at the end of `src/home/rooms_list.rs` (after the last `impl` block): + +```rust +#[cfg(test)] +mod unhide_room_tests { + use super::*; + + // Sketch: we cannot instantiate RoomsList fully (it's a Makepad widget), + // so we test the pure decision: after receiving UnhideRoom, hidden_rooms + // no longer contains the id, and a room that satisfies the default + // display_filter is pushed onto displayed_direct_rooms. + // + // If exercising the Widget handler requires a Cx, split the pure logic + // into a free function `apply_unhide_room(&mut RoomsListState, room_id)` + // during Task 2 and test that instead. + + #[test] + fn unhide_room_enum_variant_exists() { + // Compile-only smoke test: ensures we can construct the new variant. + let _ = RoomsListUpdate::UnhideRoom { + room_id: "!x:example.org".parse().unwrap(), + }; + } +} +``` + +- [ ] **Step 3: Run the test — expect it to FAIL with "no variant named UnhideRoom"** + +Run: `cargo test -p robrix unhide_room_enum_variant_exists -- --nocapture` +Expected: build error — `no variant named "UnhideRoom" on enum "RoomsListUpdate"` + +- [ ] **Step 4: Add the `UnhideRoom` variant next to `HideRoom`** + +Edit `src/home/rooms_list.rs` — inside `pub enum RoomsListUpdate { ... }` (just before or after the existing `HideRoom { room_id: OwnedRoomId },` line). Add: + +```rust + /// Clear an entry from `hidden_rooms` for `room_id`, then re-check + /// `should_display_room!` and restore the room into `displayed_direct_rooms` + /// or `displayed_regular_rooms` if it is newly eligible. + /// + /// Semantic dual of [`RoomsListUpdate::HideRoom`]. Used by + /// `update_room` when a Joined room's display eligibility flips from + /// hidden back to displayable without changing `RoomState`. + UnhideRoom { + room_id: OwnedRoomId, + }, +``` + +- [ ] **Step 5: Re-run the compile-smoke test — expect PASS** + +Run: `cargo test -p robrix unhide_room_enum_variant_exists -- --nocapture` +Expected: `test result: ok. 1 passed` + +- [ ] **Step 6: Add the match arm for `UnhideRoom`** + +Edit the `handle_event` dispatch block (same file, search for `RoomsListUpdate::HideRoom { room_id } =>`). Insert a new arm immediately after it: + +```rust + RoomsListUpdate::UnhideRoom { room_id } => { + let was_hidden = self.hidden_rooms.remove(&room_id); + if !was_hidden { + // Already not hidden — nothing to do. + continue; + } + if let Some(room) = self.all_joined_rooms.get(&room_id) { + let is_direct = room.is_direct; + let should_display = should_display_room!(self, &room_id, room); + if should_display { + let displayed_list = if is_direct { + &mut self.displayed_direct_rooms + } else { + &mut self.displayed_regular_rooms + }; + if !displayed_list.contains(&room_id) { + displayed_list.push(room_id); + } + } + } + // Invited-room unhide is not required: HideRoom currently + // never fires for invited rooms from sliding_sync. + } +``` + +- [ ] **Step 7: `cargo build` — expect clean build** + +Run: `cargo build` +Expected: compiles with zero new warnings related to `UnhideRoom`. + +- [ ] **Step 8: Commit** + +Run: +``` +git add src/home/rooms_list.rs +git commit -m "rooms_list: add UnhideRoom update variant and handler" +``` + +--- + +### Task 2: Extract `classify_joined_room_display_flip` pure helper (TDD) + +**Files:** +- Modify: `src/sliding_sync.rs` — add helper near line 1141, add unit tests in the existing `mod matrix_request_tests` block near line 1225. + +- [ ] **Step 1: Write the three failing tests** + +Inside `mod matrix_request_tests` in `src/sliding_sync.rs`, append: + +```rust + #[test] + fn classify_joined_room_display_flip_becomes_hidden() { + assert_eq!( + classify_joined_room_display_flip(true, false), + JoinedRoomDisplayFlip::BecameHidden + ); + } + + #[test] + fn classify_joined_room_display_flip_becomes_displayable() { + assert_eq!( + classify_joined_room_display_flip(false, true), + JoinedRoomDisplayFlip::BecameDisplayable + ); + } + + #[test] + fn classify_joined_room_display_flip_no_change_when_stable() { + assert_eq!( + classify_joined_room_display_flip(true, true), + JoinedRoomDisplayFlip::NoDisplayChange + ); + assert_eq!( + classify_joined_room_display_flip(false, false), + JoinedRoomDisplayFlip::NoDisplayChange + ); + } +``` + +- [ ] **Step 2: Run the tests — expect FAIL with "not found"** + +Run: `cargo test classify_joined_room_display_flip -- --nocapture` +Expected: compile error — `cannot find function "classify_joined_room_display_flip"` and `cannot find type "JoinedRoomDisplayFlip"`. + +- [ ] **Step 3: Implement the helper next to `should_display_joined_room_entry`** + +Edit `src/sliding_sync.rs` immediately after `should_display_joined_room_entry` (current line 1149): + +```rust +/// Semantic result of comparing a Joined room's display eligibility between +/// two successive sliding-sync snapshots, while the room stays `RoomState::Joined`. +/// +/// Used by `update_room` to decide whether the visibility flip should hide +/// or restore the room in the sidebar *without* destroying its `JoinedRoomDetails`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum JoinedRoomDisplayFlip { + /// The room became eligible for display (e.g., Empty direct DM finally got + /// a calculated name). + BecameDisplayable, + /// The room lost display eligibility (e.g., `is_direct` flipped true while + /// display_name was still `Empty`). + BecameHidden, + /// No change in display eligibility; the caller should perform no + /// visibility-only side effect. + NoDisplayChange, +} + +fn classify_joined_room_display_flip( + old_should_display: bool, + new_should_display: bool, +) -> JoinedRoomDisplayFlip { + match (old_should_display, new_should_display) { + (true, false) => JoinedRoomDisplayFlip::BecameHidden, + (false, true) => JoinedRoomDisplayFlip::BecameDisplayable, + _ => JoinedRoomDisplayFlip::NoDisplayChange, + } +} +``` + +- [ ] **Step 4: Re-run — expect all three PASS** + +Run: `cargo test classify_joined_room_display_flip -- --nocapture` +Expected: +``` +test matrix_request_tests::classify_joined_room_display_flip_becomes_hidden ... ok +test matrix_request_tests::classify_joined_room_display_flip_becomes_displayable ... ok +test matrix_request_tests::classify_joined_room_display_flip_no_change_when_stable ... ok +``` + +- [ ] **Step 5: Commit** + +Run: +``` +git add src/sliding_sync.rs +git commit -m "sliding_sync: add classify_joined_room_display_flip helper + tests" +``` + +--- + +### Task 3: Rewrite `update_room` display-flip branches to use Hide/Unhide signals + +**Files:** +- Modify: `src/sliding_sync.rs:4807–4825` (`update_room` top) + +- [ ] **Step 1: Read the current code at `src/sliding_sync.rs:4807–4825`** + +You will be replacing this block: +```rust + let new_room_id = new_room.room_id.clone(); + if old_room.room_id == new_room_id { + let old_should_display = should_display_joined_room_entry( + old_room.state, + old_room.is_direct, + old_room.display_name.as_ref(), + ); + let new_should_display = should_display_joined_room_entry( + new_room.state, + new_room.is_direct, + new_room.display_name.as_ref(), + ); + if old_should_display && !new_should_display { + remove_room(new_room); + return Ok(()); + } + if !old_should_display && new_should_display { + return add_new_room(new_room, room_list_service, true).await; + } +``` + +- [ ] **Step 2: Apply the replacement** + +Use `Edit` with the old block above and the following new block: +```rust + let new_room_id = new_room.room_id.clone(); + if old_room.room_id == new_room_id { + // Same-room update. Only treat hard state transitions (Left, Banned) + // as producers of remove_room/add_new_room — they are handled below + // in the explicit state-transition block. A pure display-eligibility + // flip while the room stays Joined must NOT destroy its + // JoinedRoomDetails (otherwise an open RoomScreen's singleton + // timeline receiver is orphaned and the pane goes blank forever). + // + // See specs/task-dm-joined-room-details-churn.spec.md. + let old_should_display = should_display_joined_room_entry( + old_room.state, + old_room.is_direct, + old_room.display_name.as_ref(), + ); + let new_should_display = should_display_joined_room_entry( + new_room.state, + new_room.is_direct, + new_room.display_name.as_ref(), + ); + match classify_joined_room_display_flip(old_should_display, new_should_display) { + JoinedRoomDisplayFlip::BecameHidden => { + log!( + "[dm-debug] Hiding joined room {} from rooms list on display flip (is_direct={}, display_name={:?})", + new_room_id, new_room.is_direct, new_room.display_name, + ); + enqueue_rooms_list_update(RoomsListUpdate::HideRoom { + room_id: new_room_id.clone(), + }); + } + JoinedRoomDisplayFlip::BecameDisplayable => { + log!( + "[dm-debug] Unhiding joined room {} in rooms list on display flip (is_direct={}, display_name={:?})", + new_room_id, new_room.is_direct, new_room.display_name, + ); + enqueue_rooms_list_update(RoomsListUpdate::UnhideRoom { + room_id: new_room_id.clone(), + }); + } + JoinedRoomDisplayFlip::NoDisplayChange => {} + } +``` + +Note: **do not** add a `return Ok(());` — processing must continue into the existing state-transition block and the `UpdateRoomName` / `UpdateIsDirect` / avatar / power-level branches further down. + +- [ ] **Step 3: `cargo build` — expect clean build** + +Run: `cargo build` +Expected: compiles. If there's a warning about `classify_joined_room_display_flip` being unused in non-test config, it is now used → warning should not appear. + +- [ ] **Step 4: Run all existing sliding_sync tests** + +Run: `cargo test -p robrix --lib matrix_request_tests -- --nocapture` +Expected: all green, including the prior: +- `should_display_joined_room_entry_hides_empty_direct_dm` +- `should_display_joined_room_entry_keeps_non_empty_or_non_direct_rooms` +- `is_active_dm_room_state_only_joined_is_reusable` +- `dm_target_matching_configured_bot_auto_binds_new_room` +- `ordinary_dm_target_does_not_auto_bind_new_room` +- `choose_reusable_dm_candidate_*` +- plus the three new `classify_joined_room_display_flip_*` tests from Task 2. + +- [ ] **Step 5: Commit** + +Run: +``` +git add src/sliding_sync.rs +git commit -m "sliding_sync: keep JoinedRoomDetails alive across display-flip updates" +``` + +--- + +### Task 4: Retire the now-dead `should_reuse_existing_joined_room_details` guard inside `add_new_room` + +**Files:** +- Modify: `src/sliding_sync.rs` — remove dead predicate at lines 1151–1156, delete its two unit tests at lines 1267–1282, restore `add_new_room` lines 5084–5142 to its pre-Codex-reuse-fix single-branch form. + +- [ ] **Step 1: Read `src/sliding_sync.rs:5081–5142`** + +Confirm the current if/else structure introduced by the previous attempt: the `if should_reuse_existing_joined_room_details(...)` branch + matching `else`. + +- [ ] **Step 2: Replace the if/else with the unconditional timeline-build body** + +Use `Edit` to replace: + +```rust + let has_existing_joined_room_details = ALL_JOINED_ROOMS.lock().unwrap().contains_key(&new_room.room_id); + if should_reuse_existing_joined_room_details(new_room.state, has_existing_joined_room_details) { + // Newly-created DMs often get a second joined-room snapshot moments later while + // the homeserver finishes resolving `is_direct` and the calculated room name. + // Replacing the JoinedRoomDetails here would orphan the open RoomScreen's + // singleton timeline receiver and leave the timeline pane blank. + log!( + "[dm-debug] add_new_room reusing existing JoinedRoomDetails for room {} while refreshing metadata: name={:?} is_direct={}", + new_room.room_id, + new_room.display_name, + new_room.is_direct, + ); + } else { + let timeline = Arc::new( + new_room.room.timeline_builder() + .with_focus(TimelineFocus::Live { + // we show threads as separate timelines in their own RoomScreen + hide_threaded_events: true, + }) + .track_read_marker_and_receipts(TimelineReadReceiptTracking::AllEvents) + .build() + .await + .map_err(|e| anyhow::anyhow!("BUG: Failed to build timeline for room {}: {e}", new_room.room_id))?, + ); + let (timeline_update_sender, timeline_update_receiver) = crossbeam_channel::unbounded(); + + let (request_sender, request_receiver) = watch::channel(Vec::new()); + let timeline_subscriber_handler_task = Handle::current().spawn(timeline_subscriber_handler( + new_room.room.clone(), + timeline.clone(), + timeline_update_sender.clone(), + request_receiver, + None, + )); + + // We need to add the room to the `ALL_JOINED_ROOMS` list before we can send + // an `AddJoinedRoom` update to the RoomsList widget, because that widget might + // immediately issue a `MatrixRequest` that relies on that room being in `ALL_JOINED_ROOMS`. + log!("Adding new joined room {}, name: {:?}", new_room.room_id, new_room.display_name); + ALL_JOINED_ROOMS.lock().unwrap().insert( + new_room.room_id.clone(), + JoinedRoomDetails { + room_id: new_room.room_id.clone(), + main_timeline: PerTimelineDetails { + timeline, + timeline_singleton_endpoints: Some((timeline_update_receiver, request_sender)), + timeline_update_sender, + timeline_subscriber_handler_task, + }, + thread_timelines: HashMap::new(), + pending_thread_timelines: HashSet::new(), + typing_notice_subscriber: None, + pinned_events_subscriber: None, + }, + ); + } +``` + +with: + +```rust + let timeline = Arc::new( + new_room.room.timeline_builder() + .with_focus(TimelineFocus::Live { + // we show threads as separate timelines in their own RoomScreen + hide_threaded_events: true, + }) + .track_read_marker_and_receipts(TimelineReadReceiptTracking::AllEvents) + .build() + .await + .map_err(|e| anyhow::anyhow!("BUG: Failed to build timeline for room {}: {e}", new_room.room_id))?, + ); + let (timeline_update_sender, timeline_update_receiver) = crossbeam_channel::unbounded(); + + let (request_sender, request_receiver) = watch::channel(Vec::new()); + let timeline_subscriber_handler_task = Handle::current().spawn(timeline_subscriber_handler( + new_room.room.clone(), + timeline.clone(), + timeline_update_sender.clone(), + request_receiver, + None, + )); + + // We need to add the room to the `ALL_JOINED_ROOMS` list before we can send + // an `AddJoinedRoom` update to the RoomsList widget, because that widget might + // immediately issue a `MatrixRequest` that relies on that room being in `ALL_JOINED_ROOMS`. + log!("Adding new joined room {}, name: {:?}", new_room.room_id, new_room.display_name); + ALL_JOINED_ROOMS.lock().unwrap().insert( + new_room.room_id.clone(), + JoinedRoomDetails { + room_id: new_room.room_id.clone(), + main_timeline: PerTimelineDetails { + timeline, + timeline_singleton_endpoints: Some((timeline_update_receiver, request_sender)), + timeline_update_sender, + timeline_subscriber_handler_task, + }, + thread_timelines: HashMap::new(), + pending_thread_timelines: HashSet::new(), + typing_notice_subscriber: None, + pinned_events_subscriber: None, + }, + ); +``` + +- [ ] **Step 3: Delete the dead predicate** + +Use `Edit` to remove the block at lines 1151–1156: +```rust +fn should_reuse_existing_joined_room_details( + room_state: RoomState, + has_existing_joined_room_details: bool, +) -> bool { + room_state == RoomState::Joined && has_existing_joined_room_details +} +``` + +- [ ] **Step 4: Delete the two dead unit tests** + +Use `Edit` to remove the test pair (inside `mod matrix_request_tests`): + +```rust + #[test] + fn should_reuse_existing_joined_room_details_for_joined_room_readd() { + assert!(should_reuse_existing_joined_room_details( + RoomState::Joined, + true, + )); + } + + #[test] + fn should_not_reuse_existing_joined_room_details_for_first_add() { + assert!(!should_reuse_existing_joined_room_details( + RoomState::Joined, + false, + )); + } +``` + +- [ ] **Step 5: `cargo build` — expect clean build** + +Run: `cargo build` +Expected: no "unused" warnings for `should_reuse_existing_joined_room_details` (it is fully removed). + +- [ ] **Step 6: Run full matrix_request_tests suite** + +Run: `cargo test -p robrix --lib matrix_request_tests -- --nocapture` +Expected: green; the two removed tests are no longer listed. + +- [ ] **Step 7: Commit** + +Run: +``` +git add src/sliding_sync.rs +git commit -m "sliding_sync: drop dead reuse-guard now that update_room preserves JoinedRoomDetails" +``` + +--- + +### Task 5: Lint the spec and verify plan coverage + +**Files:** +- Read: `specs/task-dm-joined-room-details-churn.spec.md` + +- [ ] **Step 1: Confirm `agent-spec` CLI is available** + +Run: `command -v agent-spec || cargo install agent-spec` + +- [ ] **Step 2: Parse the spec** + +Run: `agent-spec parse specs/task-dm-joined-room-details-churn.spec.md` +Expected: non-zero scenarios listed under Completion Criteria. + +- [ ] **Step 3: Lint the spec** + +Run: `agent-spec lint specs/task-dm-joined-room-details-churn.spec.md --min-score 0.7` +Expected: score >= 0.7. If below, fix the reported warnings in the spec file and re-run. + +- [ ] **Step 4: Commit** + +Run: +``` +git add specs/task-dm-joined-room-details-churn.spec.md docs/superpowers/plans/2026-04-16-dm-joined-room-details-churn-plan.md +git commit -m "spec: record DM rejoin JoinedRoomDetails churn fix" +``` + +--- + +### Task 6: Manual verification — do NOT commit or push until the user has tested + +**Files:** +- Read: `/tmp/robrix_debug.log` after the test run. + +- [ ] **Step 1: Clean previous log** + +Run: `: > /tmp/robrix_debug.log` + +- [ ] **Step 2: Run the app** + +Run: `cargo run` + +- [ ] **Step 3: Reproduce the exact flow** + +1. Log in as the test user. +2. Open an existing DM with `@octosbot`, `Leave` it. +3. Re-open People, search `octosbot`, confirm the "Create New Direct Message" modal, confirm create. +4. Send "hello" in the new DM. + +- [ ] **Step 4: Verify log assertions** + +Run: `grep -cE "Adding new joined room !" /tmp/robrix_debug.log` +Expected: `1` (exactly one add, no re-add). + +Run: `grep -cE "Dropping JoinedRoomDetails for room !" /tmp/robrix_debug.log` +Expected: `0` until the user actually Leaves the room again. + +Run: `grep -cE "\[dm-debug\] (Hiding|Unhiding) joined room !" /tmp/robrix_debug.log` +Expected: `>= 1` for at least one `Hiding` during the Empty-direct window, followed by an `Unhiding` once the server reports the calculated name. + +- [ ] **Step 5: Verify visual behavior in the app** + +- Right pane timeline of the new DM shows the user's "hello" plus any octosbot replies (no longer stuck on "Loading earlier messages…"). +- Left sidebar briefly omits the Empty direct DM, then shows it as `octosbot` once the homeserver resolves the name. It does not show a duplicate. + +- [ ] **Step 6: Hand off to user** + +Post a short report to the user including: +- Exact new room id from the reproduction. +- The four log assertion counts from Step 4. +- A note that the Dock tab label may still show `Room ID !...` (out-of-scope follow-up — see `Out of Scope` in the spec). +- Do NOT commit `Co-Authored-By: Claude` lines; do NOT auto-merge. Wait for explicit user approval before creating a PR. + +--- + +## Self-Review Checklist + +- [x] Every spec Completion Criteria scenario maps to a task: + - `classify_joined_room_display_flip_*` × 3 → Task 2 + - `rooms_list_unhide_room_restores_direct_room` → Task 1 (handler body) + - `rooms_list_unhide_room_unknown_room_is_noop` → Task 1 (handler body, early-continue) + - `manual_test_dm_rejoin_timeline_not_blank` → Task 6 + - `cargo_build_and_matrix_request_tests_green` → Tasks 3 & 4 +- [x] No `TBD` / `TODO` / placeholder steps. +- [x] Every step that changes code has the exact code block inline. +- [x] Type names are consistent across tasks: `JoinedRoomDisplayFlip` and its variants `BecameDisplayable` / `BecameHidden` / `NoDisplayChange` appear identically in the spec, tests, helper, and `update_room` match arm. +- [x] `RoomsListUpdate::UnhideRoom { room_id: OwnedRoomId }` field shape is identical in Task 1 Step 4, Task 1 Step 6, and Task 3 Step 2. + +## Execution Handoff + +Plan complete and saved to `docs/superpowers/plans/2026-04-16-dm-joined-room-details-churn-plan.md`. + +Execution choices: +1. **Inline Execution (recommended for this scope)** — 6 focused tasks, each < 10 minutes, easier to roll back the single-repo change. +2. **Subagent-Driven** — one subagent per task, fresh context. Good if the human wants a second-eye review between Tasks 3 and 4 before touching `add_new_room`. diff --git a/docs/superpowers/plans/2026-04-21-register-phase-1-skeleton.md b/docs/superpowers/plans/2026-04-21-register-phase-1-skeleton.md new file mode 100644 index 000000000..55dd246ec --- /dev/null +++ b/docs/superpowers/plans/2026-04-21-register-phase-1-skeleton.md @@ -0,0 +1,1200 @@ +# Register 阶段 1 实施计划:骨架 + HS 能力发现 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 建立 `src/register/` 模块的骨架(4 个文件),实现 homeserver 能力发现,让用户从 login 屏点击 "Sign up here" 后进入注册屏选服务器并看到 "MAS OAuth / UIAA / 不允许注册" 三种状态。本阶段**不**实现任何真正的注册动作。 + +**Architecture:** 独立 `src/register/` 模块,包含 `mod.rs`、`register_screen.rs`、`register_status_modal.rs`、`validation.rs`。`RegisterScreen` widget 主导 UI;新增 `MatrixRequest::DiscoverHomeserverCapabilities` 在后台异步探测 `.well-known/matrix/client` + `/_matrix/client/versions` + `/_matrix/client/v3/login` + 空 body `POST /register`。结果通过 `RegisterAction::CapabilitiesDiscovered(HsCapabilities)` 回到 UI。`login_screen.rs` 删除现有 signup mode 并把 "Sign up here" 按钮的点击改为派发 `LoginAction::NavigateToRegister`。 + +**Tech Stack:** Rust + Makepad 2.0 DSL (`script_mod!`),`matrix-sdk`(已引入),`url = "2.5"`(已引入),`reqwest`(通过 matrix-sdk transitive dep 已引入),`robius_open`(已引入,但本阶段不使用),`tokio`(已引入)。 + +**依赖的 spec:** `specs/task-register-flow.spec.md` ,阶段 1 对应 "骨架 + 能力发现" 子章节。 + +--- + +## File Structure + +### Create + +| 文件 | 职责 | 预计 LOC | +|---|---|---| +| `src/register/mod.rs` | 模块入口 + `RegisterAction` / `HsCapabilities` / `RegisterMode` 数据类型 + `script_mod(vm)` 聚合函数 | ~120 | +| `src/register/register_screen.rs` | `RegisterScreen` widget 及其 `script_mod!` DSL;ServerPicker、能力状态显示 | ~250 | +| `src/register/register_status_modal.rs` | 进度/状态模态(Phase 1 仅骨架) | ~80 | +| `src/register/validation.rs` | URL 归一化 + 校验 + 对应单元测试 | ~80 | + +### Modify + +| 文件 | 改动 | +|---|---| +| `src/lib.rs` | 追加 `pub mod register;`;`script_mod` 注册点调用 `register::script_mod(vm)` | +| `src/login/login_screen.rs` | 删除 signup mode 相关代码(confirm_password DSL 字段、`set_signup_mode`、Sign Up 内部注册逻辑),改 "Sign up here" 按钮 click 派发 `LoginAction::NavigateToRegister` | +| `src/login/login_screen.rs` (enum) | `LoginAction` 新增 `NavigateToRegister` 变体 | +| `src/app.rs` | 处理 `LoginAction::NavigateToRegister` / `RegisterAction::NavigateToLogin` 实现登录屏 ↔ 注册屏切换 | +| `src/sliding_sync.rs` (L695 附近) | `MatrixRequest` 新增 `DiscoverHomeserverCapabilities { url: String }` 变体 | +| `src/sliding_sync.rs` (worker loop) | 实现能力发现 handler:调 matrix-sdk + 直接 HTTP probe 四条端点 | + +### Out of Phase 1 (留给阶段 2+) + +- `src/register/oidc.rs`(阶段 2) +- `src/register/uiaa.rs`(阶段 3) +- 实际触发注册:本阶段仅显示能力状态,**不**发起真正的 /register + +--- + +## Prerequisite: 现有代码的关键位置(执行者速查) + +- `MatrixRequest` enum 位置:`src/sliding_sync.rs:695` +- `LoginRequest` enum + `RegisterAccount` struct:`src/sliding_sync.rs:1245-1270` +- `LoginAction` enum:`src/login/login_screen.rs:1223` +- 现有 signup mode 相关 click handler:`src/login/login_screen.rs` 中搜索 `set_signup_mode` +- Matrix worker loop 的 match 点:`src/sliding_sync.rs:1288`(`match request { ... }`) +- `login/mod.rs` 中的 `script_mod` 聚合模式:就是我们要在 `register/mod.rs` 复制的结构 + +--- + +## Task 1: 模块骨架 + lib.rs 注册 + +**Files:** +- Create: `src/register/mod.rs` +- Create: `src/register/validation.rs` +- Create: `src/register/register_screen.rs`(本 task 只做占位 stub) +- Create: `src/register/register_status_modal.rs`(占位 stub) +- Modify: `src/lib.rs`(追加 `pub mod register;`) + +- [ ] **Step 1:创建 `src/register/mod.rs`(最小骨架)** + +```rust +//! Account registration feature. +//! +//! Covers the dual-mode register flow (OIDC for MAS-delegated servers, +//! UIAA wizard for legacy servers). See `specs/task-register-flow.spec.md`. + +use makepad_widgets::ScriptVm; + +pub mod register_screen; +pub mod register_status_modal; +pub mod validation; + +pub fn script_mod(vm: &mut ScriptVm) { + register_status_modal::script_mod(vm); + register_screen::script_mod(vm); +} +``` + +- [ ] **Step 2:创建 `src/register/validation.rs`(占位)** + +```rust +//! URL / localpart / password validators for registration. + +// Functions are added in Task 2. +``` + +- [ ] **Step 3:创建 `src/register/register_screen.rs`(最小 stub)** + +```rust +//! RegisterScreen widget: homeserver picker + capability display. +//! +//! The full wizard body is added in later phases; Phase 1 only wires +//! server selection and shows the MAS/UIAA/Disabled three-state result. + +use makepad_widgets::*; + +script_mod! { + use makepad_widgets::base::*; + use makepad_widgets::theme_desktop_dark::*; + + pub RegisterScreen := {{RegisterScreen}} View { + width: Fill, + height: Fill, + show_bg: true, + draw_bg: { color: #x1F2124 } + + // TODO: Task 5 fills in this body. + } +} + +#[derive(Script, ScriptHook, Widget)] +pub struct RegisterScreen { + #[deref] view: View, +} + +impl Widget for RegisterScreen { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + self.view.handle_event(cx, event, scope); + } + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + self.view.draw_walk(cx, scope, walk) + } +} +``` + +- [ ] **Step 4:创建 `src/register/register_status_modal.rs`(最小 stub)** + +```rust +//! Status modal shared by both OIDC and UIAA branches. +//! +//! Phase 1 scaffolds the widget; full wiring comes in Phases 2-4. + +use makepad_widgets::*; + +script_mod! { + use makepad_widgets::base::*; + use makepad_widgets::theme_desktop_dark::*; + + pub RegisterStatusModal := {{RegisterStatusModal}} Modal { + // TODO: Phase 2 wires title + status text + cancel button. + } +} + +#[derive(Script, ScriptHook, Widget)] +pub struct RegisterStatusModal { + #[deref] modal: Modal, +} + +impl Widget for RegisterStatusModal { + fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) { + self.modal.handle_event(cx, event, scope); + } + fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep { + self.modal.draw_walk(cx, scope, walk) + } +} +``` + +- [ ] **Step 5:修改 `src/lib.rs`,追加 `pub mod register;`** + +定位:在 `pub mod login;` 后面(让 login 和 register 视觉上相邻)。 + +编辑示意: + +```rust +pub mod login; +pub mod register; // NEW +pub mod logout; +``` + +同时,在该文件的 `script_mod` 聚合函数里加入 `register::script_mod(vm)`。搜索 `login::script_mod(vm)` 所在位置,紧跟其后加一行: + +```rust +pub fn script_mod(vm: &mut ScriptVm) { + // ... existing lines ... + login::script_mod(vm); + register::script_mod(vm); // NEW + // ... other lines ... +} +``` + +> **注意**:具体 `script_mod` 聚合函数在 `src/lib.rs` 里的位置可能略有差异;执行者 grep `login::script_mod(vm)` 定位后插入即可。 + +- [ ] **Step 6:验证 `cargo build` 通过** + +```bash +cargo build 2>&1 | tail -20 +``` + +预期:无 error。有 `unused import` 或 `dead_code` warning 是正常的(本阶段是骨架)。 + +- [ ] **Step 7:commit** + +```bash +git add src/register/ src/lib.rs +git commit -m "feat(register): scaffold src/register/ module + +Add empty register module with mod.rs, register_screen.rs, +register_status_modal.rs, validation.rs. Register the module +in src/lib.rs and wire the script_mod aggregator per the +login/mod.rs pattern. + +Part of specs/task-register-flow.spec.md Phase 1." +``` + +--- + +## Task 2: URL 归一化 + 单元测试 + +**Files:** +- Modify: `src/register/validation.rs` + +**TDD 顺序:先写测试,再写实现。** + +- [ ] **Step 1:在 `src/register/validation.rs` 写测试(用 `#[cfg(test)]` 模块)** + +```rust +//! URL / localpart / password validators for registration. + +use url::Url; + +/// Errors returned by homeserver URL normalization. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum HomeserverUrlError { + /// Input was empty or whitespace only. + Empty, + /// Scheme is neither `http` nor `https`. + UnsupportedScheme(String), + /// URL could not be parsed. + Invalid, +} + +/// Normalize a user-entered homeserver URL. +/// +/// - Bare hostname (e.g. `matrix.org`) becomes `https://matrix.org`. +/// - Explicit `http(s)://` schemes are kept as-is. +/// - Any non-`http(s)` scheme is rejected. +/// - Trailing `/` is stripped. +/// - Empty string is rejected. +pub fn normalize_homeserver_url(input: &str) -> Result { + let trimmed = input.trim(); + if trimmed.is_empty() { + return Err(HomeserverUrlError::Empty); + } + + let with_scheme = if trimmed.contains("://") { + trimmed.to_string() + } else { + format!("https://{trimmed}") + }; + + let mut url = Url::parse(&with_scheme).map_err(|_| HomeserverUrlError::Invalid)?; + + match url.scheme() { + "http" | "https" => {} + other => return Err(HomeserverUrlError::UnsupportedScheme(other.to_string())), + } + + // Strip trailing slash for canonical form (Url keeps path = "/" by default; + // set to "" so discovery appends cleanly). + if url.path() == "/" { + url.set_path(""); + } + + Ok(url) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bare_hostname_gets_https() { + let url = normalize_homeserver_url("matrix.org").unwrap(); + assert_eq!(url.as_str(), "https://matrix.org"); + } + + #[test] + fn explicit_https_is_kept() { + let url = normalize_homeserver_url("https://alvin.meldry.com").unwrap(); + assert_eq!(url.as_str(), "https://alvin.meldry.com"); + } + + #[test] + fn explicit_http_is_kept() { + let url = normalize_homeserver_url("http://127.0.0.1:8128").unwrap(); + assert_eq!(url.as_str(), "http://127.0.0.1:8128"); + } + + #[test] + fn trailing_slash_is_stripped() { + let url = normalize_homeserver_url("https://matrix.org/").unwrap(); + assert_eq!(url.as_str(), "https://matrix.org"); + } + + #[test] + fn whitespace_is_trimmed() { + let url = normalize_homeserver_url(" matrix.org ").unwrap(); + assert_eq!(url.as_str(), "https://matrix.org"); + } + + #[test] + fn empty_input_is_rejected() { + assert_eq!( + normalize_homeserver_url(""), + Err(HomeserverUrlError::Empty), + ); + assert_eq!( + normalize_homeserver_url(" "), + Err(HomeserverUrlError::Empty), + ); + } + + #[test] + fn non_http_scheme_is_rejected() { + let result = normalize_homeserver_url("ftp://example.com"); + assert!(matches!(result, Err(HomeserverUrlError::UnsupportedScheme(ref s)) if s == "ftp")); + } + + #[test] + fn malformed_url_is_rejected() { + let result = normalize_homeserver_url("http:// /not a url"); + assert!(matches!(result, Err(HomeserverUrlError::Invalid))); + } +} +``` + +- [ ] **Step 2:跑测试验证** + +```bash +cargo test -p robrix register::validation 2>&1 | tail -20 +``` + +预期:全部 pass(上面 8 个 test 都应该通过)。如果 fail,检查 `url` crate 的行为是否和我假设一致(有些 trailing slash 处理和版本有关)。 + +- [ ] **Step 3:commit** + +```bash +git add src/register/validation.rs +git commit -m "feat(register): homeserver URL normalizer + tests + +Accept bare hostname (prepend https://), strip trailing slash, +reject non-http(s) schemes and empty input. 8 unit tests cover +the edge cases." +``` + +--- + +## Task 3: `RegisterAction` + `HsCapabilities` 数据类型 + +**Files:** +- Modify: `src/register/mod.rs` + +- [ ] **Step 1:在 `src/register/mod.rs` 追加数据类型** + +在 `pub fn script_mod(...)` 下方追加: + +```rust +use matrix_sdk::ruma::api::client::uiaa::UiaaInfo; +use makepad_widgets::DefaultNone; + +/// Homeserver capabilities discovered before register branching. +#[derive(Clone, Debug)] +pub struct HsCapabilities { + /// Normalized base URL the client will use. + pub base_url: String, + /// True iff the server advertises `m.authentication.issuer` in + /// `.well-known/matrix/client` (MSC2965 / MAS delegation). + pub is_mas_native_oidc: bool, + /// True iff `POST /_matrix/client/v3/register` with empty body returns + /// 401 with parseable UIAA flows (NOT 403 M_FORBIDDEN). + pub registration_enabled: bool, + /// Optional UIAA probe result (empty when server requires MAS). + pub uiaa_probe: Option, + /// Identity providers harvested from `/_matrix/client/v3/login`. + /// Phase 1 populates but does not render; Phase 4 renders buttons. + pub sso_providers: Vec, +} + +/// Minimal info per identity provider. Full matrix-sdk type is not +/// used because we only need name + id at this phase. +#[derive(Clone, Debug)] +pub struct IdentityProviderSummary { + pub id: String, + pub name: String, + pub icon_url: Option, +} + +/// Outcome classification of capability discovery. +/// +/// Derived from `HsCapabilities` by `classify()` below; used for UI display. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RegisterMode { + /// Server advertises MAS OAuth; register goes through browser. + MasWebOnly, + /// Server supports direct UIAA register. + Uiaa, + /// Server explicitly disallows registration. + Disabled, +} + +impl HsCapabilities { + /// Produce a human-routable mode. Follows element-web `Registration.tsx`: + /// MAS presence wins over UIAA even when both are possible. + pub fn mode(&self) -> RegisterMode { + if self.is_mas_native_oidc { + RegisterMode::MasWebOnly + } else if self.registration_enabled { + RegisterMode::Uiaa + } else { + RegisterMode::Disabled + } + } +} + +/// Actions produced by or consumed by the register feature. +/// +/// `Cx::post_action(RegisterAction::*)` from any widget; handled by `App` and +/// by `RegisterScreen`. +#[derive(Clone, Debug, DefaultNone)] +pub enum RegisterAction { + /// User clicked the back button on RegisterScreen. + NavigateToLogin, + /// Sliding-sync reports the result of capability discovery. + CapabilitiesDiscovered(HsCapabilities), + /// Capability discovery failed (network error, bad URL, 5xx). + DiscoveryFailed(String), + None, +} +``` + +- [ ] **Step 2:验证 `cargo build` 通过** + +```bash +cargo build 2>&1 | tail -15 +``` + +预期:无 error。`DefaultNone` 来自 `makepad_widgets` — 如果 grep `DefaultNone` 在项目中的用法并对齐(例如 `src/home/` 里常用)。 + +- [ ] **Step 3:commit** + +```bash +git add src/register/mod.rs +git commit -m "feat(register): add RegisterAction + HsCapabilities types + +Introduce the data model used across Phase 1-5: +- HsCapabilities with is_mas_native_oidc / registration_enabled / + uiaa_probe / sso_providers fields matching the spec +- RegisterMode enum (MasWebOnly / Uiaa / Disabled) derived via + HsCapabilities::mode() — MAS wins over UIAA per element-web rule +- RegisterAction with NavigateToLogin / CapabilitiesDiscovered / + DiscoveryFailed variants for Phase 1 + None default" +``` + +--- + +## Task 4: `LoginAction::NavigateToRegister` 变体 + 入口按钮改派 + +**Files:** +- Modify: `src/login/login_screen.rs`(L1223 附近 enum,以及 "Sign up here" 按钮 click handler) + +- [ ] **Step 1:在 `LoginAction` enum 追加 `NavigateToRegister` 变体** + +定位:`src/login/login_screen.rs:1223`。在 `ShowAddAccountScreen` 之后、`CancelAddAccount` 之前加: + +```rust + /// User clicked "Sign up here"; the main App should hide the + /// login screen and show RegisterScreen. + NavigateToRegister, +``` + +- [ ] **Step 2:grep 定位 "Sign up here" 按钮的现有 click handler** + +```bash +grep -n "set_signup_mode\|sign up here\|Sign up here" src/login/login_screen.rs | head -10 +``` + +找到 click 分支。它当前调用 `set_signup_mode(true)`。 + +- [ ] **Step 3:替换 click handler** + +把 `set_signup_mode(true)` 的那一整段 click 分支改成: + +```rust +Cx::post_action(LoginAction::NavigateToRegister); +``` + +(保留任何围绕的 `if button_clicked` / `if let Some(...)` 外层结构;只替换函数体。) + +> **注意**:本 Task 仅改入口。删除 signup mode 的其它残留在 Task 7 统一做,避免一次改动过大。 + +- [ ] **Step 4:`cargo build` 不应破坏现有代码** + +```bash +cargo build 2>&1 | tail -15 +``` + +预期:编译通过。如果出现 "未使用的函数 `set_signup_mode`" warning,Task 7 会删除。 + +- [ ] **Step 5:commit** + +```bash +git add src/login/login_screen.rs +git commit -m "feat(login): dispatch NavigateToRegister for Sign Up click + +Add LoginAction::NavigateToRegister variant. Replace +set_signup_mode(true) call with Cx::post_action dispatch so +the main App can route to the new RegisterScreen. + +The signup-mode rendering code is removed in a later task." +``` + +--- + +## Task 5: `RegisterScreen` widget 完整 DSL + handle_event + +**Files:** +- Modify: `src/register/register_screen.rs`(替换 Task 1 的 stub) + +- [ ] **Step 1:参考 `login_screen.rs` 的 DSL 结构和命名习惯** + +```bash +head -200 src/login/login_screen.rs | grep -E "script_mod|:= |widget|View" | head -20 +``` + +留意项目常见模式:`View { width: Fill, ... }`、命名子 widget 用 `:= {{Type}}`、事件用 `click` action。 + +- [ ] **Step 2:替换 `src/register/register_screen.rs` 完整内容** + +```rust +//! RegisterScreen widget: homeserver picker + capability display. +//! +//! Phase 1 renders: +//! - Back button (returns to login) +//! - Screen title +//! - Homeserver URL input +//! - Next button (triggers capability discovery) +//! - Three-state status area (MAS / UIAA / Disabled / errors) +//! +//! Phases 2-5 fill in OIDC launch / UIAA form / SSO buttons. + +use makepad_widgets::*; +use crate::register::{RegisterAction, RegisterMode}; +use crate::register::validation::{normalize_homeserver_url, HomeserverUrlError}; +use crate::sliding_sync::{submit_async_request, MatrixRequest}; + +script_mod! { + use makepad_widgets::base::*; + use makepad_widgets::theme_desktop_dark::*; + + pub RegisterScreen := {{RegisterScreen}} View { + width: Fill, + height: Fill, + flow: Flow.Down, + padding: Inset { top: 24., right: 32., bottom: 24., left: 32. }, + spacing: 16., + show_bg: true, + draw_bg: { color: #x1F2124 } + + back_button :=