mirror of https://github.com/jackwener/wx-cli.git
parent
6659f48984
commit
5a4de7f83b
|
|
@ -28,3 +28,30 @@ __pycache__/
|
|||
Thumbs.db
|
||||
find_all_keys_macos
|
||||
.claude/worktrees/
|
||||
|
||||
# ── GSD baseline (auto-generated) ──
|
||||
.gsd
|
||||
.gsd-id
|
||||
.mcp.json
|
||||
.bg-shell/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.idea/
|
||||
.vscode/
|
||||
*.code-workspace
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
node_modules/
|
||||
.next/
|
||||
dist/
|
||||
build/
|
||||
*.pyc
|
||||
.venv/
|
||||
venv/
|
||||
vendor/
|
||||
*.log
|
||||
coverage/
|
||||
.cache/
|
||||
tmp/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
# Codebase Map
|
||||
|
||||
Generated: 2026-05-13T05:22:55Z | Files: 52 | Described: 0/52
|
||||
<!-- gsd:codebase-meta {"generatedAt":"2026-05-13T05:22:55Z","fingerprint":"3306bb8206b573a313a49bde0ead8c4bf8f719b8","fileCount":52,"truncated":false} -->
|
||||
|
||||
### (root)/
|
||||
- `.gitignore`
|
||||
- `AGENTS.md`
|
||||
- `Cargo.toml`
|
||||
- `CLAUDE.md`
|
||||
- `config.example.json`
|
||||
- `install.ps1`
|
||||
- `install.sh`
|
||||
- `LICENSE`
|
||||
- `README.md`
|
||||
- `SKILL.md`
|
||||
|
||||
### .github/workflows/
|
||||
- `.github/workflows/release.yml`
|
||||
|
||||
### docs/
|
||||
- `docs/macos-3x-vs-4x-decryption-guide.md`
|
||||
- `docs/macos-permission-guide.md`
|
||||
|
||||
### npm/platforms/darwin-arm64/
|
||||
- `npm/platforms/darwin-arm64/package.json`
|
||||
|
||||
### npm/platforms/darwin-x64/
|
||||
- `npm/platforms/darwin-x64/package.json`
|
||||
|
||||
### npm/platforms/linux-arm64/
|
||||
- `npm/platforms/linux-arm64/package.json`
|
||||
|
||||
### npm/platforms/linux-x64/
|
||||
- `npm/platforms/linux-x64/package.json`
|
||||
|
||||
### npm/platforms/win32-x64/
|
||||
- `npm/platforms/win32-x64/package.json`
|
||||
|
||||
### npm/wx-cli/
|
||||
- `npm/wx-cli/install.js`
|
||||
- `npm/wx-cli/package.json`
|
||||
|
||||
### npm/wx-cli/bin/
|
||||
- `npm/wx-cli/bin/wx.js`
|
||||
|
||||
### src/
|
||||
- `src/config.rs`
|
||||
- `src/ipc.rs`
|
||||
- `src/main.rs`
|
||||
|
||||
### src/cli/
|
||||
- `src/cli/contacts.rs`
|
||||
- `src/cli/daemon_cmd.rs`
|
||||
- `src/cli/export.rs`
|
||||
- `src/cli/favorites.rs`
|
||||
- `src/cli/history.rs`
|
||||
- `src/cli/init.rs`
|
||||
- `src/cli/members.rs`
|
||||
- `src/cli/mod.rs`
|
||||
- `src/cli/new_messages.rs`
|
||||
- `src/cli/output.rs`
|
||||
- `src/cli/search.rs`
|
||||
- `src/cli/sessions.rs`
|
||||
- `src/cli/sns_feed.rs`
|
||||
- `src/cli/sns_notifications.rs`
|
||||
- `src/cli/sns_search.rs`
|
||||
- `src/cli/stats.rs`
|
||||
- `src/cli/transport.rs`
|
||||
- `src/cli/unread.rs`
|
||||
|
||||
### src/crypto/
|
||||
- `src/crypto/mod.rs`
|
||||
- `src/crypto/wal.rs`
|
||||
|
||||
### src/daemon/
|
||||
- `src/daemon/cache.rs`
|
||||
- `src/daemon/mod.rs`
|
||||
- `src/daemon/query.rs`
|
||||
- `src/daemon/server.rs`
|
||||
|
||||
### src/scanner/
|
||||
- `src/scanner/linux.rs`
|
||||
- `src/scanner/macos.rs`
|
||||
- `src/scanner/mod.rs`
|
||||
- `src/scanner/windows.rs`
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# Project Context
|
||||
|
||||
Auto-detected by GSD init wizard. Edit or expand as needed.
|
||||
|
||||
## Language / Stack
|
||||
|
||||
Primary: rust
|
||||
|
||||
## Project Files
|
||||
|
||||
- Cargo.toml
|
||||
- .github/workflows
|
||||
|
||||
## CI/CD
|
||||
|
||||
CI configuration detected.
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# Decisions Register
|
||||
|
||||
<!-- Append-only. Never edit or remove existing rows.
|
||||
To reverse a decision, add a new row that supersedes it.
|
||||
Read this file at the start of any planning or research phase. -->
|
||||
|
||||
| # | When | Scope | Decision | Choice | Rationale | Revisable? | Made By |
|
||||
|---|------|-------|----------|--------|-----------|------------|---------|
|
||||
| D001 | | architecture | Transport abstraction via traits | Listener and Connector traits with shared protocol.rs, implementations for Unix/Windows/TCP | Eliminates ~50 lines of duplicated JSON-line protocol handling, provides clear extension point for future transports | Yes | collaborative |
|
||||
| D002 | | architecture | Global --tcp CLI flag for transport selection | Global clap flag on root Cli struct, inherited by all subcommands | Discoverable, consistent UX. User specifies once, affects all commands | Yes | human |
|
||||
| D003 | | architecture | No built-in TCP security | No TLS, no auth tokens, no IP whitelist in this milestone. Bind exactly as user specifies. | User handles firewall/ACL at OS level. TLS adds cert management and dependency complexity. Can be added later non-breaking. | Yes | collaborative |
|
||||
| D004 | | architecture | One request per connection protocol model | One JSON-line request per connection, no keepalive or pooling | Matches existing behavior, minimal complexity, sufficient for CLI usage patterns | Yes | agent |
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
version: 1
|
||||
mode: solo
|
||||
git:
|
||||
isolation: worktree
|
||||
main_branch: main
|
||||
auto_push: true
|
||||
verification_commands:
|
||||
- cargo test
|
||||
- cargo clippy
|
||||
---
|
||||
# GSD Project Preferences
|
||||
|
||||
Generated by `/gsd init`. Edit directly or use `/gsd prefs project` to modify.
|
||||
|
||||
See `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation.
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
# wx-cli
|
||||
|
||||
## What This Is
|
||||
|
||||
A cross-platform Rust CLI tool for extracting and querying local WeChat 4.x data. Decrypts SQLCipher-encrypted databases, caches decrypted copies with mtime-aware invalidation, and provides a daemon-based IPC architecture for fast repeated queries. Currently uses Unix sockets (macOS/Linux) and Windows named pipes for local-only communication.
|
||||
|
||||
## Core Value
|
||||
|
||||
Query your local WeChat chat history, contacts, and moments from the command line with millisecond response times — data never leaves your machine.
|
||||
|
||||
## Project Shape
|
||||
|
||||
- **Complexity:** simple
|
||||
- **Why:** Well-defined scope, existing codebase with clear module boundaries, single transport addition with refactoring
|
||||
|
||||
## Current State
|
||||
|
||||
Version 0.1.10. Fully functional CLI with 17 subcommands. Daemon auto-starts on first query. Cross-platform (macOS, Linux, Windows). No integration tests. Local IPC only.
|
||||
|
||||
## Architecture / Key Patterns
|
||||
|
||||
- Single binary: client and daemon (`WX_DAEMON_MODE` env var)
|
||||
- Daemon uses tokio async runtime, Unix socket / Windows named pipe IPC
|
||||
- JSON-line protocol: one request per connection
|
||||
- mtime-aware decryption cache in `~/.wx-cli/cache/`
|
||||
- Platform-specific memory scanners for SQLCipher key extraction
|
||||
- All queries executed via rusqlite on decrypted DBs
|
||||
|
||||
## Capability Contract
|
||||
|
||||
See `.gsd/REQUIREMENTS.md` for the explicit capability contract.
|
||||
|
||||
## Milestone Sequence
|
||||
|
||||
- [ ] M001: TCP Transport — Add `--tcp host:port` global flag and TCP transport support to daemon and client
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
# Requirements
|
||||
|
||||
## Active
|
||||
|
||||
### R001 — TCP transport on server
|
||||
- Class: core-capability
|
||||
- Status: active
|
||||
- Description: Daemon listens on TCP when `--tcp host:port` is specified, in addition to local transport
|
||||
- Why it matters: Enables remote clients to query WeChat data over network
|
||||
- Source: user
|
||||
- Primary owning slice: M001/S01
|
||||
- Supporting slices: M001/S02
|
||||
- Validation: unmapped
|
||||
- Notes: Bind exactly as user specifies, no TLS, no IP whitelist
|
||||
|
||||
### R002 — TCP transport on client
|
||||
- Class: core-capability
|
||||
- Status: active
|
||||
- Description: Client connects via TCP when `--tcp host:port` is specified, with no local fallback
|
||||
- Why it matters: Users explicitly choosing TCP must connect to that address
|
||||
- Source: user
|
||||
- Primary owning slice: M001/S02
|
||||
- Supporting slices: none
|
||||
- Validation: unmapped
|
||||
- Notes: Hard error if connection fails, no silent fallback
|
||||
|
||||
### R003 — Transport abstraction layer
|
||||
- Class: quality-attribute
|
||||
- Status: active
|
||||
- Description: Transport layer uses trait-based abstraction (Listener/Connector) to eliminate platform duplication
|
||||
- Why it matters: Makes adding new transports (TCP, future TLS) easy without duplicating protocol logic
|
||||
- Source: inferred
|
||||
- Primary owning slice: M001/S01
|
||||
- Supporting slices: M001/S02, M001/S03
|
||||
- Validation: unmapped
|
||||
- Notes: Must support Unix socket, Windows named pipe, and TCP
|
||||
|
||||
### R004 — Global `--tcp` CLI flag
|
||||
- Class: primary-user-loop
|
||||
- Status: active
|
||||
- Description: `--tcp host:port` is a global CLI flag, affecting all commands including `daemon status`, `daemon logs`, and all query commands
|
||||
- Why it matters: Discoverable, consistent interface for TCP across all commands
|
||||
- Source: user
|
||||
- Primary owning slice: M001/S02
|
||||
- Supporting slices: none
|
||||
- Validation: unmapped
|
||||
- Notes: Replaces env var approach, cleaner UX
|
||||
|
||||
### R005 — Daemon start command
|
||||
- Class: primary-user-loop
|
||||
- Status: active
|
||||
- Description: New `wx daemon start` subcommand to explicitly start the daemon with configurable options
|
||||
- Why it matters: Currently daemon auto-starts on first query; explicit start gives user control over transport config
|
||||
- Source: user
|
||||
- Primary owning slice: M001/S01
|
||||
- Supporting slices: none
|
||||
- Validation: unmapped
|
||||
- Notes: Should support `--tcp` flag
|
||||
|
||||
### R006 — Cross-platform compilation
|
||||
- Class: constraint
|
||||
- Status: active
|
||||
- Description: Code compiles on macOS, Linux, and Windows (`cargo check` on all targets)
|
||||
- Why it matters: Project is cross-platform by design, TCP must work on all three
|
||||
- Source: inferred
|
||||
- Primary owning slice: M001/S01
|
||||
- Supporting slices: M001/S02, M001/S03
|
||||
- Validation: unmapped
|
||||
- Notes: TcpListener/TcpStream are std library, should be trivial
|
||||
|
||||
### R007 — Error handling for TCP failures
|
||||
- Class: failure-visibility
|
||||
- Status: active
|
||||
- Description: TCP bind/connect failures produce clear error messages with no silent fallback
|
||||
- Why it matters: Users need to know when transport configuration fails
|
||||
- Source: inferred
|
||||
- Primary owning slice: M001/S02
|
||||
- Supporting slices: none
|
||||
- Validation: unmapped
|
||||
- Notes: 15s connect timeout, 120s read/write timeout
|
||||
|
||||
### R008 — Integration: CLI ↔ daemon over TCP
|
||||
- Class: integration
|
||||
- Status: active
|
||||
- Description: End-to-end verification: CLI and daemon communicate successfully over TCP on localhost
|
||||
- Why it matters: Proves the transport actually works, not just compiles
|
||||
- Source: inferred
|
||||
- Primary owning slice: M001/S04
|
||||
- Supporting slices: none
|
||||
- Validation: unmapped
|
||||
- Notes: Manual smoke test sufficient, no automated integration tests
|
||||
|
||||
## Deferred
|
||||
|
||||
### R020 — TLS encryption for TCP transport
|
||||
- Class: compliance/security
|
||||
- Status: deferred
|
||||
- Description: Optional TLS encryption on TCP transport for secure remote access
|
||||
- Why it matters: Plaintext TCP exposes chat data to network sniffing
|
||||
- Source: inferred
|
||||
- Primary owning slice: none
|
||||
- Supporting slices: none
|
||||
- Validation: unmapped
|
||||
- Notes: Deferred — adds tokio-rustls dependency and cert management complexity
|
||||
|
||||
### R021 — Authentication tokens for TCP
|
||||
- Class: compliance/security
|
||||
- Status: deferred
|
||||
- Description: Token-based authentication for TCP connections
|
||||
- Why it matters: Prevents unauthorized access to WeChat data over network
|
||||
- Source: inferred
|
||||
- Primary owning slice: none
|
||||
- Supporting slices: none
|
||||
- Validation: unmapped
|
||||
- Notes: Deferred — requires protocol change (Auth request type)
|
||||
|
||||
### R022 — TCP connection keepalive
|
||||
- Class: quality-attribute
|
||||
- Status: deferred
|
||||
- Description: Persistent TCP connections with keepalive for reduced latency
|
||||
- Why it matters: Current one-request-per-connection model has connection overhead
|
||||
- Source: inferred
|
||||
- Primary owning slice: none
|
||||
- Supporting slices: none
|
||||
- Validation: unmapped
|
||||
- Notes: Deferred — requires protocol and connection management changes
|
||||
|
||||
## Out of Scope
|
||||
|
||||
### R030 — Network-level access control
|
||||
- Class: constraint
|
||||
- Status: out-of-scope
|
||||
- Description: IP whitelisting, firewall rules, or network ACLs within the application
|
||||
- Why it matters: Prevents scope creep into network security management
|
||||
- Source: user
|
||||
- Primary owning slice: none
|
||||
- Supporting slices: none
|
||||
- Validation: n/a
|
||||
- Notes: User handles firewall/ACL at OS level
|
||||
|
||||
## Traceability
|
||||
|
||||
| ID | Class | Status | Primary owner | Supporting | Proof |
|
||||
|---|---|---|---|---|---|
|
||||
| R001 | core-capability | active | M001/S01 | M001/S02 | unmapped |
|
||||
| R002 | core-capability | active | M001/S02 | none | unmapped |
|
||||
| R003 | quality-attribute | active | M001/S01 | M001/S02, M001/S03 | unmapped |
|
||||
| R004 | primary-user-loop | active | M001/S02 | none | unmapped |
|
||||
| R005 | primary-user-loop | active | M001/S01 | none | unmapped |
|
||||
| R006 | constraint | active | M001/S01 | M001/S02, M001/S03 | unmapped |
|
||||
| R007 | failure-visibility | active | M001/S02 | none | unmapped |
|
||||
| R008 | integration | active | M001/S04 | none | unmapped |
|
||||
| R020 | compliance/security | deferred | none | none | unmapped |
|
||||
| R021 | compliance/security | deferred | none | none | unmapped |
|
||||
| R022 | quality-attribute | deferred | none | none | unmapped |
|
||||
| R030 | constraint | out-of-scope | none | none | n/a |
|
||||
|
||||
## Coverage Summary
|
||||
|
||||
- Active requirements: 8
|
||||
- Mapped to slices: 8
|
||||
- Validated: 0
|
||||
- Unmapped active requirements: 0
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# GSD State
|
||||
|
||||
**Active Milestone:** M001: TCP Transport
|
||||
**Active Slice:** S01: Transport abstraction layer
|
||||
**Phase:** planning
|
||||
**Requirements Status:** 0 active · 0 validated · 0 deferred · 0 out of scope
|
||||
|
||||
## Milestone Registry
|
||||
- 🔄 **M001:** TCP Transport
|
||||
|
||||
## Recent Decisions
|
||||
- None recorded
|
||||
|
||||
## Blockers
|
||||
- None
|
||||
|
||||
## Next Action
|
||||
Slice S01 has no DB tasks. Plan slice tasks before execution.
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
{"version":"1","eventId":"35a18d2d-ef3f-4957-8fa0-e018c9091b9b","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.292Z","payload":{"modelId":"claude-haiku-4.5","provider":"github-copilot","api":"anthropic-messages","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"bab2980a-198e-447a-a9aa-dd1e5d508edb","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.298Z","payload":{"modelId":"claude-sonnet-4.6","provider":"github-copilot","api":"anthropic-messages","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"54bb4aac-5585-47cc-9589-dec03ac30905","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.300Z","payload":{"modelId":"gemini-3.1-pro-preview","provider":"github-copilot","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"05db147d-4818-4a9d-be76-1945e659b5c8","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.303Z","payload":{"modelId":"gpt-4.1","provider":"github-copilot","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"1a145816-1b1d-4d84-8d1d-6a08c629e27c","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.305Z","payload":{"modelId":"gpt-4o","provider":"github-copilot","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"dc301e36-8e87-4b0e-9a7e-64cbc907480c","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.308Z","payload":{"modelId":"gpt-5-mini","provider":"github-copilot","api":"openai-responses","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"99fca380-cec0-412e-ba94-7b16c5496616","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.310Z","payload":{"modelId":"gpt-5.2-codex","provider":"github-copilot","api":"openai-responses","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"45fb32d0-10b3-43e6-9f4e-1a12eb2782d3","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.313Z","payload":{"modelId":"gpt-5.4","provider":"github-copilot","api":"openai-responses","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"996a6fef-9644-4881-be02-c3dffbfd5cf0","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.315Z","payload":{"modelId":"gpt-5.4-mini","provider":"github-copilot","api":"openai-responses","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"cf87b34b-c148-4eda-b6df-f688da404221","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.318Z","payload":{"modelId":"grok-code-fast-1","provider":"github-copilot","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"5b87f7ba-7eec-4bb4-900a-be5d84e0e5bd","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.320Z","payload":{"modelId":"groq/compound","provider":"groq","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"db19cee6-e80d-48fa-aa6e-199508e92d78","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.323Z","payload":{"modelId":"groq/compound-mini","provider":"groq","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"37e1b2cb-e7ad-4417-84ad-4f5c84ef5581","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.325Z","payload":{"modelId":"llama-3.1-8b-instant","provider":"groq","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"d03691e7-7a88-45a1-85db-889e3e99a183","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.328Z","payload":{"modelId":"llama-3.3-70b-versatile","provider":"groq","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"d956e2fd-0780-4d8a-891a-0f78cf4fb885","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.330Z","payload":{"modelId":"meta-llama/llama-4-scout-17b-16e-instruct","provider":"groq","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"c96af6c7-6ab8-49f7-9b2e-63ccc3b679be","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.332Z","payload":{"modelId":"openai/gpt-oss-120b","provider":"groq","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"1162b48d-9843-48ba-91db-58cf417d2b69","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.335Z","payload":{"modelId":"openai/gpt-oss-20b","provider":"groq","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"0eb90b03-3888-46a8-82d0-e7ec7ed69c92","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.337Z","payload":{"modelId":"openai/gpt-oss-safeguard-20b","provider":"groq","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"c1fd584d-bfdd-4245-a351-d81f8961474d","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.340Z","payload":{"modelId":"qwen/qwen3-32b","provider":"groq","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"ef3e9541-3dfe-4290-a5d0-ca5094a2a4ee","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.343Z","payload":{"modelId":"llm-p710","provider":"p710","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"988b326e-91d3-48ea-9f73-b90a60536d94","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.345Z","payload":{"modelId":"qwen3.6-plus","provider":"bailian","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"0a79bcf8-24ab-4f40-a1f5-f3c3ee43928f","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.347Z","payload":{"modelId":"qwen3.5-plus","provider":"bailian","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"b927da73-de27-4d01-be2a-9bbdeed206b6","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.350Z","payload":{"modelId":"qwen3-max-2026-01-23","provider":"bailian","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"06f49f12-432f-45e5-a5cf-8d0c4b9e4630","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.352Z","payload":{"modelId":"qwen3-coder-next","provider":"bailian","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"3b2834be-fa85-44e5-828d-7886bb24a1f5","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.355Z","payload":{"modelId":"qwen3-coder-plus","provider":"bailian","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"48ad4516-199b-48a3-aa1e-308a90de7d01","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.357Z","payload":{"modelId":"MiniMax-M2.5","provider":"bailian","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"d14885db-88a6-4c0f-9f25-d9a03e5002de","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.360Z","payload":{"modelId":"glm-5","provider":"bailian","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"39852010-4285-4fec-81c7-db5578eb845e","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.362Z","payload":{"modelId":"glm-4.7","provider":"bailian","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
{"version":"1","eventId":"84dc0529-e1d2-483c-81ef-edb89fcbba8f","traceId":"model:59b3413b-c62a-487e-acdb-3c4eddecc0a3:1778648805290","turnId":"discuss-milestone:","category":"model-policy","type":"model-policy-allow","ts":"2026-05-13T05:06:45.364Z","payload":{"modelId":"kimi-k2.5","provider":"bailian","api":"openai-completions","reason":"allowed","unitType":"discuss-milestone","requirements":{"reasoning":0.6,"instruction":0.7}}}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"pid": 42440,
|
||||
"startedAt": "2026-05-13T05:32:05.812Z",
|
||||
"unitType": "starting",
|
||||
"unitId": "bootstrap",
|
||||
"unitStartedAt": "2026-05-13T05:32:05.812Z"
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"v":2,"cmd":"plan-milestone","params":{"milestoneId":"M001"},"ts":"2026-05-13T05:31:30.650Z","actor":"agent","actor_name":"executor-01","trigger_reason":"plan-phase complete","hash":"e7646f64e62daa33","session_id":"dbe22dc5-e220-475e-aa1b-14d620ab6d6d"}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
# M001: TCP Transport
|
||||
|
||||
**Gathered:** 2026-01-13
|
||||
**Status:** Ready for planning
|
||||
|
||||
## Project Description
|
||||
|
||||
Add TCP socket transport to wx-cli's daemon communication layer, enabling remote clients to query WeChat data over the network. Refactor the existing platform-specific IPC code into a trait-based abstraction to eliminate duplication and make future transport additions easy.
|
||||
|
||||
## Why This Milestone
|
||||
|
||||
Currently wx-cli only supports local IPC (Unix sockets on macOS/Linux, named pipes on Windows). This limits usage to the same machine as the WeChat daemon. Adding TCP transport enables remote access, containerized deployments, and multi-machine setups.
|
||||
|
||||
## User-Visible Outcome
|
||||
|
||||
### When this milestone is complete, the user can:
|
||||
|
||||
- Start the daemon with TCP listening: `wx daemon start --tcp 127.0.0.1:9876`
|
||||
- Query WeChat data over TCP: `wx sessions --tcp 127.0.0.1:9876`
|
||||
- Use all existing commands without `--tcp` and get unchanged local behavior
|
||||
- Check daemon status and logs over TCP: `wx daemon status --tcp 127.0.0.1:9876`
|
||||
|
||||
### Entry point / environment
|
||||
|
||||
- Entry point: `wx` CLI command with global `--tcp host:port` flag
|
||||
- Environment: local dev or remote machine (TCP network)
|
||||
- Live dependencies involved: wx-daemon process
|
||||
|
||||
## Completion Class
|
||||
|
||||
- Contract complete means: Transport traits defined, all three implementations compile, protocol handling is shared
|
||||
- Integration complete means: Daemon listens on local + TCP simultaneously, client connects via TCP and gets correct response
|
||||
- Operational complete means: Daemon starts with `--tcp`, handles bind errors cleanly, client fails with clear error when TCP unreachable
|
||||
|
||||
## Final Integrated Acceptance
|
||||
|
||||
To call this milestone complete, we must prove:
|
||||
|
||||
- `cargo check` passes on macOS, Linux, and Windows targets
|
||||
- Daemon started with `--tcp 127.0.0.1:9876` accepts TCP connections and responds correctly
|
||||
- Client with `--tcp 127.0.0.1:9876` returns same results as local transport
|
||||
- Client with `--tcp 127.0.0.1:9999` (unreachable) fails with clear error within 15s
|
||||
- Commands without `--tcp` still work via local transport (no regression)
|
||||
|
||||
## Architectural Decisions
|
||||
|
||||
### Transport abstraction via traits
|
||||
|
||||
**Decision:** Use `Listener` and `Connector` traits to abstract transport primitives, implement for Unix socket, Windows named pipe, and TCP.
|
||||
|
||||
**Rationale:** Current code has ~50 lines of duplicated JSON-line protocol handling across Unix/Windows. Traits eliminate duplication and provide clear extension point for future transports (TLS, WebSocket).
|
||||
|
||||
**Alternatives Considered:**
|
||||
- Continue #[cfg] branching — current approach, hard to extend, duplicative
|
||||
- `interprocess` crate for all transports — doesn't support TCP natively
|
||||
- Abstract at protocol level only — would still need per-platform listener/connection code
|
||||
|
||||
### One request per connection (unchanged)
|
||||
|
||||
**Decision:** Keep existing protocol model — one JSON-line request per connection, no keepalive or pooling.
|
||||
|
||||
**Rationale:** Matches existing behavior, minimal complexity, sufficient for CLI usage patterns.
|
||||
|
||||
**Alternatives Considered:**
|
||||
- Persistent connections with multiplexing — adds protocol complexity, not needed for CLI
|
||||
- Connection pooling — overkill for single-client CLI tool
|
||||
|
||||
### Global CLI flag for TCP
|
||||
|
||||
**Decision:** `--tcp host:port` as global clap flag on root `Cli` struct, inherited by all subcommands.
|
||||
|
||||
**Rationale:** Discoverable, consistent UX. User specifies once, affects all commands.
|
||||
|
||||
**Alternatives Considered:**
|
||||
- Environment variables — hidden, harder to discover
|
||||
- Per-subcommand flag — repetitive, inconsistent
|
||||
- Config file only — requires edit before use
|
||||
|
||||
### No built-in TCP security
|
||||
|
||||
**Decision:** No TLS, no auth tokens, no IP whitelist in this milestone. Bind exactly as user specifies.
|
||||
|
||||
**Rationale:** User handles firewall/ACL at OS level. Adding TLS requires cert management, tokio-rustls dependency, and significantly more complexity. Can be added later non-breaking.
|
||||
|
||||
**Alternatives Considered:**
|
||||
- Default to localhost-only — too restrictive, user should control bind address
|
||||
- Built-in IP whitelist — adds config complexity, OS firewall is better tool
|
||||
|
||||
## Error Handling Strategy
|
||||
|
||||
- **TCP bind failure:** `"TCP bind failed on {addr}: {reason}"` — daemon aborts startup
|
||||
- **TCP connection failure:** `"Failed to connect to {addr}: {reason}"` — hard error, no fallback
|
||||
- **Connection timeout:** 15s connect, 120s read/write (matches existing)
|
||||
- **Connection dropped mid-request:** `"Connection lost: daemon closed or network error"`
|
||||
- **Mixed transport mismatch:** `"No daemon listening on {addr}"` — same as current "daemon not alive" path
|
||||
- **No `--tcp`:** Existing local transport behavior, no change
|
||||
|
||||
## Risks and Unknowns
|
||||
|
||||
- Windows named pipe refactoring may require `interprocess` crate changes — the crate's API differs from std Unix sockets
|
||||
- `daemon start` subcommand needs to handle existing auto-start behavior (currently daemon starts on first query via `ensure_daemon()`)
|
||||
|
||||
## Existing Codebase / Prior Art
|
||||
|
||||
- `src/daemon/server.rs` — current IPC server, needs refactoring to use Listener trait
|
||||
- `src/cli/transport.rs` — current IPC client, needs refactoring to use Connector trait
|
||||
- `src/ipc.rs` — protocol types (Request/Response), well-abstracted, no changes needed
|
||||
- `src/config.rs` — needs tcp_addr field extension
|
||||
|
||||
## Relevant Requirements
|
||||
|
||||
- R001 — TCP transport on server (M001/S01)
|
||||
- R002 — TCP transport on client (M001/S02)
|
||||
- R003 — Transport abstraction layer (M001/S01)
|
||||
- R004 — Global `--tcp` CLI flag (M001/S02)
|
||||
- R005 — Daemon start command (M001/S01)
|
||||
- R006 — Cross-platform compilation (M001/S01)
|
||||
- R007 — Error handling for TCP failures (M001/S02)
|
||||
- R008 — Integration: CLI ↔ daemon over TCP (M001/S04)
|
||||
|
||||
## Scope
|
||||
|
||||
### In Scope
|
||||
|
||||
- Trait-based transport abstraction (Listener, Connector)
|
||||
- TCP implementation (TcpListener, TcpStream)
|
||||
- Global `--tcp host:port` CLI flag
|
||||
- `wx daemon start` subcommand
|
||||
- Error handling for TCP failures
|
||||
- Cross-platform compilation
|
||||
|
||||
### Out of Scope / Non-Goals
|
||||
|
||||
- TLS encryption
|
||||
- Authentication tokens
|
||||
- IP whitelisting
|
||||
- Connection pooling / keepalive
|
||||
- Changing the JSON-line protocol
|
||||
|
||||
## Technical Constraints
|
||||
|
||||
- Must maintain backwards compatibility: no `--tcp` = existing behavior
|
||||
- tokio is already a dependency (TcpListener/TcpStream available)
|
||||
- `interprocess` crate for Windows named pipes — API differs from std
|
||||
|
||||
## Integration Points
|
||||
|
||||
- `src/daemon/server.rs` → `src/transport/` — server uses Listener trait
|
||||
- `src/cli/transport.rs` → `src/transport/` — client uses Connector trait
|
||||
- `src/config.rs` → optional tcp_addr field
|
||||
- `src/cli/mod.rs` → global --tcp flag on Cli struct
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
- `cargo check` on x86_64-unknown-linux-gnu, x86_64-pc-windows-msvc, and current platform
|
||||
- Unit tests for transport::protocol.rs (JSON round-trip)
|
||||
- Existing scanner tests continue passing
|
||||
- Manual smoke test: daemon on TCP, client queries over TCP
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- S01: Transport traits defined, all implementations compile on all platforms, existing behavior unchanged
|
||||
- S02: `wx daemon start --tcp 127.0.0.1:9876` starts daemon listening on TCP
|
||||
- S03: `wx sessions --tcp 127.0.0.1:9876` connects via TCP and returns correct results
|
||||
- S04: End-to-end TCP communication verified manually on localhost
|
||||
|
||||
## Open Questions
|
||||
|
||||
- None — scope confirmed, architecture agreed, error strategy defined
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# M001: TCP Transport
|
||||
|
||||
**Vision:** Add TCP socket transport to wx-cli's daemon communication layer with trait-based abstraction, enabling remote clients to query WeChat data over the network.
|
||||
|
||||
## Slices
|
||||
|
||||
- [ ] **S01: Transport abstraction layer** `risk:high` `depends:[]`
|
||||
> After this: Refactor complete, `cargo check` passes on all platforms, existing behavior unchanged. Transport traits defined and implemented for Unix socket + Windows named pipe.
|
||||
|
||||
- [ ] **S02: TCP server support** `risk:medium` `depends:[S01]`
|
||||
> After this: `wx daemon start --tcp 127.0.0.1:9876` starts daemon listening on TCP port 9876
|
||||
|
||||
- [ ] **S03: TCP client + global --tcp flag** `risk:medium` `depends:[S01]`
|
||||
> After this: `wx sessions --tcp 127.0.0.1:9876` connects via TCP and returns session data
|
||||
|
||||
- [ ] **S04: Integration smoke test** `risk:low` `depends:[S02,S03]`
|
||||
> After this: Daemon on TCP + client queries return same data as local transport
|
||||
|
||||
## Boundary Map
|
||||
|
||||
Not provided.
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
{"id":"ba5f368a-35d5-4624-bf93-53fd53117126","ts":"2026-05-13T04:37:51.107Z","severity":"info","message":"Use /gsd prefs project to update project preferences.","source":"notify","read":false}
|
||||
{"id":"826493b3-d1b2-481a-8989-db9d9c4217b7","ts":"2026-05-13T04:37:57.391Z","severity":"info","message":"Using existing global GSD skill preferences at C:\\Users\\david\\.gsd\\PREFERENCES.md","source":"notify","read":false}
|
||||
{"id":"25bd9781-bb82-4209-bb02-1a8b8b4d710a","ts":"2026-05-13T04:37:57.395Z","severity":"info","message":"GSD preferences (global) — pick a category to configure.","source":"notify","read":false}
|
||||
{"id":"bedf9f7f-38b0-4724-abd0-2f044eb0a7d2","ts":"2026-05-13T04:40:53.586Z","severity":"info","message":"Saved global preferences to C:\\Users\\david\\.gsd\\PREFERENCES.md","source":"notify","read":false}
|
||||
{"id":"2c83aa1f-d2a4-4faf-903e-9bc1b361a665","ts":"2026-05-13T04:41:31.640Z","severity":"warning","message":"[migration] Provider-specific default fallback used without an explicit available model; configure provider-aware model preferences before removing defaults.","source":"workflow-logger","read":false}
|
||||
{"id":"07df36ac-fb0f-4895-a3d9-845e563b47f3","ts":"2026-05-13T05:02:59.382Z","severity":"info","message":"Using existing global GSD skill preferences at C:\\Users\\david\\.gsd\\PREFERENCES.md","source":"notify","read":false}
|
||||
{"id":"0bf8cebb-68d5-4d14-9c6a-6cdda12b33e2","ts":"2026-05-13T05:02:59.385Z","severity":"info","message":"GSD preferences (global) — pick a category to configure.","source":"notify","read":false}
|
||||
{"id":"b91acb14-a891-4934-a0e2-4bec6e0ed178","ts":"2026-05-13T05:03:22.769Z","severity":"info","message":"Mode: solo — defaults: auto_push=true, push_branches=false, pre_merge_check=auto, merge_strategy=squash, isolation=worktree, unique_milestone_ids=false","source":"notify","read":false}
|
||||
{"id":"24385a47-fae4-433a-83c8-238dd1ee450d","ts":"2026-05-13T05:05:30.603Z","severity":"info","message":"Saved global preferences to C:\\Users\\david\\.gsd\\PREFERENCES.md","source":"notify","read":false}
|
||||
{"id":"52a852d3-aada-423a-9c23-0802a0735162","ts":"2026-05-13T05:05:41.228Z","severity":"info","message":"GSD — Get Shit Done\n\nQUICK START\n /gsd start <tpl> Start a workflow template\n /gsd Run next unit (same as /gsd next)\n /gsd auto Run all queued units continuously\n /gsd pause Pause auto-mode\n /gsd stop Stop auto-mode gracefully\n\nVISIBILITY\n /gsd status Dashboard (Ctrl+Alt+G / Ctrl+Shift+G)\n /gsd parallel watch Parallel monitor (Ctrl+Alt+P)\n /gsd notifications Notification history (Ctrl+Alt+N / Ctrl+Shift+N)\n /gsd visualize Intera…","source":"notify","read":false}
|
||||
{"id":"3d539aca-9626-4add-9792-45a220fde9ae","ts":"2026-05-13T05:05:44.707Z","severity":"info","message":"Project detected:\n rust project\n Project files: Cargo.toml, .github/workflows\n CI/CD: detected\n Verification: cargo test, cargo clippy","source":"notify","read":false}
|
||||
{"id":"1c077a14-1cba-4370-9e08-d3d4842b766e","ts":"2026-05-13T05:06:30.925Z","severity":"info","message":"Installing Rust, Skill Authoring, Document Handling skills...","source":"notify","read":false}
|
||||
{"id":"9da47155-1524-4474-8692-48e3e15ba878","ts":"2026-05-13T05:06:30.935Z","severity":"info","message":"Installing Rust Async Patterns, CI/CD Automation, Code Review & Quality, Git Advanced Workflows skills...","source":"notify","read":false}
|
||||
{"id":"dbdce8ef-473c-4855-b3ce-42b1040e74a3","ts":"2026-05-13T05:06:30.943Z","severity":"info","message":"Installing Skill Discovery skills...","source":"notify","read":false}
|
||||
{"id":"0d1ff703-1a0f-432b-9fac-4033d5069a2f","ts":"2026-05-13T05:06:30.950Z","severity":"info","message":"Failed to install Rust — try manually: npx skills add anthropics/skills","source":"notify","read":false}
|
||||
{"id":"8faf17c7-fa9f-4088-be28-583abc163801","ts":"2026-05-13T05:06:30.951Z","severity":"info","message":"Failed to install Rust Async Patterns — try manually: npx skills add wshobson/agents","source":"notify","read":false}
|
||||
{"id":"66f7d6e4-4ef7-46ab-901e-0de1faa4890c","ts":"2026-05-13T05:06:30.952Z","severity":"info","message":"Failed to install CI/CD Automation — try manually: npx skills add wshobson/agents","source":"notify","read":false}
|
||||
{"id":"3380ca5b-612f-48f4-8355-8c9096a0e43f","ts":"2026-05-13T05:06:30.952Z","severity":"info","message":"Failed to install Skill Discovery — try manually: npx skills add vercel-labs/skills","source":"notify","read":false}
|
||||
{"id":"99a403d9-4270-4dc7-a250-8a938736f717","ts":"2026-05-13T05:06:30.953Z","severity":"info","message":"Failed to install Skill Authoring — try manually: npx skills add anthropics/skills","source":"notify","read":false}
|
||||
{"id":"2939bc3a-0d64-431e-ae46-3823d62df1c0","ts":"2026-05-13T05:06:30.954Z","severity":"info","message":"Failed to install Document Handling — try manually: npx skills add anthropics/skills","source":"notify","read":false}
|
||||
{"id":"2f34be20-36b9-4d3f-815a-14f18e148396","ts":"2026-05-13T05:06:30.954Z","severity":"info","message":"Failed to install Code Review & Quality — try manually: npx skills add wshobson/agents","source":"notify","read":false}
|
||||
{"id":"f983e68f-0ee7-4af4-9e4a-04222c6f27e5","ts":"2026-05-13T05:06:30.955Z","severity":"info","message":"Failed to install Git Advanced Workflows — try manually: npx skills add wshobson/agents","source":"notify","read":false}
|
||||
{"id":"f246d948-6cff-405c-b6dd-a88d72c014ef","ts":"2026-05-13T05:06:43.256Z","severity":"info","message":"Codebase map generated: 52 files","source":"notify","read":false}
|
||||
{"id":"a6ccb4fe-fc00-4162-bbd3-2e8a1a8684c4","ts":"2026-05-13T05:06:43.514Z","severity":"info","message":"GSD initialized. Starting your first milestone...","source":"notify","read":false}
|
||||
{"id":"29a93a25-8daa-44a1-8eb5-4a4e9b01e59d","ts":"2026-05-13T05:06:45.030Z","severity":"info","message":"Analyzing codebase...","source":"notify","read":false}
|
||||
{"id":"6a93a35a-02cf-4b7e-8367-79fa01bfed6e","ts":"2026-05-13T05:06:45.043Z","severity":"success","message":"✓ Analyzed codebase","source":"notify","read":false}
|
||||
{"id":"853a2d66-548f-4b18-84fe-71c74b8741b5","ts":"2026-05-13T05:06:45.044Z","severity":"info","message":"Reviewing prior context...","source":"notify","read":false}
|
||||
{"id":"39321b35-1395-4a4b-8f04-63a8d9d8129b","ts":"2026-05-13T05:06:45.053Z","severity":"success","message":"✓ Reviewed prior context","source":"notify","read":false}
|
||||
{"id":"0fa5db21-6a50-4fec-9ddd-c0020c8c35f4","ts":"2026-05-13T05:23:12.475Z","severity":"info","message":"discuss-milestone M001 is waiting for your approval - pausing before more tool calls run.","source":"notify","read":false}
|
||||
{"id":"24d68af1-955c-4fbd-a841-357605a9c2f0","ts":"2026-05-13T05:25:36.823Z","severity":"info","message":"discuss-milestone M001 is waiting for your approval - pausing before more tool calls run.","source":"notify","read":false}
|
||||
{"id":"b8ff6fb1-2775-4790-a494-2c0da94f8ee1","ts":"2026-05-13T05:32:05.786Z","severity":"success","message":"Milestone M001 ready.","source":"notify","read":false}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
{
|
||||
"version": 1,
|
||||
"exported_at": "2026-05-13T05:31:30.649Z",
|
||||
"milestones": [
|
||||
{
|
||||
"id": "M001",
|
||||
"title": "TCP Transport",
|
||||
"status": "active",
|
||||
"depends_on": [],
|
||||
"created_at": "2026-05-13T05:31:30.629Z",
|
||||
"completed_at": null,
|
||||
"vision": "Add TCP socket transport to wx-cli's daemon communication layer with trait-based abstraction, enabling remote clients to query WeChat data over the network.",
|
||||
"success_criteria": [],
|
||||
"key_risks": [
|
||||
{
|
||||
"risk": "Windows named pipe refactoring may require interprocess crate API changes",
|
||||
"whyItMatters": "interprocess crate API differs from std Unix sockets, may need adaptation for trait compatibility"
|
||||
},
|
||||
{
|
||||
"risk": "Daemon start subcommand conflicts with existing auto-start behavior",
|
||||
"whyItMatters": "Currently daemon auto-starts on first query via ensure_daemon(). New explicit start must coexist."
|
||||
}
|
||||
],
|
||||
"proof_strategy": [
|
||||
{
|
||||
"riskOrUnknown": "Windows named pipe trait compatibility with interprocess crate",
|
||||
"retireIn": "S01",
|
||||
"whatWillBeProven": "Transport traits work for Unix socket and named pipe"
|
||||
},
|
||||
{
|
||||
"riskOrUnknown": "TCP bind and accept behavior on all platforms",
|
||||
"retireIn": "S02",
|
||||
"whatWillBeProven": "TCP listener accepts connections and handles requests correctly"
|
||||
},
|
||||
{
|
||||
"riskOrUnknown": "Protocol works identically over TCP as over local transport",
|
||||
"retireIn": "S04",
|
||||
"whatWillBeProven": "End-to-end TCP communication returns correct results"
|
||||
}
|
||||
],
|
||||
"verification_contract": "- Contract verification: `cargo check` on all three targets, unit tests for protocol handling\n- Integration verification: Manual smoke test of CLI ↔ daemon over TCP on localhost\n- Operational verification: Daemon starts/stops cleanly with TCP, handles bind errors\n- UAT / human verification: Verify TCP results match local transport results",
|
||||
"verification_integration": "Manual smoke test: daemon on TCP + client queries return same data as local transport",
|
||||
"verification_operational": "Daemon starts with --tcp, binds to specified address, handles errors cleanly. Client connects via TCP, fails clearly on unreachable address.",
|
||||
"verification_uat": "",
|
||||
"definition_of_done": [
|
||||
"All slice deliverables complete",
|
||||
"Transport abstraction layer wired into daemon and client",
|
||||
"cargo check passes on macOS, Linux, and Windows targets",
|
||||
"Daemon can listen on local + TCP simultaneously",
|
||||
"Client connects via TCP with --tcp, errors clearly on failure",
|
||||
"wx daemon start subcommand works",
|
||||
"Manual smoke test passes: CLI ↔ daemon over TCP on localhost",
|
||||
"No regression to local-only transport"
|
||||
],
|
||||
"requirement_coverage": "Covers: R001, R002, R003, R004, R005, R006, R007, R008\nPartially covers: none\nLeaves for later: R020 (TLS), R021 (Auth), R022 (Keepalive)\nOrphan risks: none",
|
||||
"boundary_map_markdown": "Not provided.",
|
||||
"sequence": 0
|
||||
}
|
||||
],
|
||||
"slices": [
|
||||
{
|
||||
"milestone_id": "M001",
|
||||
"id": "S01",
|
||||
"title": "Transport abstraction layer",
|
||||
"status": "pending",
|
||||
"risk": "high",
|
||||
"depends": [],
|
||||
"demo": "Refactor complete, `cargo check` passes on all platforms, existing behavior unchanged. Transport traits defined and implemented for Unix socket + Windows named pipe.",
|
||||
"created_at": "2026-05-13T05:31:30.630Z",
|
||||
"completed_at": null,
|
||||
"full_summary_md": "",
|
||||
"full_uat_md": "",
|
||||
"goal": "Refactor transport layer into trait-based abstraction, eliminating platform-specific duplication. Implement Unix socket and Windows named pipe using new traits. Add `wx daemon start` subcommand.",
|
||||
"success_criteria": "- `cargo check` passes on all three target platforms\n- Existing CLI commands work unchanged (no regression)\n- Transport traits (Listener, Connector) defined in `src/transport/traits.rs`\n- Protocol handling shared in `src/transport/protocol.rs`\n- `wx daemon start` subcommand exists and starts daemon",
|
||||
"proof_level": "contract",
|
||||
"integration_closure": "Daemon starts via `wx daemon start` and listens on local transport (Unix socket / named pipe). Client queries work via local transport as before.",
|
||||
"observability_impact": "Daemon startup logs show which transports are active",
|
||||
"sequence": 1,
|
||||
"replan_triggered_at": null,
|
||||
"is_sketch": 0,
|
||||
"sketch_scope": ""
|
||||
},
|
||||
{
|
||||
"milestone_id": "M001",
|
||||
"id": "S02",
|
||||
"title": "TCP server support",
|
||||
"status": "pending",
|
||||
"risk": "medium",
|
||||
"depends": [
|
||||
"S01"
|
||||
],
|
||||
"demo": "`wx daemon start --tcp 127.0.0.1:9876` starts daemon listening on TCP port 9876",
|
||||
"created_at": "2026-05-13T05:31:30.630Z",
|
||||
"completed_at": null,
|
||||
"full_summary_md": "",
|
||||
"full_uat_md": "",
|
||||
"goal": "Implement TCP server support. Add `--tcp` flag to daemon start. Daemon listens on local transport AND TCP simultaneously when --tcp is specified.",
|
||||
"success_criteria": "- `wx daemon start --tcp 127.0.0.1:9876` starts daemon on both local and TCP\n- TCP connections accepted and handled correctly\n- TCP bind failure produces clear error message\n- `cargo check` passes on all platforms",
|
||||
"proof_level": "contract",
|
||||
"integration_closure": "Daemon accepts TCP connections. JSON-line protocol works over TCP. Bind errors are clear.",
|
||||
"observability_impact": "Daemon logs show TCP bind address and accepted connections",
|
||||
"sequence": 2,
|
||||
"replan_triggered_at": null,
|
||||
"is_sketch": 0,
|
||||
"sketch_scope": ""
|
||||
},
|
||||
{
|
||||
"milestone_id": "M001",
|
||||
"id": "S03",
|
||||
"title": "TCP client + global --tcp flag",
|
||||
"status": "pending",
|
||||
"risk": "medium",
|
||||
"depends": [
|
||||
"S01"
|
||||
],
|
||||
"demo": "`wx sessions --tcp 127.0.0.1:9876` connects via TCP and returns session data",
|
||||
"created_at": "2026-05-13T05:31:30.630Z",
|
||||
"completed_at": null,
|
||||
"full_summary_md": "",
|
||||
"full_uat_md": "",
|
||||
"goal": "Implement TCP client support. Add global `--tcp host:port` flag to CLI. Client connects directly via TCP when flag is specified, with no local fallback.",
|
||||
"success_criteria": "- `wx sessions --tcp 127.0.0.1:9876` connects via TCP and returns results\n- `wx sessions --tcp 127.0.0.1:9999` fails with clear error within 15s\n- `wx daemon status --tcp 127.0.0.1:9876` works over TCP\n- `wx daemon logs --tcp 127.0.0.1:9876` works over TCP\n- All commands without --tcp still work via local transport",
|
||||
"proof_level": "contract",
|
||||
"integration_closure": "Client connects via TCP to running daemon. All query commands work. Error messages clear on failure.",
|
||||
"observability_impact": "Client error messages show TCP address when connection fails",
|
||||
"sequence": 3,
|
||||
"replan_triggered_at": null,
|
||||
"is_sketch": 0,
|
||||
"sketch_scope": ""
|
||||
},
|
||||
{
|
||||
"milestone_id": "M001",
|
||||
"id": "S04",
|
||||
"title": "Integration smoke test",
|
||||
"status": "pending",
|
||||
"risk": "low",
|
||||
"depends": [
|
||||
"S02",
|
||||
"S03"
|
||||
],
|
||||
"demo": "Daemon on TCP + client queries return same data as local transport",
|
||||
"created_at": "2026-05-13T05:31:30.630Z",
|
||||
"completed_at": null,
|
||||
"full_summary_md": "",
|
||||
"full_uat_md": "",
|
||||
"goal": "End-to-end integration verification. Daemon on TCP, client queries over TCP, results match local transport.",
|
||||
"success_criteria": "- Manual smoke test: daemon started with --tcp, client queries over TCP\n- TCP and local transport return identical results\n- No regression to local-only mode\n- All cargo check targets pass",
|
||||
"proof_level": "integration",
|
||||
"integration_closure": "Full end-to-end: CLI ↔ daemon over TCP on localhost, same results as local transport.",
|
||||
"observability_impact": "Observable end-to-end TCP communication between CLI and daemon",
|
||||
"sequence": 4,
|
||||
"replan_triggered_at": null,
|
||||
"is_sketch": 0,
|
||||
"sketch_scope": ""
|
||||
}
|
||||
],
|
||||
"tasks": [],
|
||||
"decisions": [],
|
||||
"verification_evidence": []
|
||||
}
|
||||
|
|
@ -0,0 +1,455 @@
|
|||
# wx-cli Architecture Analysis
|
||||
|
||||
## Overview
|
||||
|
||||
**wx-cli** is a cross-platform Rust CLI tool for extracting and querying local WeChat 4.x data. It decrypts SQLCipher-encrypted databases, caches decrypted copies with mtime-aware invalidation, and provides a daemon-based IPC architecture for fast repeated queries.
|
||||
|
||||
**Key characteristics:**
|
||||
- Single binary, zero runtime dependencies
|
||||
- Cross-platform: macOS, Linux, Windows
|
||||
- Millisecond response times via daemon caching
|
||||
- AI-friendly output (YAML by default, JSON optional)
|
||||
- All data processed locally, no network calls
|
||||
|
||||
---
|
||||
|
||||
## High-Level Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ wx (CLI client) │
|
||||
│ src/cli/mod.rs - clap-based command parsing │
|
||||
│ Commands: init, sessions, history, search, contacts, export, │
|
||||
│ unread, members, new-messages, stats, favorites, sns-* │
|
||||
└────────────────────────────┬────────────────────────────────────────┘
|
||||
│ IPC (Unix socket / Windows named pipe)
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ wx-daemon (background process) │
|
||||
│ src/daemon/mod.rs - tokio async runtime │
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
|
||||
│ │ DbCache │ │ Names │ │ IPC Server │ │
|
||||
│ │ (mtime-aware)│ │ (contact map)│ │ (JSON line protocol) │ │
|
||||
│ │ src/daemon/ │ │ src/daemon/ │ │ src/daemon/ │ │
|
||||
│ │ cache.rs │ │ query.rs │ │ server.rs │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────────────┘ │
|
||||
│ │
|
||||
│ On startup: │
|
||||
│ 1. Load config + keys from ~/.wx-cli/ │
|
||||
│ 2. Pre-warm: decrypt session.db, sns.db, load contacts │
|
||||
│ 3. Listen on socket/pipe for requests │
|
||||
└────────────────────────────┬────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Crypto Layer │
|
||||
│ src/crypto/mod.rs + wal.rs │
|
||||
│ │
|
||||
│ - SQLCipher 4 page decryption (AES-256-CBC) │
|
||||
│ - WAL (Write-Ahead Log) application │
|
||||
│ - Streaming decryption (page-by-page, avoids full-file load) │
|
||||
└────────────────────────────┬────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Scanner Layer │
|
||||
│ src/scanner/{macos,linux,windows}.rs │
|
||||
│ │
|
||||
│ Platform-specific memory scanners: │
|
||||
│ - macOS: Mach VM API (task_for_pid, mach_vm_region, mach_vm_read) │
|
||||
│ - Linux: /proc/<pid>/mem + /proc/<pid>/maps │
|
||||
│ - Windows: CreateToolhelp32Snapshot + ReadProcessMemory │
|
||||
│ │
|
||||
│ Pattern: x'<64hex_key><32hex_salt>' in WeChat process memory │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Module Breakdown
|
||||
|
||||
### 1. Entry Point (`src/main.rs`)
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
if std::env::var("WX_DAEMON_MODE").is_ok() {
|
||||
daemon::run(); // Background daemon mode
|
||||
} else {
|
||||
cli::run(); // CLI client mode
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Single binary acts as both client and daemon. Daemon spawned via `WX_DAEMON_MODE=1` env var.
|
||||
|
||||
---
|
||||
|
||||
### 2. CLI Layer (`src/cli/`)
|
||||
|
||||
**`mod.rs`** - Command definitions via clap derive macros:
|
||||
- 17 subcommands (Init, Sessions, History, Search, Contacts, Export, Unread, Members, NewMessages, Stats, Favorites, SnsNotifications, SnsFeed, SnsSearch, Daemon)
|
||||
- Each command dispatches to dedicated module (e.g., `history::cmd_history`)
|
||||
- All commands share `--json` flag for output format toggle
|
||||
|
||||
**`transport.rs`** - IPC client:
|
||||
- `ensure_daemon()` - auto-start daemon if not running
|
||||
- `send()` - JSON line protocol over Unix socket / Windows named pipe
|
||||
- Timeout handling (15s startup, 120s request)
|
||||
- Permission preflight check for ~/.wx-cli/ directory
|
||||
|
||||
**Command modules** (`sessions.rs`, `history.rs`, etc.):
|
||||
- Parse CLI args → build IPC `Request`
|
||||
- Send to daemon → receive `Response`
|
||||
- Format output (YAML/JSON) via `output.rs`
|
||||
|
||||
---
|
||||
|
||||
### 3. Daemon Layer (`src/daemon/`)
|
||||
|
||||
**`mod.rs`** - Daemon lifecycle:
|
||||
```rust
|
||||
async fn async_run() -> Result<()> {
|
||||
// 1. Create ~/.wx-cli/ + cache/ directories
|
||||
// 2. Write PID file
|
||||
// 3. Setup signal handlers (SIGTERM/SIGINT)
|
||||
// 4. Load config + keys
|
||||
// 5. Initialize DbCache (mtime-aware decryption cache)
|
||||
// 6. Pre-warm: load contacts, decrypt session.db + sns.db
|
||||
// 7. Start IPC server (blocking loop)
|
||||
}
|
||||
```
|
||||
|
||||
**`cache.rs`** - DbCache (critical performance component):
|
||||
- `HashMap<String, CacheEntry>` in-memory cache
|
||||
- `CacheEntry`: `{ db_mtime, wal_mtime, decrypted_path }`
|
||||
- **mtime-aware invalidation**: re-decrypt only when `.db` or `.db-wal` mtime changes
|
||||
- Persistent mtime records in `~/.wx-cli/cache/_mtimes.json`
|
||||
- Cache reuse on daemon restart (avoids re-decryption)
|
||||
- Uses MD5 hash of rel_key for cache filename
|
||||
|
||||
**`server.rs`** - IPC server:
|
||||
- Unix: `tokio::net::UnixListener` on `~/.wx-cli/daemon.sock`
|
||||
- Windows: `interprocess` named pipe `\\.\pipe\wx-cli-daemon`
|
||||
- One connection per request, JSON line protocol
|
||||
- `dispatch()` routes `Request` → query functions
|
||||
|
||||
**`query.rs`** - Query implementations (~1500 lines):
|
||||
- `Names` struct: contact name cache + MD5→username lookup + verify_flags
|
||||
- `chat_type_of()`: classify as `private`/`group`/`official_account`/`folded`
|
||||
- Query functions: `q_sessions`, `q_history`, `q_search`, `q_contacts`, `q_unread`, `q_members`, `q_new_messages`, `q_stats`, `q_favorites`, `q_sns_*`
|
||||
- Message parsing: zstd decompression, XML extraction (appmsg, sysmsg, revokemsg)
|
||||
- Uses `spawn_blocking` for SQLite queries (rusqlite is sync)
|
||||
|
||||
---
|
||||
|
||||
### 4. Crypto Layer (`src/crypto/`)
|
||||
|
||||
**`mod.rs`** - SQLCipher 4 decryption:
|
||||
```rust
|
||||
// Constants
|
||||
PAGE_SZ = 4096
|
||||
SALT_SZ = 16
|
||||
RESERVE_SZ = 80 // IV(16) + HMAC(64)
|
||||
|
||||
// Key operations
|
||||
fn decrypt_page(enc_key: &[u8; 32], page_data: &[u8], pgno: u32) -> Vec<u8>
|
||||
fn full_decrypt(db_path: &Path, out_path: &Path, enc_key: &[u8; 32])
|
||||
```
|
||||
|
||||
**Algorithm:**
|
||||
- AES-256-CBC decryption
|
||||
- IV located at page end: `PAGE_SZ - RESERVE_SZ` offset
|
||||
- Page 1 special handling: skip 16-byte SALT, write SQLite magic header
|
||||
- Other pages: decrypt `[0..PAGE_SZ-RESERVE_SZ]`
|
||||
- Streaming (page-by-page) to avoid full-file memory load
|
||||
|
||||
**`wal.rs`** - WAL application:
|
||||
- WAL header: 32 bytes (magic, format, page_sz, ckpt_seq, salt1/2, cksum1/2)
|
||||
- Frame: 24-byte header + PAGE_SZ data
|
||||
- Frame matching via salt1/2 validation
|
||||
- Random-write to decrypted DB at `(pgno-1) * PAGE_SZ`
|
||||
|
||||
---
|
||||
|
||||
### 5. Scanner Layer (`src/scanner/`)
|
||||
|
||||
**Common interface** (`mod.rs`):
|
||||
```rust
|
||||
pub struct KeyEntry {
|
||||
db_name: String, // relative path
|
||||
enc_key: String, // 64-char hex (32 bytes)
|
||||
salt: String, // 32-char hex (16 bytes)
|
||||
}
|
||||
|
||||
pub fn scan_keys(db_dir: &Path) -> Result<Vec<KeyEntry>> // platform-specific
|
||||
pub fn read_db_salt(path: &Path) -> Option<String>
|
||||
pub fn collect_db_salts(db_dir: &Path) -> Vec<(String, String)>
|
||||
```
|
||||
|
||||
**Pattern searched**: `x'<96 hex chars>'` = 64-char key + 32-char salt
|
||||
|
||||
**macOS** (`macos.rs`):
|
||||
- `task_for_pid` → get Mach task port (requires root + ad-hoc signed WeChat)
|
||||
- `mach_vm_region` → enumerate VM regions
|
||||
- `mach_vm_read` → read 2MB chunks
|
||||
- Filter: `VM_PROT_READ | VM_PROT_WRITE` regions only
|
||||
- Deduplication by (key, salt) pair
|
||||
|
||||
**Linux** (`linux.rs`):
|
||||
- `/proc/<pid>/comm` → find `wechat`/`weixin` process
|
||||
- `/proc/<pid>/maps` → parse `rw-` regions
|
||||
- `/proc/<pid>/mem` → seek + read
|
||||
- Same chunk/dedup strategy
|
||||
|
||||
**Windows** (`windows.rs`):
|
||||
- `CreateToolhelp32Snapshot` → find `Weixin.exe`
|
||||
- `OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION)`
|
||||
- `VirtualQueryEx` → enumerate `MEM_COMMIT + PAGE_READWRITE` regions
|
||||
- `ReadProcessMemory` → chunk read
|
||||
|
||||
---
|
||||
|
||||
### 6. IPC Protocol (`src/ipc.rs`)
|
||||
|
||||
**Request** (tagged enum):
|
||||
```rust
|
||||
pub enum Request {
|
||||
Ping,
|
||||
Sessions { limit: usize },
|
||||
History { chat, limit, offset, since, until, msg_type },
|
||||
Search { keyword, chats, limit, since, until, msg_type },
|
||||
Contacts { query, limit },
|
||||
Unread { limit, filter },
|
||||
Members { chat },
|
||||
NewMessages { state, limit },
|
||||
Stats { chat, since, until },
|
||||
Favorites { limit, fav_type, query },
|
||||
SnsNotifications { limit, since, until, include_read },
|
||||
SnsFeed { limit, since, until, user },
|
||||
SnsSearch { keyword, limit, since, until, user },
|
||||
}
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```rust
|
||||
pub struct Response {
|
||||
ok: bool,
|
||||
error: Option<String>,
|
||||
data: Value, // flattened JSON
|
||||
}
|
||||
```
|
||||
|
||||
Protocol: newline-delimited JSON, one request per connection.
|
||||
|
||||
---
|
||||
|
||||
### 7. Config Layer (`src/config.rs`)
|
||||
|
||||
**Config struct**:
|
||||
```rust
|
||||
pub struct Config {
|
||||
db_dir: PathBuf, // WeChat db_storage path
|
||||
keys_file: PathBuf, // all_keys.json
|
||||
decrypted_dir: PathBuf, // (unused, cache dir used instead)
|
||||
wechat_process: String, // process name for scanner
|
||||
}
|
||||
```
|
||||
|
||||
**Paths**:
|
||||
- `cli_dir()`: `~/.wx-cli/`
|
||||
- `sock_path()`: `~/.wx-cli/daemon.sock`
|
||||
- `cache_dir()`: `~/.wx-cli/cache/`
|
||||
- `mtime_file()`: `~/.wx-cli/cache/_mtimes.json`
|
||||
|
||||
**Auto-detection** (`auto_detect_db_dir()`):
|
||||
- macOS: `~/Library/Containers/com.tencent.xinWeChat/.../xwechat_files/*/db_storage`
|
||||
- Linux: `~/Documents/xwechat_files/*/db_storage` + legacy path
|
||||
- Windows: `%APPDATA%/Tencent/xwechat/config/*.ini` → parse data root
|
||||
|
||||
---
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Init Flow (`wx init`)
|
||||
|
||||
```
|
||||
1. Auto-detect db_dir → scan for db_storage directory
|
||||
2. collect_db_salts(db_dir) → (salt_hex, rel_path) list
|
||||
3. scan_keys(db_dir) → memory scan → (key_hex, salt_hex) candidates
|
||||
4. Match: salt_hex == db_salt → KeyEntry { db_name, enc_key, salt }
|
||||
5. Write ~/.wx-cli/config.json + ~/.wx-cli/all_keys.json
|
||||
```
|
||||
|
||||
### Query Flow (e.g., `wx history "张三"`)
|
||||
|
||||
```
|
||||
1. CLI: parse args → Request::History { chat: "张三", limit: 50 }
|
||||
2. transport::ensure_daemon() → start if not alive
|
||||
3. transport::send(Request) → Unix socket/pipe → daemon
|
||||
4. daemon::dispatch(Request) → q_history()
|
||||
a. resolve_username("张三") → "wxid_xxx" (fuzzy match against Names)
|
||||
b. find_msg_tables(db, names, username) → [(db_path, "Msg_<md5>")]
|
||||
c. spawn_blocking: SQLite query on decrypted db_path
|
||||
d. decompress_message (zstd) + fmt_content (XML parsing)
|
||||
5. Response::ok(json!{ chat, messages, ... })
|
||||
6. CLI: output.rs → YAML/JSON formatting
|
||||
```
|
||||
|
||||
### Decryption Flow (DbCache::get)
|
||||
|
||||
```
|
||||
1. Check in-memory cache: if entry.mtime matches → return cached path
|
||||
2. mtime mismatch or missing → spawn_blocking decrypt:
|
||||
a. crypto::full_decrypt(db_path, out_path, enc_key)
|
||||
b. If .db-wal exists: wal::apply_wal(wal_path, out_path, enc_key)
|
||||
3. Update cache entry + persist mtimes to _mtimes.json
|
||||
4. Return decrypted path for SQLite query
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Schema Knowledge
|
||||
|
||||
**session/session.db**:
|
||||
- `SessionTable`: username, unread_count, summary, last_timestamp, last_msg_type, last_msg_sender
|
||||
|
||||
**contact/contact.db**:
|
||||
- `contact`: username, nick_name, remark, verify_flag
|
||||
- `chat_room`: id, owner (for group info)
|
||||
- `chatroom_member`: room_id, member_id (joined with contact)
|
||||
|
||||
**message/message_N.db**:
|
||||
- `Msg_<md5(username)>`: local_id, local_type, create_time, real_sender_id, message_content, WCDB_CT_message_content
|
||||
- `Name2Id`: rowid → user_name (sender lookup)
|
||||
- WCDB_CT = 4 means zstd compression
|
||||
|
||||
**sns/sns.db**:
|
||||
- `sns_notification`: type (like/comment), from_nickname, content, feed_preview
|
||||
- `sns_feed_xml`: author, contentDesc, media XML, createTime
|
||||
|
||||
**favorite/favorite.db**:
|
||||
- `fav_db_item`: local_id, type, update_time, content, fromusr
|
||||
|
||||
---
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
1. **mtime-aware caching**: Only re-decrypt when source file changes
|
||||
2. **Pre-warming**: Decrypt session.db + sns.db + contacts on daemon start
|
||||
3. **Arc-wrapped Names**: Contact cache shared via Arc, cloned in O(1)
|
||||
4. **spawn_blocking**: Sync SQLite ops off async runtime
|
||||
5. **Streaming decrypt**: Page-by-page, no full file in memory
|
||||
6. **WAL handling**: Apply uncommitted writes without re-decrypt
|
||||
7. **MD5 table lookup**: `Msg_<md5>` → username via precomputed hash map
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Root/Admin required**: Memory scan needs elevated privileges
|
||||
2. **No secrets logged**: Keys written to file, never echoed
|
||||
3. **Socket permissions**: Unix socket mode 0600
|
||||
4. **Local-only**: All IPC is localhost, no network exposure
|
||||
5. **User consent implied**: Only decrypts own WeChat data
|
||||
|
||||
---
|
||||
|
||||
## Error Handling Patterns
|
||||
|
||||
- `anyhow::Result` throughout
|
||||
- Context messages for chain debugging
|
||||
- Graceful degradation: missing tables → fallback paths
|
||||
- Preflight checks (e.g., ~/.wx-cli writable before daemon spawn)
|
||||
- Signal handlers for clean shutdown (socket/PID file cleanup)
|
||||
|
||||
---
|
||||
|
||||
## Cross-Platform Notes
|
||||
|
||||
| Platform | Scanner API | IPC | Privilege | DB Path |
|
||||
|----------|-------------|-----|-----------|---------|
|
||||
| macOS | Mach VM | Unix socket | sudo + codesign | ~/Library/Containers/... |
|
||||
| Linux | /proc/pid/mem | Unix socket | sudo | ~/Documents/xwechat_files |
|
||||
| Windows | ToolHelp + ReadProcessMemory | Named pipe | Admin | %APPDATA%/Tencent/xwechat |
|
||||
|
||||
---
|
||||
|
||||
## Testing Coverage
|
||||
|
||||
- `src/crypto/mod.rs`: hex encoding, salt reading, recursive collection
|
||||
- `src/scanner/macos.rs`: pattern matching (uppercase, dedup, embedded, edge cases)
|
||||
- Unit tests for helper functions; integration tests would require live WeChat
|
||||
|
||||
---
|
||||
|
||||
## Extension Points
|
||||
|
||||
1. **New commands**: Add to `cli/mod.rs` enum + dispatch + query.rs function
|
||||
2. **New message types**: Extend `fmt_type()` + `fmt_content()` parsers
|
||||
3. **New DB sources**: Add to DbCache key list + query functions
|
||||
4. **Output formats**: Extend `output.rs` formatter
|
||||
|
||||
---
|
||||
|
||||
## File Structure Summary
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.rs # Entry point (daemon/CLI switch)
|
||||
├── config.rs # Config loading + auto-detect
|
||||
├── ipc.rs # Request/Response protocol types
|
||||
├── cli/
|
||||
│ ├── mod.rs # clap command definitions + dispatch
|
||||
│ ├── transport.rs # IPC client + daemon lifecycle
|
||||
│ ├── output.rs # YAML/JSON formatting
|
||||
│ ├── init.rs # wx init implementation
|
||||
│ ├── sessions.rs # etc. (thin wrappers around IPC)
|
||||
│ └── daemon_cmd.rs # daemon status/stop/logs
|
||||
├── daemon/
|
||||
│ ├── mod.rs # daemon entry + async_run
|
||||
│ ├── cache.rs # DbCache (mtime-aware decryption cache)
|
||||
│ ├── server.rs # IPC server (Unix/Windows)
|
||||
│ └── query.rs # All query implementations
|
||||
├── crypto/
|
||||
│ ├── mod.rs # SQLCipher page decryption
|
||||
│ └── wal.rs # WAL application
|
||||
└── scanner/
|
||||
├── mod.rs # common interface + salt collection
|
||||
├── macos.rs # Mach VM memory scanner
|
||||
├── linux.rs # /proc scanner
|
||||
└── windows.rs # Windows API scanner
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
**Core crates:**
|
||||
- `clap` (derive) - CLI parsing
|
||||
- `tokio` (full) - async runtime
|
||||
- `serde`/`serde_json` - serialization
|
||||
- `rusqlite` (bundled) - SQLite queries
|
||||
- `aes`/`cbc`/`hmac`/`sha2`/`pbkdf2` - crypto primitives
|
||||
- `zstd` - message decompression
|
||||
- `chrono` - timestamp formatting
|
||||
- `anyhow` - error handling
|
||||
- `dirs` - home directory
|
||||
- `md5` - table name hashing
|
||||
- `regex` - Msg_<md5> pattern matching
|
||||
|
||||
**Platform-specific:**
|
||||
- Unix: `libc` (setsid, signal handling)
|
||||
- Windows: `windows` crate (process/memory APIs), `interprocess` (named pipes)
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
wx-cli is a well-architected Rust project demonstrating:
|
||||
- Clean separation of CLI/daemon/crypto/scanner layers
|
||||
- Async-first daemon with sync-offload for SQLite
|
||||
- Smart caching strategy (mtime-based invalidation)
|
||||
- Cross-platform memory scanning for SQLCipher key extraction
|
||||
- AI-friendly output design (YAML default, JSON optional)
|
||||
- Comprehensive command coverage for WeChat local data
|
||||
|
|
@ -0,0 +1,847 @@
|
|||
# Communication Layer Analysis & TCP Socket Proposal
|
||||
|
||||
## Current Communication Architecture
|
||||
|
||||
### Layer Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Protocol Layer │
|
||||
│ src/ipc.rs │
|
||||
│ Request / Response types + JSON serialization │
|
||||
│ (Well abstracted - transport-agnostic) │
|
||||
└────────────────────────────┬────────────────────────────────┘
|
||||
│
|
||||
┌────────────────────────────┴────────────────────────────────┐
|
||||
│ Server Layer │
|
||||
│ src/daemon/server.rs │
|
||||
│ Platform-specific listeners + connection handlers │
|
||||
│ (POOR abstraction - duplicated logic per platform) │
|
||||
└────────────────────────────┬────────────────────────────────┘
|
||||
│
|
||||
┌────────────────────────────┴────────────────────────────────┐
|
||||
│ Client Layer │
|
||||
│ src/cli/transport.rs │
|
||||
│ Platform-specific connection + send functions │
|
||||
│ (POOR abstraction - duplicated logic per platform) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Abstraction Assessment
|
||||
|
||||
### Protocol Layer (src/ipc.rs) — **HIGH abstraction**
|
||||
|
||||
**Strengths:**
|
||||
- Pure data types with serde derive
|
||||
- No transport-specific code
|
||||
- Clean API: `Request` enum, `Response` struct
|
||||
- `to_json_line()` helper for serialization
|
||||
- Transport-agnostic by design
|
||||
|
||||
**Example:**
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "cmd", rename_all = "snake_case")]
|
||||
pub enum Request {
|
||||
Ping,
|
||||
Sessions { limit: usize },
|
||||
History { chat: String, limit: usize, ... },
|
||||
// ... all commands
|
||||
}
|
||||
|
||||
pub struct Response {
|
||||
pub ok: bool,
|
||||
pub error: Option<String>,
|
||||
#[serde(flatten)]
|
||||
pub data: Value,
|
||||
}
|
||||
```
|
||||
|
||||
**Verdict:** This layer is well-designed and TCP-ready. No changes needed.
|
||||
|
||||
---
|
||||
|
||||
### Server Layer (src/daemon/server.rs) — **LOW abstraction**
|
||||
|
||||
**Current structure:**
|
||||
```rust
|
||||
// Top-level entry with #[cfg] branching
|
||||
pub async fn serve(db, names) -> Result<()> {
|
||||
#[cfg(unix)]
|
||||
serve_unix(db, names).await?;
|
||||
#[cfg(windows)]
|
||||
serve_windows(db, names).await?;
|
||||
}
|
||||
|
||||
// Unix implementation (40 lines)
|
||||
#[cfg(unix)]
|
||||
async fn serve_unix(db, names) -> Result<()> {
|
||||
let listener = UnixListener::bind(&sock_path)?;
|
||||
loop {
|
||||
let (stream, _) = listener.accept().await?;
|
||||
tokio::spawn(async { handle_connection_unix(stream, db, names) });
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn handle_connection_unix(stream, db, names) -> Result<()> {
|
||||
let (reader, mut writer) = stream.into_split();
|
||||
let mut lines = BufReader::new(reader).lines();
|
||||
let line = lines.next_line().await?;
|
||||
let req: Request = serde_json::from_str(&line)?;
|
||||
let resp = dispatch(req, &db, &names).await;
|
||||
writer.write_all(resp.to_json_line()?.as_bytes()).await?;
|
||||
}
|
||||
|
||||
// Windows implementation (40 lines) - SAME LOGIC, DIFFERENT TYPES
|
||||
#[cfg(windows)]
|
||||
async fn serve_windows(db, names) -> Result<()> {
|
||||
let listener = ListenerOptions::new().name(name).create_tokio()?;
|
||||
loop {
|
||||
let conn = listener.accept().await?;
|
||||
tokio::spawn(async { handle_connection_windows(conn, db, names) });
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
async fn handle_connection_windows(conn, db, names) -> Result<()> {
|
||||
let (reader, mut writer) = tokio::io::split(conn);
|
||||
let mut lines = BufReader::new(reader).lines();
|
||||
let line = lines.next_line().await?;
|
||||
let req: Request = serde_json::from_str(&line)?;
|
||||
let resp = dispatch(req, &db, &names).await;
|
||||
writer.write_all(resp.to_json_line()?.as_bytes()).await?;
|
||||
}
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
1. **Duplicated connection handling**: `handle_connection_unix` and `handle_connection_windows` have identical logic
|
||||
2. **No abstraction for stream types**: `UnixStream` vs `interprocess::Stream` handled separately
|
||||
3. **No abstraction for listener types**: `UnixListener` vs `interprocess::Listener` handled separately
|
||||
4. **#[cfg] branching at function level**: Makes extension difficult
|
||||
5. **`dispatch()` is shared but buried**: Good pattern, but underutilized
|
||||
|
||||
**Duplication count:** ~30 lines of identical JSON-line protocol handling duplicated per platform
|
||||
|
||||
---
|
||||
|
||||
### Client Layer (src/cli/transport.rs) — **LOW abstraction**
|
||||
|
||||
**Current structure:**
|
||||
```rust
|
||||
// is_alive() with #[cfg] branching
|
||||
pub fn is_alive() -> bool {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let stream = UnixStream::connect(&sock_path)?;
|
||||
// ping logic
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let stream = Stream::connect(name)?;
|
||||
// ping logic (different API)
|
||||
}
|
||||
}
|
||||
|
||||
// send() with #[cfg] branching
|
||||
pub fn send(req: Request) -> Result<Response> {
|
||||
ensure_daemon()?;
|
||||
#[cfg(unix)]
|
||||
{ send_unix(req) }
|
||||
#[cfg(windows)]
|
||||
{ send_windows(req) }
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn send_unix(req: Request) -> Result<Response> {
|
||||
let stream = UnixStream::connect(&sock_path)?;
|
||||
stream.write_all(serde_json::to_string(&req)? + "\n");
|
||||
let line = BufReader::new(&stream).read_line();
|
||||
let resp: Response = serde_json::from_str(&line)?;
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn send_windows(req: Request) -> Result<Response> {
|
||||
let stream = Stream::connect(name)?;
|
||||
stream.write_all(serde_json::to_string(&req)? + "\n");
|
||||
let line = BufReader::new(stream).read_line();
|
||||
let resp: Response = serde_json::from_str(&line)?;
|
||||
Ok(resp)
|
||||
}
|
||||
```
|
||||
|
||||
**Problems:**
|
||||
1. **Duplicated request/response handling**: Same JSON-line protocol, different stream types
|
||||
2. **No abstraction for stream type**: Each platform uses different types
|
||||
3. **`is_alive()` logic differs**: Windows version doesn't do full ping
|
||||
4. **#[cfg] branching scattered**: 3 separate locations
|
||||
|
||||
**Duplication count:** ~20 lines of identical protocol handling duplicated per platform
|
||||
|
||||
---
|
||||
|
||||
## Abstraction Score Summary
|
||||
|
||||
| Layer | Abstraction Level | Duplicated Lines | Extension Difficulty |
|
||||
|----------------|-------------------|------------------|---------------------|
|
||||
| Protocol | HIGH | 0 | Easy |
|
||||
| Server | LOW | ~30 | Hard |
|
||||
| Client | LOW | ~20 | Hard |
|
||||
|
||||
**Total duplicated code:** ~50 lines of identical JSON-line protocol handling
|
||||
|
||||
**Root cause:** No trait abstraction for `Listener` and `Connection` types
|
||||
|
||||
---
|
||||
|
||||
## Proposed Architecture for TCP Support
|
||||
|
||||
### Strategy: Trait-Based Abstraction
|
||||
|
||||
Introduce traits for transport primitives, implement for:
|
||||
1. Unix socket (existing)
|
||||
2. Windows named pipe (existing)
|
||||
3. TCP socket (new)
|
||||
|
||||
---
|
||||
|
||||
### New Trait Definitions
|
||||
|
||||
```rust
|
||||
// src/transport/traits.rs
|
||||
|
||||
use anyhow::Result;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
|
||||
/// Trait for accepting connections (server-side)
|
||||
pub trait Listener: Send + Sync {
|
||||
type Connection: AsyncRead + AsyncWrite + Send + Sync + 'static;
|
||||
|
||||
async fn accept(&self) -> Result<Self::Connection>;
|
||||
fn addr_desc(&self) -> String; // for logging
|
||||
}
|
||||
|
||||
/// Trait for connecting to server (client-side)
|
||||
pub trait Connector: Send + Sync {
|
||||
type Connection: AsyncRead + AsyncWrite + Send + Sync + 'static;
|
||||
|
||||
async fn connect(&self) -> Result<Self::Connection>;
|
||||
fn is_available(&self) -> bool; // quick check before connect
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### New Module Structure
|
||||
|
||||
```
|
||||
src/transport/
|
||||
├── mod.rs # Public API: send(), handle_connection()
|
||||
├── traits.rs # Listener + Connector traits
|
||||
├── unix.rs # UnixListener + UnixConnector
|
||||
├── windows.rs # PipeListener + PipeConnector
|
||||
├── tcp.rs # TcpListener + TcpConnector
|
||||
└── protocol.rs # JSON-line protocol handling (shared)
|
||||
```
|
||||
|
||||
**Key change:** Protocol handling moves to `protocol.rs`, shared by all transports
|
||||
|
||||
---
|
||||
|
||||
### Protocol Handler (Shared Code)
|
||||
|
||||
```rust
|
||||
// src/transport/protocol.rs
|
||||
|
||||
use anyhow::Result;
|
||||
use tokio::io::{AsyncRead, AsyncWrite, AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
|
||||
use crate::ipc::{Request, Response};
|
||||
|
||||
/// Handle a single connection (server-side)
|
||||
pub async fn handle_connection<C: AsyncRead + AsyncWrite + Unpin>(
|
||||
conn: C,
|
||||
db: Arc<DbCache>,
|
||||
names: Arc<tokio::sync::RwLock<Arc<Names>>>,
|
||||
) -> Result<()> {
|
||||
let (reader, mut writer) = tokio::io::split(conn);
|
||||
let mut lines = BufReader::new(reader).lines();
|
||||
|
||||
let line = match lines.next_line().await? {
|
||||
Some(l) => l,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let req: Request = match serde_json::from_str(&line) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
let resp = Response::err(format!("JSON parse error: {}", e));
|
||||
writer.write_all(resp.to_json_line()?.as_bytes()).await?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let resp = dispatch(req, db, names).await;
|
||||
writer.write_all(resp.to_json_line()?.as_bytes()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send request and receive response (client-side)
|
||||
pub async fn send_over_connection<C: AsyncRead + AsyncWrite + Unpin>(
|
||||
conn: C,
|
||||
req: &Request,
|
||||
) -> Result<Response> {
|
||||
let (reader, mut writer) = tokio::io::split(conn);
|
||||
|
||||
let req_str = serde_json::to_string(req)? + "\n";
|
||||
writer.write_all(req_str.as_bytes()).await?;
|
||||
|
||||
let mut lines = BufReader::new(reader).lines();
|
||||
let line = lines.next_line().await?
|
||||
.ok_or_else(|| anyhow::anyhow!("No response received"))?;
|
||||
|
||||
let resp: Response = serde_json::from_str(&line)?;
|
||||
if !resp.ok {
|
||||
anyhow::bail!("{}", resp.error.as_deref().unwrap_or("Unknown error"));
|
||||
}
|
||||
Ok(resp)
|
||||
}
|
||||
```
|
||||
|
||||
**This eliminates all 50 lines of duplication.**
|
||||
|
||||
---
|
||||
|
||||
### Unix Socket Implementation
|
||||
|
||||
```rust
|
||||
// src/transport/unix.rs
|
||||
|
||||
use anyhow::Result;
|
||||
use tokio::net::{UnixListener, UnixStream};
|
||||
|
||||
use super::traits::{Listener, Connector};
|
||||
|
||||
pub struct UnixSocketListener {
|
||||
listener: UnixListener,
|
||||
path: std::path::PathBuf,
|
||||
}
|
||||
|
||||
impl Listener for UnixSocketListener {
|
||||
type Connection = UnixStream;
|
||||
|
||||
async fn accept(&self) -> Result<Self::Connection> {
|
||||
let (stream, _) = self.listener.accept().await?;
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
fn addr_desc(&self) -> String {
|
||||
self.path.display().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UnixSocketConnector {
|
||||
path: std::path::PathBuf,
|
||||
}
|
||||
|
||||
impl Connector for UnixSocketConnector {
|
||||
type Connection = UnixStream;
|
||||
|
||||
async fn connect(&self) -> Result<Self::Connection> {
|
||||
UnixStream::connect(&self.path).await?
|
||||
}
|
||||
|
||||
fn is_available(&self) -> bool {
|
||||
self.path.exists()
|
||||
}
|
||||
}
|
||||
|
||||
// Factory functions
|
||||
pub fn create_listener(path: &std::path::Path) -> Result<UnixSocketListener> {
|
||||
if path.exists() {
|
||||
std::fs::remove_file(path)?;
|
||||
}
|
||||
let listener = UnixListener::bind(path)?;
|
||||
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
|
||||
Ok(UnixSocketListener { listener, path: path.to_owned() })
|
||||
}
|
||||
|
||||
pub fn connector(path: &std::path::Path) -> UnixSocketConnector {
|
||||
UnixSocketConnector { path: path.to_owned() }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Windows Named Pipe Implementation
|
||||
|
||||
```rust
|
||||
// src/transport/windows.rs
|
||||
|
||||
use anyhow::Result;
|
||||
use interprocess::local_socket::{
|
||||
tokio::prelude::*,
|
||||
GenericNamespaced, ListenerOptions,
|
||||
};
|
||||
|
||||
use super::traits::{Listener, Connector};
|
||||
|
||||
pub struct PipeListener {
|
||||
listener: interprocess::local_socket::tokio::Listener,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Listener for PipeListener {
|
||||
type Connection = interprocess::local_socket::tokio::Stream;
|
||||
|
||||
async fn accept(&self) -> Result<Self::Connection> {
|
||||
self.listener.accept().await?
|
||||
}
|
||||
|
||||
fn addr_desc(&self) -> String {
|
||||
format!("\\\\.\\pipe\\{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PipeConnector {
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Connector for PipeConnector {
|
||||
type Connection = interprocess::local_socket::tokio::Stream;
|
||||
|
||||
async fn connect(&self) -> Result<Self::Connection> {
|
||||
let ns_name = self.name.to_ns_name::<GenericNamespaced>()?;
|
||||
Stream::connect(ns_name).await?
|
||||
}
|
||||
|
||||
fn is_available(&self) -> bool {
|
||||
// Windows named pipes don't have filesystem presence
|
||||
// Try a quick connect to check
|
||||
self.connect().await.is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_listener(name: &str) -> Result<PipeListener> {
|
||||
let ns_name = name.to_ns_name::<GenericNamespaced>()?;
|
||||
let listener = ListenerOptions::new().name(ns_name).create_tokio()?;
|
||||
Ok(PipeListener { listener, name: name.to_owned() })
|
||||
}
|
||||
|
||||
pub fn connector(name: &str) -> PipeConnector {
|
||||
PipeConnector { name: name.to_owned() }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### TCP Socket Implementation (NEW)
|
||||
|
||||
```rust
|
||||
// src/transport/tcp.rs
|
||||
|
||||
use anyhow::Result;
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
|
||||
use super::traits::{Listener, Connector};
|
||||
|
||||
pub struct TcpSocketListener {
|
||||
listener: TcpListener,
|
||||
addr: std::net::SocketAddr,
|
||||
}
|
||||
|
||||
impl Listener for TcpSocketListener {
|
||||
type Connection = TcpStream;
|
||||
|
||||
async fn accept(&self) -> Result<Self::Connection> {
|
||||
let (stream, addr) = self.listener.accept().await?;
|
||||
eprintln!("[tcp] connection from {}", addr);
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
fn addr_desc(&self) -> String {
|
||||
self.addr.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TcpSocketConnector {
|
||||
addr: std::net::SocketAddr,
|
||||
}
|
||||
|
||||
impl Connector for TcpSocketConnector {
|
||||
type Connection = TcpStream;
|
||||
|
||||
async fn connect(&self) -> Result<Self::Connection> {
|
||||
TcpStream::connect(&self.addr).await?
|
||||
}
|
||||
|
||||
fn is_available(&self) -> bool {
|
||||
// TCP port check - try quick connect
|
||||
std::net::TcpStream::connect_timeout(&self.addr, std::time::Duration::from_millis(100)).is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_listener(bind: &str) -> Result<TcpSocketListener> {
|
||||
let listener = TcpListener::bind(bind).await?;
|
||||
let addr = listener.local_addr()?;
|
||||
Ok(TcpSocketListener { listener, addr })
|
||||
}
|
||||
|
||||
pub fn connector(addr: std::net::SocketAddr) -> TcpSocketConnector {
|
||||
TcpSocketConnector { addr }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Server Refactor (src/daemon/server.rs)
|
||||
|
||||
```rust
|
||||
// src/daemon/server.rs
|
||||
|
||||
use std::sync::Arc;
|
||||
use crate::transport::{Listener, handle_connection};
|
||||
|
||||
pub async fn serve(
|
||||
db: Arc<DbCache>,
|
||||
names: Arc<tokio::sync::RwLock<Arc<Names>>>,
|
||||
) -> Result<()> {
|
||||
// Determine transport based on config/env
|
||||
let listeners: Vec<Box<dyn Listener>> = build_listeners()?;
|
||||
|
||||
for listener in listeners {
|
||||
eprintln!("[server] listening on {}", listener.addr_desc());
|
||||
let db2 = Arc::clone(&db);
|
||||
let names2 = Arc::clone(&names);
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match listener.accept().await {
|
||||
Ok(conn) => {
|
||||
let db3 = Arc::clone(&db2);
|
||||
let names3 = Arc::clone(&names2);
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_connection(conn, db3, names3).await {
|
||||
eprintln!("[server] connection error: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => eprintln!("[server] accept error: {}", e),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Keep daemon alive
|
||||
tokio::signal::ctrl_c().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_listeners() -> Result<Vec<Box<dyn Listener>>> {
|
||||
let mut listeners = Vec::new();
|
||||
|
||||
// Always add local transport (Unix/Pipe)
|
||||
#[cfg(unix)]
|
||||
listeners.push(Box::new(
|
||||
crate::transport::unix::create_listener(&crate::config::sock_path())?
|
||||
));
|
||||
|
||||
#[cfg(windows)]
|
||||
listeners.push(Box::new(
|
||||
crate::transport::windows::create_listener("wx-cli-daemon")?
|
||||
));
|
||||
|
||||
// Optionally add TCP (if configured)
|
||||
if let Ok(tcp_bind) = std::env::var("WX_TCP_BIND") {
|
||||
let tcp_listener = crate::transport::tcp::create_listener(&tcp_bind).await?;
|
||||
eprintln!("[server] TCP enabled on {}", tcp_listener.addr_desc());
|
||||
listeners.push(Box::new(tcp_listener));
|
||||
}
|
||||
|
||||
Ok(listeners)
|
||||
}
|
||||
```
|
||||
|
||||
**Key changes:**
|
||||
1. Single `serve()` function, no #[cfg] branching
|
||||
2. `build_listeners()` constructs appropriate transport(s)
|
||||
3. Can listen on multiple transports simultaneously (local + TCP)
|
||||
4. `handle_connection()` from `transport::protocol` is shared
|
||||
|
||||
---
|
||||
|
||||
### Client Refactor (src/cli/transport.rs)
|
||||
|
||||
```rust
|
||||
// src/cli/transport.rs (renamed to src/transport/mod.rs)
|
||||
|
||||
use anyhow::Result;
|
||||
use crate::ipc::{Request, Response};
|
||||
use crate::transport::{Connector, send_over_connection};
|
||||
|
||||
pub async fn send(req: Request) -> Result<Response> {
|
||||
ensure_daemon()?;
|
||||
|
||||
// Try connectors in priority order
|
||||
let connectors = build_connectors();
|
||||
|
||||
for connector in connectors {
|
||||
if connector.is_available() {
|
||||
let conn = connector.connect().await?;
|
||||
return send_over_connection(conn, &req).await;
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::bail!("No available transport to daemon")
|
||||
}
|
||||
|
||||
fn build_connectors() -> Vec<Box<dyn Connector>> {
|
||||
let mut connectors = Vec::new();
|
||||
|
||||
// Local transport first (faster, more secure)
|
||||
#[cfg(unix)]
|
||||
connectors.push(Box::new(
|
||||
crate::transport::unix::connector(&crate::config::sock_path())
|
||||
));
|
||||
|
||||
#[cfg(windows)]
|
||||
connectors.push(Box::new(
|
||||
crate::transport::windows::connector("wx-cli-daemon")
|
||||
));
|
||||
|
||||
// TCP fallback (if configured)
|
||||
if let Ok(tcp_addr) = std::env::var("WX_TCP_ADDR") {
|
||||
if let Ok(addr) = tcp_addr.parse() {
|
||||
connectors.push(Box::new(
|
||||
crate::transport::tcp::connector(addr)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
connectors
|
||||
}
|
||||
|
||||
pub fn ensure_daemon() -> Result<()> {
|
||||
// Try ping on each connector
|
||||
for connector in build_connectors() {
|
||||
if connector.is_available() {
|
||||
// Try quick ping
|
||||
if let Ok(conn) = connector.connect().await? {
|
||||
// Use blocking ping for startup check
|
||||
// ... (existing logic wrapped)
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No daemon found, start it
|
||||
start_daemon()?;
|
||||
|
||||
// Wait for any connector to become available
|
||||
let deadline = std::time::Instant::now() + Duration::from_secs(15);
|
||||
while std::time::Instant::now() < deadline {
|
||||
for connector in build_connectors() {
|
||||
if connector.is_available() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(300));
|
||||
}
|
||||
|
||||
anyhow::bail!("Daemon startup timeout")
|
||||
}
|
||||
```
|
||||
|
||||
**Key changes:**
|
||||
1. Async `send()` using `send_over_connection()`
|
||||
2. `build_connectors()` returns prioritized list
|
||||
3. Fallback chain: Unix/Pipe → TCP
|
||||
4. No #[cfg] branching in main logic
|
||||
|
||||
---
|
||||
|
||||
## Configuration for TCP
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# Server: enable TCP listener
|
||||
WX_TCP_BIND=127.0.0.1:9876 # bind address (default: none)
|
||||
WX_TCP_BIND=0.0.0.0:9876 # allow external connections (security risk)
|
||||
|
||||
# Client: TCP fallback address
|
||||
WX_TCP_ADDR=127.0.0.1:9876 # connect address
|
||||
WX_TCP_ADDR=192.168.1.100:9876 # remote daemon
|
||||
```
|
||||
|
||||
### Config File Extension
|
||||
|
||||
```json
|
||||
// ~/.wx-cli/config.json
|
||||
{
|
||||
"db_dir": "...",
|
||||
"keys_file": "...",
|
||||
"tcp": {
|
||||
"bind": "127.0.0.1:9876", // optional
|
||||
"allow_remote": false // security flag
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations for TCP
|
||||
|
||||
### Risks
|
||||
|
||||
1. **No encryption**: JSON-line protocol sent in plaintext
|
||||
2. **No authentication**: Anyone can query WeChat data
|
||||
3. **Data exposure**: Chat history, contacts, etc. visible to network
|
||||
|
||||
### Recommended Safeguards
|
||||
|
||||
```rust
|
||||
// src/transport/tcp.rs
|
||||
|
||||
pub struct TcpSocketListener {
|
||||
listener: TcpListener,
|
||||
addr: SocketAddr,
|
||||
allowed_hosts: Vec<IpNet>, // CIDR whitelist
|
||||
}
|
||||
|
||||
impl Listener for TcpSocketListener {
|
||||
async fn accept(&self) -> Result<Self::Connection> {
|
||||
let (stream, addr) = self.listener.accept().await?;
|
||||
|
||||
// Check source IP against whitelist
|
||||
let ip = addr.ip();
|
||||
if !self.allowed_hosts.iter().any(|net| net.contains(&ip)) {
|
||||
eprintln!("[tcp] rejected connection from {}", addr);
|
||||
return Err(anyhow::anyhow!("IP not in whitelist"));
|
||||
}
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
}
|
||||
|
||||
// Config
|
||||
pub struct TcpConfig {
|
||||
bind: String,
|
||||
allow_remote: bool,
|
||||
allowed_hosts: Vec<String>, // ["127.0.0.1/8", "192.168.1.0/24"]
|
||||
}
|
||||
```
|
||||
|
||||
### Authentication Proposal (Optional)
|
||||
|
||||
```rust
|
||||
// Add to Request enum
|
||||
pub enum Request {
|
||||
Auth { token: String }, // new
|
||||
Ping,
|
||||
Sessions { ... },
|
||||
}
|
||||
|
||||
// Server checks token before processing
|
||||
async fn dispatch(req: Request, db: &DbCache, names: &Names, auth: &AuthState) -> Response {
|
||||
if !auth.is_authenticated() && !req.is_auth_request() {
|
||||
return Response::err("Not authenticated");
|
||||
}
|
||||
// ... normal dispatch
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Roadmap
|
||||
|
||||
### Phase 1: Refactor Existing Code
|
||||
|
||||
1. Create `src/transport/` module
|
||||
2. Define `Listener` and `Connector` traits
|
||||
3. Move Unix/Pipe implementations to `unix.rs` / `windows.rs`
|
||||
4. Extract protocol handling to `protocol.rs`
|
||||
5. Refactor `server.rs` to use trait
|
||||
6. Refactor `transport.rs` to use trait
|
||||
|
||||
**Effort:** ~4 hours
|
||||
**Benefit:** Eliminate 50 lines duplication, cleaner architecture
|
||||
|
||||
### Phase 2: Add TCP Support
|
||||
|
||||
1. Create `tcp.rs` with `TcpSocketListener` / `TcpSocketConnector`
|
||||
2. Update `build_listeners()` / `build_connectors()`
|
||||
3. Add config parsing for TCP options
|
||||
4. Add IP whitelist validation
|
||||
|
||||
**Effort:** ~2 hours
|
||||
**Benefit:** TCP connectivity for remote clients
|
||||
|
||||
### Phase 3: Security Hardening
|
||||
|
||||
1. Add authentication token support
|
||||
2. TLS wrapper option (tokio-rustls)
|
||||
3. Connection logging/audit
|
||||
|
||||
**Effort:** ~3 hours
|
||||
**Benefit:** Production-safe remote access
|
||||
|
||||
---
|
||||
|
||||
## Backwards Compatibility
|
||||
|
||||
- Local transport (Unix/Pipe) remains default
|
||||
- TCP opt-in via config/env (not automatic)
|
||||
- CLI unchanged (same commands)
|
||||
- Protocol unchanged (same Request/Response types)
|
||||
|
||||
---
|
||||
|
||||
## Alternative: Zero-Change TCP Proxy
|
||||
|
||||
If refactoring is not desired, a simpler approach:
|
||||
|
||||
```bash
|
||||
# Use socat/proxy to expose Unix socket over TCP
|
||||
socat TCP-LISTEN:9876,reuseaddr,fork UNIX-CONNECT:/home/user/.wx-cli/daemon.sock
|
||||
```
|
||||
|
||||
**Pros:** No code changes
|
||||
**Cons:** Requires external tool, no IP filtering, less integrated
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Aspect | Current State | Proposed State |
|
||||
|---------------------------|-------------------------|-----------------------------|
|
||||
| Protocol abstraction | HIGH (good) | HIGH (unchanged) |
|
||||
| Transport abstraction | LOW (platform-specific) | HIGH (trait-based) |
|
||||
| Duplicated code | ~50 lines | 0 lines |
|
||||
| Extension difficulty | Hard | Easy |
|
||||
| TCP support | None | Full |
|
||||
| Multi-listener support | None | Yes (local + TCP) |
|
||||
|
||||
**Recommended path:** Proceed with Phase 1 refactor, then Phase 2 TCP addition. Phase 3 security can follow based on use case.
|
||||
|
||||
---
|
||||
|
||||
## Code Impact Summary
|
||||
|
||||
| File | Change Type | Lines Changed |
|
||||
|--------------------------|--------------------|---------------|
|
||||
| src/transport/mod.rs | New | ~60 |
|
||||
| src/transport/traits.rs | New | ~20 |
|
||||
| src/transport/protocol.rs| New (from existing)| ~40 |
|
||||
| src/transport/unix.rs | New (refactor) | ~40 |
|
||||
| src/transport/windows.rs | New (refactor) | ~40 |
|
||||
| src/transport/tcp.rs | New | ~50 |
|
||||
| src/daemon/server.rs | Refactor | ~30 (from 90) |
|
||||
| src/cli/transport.rs | Delete (moved) | 0 |
|
||||
| src/ipc.rs | Unchanged | 0 |
|
||||
|
||||
**Net change:** +250 new lines, -90 old lines, -50 duplication = +110 total
|
||||
**Complexity reduction:** Platform branching centralized, extension point clear
|
||||
Loading…
Reference in New Issue