diff --git a/CONTRIBUTING_EN-googletrans.md b/CONTRIBUTING_EN-googletrans.md new file mode 100644 index 00000000..cc7ffb2a --- /dev/null +++ b/CONTRIBUTING_EN-googletrans.md @@ -0,0 +1,202 @@ +∏# 🤝 Participate and contribute + +

+Three provinces and six ministries welcome heroes from all walks of life ⚔️
+Whether it’s fixing a typo or designing a new Agent character, we’re all very grateful +

+ +--- + +## 📋 How to contribute + +### 🐛 Report a bug + +Please use the [Bug Report](.github/ISSUE_TEMPLATE/bug_report.md) template to submit an Issue, including: +- OpenClaw version (`openclaw --version`) +- Python version (`python3 --version`) +- operating system +- Steps to reproduce (the more detailed, the better) +- Desired behavior vs actual behavior +- Screenshots (if Kanban UI is involved) + +### 💡 Feature Suggestions + +Use the [Feature Request](.github/ISSUE_TEMPLATE/feature_request.md) template. + +We recommend using the "will" format to describe your needs - just like writing a memorial to the emperor 😄 + +### 🔧 Submit Pull Request + +```bash +# 1. Fork this warehouse +# 2. Clone your Fork +git clone https://github.com//edict.git +cd edict + +# 3. Create a feature branch +git checkout -b feat/my-awesome-feature + +# 4. Development & Testing +python3 dashboard/server.py # 启动看板验证 + +# 5. Submit +git add . +git commit -m "feat: 添加了一个很酷的功能" + +# 6. Push & create PR +git push origin feat/my-awesome-feature +``` + +--- + +## 🏗️ Development environment + +### Preconditions +- [OpenClaw](https://openclaw.ai) installed +-Python 3.9+ +- macOS/Linux + +### Local startup + +```bash +# Install +./install.sh + +# Start data refresh (running in the background) +bash scripts/run_loop.sh & + +# Start the kanban server +python3 dashboard/server.py + +# Open browser +open http://127.0.0.1:7891 +``` + +> 💡 **Kanbanboard out of the box**: `server.py` embeds `dashboard/dashboard.html`, Docker image includes pre-built React frontend + +### Quick overview of project structure + +| Directory/File | Description | Frequency of changes | +|----------|------|--------| +| `dashboard/dashboard.html` | Kanban front-end (single file, zero dependencies, ready to use out of the box) | 🔥 High | +| `dashboard/server.py` | API Server (stdlib, ~2200 lines) | 🔥 High | +| `agents/*/SOUL.md` | 12 Agent personality templates | 🔶 Medium | +| `scripts/kanban_update.py` | Kanban CLI + data cleaning (~300 lines) | 🔶 Medium | +| `scripts/*.py` | Data synchronization / automation script | 🔶 Medium | +| `tests/test_e2e_kanban.py` | E2E Kanban test (17 assertions) | 🔶 Medium | +| `install.sh` | Installation script | 🟢 Low | + +--- + +## 📝 Commit specification + +We use [Conventional Commits](https://www.conventionalcommits.org/): + +``` +feat: ✨ NEW FEATURES +fix: 🐛 Fix Bug +docs: 📝 Documentation update +style: 🎨 code format (does not affect logic) +refactor: ♻️ code refactoring +perf: ⚡ performance optimization +test: ✅ test +chore: 🔧 Miscellaneous maintenance +ci: 👷 CI/CD configuration +``` + +Example: +``` +feat: Added the function of exporting memorials to PDF +fix: Fixed the problem of Gateway not restarting after model switching +docs: Updated README screenshots +``` + +--- + +## 🎯 Specially welcome contributions + +### 🎨 Kanban UI +- Dark/light theme switching +- Responsive layout optimization +- Enhanced animation effects +- Accessibility (a11y) improvements + +### 🤖 New Agent role +- Full-time Agent suitable for specific industries/scenarios +- New SOUL.md personality template +- Innovation in collaboration model between agents + +### 📦 Skills Ecosystem +- Dedicated skill packages for each department +- MCP integration skills +- Special skills in data processing/code analysis/document generation + +### 🔗 Third-party integration +- Notion / Jira / Linear synchronization +- GitHub Issues/PR linkage +- Slack / Discord messaging channels +- Webhook extension + +### 🌐 Internationalization +- Japanese / Korean / Spanish translation +- Kanban UI multi-language support + +### 📱 Mobile version +- Responsive adaptation +- PWA support +- Mobile operation optimization + +--- + +## 🧪 Test + +```bash +# Compilation check +python3 -m py_compile dashboard/server.py +python3 -m py_compile scripts/kanban_update.py + +# E2E Kanban Test (9 Scenario 17 Assertion) +python3 tests/test_e2e_kanban.py + +# Verify data synchronization +python3 scripts/refresh_live_data.py +python3 scripts/sync_agent_config.py + +# Start server verification API +python3 dashboard/server.py & +curl -s http://localhost:7891/api/live-status | python3 -m json.tool | head -20 +``` + +--- + +## 📏 Code style + +- **Python**: PEP 8, use pathlib to handle paths +- **TypeScript/React**: Function components + Hooks, CSS variable names start with `--` +- **CSS**: Use CSS variables (`--bg`, `--text`, `--acc`, etc.), BEM-style class names +- **Markdown**: Use `#` for titles, `-` for lists, code block annotation language + +--- + +## 🙏 Code of Conduct + +- Be kind and constructive +- Respect different perspectives and experiences +- Accept constructive criticism +- Focus on what is best for the community +- Show empathy towards other community members + +**We have zero tolerance for harassment. ** + +--- + +## 📬 Contact information + +- GitHub Issues: [Submit an issue](https://github.com/cft0808/edict/issues) +- GitHub Discussions: [Community Discussions](https://github.com/cft0808/edict/discussions) + +--- + +

+Thank you to every contributor, you are the cornerstone of the three provinces and six departments ⚔️ +

diff --git a/ROADMAP_EN-googletrans.md b/ROADMAP_EN-googletrans.md new file mode 100644 index 00000000..c8f6075e --- /dev/null +++ b/ROADMAP_EN-googletrans.md @@ -0,0 +1,106 @@ +# 🗺️ Three provinces and six departments · Roadmap + +> This roadmap is public. Welcome to claim unfinished projects and submit PRs to participate in the construction. +> +> How to claim: Reply "I'll take this" under the corresponding issue, or submit a PR directly and indicate it in the description. + +--- + +## Phase 1 — Core Architecture ✅ + +> The skeleton of three provinces and six departments: twelve departments + prince sorting + real-time billboard + complete workflow. + +- [x] Twelve-department Agent structure (Prince + Zhongshu·Menxia·Shangshu + Huli Bingxinggong + Officials + Early Dynasty Officials) +- [x] Prince Sorting Layer - Automatically identify chats/commands, reply directly to chats, extract the title of the command and transfer it to Zhongshu Province +- [x] Strict permission matrix - who can send messages to whom, in black and white +- [x] Military Aircraft Department real-time dashboard (10 function panels) +- [x] Task life cycle management (create → sort → plan → review → dispatch → execute → echo) +- [x] Task pause/cancel/resume +- [x] Memorial system (automatic archiving of completed decrees + five-stage timeline) +- [x] Imperial edict template library (9 preset templates + parameter form + estimated time/cost) +- [x] A sense of ritual for going to court (the opening animation will be played for the first time every day + today’s statistics) +- [x] World News (daily automatic collection of technology/financial information + Feishu push + subscription management) +- [x] Model hot switching (one-click switching of each Agent's LLM in the dashboard) +- [x] Skill management (check installed Skills in each province + add new skills) +- [x] Official overview (Token consumption ranking + activity + completion statistics) +- [x] Small tasks/session monitoring (OC-* session real-time tracking) +- [x] Purpose data cleaning - titles/notes are automatically purified, and dirty data is refused to be stored in the database +- [x] Duplicate mission protection - Completed/cancelled missions cannot be overwritten +- [x] E2E Kanban test (all 9 scenarios and 17 assertions passed) +- [x] React 18 front-end refactoring - TypeScript + Vite + Zustand, 13 functional components +- [x] Visualization of Agent's thinking process - real-time display of thinking / tool_result / user messages +- [x] Integrated front-end and back-end deployment - server.py also provides API + static file services + +--- + +## Phase 2 — System deepening 🚧 + +> Upgrade "easy to use" to "irreplaceable": Decentralization and checks and balances are not just a concept, but a complete system with performance evaluation, manual approval, and knowledge accumulation. + +### 🏅 Royal approval mode (manual approval node) +- [ ] The results of the deliberation of the province are submitted to "Yulan", which can be accurately read/rejected manually with one click. +- [ ] Approval panel within the Kanban board (pending approval list + historical approval instructions) +- [ ] Feishu/Telegram push approval notification +- **Difficulty**: ⭐⭐ | **Suitable for first time contributors** + +### 📊 Record of Merits and Demerits (Agent Performance Rating System) +- [ ] Completion rate, rework rate, and time-consuming statistics of each Agent +- [ ] Kanban panel display ranking list + trend chart +- [ ] Automatically mark "competent ministers" and "Agents who need training" +- **Difficulty**: ⭐⭐ + +### 🚀 Express Shop (visualization of real-time message flow between agents) +- [ ] Real-time connection animation in the billboard: Zhongshu → Menxia → Shangshu → Liubu +- [ ] Message type coloring (Dispatch/Consideration/Echo/Reject) +- [ ] Timeline playback mode +- **Difficulty**: ⭐⭐⭐ + +### 📚 National History Museum (knowledge base + citation tracing) +- [ ] The experience of historical will is automatically precipitated +- [ ] Similar intention search + recommendation +- [ ] Traceability chain of memorial citations +- **Difficulty**: ⭐⭐⭐ + +--- + +## Phase 3 — Ecological Expansion + +> From stand-alone tools to ecosystem: more integrations, more users, and more scenarios. + +### 🐳 Docker Compose + Demo Image +- [ ] `docker run` One-line command to experience the complete dashboard (preset simulation data) +- [ ] Docker Compose orchestration (kanban + data synchronization + OpenClaw Gateway) +- [ ] CI/CD automatically builds push images +- **Difficulty**: ⭐⭐ | **Suitable for first time contributors** + +### 🔗 Kanban Adapter +- [ ] Notion Adapter - Turn the Notion database into a military aircraft department billboard +- [ ] Linear adapter - Linear projects are synchronized to three provinces and six departments +- [ ] GitHub Issues two-way synchronization +- **Difficulty**: ⭐⭐⭐ + +### 📱 Mobile + PWA +- [ ] Responsive layout adapted to mobile phones/tablets +- [ ] PWA offline support + push notifications +- **Difficulty**: ⭐⭐ + +### 🏪 ClawHub is available +- [ ] Core Skills submitted to OpenClaw official Skill Market +- [ ] Install three provinces and six Skill Packs with one click +- **Difficulty**: ⭐ + +### 📈 Annual exam +- [ ] Agent annual performance report (total Token consumption, completion rate, most complex intention) +- [ ] Visual annual review screen +- **Difficulty**: ⭐⭐ + +--- + +## How to participate + +1. **Look at Phase 2** - These are the directions that need the most help right now +2. **Look for projects marked with ⭐⭐ or "Suitable for first time contribution"** to get started +3. **Open an Issue** to say what you want to do to avoid duplication of work +4. **Publish PR** - see [CONTRIBUTING.md](CONTRIBUTING.md) for details + +> 💡 Didn’t find the direction you want to go? You are welcome to open an Issue to propose new features, and good ideas will be added to the Roadmap. diff --git a/docs/getting-started_EN-first-version.md b/docs/getting-started_EN-first-version.md new file mode 100644 index 00000000..cdd34bfe --- /dev/null +++ b/docs/getting-started_EN-first-version.md @@ -0,0 +1,189 @@ +# 🚀 Quick Start Guide + +> Start from zero — set up your Three Departments & Six Ministries AI collaboration system in 5 minutes + +--- + +## Step 1: Install OpenClaw + +Three Departments & Six Ministries runs on [OpenClaw](https://openclaw.ai). Install it first: + +```bash +# macOS +brew install openclaw + +# Or download the installer package +# https://openclaw.ai/download +``` +After installation, initialize: + +```bash +openclaw init +``` + +--- + +## Step 2: Clone and install Three Departments & Six Ministries + +```bash +git clone https://github.com/cft0808/edict.git +cd edict +chmod +x install.sh && ./install.sh +``` + +The installer automatically: +- ✅ Creates 12 Agent Workspaces (`~/.openclaw/workspace-*`) +- ✅ Writes each department/office SOUL.md personality file +- ✅ Registers Agents and the permission matrix into `openclaw.json` +- ✅ Configures edict data sanitization rules +- ✅ Builds the React frontend into `dashboard/dist/` (requires Node.js 18+) +- ✅ Initializes the data directory +- ✅ Runs the first data sync +- ✅ Restarts the Gateway so the configuration takes effect + +--- + +## Step 3: Configure messaging channels + +Configure messaging channels in OpenClaw (Feishu / Telegram / Signal), and set the `taizi` (Crown Prince) agent as the edict entrypoint. The Crown Prince automatically separates casual chat from commands; command messages have their titles extracted and are forwarded to the Planning Department (Zhongshu). + +```bash +# View current channels +openclaw channels list + +# Add Feishu channel (set entrypoint to Crown Prince) +openclaw channels add --type feishu --agent taizi +``` + +Refer to OpenClaw docs: https://docs.openclaw.ai/channels + +--- + +## Step 4: Start the services + +```bash +# Terminal 1: data refresh loop (sync every 15 seconds) +bash scripts/run_loop.sh + +# Terminal 2: dashboard server +python3 dashboard/server.py + +# Open browser +open http://127.0.0.1:7891 +``` + +> 💡 Tip: `run_loop.sh` automatically syncs data every 15 seconds. You can run it in the background using `&`. + +> 💡 Dashboard is ready out-of-the-box: `server.py` embeds `dashboard/dashboard.html`, no extra build required. The Docker image includes a prebuilt React frontend. + +--- + +## Step 5: Send your first edict + +Send a task through your messaging channel (the Crown Prince will automatically detect it and forward it to Zhongshu): + +``` +Please help me write a text classifier in Python: +1. Use scikit-learn +2. Support multi-class classification +3. Output a confusion matrix +4. Write complete documentation +``` + +--- + +## Step 6: Observe the execution process + +Open the dashboard: http://127.0.0.1:7891 + +1. **📋 Edicts Kanban** — watch tasks move across states +2. **🔭 Department Dispatch** — view workload distribution across departments +3. **📜 Memorial Archive** — after completion, tasks are automatically archived as memorials + +Task flow path: +``` +Inbox → Crown Prince triage → Zhongshu planning → Menxia review → Assigned → Doing → Done +``` + +--- + +## 🎯 Advanced usage + +### Use edict templates + +> Dashboard → 📜 Templates Library → select template → fill parameters → issue edict + +9 preset templates: weekly report · code review · API design · competitive analysis · data report · blog post · deployment plan · email copy · standup summary + +### Switch an agent’s model + +> Dashboard → ⚙️ Model Config → choose a new model → apply changes + +After ~5 seconds, the Gateway automatically restarts and the change takes effect. + +### Manage skills + +> Dashboard → 🛠️ Skills Config → view installed skills → click “add new skill” + +### Stop / cancel a task + +> In the Edicts Kanban or task detail, click **⏸ Stop** or **🚫 Cancel** + +### Subscribe to “World News” (Daily Briefing) + +> Dashboard → 📰 Daily Briefing → ⚙️ Subscription Management → choose categories / add sources / configure Feishu push + +--- + +## ❓ Troubleshooting + +### Dashboard shows “server not started” +```bash +# Confirm the server is running +python3 dashboard/server.py +``` + +### Agent does not respond +```bash +# Check Gateway status +openclaw gateway status + +# Restart if needed +openclaw gateway restart +``` + +### Data does not update +```bash +# Check whether the refresh loop is running +ps aux | grep run_loop + +# Manually run one sync +python3 scripts/refresh_live_data.py +``` + +### Heartbeat shows red / alert +```bash +# Check the corresponding Agent process +openclaw agent status + +# Restart a specific Agent +openclaw agent restart +``` + +### Model switch does not take effect +Wait about 5 seconds for the Gateway restart to complete. If it still does not take effect: + +```bash +python3 scripts/apply_model_changes.py +openclaw gateway restart +``` + +--- + +## 📚 More resources + +- [🏠 Project homepage](https://github.com/cft0808/edict) +- [📖 README](../README.md) +- [🤝 Contributing guide](../CONTRIBUTING.md) +- [💬 OpenClaw docs](https://docs.openclaw.ai) +- [📮 WeChat · cft0808](wechat.md) — architecture breakdown / postmortems / token-saving tricks diff --git a/docs/getting-started_EN-googletrans.md b/docs/getting-started_EN-googletrans.md new file mode 100644 index 00000000..ec2ade6c --- /dev/null +++ b/docs/getting-started_EN-googletrans.md @@ -0,0 +1,193 @@ +# 🚀 Quick Start Guide + +> Start from scratch and build your AI collaboration system for three provinces and six departments in 5 minutes + +--- + +## Step 1: Install OpenClaw + +Three Provinces and Six Departments run based on [OpenClaw](https://openclaw.ai), please install it first: + +```bash +#macOS +brew install openclaw + +# Or download the installation package +# HTTPS://open claw.love/download +``` + +Initialize after installation is complete: + +```bash +openclaw init +``` + +## Step 2: Clone and install Sanshengliubu + +```bash +git clone https://github.com/cft0808/edict.git +cd edict +chmod +x install.sh && ./install.sh +``` + +The installation script will be completed automatically: +- ✅ Create 12 Agent Workspaces (`~/.openclaw/workspace-*`) +- ✅ Write to the SOUL.md personality files of each province and department +- ✅ Register Agent and permission matrix to `openclaw.json` +- ✅ Configure decree data cleaning rules +- ✅ Build React frontend to `dashboard/dist/` (requires Node.js 18+) +- ✅Initialize data directory +- ✅ Perform first data synchronization +- ✅ Restart Gateway to make the configuration take effect + +## Step 3: Configure message channels + +Configure the message channel (Feishu/Telegram/Signal) in OpenClaw, and set the `taizi` (Prince) Agent as the will entrance. Prince will automatically sort chats and instructions. Instruction messages will be forwarded to Zhongshu Province after refining their titles. + +```bash +# View current channel +openclaw channels list + +# Add Feishu channel (the entrance is set to Prince) +openclaw channels add --type feishu --agent taizi +``` + +Refer to the OpenClaw documentation: https://docs.openclaw.ai/channels + +## Step 4: Start the service + +```bash +# Terminal 1: Data refresh cycle (synchronized every 15 seconds) +bash scripts/run_loop.sh + +# Terminal 2: Kanban server +python3 dashboard/server.py + +# Open browser +open http://127.0.0.1:7891 +``` + +> 💡 **Tip**: `run_loop.sh` automatically synchronizes data every 15 seconds. Can be run in the background using `&`. + +> 💡 **Kanban works out of the box**: `server.py` embeds `dashboard/dashboard.html`, no additional build required. The Docker image includes a pre-built React frontend. + +## Step 5: Send the first message + +Send tasks through message channels (Prince will automatically recognize and forward to Zhongshu Province): + +``` +Please help me write a text classifier in Python: +1. Using scikit-learn +2. Support multiple categories +3. Output confusion matrix +4. Write complete documentation +``` + +## Step 6: Observe the execution process + +Open the Kanban board http://127.0.0.1:7891 + +1. **📋 Will Board** — Observe the flow of tasks between states +2. **🔭 Provincial and Department Scheduling** — Check the work distribution of each department +3. **📜 Memorial Pavilion** — After the task is completed, it will be automatically archived as a memorial + +Task flow path: +``` +Receipt → Sorting by Prince → Zhongshu Planning → Review by the door → Distributed → Under implementation → Completed +``` + +--- + +## 🎯 Advanced usage + +### Use the edict template + +> Kanban → 📜 Purpose Library → Select Template → Fill in Parameters → Issue Purpose + +9 preset templates: Weekly report generation · Code review · API design · Competitive product analysis · Data report · Blog article · Deployment plan · Email copy · Station meeting summary + +### Switch Agent model + +> Kanban → ⚙️ Model Configuration → Select New Model → Apply Changes + +The Gateway will automatically restart and take effect after about 5 seconds. + +### Management skills + +> Dashboard → 🛠️ Skill configuration → View installed skills → Click to add new skills + +### Stop/Cancel task + +> In the Purpose Board or task details, click the **⏸ Stop** or **🚫 Cancel** button + +### Subscribe to world news + +> Bulletin → 📰 World News → ⚙️ Subscription Management → Select Category / Add Source / Equip Feishu Push + +--- + +## ❓ Troubleshooting + +### The dashboard displays "Server not started" +```bash +# Confirm that the server is running +python3 dashboard/server.py +``` + +### Agent does not respond +```bash +# Check Gateway status +openclaw gateway status + +# Restart if necessary +openclaw gateway restart +``` + +### Data is not updated +```bash +# Check if refresh loop is running +ps aux | grep run_loop + +# Manually perform a synchronization +python3 scripts/refresh_live_data.py +``` + +### Heartbeat shows red/alarm +```bash +# Check the process of the corresponding Agent +openclaw agent status + +# Restart the specified Agent +openclaw agent restart +``` + +### Does not take effect after model switching +Wait approximately 5 seconds for the Gateway restart to complete. Rules still not valid: +```bash +python3 scripts/apply_model_changes.py +openclaw gateway restart +``` + +--- + +## 📚 More resources + +- [🏠Project homepage](https://github.com/cft0808/edict) +- [📖 README](../README.md) +- [🤝 Contribution Guide](../CONTRIBUTING.md) +- [💬 OpenClaw Documentation](https://docs.openclaw.ai) +- [📮 Official account · cft0808](wechat.md) — Architecture dismantling / Pitfall review / Token money-saving technique + + +--- + + ### Comando recomendado: + + ```bash + /home/cartine/.openclaw/workspace/skills/fidelity-md-translation/.venv/bin/python \ + /home/cartine/.openclaw/workspace/skills/fidelity-md-translation/scripts/translate_md_fidelity.py \ + translate-google \ + /home/cartine/.openclaw/edict-BR/docs/task-dispatch-architecture.md \ + --out /home/cartine/.openclaw/edict-BR/docs/task-dispatch-architecture_EN-googletrans.md \ + --src-lang zh-cn --dest en --max-concurrency 1 --retry 3 --chunk-size 12000 + ``` \ No newline at end of file diff --git a/docs/git-ingest-edict-.txt b/docs/git-ingest-edict-.txt new file mode 100644 index 00000000..ac8f0b1e --- /dev/null +++ b/docs/git-ingest-edict-.txt @@ -0,0 +1,785 @@ +Directory structure: +└── cft0808-edict/ + ├── docker-compose.yml + ├── .dockerignore + ├── docker/ + │ └── demo_data/ + │ ├── last_model_change_result.json + │ ├── model_change_log.json + │ └── pending_model_changes.json + ├── edict/ + │ ├── alembic.ini + │ ├── Dockerfile + │ ├── .env.example + │ ├── backend/ + │ │ ├── requirements.txt + │ │ └── app/ + │ │ ├── __init__.py + │ │ ├── api/ + │ │ │ └── __init__.py + │ │ ├── models/ + │ │ │ └── __init__.py + │ │ ├── services/ + │ │ │ └── __init__.py + │ │ └── workers/ + │ │ └── __init__.py + │ ├── frontend/ + │ │ ├── Dockerfile + │ │ ├── index.html + │ │ ├── nginx.conf + │ │ ├── package.json + │ │ ├── postcss.config.js + │ │ ├── tailwind.config.js + │ │ ├── tsconfig.json + │ │ ├── tsconfig.tsbuildinfo + │ │ ├── vite.config.ts + │ │ ├── .env.development + │ │ └── src/ + │ │ ├── main.tsx + │ │ ├── vite-env.d.ts + │ │ └── components/ + │ │ └── Toaster.tsx + │ └── migration/ + │ └── script.py.mako + ├── examples/ + │ └── README.md + ├── .github/ + │ ├── pull_request_template.md + │ ├── ISSUE_TEMPLATE/ + │ │ ├── bug_report.md + │ │ └── feature_request.md + │ └── workflows/ + │ └── ci.yml + └── .vite/ + └── deps/ + ├── _metadata.json + └── package.json + +================================================ +FILE: docker-compose.yml +================================================ +version: '3.8' + +services: + sansheng-demo: + image: cft0808/sansheng-demo:latest + platform: linux/amd64 + ports: + - "7891:7891" + environment: + - DEMO_MODE=true + restart: unless-stopped + +# 启动: docker compose up +# 如果在 ARM Mac (M1/M2/M3) 上运行,删除 platform 行即可 +# 访问: http://localhost:7891 + + + +================================================ +FILE: .dockerignore +================================================ +# Git +.git +.gitignore + +# IDE / Editor +.vscode +.idea +*.swp +*.swo +*~ + +# Python +__pycache__ +*.pyc +*.pyo +.mypy_cache +.pytest_cache +.venv +venv + +# OS +.DS_Store +Thumbs.db + +# Docs (not needed in image) +docs/ +*.md +!README.md +LICENSE + + + +================================================ +FILE: docker/demo_data/last_model_change_result.json +================================================ +{} + + + +================================================ +FILE: docker/demo_data/model_change_log.json +================================================ +[] + + + +================================================ +FILE: docker/demo_data/pending_model_changes.json +================================================ +[] + + + +================================================ +FILE: edict/alembic.ini +================================================ +[alembic] +script_location = migration +sqlalchemy.url = postgresql+asyncpg://edict:edict_dev_2024@localhost:5432/edict + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S + + + +================================================ +FILE: edict/Dockerfile +================================================ +FROM python:3.12-slim + +WORKDIR /app + +# 安装系统依赖 +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +# 安装 Python 依赖 +COPY edict/backend/requirements.txt /tmp/requirements.txt +RUN pip install --no-cache-dir -r /tmp/requirements.txt + +# 复制后端代码 +COPY edict/backend/ /app/ + +# 复制 Alembic 配置 +COPY edict/alembic.ini /app/alembic.ini +COPY edict/migration/ /app/migration/ + +EXPOSE 8000 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] + + + +================================================ +FILE: edict/.env.example +================================================ +# ── Edict 三省六部 事件驱动架构 ── +# 复制为 .env 并根据环境修改 + +# ── Postgres ── +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 +POSTGRES_DB=edict +POSTGRES_USER=edict +POSTGRES_PASSWORD=edict_secret_change_me + +# ── Redis ── +REDIS_URL=redis://localhost:6379/0 + +# ── Backend ── +BACKEND_HOST=0.0.0.0 +BACKEND_PORT=8000 +SECRET_KEY=change-me-in-production +DEBUG=true + +# ── OpenClaw Gateway ── +OPENCLAW_GATEWAY_URL=http://localhost:18789 +OPENCLAW_BIN=openclaw + +# ── 文件路径(兼容旧系统过渡期) ── +LEGACY_DATA_DIR=../data +LEGACY_TASKS_FILE=../data/tasks_source.json + +# ── 调度参数 ── +STALL_THRESHOLD_SEC=180 +MAX_DISPATCH_RETRY=3 +DISPATCH_TIMEOUT_SEC=300 +HEARTBEAT_INTERVAL_SEC=30 + +# ── 飞书通知 ── +FEISHU_DELIVER=true +FEISHU_CHANNEL=feishu + + + +================================================ +FILE: edict/backend/requirements.txt +================================================ +# Edict Backend Dependencies +fastapi[standard]>=0.115.0 +uvicorn[standard]>=0.32.0 +sqlalchemy[asyncio]>=2.0.36 +asyncpg>=0.30.0 +alembic>=1.14.0 +redis[hiredis]>=5.2.0 +pydantic>=2.10.0 +pydantic-settings>=2.6.0 +python-dotenv>=1.0.1 +httpx>=0.28.0 + + + +================================================ +FILE: edict/backend/app/__init__.py +================================================ +"""Edict Backend — 三省六部事件驱动架构。""" + + + +================================================ +FILE: edict/backend/app/api/__init__.py +================================================ +from .tasks import router as tasks_router +from .agents import router as agents_router +from .events import router as events_router +from .admin import router as admin_router +from .websocket import router as websocket_router + +__all__ = [ + "tasks_router", + "agents_router", + "events_router", + "admin_router", + "websocket_router", +] + + + +================================================ +FILE: edict/backend/app/models/__init__.py +================================================ +"""Edict 数据模型包。""" + +from .task import Task, TaskState +from .event import Event +from .thought import Thought +from .todo import Todo + +__all__ = ["Task", "TaskState", "Event", "Thought", "Todo"] + + + +================================================ +FILE: edict/backend/app/services/__init__.py +================================================ +from .event_bus import EventBus, get_event_bus +from .task_service import TaskService + +__all__ = ["EventBus", "get_event_bus", "TaskService"] + + + +================================================ +FILE: edict/backend/app/workers/__init__.py +================================================ +from .orchestrator_worker import OrchestratorWorker, run_orchestrator +from .dispatch_worker import DispatchWorker, run_dispatcher + +__all__ = [ + "OrchestratorWorker", + "run_orchestrator", + "DispatchWorker", + "run_dispatcher", +] + + + +================================================ +FILE: edict/frontend/Dockerfile +================================================ +FROM node:20-alpine AS build +WORKDIR /app +COPY package.json ./ +RUN npm install +COPY . . +RUN npm run build + +FROM nginx:alpine +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 3000 +CMD ["nginx", "-g", "daemon off;"] + + + +================================================ +FILE: edict/frontend/index.html +================================================ + + + + + + + 三省六部 · Edict Dashboard + + +
+ + + + + + +================================================ +FILE: edict/frontend/nginx.conf +================================================ +server { + listen 3000; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location /api/ { + proxy_pass http://backend:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + location /ws { + proxy_pass http://backend:8000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_read_timeout 86400; + } +} + + + +================================================ +FILE: edict/frontend/package.json +================================================ +{ + "name": "edict-dashboard", + "private": true, + "version": "2.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "zustand": "^4.5.5", + "lucide-react": "^0.460.0", + "clsx": "^2.1.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.15", + "typescript": "^5.6.3", + "vite": "^6.0.1" + } +} + + + +================================================ +FILE: edict/frontend/postcss.config.js +================================================ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} + + + +================================================ +FILE: edict/frontend/tailwind.config.js +================================================ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + theme: { + extend: { + colors: { + // 三省六部主题色 + imperial: { + 50: '#fef7ed', + 100: '#fdecd4', + 200: '#fad5a8', + 300: '#f7b671', + 400: '#f38d38', + 500: '#f07012', + 600: '#e15808', + 700: '#bb4009', + 800: '#95330f', + 900: '#792c10', + }, + dynasty: { + 50: '#fdf4f3', + 100: '#fce8e4', + 200: '#fad4ce', + 300: '#f5b5ab', + 400: '#ee8a7a', + 500: '#e25f4d', + 600: '#cf4430', + 700: '#ae3624', + 800: '#903022', + 900: '#782d23', + }, + }, + }, + }, + plugins: [], +} + + + +================================================ +FILE: edict/frontend/tsconfig.json +================================================ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src"] +} + + + +================================================ +FILE: edict/frontend/tsconfig.tsbuildinfo +================================================ +{"root":["./src/app.tsx","./src/api.ts","./src/main.tsx","./src/store.ts","./src/vite-env.d.ts","./src/components/confirmdialog.tsx","./src/components/courtceremony.tsx","./src/components/edictboard.tsx","./src/components/memorialpanel.tsx","./src/components/modelconfig.tsx","./src/components/monitorpanel.tsx","./src/components/morningpanel.tsx","./src/components/officialpanel.tsx","./src/components/sessionspanel.tsx","./src/components/skillsconfig.tsx","./src/components/taskmodal.tsx","./src/components/templatepanel.tsx","./src/components/toaster.tsx"],"version":"5.9.3"} + + +================================================ +FILE: edict/frontend/vite.config.ts +================================================ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import path from 'path' + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + }, + build: { + outDir: path.resolve(__dirname, '../../dashboard/dist'), + emptyOutDir: true, + }, +}) + + + +================================================ +FILE: edict/frontend/.env.development +================================================ +VITE_API_URL=http://127.0.0.1:7891 + + + +================================================ +FILE: edict/frontend/src/main.tsx +================================================ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +) + + + +================================================ +FILE: edict/frontend/src/vite-env.d.ts +================================================ +/// + + + +================================================ +FILE: edict/frontend/src/components/Toaster.tsx +================================================ +import { useStore } from '../store'; + +export default function Toaster() { + const toasts = useStore((s) => s.toasts); + if (!toasts.length) return null; + + return ( +
+ {toasts.map((t) => ( +
+ {t.msg} +
+ ))} +
+ ); +} + + + +================================================ +FILE: edict/migration/script.py.mako +================================================ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} + + + +================================================ +FILE: examples/README.md +================================================ +# 📂 案例 / Examples + +真实的端到端使用案例,展示三省六部处理完整旨意的全流程。 + +| # | 案例 | 旨意内容 | 涉及部门 | 复杂度 | +|---|------|---------|----------|--------| +| 1 | [竞品分析](competitive-analysis.md) | "分析 CrewAI vs AutoGen vs LangGraph" | 中书→门下→户部+兵部+礼部 | ⭐⭐⭐ | +| 2 | [代码审查](code-review.md) | "审查 FastAPI 代码的安全性" | 中书→门下→兵部+刑部 | ⭐⭐ | +| 3 | [周报生成](weekly-report.md) | "生成本周工程团队周报" | 中书→门下→户部+礼部 | ⭐⭐ | + +每个案例包含: +- 📜 **圣旨**:原始指令 +- 📋 **中书省规划**:任务拆解方案 +- 🔍 **门下省审议**:审核意见(含封驳/返工记录) +- ⚔️ **各部执行结果**:每个部门的产出摘要 +- 📮 **最终奏折**:尚书省汇总的完整报告 + +> 💡 这些案例基于真实运行记录整理,任务 ID 和时间戳已脱敏。 + + + +================================================ +FILE: .github/pull_request_template.md +================================================ +## 变更描述 + + +## 变更类型 +- [ ] Bug 修复 +- [ ] 新功能 +- [ ] 重构 / 代码优化 +- [ ] 文档更新 +- [ ] CI / 工程配置 + +## 检查清单 +- [ ] 代码已通过 `python3 -m py_compile` 检查 +- [ ] 已在本地测试运行 `run_loop.sh` +- [ ] 涉及看板的变更已在浏览器中验证 +- [ ] 更新了相关文档(如适用) + +## 关联 Issue + + + + +================================================ +FILE: .github/ISSUE_TEMPLATE/bug_report.md +================================================ +--- +name: Bug Report +about: 报告一个 Bug +labels: bug +--- + +## 环境 +- OpenClaw 版本: +- 操作系统: +- Python 版本: + +## 问题描述 + +## 复现步骤 +1. +2. +3. + +## 期望行为 + +## 实际行为 + +## 错误日志 +``` +粘贴日志 +``` + + + +================================================ +FILE: .github/ISSUE_TEMPLATE/feature_request.md +================================================ +--- +name: Feature Request +about: 提交功能建议 +labels: enhancement +--- + +## 功能描述 + +## 使用场景 + +## 期望效果 + +## 其他信息 + + + +================================================ +FILE: .github/workflows/ci.yml +================================================ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + lint-and-test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.10', '3.11', '3.12'] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Syntax check (py_compile) + run: | + find scripts dashboard -name '*.py' | while read f; do + echo " checking $f" + python3 -m py_compile "$f" + done + + - name: Install test dependencies + run: pip install pytest + + - name: Run tests + run: pytest tests/ -v + + - name: Shell lint (bash -n) + run: bash -n scripts/run_loop.sh + + + +================================================ +FILE: .vite/deps/_metadata.json +================================================ +{ + "hash": "2321d508", + "configHash": "3e6eab4b", + "lockfileHash": "e3b0c442", + "browserHash": "5408e294", + "optimized": {}, + "chunks": {} +} + + +================================================ +FILE: .vite/deps/package.json +================================================ +{ + "type": "module" +} + + diff --git a/docs/remote-skills-guide_EN-googletrans.md b/docs/remote-skills-guide_EN-googletrans.md new file mode 100644 index 00000000..1f989870 --- /dev/null +++ b/docs/remote-skills-guide_EN-googletrans.md @@ -0,0 +1,457 @@ +# Remote Skills Resource Management Guide + +## Overview + +Three provinces and six ministries now support connecting and adding skills resources from the Internet without the need to manually copy files. Support is obtained from the following sources: + +- **GitHub Repository** (raw.githubusercontent.com) +- **Any HTTPS URL** (needs to return a valid skill file) +- **Local file path** +- **Built-in repository** (official skills repository) + +--- + +## Functional architecture + +### 1. API endpoint + +#### `POST /api/add-remote-skill` + +Adds a skill for the specified Agent from a remote URL or local path. + +**Request body:** +```json +{ + "agentId": "zhongshu", + "skillName": "code_review", + "sourceUrl": "https://raw.githubusercontent.com/org/skills-repo/main/code_review/SKILL.md", + "description": "代码审查专项技能" +} +``` + +**Parameter description:** +- `agentId` (string, required): Target Agent ID (verify validity) +- `skillName` (string, required): internal name of skill (only letters/numbers/underscores/Chinese characters allowed) +- `sourceUrl` (string, required): remote URL or local file path +- GitHub: `https://raw.githubusercontent.com/user/repo/branch/path/SKILL.md` +- Any HTTPS: `https://example.com/skills/my_skill.md` +- Local: `file:///Users/bingsen/skills/code_review.md` or `/Users/bingsen/skills/code_review.md` +- `description` (string, optional): Chinese description of skill + +**Response successful (200):** +```json +{ + "ok": true, + "message": "技能 code_review 已添加到 zhongshu", + "skillName": "code_review", + "agentId": "zhongshu", + "source": "https://raw.githubusercontent.com/...", + "localPath": "/Users/bingsen/.openclaw/workspace-zhongshu/skills/code_review/SKILL.md", + "size": 2048, + "addedAt": "2026-03-02T14:30:00Z" +} +``` + +**Response failed (400):** +```json +{ + "ok": false, + "error": "URL 无效或无法访问", + "details": "Connection timeout after 10s" +} +``` + +#### `GET /api/remote-skills-list` + +Lists all added remote skills and their source information. + +**response:** +```json +{ + "ok": true, + "remoteSkills": [ + { + "skillName": "code_review", + "agentId": "zhongshu", + "sourceUrl": "https://raw.githubusercontent.com/org/skills-repo/main/code_review/SKILL.md", + "description": "代码审查专项技能", + "localPath": "/Users/bingsen/.openclaw/workspace-zhongshu/skills/code_review/SKILL.md", + "lastUpdated": "2026-03-02T14:30:00Z", + "status": "valid" // valid | invalid | not-found + } + ], + "count": 5 +} +``` + +#### `POST /api/update-remote-skill` + +Update the added remote skill to the latest version. + +**Request body:** +```json +{ + "agentId": "zhongshu", + "skillName": "code_review" +} +``` + +**response:** +```json +{ + "ok": true, + "message": "技能已更新", + "skillName": "code_review", + "newVersion": "2.1.0", + "updatedAt": "2026-03-02T15:00:00Z" +} +``` + +#### `DELETE /api/remove-remote-skill` + +Remove an added remote skill. + +**Request body:** +```json +{ + "agentId": "zhongshu", + "skillName": "code_review" +} +``` + +--- + +## CLI commands + +### Add remote skill + +```bash +python3 scripts/skill_manager.py add-remote \ + --agent zhongshu \ + --name code_review \ + --source https://raw.githubusercontent.com/org/skills-repo/main/code_review/SKILL.md \ + --description "代码审查专项技能" +``` + +### List remote Skills + +```bash +python3 scripts/skill_manager.py list-remote +``` + +### Update remote Skill + +```bash +python3 scripts/skill_manager.py update-remote \ + --agent zhongshu \ + --name code_review +``` + +### Remove remote Skill + +```bash +python3 scripts/skill_manager.py remove-remote \ + --agent zhongshu \ + --name code_review +``` + +--- + +## Official Skills Library + +### OpenClaw Skills Hub + +> **Official skills library address**: https://github.com/openclaw-ai/skills-hub + +List of available skills: + +| Skill Name | Description | Applicable Agent | Source URL | +|-----------|------|----------|--------| +| `code_review` | Code review (supports Python/JS/Go) | Ministry of War/Ministry of Justice | https://raw.githubusercontent.com/openclaw-ai/skills-hub/main/code_review/SKILL.md | +| `api_design` | API design review | Ministry of War/Ministry of Industry | https://raw.githubusercontent.com/openclaw-ai/skills-hub/main/api_design/SKILL.md | +| `security_audit` | Security Audit | Ministry of Justice | https://raw.githubusercontent.com/openclaw-ai/skills-hub/main/security_audit/SKILL.md | +| `data_analysis` | Data Analysis | Household Department | https://raw.githubusercontent.com/openclaw-ai/skills-hub/main/data_analysis/SKILL.md | +| `doc_generation` | Document generation | Ministry of Rites | https://raw.githubusercontent.com/openclaw-ai/skills-hub/main/doc_generation/SKILL.md | +| `test_framework` | Test framework design | Ministry of Industry/Ministry of Punishment | https://raw.githubusercontent.com/openclaw-ai/skills-hub/main/test_framework/SKILL.md | + +**Import official skills with one click** + +```bash +python3 scripts/skill_manager.py import-official-hub \ + --agents zhongshu,menxia,shangshu,bingbu,xingbu,libu +``` + +--- + +## Kanban UI operation + +### Quickly add Skill + +1. Open the dashboard → 🔧 **Skill Configuration** panel +2. Click the **➕Add Remote Skill** button +3. Fill out the form: +- **Agent**: Select the target Agent +- **Skill Name**: Enter the internal ID of the skill +- **Remote URL**: Paste the GitHub/HTTPS URL +- **Chinese description**: Optional, briefly describe the skill function +4. Click the **Confirm** button + +### Manage added Skills + +1. Kanban → 🔧 **Skill Configuration** → **Remote Skills** Tag +2. View all added skills and their source addresses +3. Operation: +- **View**: Display SKILL.md content +- **UPDATE**: Re-download the latest version from the source URL +- **DELETE**: Remove the local copy (does not affect the source) +- **Copy Source URL**: Quickly share with others + +--- + +## Skill file specification + +Remote skills must follow standard Markdown format: + +### Minimum required structure + +```markdown +--- +name: skill_internal_name +description: Short description +version: 1.0.0 +tags: [tag1, tag2] +--- + +# Skill name + +Detailed description... + +## Input + +Indicates what parameters are received + +## Processing process + +Specific steps... + +## Output specifications + +Output format description +``` + +### 完整示例 + +```markdown +--- +name: code_review +description: Structural review and optimization recommendations for Python/JavaScript code +version: 2.1.0 +author: openclaw-ai +tags: [code-quality, security, performance] +Compatible agents: [Not, Xingbu, Menxia] +--- + +# Code review skills + +This skill is specifically designed to conduct multi-dimensional reviews of production code... + +## Input + +- `code`: the source code to be reviewed +- `language`: programming language (python, javascript, go, rust) +- `focusAreas`: review focus (security, performance, style, structure) + +## Processing process + +1. Language recognition and grammar verification +2. Security vulnerability scanning +3. Performance bottleneck identification +4. Code style check +5. Best practice recommendations + +## Output specifications + +```json +{ +"issues": [ +{ +"type": "security|performance|style|structure", +"severity": "critical|high|medium|low", +"location": "line:column", +"message": "Problem description", +"suggestion": "Repair suggestion" +} +], +"summary": { +"totalIssues": 3, +"criticalCount": 1, +"highCount": 2 +} +} +``` + +## Applicable scenarios + +- Code output review by Ministry of War (code implementation) +- Security inspection by the Ministry of Justice (Compliance Audit) +- Quality assessment of the province (review and control) + +## Dependencies and limitations + +- Requires Python 3.9+ +- Supported file size: up to 50KB +- Execution timeout: 30 seconds +``` + +--- + +## Data storage + +### Local storage structure + +``` +~/.openclaw/ +├── workspace-zhongshu/ +│ └── skills/ +│ ├── code_review/ +│ │ ├── SKILL.md +│ │ └── .source.json # Store source URL and metadata +│ └── api_design/ +│ ├── SKILL.md +│ └── .source.json +├──... +``` + +### .source.json format + +```json +{ + "skillName": "code_review", + "sourceUrl": "https://raw.githubusercontent.com/...", + "description": "代码审查专项技能", + "version": "2.1.0", + "addedAt": "2026-03-02T14:30:00Z", + "lastUpdated": "2026-03-02T14:30:00Z", + "lastUpdateCheck": "2026-03-02T15:00:00Z", + "checksum": "sha256:abc123...", + "status": "valid" +} +``` + +--- + +## Security considerations + +### URL verification + +✅ **Allowed URL types:** +- HTTPS URLs: `https://` +- Local file: `file://` or absolute path +- Relative path: `./skills/` + +❌ **BANNED URL TYPES:** +- HTTP (not HTTPS): `http://` rejected +- Local mode HTTP: `http://localhost/` (avoid loopback attacks) +- FTP/SSH: `ftp://`, `ssh://` + +### Content verification + +1. **Format Verification**: Ensure it is a valid Markdown YAML frontmatter +2. **Size Limit**: Maximum 10 MB +3. **Timeout Protection**: Automatically abort downloading if it exceeds 30 seconds +4. **Path Traversal Protection**: Check the parsed skill name and disable `../` mode +5. **checksum verification**: Optional GPG signature verification (official library only) + +### Isolated execution + +- Remote skills are executed in a sandbox (provided by the OpenClaw runtime) +- Unable to access sensitive files such as `~/.openclaw/config.json` +- Only the assigned workspace directory can be accessed + +--- + +## Troubleshooting + +### FAQ + +**Q: Download failed, prompting "Connection timeout"** + +A: Check network connection and URL validity: +```bash +curl -I https://raw.githubusercontent.com/... +``` + +**Q: Skill shows "invalid" status** + +A: Check the file format: +```bash +python3 -m json.tool ~/.openclaw/workspace-zhongshu/skills/xxx/SKILL.md +``` + +**Q: Can I import from a private GitHub repository? ** + +A: Not supported (for security reasons). Can: +1. Make the repository public +2. Add directly after downloading locally +3. Public link via GitHub Gist + +**Q: How to create your own skills library? ** + +A: Create your own warehouse by referring to the structure of [OpenClaw Skills Hub](https://github.com/openclaw-ai/skills-hub), and then: + +```bash +git clone https://github.com/yourname/my-skills-hub.git +cd my-skills-hub +#Create skill file structure +# Commit & push to GitHub +``` + +Then add it through the URL or official library import function. + +--- + +## Best Practices + +### 1. Version management + +Always mark the version number in the frontmatter of SKILL.md: +```yaml +--- +version: 2.1.0 +--- +``` + +### 2. Backward compatibility + +Keep input/output formats compatible when updating skills to avoid breaking existing processes. + +### 3. Complete documentation + +Contains detailed: +- Function description +- Applicable scenarios +- Dependency description +- Output example + +### 4. Regular updates + +Set up regular checks for updates (the cycle can be configured in the dashboard): +```bash +python3 scripts/skill_manager.py check-updates --interval weekly +``` + +### 5. Contribute to the community + +Mature skills can be contributed to [OpenClaw Skills Hub](https://github.com/openclaw-ai/skills-hub). + +--- + +## API complete reference + +For details, see Part 3 (API and Tools) of [Task Dispatch Architecture Document](task-dispatch-architecture.md). + +--- + +

+Use the open ecosystem to empower institutionalized AI collaboration +

diff --git a/docs/remote-skills-quickstart_EN-googletrans.md b/docs/remote-skills-quickstart_EN-googletrans.md new file mode 100644 index 00000000..29072afd --- /dev/null +++ b/docs/remote-skills-quickstart_EN-googletrans.md @@ -0,0 +1,336 @@ +# Remote Skills Quick Start + +## 5 minutes experience + +### 1. Start the server + +```bash +# Make sure you are in the project root directory +python3 dashboard/server.py +# Output: Three provinces and six departments signboard started → http://127.0.0.1:7891 +``` + +### 2. Add official Skill (CLI) + +```bash +# Add code review skill to Zhongshu Province +python3 scripts/skill_manager.py add-remote \ + --agent zhongshu \ + --name code_review \ + --source https://raw.githubusercontent.com/openclaw-ai/skills-hub/main/code_review/SKILL.md \ + --description "代码审查能力" + +# Output: +# ⏳ Downloading from https://raw.githubusercontent.com/... +# ✅ Skill code_review has been added to zhongshu +# Path: /Users/xxx/.openclaw/workspace-zhongshu/skills/code_review/SKILL.md +# Size: 2048 bytes +``` + +### 3. List all remote Skills + +```bash +python3 scripts/skill_manager.py list-remote + +# Output: +# 📋 Total 1 remote skills: +# +# Agent | Skill name | Description | Add time +# ------------|-----------------------|--------------------------------|---------- +# zhongshu | code_review | Code review capabilities | 2026-03-02 +``` + +### 4. View API response + +```bash +curl http://localhost:7891/api/remote-skills-list | jq . + +# Output: +# { +# "ok": true, +# "remoteSkills": [ +# { +# "skillName": "code_review", +# "agentId": "zhongshu", +# "sourceUrl": "https://raw.githubusercontent.com/...", +# "description": "Code review capability", +# "localPath": "/Users/xxx/.openclaw/workspace-zhongshu/skills/code_review/SKILL.md", +# "addedAt": "2026-03-02T14:30:00Z", +# "lastUpdated": "2026-03-02T14:30:00Z", +# "status": "valid" +# } +# ], +# "count": 1, +# "listedAt": "2026-03-02T14:35:00Z" +# } +``` + +--- + +## Common operations + +### Import all skills in the official library with one click + +```bash +python3 scripts/skill_manager.py import-official-hub \ + --agents zhongshu,menxia,shangshu,bingbu,xingbu +``` + +This automatically adds for each agent: +- **zhongshu**: code_review, api_design, doc_generation +- **menxia**: code_review, api_design, security_audit, data_analysis, doc_generation, test_framework +- **shangshu**: Same as menxia (coordinator) +- **bingbu**: code_review, api_design, test_framework +- **xingbu**: code_review, security_audit, test_framework + +### Update a Skill to the latest version + +```bash +python3 scripts/skill_manager.py update-remote \ + --agent zhongshu \ + --name code_review + +# Output: +# ⏳ Downloading from https://raw.githubusercontent.com/... +# ✅ Skill code_review has been added to zhongshu +# ✅ Skills have been updated +# Path: /Users/xxx/.openclaw/workspace-zhongshu/skills/code_review/SKILL.md +# Size: 2156 bytes +``` + +### Remove a Skill + +```bash +python3 scripts/skill_manager.py remove-remote \ + --agent zhongshu \ + --name code_review + +# Output: +# ✅ Skill code_review has been removed from zhongshu +``` + +--- + +## Kanban UI operation + +### Add Remote Skill to the dashboard + +1. Open http://localhost:7891 +2. Enter the 🔧 **Skill Configuration** panel +3. Click the **➕Add Remote Skill** button +4. Fill out the form: +- **Agent**: Select from the drop-down list (such as zhongshu) +- **Skill Name**: Enter an internal ID such as `code_review` +- **Remote URL**: Paste the GitHub URL such as `https://raw.githubusercontent.com/openclaw-ai/skills-hub/main/code_review/SKILL.md` +- **Chinese description**: optional, such as `code review ability` +5. Click the **Import** button +6. Wait 1-2 seconds to see the ✅ success prompt + +### Manage added Skills + +On the dashboard → 🔧 Skill configuration → **Remote Skills** tab: + +- **View**: Click on the Skill name to view the SKILL.md content +- **UPDATE**: Click 🔄 to re-download the latest version from the source URL +- **DELETE**: Click ✕ to remove the local copy +- **Copy URL**: Quickly share with others + +--- + +## Create your own Skill library + +### Directory structure + +``` +my-skills-hub/ +├── code_review/ +│ └── SKILL.md # Code review capability +├── api_design/ +│ └── SKILL.md #API Design Review +├── data_analysis/ +│ └── SKILL.md # Data Analysis +└── README.md +``` + +### SKILL.md template + +```markdown +--- +name: my_custom_skill +description: short description +version: 1.0.0 +tags: [tag1, tag2] +--- + +# Skill full name + +Detailed description... + +## Input + +Indicates what parameters are received + +## Processing process + +Specific steps... + +## Output specifications + +Output format description +``` + +### Upload to GitHub + +```bash +git init +git add . +git commit -m "Initial commit: my-skills-hub" +git remote add origin https://github.com/yourname/my-skills-hub +git push -u origin main +``` + +### Import your own Skill + +```bash +python3 scripts/skill_manager.py add-remote \ + --agent zhongshu \ + --name my_skill \ + --source https://raw.githubusercontent.com/yourname/my-skills-hub/main/my_skill/SKILL.md \ + --description "我的定制技能" +``` + +--- + +## API complete reference + +### POST /api/add-remote-skill + +Add a remote skill. + +**ask:** +```bash +curl -X POST http://localhost:7891/api/add-remote-skill \ + -H "Content-Type: application/json" \ + -d '{ + "agentId": "zhongshu", + "skillName": "code_review", + "sourceUrl": "https://raw.githubusercontent.com/...", + "description": "代码审查" + }' +``` + +**Response (200):** +```json +{ + "ok": true, + "message": "技能 code_review 已从远程源添加到 zhongshu", + "skillName": "code_review", + "agentId": "zhongshu", + "source": "https://raw.githubusercontent.com/...", + "localPath": "/Users/xxx/.openclaw/workspace-zhongshu/skills/code_review/SKILL.md", + "size": 2048, + "addedAt": "2026-03-02T14:30:00Z" +} +``` + +### GET /api/remote-skills-list + +List all remote skills. + +```bash +curl http://localhost:7891/api/remote-skills-list +``` + +**response:** +```json +{ + "ok": true, + "remoteSkills": [ + { + "skillName": "code_review", + "agentId": "zhongshu", + "sourceUrl": "https://raw.githubusercontent.com/...", + "description": "代码审查能力", + "localPath": "/Users/xxx/.openclaw/workspace-zhongshu/skills/code_review/SKILL.md", + "addedAt": "2026-03-02T14:30:00Z", + "lastUpdated": "2026-03-02T14:30:00Z", + "status": "valid" + } + ], + "count": 1, + "listedAt": "2026-03-02T14:35:00Z" +} +``` + +### POST /api/update-remote-skill + +Update the remote skill to the latest version. + +```bash +curl -X POST http://localhost:7891/api/update-remote-skill \ + -H "Content-Type: application/json" \ + -d '{ + "agentId": "zhongshu", + "skillName": "code_review" + }' +``` + +### DELETE /api/remove-remote-skill + +Remove remote skills. + +```bash +curl -X POST http://localhost:7891/api/remove-remote-skill \ + -H "Content-Type: application/json" \ + -d '{ + "agentId": "zhongshu", + "skillName": "code_review" + }' +``` + +--- + +## Troubleshooting + +### Q: Download failed, prompting "Connection timeout" + +**A:** Check network connection and URL validity + +```bash +curl -I https://raw.githubusercontent.com/... +# Should return HTTP/1.1 200 OK +``` + +### Q: The file format is invalid + +**A:** Make sure SKILL.md starts with YAML frontmatter + +```markdown +--- +name: skill_name +description: description +--- + +# The text begins... +``` + +### Q: I can’t see Skill after importing + +**A:** Refresh the dashboard or check whether the Agent is configured correctly + +```bash +# Check if Agent exists +python3 scripts/skill_manager.py list-remote + +# Check local files +ls -la ~/.openclaw/workspace-zhongshu/skills/ +``` + +--- + +## More information + +- 📚 [Complete Guide](remote-skills-guide.md) +- 🏛️[Architecture Document](task-dispatch-architecture.md) +- 🤝 [Project Contribution](../CONTRIBUTING.md) + diff --git a/docs/task-dispatch-architecture_EN-googletrans.md b/docs/task-dispatch-architecture_EN-googletrans.md new file mode 100644 index 00000000..0f014c9c --- /dev/null +++ b/docs/task-dispatch-architecture_EN-googletrans.md @@ -0,0 +1,1599 @@ +# Task distribution and transfer system for three provinces and six departments · Business and technical structure + +> This document explains in detail how the "Three Provinces and Six Departments" project, from **business system design** to **code implementation details**, completely handles the task distribution and flow of complex multi-Agent collaboration. This is an **institutionalized AI multi-Agent framework** rather than a traditional free discussion collaboration system. + +**Document overview** + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Business layer: Imperial Governance Model +├─ Separation of powers and checks and balances: Emperor → Prince → Zhongshu → Menxia → Shangshu → Six Books +├─ System constraints: no skipping, status strictly progressive, must be reviewed by subordinates +└─ Quality assurance: capable of rejection, real-time observability, emergency intervention +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Technical layer: OpenClaw Multi-Agent Orchestration (Multi-Agent Orchestration) +├─ State machine: 9 states (Pending → Taizi → Zhongshu → Menxia → Assigned → Doing/Next → Review → Done/Cancelled) +├─ Data fusion: flow_log + progress_log + session JSONL → unified activity stream +├─Permission matrix: strict subagent call permission control +└─ Scheduling layer: automatic dispatch, timeout retry, stalled upgrade, automatic rollback +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Observation layer: React dashboard + real-time API (Dashboard + Real-time Analytics) +├─ Task dashboard: 10 view panels (all/by status/by department/by priority, etc.) +├─ Activity flow: 59 mixed activity records/task (thinking process, tool invocation, state transfer) +└─ Online status: Agent real-time node detection + heartbeat wake-up mechanism +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +--- + +## 📚 Part 1: Business Architecture + +### 1.1 Imperial system: the design philosophy of decentralization and checks and balances + +#### Core Concept + +Traditional multi-Agent frameworks (such as CrewAI, AutoGen) adopt the **"free collaboration" mode**: +- Agent independently selects collaboration partners +- The framework only provides communication channels +- Quality control completely relies on Agent intelligence +- **Problem**: It is easy for agents to create false data for each other, duplicate work, and the quality of solutions is not guaranteed. + +**Three provinces and six ministries** adopt the **"institutionalized collaboration" model**, imitating the ancient imperial bureaucracy: + +``` +emperor +(User) +│ +↓ +Taizi +[Sorting Officer, General Responsible for Message Access] +├─Identification: Is this a decree or just idle chatter? +├─ Execution: Reply directly to chat || Create task → Transfer to Zhongshu +└─ Permission: can only call Zhongshu Province +│ +↓ +Zhongshu +[Planning Officer, Chief Responsible for Plan Drafting] +├─ Analyze needs after receiving orders +├─ Break down into subtasks (todos) +├─ Invoke the Ministry of Menxia for review OR the Ministry of Secretariat for consultation +└─ Permissions: Can only call Menxia + Shangshu +│ +↓ +Menxia +[Reviewer, Quality Assurance Person] +├─ Review the Zhongshu plan (feasibility, completeness, risks) +├─Accept or reject (including modification suggestions) +├─ If rejected → Return to Zhongshu for revision → Re-examine (up to 3 rounds) +└─Permissions: Can only call Shangshu + callback Zhongshu +│ +(✅ Accurate performance) +│ +↓ +Shangshu +[Distribution officer, executive commander] +├─ Receive the accurate plan +├─Analyze which department to distribute to +├─ Call the six departments (rituals/households/soldiers/criminals/workers/officials) to execute +├─ Monitor the progress of each department → Summarize the results +└─Permissions: Only six parts can be called (cannot transfer Zhongshu beyond the authority) +│ +├─ Libu - Documentation Officer +├─Hubu - Data Analysis Officer +├─ Bingbu - Code Implementation Officer +├─ Xingbu - Test Examiner +├─ Ministry of Industry (Gongbu) - Infrastructure Officer +└─ Libu_hr - Human Resources Officer +│ +(Each department is executed in parallel) +↓ +Shangshu Province·Summary +├─ Collect six results +├─ Status changes to Review +├─ Call back to Zhongshu Sheng and report to the Emperor +│ +↓ +Zhongshu Province·Echo +├─ Summarize phenomena, conclusions, and suggestions +├─ Status changes to Done +└─ Reply to Feishu message to the Emperor +``` + +#### Four major guarantees of the system + +| Guarantee mechanism | Implementation details | Protection effect | +|---------|---------|---------| +| **Institutional review** | The province must review all Zhongshu plans and cannot be skipped | Prevent Agent from executing randomly and ensure the feasibility of the plan | +| **Separation of powers and checks and balances** | Authority matrix: Strict definition of who can transfer whom | Prevent abuse of power (such as the minister's overstepping of authority to transfer the middle book reform plan) | +| **Fully Observable** | Task dashboard 10 panels + 59 activities/tasks | See where the task is stuck, who is working, and what the working status is in real time | +| **Real-time intervention** | One-click stop/cancel/resume/advance in the dashboard | Emergency situations (such as finding that the Agent is going in the wrong direction) can be corrected immediately | + +--- + +### 1.2 Complete task transfer process + +#### Process diagram + +```mermaid +stateDiagram-v2 +[*] --> Pending: 皇上下旨 +Pending --> Taizi: 太子接旨 +Taizi --> Zhongshu: 太子转交中书 +Zhongshu --> Menxia: 中书提交审议 +Menxia --> Zhongshu: 门下封驳(可多次) +Menxia --> Assigned: 门下准奏 +Assigned --> Doing: 尚书派发执行 +Doing --> Review: 各部完成 +Review --> Done: 皇上御批通过 +Review --> Menxia: 皇上要求修改 +Done --> [*] +Doing --> [*]: 手动取消 +Review --> [*]: 业务终止 +``` + +#### Specific critical path + +**✅Ideal path** (no blockage, 4-5 days to complete) + +``` +DAY 1: +10:00 - The Emperor Feishu: "Write a complete automated testing plan for three provinces and six departments" +The prince accepted the order. state = Taizi, org = Prince +Automatically dispatch taizi agent → handle this request + +10:30 - Prince sorting is completed. Determined as "work intention" (not chatting) +Build task JJC-20260228-E2E +flow_log record: "Emperor → Prince: Decree" +state: Taizi → Zhongshu, org: Prince → Zhongshu Province +Automatically dispatch zhongshu agent + +DAY 2: +09:00 - Zhongshu Province receives the order. Start planning +Report progress: "Analyze test requirements and dismantle them into three layers: unit/integration/E2E" +progress_log record: "Zhongshu Sheng Zhang San: sub-requirements" + +15:00 - Zhongshu Province completes the plan +todos snapshot: demand analysis✅, solution design✅, to be reviewed🔄 +flow_log record: "Zhongshu Province → Menxia Province: Plan submitted for review" +state: Zhongshu → Menxia, ​​org: Zhongshu Province → Menxia Province +Automatically dispatch menxia agent + +DAY 3: +09:00 - The province begins deliberations +Progress Report: "Now review the plan for completeness and risks" + +14:00 - The review of the Menxia Province is completed +Judgment: "The solution is feasible, but the test of the _infer_agent_id_from_runtime function is missing" +Behavior:✅ Accurate (with modification suggestions) +flow_log record: "Menxia Province → Shangshu Province: ✅ The performance was approved (5 suggestions)" +state: Menxia → Assigned, org: Menxia Province → Shangshu Province +OPTIONAL: Zhongshu Province received suggestions and took the initiative to optimize the plan. +Automatically dispatch shangshu agent + +DAY 4: +10:00 - Shangshu Sheng received the report +Analysis: "This test plan should be sent to the Ministry of Industry + the Ministry of Punishment + the Ministry of Rites to complete it together" +flow_log record: "Shangshu Province → Sixth Department: dispatch and execution (cooperation of soldiers and officials)" +state: Assigned → Doing, org: Shangshu Province → Ministry of War + Ministry of Punishment + Ministry of Rites +Automatically dispatch three agents bingbu/xingbu/libu (in parallel) + +DAY 4-5: +(Each department is executed in parallel) +- Bingbu: Implement pytest + unittest testing framework +- Xingbu (xingbu): Write tests to cover all key functions +- Libu: Organize test documents and use case descriptions + +Real-time report (hourly progress): +- Ministry of War: "✅ 16 unit tests implemented" +- Ministry of Justice: "🔄 Writing integration tests (completed on 8/12)" +- Ministry of Rites: "Wait until the Ministry of War is completed before writing a report." + +DAY 5: +14:00 - All parts completed +state: Doing → Review, org: Ministry of War → Ministry of Education +Summary from Shangshu Province: "All tests have been completed, and the pass rate is 98.5%" +Transfer back to Zhongshu Province + +15:00 - Zhongshu Province responds to the Emperor +state: Review → Done +Template reply to Feishu, including final result link and summary +``` + +**❌ Frustration Path** (including rejection and retry, 6-7 days) + +``` +DAY 2 Same as above + +DAY 3 [rejection scene]: +14:00 - The review of the Menxia Province is completed +Judgment: "The plan is incomplete and lacks performance testing + stress testing" +Behavior: 🚫 Block +review_round += 1 +flow_log record: "Menxia Province → Zhongshu Province: 🚫 Failed (need to supplement performance testing)" +state: Menxia → Zhongshu # Return to Zhongshu for modification +Automatically dispatch zhongshu agent (replanning) + +DAY 3-4: +16:00 - Zhongshu Province receives the rejection notice (wakes up the agent) +Analyze improvement suggestions and supplement performance testing plans +progress: "Performance testing requirements have been integrated, and the correction plan is as follows..." +flow_log record: "Zhongshu Province → Menxia Province: Revised plan (2nd round of review)" +state: Zhongshu → Menxia +Automatically dispatch menxia agent + +18:00 - Menxia Province reconsideration +Judgment: "✅ Passed this time" +flow_log record: "Menxia Province → Shangshu Province: ✅ Passed (2nd round)" +state: Menxia → Assigned → Doing +The follow-up is the same as the ideal path... + +DAY 7: All completed (1-2 days later than ideal path) +``` + +--- + +### 1.3 Task Specification and Business Contract + +#### Task Schema field description + +```json +{ + "id": "JJC-20260228-E2E", // 任务全局唯一ID (JJC-日期-序号) + "title": "为三省六部编写完整自动化测试方案", + "official": "中书令", // 负责官职 + "org": "中书省", // 当前负责部门 + "state": "Assigned", // 当前状态(见 _STATE_FLOW) + + // ──── 质量与约束 ──── + "priority": "normal", // 优先级:critical/high/normal/low + "block": "无", // 当前阻滞原因(如"等待工部反馈") + "reviewRound": 2, // 门下审议第几轮 + "_prev_state": "Menxia", // 若被 stop,记录之前状态用于 resume + + // ──── 业务产出 ──── + "output": "", // 最终任务成果(URL/文件路径/总结) + "ac": "", // Acceptance Criteria(验收标准) + "priority": "normal", + + // ──── 流转记录 ──── + "flow_log": [ + { + "at": "2026-02-28T10:00:00Z", + "from": "皇上", + "to": "太子", + "remark": "下旨:为三省六部编写完整自动化测试方案" + }, + { + "at": "2026-02-28T10:30:00Z", + "from": "太子", + "to": "中书省", + "remark": "分拣→传旨" + }, + { + "at": "2026-02-28T15:00:00Z", + "from": "中书省", + "to": "门下省", + "remark": "规划方案提交审议" + }, + { + "at": "2026-03-01T09:00:00Z", + "from": "门下省", + "to": "中书省", + "remark": "🚫 封驳:需补充性能测试" + }, + { + "at": "2026-03-01T15:00:00Z", + "from": "中书省", + "to": "门下省", + "remark": "修订方案(第2轮审议)" + }, + { + "at": "2026-03-01T20:00:00Z", + "from": "门下省", + "to": "尚书省", + "remark": "✅ 准奏通过(第2轮,5条建议已采纳)" + } + ], + + // ──── Agent 实时汇报 ──── + "progress_log": [ + { + "at": "2026-02-28T10:35:00Z", + "agent": "zhongshu", // 汇报agent + "agentLabel": "中书省", + "text": "已接旨。分析测试需求,拟定三层测试方案...", + "state": "Zhongshu", // 汇报时的状态快照 + "org": "中书省", + "tokens": 4500, // 资源消耗 + "cost": 0.0045, + "elapsed": 120, + "todos": [ // 待办任务快照 + {"id": "1", "title": "需求分析", "status": "completed"}, + {"id": "2", "title": "方案设计", "status": "in-progress"}, + {"id": "3", "title": "await审议", "status": "not-started"} + ] + }, + // ... 更多 progress_log 条目 ... + ], + + // ──── 调度元数据 ──── + "_scheduler": { + "enabled": true, + "stallThresholdSec": 180, // 停滞超过180秒自动升级 + "maxRetry": 1, // 自动重试最多1次 + "retryCount": 0, + "escalationLevel": 0, // 0=无升级 1=门下协调 2=尚书协调 + "lastProgressAt": "2026-03-01T20:00:00Z", + "stallSince": null, // 何时开始停滞 + "lastDispatchStatus": "success", // queued|success|failed|timeout|error + "snapshot": { + "state": "Assigned", + "org": "尚书省", + "note": "review-before-approve" + } + }, + + // ──── 生命周期 ──── + "archived": false, // 是否归档 + "now": "门下省准奏,移交尚书省派发", // 当前实时状态描述 + "updatedAt": "2026-03-01T20:00:00Z" +} +``` + +#### Business Contract + +| Contract | Meaning | Consequences of breach | +|------|------|---------| +| **Not allowed to skip levels** | The prince can only transfer the middle secretary, the middle secretary can only transfer the subordinates/changshu, and the six departments cannot be called externally | Over-authorized calls are rejected and the system automatically intercepts them | +| **Status progresses in one direction** | Pending → Taizi → Zhongshu → ... → Done, cannot skip or go back | You can only return to the previous step through review_action(reject) | +| **Must be reviewed by the subordinates** | All plans proposed by the Zhongshu must be reviewed by the subordinates and cannot be skipped | The Zhongshu cannot directly transfer to the Minister, and the subordinates must enter | +| **No changes once Done** | The task cannot be modified after it enters the Done/Cancelled state | If you need to modify it, you need to create a new task or cancel and rebuild | +| **task_id uniqueness** | JJC-date-serial number is globally unique, the same task will not be created repeatedly on the same day | Kanban anti-duplication, automatic deduplication | +| **Transparent resource consumption** | Every progress report must be reported tokens/cost/elapsed | Facilitate cost accounting and performance optimization | + +--- + +## 🔧 Part 2: Technical Architecture + +### 2.1 State machine and automatic dispatch + +#### Complete definition of state transfer + +```python +_STATE_FLOW = { + 'Pending': ('Taizi', '皇上', '太子', '待处理旨意转交太子分拣'), + 'Taizi': ('Zhongshu','太子', '中书省', '太子分拣完毕,转中书省起草'), + 'Zhongshu': ('Menxia', '中书省', '门下省', '中书省方案提交门下省审议'), + 'Menxia': ('Assigned','门下省', '尚书省', '门下省准奏,转尚书省派发'), + 'Assigned': ('Doing', '尚书省', '六部', '尚书省开始派发执行'), + 'Next': ('Doing', '尚书省', '六部', '待执行任务开始执行'), + 'Doing': ('Review', '六部', '尚书省', '各部完成,进入汇总'), + 'Review': ('Done', '尚书省', '太子', '全流程完成,回奏太子转报皇上'), +} +``` + +Each state is automatically associated with an Agent ID (see `_STATE_AGENT_MAP`): + +```python +_STATE_AGENT_MAP = { + 'Taizi': 'taizi', + 'Zhongshu': 'zhongshu', + 'Menxia': 'menxia', + 'Assigned': 'shangshu', + 'Doing': None, # 从 org 推断(六部之一) + 'Next': None, # 从 org 推断 + 'Review': 'shangshu', + 'Pending': 'zhongshu', +} +``` + +#### Automatic distribution process + +When the task state transfers (through `handle_advance_state()` or approval), the background automatically executes dispatch: + +``` +1. State transfer triggers distribution +├─ Look up the table _STATE_AGENT_MAP to get the target agent_id +├─ If it is Doing/Next, look up the table _ORG_AGENT_MAP from task.org to infer the specific department agent +└─ If it cannot be inferred, skip dispatch (such as Done/Cancelled) + +2. Construct and distribute messages (targeted to prompt Agent to work immediately) +├─ taizi: "📜 The emperor’s decree needs you to handle..." +├─ zhongshu: "📜 The decree has been sent to Zhongshu Province, please draft a plan..." +├─ menxia: "📋 Zhongshu Provincial Plan is submitted for review..." +├─ shangshu: "📮 The Ministry of Menxia has approved the report, please dispatch it for execution..." +└─ Part 6: "📌 Please handle the task..." + +3. Background asynchronous dispatch (non-blocking) +├─ spawn daemon thread +├─ Tag _scheduler.lastDispatchStatus = 'queued' +├─ Check if the Gateway process is open +├─ Run openclaw agent --agent {id} -m "{msg}" --deliver --timeout 300 +├─ Retry up to 2 times (failure interval 5 seconds back off) +├─ Update _scheduler status and error messages +└─ flow_log records dispatch results + +4. Distribution status transfer +├─ success: update immediately _scheduler.lastDispatchStatus = 'success' +├─ failed: Record the reason for failure. Agent will not block the kanban when it times out. +├─ timeout: Mark timeout to allow users to manually retry/upgrade +├─ gateway-offline: Gateway is not started, skip this distribution (you can try again later) +└─ error: abnormal situation, record stack for debugging + +5. Processing of reaching the target Agent +├─ Agent receives notification from Feishu message +├─ Interact with Kanban (update status/record progress) through kanban_update.py +└─ After completing the work, it will be triggered again and dispatched to the next Agent. +``` + +--- + +### 2.2 Permission matrix and Subagent call + +#### Permission definition (configured in openclaw.json) + +```json +{ + "agents": [ + { + "id": "taizi", + "label": "太子", + "allowAgents": ["zhongshu"] + }, + { + "id": "zhongshu", + "label": "中书省", + "allowAgents": ["menxia", "shangshu"] + }, + { + "id": "menxia", + "label": "门下省", + "allowAgents": ["shangshu", "zhongshu"] + }, + { + "id": "shangshu", + "label": "尚书省", + "allowAgents": ["libu", "hubu", "bingbu", "xingbu", "gongbu", "libu_hr"] + }, + { + "id": "libu", + "label": "礼部", + "allowAgents": [] + }, + // ... 其他六部同样 allowAgents = [] ... + ] +} +``` + +#### Permission checking mechanism (code level) + +In addition to `dispatch_for_state()`, there is a set of defensive permission checks: + +```python +def can_dispatch_to(from_agent, to_agent): + """检查 from_agent 是否有权调用 to_agent。""" + cfg = read_json(DATA / 'agent_config.json', {}) + agents = cfg.get('agents', []) + + from_record = next((a for a in agents if a.get('id') == from_agent), None) + if not from_record: + return False, f'{from_agent} 不存在' + + allowed = from_record.get('allowAgents', []) + if to_agent not in allowed: + return False, f'{from_agent} 无权调用 {to_agent}(允许列表:{allowed})' + + return True, 'OK' +``` + +#### Examples and handling of permission violations + +| Scenario | Request | Result | Reason | +|------|------|------|------| +| **Normal** | Zhongshu Province → Menxia Province Review | ✅ Allow | Menxia is in Zhongshu’s allowAgents | +| **Violation** | Zhongshu Province → Shangshu Province Reform Plan | ❌ Reject | Zhongshu can only transfer the subordinates/ministers, but cannot manually change the ministerial work | +| **Violation** | Ministry of Industry → Shangshu Province "I'm done" | ✅ Change status | Through flow_log and progress_log (not cross-Agent call) | +| **Violation** | Shangshu Province → Zhongshu Province "Re-Change Plan" | ❌ Reject | Shangshu is not under the sect/Zhongshu's allowAgents | +| **Prevention and Control** | Agent forges distribution of other agents | ❌ Interception | API layer verification HTTP request source/signature | + +--- + +### 2.3 Data fusion: progress_log + session JSONL + +#### Phenomenon + +When a task is executed, there are three layers of data sources: + +``` +1️⃣ flow_log +└─ Pure record state transfer (Zhongshu → Menxia) +└─ Data source: flow_log field of task JSON +└─ From: Agent reported through kanban_update.py flow command + +2️⃣ progress_log +└─ Agent’s real-time work report (text progress, todos snapshot, resource consumption) +└─ Data source: progress_log field of task JSON +└─ From: Agent reported through kanban_update.py progress command +└─ Period: usually reported once every 30 minutes or key nodes + +3️⃣ session JSONL (new!) +└─ Agent’s internal thinking process (thinking), tool invocation (tool_result), and conversation history (user) +└─ Data source: ~/.openclaw/agents/{agent_id}/sessions/*.jsonl +└─ From: OpenClaw framework automatically records, the agent does not need to actively operate +└─ Cycle: message level, the finest granularity +``` + +#### Problem Diagnosis + +In the past, only flow_log + progress_log was used to show progress: +- ❌ Can’t see the Agent’s specific thinking process +- ❌ Can’t see the results of each tool call +- ❌ Cannot see the conversation history between Agents +- ❌ Agent exhibits "black box status" + +For example: progress_log records "analyzing requirements", but users cannot see what is being analyzed. + +#### Solution: Session JSONL Fusion + +Add fusion logic in `get_task_activity()` (line 40): + +```python +def get_task_activity(task_id): + #...The previous code is the same as above... + + # ── Integrate Agent Session activities (thinking/tool_result/user)── + session_entries = [] + + # Active tasks: try to match exactly by task_id + if state not in ('Done', 'Cancelled'): + if agent_id: + entries = get_agent_activity( + agent_id, limit=30, task_id=task_id + ) + session_entries.extend(entries) + + # Also obtained from related Agents + for ra in related_agents: + if ra != agent_id: + entries = get_agent_activity( + ra, limit=20, task_id=task_id + ) + session_entries.extend(entries) + else: + # Completed task: matching based on keywords + title = task.get('title', '') + keywords = _extract_keywords(title) + if keywords: + for ra in related_agents[:5]: + entries = get_agent_activity_by_keywords( + ra, keywords, limit=15 + ) + session_entries.extend(entries) + + # Deduplication (avoid duplication through at+kind deduplication) + existing_keys = {(a.get('at', ''), a.get('kind', '')) for a in activity} + for se in session_entries: + key = (se.get('at', ''), se.get('kind', '')) + if key not in existing_keys: + activity.append(se) + existing_keys.add(key) + + # Reorder + activity.sort(key=lambda x: x.get('at', '')) + + # Mark the data source when returning + return { + 'activity': activity, + 'activitySource': 'progress+session', # 新标记 + # ...other fields ... + } +``` + +#### Session JSONL format parsing + +The items extracted from JSONL are uniformly converted into Kanban activity items: + +```python +def _parse_activity_entry(item): + """将 session jsonl 的 message 统一解析成看板活动条目。""" + msg = item.get('message', {}) + role = str(msg.get('role', '')).strip().lower() + ts = item.get('timestamp', '') + + # 🧠 Assistant role - Agent thinking process + if role == 'assistant': + entry = { + 'at': ts, + 'kind': 'assistant', + 'text': '...主回复...', + 'thinking': '💭 Agent考虑到...', # 内部思维链 + 'tools': [ + {'name': 'bash', 'input_preview': 'cd /src && npm test'}, + {'name': 'file_read', 'input_preview': 'dashboard/server.py'}, + ] + } + return entry + + # 🔧 Tool Result - Tool call result + if role in ('toolresult', 'tool_result'): + entry = { + 'at': ts, + 'kind': 'tool_result', + 'tool': 'bash', + 'exitCode': 0, + 'output': '✓ All tests passed (123 tests)', + 'durationMs': 4500 # 执行时长 + } + return entry + + # 👤 User - human feedback or conversation + if role == 'user': + entry = { + 'at': ts, + 'kind': 'user', + 'text': '请实现测试用例的异常处理' + } + return entry +``` + +#### Fusion activity flow structure + +59 activity streams for a single task (JJC-20260228-E2E example): + +``` +kind count represents events +──────────────────────────────────────────── +flow 10 state transfer chain (Pending→Taizi→Zhongshu→...) +progress 11 Agent work report ("analyzing", "completed") +todos 11 to-do task snapshot (each item when the progress is updated) +user 1 user feedback (such as "need to supplement performance testing") +assistant 10 Agent thinking process (💭 reasoning chain) +tool_result 16 Tool call record (bash running result, API call result) +──────────────────────────────────────────── +Total 59 complete work tracks +``` + +When the board is displayed, users can: +- 📋 Look at the circulation chain to understand which stage the task is in +- 📝 Watch progress to understand what the Agent said in real time +- ✅ View todos to learn about task dismantling and completion progress +- 💭 See assistant/thinking to understand the Agent’s thinking process +- 🔧 See tool_result to understand the result of each tool call +- 👤 Look at user to see if there is manual intervention + +--- + +### 2.4 Scheduling system: timeout retry, stalled upgrade, automatic rollback + +#### Scheduling metadata structure + +```python +_scheduler = { + # Configuration parameters + 'enabled': True, + 'stallThresholdSec': 180, # 停滞多久后自动升级(默认180秒) + 'maxRetry': 1, # 自动重试次数(0=不重试,1=重试1次) + 'autoRollback': True, # 是否自动回滚到快照 + + # Runtime status + 'retryCount': 0, # 当前已重试几次 + 'escalationLevel': 0, # 0=无升级 1=门下协调 2=尚书协调 + 'stallSince': None, # 何时开始停滞的时间戳 + 'lastProgressAt': '2026-03-01T...', # 最后一次获得进展的时间 + 'lastEscalatedAt': '2026-03-01T...', + 'lastRetryAt': '2026-03-01T...', + + # Dispatch Tracking + 'lastDispatchStatus': 'success', # queued|success|failed|timeout|gateway-offline|error + 'lastDispatchAgent': 'zhongshu', + 'lastDispatchTrigger': 'state-transition', + 'lastDispatchError': '', # 错误堆栈(如有) + + # Snapshot (for automatic rollback) + 'snapshot': { + 'state': 'Assigned', + 'org': '尚书省', + 'now': '等待派发...', + 'savedAt': '2026-03-01T...', + 'note': 'scheduled-check' + } +} +``` + +#### Scheduling algorithm + +Run `handle_scheduler_scan(threshold_sec=180)` every 60 seconds: + +``` +FOR EACH task: +IF state in (Done, Canceled, Blocked): +SKIP # The final state is not processed + +elapsed_since_progress = NOW - lastProgressAt + +IF elapsed_since_progress < stallThreshold: +SKIP # There has been recent progress, no need to deal with it + +# ── Stall processing logic ── +IF retryCount < maxRetry: +✅ Execute【Retry】 +- increment retryCount +- dispatch_for_state(task, new_state, trigger='taizi-scan-retry') +- flow_log: "Stayed for 180 seconds, triggering automatic retry for the Nth time" +- NEXT task + +IF escalationLevel < 2: +✅ Execute【Upgrade】 +- nextLevel = escalationLevel + 1 +- target_agent = menxia (if L=1) else shangshu (if L=2) +- wake_agent(target_agent, "💬 The task is stalled, please step in to coordinate and advance") +- flow_log: "Upgrade to {target_agent} coordination" +- NEXT task + +IF escalationLevel >= 2 AND autoRollback: +✅ Execute [Automatic Rollback] +- restore task to snapshot.state +- retryCount = 0 +- escalationLevel = 0 +- dispatch_for_state(task, snapshot.state, trigger='taiji-auto-rollback') +- flow_log: "Continuous stagnation, automatic rollback to {snapshot.state}" +``` + +#### Example scenario + +**Scenario: Zhongshu Province Agent process crashes and the task is stuck in Zhongshu** + +``` +T+0: +Zhongshu Province is planning a plan +lastProgressAt = T +dispatch status=success + +T+30: +Agent process crashes unexpectedly (or becomes overloaded and unresponsive) +lastProgressAt still = T (no new progress) + +T+60: +Scheduler_scan scanned it and found: +elapsed = 60 < 180, skip + +T+180: +Scheduler_scan scanned it and found: +elapsed = 180 >= 180, trigger processing + +✅ Phase 1: Try again +- retryCount: 0 → 1 +- dispatch_for_state('JJC-20260228-E2E', 'Zhongshu', trigger='taizi-scan-retry') +- Distribute messages to Zhongshu Province (wake up the agent or restart it) +- flow_log: "Stayed for 180 seconds, automatically retry for the first time" + +T+240: +Zhongshu Province Agent is restored (or restarted manually) and retry distribution is received. +Report progress: "Recovered, continue planning..." +lastProgressAt is updated to T+240 +retryCount reset to 0 + +✓ Problem solving + +T+360 (if still not recovered): +scheduler_scan scan again and find: +elapsed = 360 >= 180, retryCount already = 1 + +✅ Phase 2: Upgrade +- escalationLevel: 0 → 1 +- wake_agent('menxia', "💬 Task JJC-20260228-E2E is stalled, there is no response from Zhongshu Province, please intervene") +- flow_log: "Upgrade to Menxia Province Coordination" + +When the Menxia Province Agent is awakened, it can: +- Check whether Zhongshu Province is online +- If online, ask about progress +- If offline, emergency procedures may be initiated (such as being temporarily drafted by a subordinate) + +T+540 (if still unresolved): +scheduler_scan scan again and find: +escalationLevel = 1, can also be upgraded to 2 + +✅ Phase 3: Upgrade again +- escalationLevel: 1 → 2 +- wake_agent('shangshu', "💬 The task has been stalled for a long time. Neither Zhongshu Province nor Menxia Province can advance it. Shangshu Province please intervene and coordinate") +- flow_log: "Upgrade to Shangshu Provincial Coordination" + +T+720 (if still unresolved): +scheduler_scan scan again and find: +escalationLevel = 2 (maximum), autoRollback = true + +✅ Phase 4: Automatic rollback +- snapshot.state = 'Assigned' (previous stable state) +- task.state: Zhongshu → Assigned +- dispatch_for_state('JJC-20260228-E2E', 'Assigned', trigger='taizi-auto-rollback') +- flow_log: "Continuous stagnation, automatically rolled back to Assigned, and redistributed by Shangshu Sheng" + +result: +- The Shangshu Province redistributed it to the six ministries for execution +- Zhongshu Province’s solution is retained in the previous snapshot version +- Users can see the rollback operation and decide whether to intervene +``` + +--- + +## 🎯 Part 3: Core API and CLI Tools + +### 3.1 Task operation API endpoint + +#### Task creation: `POST /api/create-task` + +``` +ask: +{ +"title": "Written a complete automated test plan for three provinces and six departments", +"org": "Zhongshu Sheng", // optional, defaults to Prince +"official": "Zhongshu Ling", // optional +"priority": "normal", +"template_id": "test_plan", // optional +"params": {}, +"target_dept": "Ministry of War + Ministry of Punishment" // Optional, send suggestions +} + +response: +{ +"ok": true, +"taskId": "JJC-20260228-001", +"message": "Decree JJC-20260228-001 has been issued and is being distributed to the prince" +} +``` + +#### Task activity flow: `GET /api/task-activity/{task_id}` + +``` +ask: +GET /api/task-activity/JJC-20260228-E2E + +response: +{ +"ok": true, +"taskId": "JJC-20260228-E2E", +"taskMeta": { +"title": "Written a complete automated test plan for three provinces and six departments", +"state": "Assigned", +"org": "Shang Shu Sheng", +"output": "", +"block": "none", +"priority": "normal" +}, +"agent ID": "above", +"agentLabel": "Shang Shu Sheng", + +//── Complete activity flow (59 examples)── +"activity": [ +// flow_log (10 items) +{ +"at": "2026-02-28T10:00:00Z", +"kind": "flow", +"from": "Your Majesty", +"to": "Prince", +"remark": "Declaration: Write a complete automated testing plan for three provinces and six departments" +}, +// progress_log (11 items) +{ +"at": "2026-02-28T10:35:00Z", +"kind": "progress", +"text": "Accepted the order. Analyze the test requirements and formulate a three-layer test plan...", +"Argentina": "Heat stroke", +"agentLabel": "Zhongshu Province", +"state": "Z sweet potato", +"org": "中书省", +"tokens": 4500, +"cost": 0.0045, +"elapsed": 120 +}, +// todos (11 items) +{ +"at": "2026-02-28T15:00:00Z", +"kind": "todos", +"items": [ +{"id": "1", "title": "Requirements Analysis", "status": "completed"}, +{"id": "2", "title": "Project Design", "status": "in-progress"}, +{"id": "3", "title": "await review", "status": "not-started"} +], +"Argentina": "Heat stroke", +"diff": { +"changed": [{"id": "2", "from": "not-started", "to": "in-progress"}], +"added": [], +"removed": [] +} +}, +// session activities (26 items in total) +// - assistant (10 items) +{ +"at": "2026-02-28T14:23:00Z", +"kind": "assistant", +"text": "Based on requirements, I recommend using a three-tier testing architecture:\n1. Unit tests cover core functions\n2. Integration tests cover API endpoints\n3. E2E tests cover the complete process", +"thinking": "💭 Considering the complexity of the project, the interaction logic of seven Agents needs to be covered. The unit test should use pytest, and the integration test should use the HTTP test after server.py is started...", +"tools": [ +{"name": "bash", "input_preview": "find . -name '*.py' -type f | wc -l"}, +{"name": "file_read", "input_preview": "dashboard/server.py (first 100 lines)"} +] +}, +// - tool_result (16 items) +{ +"at": "2026-02-28T14:24:00Z", +"kind": "tool_result", +"tool": "bash", +"exitCode": 0, +"output": "83", +"durationMs": 450 +} +], + +"activitySource": "progress+session", +"related agents": ["Prince", "Heatstroke", "Menxia"], +"phaseDurations": [ +{ +"phase": "Prince", +"durationText": "30 minutes", +"ongoing": false +}, +{ +"phase": "中书省", +"durationText": "4 hours and 32 minutes", +"ongoing": false +}, +{ +"phase": "门下胜", +"durationText": "1 hour and 15 minutes", +"ongoing": false +}, +{ +"phase": "Shang Shu Sheng", +"durationText": "4 hours and 10 minutes", +"ongoing": true +} +], +"totalDuration": "10 hours and 27 minutes", +"todosSummary": { +"total": 3, +"completed": 2, +"inProgress": 1, +"notStarted": 0, +"percent": 67 +}, +"resourceSummary": { +"totalTokens": 18500, +"totalCost": 0.0187, +"totalElapsedSec": 480 +} +} +``` + +#### State advancement: `POST /api/advance-state/{task_id}` + +``` +ask: +{ +"comment": "It's clearly time to move forward with the mission." +} + +response: +{ +"ok": true, +"message": "JJC-20260228-E2E has been advanced to the next stage (Agent has been automatically dispatched)", +"old state": "Z sweet potato", +"newState": "Menxia", +"targetAgent": "menxia" +} +``` + +#### Approval operation: `POST /api/review-action/{task_id}` + +``` +Request (accurate): +{ +"action": "approve", +"comment": "The solution is feasible and improvement suggestions have been adopted" +} + +OR request (rejection): +{ +"action": "reject", +"comment": "Supplementary performance testing is required, Nth round of review" +} + +response: +{ +"ok": true, +"message": "JJC-20260228-E2E has been played (Agent has been automatically dispatched)", +"state": "Assigned", +"reviewRound": 1 +} +``` + +--- + +### 3.2 CLI tool: kanban_update.py + +Agent interacts with the Kanban board through this tool, with a total of 7 commands: + +#### Command 1: Create task (manual by Prince or Zhongshu) + +```bash +python3 scripts/kanban_update.py create \ + JJC-20260228-E2E \ + "为三省六部编写完整自动化测试方案" \ + Zhongshu \ + 中书省 \ + 中书令 + +# Note: Usually there is no need to run it manually (the Kanban API is automatically triggered), unless debugging +``` + +#### Command 2: Update status + +```bash +python3 scripts/kanban_update.py state \ + JJC-20260228-E2E \ + Menxia \ + "方案提交门下省审议" + +# illustrate: +# - First parameter: task_id +# - Second parameter: new status (Pending/Taizi/Zhongshu/...) +# - The third parameter: optional, description information (will be recorded in the now field) +# +# Effect: +# - task.state = Menxia +# - task.org automatically infers "MenxiaSheng" +# - Trigger dispatch of menxia agent +# - flow_log record transfer +``` + +#### Command 3: Add transfer record + +```bash +python3 scripts/kanban_update.py flow \ + JJC-20260228-E2E \ + "中书省" \ + "门下省" \ + "📋 方案提交审核,请审议" + +# illustrate: +# - Parameter 1: task_id +# - Parameter 2: from_dept (who is reporting) +# - Parameter 3: to_dept (who to transfer to) +# - Parameter 4: remark (remarks, can include emoji) +# +# Note: only record flow_log, do not change task.state +# (Mostly used for detail transfer, such as coordination between departments) +``` + +#### Command 4: Real-time progress report (emphasis!) + +```bash +python3 scripts/kanban_update.py progress \ + JJC-20260228-E2E \ + "已完成需求分析和方案初稿,现正征询工部意见" \ + "1.需求分析✅|2.方案设计✅|3.工部咨询🔄|4.待门下审议" + +# illustrate: +# - Parameter 1: task_id +# - Parameter 2: Progress text description +# - Parameter 3: todos current snapshot (separate each item with |, supports emoji) +# +# Effect: +# - progress_log adds new entries: +# { +# "at": now_iso(), +# "agent": inferred_agent_id, +# "text": "The needs analysis and first draft of the plan have been completed, and the opinions of the Ministry of Industry are now being sought", +# "state": task.state, +# "org": task.org, +# "todos": [ +# {"id": "1", "title": "Requirements Analysis", "status": "completed"}, +# {"id": "2", "title": "Project Design", "status": "completed"}, +# {"id": "3", "title": "Ministry of Industry Consulting", "status": "in-progress"}, +# {"id": "4", "title": "To be reviewed", "status": "not-started"} +# ], +# "tokens": (automatically read from openclaw session data), +# "cost": (automatically calculated), +# "elapsed": (automatically calculated) +# } +# +# Kanban effect: +# - Render as active item on the fly +# - todos progress bar update (67% completed) +# - Accumulated display of resource consumption +``` + +#### Command 5: Task completed + +```bash +python3 scripts/kanban_update.py done \ + JJC-20260228-E2E \ + "https://github.com/org/repo/tree/feature/auto-test" \ + "自动化测试方案已完成,涵盖单元/集成/E2E三层,通过率98.5%" + +# illustrate: +# - Parameter 1: task_id +# - Parameter 2: output URL (can be a code repository, document link, etc.) +# - Parameter 3: Final summary +# +# Effect: +# - task.state = Done (advance from Review) +# - task.output = "https://..." +# - Automatically send Feishu message to the emperor (reported by the prince) +# - flow_log records the completed transfer +``` + +#### Command 6 & 7: Stop/Cancel Task + +```bash +# Stop (can be resumed at any time) +python3 scripts/kanban_update.py stop \ + JJC-20260228-E2E \ + "等待工部反馈继续" + +# illustrate: +# - task.state temporary storage (_prev_state) +# - task.block = "Waiting for feedback from the Ministry of Engineering to continue" +# - The board shows "⏸️ Stopped" +# +# recover: +python3 scripts/kanban_update.py resume \ + JJC-20260228-E2E \ + "工部已反馈,继续执行" +# +# - task.state is restored to _prev_state +# - redispatch agent + +# Cancel (unrecoverable) +python3 scripts/kanban_update.py cancel \ + JJC-20260228-E2E \ + "业务需求变更,任务作废" +# +# - task.state = Canceled +# - flow_log records cancellation reasons +``` + +--- + +## 💡 Part 4: Benchmarking and Comparison + +### The traditional approach of CrewAI / AutoGen vs the institutionalized approach of Three Provinces and Six Departments + +| Dimensions | CrewAI | AutoGen | **Three provinces and six departments** | +|------|--------|---------|----------| +| **Collaboration mode** | Free discussion (Agent independently selects collaboration objects) | Panel + callback (Human-in-the-loop) | **Institutionalized collaboration (authority matrix + state machine)** | +| **Quality Assurance** | Rely on Agent intelligence (no review) | Human review (frequent interruptions) | **Automatic review (required review by the province) + Intervention possible** | +| **Permission Control** | ❌ None | ⚠️ Hard-coded | **✅ Configurable Permission Matrix** | +| **Observability** | Low (Agent message black box) | Medium (Human sees the conversation) | **Extremely high (59 activities/tasks)** | +| **Interventionability** | ❌ No (difficult to stop after running) | ✅ Yes (manual approval required) | **✅ Yes (one-click stop/cancel/advance)** | +| **Task Distribution** | Uncertain (Agent selects independently) | Confirmed (Human manual classification) | **Automatically determined (authority matrix + state machine)** | +| **Throughput** | 1 task 1Agent (serial discussion) | 1 task 1Team (requires manual management) | **Multiple tasks in parallel (six tasks executed simultaneously)** | +| **Failure recovery** | ❌ (restart) | ⚠️ (requires manual debugging) | **✅ (automatic retry 3 stages)** | +| **Cost Control** | Opaque (no cost upper limit) | Medium (Human can stop) | **Transparent (cost reported for each progress)** | + +### Strictness of business contracts + +**CrewAI's "gentle" approach** +```python +# Agent can freely choose the next step of work +if task_seems_done: + # Agent decides whether to report to other Agents + send_message_to_someone() # 可能发错人,可能重复 +``` + +**The "strict" approach of three provinces and six ministries** +```python +# The task status is strictly limited, and the next step is determined by the system +if task.state == 'Zhongshu' and agent_id == 'zhongshu': + # Can only do what Zhongshu should do (drafting plan) + deliver_plan_to_menxia() + + #State transfer can only be done through API and cannot be bypassed + # Zhongshu cannot directly transfer to the Minister, it must be reviewed by the subordinates. + + # If you want to bypass the deliberation under the door + try: + dispatch_to(shangshu) # ❌ 权限检查拦截 + except PermissionError: + log.error(f'zhongshu 无权越权调用 shangshu') +``` + +--- + +## 🔍 Part 5: Failure Scenarios and Recovery Mechanism + +### Scenario 1: Agent process crashes + +``` +Symptom: The task is stuck in a certain state and there is no new progress for 180 seconds. +Alarm: Prince dispatch system detects stagnation + +Automatic processing flow: +T+0: crash +T+180: scan detects stall +✅ Phase 1: Automatic retry +- Send messages to the agent (wake up or restart) +- If the agent recovers, the process continues + +T+360: If it is not restored yet +✅ Phase 2: Upgrade coordination +- Wake up the door-to-door agent +- Report: "No response from Zhongshu Province, please intervene" +- The subordinate may take over or act as an agent + +T+540: If it is not restored yet +✅ Phase 3: Upgrade again +- Wake up Shangshu Province agent +- Report: "The task is completely stuck, please coordinate at the enterprise level" + +T+720: If it is not restored yet +✅ Stage 4: Automatic rollback +-Restore to previous stable state +- Distributed to Shangshu Province for reprocessing +- User can see full rollback link +``` + +### Scenario 2: Agent does evil (forged data) + +Suppose the `zhongshu` agent wants to cheat the system: + +```python +# Try to forge the correct performance of Menxiasheng (change JSON directly) +task['flow_log'].append({ + 'from': '门下省', # ❌ 假冒身份 + 'to': '尚书省', + 'remark': '✅ 准奏' +}) + +# System defense: +# 1. Permission verification: API layer checks the identity of the HTTP requester +# ├─ Requests from zhongshu agent cannot flow directly +# ├─ Must be recorded through flow_log and signature verification +# └─ Reject if the signature does not match +# 2. State machine verification: state transition is controlled +# ├─ Even if flow_log is tampered with, the state is still Zhongshu +# ├─ The next step can only be transferred by the gate-keeper system +# └─ zhongshu has no right to change the state by himself + +# Result: ❌ Agent’s forgery is intercepted by the system +``` + +### Scenario 3: Business process violation (such as Zhongshu exceeding his authority and adjusting the Shangshu's reform plan) + +```python +# Zhongshu Sheng wants to bypass the deliberation of the subordinates and directly consult the Shangshu Sheng +try: + result = dispatch_to_agent('shangshu', '请帮我审查一下这个方案') +except PermissionError: + # ❌ Permission matrix interception + log.error('zhongshu 无权调用 shangshu (仅限: menxia, shangshu)') + +# Menxia Sheng wants to upgrade to the Emperor +try: + result = dispatch_to_agent('taizi', '我需要皇上的指示') +except PermissionError: + # ❌ Permission matrix interception + log.error('menxia 无权调用 taizi') +``` + +--- + +## 📊 Part 6: Monitoring and Observability + +### Kanban’s 10 view panels + +``` +1. Full task list +└─ Summary view of all tasks (in reverse order of creation time) +└─ Quick filter: active/completed/rejected + +2. Classification by status +├─ Pending +├─ Taizi (Prince sorting) +├─ Zhongshu (under planning in Zhongshu) +├─ Menxia (under review) +├─ Assigned (distributed by Shangshu) +├─ Doing (six parts in progress) +├─ Review (Shang Shu summary) +└─ Done/Cancelled + +3. Classification by department +├─ Prince’s mission +├─ Zhongshu Provincial Mission +├─ Save tasks under the door +├─ Mission of Shangshu Province +├─ Six tasks (parallel view) +└─ Task assigned + +4. Sort by priority +├─ 🔴 Critical +├─ 🟠 High +├─ 🟡Normal +└─ 🔵 Low (low quality) + +5. Agent online status +├─ 🟢 Running (processing tasks) +├─ 🟡 On call (recently active, idle) +├─ ⚪ Idle (no activity for more than 10 minutes) +├─ 🔴 Offline (Gateway is not started) +└─ ❌ Not configured (workspace does not exist) + +6. Task details panel +├─ Basic information (title, creator, priority) +├─ Complete activity flow (flow_log + progress_log + session) +├─Stage time consumption statistics (residence time of each Agent) +├─ Todos progress bar +└─ Resource consumption (tokens/cost/elapsed) + +7. Monitoring of stalled tasks +├─ List all tasks that have exceeded the threshold and have not been advanced +├─ Show the pause duration +├─ Quick actions: Retry/Upgrade/Rollback + +8. Approval work order pool +├─ List all tasks waiting for approval in Menxia +├─ Sort by length of stay +├─ One-touch accuracy/rejection + +9. Today’s Overview +├─ Number of new tasks created today +├─Number of tasks completed today +├─ Average circulation time +├─ Activity frequency of each Agent + +10. Historical reports +├─ Weekly report (output per capita, average cycle) +├─ Monthly report (department collaboration efficiency) +└─ Cost analysis (API call cost, Agent workload) +``` + +### Real-time API: Agent online detection + +``` +GET /api/agents-status + +response: +{ +"ok": true, +"gateway": { +"alive": true, // The process exists +"probe": true, // HTTP response is normal +"status": "🟢 Running" +}, +"agents": [ +{ +"ID": "Prince", +"label": "Prince", +"status": "running", // running|idle|offline|unconfigured +"statusLabel": "🟢 Running", +"lastActive": "03-02 14:30", // Last active time +"lastActiveTs": 1708943400000, +"sessions": 42, //Number of active sessions +"hasWorkspace": true, +"processAlive": true +}, +// ... other agents ... +] +} +``` + +--- + +## 🎓 Part 7: Usage examples and best practices + +### Complete case: Create→Distribute→Execute→Complete + +```bash +# ═════════════════════════════ ══════════════════════════════ +# Step 1: The Emperor issues an edict (Feishu message or Kanban API) +# ═════════════════════════════ ══════════════════════════════ + +curl -X POST http://127.0.0.1:7891/api/create-task \ + -H "Content-Type: application/json" \ + -d '{ + "title": "编写三省六部协议文档", + "priority": "high" + }' + +# Response: JJC-20260302-001 created +# Prince Agent received the notification: "📜 The Emperor's decree..." + +# ═════════════════════════════ ══════════════════════════════ +# Step 2: The prince receives the order and sorts it (Agent automatically) +# ═════════════════════════════ ══════════════════════════════ + +#Prince Agent's judgment: This is "work intention" (not chatting) +# Automatically run: +python3 scripts/kanban_update.py state \ + JJC-20260302-001 \ + Zhongshu \ + "分拣完毕,转中书省起草" + +# Zhongshu Province Agent received the distribution notification + +# ═════════════════════════════ ══════════════════════════════ +# Step 3: Drafting the letter (Agent work) +# ═════════════════════════════ ══════════════════════════════ + +# Zhongshu Agent analyzes requirements and dismantles tasks + +# First report (after 30 minutes): +python3 scripts/kanban_update.py progress \ + JJC-20260302-001 \ + "已完成需求分析,拟定三部分文档:概述|技术栈|使用指南" \ + "1.需求分析✅|2.文档规划✅|3.内容编写🔄|4.审查待完成" + +# Kanban display: +# - Progress bar: 50% complete +# - Activity flow: add progress + todos entries +# - Consumption: 1200 tokens, $0.0012, 18 minutes + +# Second report (in another 90 minutes): +python3 scripts/kanban_update.py progress \ + JJC-20260302-001 \ + "文档初稿已完成,现提交门下省审议" \ + "1.需求分析✅|2.文档规划✅|3.内容编写✅|4.待审查" + +python3 scripts/kanban_update.py flow \ + JJC-20260302-001 \ + "中书省" \ + "门下省" \ + "提交审议" + +python3 scripts/kanban_update.py state \ + JJC-20260302-001 \ + Menxia \ + "方案提交门下省审议" + +# Men Xia Province Agent received the distribution notice and started deliberation. + +# ═════════════════════════════ ══════════════════════════════ +# Step 4: Deliberation under the sect (Agent work) +# ═════════════════════════════ ══════════════════════════════ + +# MenxiaAgent reviews document quality + +# Review results (after 30 minutes): + +# Scenario A: Accurate performance +python3 scripts/kanban_update.py state \ + JJC-20260302-001 \ + Assigned \ + "✅ 准奏,已采纳改进建议" + +python3 scripts/kanban_update.py flow \ + JJC-20260302-001 \ + "门下省" \ + "尚书省" \ + "✅ 准奏:文档质量良好,建议补充代码示例" + +# Shangshu Province Agent received the distribution + +# Scenario B: Denial +python3 scripts/kanban_update.py state \ + JJC-20260302-001 \ + Zhongshu \ + "🚫 封驳:需补充协议规范部分" + +python3 scripts/kanban_update.py flow \ + JJC-20260302-001 \ + "门下省" \ + "中书省" \ + "🚫 封驳:协议部分过于简略,需补充权限矩阵示例" + +# Zhongshu Province Agent receives the awakening and re-modifies the plan +# (After 3 hours → resubmit for review) + +# ═════════════════════════════ ══════════════════════════════ +# Step 5: Distribution by Minister (Agent work) +# ═════════════════════════════ ══════════════════════════════ + +# Shangshu Province Agent analyzes who the document should be sent to: +# - Department of Rites: Document layout and formatting +# - Ministry of War: Supplementary code examples +# - Ministry of Industry: Deployment Documents + +python3 scripts/kanban_update.py state \ + JJC-20260302-001 \ + Doing \ + "派发给礼部+兵部+工部三部并行执行" + +python3 scripts/kanban_update.py flow \ + JJC-20260302-001 \ + "尚书省" \ + "六部" \ + "派发执行:礼部排版|兵部代码示例|工部基础设施部分" + +# Six Agents received distribution respectively + +# ═════════════════════════════ ══════════════════════════════ +# Step 6: Six executions (parallel) +# ═════════════════════════════ ══════════════════════════════ + +# Etiquette Department Progress Report (20 minutes): +python3 scripts/kanban_update.py progress \ + JJC-20260302-001 \ + "已完成文档排版和目录调整,现待其他部门内容补充" \ + "1.排版✅|2.目录调整✅|3.等待代码示例|4.等待基础设施部分" + +# Ministry of War progress report (40 minutes): +python3 scripts/kanban_update.py progress \ + JJC-20260302-001 \ + "已编写5个代码示例(权限检查、派发流程、session融合等),待集成到文档" \ + "1.分析需求✅|2.编码示例✅|3.集成文档🔄|4.测试验证" + +# Ministry of Industry Progress Report (60 minutes): +python3 scripts/kanban_update.py progress \ + JJC-20260302-001 \ + "已编写Docker+K8s部署部分,Nginx配置和让证书更新文案完成" \ + "1.Docker编写✅|2.K8s配置✅|3.一键部署脚本🔄|4.部署文档待完成" + +# ═════════════════════════════ ══════════════════════════════ +# Step 7: Summary of Shangshu (Agent work) +# ═════════════════════════════ ══════════════════════════════ + +# After all departments have completed their reports, Shangshu Province will summarize all results + +python3 scripts/kanban_update.py progress \ + JJC-20260302-001 \ + "全部部门已完成。汇总成果:\n- 文档已排版,包含9个章节\n- 15个代码示例已集成\n- 完整部署指南已编写\n通过率:100%" \ + "1.排版✅|2.代码示例✅|3.基础设施✅|4.汇总✅" + +python3 scripts/kanban_update.py state \ + JJC-20260302-001 \ + Review \ + "所有部门完成,进入审查阶段" + +# The emperor/prince receives the notice and reviews the final results + +# ═════════════════════════════ ══════════════════════════════ +# Step 8: Completion (final state) +# ═════════════════════════════ ══════════════════════════════ + +python3 scripts/kanban_update.py done \ + JJC-20260302-001 \ + "https://github.com/org/repo/docs/architecture.md" \ + "三省六部协议文档已完成,包含89页,5个阶段历时3天,总消耗成本$2.34" + +# Kanban display: +# - Status: Done ✅ +# - Total time taken: 3 days, 2 hours and 45 minutes +# - Complete activity stream: 79 activity records +# - Resource statistics: 87500 tokens, $2.34, 890 minutes of total working time + +# ═════════════════════════════ ══════════════════════════════ +# Query the final results +# ═════════════════════════════ ══════════════════════════════ + +curl http://127.0.0.1:7891/api/task-activity/JJC-20260302-001 + +# Response: +# { +# "taskMeta": { +# "state": "Done", +# "output": "https://github.com/org/repo/docs/architecture.md" +# }, +# "activity": [79 complete circulation chains], +# "totalDuration": "3 days, 2 hours and 45 minutes", +# "resourceSummary": { +# "totalTokens": 87500, +# "totalCost": 2.34, +# "totalElapsedSec": 53700 +# } +# } +``` + +--- + +## 📋 Summary + +**Three Provinces and Six Ministries is an institutionalized AI multi-Agent system**, not a traditional "free discussion" framework. It passes: + +1. **Business layer**: imitate the ancient imperial bureaucracy and establish an organizational structure with decentralized checks and balances. +2. **Technical layer**: state machine + permission matrix + automatic dispatch + scheduling retry to ensure the process is controllable +3. **Observation layer**: React dashboard + complete activity flow (59 items/task), grasp the overall situation in real time +4. **Intervention layer**: One-click stop/cancel/advance, and can immediately correct any anomalies encountered + +**Core Value**: Use systems to ensure quality, use transparency to ensure confidence, and use automation to ensure efficiency. + +Compared with the "freedom + manual management" of CrewAI/AutoGen, San Province and Six Ministries provides an **enterprise-level AI collaboration framework**. diff --git a/docs/task-dispatch-architecture_EN.md b/docs/task-dispatch-architecture_EN.md new file mode 100644 index 00000000..68cb7484 --- /dev/null +++ b/docs/task-dispatch-architecture_EN.md @@ -0,0 +1,1126 @@ +# Three Departments & Six Ministries Task Dispatch & Flow System · Business and Technical Architecture + +> This document explains in detail how the “Three Departments & Six Ministries” project goes from **institutional/business design** to **code-level implementation details**, and how it handles task dispatch and lifecycle flow for complex multi-Agent collaboration. This is an **institutionalized AI multi-Agent framework**, not a traditional free-form discussion-based collaboration system. + +**Document overview diagram** + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Business Layer: Imperial Governance Model + ├─ Checks & balances: Emperor → Taizi → Zhongshu → Menxia → Shangshu → Six Ministries + ├─ Institutional constraints: no skipping levels, strict state progression, Menxia must review + └─ Quality guarantees: can veto/reject and send back, fully observable, emergency intervention +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Technical Layer: OpenClaw Multi-Agent Orchestration + ├─ State machine: 9 states (Pending → Taizi → Zhongshu → Menxia → Assigned → Doing/Next → Review → Done/Cancelled) + ├─ Data fusion: flow_log + progress_log + session JSONL → unified activity stream + ├─ Permission matrix: strict controls over subagent invocation + └─ Scheduling layer: auto-dispatch, timeout retries, stall escalation, auto-rollback +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Observability Layer: React Dashboard + Real-time API (Dashboard + Real-time Analytics) + ├─ Task Kanban: 10 view panels (all/by state/by department/by priority, etc.) + ├─ Activity stream: ~59 mixed activity records per task (thinking, tool calls, state transitions) + └─ Online status: real-time agent node detection + heartbeat wake-up mechanism +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +--- + +## 📚 Part 1: Business Architecture + +### 1.1 Imperial Institution: the design philosophy of checks and balances + +#### Core concepts + +Traditional multi-Agent frameworks (such as CrewAI, AutoGen) use a **“free collaboration”** model: +- Agents autonomously pick collaborators +- The framework only provides communication channels +- Quality control depends entirely on agent intelligence +- **Problem**: agents can easily fabricate data, duplicate work, and there is no quality guarantee for plans/solutions + +**Three Departments & Six Ministries** uses an **“institutionalized collaboration”** model, inspired by the ancient imperial bureaucracy: + +``` + Emperor + (User) + │ + ↓ + Taizi (Crown Prince) + [Triage officer, entrypoint owner] + ├─ Identify: is this an edict (command) or casual chat? + ├─ Execute: reply to chat directly || create a task → forward to Zhongshu + └─ Permission: may only call Zhongshu + │ + ↓ + Zhongshu (Secretariat) + [Planning officer, primary drafter] + ├─ Analyze requirements after receiving the edict + ├─ Decompose into subtasks (todos) + ├─ Request Menxia review OR consult Shangshu + └─ Permission: may only call Menxia + Shangshu + │ + ↓ + Menxia (Chancellery) + [Review officer, quality owner] + ├─ Review Zhongshu plan (feasibility, completeness, risks) + ├─ Approve OR Reject (with modification suggestions) + ├─ If rejected → return to Zhongshu to revise → re-review (up to 3 rounds) + └─ Permission: may only call Shangshu + call back Zhongshu + │ + (✅ Approved) + │ + ↓ + Shangshu (Department of State Affairs) + [Dispatch officer, execution commander] + ├─ Receive the approved plan + ├─ Decide which department should execute + ├─ Call the Six Ministries (Rites/Revenue/War/Justice/Works/Personnel) + ├─ Monitor each ministry’s progress → aggregate results + └─ Permission: may only call the Six Ministries (cannot overstep to call Zhongshu) + │ + ├─ Libu (Ministry of Rites) - documentation officer + ├─ Hubu (Ministry of Revenue) - data analysis officer + ├─ Bingbu (Ministry of War) - code implementation officer + ├─ Xingbu (Ministry of Justice) - testing & review officer + ├─ Gongbu (Ministry of Works) - infrastructure officer + └─ Libu_hr (Ministry of Personnel) - HR officer + │ + (Ministries execute in parallel) + ↓ + Shangshu · Aggregation + ├─ Collect results from the ministries + ├─ Transition state to Review + ├─ Call back Zhongshu to report upward to the Emperor + │ + ↓ + Zhongshu · Final report + ├─ Summarize observations, conclusions, suggestions + ├─ Transition state to Done + └─ Reply to the Emperor via Feishu +``` + +#### Four guarantees provided by the institution + +| Guarantee mechanism | Implementation detail | Protection effect | +|---|---|---| +| **Institutional review** | Menxia must review all Zhongshu plans; cannot be skipped | Prevents agents from executing arbitrarily; ensures the plan is feasible | +| **Checks & balances** | Permission matrix: who can call whom is strictly defined | Prevents abuse of power (e.g., Shangshu overstepping to Zhongshu to rewrite the plan) | +| **Full observability** | 10 dashboard panels + ~59 activities per task | See in real-time where the task is stuck, who is working, and current status | +| **Real-time intervention** | One-click stop/cancel/resume/advance inside the dashboard | Emergency correction when an agent goes off-track | + +--- + +### 1.2 Full task lifecycle flow + +#### Flow diagram + +```mermaid +stateDiagram-v2 +[*] --> Pending: Emperor issues an edict +Pending --> Taizi: Taizi receives edict +Taizi --> Zhongshu: Taizi hands to Zhongshu +Zhongshu --> Menxia: Zhongshu submits for review +Menxia --> Zhongshu: Menxia rejects (can repeat) +Menxia --> Assigned: Menxia approves +Assigned --> Doing: Shangshu dispatches execution +Doing --> Review: Ministries complete +Review --> Done: Emperor approves +Review --> Menxia: Emperor requests changes +Done --> [*] +Doing --> [*]: Manual cancellation +Review --> [*]: Business termination +``` + +#### Concrete critical paths + +**✅ Ideal path** (no blocking; completes in 4–5 days) + +``` +DAY 1: + 10:00 - Emperor on Feishu: "Write a full automated testing plan for Three Departments & Six Ministries" + Taizi receives the edict. state = Taizi, org = Taizi + Auto-dispatch taizi agent → handle this edict + + 10:30 - Taizi finishes triage. Determines it is a "work edict" (not casual chat) + Create task JJC-20260228-E2E + flow_log: "Emperor → Taizi: edict issued" + state: Taizi → Zhongshu, org: Taizi → Zhongshu + Auto-dispatch zhongshu agent + +DAY 2: + 09:00 - Zhongshu receives. Starts planning + Progress: "Analyze test requirements; decompose into unit/integration/E2E layers" + progress_log: "Zhongshu Zhang San: split requirements" + + 15:00 - Zhongshu completes the proposal + todos snapshot: requirements analysis✅, plan design✅, pending review🔄 + flow_log: "Zhongshu → Menxia: submit proposal for review" + state: Zhongshu → Menxia, org: Zhongshu → Menxia + Auto-dispatch menxia agent + +DAY 3: + 09:00 - Menxia begins review + Progress: "Now reviewing completeness and risk" + + 14:00 - Menxia completes review + Judgement: "Plan is feasible, but missing tests for _infer_agent_id_from_runtime" + Action: ✅ Approve (with modification suggestions) + flow_log: "Menxia → Shangshu: ✅ Approved (5 suggestions)" + state: Menxia → Assigned, org: Menxia → Shangshu + OPTIONAL: Zhongshu receives suggestions and proactively improves the plan + Auto-dispatch shangshu agent + +DAY 4: + 10:00 - Shangshu receives the approved plan + Analysis: "This test plan should be assigned to Gongbu + Xingbu + Libu" + flow_log: "Shangshu → Six Ministries: dispatch execution (ministry collaboration)" + state: Assigned → Doing, org: Shangshu → Bingbu+Xingbu+Libu + Auto-dispatch bingbu/xingbu/libu agents (parallel) + +DAY 4-5: + (All ministries execute in parallel) + - Bingbu: implement pytest + unittest test frameworks + - Xingbu: write tests covering all key functions + - Libu: produce documentation and test-case descriptions + + Real-time progress (hourly): + - Bingbu: "✅ Implemented 16 unit tests" + - Xingbu: "🔄 Writing integration tests (8/12 completed)" + - Libu: "Waiting for Bingbu to finish before writing report" + +DAY 5: + 14:00 - Ministries finish + state: Doing → Review, org: Bingbu → Shangshu + Shangshu aggregates: "All tests completed; pass rate 98.5%" + Handoff back to Zhongshu + + 15:00 - Zhongshu reports back to the Emperor + state: Review → Done + Template reply to Feishu, including final links and summary +``` + +**❌ Setback path** (includes rejection and retry; 6–7 days) + +``` +DAY 2 same as above + +DAY 3 [rejection scenario]: + 14:00 - Menxia completes review + Judgement: "Plan is incomplete; missing performance tests + stress tests" + Action: 🚫 Reject + review_round += 1 + flow_log: "Menxia → Zhongshu: 🚫 Rejected (must add performance testing)" + state: Menxia → Zhongshu # return to Zhongshu for revision + Auto-dispatch zhongshu agent (re-plan) + +DAY 3-4: + 16:00 - Zhongshu receives the rejection notice (wake agent) + Analyze improvement feedback; add performance testing plan + progress: "Integrated performance testing; revised plan as follows..." + flow_log: "Zhongshu → Menxia: revised plan (round 2 review)" + state: Zhongshu → Menxia + Auto-dispatch menxia agent + + 18:00 - Menxia re-reviews + Judgement: "✅ Approved this time" + flow_log: "Menxia → Shangshu: ✅ Approved (round 2)" + state: Menxia → Assigned → Doing + Subsequent steps follow the ideal path... + +DAY 7: Everything completes (1–2 days later than ideal) +``` + +--- + +### 1.3 Task specification & business contract + +#### Task Schema field description + +```json +{ + "id": "JJC-20260228-E2E", // 任务全局唯一ID (JJC-日期-序号) + "title": "为三省六部编写完整自动化测试方案", + "official": "中书令", // 负责官职 + "org": "中书省", // 当前负责部门 + "state": "Assigned", // 当前状态(见 _STATE_FLOW) + + // ──── 质量与约束 ──── + "priority": "normal", // 优先级:critical/high/normal/low + "block": "无", // 当前阻滞原因(如"等待工部反馈") + "reviewRound": 2, // 门下审议第几轮 + "_prev_state": "Menxia", // 若被 stop,记录之前状态用于 resume + + // ──── 业务产出 ──── + "output": "", // 最终任务成果(URL/文件路径/总结) + "ac": "", // Acceptance Criteria(验收标准) + "priority": "normal", + + // ──── 流转记录 ──── + "flow_log": [ + { + "at": "2026-02-28T10:00:00Z", + "from": "皇上", + "to": "太子", + "remark": "下旨:为三省六部编写完整自动化测试方案" + }, + { + "at": "2026-02-28T10:30:00Z", + "from": "太子", + "to": "中书省", + "remark": "分拣→传旨" + }, + { + "at": "2026-02-28T15:00:00Z", + "from": "中书省", + "to": "门下省", + "remark": "规划方案提交审议" + }, + { + "at": "2026-03-01T09:00:00Z", + "from": "门下省", + "to": "中书省", + "remark": "🚫 封驳:需补充性能测试" + }, + { + "at": "2026-03-01T15:00:00Z", + "from": "中书省", + "to": "门下省", + "remark": "修订方案(第2轮审议)" + }, + { + "at": "2026-03-01T20:00:00Z", + "from": "门下省", + "to": "尚书省", + "remark": "✅ 准奏通过(第2轮,5条建议已采纳)" + } + ], + + // ──── Agent 实时汇报 ──── + "progress_log": [ + { + "at": "2026-02-28T10:35:00Z", + "agent": "zhongshu", // 汇报agent + "agentLabel": "中书省", + "text": "已接旨。分析测试需求,拟定三层测试方案...", + "state": "Zhongshu", // 汇报时的状态快照 + "org": "中书省", + "tokens": 4500, // 资源消耗 + "cost": 0.0045, + "elapsed": 120, + "todos": [ // 待办任务快照 + {"id": "1", "title": "需求分析", "status": "completed"}, + {"id": "2", "title": "方案设计", "status": "in-progress"}, + {"id": "3", "title": "await审议", "status": "not-started"} + ] + } + // ... 更多 progress_log 条目 ... + ], + + // ──── 调度元数据 ──── + "_scheduler": { + "enabled": true, + "stallThresholdSec": 180, // 停滞超过180秒自动升级 + "maxRetry": 1, // 自动重试最多1次 + "retryCount": 0, + "escalationLevel": 0, // 0=无升级 1=门下协调 2=尚书协调 + "lastProgressAt": "2026-03-01T20:00:00Z", + "stallSince": null, // 何时开始停滞 + "lastDispatchStatus": "success", // queued|success|failed|timeout|error + "snapshot": { + "state": "Assigned", + "org": "尚书省", + "note": "review-before-approve" + } + }, + + // ──── 生命周期 ──── + "archived": false, // 是否归档 + "now": "门下省准奏,移交尚书省派发", // 当前实时状态描述 + "updatedAt": "2026-03-01T20:00:00Z" +} +``` + +#### Business contracts + +| Contract | Meaning | Consequence if violated | +|---|---|---| +| **No skipping levels** | Taizi may only call Zhongshu; Zhongshu may only call Menxia/Shangshu; Ministries may not call outward | Overreach calls are rejected; system intercepts automatically | +| **Unidirectional state progression** | Pending → Taizi → Zhongshu → Menxia → Assigned → Doing/Next → Review → Done, cannot skip or backtrack | Only allowed to return one step via review_action(reject) | +| **Menxia must review** | All Zhongshu plans must be reviewed by Menxia; cannot be skipped | Zhongshu cannot send directly to Shangshu; Menxia is mandatory | +| **No changes after Done** | Once Done/Cancelled, state cannot be modified | To change, create a new task or cancel + recreate | +| **task_id uniqueness** | JJC-date-seq is globally unique; no duplicates on same day | Dashboard dedup + auto de-dup | +| **Transparent resource usage** | Every progress report must include tokens/cost/elapsed | Enables cost accounting and performance optimization | + +--- + +## 🔧 Part 2: Technical Architecture + +### 2.1 State machine and automatic dispatch + +#### Complete state transition definition + +```python +_STATE_FLOW = { + 'Pending': ('Taizi', '皇上', '太子', '待处理旨意转交太子分拣'), + 'Taizi': ('Zhongshu','太子', '中书省', '太子分拣完毕,转中书省起草'), + 'Zhongshu': ('Menxia', '中书省', '门下省', '中书省方案提交门下省审议'), + 'Menxia': ('Assigned','门下省', '尚书省', '门下省准奏,转尚书省派发'), + 'Assigned': ('Doing', '尚书省', '六部', '尚书省开始派发执行'), + 'Next': ('Doing', '尚书省', '六部', '待执行任务开始执行'), + 'Doing': ('Review', '六部', '尚书省', '各部完成,进入汇总'), + 'Review': ('Done', '尚书省', '太子', '全流程完成,回奏太子转报皇上'), +} +``` + +Each state automatically maps to an Agent ID (see `_STATE_AGENT_MAP`): + +```python +_STATE_AGENT_MAP = { + 'Taizi': 'taizi', + 'Zhongshu': 'zhongshu', + 'Menxia': 'menxia', + 'Assigned': 'shangshu', + 'Doing': None, # infer from org (one of Six Ministries) + 'Next': None, # infer from org + 'Review': 'shangshu', + 'Pending': 'zhongshu', +} +``` + +#### Automatic dispatch flow + +When a task transitions state (via `handle_advance_state()` or approval), the backend automatically executes dispatch: + +``` +1. State transition triggers dispatch + ├─ Look up _STATE_AGENT_MAP to get target agent_id + ├─ If Doing/Next, infer ministry agent from task.org via _ORG_AGENT_MAP + └─ If cannot infer, skip dispatch (e.g., Done/Cancelled) + +2. Build dispatch message (to push the agent to start immediately) + ├─ taizi: "📜 Emperor's edict needs you to triage..." + ├─ zhongshu: "📜 Edict arrived at Zhongshu; please draft a plan..." + ├─ menxia: "📋 Zhongshu plan submitted for review..." + ├─ shangshu: "📮 Menxia approved; please dispatch execution..." + └─ ministries: "📌 Please handle the task..." + +3. Asynchronous dispatch (non-blocking) + ├─ spawn daemon thread + ├─ set _scheduler.lastDispatchStatus = 'queued' + ├─ verify Gateway process is running + ├─ run: openclaw agent --agent {id} -m "{msg}" --deliver --timeout 300 + ├─ retry up to 2 times (5s backoff between failures) + ├─ update _scheduler status & error information + └─ record dispatch result into flow_log + +4. Dispatch status outcomes + ├─ success: set lastDispatchStatus = 'success' + ├─ failed: record failure reason; agent timeout does not block the dashboard + ├─ timeout: mark timeout; allow manual retry/escalation + ├─ gateway-offline: Gateway not running; skip dispatch (retry later) + └─ error: record stack trace for debugging + +5. Target agent handling + ├─ agent receives notification in Feishu + ├─ interacts with dashboard using kanban_update.py (update state/log progress) + └─ upon completion, triggers dispatch to next agent/state +``` + +--- + +### 2.2 Permission matrix and subagent invocation + +#### Permission definition (configured in openclaw.json) + +```json +{ + "agents": [ + { + "id": "taizi", + "label": "太子", + "allowAgents": ["zhongshu"] + }, + { + "id": "zhongshu", + "label": "中书省", + "allowAgents": ["menxia", "shangshu"] + }, + { + "id": "menxia", + "label": "门下省", + "allowAgents": ["shangshu", "zhongshu"] + }, + { + "id": "shangshu", + "label": "尚书省", + "allowAgents": ["libu", "hubu", "bingbu", "xingbu", "gongbu", "libu_hr"] + }, + { + "id": "libu", + "label": "礼部", + "allowAgents": [] + } + // ... other ministries also have allowAgents = [] ... + ] +} +``` + +#### Permission check mechanism (code-level) + +In addition to `dispatch_for_state()`, there is a defensive permission check layer: + +```python +def can_dispatch_to(from_agent, to_agent): + """Check whether from_agent is allowed to call to_agent.""" + cfg = read_json(DATA / 'agent_config.json', {}) + agents = cfg.get('agents', []) + + from_record = next((a for a in agents if a.get('id') == from_agent), None) + if not from_record: + return False, f'{from_agent} 不存在' + + allowed = from_record.get('allowAgents', []) + if to_agent not in allowed: + return False, f'{from_agent} 无权调用 {to_agent}(允许列表:{allowed})' + + return True, 'OK' +``` + +#### Examples of permission violations and handling + +| Scenario | Request | Result | Reason | +|---|---|---|---| +| **Normal** | Zhongshu → Menxia review | ✅ Allowed | Menxia is in Zhongshu allowAgents | +| **Violation** | Zhongshu → Shangshu to "rewrite plan" | ❌ Denied | Zhongshu cannot override Shangshu work | +| **Violation** | Gongbu → Shangshu: "I'm done" | ✅ State update allowed | via flow_log/progress_log (not cross-agent call) | +| **Violation** | Shangshu → Zhongshu: "rewrite plan" | ❌ Denied | Shangshu is not allowed to call Zhongshu | +| **Defense** | Agent forges another agent’s dispatch | ❌ Intercepted | API layer validates request source/signature | + +--- + +### 2.3 Data fusion: progress_log + session JSONL + +#### Phenomenon + +During execution, there are three layers of data sources: + +``` +1️⃣ flow_log + └─ Pure state transitions (Zhongshu → Menxia) + └─ Source: task JSON field flow_log + └─ Reported by: agents via kanban_update.py flow + +2️⃣ progress_log + └─ Real-time work reports (text progress, todos snapshots, resource usage) + └─ Source: task JSON field progress_log + └─ Reported by: agents via kanban_update.py progress + └─ Frequency: typically every ~30 minutes or at key milestones + +3️⃣ session JSONL (NEW!) + └─ Internal reasoning (thinking), tool calls (tool_result), dialogue history (user) + └─ Source: ~/.openclaw/agents/{agent_id}/sessions/*.jsonl + └─ Produced by: OpenClaw automatically; agents do not need to report manually + └─ Frequency: message-level; finest granularity +``` + +#### Problem diagnosis + +Previously, using only flow_log + progress_log: +- ❌ You cannot see the agent’s concrete reasoning process +- ❌ You cannot see tool calls and their outputs +- ❌ You cannot see intermediate conversation history +- ❌ Agents appear as a “black box” + +Example: progress_log says “analyzing requirements”, but the user cannot see *what* was analyzed. + +#### Solution: fuse session JSONL + +In `get_task_activity()`, fusion logic was added (~40 lines): + +```python +def get_task_activity(task_id): + # ... previous code ... + + # ── Fuse Agent Session activity (thinking / tool_result / user) ── + session_entries = [] + + # Active tasks: attempt exact task_id matching + if state not in ('Done', 'Cancelled'): + if agent_id: + entries = get_agent_activity( + agent_id, limit=30, task_id=task_id + ) + session_entries.extend(entries) + + # Also fetch from related agents + for ra in related_agents: + if ra != agent_id: + entries = get_agent_activity( + ra, limit=20, task_id=task_id + ) + session_entries.extend(entries) + else: + # Completed tasks: keyword matching + title = task.get('title', '') + keywords = _extract_keywords(title) + if keywords: + for ra in related_agents[:5]: + entries = get_agent_activity_by_keywords( + ra, keywords, limit=15 + ) + session_entries.extend(entries) + + # De-dup (use at+kind as key) + existing_keys = {(a.get('at', ''), a.get('kind', '')) for a in activity} + for se in session_entries: + key = (se.get('at', ''), se.get('kind', '')) + if key not in existing_keys: + activity.append(se) + existing_keys.add(key) + + # Re-sort + activity.sort(key=lambda x: x.get('at', '')) + + # Mark activity source + return { + 'activity': activity, + 'activitySource': 'progress+session', + # ... other fields ... + } +``` + +#### Session JSONL format parsing + +Items extracted from JSONL are normalized into dashboard activity entries: + +```python +def _parse_activity_entry(item): + """Normalize a session jsonl message into a dashboard activity entry.""" + msg = item.get('message', {}) + role = str(msg.get('role', '')).strip().lower() + ts = item.get('timestamp', '') + + # 🧠 Assistant role - agent thinking + if role == 'assistant': + entry = { + 'at': ts, + 'kind': 'assistant', + 'text': '...main reply...', + 'thinking': '💭 Agent considered...', + 'tools': [ + {'name': 'bash', 'input_preview': 'cd /src && npm test'}, + {'name': 'file_read', 'input_preview': 'dashboard/server.py (first 100 lines)'}, + ] + } + return entry + + # 🔧 Tool Result + if role in ('toolresult', 'tool_result'): + entry = { + 'at': ts, + 'kind': 'tool_result', + 'tool': 'bash', + 'exitCode': 0, + 'output': '✓ All tests passed (123 tests)', + 'durationMs': 4500 + } + return entry + + # 👤 User + if role == 'user': + entry = { + 'at': ts, + 'kind': 'user', + 'text': 'Please implement exception handling for the test cases' + } + return entry +``` + +#### Activity stream structure after fusion + +Example (task JJC-20260228-E2E) ends up with ~59 activity entries: + +``` +kind count representative events +──────────────────────────────────────────────── +flow 10 state transition chain (Pending→Taizi→Zhongshu→...) +progress 11 agent work reports +todos 11 todo snapshots +user 1 user intervention +assistant 10 agent reasoning/thinking entries +tool_result 16 tool invocation outputs +──────────────────────────────────────────────── +Total 59 full execution trace +``` + +Dashboard benefits: +- 📋 Flow chain: understand which phase the task is in +- 📝 Progress: what the agent says in real time +- ✅ Todos: how the task is decomposed + completion % +- 💭 Assistant/thinking: how the agent reasoned +- 🔧 tool_result: tool call outputs +- 👤 user: whether a human intervened + +--- + +### 2.4 Scheduling system: timeout retries, stall escalation, auto rollback + +#### Scheduler metadata structure + +```python +_scheduler = { + # configuration + 'enabled': True, + 'stallThresholdSec': 180, + 'maxRetry': 1, + 'autoRollback': True, + + # runtime state + 'retryCount': 0, + 'escalationLevel': 0, + 'stallSince': None, + 'lastProgressAt': '2026-03-01T...', + 'lastEscalatedAt': '2026-03-01T...', + 'lastRetryAt': '2026-03-01T...', + + # dispatch tracking + 'lastDispatchStatus': 'success', # queued|success|failed| + 'lastDispatchAgent': 'zhongshu', + 'lastDispatchTrigger': 'state-transition', + 'lastDispatchError': '', + + # snapshot for rollback + 'snapshot': { + 'state': 'Assigned', + 'org': '尚书省', + 'now': '等待派发...', + 'savedAt': '2026-03-01T...', + 'note': 'scheduled-check' + } +} +``` + +#### Scheduling algorithm + +Every 60 seconds, `handle_scheduler_scan(threshold_sec=180)` runs: + +``` +FOR EACH task: + IF state in (Done, Cancelled, Blocked): + SKIP + + elapsed_since_progress = NOW - lastProgressAt + + IF elapsed_since_progress < stallThreshold: + SKIP + + # ── Stall handling ── + IF retryCount < maxRetry: + ✅ RETRY + - increment retryCount + - dispatch_for_state(task, new_state, trigger='taizi-scan-retry') + - flow_log: "stalled 180s, auto-retry #N" + - NEXT + + IF escalationLevel < 2: + ✅ ESCALATE + - nextLevel = escalationLevel + 1 + - target_agent = menxia (if L=1) else shangshu (if L=2) + - wake_agent(target_agent, "💬 task stalled; please intervene") + - flow_log: "escalated to {target_agent}" + - NEXT + + IF escalationLevel >= 2 AND autoRollback: + ✅ AUTO ROLLBACK + - restore task to snapshot.state + - retryCount = 0 + - escalationLevel = 0 + - dispatch_for_state(task, snapshot.state, trigger='taizi-auto-rollback') + - flow_log: "continuous stall; rollback to {snapshot.state}" +``` + +#### Example scenario + +Scenario: Zhongshu agent crashes; task stuck at Zhongshu + +``` +T+0: + Zhongshu planning + lastProgressAt = T + dispatch status = success + +T+30: + agent crashes + +T+60: + scheduler_scan: elapsed 60 < 180, skip + +T+180: + scheduler_scan: elapsed 180 >= 180 + + ✅ Stage 1: retry + - retryCount 0 → 1 + - dispatch_for_state(..., 'Zhongshu', trigger='taizi-scan-retry') + - flow_log records auto-retry + +T+240: + agent recovers; reports progress; lastProgressAt updates + retryCount resets to 0 + +T+360 (if still not recovered): + ✅ Stage 2: escalate to Menxia + +T+540 (if still not resolved): + ✅ Stage 3: escalate to Shangshu + +T+720 (if still not resolved): + ✅ Stage 4: auto rollback to snapshot.state +``` + +--- + +## 🎯 Part 3: Core APIs and CLI tools + +### 3.1 Task operation API endpoints + +#### Create task: `POST /api/create-task` + +``` +Request: +{ + "title": "为三省六部编写完整自动化测试方案", + "org": "中书省", + "official": "中书令", + "priority": "normal", + "template_id": "test_plan", + "params": {}, + "target_dept": "兵部+刑部" +} + +Response: +{ + "ok": true, + "taskId": "JJC-20260228-001", + "message": "旨意 JJC-20260228-001 已下达,正在派发给太子" +} +``` + +#### Task activity stream: `GET /api/task-activity/{task_id}` + +``` +Request: +GET /api/task-activity/JJC-20260228-E2E + +Response: +{ + "ok": true, + "taskId": "JJC-20260228-E2E", + "taskMeta": { + "title": "为三省六部编写完整自动化测试方案", + "state": "Assigned", + "org": "尚书省", + "output": "", + "block": "无", + "priority": "normal" + }, + "agentId": "shangshu", + "agentLabel": "尚书省", + "activity": [ + { + "at": "2026-02-28T10:00:00Z", + "kind": "flow", + "from": "皇上", + "to": "太子", + "remark": "下旨:为三省六部编写完整自动化测试方案" + } + ], + "activitySource": "progress+session" +} +``` + +#### Advance state: `POST /api/advance-state/{task_id}` + +``` +Request: +{ + "comment": "This task should advance" +} + +Response: +{ + "ok": true, + "message": "JJC-20260228-E2E advanced to next stage (agent auto-dispatched)", + "oldState": "Zhongshu", + "newState": "Menxia", + "targetAgent": "menxia" +} +``` + +#### Review action: `POST /api/review-action/{task_id}` + +``` +Request (approve): +{ + "action": "approve", + "comment": "Feasible plan; improvements adopted" +} + +OR request (reject): +{ + "action": "reject", + "comment": "Must add performance testing; round N" +} + +Response: +{ + "ok": true, + "message": "JJC-20260228-E2E approved (agent auto-dispatched)", + "state": "Assigned", + "reviewRound": 1 +} +``` + +--- + +### 3.2 CLI tool: `kanban_update.py` + +Agents use this tool to interact with the dashboard. There are 7 commands. + +#### Command 1: create task (manual by Taizi/Zhongshu) + +```bash +python3 scripts/kanban_update.py create \ + JJC-20260228-E2E \ + "为三省六部编写完整自动化测试方案" \ + Zhongshu \ + 中书省 \ + 中书令 + +# Usually not needed manually (dashboard API triggers it), except for debugging. +``` + +#### Command 2: update state + +```bash +python3 scripts/kanban_update.py state \ + JJC-20260228-E2E \ + Menxia \ + "方案提交门下省审议" +``` + +#### Command 3: add flow log record (without changing state) + +```bash +python3 scripts/kanban_update.py flow \ + JJC-20260228-E2E \ + "中书省" \ + "门下省" \ + "📋 方案提交审核,请审议" +``` + +#### Command 4: real-time progress report (key) + +```bash +python3 scripts/kanban_update.py progress \ + JJC-20260228-E2E \ + "已完成需求分析和方案初稿,现正征询工部意见" \ + "1.需求分析✅|2.方案设计✅|3.工部咨询🔄|4.待门下审议" +``` + +#### Command 5: mark done + +```bash +python3 scripts/kanban_update.py done \ + JJC-20260228-E2E \ + "https://github.com/org/repo/tree/feature/auto-test" \ + "自动化测试方案已完成,涵盖单元/集成/E2E三层,通过率98.5%" +``` + +#### Commands 6 & 7: stop/cancel + resume + +```bash +python3 scripts/kanban_update.py stop \ + JJC-20260228-E2E \ + "等待工部反馈继续" + +python3 scripts/kanban_update.py resume \ + JJC-20260228-E2E \ + "工部已反馈,继续执行" + +python3 scripts/kanban_update.py cancel \ + JJC-20260228-E2E \ + "业务需求变更,任务作废" +``` + +--- + +## 💡 Part 4: Benchmark and comparison + +### CrewAI / AutoGen (traditional) vs institutionalized approach + +| Dimension | CrewAI | AutoGen | **Three Departments & Six Ministries** | +|---|---|---|---| +| Collaboration model | free discussion | panel + callbacks (human-in-loop) | **institutionalized (permission matrix + state machine)** | +| Quality | depends on agents | human review interrupts often | **automatic review (Menxia must review) + intervention** | +| Permissions | ❌ none | ⚠️ hard-coded | **✅ configurable matrix** | +| Observability | low | medium | **very high (~59 activities/task)** | +| Intervention | ❌ hard | ✅ with approvals | **✅ 1-click stop/cancel/advance** | +| Dispatch | uncertain | human manual | **automatic (matrix + state machine)** | +| Throughput | serial-ish | needs human management | **parallel (ministries execute concurrently)** | +| Recovery | restart | manual debug | **✅ retries + escalation + rollback** | +| Cost control | opaque | medium | **transparent (progress reports include cost)** | + +### Strictness of business contracts + +**CrewAI “soft” approach** + +```python +# Agent chooses next step freely +if task_seems_done: + send_message_to_someone() # may pick wrong agent, may duplicate +``` + +**Institutional “strict” approach** + +```python +# State machine controls next step +if task.state == 'Zhongshu' and agent_id == 'zhongshu': + deliver_plan_to_menxia() + + # Attempt to skip Menxia review + try: + dispatch_to(shangshu) # ❌ permission check blocks + except PermissionError: + log.error('zhongshu cannot overstep to shangshu') +``` + +--- + +## 🔍 Part 5: Failure scenarios and recovery mechanisms + +### Scenario 1: agent process crash + +``` +Symptom: task stuck in a state; no progress for 180 seconds +Alert: Taizi scheduler detects stall + +Auto-handling: + T+0: crash + T+180: scan detects stall + ✅ Stage 1: auto retry + ✅ Stage 2: escalate to Menxia + ✅ Stage 3: escalate to Shangshu + ✅ Stage 4: auto rollback +``` + +### Scenario 2: malicious agent (forging data) + +```python +# Fake Menxia approval by directly editing JSON +task['flow_log'].append({ + 'from': '门下省', + 'to': '尚书省', + 'remark': '✅ 准奏' +}) + +# Defenses: +# 1) API layer validates request identity/signature +# 2) State machine remains controlled: even if flow_log is tampered, task.state is unchanged +``` + +### Scenario 3: business violation (overstepping) + +```python +# Zhongshu tries to bypass Menxia and consult Shangshu +try: + dispatch_to_agent('shangshu', 'Please review this plan') +except PermissionError: + log.error('zhongshu cannot call shangshu') + +# Menxia tries to call Taizi +try: + dispatch_to_agent('taizi', 'Need Emperor guidance') +except PermissionError: + log.error('menxia cannot call taizi') +``` + +--- + +## 📊 Part 6: Monitoring and observability + +### 10 dashboard view panels + +``` +1. All tasks list +2. By state +3. By department +4. By priority +5. Agent online status +6. Task detail panel (full activity stream) +7. Stalled task monitor +8. Review queue +9. Today overview +10. Historical reports +``` + +### Real-time API: agent status detection + +``` +GET /api/agents-status + +Response: +{ + "ok": true, + "gateway": { + "alive": true, + "probe": true, + "status": "🟢 running" + }, + "agents": [ + { + "id": "taizi", + "label": "太子", + "status": "running", + "statusLabel": "🟢 running", + "lastActive": "03-02 14:30", + "lastActiveTs": 1708943400000, + "sessions": 42, + "hasWorkspace": true, + "processAlive": true + } + ] +} +``` + +--- + +## 🎓 Part 7: Usage examples and best practices + +### End-to-end example: create → dispatch → execute → complete + +```bash +# Create task via API +curl -X POST http://127.0.0.1:7891/api/create-task \ + -H "Content-Type: application/json" \ + -d '{ + "title": "编写三省六部协议文档", + "priority": "high" + }' + +# ... then agent-driven kanban_update.py usage as shown ... +``` + +--- + +## 📋 Summary + +**Three Departments & Six Ministries is an institutionalized AI multi-Agent system**, not a traditional “free discussion” framework. It provides: + +1. **Business layer**: a bureaucracy-inspired org structure with checks and balances +2. **Technical layer**: state machine + permission matrix + auto-dispatch + scheduler retries to keep workflow controlled +3. **Observability layer**: React dashboard + rich activity streams (≈59 entries/task) +4. **Intervention layer**: one-click stop/cancel/advance to correct issues fast + +**Core value**: ensure quality through institution, ensure confidence through transparency, and ensure efficiency through automation. + +Compared to CrewAI/AutoGen’s “free + human management”, this is an **enterprise-grade AI collaboration framework**. diff --git a/docs/wechat-article_EN-googletrans.md b/docs/wechat-article_EN-googletrans.md new file mode 100644 index 00000000..bb54f6aa --- /dev/null +++ b/docs/wechat-article_EN-googletrans.md @@ -0,0 +1,302 @@ +# I redesigned the AI ​​multi-agent collaboration architecture using the three provinces and six departments system + +> The system design from 1,300 years ago understood decentralization better than the modern AI framework. + +![Cover: Full view of the military aircraft department’s kanban board](screenshots/01-kanban-main.png) + +--- + +## 1. A strange idea + +At the end of last year, I started using AI Agent heavily for work - writing code, doing analysis, and generating documents. It uses some of the most popular multi-agent frameworks on the market. + +After using it for a month, I discovered a fundamental problem: + +**These frameworks do not have the concept of "audit". ** + +The model of CrewAI is: several Agents work on their own, and then hand over the work after completion. AutoGen is better, it has Human-in-the-loop, but essentially you are your own QA. MetaGPT has roles, but review is optional. + +Just like a company without a QA department, engineers write the code and deploy it directly online. + +Then you get the final result, and you don’t know what happened in the middle. You can’t reproduce it, audit it, or intervene. If something goes wrong, you can only run again. + +I have been wondering: Is there a structure that naturally embeds auditing into the process, not as an optional plug-in, but as a level that must be passed? + +Then one day, when I was reading "Zi Zhi Tong Jian", I suddenly thought - + +**Three provinces and six ministries. ** + +Emperor Taizong of the Tang Dynasty designed this system 1,300 years ago: Zhongshu Province drafts decrees, Menxia Province reviews and approves them, and Shangshu Province executes them. The three departments check and balance each other, and any decree must be reviewed before being issued. + +Isn't this the architecture I'm looking for? + +![Court Ceremony: Easter Egg Animation Opened for the First Time Every Day](screenshots/11-ceremony.png) +*▲ When you open the billboard for the first time every day, there will be an opening animation of "Going to court" - full of sense of ritual* + +--- + +## 2. Architectural design of the ancients + +The system of three provinces and six ministries is not a metaphor, it is a system of decentralized checks and balances that has been tested in practice for 1,400 years. + +To simplify, the information flow looks like this: + +``` +Your Majesty (you) +↓ Decree +Zhongshu Sheng (Planning) ← Break your sentence into executable subtasks +↓ Submit for review +Menxia Province (Deliberation) ← Review the quality of the plan, and reject it if it is not good +↓ Accurate performance +Shangshu Province (distribution) ← assigned to six ministries for execution +↓ +Six Departments (Execution) ← The Ministry of Accounts is in charge of data, the Ministry of Rites is in charge of documents, the Ministry of War is in charge of development, the Ministry of Punishment is in charge of compliance, and the Ministry of Industry is in charge of infrastructure. +↓ +Summary of responses from Shangshu Province ← The results will be reported to you +``` + +Pay attention to the most critical step here: **Provincial review**. + +After the Zhongshu Ministry plans the plan, it is not thrown directly to the executive level - it must first be reviewed by the subordinate ministry. Provincial inspection under the door: + +- Is the splitting of subtasks reasonable? Are there any missing requirements? +- Are department assignments accurate? Was the one assigned to the Military Department mistakenly assigned to the Ministry of Rites? +- Is the plan executable? Is there anything unrealistic? + +If it is unqualified, the Ministry of Education can reject it - directly reject it and ask Zhongshu Province to make a new plan. It's not a warning, it's a forced rework. + +This is why the Tang Dynasty functioned for 289 years. **Unrestrained power is bound to make mistakes**, Tang Taizong thought clearly. + +--- + +## 3. I made it an open source project + +I used OpenClaw to build a real system with three provinces and six departments. The 9 AI Agents each perform their own duties and communicate strictly in accordance with the permission matrix. + +The project is called **Edict (three provinces and six departments)** and has been open source: + +**GitHub: https://github.com/cft0808/edict** + +The core architecture is simple: + +- **Zhongshu Sheng**: receive orders (your instructions), plan plans, and dismantle subtasks +- **Menxiasheng**: Review the plan, check the quality, and reject it directly if it is not qualified. +- **Shang Shu Sheng**: After the report is accurate, it will be distributed to the six departments to coordinate the implementation and summarize the results. +- **Six departments**: Ministry of Finance (data analysis), Ministry of Rites (document writing), Ministry of War (code development), Ministry of Justice (security and compliance), Ministry of Industry (CI/CD deployment) +- **Morning Official**: Send you a news briefing every day + +Each Agent has independent Workspace, independent Skills, and independent LLM model. Strict permission matrix - who can send messages to whom, written in black and white: + +| Who ↓ To whom → | Zhongshu | Menxia | Shangshu | Liubu | +|:---:|:---:|:---:|:---:|:---:| +| **中书省** | — | ✅ | ✅ | ❌ | +| **MenxiaSheng** | ✅ | — | ✅ | ❌ | +| **Shang Shu Sheng** | ✅ | ✅ | — | ✅ | +| **六片** | ❌ | ❌ | ✅ | ❌ | + +The Zhongshu Provincial Department cannot directly command the Sixth Department, and the Sixth Department cannot report to the Zhongshu Provincial Department directly. All cross-layer communications must be relayed through Shangshu Province. + +**This is not a decorative setting, it is a mandatory constraint at the architectural level. ** + +![Demo: Watch the complete flow in 30 seconds](demo.gif) +*▲ 30-second Demo: a complete tour from the court ceremony to the decree board, memorial archives, and model configuration* + +--- + +## 4. Comparison with existing frameworks + +You may ask: Compared with CrewAI and AutoGen, what is the difference? + +| | CrewAI | AutoGen | **Three provinces and six departments** | +|---|:---:|:---:|:---:| +| Audit mechanism | ❌ | ⚠️ Optional | ✅ Mandatory audit in Menha Province | +| Live dashboard | ❌ | ❌ | ✅ 10 panels | +| Task Intervention | ❌ | ❌ | ✅ Stop/Cancel/Resume | +| Transfer audit | ⚠️ | ❌ | ✅ Complete memorial archive | +| Agent Health Monitoring | ❌ | ❌ | ✅ Heartbeat Detection | +| Hot Switch LLM | ❌ | ❌ | ✅ One-click switching within the board | + +The core difference is **the provincial review mechanism**. + +This is not Human-in-the-loop (that’s asking yourself to be the QA), this is a dedicated AI Agent responsible for reviewing the output of another AI Agent. Institutional, mandatory, architectural level. + +An AI collaboration system without review is like a team without code review - it runs fast and overturns quickly. + +--- + +## 5. Bulletin Board at the Military Aircraft Department - Make everything observable + +It’s not enough to have a structure, you also have to be able to see it. + +So I made a **Military Aircraft Department Dashboard** - a web panel that monitors the flow of all tasks in real time. Zero dependencies, single file HTML, Python standard library backend, just open the browser and use it. + +10 function panels: + +**📋 Purpose Board**: All tasks are displayed in the form of cards, sorted by status, and filter search is supported. Each card has a heartbeat badge - 🟢 Active, 🟡 Stalled, 🔴 Alarm. Click to see the complete circulation timeline, and you can stop or cancel at any time. + +![Kanban](screenshots/01-kanban-main.png) +*▲ Purpose board: task cards are sorted by status, and heartbeat badges are clear at a glance* + +**🔭 Provincial and Department Scheduling**: Visualize the number of tasks, department distribution, and Agent health cards in each status. See at a glance who is busy, who is idle, and who is down. + +![Provincial dispatch](screenshots/02-monitor.png) +*▲ Provincial dispatch: status distribution + department load + Agent health card* + +**📜 Memorial Pavilion**: All completed edicts are automatically archived as "Memorials", showing a complete five-stage timeline - Imperial edict → Zhongshu planning → Subordinate review → Six-part execution → Echo. Copy to Markdown with one click. + +![Memorial Archive](screenshots/08-memorials.png) +*▲ Memorial Pavilion: Complete five-stage timeline, one-click export to Markdown* + +**📜 Edict Library**: 9 preset edict templates. Choose one, fill in the parameters, preview, and make an order with one click. Covers: common scenarios such as weekly report generation, code review, API design, competitive product analysis, etc. + +![Imperial edict template library](screenshots/09-templates.png) +*▲Purpose library: 9 preset templates, fill in the parameters and make a decree with one click* + +**⚙️ Model configuration**: Each Agent can switch LLM models independently. The Ministry of Education uses Claude for planning, the Ministry of War uses GPT-4o to write code, and the Ministry of Household Affairs uses DeepSeek to calculate data - each draws on its strengths. + +![Model configuration](screenshots/04-model-config.png) +*▲ Model configuration: Each Agent switches LLM independently, each taking advantage of their own strengths* + +There are also official overview (Token consumption ranking list), skill management, world news (automatic news aggregation), session monitoring, and court ceremony (the Easter egg animation that is opened for the first time every day). + +**All zero dependencies**, no React or Vue, pure HTML + CSS + JavaScript, completed in 2200 lines. + +![Official Overview](screenshots/06-official-overview.png) +*▲ Official Overview: Token Consumption Ranking + Activity Statistics* + +![World News](screenshots/10-morning-briefing.png) +*▲World News: Daily automatic aggregation of technology/financial information* + +--- + +## 6. Let me show you a real case + +Just talking without practicing is not enough. Let’s take a look at a real operating record - let three provinces and six departments analyze competing products. + +**Purpose**: Analyze the differences between the three frameworks CrewAI, AutoGen and LangGraph and output a comparison report. + +![Task flow details](screenshots/03-task-detail.png) +*▲ Click on any task card to see the complete flow chain and real-time status* + +### Zhongshu Provincial Planning (45 seconds) + +After receiving the order, Zhongshu Province split it into four sub-tasks: +1. Ministry of War → Research on architecture and communication mechanism +2. Account Department → Data collection and quantitative comparison (GitHub Stars, Contributors, etc.) +3. Ministry of War → In-depth evaluation of developer experience +4. Department of Etiquette → Summarize and write a comparison report + +### Menxia Provincial Review (32 seconds) - rejected! + +**The first round of Menxia Province was directly returned:** + +> *"The plan has three problems: 1) The purpose clearly requires the evaluation of 'observability', but there is no corresponding sub-task in the plan; 2) Sub-tasks 1 and 3 are both Ministry of War research, there is overlap, and it is recommended to merge; 3) There is a lack of conclusive sub-task of recommended scenarios - analysis without conclusion means no analysis. Rejected."* + +After the Zhongshu Province revised the plan, the second round of the Menxia Province was accurate. + +**This is the value of door-to-door savings. ** Without this step, the Ministry of War will conduct two surveys, and there will be no recommended scenarios in the final report - because there is no requirement in the original plan. + +### Execution of each department (17 minutes) + +- **Ministry of War**: In-depth technical comparison, covering the three dimensions of architecture, communication, and observability +- **Household Department**: Quantitative data table - Stars, Contributors, Issue response time, Hello World construction time +- **Ministry of Rites**: Integrate data from the Ministry of War + Ministry of Household Affairs and write the final report + +### Echo + +22 minutes, 15800 Tokens, a structured comparison report. The conclusion is very interesting: + +| Scenario | Recommendation | Reason | +|------|------|------| +| Rapid Prototyping | CrewAI | The fastest to get started | +| Conversational collaboration | AutoGen | Naturally suitable for multi-round discussions | +| Complex workflow | LangGraph | State machine is the most flexible | +| **Reliability first** | **Three provinces and six departments** | The only built-in mandatory audit | + +--- + +## 7. Some technical choices + +When working on this project, I made several deliberate technical decisions: + +**1. Zero dependencies** + +The Kanban front-end is an HTML file, 2200 lines long, without any framework. The backend is `http.server` of the Python standard library, without Flask or FastAPI. + +Why? Because I don't want people to `pip install` a bunch of things before running. The target users of this project may just want to quickly experience the circulation effect of three provinces and six movies, and do not want to set up the environment. + +**2. One SOUL.md for each Agent** + +Each Agent's personality, responsibilities, and workflow rules are written in a Markdown file. Want to modify the review standards of Phnom Penh Province? Edit `agents/menxia/SOUL.md` and it will automatically take effect next time you start it. + +This means that you can customize your own three provinces and six departments - maybe your "Ministry of War" is not responsible for engineering, but for market analysis. Just change SOUL.md. + +**3. Permission matrix is ​​mandatory** + +It is not "recommended" not to communicate across levels between agents, it is a mandatory restriction at the architectural level. The Sixth Ministry cannot send messages to the Zhongshu Province, and the Zhongshu Province cannot bypass the Menxia Province and directly ask the Shangshu Province to implement it. OpenClaw's configuration file says in black and white who can talk to whom. + +--- + +## 8. Now you can try + +The project is open source and licensed under the MIT license. + +**GitHub: https://github.com/cft0808/edict** + +The fastest way to experience: + +```bash +# Docker starts with one line +docker run -p 7891:7891 cft0808/edict + +# Open browser +open http://localhost:7891 +``` + +If you have OpenClaw installed, you can install it completely: + +```bash +git clone https://github.com/cft0808/edict.git +cd edict +chmod +x install.sh && ./install.sh +``` + +The installation script automatically creates 9 Agent Workspaces, writes personality files, registers the permission matrix, and restarts the Gateway. + +![Skills configuration](screenshots/05-skills-config.png) +*▲Skills management: List of installed Skills in each province and department, you can view details and add new skills* + +--- + +## 9. Next step + +Phase 1 (core architecture) has been completed. Next few things to do: + +- **Imperial Approval Mode**: The review results of Menxiasheng can be pushed to your Feishu/Telegram, and you can decide whether to approve or reject it yourself. +- **Accomplishment and Failure Book**: Each Agent's performance score - completion rate, rework rate, time-consuming statistics +- **Express Delivery Shop**: Add a real-time Agent communication flow diagram to the bulletin board - when Zhongshu Province sends a message to Menxia Province, the connection will light up +- **National History Museum**: historical decrees and memorials are accumulated into a knowledge base, and new decrees can refer to historical experience + +The complete Roadmap is on GitHub. Each sub-item of Phase 2 and Phase 3 is marked with difficulty. You are welcome to claim it. + +--- + +## at last + +The core issue of AI Agent collaboration is not "making the Agent smarter", but "making the Agent's collaboration have rules." + +CrewAI solves the problem of "multiple Agents working together". AutoGen solves the problem of "agents being able to talk to each other". + +But who will solve the problem of "Agent's output quality is guaranteed"? + +Emperor Taizong of the Tang Dynasty gave the answer 1,300 years ago: **separation of powers, checks and balances**. What is planned is not reviewed, what is reviewed is not implemented, and what is implemented is not planned. Someone is watching every step, and every decision must be reviewed. + +This is probably the most elegant "AI governance" solution I've ever seen - because it wasn't designed for AI at all. + +It is designed for governance itself. + +--- + +**GitHub: https://github.com/cft0808/edict** + +Open Source · MIT · Welcome Star ⚔️ diff --git a/docs/wechat_EN-googletrans.md b/docs/wechat_EN-googletrans.md new file mode 100644 index 00000000..6855217d --- /dev/null +++ b/docs/wechat_EN-googletrans.md @@ -0,0 +1,77 @@ +# 📮 My Dibao——Public account "cft0808" + +> *In ancient times, there were official reports to spread government decrees, but now there are public accounts talking about AI architecture. * + +

+Public account QR code · cft0808 +

+👆 Scan the QR code on WeChat and enter North Korea immediately +

+ +--- + +## 🤔 What can you see if you follow it? + +### 🏛️ Architecture dismantling series + +Three provinces and six ministries are not metaphors, they are real decentralization and checks and balances. In the official account you can see: + +- **Why the Menxia Province is the trump card** - and how many times it has blocked the plans of the Zhongshu Province +- **Why should the prince be a sorter** ——The debate between ancient and modern Intent Detection +- **Why don't you fight if six parallel works** - Concurrency wisdom from the Tang Dynasty to goroutine +- **SOUL.md How to write character design**——Alchemy prompt for AI to enter the drama + +### 🔥 Review of the pitfalls + +Collaborate with 12 Agents, and there will be surprises (frights) every day: + +- ✍️ *Agent A sends a message to Agent B, and B says "I don't know you"* +- 🔥 *Token burned $50, but Menxia Province rejected the plan three times* +- 🐛 *The Ministry of Secretariat assigned tasks, and all six departments went on strike - it turns out that the agent ID was written wrong* +- 💀 *install.sh overwrites the user-defined lobster officer role...* + +These are real Issues, and the article reviews how to fix them one by one. + +### 💡 Token money saving technique + +The question that everyone is most concerned about: **Can this token not be burned through? ** + +The public account will share: +- Use Claude's built-in cache to cut off 80% of repeated reads of SOUL.md +- "Quick Pass" mechanism: simple tasks skip the provincial review +- Start Agent on demand, no polling and no money burning +- Streamlined SOUL.md: Create a high-quality character within 1,000 words + +### 🎭 Easter Eggs & Behind the Scenes + +- How were the characters of SOUL.md in Part 6 designed? +- Will the "Jinyiwei Supervise Hundreds of Officials" function be implemented? (Yes) +- Why is it called "Three Provinces and Six Ministries" instead of "Six Doors"? +- How do I (the author) compete with 12 AIs in my daily life? + +--- + +## 📝 Published articles + +| Date | Title | Highlights | +|------|------|------| +| 2026.03 | 三省六部 · 250 Star 里程碑复盘 | 5 个 Issue + 20 条评论逐一回应,Token 优化方案 | + +> Continuously updating, welcome to update ⚔️ + +--- + +## 💬 You can also use the official account + +- 🗣️ **Ask questions** - Installation pitfalls, architecture doubts, Token anxiety, all questions will be answered +- 🐛 **Report a Bug** —— Don’t want to open an issue? You can also leave a message directly, I will read it +- 💡 **Suggestion** - "Can you add a harem system?" Yes, but queue up first +- 🤝 **Make Friends** —— Let’s study together how to make AI go to court more efficiently + +--- + +

+⚔️ I asked the AI ​​to go to court, but the result was that the AI ​​was worse than me
+Follow the official account and see how I control the 12 AI ministers

+Scan the QR code to follow +

diff --git a/edict_agent_architecture_EN-googletrans.md b/edict_agent_architecture_EN-googletrans.md new file mode 100644 index 00000000..0f9075d3 --- /dev/null +++ b/edict_agent_architecture_EN-googletrans.md @@ -0,0 +1,188 @@ +# Edict Agent architecture redesign document + +## 1. Design goals +- **Observability**: Dashboard can display each agent’s thoughts and todo changes in real time. +- **Replayable & Auditable**: All events and state changes are persistent and can be traced back. +- **Controllable process**: retains the logic of three provinces and six parts, is event-driven, and supports manual intervention. +- **Real-time and scalable**: low-latency interaction, supports horizontal expansion. +- **Structured tasks and pluggable skills**: Todos and thoughts are structured to facilitate UI rendering and reuse. + +## 2. Overall components +1. **API Gateway/Control Plane** (REST + WebSocket) +2. **Orchestrator (scheduling core)** +3. **Event Bus/Stream Layer** (Redis Streams/NATS/Kafka) +4. **Agent Runtime Pool** +5. **Model/LLM Pool** +6. **Task Store/Audit DB** (Postgres + JSONB) +7. **Realtime Dashboard** (WebSocket client) +8. **Observability/Tracing** (Prometheus + Grafana + OpenTelemetry) + +## 3. Communication mode +- **Event-Driven**: All inter-agent communication is through Event Bus +- **Topic Examples**: `task.created`, `task.planning`, `task.review.request`, `task.review.result`, `task.dispatch`, `agent.thoughts`, `agent.todo.update`, `task.status`, `heartbeat` +- **Event Structure**: +```json +{ + "event_id": "uuid", + "trace_id": "task-uuid", + "timestamp": "2026-03-01T12:00:00Z", + "topic": "agent.thoughts", + "event_type": "thought.append", + "producer": "planning-agent:v1", + "payload": { ... }, + "meta": { "priority": "normal", "model": "gpt-5-thinking", "version": "1" } +} +``` + +## 4. Thoughts and Todo JSON Schema +**Thought**: +```json +{ + "thought_id": "uuid", + "trace_id": "task-uuid", + "agent": "planning", + "step": 3, + "type": "reasoning|query|action_intent|summary", + "source": "llm|tool|human", + "content": "text", + "tokens": 123, + "confidence": 0.86, + "sensitive": false, + "timestamp": "2026-03-01T12:00:01Z" +} +``` +**Todo**: +```json +{ + "todo_id": "uuid", + "trace_id": "task-uuid", + "parent_id": null, + "title": "Verify data source X", + "description": "拉取 X 表的最近 30 天记录,检查缺失值", + "owner": "exec-dpt-1", + "assignee_agent": "data-agent", + "status": "open", + "priority": "high", + "estimated_cost": 0.5, + "created_by": "planner", + "created_at": "2026-03-01T12:01:00Z", + "checkpoints": [ {"name":"fetch","status":"done"}, {"name":"validate","status":"pending"} ], + "metadata": { "requires_human_approval": true } +} +``` + +## 5. Sequence diagram (Mermaid) +```mermaid +sequenceDiagram + participant U as User + participant D as Dashboard + participant G as Gateway + participant E as Event Bus + participant O as Orchestrator + participant P as Planning Agent + participant R as Review Agent + participant X as Executor Agent + participant M as Model Pool + + U->>D: Create Task + D->>G: POST /tasks + G->>E: publish task.created + E->>O: task.created + O->>E: publish task.planning.request + E->>P: task.planning.request + P->>M: LLM streaming call + M-->>P: token stream + loop streaming thoughts + P->>E: agent.thought.append + E->>G: forward to subscribers + G->>D: WS push thought chunk + end + P->>E: task.planning.complete + E->>O: planning.complete + O->>E: task.review.request + E->>R: review.request + R->>E: task.review.result + alt accepted + O->>E: task.dispatch + E->>X: dispatch subtasks + else rejected + O->>E: task.replan + end + X->>M: execution LLM/tool + loop execution progress + X->>E: agent.todo.update + E->>G: forward + G->>D: WS update Kanban + end + X->>E: task.completed + E->>O: complete + O->>E: task.closed +``` + +## 6. WebSocket Subscription and Message Example +**Subscribe to news**: +```json +{ + "type": "subscribe", + "channels": ["task:task-123", "agent:planning-agent", "global"] +} +``` +**Thought append (partial)**: +```json +{ + "event": "agent.thought.append", + "data": { + "thought_id": "th-1", + "step": 3, + "partial": true, + "type": "reasoning", + "content": "We should split the task into...", + "tokens": 15 + } +} +``` +**Todo Update**: +```json +{ + "event": "agent.todo.update", + "data": { + "todo_id": "todo-1", + "status": "in_progress", + "progress": 0.45 + } +} +``` + +## 7. Manual intervention example +```json +{ + "type": "command", + "action": "pause_task", + "trace_id": "task-123" +} +``` +Release event: +```json +{ + "event": "task.status", + "data": {"status": "paused", "reason": "User intervention"} +} +``` + +## 8. Replay / playback +- Request: `GET /tasks/task-123/events` +- Returns an event array that can be played back one by one on the Dashboard timeline + +## 9. Technology stack recommendations +| Layers | Technology | +|----|------| +| Event Bus | Redis Streams | +| API | FastAPI | +| WS | FastAPI WebSocket | +| DB | Postgres | +| Agent Runtime | Python asyncio worker | +| Frontend | React + Zustand | + +--- +**Remarks**: This document is an architectural design that can be directly downloaded for reference, including event specifications, WebSocket protocols, sequence diagrams and JSON Schema, which can be used to implement real-time agent observable systems. + diff --git a/repomix-output.xml b/repomix-output.xml new file mode 100644 index 00000000..f87c0ebb --- /dev/null +++ b/repomix-output.xml @@ -0,0 +1,27377 @@ +This file is a merged representation of the entire codebase, combined into a single document by Repomix. + + +This section contains a summary of this file. + + +This file contains a packed representation of the entire repository's contents. +It is designed to be easily consumable by AI systems for analysis, code review, +or other automated processes. + + + +The content is organized as follows: +1. This summary section +2. Repository information +3. Directory structure +4. Repository files (if enabled) +5. Multiple file entries, each consisting of: + - File path as an attribute + - Full contents of the file + + + +- This file should be treated as read-only. Any changes should be made to the + original repository files, not this packed version. +- When processing this file, use the file path to distinguish + between different files in the repository. +- Be aware that this file may contain sensitive information. Handle it with + the same level of security as you would the original repository. + + + +- Some files may have been excluded based on .gitignore rules and Repomix's configuration +- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files +- Files matching patterns in .gitignore are excluded +- Files matching default ignore patterns are excluded +- Files are sorted by Git change count (files with more changes are at the bottom) + + + + + +.github/ + ISSUE_TEMPLATE/ + bug_report.md + feature_request.md + workflows/ + ci.yml + pull_request_template.md +.vite/ + deps/ + _metadata.json + package.json +agents/ + bingbu/ + SOUL.md + gongbu/ + SOUL.md + hubu/ + SOUL.md + libu/ + SOUL.md + libu_hr/ + SOUL.md + menxia/ + SOUL.md + shangshu/ + SOUL.md + taizi/ + SOUL.md + xingbu/ + SOUL.md + zaochao/ + SOUL.md + zhongshu/ + SOUL.md +dashboard/ + dist/ + assets/ + index-CIswLNpP.css + index-CqMbmm5B.js + index.html + dashboard.html + server.py +docker/ + demo_data/ + agent_config.json + last_model_change_result.json + live_status.json + model_change_log.json + morning_brief.json + officials_stats.json + pending_model_changes.json + tasks_source.json +docs/ + assets/ + wechat-qrcode.jpg + screenshots/ + 01-kanban-main.png + 02-monitor.png + 03-task-detail.png + 04-model-config.png + 05-skills-config.png + 06-official-overview.png + 07-sessions.png + 08-memorials.png + 09-templates.png + 10-morning-briefing.png + 11-ceremony.png + README.md + Agent_video_Pippit_20260225121727.mp4 + demo.gif + getting-started.md + git-ingest-edict-.txt + remote-skills-guide.md + remote-skills-quickstart.md + task-dispatch-architecture.md + wechat-article.md + wechat.md +edict/ + backend/ + app/ + api/ + __init__.py + admin.py + agents.py + events.py + legacy.py + tasks.py + websocket.py + models/ + __init__.py + event.py + task.py + thought.py + todo.py + services/ + __init__.py + event_bus.py + task_service.py + workers/ + __init__.py + dispatch_worker.py + orchestrator_worker.py + __init__.py + config.py + db.py + main.py + requirements.txt + frontend/ + src/ + components/ + ConfirmDialog.tsx + CourtCeremony.tsx + EdictBoard.tsx + MemorialPanel.tsx + ModelConfig.tsx + MonitorPanel.tsx + MorningPanel.tsx + OfficialPanel.tsx + SessionsPanel.tsx + SkillsConfig.tsx + TaskModal.tsx + TemplatePanel.tsx + Toaster.tsx + api.ts + App.tsx + index.css + main.tsx + store.ts + vite-env.d.ts + .env.development + Dockerfile + index.html + nginx.conf + package.json + postcss.config.js + tailwind.config.js + tsconfig.json + tsconfig.tsbuildinfo + vite.config.ts + migration/ + versions/ + 001_initial.py + env.py + migrate_json_to_pg.py + script.py.mako + scripts/ + kanban_update_edict.py + .env.example + alembic.ini + docker-compose.yml + Dockerfile +examples/ + code-review.md + competitive-analysis.md + README.md + weekly-report.md +scripts/ + apply_model_changes.py + fetch_morning_news.py + file_lock.py + kanban_update.py + record_demo.py + refresh_live_data.py + run_loop.sh + skill_manager.py + sync_agent_config.py + sync_from_openclaw_runtime.py + sync_officials_stats.py + take_screenshots.py + utils.py +tests/ + test_e2e_kanban.py + test_file_lock.py + test_kanban.py + test_server.py +.dockerignore +.gitignore +CONTRIBUTING.md +docker-compose.yml +Dockerfile +Edict Agent Architecture.pdf +edict_agent_architecture.md +install.sh +LICENSE +README_EN.md +README.md +ROADMAP.md + + + +This section contains the contents of the repository's files. + + +Directory structure: +└── cft0808-edict/ + ├── docker-compose.yml + ├── .dockerignore + ├── docker/ + │ └── demo_data/ + │ ├── last_model_change_result.json + │ ├── model_change_log.json + │ └── pending_model_changes.json + ├── edict/ + │ ├── alembic.ini + │ ├── Dockerfile + │ ├── .env.example + │ ├── backend/ + │ │ ├── requirements.txt + │ │ └── app/ + │ │ ├── __init__.py + │ │ ├── api/ + │ │ │ └── __init__.py + │ │ ├── models/ + │ │ │ └── __init__.py + │ │ ├── services/ + │ │ │ └── __init__.py + │ │ └── workers/ + │ │ └── __init__.py + │ ├── frontend/ + │ │ ├── Dockerfile + │ │ ├── index.html + │ │ ├── nginx.conf + │ │ ├── package.json + │ │ ├── postcss.config.js + │ │ ├── tailwind.config.js + │ │ ├── tsconfig.json + │ │ ├── tsconfig.tsbuildinfo + │ │ ├── vite.config.ts + │ │ ├── .env.development + │ │ └── src/ + │ │ ├── main.tsx + │ │ ├── vite-env.d.ts + │ │ └── components/ + │ │ └── Toaster.tsx + │ └── migration/ + │ └── script.py.mako + ├── examples/ + │ └── README.md + ├── .github/ + │ ├── pull_request_template.md + │ ├── ISSUE_TEMPLATE/ + │ │ ├── bug_report.md + │ │ └── feature_request.md + │ └── workflows/ + │ └── ci.yml + └── .vite/ + └── deps/ + ├── _metadata.json + └── package.json + +================================================ +FILE: docker-compose.yml +================================================ +version: '3.8' + +services: + sansheng-demo: + image: cft0808/sansheng-demo:latest + platform: linux/amd64 + ports: + - "7891:7891" + environment: + - DEMO_MODE=true + restart: unless-stopped + +# 启动: docker compose up +# 如果在 ARM Mac (M1/M2/M3) 上运行,删除 platform 行即可 +# 访问: http://localhost:7891 + + + +================================================ +FILE: .dockerignore +================================================ +# Git +.git +.gitignore + +# IDE / Editor +.vscode +.idea +*.swp +*.swo +*~ + +# Python +__pycache__ +*.pyc +*.pyo +.mypy_cache +.pytest_cache +.venv +venv + +# OS +.DS_Store +Thumbs.db + +# Docs (not needed in image) +docs/ +*.md +!README.md +LICENSE + + + +================================================ +FILE: docker/demo_data/last_model_change_result.json +================================================ +{} + + + +================================================ +FILE: docker/demo_data/model_change_log.json +================================================ +[] + + + +================================================ +FILE: docker/demo_data/pending_model_changes.json +================================================ +[] + + + +================================================ +FILE: edict/alembic.ini +================================================ +[alembic] +script_location = migration +sqlalchemy.url = postgresql+asyncpg://edict:edict_dev_2024@localhost:5432/edict + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S + + + +================================================ +FILE: edict/Dockerfile +================================================ +FROM python:3.12-slim + +WORKDIR /app + +# 安装系统依赖 +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +# 安装 Python 依赖 +COPY edict/backend/requirements.txt /tmp/requirements.txt +RUN pip install --no-cache-dir -r /tmp/requirements.txt + +# 复制后端代码 +COPY edict/backend/ /app/ + +# 复制 Alembic 配置 +COPY edict/alembic.ini /app/alembic.ini +COPY edict/migration/ /app/migration/ + +EXPOSE 8000 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] + + + +================================================ +FILE: edict/.env.example +================================================ +# ── Edict 三省六部 事件驱动架构 ── +# 复制为 .env 并根据环境修改 + +# ── Postgres ── +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 +POSTGRES_DB=edict +POSTGRES_USER=edict +POSTGRES_PASSWORD=edict_secret_change_me + +# ── Redis ── +REDIS_URL=redis://localhost:6379/0 + +# ── Backend ── +BACKEND_HOST=0.0.0.0 +BACKEND_PORT=8000 +SECRET_KEY=change-me-in-production +DEBUG=true + +# ── OpenClaw Gateway ── +OPENCLAW_GATEWAY_URL=http://localhost:18789 +OPENCLAW_BIN=openclaw + +# ── 文件路径(兼容旧系统过渡期) ── +LEGACY_DATA_DIR=../data +LEGACY_TASKS_FILE=../data/tasks_source.json + +# ── 调度参数 ── +STALL_THRESHOLD_SEC=180 +MAX_DISPATCH_RETRY=3 +DISPATCH_TIMEOUT_SEC=300 +HEARTBEAT_INTERVAL_SEC=30 + +# ── 飞书通知 ── +FEISHU_DELIVER=true +FEISHU_CHANNEL=feishu + + + +================================================ +FILE: edict/backend/requirements.txt +================================================ +# Edict Backend Dependencies +fastapi[standard]>=0.115.0 +uvicorn[standard]>=0.32.0 +sqlalchemy[asyncio]>=2.0.36 +asyncpg>=0.30.0 +alembic>=1.14.0 +redis[hiredis]>=5.2.0 +pydantic>=2.10.0 +pydantic-settings>=2.6.0 +python-dotenv>=1.0.1 +httpx>=0.28.0 + + + +================================================ +FILE: edict/backend/app/__init__.py +================================================ +"""Edict Backend — 三省六部事件驱动架构。""" + + + +================================================ +FILE: edict/backend/app/api/__init__.py +================================================ +from .tasks import router as tasks_router +from .agents import router as agents_router +from .events import router as events_router +from .admin import router as admin_router +from .websocket import router as websocket_router + +__all__ = [ + "tasks_router", + "agents_router", + "events_router", + "admin_router", + "websocket_router", +] + + + +================================================ +FILE: edict/backend/app/models/__init__.py +================================================ +"""Edict 数据模型包。""" + +from .task import Task, TaskState +from .event import Event +from .thought import Thought +from .todo import Todo + +__all__ = ["Task", "TaskState", "Event", "Thought", "Todo"] + + + +================================================ +FILE: edict/backend/app/services/__init__.py +================================================ +from .event_bus import EventBus, get_event_bus +from .task_service import TaskService + +__all__ = ["EventBus", "get_event_bus", "TaskService"] + + + +================================================ +FILE: edict/backend/app/workers/__init__.py +================================================ +from .orchestrator_worker import OrchestratorWorker, run_orchestrator +from .dispatch_worker import DispatchWorker, run_dispatcher + +__all__ = [ + "OrchestratorWorker", + "run_orchestrator", + "DispatchWorker", + "run_dispatcher", +] + + + +================================================ +FILE: edict/frontend/Dockerfile +================================================ +FROM node:20-alpine AS build +WORKDIR /app +COPY package.json ./ +RUN npm install +COPY . . +RUN npm run build + +FROM nginx:alpine +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 3000 +CMD ["nginx", "-g", "daemon off;"] + + + +================================================ +FILE: edict/frontend/index.html +================================================ + + + + + + + 三省六部 · Edict Dashboard + + +
+ + + + + + +================================================ +FILE: edict/frontend/nginx.conf +================================================ +server { + listen 3000; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location /api/ { + proxy_pass http://backend:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + location /ws { + proxy_pass http://backend:8000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_read_timeout 86400; + } +} + + + +================================================ +FILE: edict/frontend/package.json +================================================ +{ + "name": "edict-dashboard", + "private": true, + "version": "2.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "zustand": "^4.5.5", + "lucide-react": "^0.460.0", + "clsx": "^2.1.1" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.15", + "typescript": "^5.6.3", + "vite": "^6.0.1" + } +} + + + +================================================ +FILE: edict/frontend/postcss.config.js +================================================ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} + + + +================================================ +FILE: edict/frontend/tailwind.config.js +================================================ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + theme: { + extend: { + colors: { + // 三省六部主题色 + imperial: { + 50: '#fef7ed', + 100: '#fdecd4', + 200: '#fad5a8', + 300: '#f7b671', + 400: '#f38d38', + 500: '#f07012', + 600: '#e15808', + 700: '#bb4009', + 800: '#95330f', + 900: '#792c10', + }, + dynasty: { + 50: '#fdf4f3', + 100: '#fce8e4', + 200: '#fad4ce', + 300: '#f5b5ab', + 400: '#ee8a7a', + 500: '#e25f4d', + 600: '#cf4430', + 700: '#ae3624', + 800: '#903022', + 900: '#782d23', + }, + }, + }, + }, + plugins: [], +} + + + +================================================ +FILE: edict/frontend/tsconfig.json +================================================ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src"] +} + + + +================================================ +FILE: edict/frontend/tsconfig.tsbuildinfo +================================================ +{"root":["./src/app.tsx","./src/api.ts","./src/main.tsx","./src/store.ts","./src/vite-env.d.ts","./src/components/confirmdialog.tsx","./src/components/courtceremony.tsx","./src/components/edictboard.tsx","./src/components/memorialpanel.tsx","./src/components/modelconfig.tsx","./src/components/monitorpanel.tsx","./src/components/morningpanel.tsx","./src/components/officialpanel.tsx","./src/components/sessionspanel.tsx","./src/components/skillsconfig.tsx","./src/components/taskmodal.tsx","./src/components/templatepanel.tsx","./src/components/toaster.tsx"],"version":"5.9.3"} + + +================================================ +FILE: edict/frontend/vite.config.ts +================================================ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import path from 'path' + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + }, + build: { + outDir: path.resolve(__dirname, '../../dashboard/dist'), + emptyOutDir: true, + }, +}) + + + +================================================ +FILE: edict/frontend/.env.development +================================================ +VITE_API_URL=http://127.0.0.1:7891 + + + +================================================ +FILE: edict/frontend/src/main.tsx +================================================ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +) + + + +================================================ +FILE: edict/frontend/src/vite-env.d.ts +================================================ +/// + + + +================================================ +FILE: edict/frontend/src/components/Toaster.tsx +================================================ +import { useStore } from '../store'; + +export default function Toaster() { + const toasts = useStore((s) => s.toasts); + if (!toasts.length) return null; + + return ( +
+ {toasts.map((t) => ( +
+ {t.msg} +
+ ))} +
+ ); +} + + + +================================================ +FILE: edict/migration/script.py.mako +================================================ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} + + + +================================================ +FILE: examples/README.md +================================================ +# 📂 案例 / Examples + +真实的端到端使用案例,展示三省六部处理完整旨意的全流程。 + +| # | 案例 | 旨意内容 | 涉及部门 | 复杂度 | +|---|------|---------|----------|--------| +| 1 | [竞品分析](competitive-analysis.md) | "分析 CrewAI vs AutoGen vs LangGraph" | 中书→门下→户部+兵部+礼部 | ⭐⭐⭐ | +| 2 | [代码审查](code-review.md) | "审查 FastAPI 代码的安全性" | 中书→门下→兵部+刑部 | ⭐⭐ | +| 3 | [周报生成](weekly-report.md) | "生成本周工程团队周报" | 中书→门下→户部+礼部 | ⭐⭐ | + +每个案例包含: +- 📜 **圣旨**:原始指令 +- 📋 **中书省规划**:任务拆解方案 +- 🔍 **门下省审议**:审核意见(含封驳/返工记录) +- ⚔️ **各部执行结果**:每个部门的产出摘要 +- 📮 **最终奏折**:尚书省汇总的完整报告 + +> 💡 这些案例基于真实运行记录整理,任务 ID 和时间戳已脱敏。 + + + +================================================ +FILE: .github/pull_request_template.md +================================================ +## 变更描述 + + +## 变更类型 +- [ ] Bug 修复 +- [ ] 新功能 +- [ ] 重构 / 代码优化 +- [ ] 文档更新 +- [ ] CI / 工程配置 + +## 检查清单 +- [ ] 代码已通过 `python3 -m py_compile` 检查 +- [ ] 已在本地测试运行 `run_loop.sh` +- [ ] 涉及看板的变更已在浏览器中验证 +- [ ] 更新了相关文档(如适用) + +## 关联 Issue + + + + +================================================ +FILE: .github/ISSUE_TEMPLATE/bug_report.md +================================================ +--- +name: Bug Report +about: 报告一个 Bug +labels: bug +--- + +## 环境 +- OpenClaw 版本: +- 操作系统: +- Python 版本: + +## 问题描述 + +## 复现步骤 +1. +2. +3. + +## 期望行为 + +## 实际行为 + +## 错误日志 +``` +粘贴日志 +``` + + + +================================================ +FILE: .github/ISSUE_TEMPLATE/feature_request.md +================================================ +--- +name: Feature Request +about: 提交功能建议 +labels: enhancement +--- + +## 功能描述 + +## 使用场景 + +## 期望效果 + +## 其他信息 + + + +================================================ +FILE: .github/workflows/ci.yml +================================================ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + lint-and-test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.10', '3.11', '3.12'] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Syntax check (py_compile) + run: | + find scripts dashboard -name '*.py' | while read f; do + echo " checking $f" + python3 -m py_compile "$f" + done + + - name: Install test dependencies + run: pip install pytest + + - name: Run tests + run: pytest tests/ -v + + - name: Shell lint (bash -n) + run: bash -n scripts/run_loop.sh + + + +================================================ +FILE: .vite/deps/_metadata.json +================================================ +{ + "hash": "2321d508", + "configHash": "3e6eab4b", + "lockfileHash": "e3b0c442", + "browserHash": "5408e294", + "optimized": {}, + "chunks": {} +} + + +================================================ +FILE: .vite/deps/package.json +================================================ +{ + "type": "module" +} +
+ + +--- +name: Bug Report +about: 报告一个 Bug +labels: bug +--- + +## 环境 +- OpenClaw 版本: +- 操作系统: +- Python 版本: + +## 问题描述 + +## 复现步骤 +1. +2. +3. + +## 期望行为 + +## 实际行为 + +## 错误日志 +``` +粘贴日志 +``` + + + +--- +name: Feature Request +about: 提交功能建议 +labels: enhancement +--- + +## 功能描述 + +## 使用场景 + +## 期望效果 + +## 其他信息 + + + +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + lint-and-test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.10', '3.11', '3.12'] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Syntax check (py_compile) + run: | + find scripts dashboard -name '*.py' | while read f; do + echo " checking $f" + python3 -m py_compile "$f" + done + + - name: Install test dependencies + run: pip install pytest + + - name: Run tests + run: pytest tests/ -v + + - name: Shell lint (bash -n) + run: bash -n scripts/run_loop.sh + + + +## 变更描述 + + +## 变更类型 +- [ ] Bug 修复 +- [ ] 新功能 +- [ ] 重构 / 代码优化 +- [ ] 文档更新 +- [ ] CI / 工程配置 + +## 检查清单 +- [ ] 代码已通过 `python3 -m py_compile` 检查 +- [ ] 已在本地测试运行 `run_loop.sh` +- [ ] 涉及看板的变更已在浏览器中验证 +- [ ] 更新了相关文档(如适用) + +## 关联 Issue + + + + +{ + "hash": "2321d508", + "configHash": "3e6eab4b", + "lockfileHash": "e3b0c442", + "browserHash": "5408e294", + "optimized": {}, + "chunks": {} +} + + + +{ + "type": "module" +} + + + +# 吏部 · 尚书 + +你是吏部尚书,负责在尚书省派发的任务中承担**人事管理、团队建设与能力培训**相关的执行工作。 + +## 专业领域 +吏部掌管人才铨选,你的专长在于: +- **Agent 管理**:新 Agent 接入评估、SOUL 配置审核、能力基线测试 +- **技能培训**:Skill 编写与优化、Prompt 调优、知识库维护 +- **考核评估**:输出质量评分、token 效率分析、响应时间基准 +- **团队文化**:协作规范制定、沟通模板标准化、最佳实践沉淀 + +当尚书省派发的子任务涉及以上领域时,你是首选执行者。 + +## 核心职责 +1. 接收尚书省下发的子任务 +2. **立即更新看板**(CLI 命令) +3. 执行任务,随时更新进展 +4. 完成后**立即更新看板**,上报成果给尚书省 + +--- + +## 🛠 看板操作(必须用 CLI 命令) + +> ⚠️ **所有看板操作必须用 `kanban_update.py` CLI 命令**,不要自己读写 JSON 文件! +> 自行操作文件会因路径问题导致静默失败,看板卡住不动。 + +### ⚡ 接任务时(必须立即执行) +```bash +python3 scripts/kanban_update.py state JJC-xxx Doing "吏部开始执行[子任务]" +python3 scripts/kanban_update.py flow JJC-xxx "吏部" "吏部" "▶️ 开始执行:[子任务内容]" +``` + +### ✅ 完成任务时(必须立即执行) +```bash +python3 scripts/kanban_update.py flow JJC-xxx "吏部" "尚书省" "✅ 完成:[产出摘要]" +``` + +然后用 `sessions_send` 把成果发给尚书省。 + +### 🚫 阻塞时(立即上报) +```bash +python3 scripts/kanban_update.py state JJC-xxx Blocked "[阻塞原因]" +python3 scripts/kanban_update.py flow JJC-xxx "吏部" "尚书省" "🚫 阻塞:[原因],请求协助" +``` + +## ⚠️ 合规要求 +- 接任/完成/阻塞,三种情况**必须**更新看板 +- 尚书省设有24小时审计,超时未更新自动标红预警 + + + +*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.static{position:static}.relative{position:relative}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.grid{display:grid}.contents{display:contents}.hidden{display:none}.resize{resize:both}.break-all{word-break:break-all}.border{border-width:1px}.line-through{text-decoration-line:line-through}.outline{outline-style:solid}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}:root{--bg: #07090f;--panel: #0f1219;--panel2: #141824;--line: #1c2236;--text: #dde4f8;--muted: #5a6b92;--ok: #2ecc8a;--warn: #f5c842;--danger: #ff5270;--acc: #6a9eff;--acc2: #a07aff}*{box-sizing:border-box;margin:0;padding:0}body{background:var(--bg);color:var(--text);font-family:PingFang SC,Inter,-apple-system,Segoe UI,sans-serif;min-height:100vh}::-webkit-scrollbar{width:4px;height:4px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:#1e2538;border-radius:4px}.wrap{max-width:1400px;margin:0 auto;padding:16px}.hdr{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;margin-bottom:16px;padding-bottom:14px;border-bottom:1px solid var(--line)}.logo{font-size:20px;font-weight:800;background:linear-gradient(135deg,#6a9eff,#a07aff);-webkit-background-clip:text;-webkit-text-fill-color:transparent}.sub-text{font-size:11px;color:var(--muted)}.hdr-r{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.chip{font-size:11px;padding:3px 9px;border:1px solid var(--line);border-radius:999px;background:var(--panel);color:var(--muted)}.chip.ok{border-color:#2ecc8a44;color:var(--ok)}.chip.warn{border-color:#f5c84244;color:var(--warn)}.chip.err{border-color:#ff527044;color:var(--danger)}.btn-refresh{font-size:11px;padding:4px 10px;border:1px solid var(--acc);border-radius:6px;background:transparent;color:var(--acc);cursor:pointer}.btn-refresh:hover{background:#0a1228}.tabs{display:flex;gap:2px;margin-bottom:18px;border-bottom:1px solid var(--line);overflow-x:auto}.tab{font-size:13px;padding:8px 16px;border-radius:8px 8px 0 0;cursor:pointer;color:var(--muted);border:1px solid transparent;border-bottom:none;white-space:nowrap;position:relative;bottom:-1px;transition:all .15s;-webkit-user-select:none;-moz-user-select:none;user-select:none}.tab:hover{color:var(--text);background:var(--panel)}.tab.active{color:var(--text);background:var(--panel);border-color:var(--line);font-weight:600}.tbadge{font-size:10px;padding:1px 5px;border-radius:999px;background:#1a2040;color:var(--acc);margin-left:4px}.edict-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:12px}.edict-card{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:18px;cursor:pointer;transition:border-color .15s,transform .1s,box-shadow .15s}.edict-card:hover{border-color:var(--acc);transform:translateY(-2px);box-shadow:0 4px 20px #6a9eff1a}.edict-card.archived{opacity:.55;border-style:dashed}.edict-card.archived:hover{opacity:.85}.ec-pipe{display:flex;align-items:center;gap:0;margin-bottom:14px;overflow-x:auto;padding-bottom:2px}.ep-node{display:flex;flex-direction:column;align-items:center;gap:1px;padding:5px 8px;border-radius:6px;flex-shrink:0;min-width:52px}.ep-node.done{background:#0a2018}.ep-node.active{background:#0f1a38;border:1px solid var(--acc)}.ep-node.pending{opacity:.3}.ep-icon{font-size:14px}.ep-name{font-size:9px;color:var(--muted);white-space:nowrap}.ep-node.done .ep-name{color:var(--ok)}.ep-node.active .ep-name{color:var(--acc);font-weight:700}.ep-arrow{font-size:10px;color:#1c2236;padding:0 1px;flex-shrink:0}.ec-id{font-size:10px;color:var(--acc);font-weight:700;letter-spacing:.04em;margin-bottom:5px}.ec-title{font-size:15px;font-weight:700;line-height:1.4;margin-bottom:10px;color:var(--text)}.ec-meta{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-bottom:8px}.tag{font-size:10px;padding:2px 7px;border-radius:4px;border:1px solid;display:inline-block;white-space:nowrap}.st-Inbox{border-color:#3a4a7a44;color:#7a9aff;background:#0a1028}.st-Taizi{border-color:#e8a04044;color:#e8a040;background:#281a08}.st-Zhongshu{border-color:#a07aff44;color:#a07aff;background:#110a28}.st-Menxia{border-color:#ff9a6a44;color:#ff9a6a;background:#280f0a}.st-Assigned,.st-Doing{border-color:#6a9eff44;color:#6a9eff;background:#0a1428}.st-Review{border-color:#f5c84244;color:#f5c842;background:#201a08}.st-Done{border-color:#2ecc8a44;color:var(--ok);background:#0a2018}.st-Blocked{border-color:#ff527044;color:var(--danger);background:#200a10}.st-Cancelled{border-color:#8884;color:#888;background:#1a1a1a}.st-Next{border-color:#4a9adf44;color:#4a9adf;background:#0a1424}.st-Pending{border-color:#3a4a7a44;color:#7a9aff;background:#0a1028}.dt-中书省{border-color:#a07aff44;color:#a07aff;background:#1a0f38}.dt-门下省{border-color:#6a9eff44;color:#6a9eff;background:#0f1a38}.dt-尚书省{border-color:#6aef9a44;color:#6aef9a;background:#0a2018}.dt-礼部{border-color:#f5c84244;color:#f5c842;background:#201a08}.dt-户部{border-color:#ff9a6a44;color:#ff9a6a;background:#28100a}.dt-兵部{border-color:#ff527044;color:#ff5270;background:#280a10}.dt-刑部{border-color:#c444;color:#c44;background:#280808}.dt-工部{border-color:#4af4;color:#4af;background:#081828}.ec-footer{display:flex;align-items:center;justify-content:space-between;margin-top:10px;flex-wrap:wrap;gap:6px}.hb{font-size:10px;padding:2px 7px;border-radius:999px;border:1px solid var(--line)}.hb.active{border-color:#2ecc8a44;color:var(--ok)}.hb.warn{border-color:#f5c84244;color:var(--warn)}.hb.stalled{border-color:#ff527044;color:var(--danger);animation:pulse 1.5s infinite}.hb.unknown{color:var(--muted)}@keyframes pulse{0%,to{opacity:1}50%{opacity:.4}}.modal-bg{position:fixed;top:0;right:0;bottom:0;left:0;background:#000000b3;z-index:100;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);overflow-y:auto;display:flex;align-items:flex-start;justify-content:center;padding:40px 16px}.modal{background:var(--panel);border:1px solid var(--line);border-radius:18px;width:100%;max-width:760px;padding:28px;position:relative;box-shadow:0 20px 60px #0009}.modal-close{position:absolute;top:16px;right:16px;width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:8px;cursor:pointer;font-size:18px;color:var(--muted);background:none;border:none}.modal-close:hover{background:var(--panel2);color:var(--text)}.modal-id{font-size:11px;color:var(--acc);font-weight:700;letter-spacing:.04em;margin-bottom:6px}.modal-title{font-size:22px;font-weight:800;line-height:1.3;margin-bottom:18px}.m-pipe{display:flex;align-items:stretch;gap:0;overflow-x:auto;padding:16px;background:var(--panel2);border-radius:12px;margin-bottom:20px}.mp-stage{display:flex;align-items:center;flex-shrink:0}.mp-node{display:flex;flex-direction:column;align-items:center;gap:4px;padding:10px 14px;border-radius:10px;min-width:80px;position:relative}.mp-node.done{background:#0a2018;border:1px solid #2ecc8a44}.mp-node.active{background:#0f1838;border:2px solid var(--acc);box-shadow:0 0 14px #6a9eff33}.mp-node.pending{opacity:.25;border:1px dashed var(--line)}.mp-icon{font-size:22px}.mp-dept{font-size:12px;font-weight:700;margin-top:2px}.mp-node.done .mp-dept{color:var(--ok)}.mp-node.active .mp-dept{color:var(--acc)}.mp-node.pending .mp-dept{color:var(--muted)}.mp-action{font-size:10px;color:var(--muted);margin-top:1px}.mp-node.active .mp-action{color:#6a9eff88}.mp-done-tick{position:absolute;top:-6px;right:-6px;width:16px;height:16px;background:var(--ok);border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:9px;color:#000;font-weight:700}.mp-arrow{color:#1c2236;font-size:18px;padding:0 6px;margin-top:-10px}.cur-stage{display:flex;align-items:center;gap:10px;padding:12px 16px;background:#0a1228;border:1px solid var(--acc);border-radius:10px;margin-bottom:18px}.cs-icon{font-size:24px}.cs-info .cs-dept{font-size:16px;font-weight:700;color:var(--acc)}.cs-info .cs-action{font-size:12px;color:var(--muted);margin-top:2px}.cs-hb{margin-left:auto}.m-section{margin-bottom:18px}.m-sec-label{font-size:11px;font-weight:700;color:var(--muted);letter-spacing:.06em;text-transform:uppercase;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid var(--line)}.fl-timeline{display:flex;flex-direction:column;gap:0;position:relative}.fl-timeline:before{content:"";position:absolute;left:60px;top:0;bottom:0;width:1px;background:var(--line)}.fl-item{display:flex;gap:0;position:relative;padding:8px 0}.fl-time{min-width:60px;font-size:10px;color:var(--muted);text-align:right;padding-right:14px;flex-shrink:0;padding-top:3px}.fl-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0;margin-top:3px;position:relative;z-index:1}.fl-content{padding-left:12px;flex:1}.fl-who{font-size:12px;margin-bottom:2px}.fl-who .from,.fl-who .to{font-weight:700}.fl-rem{font-size:11px;color:var(--muted);line-height:1.5}.m-rows{display:grid;grid-template-columns:1fr 1fr;gap:8px}.m-row{background:var(--panel2);border-radius:8px;padding:10px 12px}.mr-label{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em;margin-bottom:3px}.mr-val{font-size:13px;font-weight:600;word-break:break-all}.duty-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(310px,1fr));gap:12px}.duty-card{background:var(--panel);border:1px solid var(--line);border-radius:14px;overflow:hidden;transition:border-color .15s}.duty-card:hover{border-color:#2e3d6a}.duty-card.active-card{border-color:var(--acc)}.duty-card.blocked-card{border-color:#ff527055}.dc-hdr{display:flex;align-items:center;gap:10px;padding:12px 16px;background:var(--panel2);border-bottom:1px solid var(--line)}.dc-emoji{font-size:22px}.dc-info{flex:1}.dc-name{font-size:14px;font-weight:800}.dc-role{font-size:10px;color:var(--muted)}.dc-status{display:flex;align-items:center;gap:5px;font-size:11px}.dc-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}.dc-dot.active{background:var(--ok)}.dc-dot.busy{background:var(--warn);animation:pulse 1.5s infinite}.dc-dot.blocked{background:var(--danger);animation:pulse 1s infinite}.dc-dot.idle{background:#2a3a5a}.dc-body{padding:14px 16px}.dc-idle{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:13px;padding:6px 0}.dc-task{display:flex;flex-direction:column;gap:6px;cursor:pointer;padding:6px;border-radius:8px;border:1px solid var(--line);margin-bottom:6px}.dc-task:hover{border-color:var(--acc)}.dc-task-id{font-size:10px;color:var(--acc);font-weight:700;letter-spacing:.04em}.dc-task-title{font-size:14px;font-weight:700;color:var(--text);line-height:1.3}.dc-task-now{font-size:12px;color:var(--muted);line-height:1.5;margin-top:2px}.dc-task-meta{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:6px}.dc-footer{padding:8px 16px;border-top:1px solid var(--line);display:flex;align-items:center;gap:8px;background:var(--panel2)}.dc-model{font-size:10px;color:var(--muted)}.dc-la{font-size:10px;color:var(--muted);margin-left:auto}.as-panel{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:14px 18px;margin-bottom:16px}.as-header{display:flex;align-items:center;gap:10px;margin-bottom:12px}.as-title{font-size:13px;font-weight:700}.as-gw{font-size:11px;padding:3px 10px;border-radius:999px;margin-left:auto}.as-gw.ok{background:#0a2018;border:1px solid #2ecc8a44;color:var(--ok)}.as-gw.err{background:#200a10;border:1px solid #ff527044;color:var(--danger)}.as-gw.warn{background:#201a08;border:1px solid #f5c84244;color:var(--warn)}.as-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:8px}.as-card{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:10px;text-align:center;cursor:pointer;transition:border-color .15s,background .15s;position:relative}.as-card:hover{border-color:var(--acc);background:#0a1228}.as-dot{position:absolute;top:6px;right:6px;width:8px;height:8px;border-radius:50%}.as-dot.running{background:#2ecc8a;box-shadow:0 0 6px #2ecc8a88;animation:pulse 1.5s infinite}.as-dot.idle{background:#4a5568}.as-dot.offline{background:#ff5270;animation:pulse 1.2s infinite}.as-dot.unconfigured{background:#6b7280}.as-wake-btn{font-size:10px;padding:2px 8px;border-radius:6px;border:1px solid var(--acc);color:var(--acc);background:transparent;cursor:pointer;margin-top:6px;transition:background .15s}.as-wake-btn:hover{background:var(--acc);color:#fff}.as-summary{font-size:11px;color:var(--muted);display:flex;gap:12px;margin-top:10px;padding-top:8px;border-top:1px solid var(--line)}.task-actions{display:flex;gap:8px;margin-bottom:18px;flex-wrap:wrap}.btn-action{font-size:12px;padding:7px 16px;border-radius:8px;border:none;cursor:pointer;font-weight:700;transition:all .15s}.btn-stop{background:#ff527022;color:#ff5270;border:1px solid #ff527044}.btn-stop:hover{background:#ff527044}.btn-cancel-action{background:#8882;color:#888;border:1px solid #88888844}.btn-cancel-action:hover{background:#8884}.btn-resume{background:#2ecc8a22;color:#2ecc8a;border:1px solid #2ecc8a44}.btn-resume:hover{background:#2ecc8a44}.sched-section{margin-bottom:18px;background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:12px}.sched-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px}.sched-title{font-size:11px;font-weight:700;letter-spacing:.06em;color:var(--acc)}.sched-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-bottom:10px}.sched-kpi{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:8px 10px}.sched-kpi .k{font-size:10px;color:var(--muted);margin-bottom:2px}.sched-kpi .v{font-size:13px;font-weight:700}.sched-btn{font-size:11px;padding:5px 10px;border-radius:6px;border:1px solid var(--line);background:transparent;color:var(--muted);cursor:pointer;transition:all .15s}.sched-btn:hover{border-color:var(--acc);color:var(--text)}.todo-section{margin-bottom:18px}.todo-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.todo-progress{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--muted)}.todo-bar{width:120px;height:6px;background:#0e1320;border-radius:3px;overflow:hidden}.todo-bar-fill{height:100%;border-radius:3px;background:var(--ok);transition:width .3s}.todo-list{display:flex;flex-direction:column;gap:4px}.todo-item{display:flex;flex-direction:column;background:var(--panel2);border-radius:8px;font-size:12px;transition:opacity .15s}.todo-item.done{opacity:.55}.todo-item .t-row{display:flex;align-items:center;gap:8px;padding:7px 10px}.todo-item .t-icon{font-size:14px;flex-shrink:0}.todo-item .t-title{flex:1;color:var(--text)}.todo-item.done .t-title{text-decoration:line-through;color:var(--muted)}.todo-item .t-status{font-size:10px;padding:2px 6px;border-radius:4px}.todo-item .t-status.s-done{color:var(--ok);background:#0a2018;border:1px solid #2ecc8a44}.todo-item .t-status.s-progress{color:var(--acc);background:#0a1228;border:1px solid #6a9eff44}.todo-item .t-status.s-notstarted{color:var(--muted);background:var(--panel);border:1px solid var(--line)}.ec-todo-bar{display:flex;align-items:center;gap:6px;font-size:10px;color:var(--muted);margin-top:6px}.ec-todo-track{flex:1;max-width:80px;height:4px;background:#0e1320;border-radius:2px;overflow:hidden}.ec-todo-fill{height:100%;background:var(--ok);border-radius:2px}.ec-actions{display:flex;gap:4px;margin-top:8px}.ec-actions .mini-act{font-size:10px;padding:3px 8px;border-radius:5px;border:1px solid var(--line);background:transparent;cursor:pointer;color:var(--muted);transition:all .12s}.ec-actions .mini-act:hover{border-color:var(--acc);color:var(--text)}.ec-actions .mini-act.danger:hover{border-color:#ff5270;color:#ff5270}.archive-bar{display:flex;align-items:center;gap:8px;margin-bottom:14px;flex-wrap:wrap}.ab-label{font-size:12px;color:var(--muted);margin-right:4px}.ab-btn{font-size:11px;padding:4px 12px;border-radius:6px;border:1px solid var(--line);background:transparent;cursor:pointer;color:var(--muted);transition:all .15s;font-weight:600}.ab-btn:hover{border-color:var(--acc);color:var(--text)}.ab-btn.active{border-color:var(--acc);color:var(--acc);background:#0f1a38}.ab-count{font-size:10px;color:var(--muted);margin-left:auto}.ab-scan{font-size:11px;padding:4px 12px;border-radius:6px;border:1px solid #6a9eff44;background:transparent;cursor:pointer;color:var(--acc);font-weight:600;transition:all .15s}.ab-scan:hover{background:#0a1228;border-color:var(--acc)}.ab-scan-status{font-size:10px;color:var(--muted)}.la-section{margin-bottom:18px}.la-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.la-title{font-size:11px;font-weight:700;color:var(--acc);letter-spacing:.06em}.la-log{max-height:320px;overflow-y:auto;background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:10px 12px;display:flex;flex-direction:column;gap:6px;font-size:12px}.la-entry{display:flex;gap:8px;align-items:flex-start;padding:5px 8px;border-radius:6px;line-height:1.5;word-break:break-all}.la-entry:hover{background:#6a9eff0a}.la-empty{text-align:center;color:var(--muted);padding:20px;font-size:12px}.model-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:12px;margin-bottom:18px}.mc-card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px}.mc-top{display:flex;align-items:center;gap:10px;margin-bottom:12px}.mc-emoji{font-size:22px}.mc-name{font-size:15px;font-weight:700}.mc-role{font-size:11px;color:var(--muted)}.mc-cur{font-size:11px;color:var(--muted);margin-bottom:8px}.mc-cur b{color:var(--text)}.msel{width:100%;background:var(--panel2);border:1px solid var(--line);border-radius:7px;color:var(--text);padding:7px 10px;font-size:12px;outline:none;cursor:pointer}.msel:focus{border-color:var(--acc)}.mc-btns{display:flex;gap:6px;margin-top:8px}.btn{font-size:12px;padding:6px 14px;border-radius:7px;border:none;cursor:pointer;font-weight:600}.btn-p{background:var(--acc);color:#000}.btn-p:hover{filter:brightness(1.15)}.btn-p:disabled{background:#2a3a6a;color:var(--muted);cursor:not-allowed}.btn-g{background:transparent;border:1px solid var(--line);color:var(--muted)}.btn-g:hover{border-color:#2e3d6a;color:var(--text)}.cl-wrap{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px}.cl-title{font-size:12px;font-weight:700;color:var(--muted);letter-spacing:.05em;text-transform:uppercase;margin-bottom:10px}.cl-row{display:flex;gap:10px;font-size:11px;padding:5px 0;border-bottom:1px solid var(--line)}.cl-row:last-child{border-bottom:none}.skills-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}.sk-card{background:var(--panel);border:1px solid var(--line);border-radius:12px;overflow:hidden}.sk-hdr{display:flex;align-items:center;gap:9px;padding:11px 14px;background:var(--panel2);border-bottom:1px solid var(--line)}.sk-list{padding:10px}.sk-item{display:flex;gap:8px;padding:8px 10px;border-radius:7px;font-size:12px;margin-bottom:3px;cursor:pointer;border:1px solid transparent;transition:all .12s}.sk-item:hover{background:var(--panel2);border-color:var(--line)}.sess-filters{display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap;align-items:center}.sess-filter{font-size:11px;padding:4px 10px;border-radius:999px;border:1px solid var(--line);background:var(--panel);color:var(--muted);cursor:pointer;transition:all .12s}.sess-filter:hover{border-color:var(--acc);color:var(--text)}.sess-filter.active{border-color:var(--acc);color:var(--acc);background:#0a1228}.sess-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:10px}.sess-card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px;transition:border-color .12s;cursor:pointer}.sess-card:hover{border-color:#2e3d6a}.off-activity{display:flex;align-items:center;gap:8px;padding:8px 14px;background:#0a1228;border:1px solid #1a2a4a;border-radius:10px;margin-bottom:14px;font-size:12px;flex-wrap:wrap}.off-kpi{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:16px}.kpi{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px 16px}.kpi-v{font-size:24px;font-weight:800;margin-bottom:3px}.kpi-l{font-size:11px;color:var(--muted)}.off-layout{display:grid;grid-template-columns:260px 1fr;gap:14px}@media(max-width:700px){.off-layout{grid-template-columns:1fr}.off-kpi{grid-template-columns:repeat(2,1fr)}}.off-ranklist{background:var(--panel);border:1px solid var(--line);border-radius:14px;overflow:hidden}.orl-hdr{padding:10px 14px;background:var(--panel2);border-bottom:1px solid var(--line);font-size:11px;font-weight:700;color:var(--muted);letter-spacing:.06em;text-transform:uppercase}.orl-item{display:flex;align-items:center;gap:10px;padding:10px 14px;cursor:pointer;border-bottom:1px solid var(--line);transition:background .1s}.orl-item:last-child{border-bottom:none}.orl-item:hover{background:var(--panel2)}.orl-item.selected{background:#0a1228;border-left:3px solid var(--acc)}.off-detail{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:22px;min-height:400px}.mem-list{display:flex;flex-direction:column;gap:8px}.mem-card{display:flex;gap:14px;align-items:flex-start;background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px 16px;cursor:pointer;transition:border-color .12s}.mem-card:hover{border-color:var(--acc)}.tpl-cats{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px}.tpl-cat{font-size:12px;padding:6px 14px;border-radius:999px;border:1px solid var(--line);background:var(--panel);color:var(--muted);cursor:pointer;transition:all .12s}.tpl-cat:hover{border-color:var(--acc);color:var(--text)}.tpl-cat.active{border-color:var(--acc);color:var(--acc);background:#0a1228}.tpl-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:10px}.tpl-card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px;transition:border-color .12s;cursor:pointer;display:flex;flex-direction:column}.tpl-card:hover{border-color:var(--acc)}.mb-hdr{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:18px;flex-wrap:wrap;gap:10px}.mb-title{font-size:20px;font-weight:800;background:linear-gradient(135deg,#f5c842,#ff9a4a);-webkit-background-clip:text;-webkit-text-fill-color:transparent}.mb-sub{font-size:12px;color:var(--muted);margin-top:3px}.mb-cats{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:16px}.mb-cat{background:var(--panel);border:1px solid var(--line);border-radius:14px;overflow:hidden}.mb-cat-hdr{display:flex;align-items:center;gap:8px;padding:10px 16px;border-bottom:1px solid var(--line)}.mb-news-list{padding:10px}.mb-card{display:flex;gap:12px;padding:10px 8px;border-radius:10px;margin-bottom:6px;cursor:pointer;transition:background .12s;border-bottom:1px solid var(--line)}.mb-card:last-child{border-bottom:none}.mb-card:hover{background:var(--panel2)}.ceremony-bg{position:fixed;top:0;right:0;bottom:0;left:0;z-index:9999;background:#07090f;display:flex;flex-direction:column;align-items:center;justify-content:center;opacity:0;animation:crmFadeIn .6s ease forwards;cursor:pointer}.ceremony-bg.out{animation:crmFadeOut .5s ease forwards}.crm-glow{position:absolute;width:400px;height:400px;border-radius:50%;background:radial-gradient(circle,rgba(106,158,255,.08),transparent 70%);animation:crmPulse 3s ease-in-out infinite}.crm-line1{font-family:"Noto Serif SC",serif;font-size:52px;font-weight:900;color:#dde4f8;letter-spacing:.15em;opacity:0;transform:translateY(20px)}.crm-line2{font-family:"Noto Serif SC",serif;font-size:22px;font-weight:700;color:var(--acc);letter-spacing:.2em;margin-top:12px;opacity:0;transform:translateY(15px)}.crm-line3{font-size:14px;color:var(--muted);margin-top:24px;opacity:0;letter-spacing:.05em}.crm-date{font-size:12px;color:#2a3555;margin-top:40px;opacity:0;letter-spacing:.08em}.crm-skip{font-size:11px;color:#2a3555;margin-top:18px;opacity:0;animation:crmChar .4s 2.5s forwards}.crm-line1.in{animation:crmSlideUp .6s .3s ease forwards}.crm-line2.in{animation:crmSlideUp .5s 1.1s ease forwards}.crm-line3.in{animation:crmSlideUp .5s 1.6s ease forwards}.crm-date.in{animation:crmChar .4s 2s ease forwards}@keyframes crmFadeIn{to{opacity:1}}@keyframes crmFadeOut{to{opacity:0;pointer-events:none}}@keyframes crmSlideUp{to{opacity:1;transform:translateY(0)}}@keyframes crmChar{to{opacity:1}}@keyframes crmPulse{0%,to{transform:scale(1);opacity:.5}50%{transform:scale(1.1);opacity:.8}}.confirm-bg{position:fixed;top:0;right:0;bottom:0;left:0;background:#000000b3;z-index:200;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);display:flex;align-items:center;justify-content:center}.confirm-box{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:24px;max-width:420px;width:90%;box-shadow:0 20px 60px #0009}.confirm-title{font-size:16px;font-weight:700;margin-bottom:8px}.confirm-msg{font-size:13px;color:var(--muted);margin-bottom:14px;line-height:1.5}.confirm-input{width:100%;background:var(--panel2);border:1px solid var(--line);border-radius:7px;color:var(--text);padding:8px 10px;font-size:12px;outline:none;margin-bottom:14px}.confirm-input:focus{border-color:var(--acc)}.confirm-btns{display:flex;gap:8px;justify-content:flex-end}.toaster{position:fixed;bottom:20px;right:20px;display:flex;flex-direction:column;gap:8px;z-index:300;pointer-events:none}.toast{font-size:13px;padding:10px 16px;border-radius:10px;border:1px solid var(--line);background:var(--panel);color:var(--text);box-shadow:0 4px 20px #0006;animation:tin .2s;max-width:320px;pointer-events:auto}.toast.ok{border-color:#2ecc8a55;background:#0a1a10}.toast.err{border-color:#ff527055;background:#200a10}@keyframes tin{0%{transform:translate(40px);opacity:0}to{transform:translate(0);opacity:1}}.sub-config{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:18px;margin-bottom:18px}.sub-section{margin-bottom:16px;padding-bottom:14px;border-bottom:1px solid var(--line)}.sub-section:last-child{border-bottom:none;margin-bottom:0;padding-bottom:0}.sub-sec-title{font-size:13px;font-weight:700;margin-bottom:10px}.sub-input{background:var(--panel2);border:1px solid var(--line);border-radius:7px;color:var(--text);padding:7px 10px;font-size:12px;outline:none;min-width:0}.sub-input:focus{border-color:var(--acc)}.si-name{font-weight:600;min-width:100px}.si-desc{color:var(--muted);flex:1;line-height:1.4}.si-arrow{color:var(--muted);font-size:14px;opacity:.3;transition:opacity .12s}.sk-item:hover .si-arrow{opacity:1}.sk-emoji{font-size:18px}.sk-name{font-size:14px;font-weight:700}.sk-cnt{font-size:11px;color:var(--muted);margin-left:auto}.sk-empty{font-size:12px;color:var(--muted);padding:12px;text-align:center;opacity:.6}.sk-add{display:flex;align-items:center;justify-content:center;gap:6px;padding:8px;font-size:12px;color:var(--acc);cursor:pointer;border-top:1px solid var(--line);transition:background .12s}.sk-add:hover{background:var(--panel2)}.sk-modal-body{max-height:70vh;overflow-y:auto}.sk-md{font-size:13px;line-height:1.7;color:var(--text)}.sk-md h1,.sk-md h2,.sk-md h3{margin:16px 0 8px;color:var(--text)}.sk-md h1{font-size:18px}.sk-md h2{font-size:15px;border-bottom:1px solid var(--line);padding-bottom:6px}.sk-md h3{font-size:13px}.sk-md p{margin:6px 0}.sk-md ul,.sk-md ol{padding-left:20px;margin:6px 0}.sk-md li{margin:3px 0}.sk-md code{font-size:11px;background:var(--panel2);padding:2px 6px;border-radius:4px;font-family:monospace}.sk-md pre{background:var(--panel2);border:1px solid var(--line);border-radius:8px;padding:12px;overflow-x:auto;margin:8px 0}.sk-md pre code{background:none;padding:0}.sk-md table{width:100%;border-collapse:collapse;font-size:12px;margin:8px 0}.sk-md th,.sk-md td{padding:6px 10px;border:1px solid var(--line);text-align:left}.sk-md th{background:var(--panel2)}.sk-md hr{border:none;border-top:1px solid var(--line);margin:14px 0}.sk-path{font-size:10px;color:var(--muted);padding:8px 0;word-break:break-all;border-top:1px solid var(--line);margin-top:12px}.sc-top{display:flex;align-items:center;gap:10px;margin-bottom:8px}.sc-emoji{font-size:20px}.sc-agent{font-size:13px;font-weight:700}.sc-org{font-size:11px;color:var(--muted)}.sc-title{font-size:13px;font-weight:600;margin-bottom:6px;line-height:1.4}.sc-now{font-size:11px;color:var(--muted);line-height:1.5;margin-bottom:6px}.sc-meta{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.sc-id{font-size:10px;color:var(--acc);font-weight:600}.sc-time{font-size:10px;color:var(--muted);margin-left:auto}.mc-st{font-size:11px;margin-top:6px;padding:4px 8px;border-radius:5px;display:none}.mc-st.ok{display:block;background:#0a2018;color:var(--ok);border:1px solid #2ecc8a44}.mc-st.err{display:block;background:#200a10;color:var(--danger);border:1px solid #ff527044}.mc-st.pending{display:block;background:#0a1228;color:var(--acc);border:1px solid #6a9eff44}.cl-t{color:var(--muted);min-width:115px}.cl-a{color:var(--acc);min-width:80px}.cl-c{color:var(--muted)}.cl-c b{color:var(--text)}.cl-list{display:flex;flex-direction:column}.mem-icon{font-size:28px;flex-shrink:0;margin-top:2px}.mem-info{flex:1;min-width:0}.mem-title{font-size:14px;font-weight:700;margin-bottom:4px}.mem-sub{font-size:11px;color:var(--muted);line-height:1.5}.mem-tags{display:flex;gap:4px;flex-wrap:wrap;margin-top:6px}.mem-tag{font-size:10px;padding:2px 8px;border-radius:4px;background:var(--panel2);color:var(--muted);border:1px solid var(--line)}.mem-right{display:flex;flex-direction:column;align-items:flex-end;gap:4px;flex-shrink:0}.mem-date{font-size:10px;color:var(--muted)}.mem-cost{font-size:10px;color:var(--acc)}.mem-empty{text-align:center;padding:40px;color:var(--muted);font-size:13px}.md-timeline{position:relative;padding-left:24px;margin:16px 0}.md-timeline:before{content:"";position:absolute;left:7px;top:0;bottom:0;width:2px;background:var(--line)}.md-tl-item{position:relative;margin-bottom:14px;padding-bottom:14px;border-bottom:1px solid var(--line)}.md-tl-item:last-child{border-bottom:none;margin-bottom:0;padding-bottom:0}.md-tl-dot{position:absolute;left:-20px;top:3px;width:10px;height:10px;border-radius:50%;background:var(--acc);border:2px solid var(--bg)}.md-tl-dot.green{background:var(--ok)}.md-tl-dot.yellow{background:var(--warn)}.md-tl-dot.red{background:var(--danger)}.md-tl-from{font-size:11px;font-weight:700;color:var(--acc)}.md-tl-to{font-size:11px;color:var(--muted)}.md-tl-remark{font-size:12px;margin-top:3px;line-height:1.5}.md-tl-time{font-size:10px;color:var(--muted);margin-top:2px}.tpl-top{display:flex;align-items:center;gap:10px;margin-bottom:10px}.tpl-icon{font-size:24px}.tpl-name{font-size:14px;font-weight:700}.tpl-pop{font-size:10px;color:var(--muted);margin-left:auto}.tpl-desc{font-size:12px;color:var(--muted);line-height:1.5;margin-bottom:10px;flex:1}.tpl-footer{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.tpl-dept{font-size:10px;padding:2px 6px;border-radius:4px;background:var(--panel2);color:var(--acc)}.tpl-est{font-size:10px;color:var(--muted);margin-left:auto}.tpl-go{font-size:11px;padding:5px 14px;border-radius:6px;background:var(--acc);color:#fff;border:none;cursor:pointer;font-weight:600;margin-left:8px;transition:opacity .12s}.tpl-go:hover{opacity:.85}.tpl-form{margin-top:18px}.tpl-field{margin-bottom:14px}.tpl-label{font-size:12px;font-weight:600;display:block;margin-bottom:6px}.tpl-input{width:100%;padding:10px 12px;background:var(--bg);border:1px solid var(--line);border-radius:8px;color:var(--text);font-size:13px;outline:none}.mb-img{width:72px;height:52px;border-radius:7px;-o-object-fit:cover;object-fit:cover;flex-shrink:0;background:var(--panel2);display:flex;align-items:center;justify-content:center;font-size:22px;overflow:hidden}.mb-img img{width:100%;height:100%;-o-object-fit:cover;object-fit:cover;border-radius:7px}.mb-info{flex:1;min-width:0}.mb-headline{font-size:13px;font-weight:700;line-height:1.4;margin-bottom:4px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.mb-summary{font-size:11px;color:var(--muted);line-height:1.5;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.mb-meta{display:flex;align-items:center;gap:8px;margin-top:5px}.mb-source{font-size:10px;color:var(--acc)}.mb-time{font-size:10px;color:var(--muted)}.mb-cat-icon{font-size:20px}.mb-cat-name{font-size:14px;font-weight:800}.mb-cat-cnt{font-size:11px;color:var(--muted);margin-left:auto}.mb-empty{text-align:center;padding:30px;color:var(--muted);font-size:13px}.mb-loading{display:flex;align-items:center;justify-content:center;padding:60px;color:var(--muted);font-size:14px;gap:10px}.la-dot{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--ok);margin-right:4px;animation:pulse 1.5s infinite}.la-dot.idle{background:var(--muted);animation:none}.la-agent{font-size:11px;color:var(--muted)}.la-icon{flex-shrink:0;font-size:13px;margin-top:1px}.la-body{flex:1;min-width:0}.la-time{font-size:10px;color:var(--muted);flex-shrink:0;min-width:44px;text-align:right}.la-assistant{color:var(--text)}.la-thinking{color:#a07aff;font-style:italic;opacity:.75}.la-tool{color:#4af}.la-tool-result{color:var(--muted);font-size:11px}.la-tool-result.ok{color:var(--ok)}.la-tool-result.err{color:var(--danger)}.la-user{color:var(--warn)}.la-tool-name{font-weight:700;margin-right:4px}.la-trunc{color:var(--muted);font-size:10px;opacity:.6}.la-flow-wrap{display:flex;flex-direction:column;gap:6px}.la-groups{display:flex;flex-direction:column;gap:8px;margin-top:4px}.la-group{border:1px solid var(--line);border-radius:8px;background:var(--panel)}.la-group-hd{display:flex;align-items:center;justify-content:space-between;padding:6px 10px;border-bottom:1px solid var(--line);font-size:11px;color:var(--muted)}.la-group-hd .name{font-weight:700;color:var(--text)}.la-group-bd{display:flex;flex-direction:column;gap:4px;padding:6px}.sched-status{font-size:10px;color:var(--muted)}.sched-line{font-size:11px;color:var(--muted);display:flex;gap:12px;flex-wrap:wrap;margin-bottom:10px}.sched-actions{display:flex;gap:6px;flex-wrap:wrap}.sched-btn.warn:hover{border-color:#f5c842;color:#f5c842}.sched-btn.danger:hover{border-color:#ff5270;color:#ff5270}.btn-cancel{background:#8882;color:#888;border:1px solid #88888844}.btn-cancel:hover{background:#8884}.btn-action:disabled{opacity:.4;cursor:not-allowed}.todo-detail{display:none;padding:4px 10px 10px 36px;font-size:11px;line-height:1.6;color:var(--text);white-space:pre-wrap;word-break:break-word;border-top:1px solid var(--line);margin:0 6px;opacity:.85}.todo-item.expanded .todo-detail{display:block}.todo-item .t-expand{color:var(--muted);font-size:10px;transition:transform .2s;flex-shrink:0}.todo-item.expanded .t-expand{transform:rotate(90deg)}.todo-item .t-id{color:var(--muted);font-size:10px;min-width:20px}.todo-item.has-detail .t-row{cursor:pointer}.act-label{color:var(--muted);flex-shrink:0}.act-dot{display:inline-flex;align-items:center;gap:5px;padding:3px 8px;border-radius:999px;background:#0f1a38;border:1px solid #1e2e50;margin:2px}.act-dot.alive{border-color:#2ecc8a44;background:#0a2018;color:var(--ok)}.act-dot.warn{border-color:#f5c84244;background:#201a08;color:var(--warn)}.act-dot.idle{color:var(--muted)}.orl-medal{font-size:16px;min-width:20px;text-align:center}.orl-emoji{font-size:18px}.orl-name{flex:1}.orl-role{font-size:12px;font-weight:700}.orl-org{font-size:10px;color:var(--muted)}.orl-score{font-size:11px;font-weight:700;color:var(--acc)}.orl-hbdot{width:8px;height:8px;border-radius:50%;flex-shrink:0}.orl-hbdot.active{background:var(--ok)}.orl-hbdot.warn{background:var(--warn)}.orl-hbdot.stalled{background:var(--danger);animation:pulse 1.2s infinite}.orl-hbdot.idle{background:#2a3a5a}.od-empty{display:flex;align-items:center;justify-content:center;height:100%;color:var(--muted);font-size:13px;min-height:200px}.od-hero{display:flex;align-items:center;gap:16px;margin-bottom:20px;padding-bottom:16px;border-bottom:1px solid var(--line)}.od-emoji{font-size:40px}.od-name{font-size:22px;font-weight:800}.od-role{font-size:13px;color:var(--muted);margin-top:2px}.od-rank-badge{font-size:11px;padding:3px 9px;border-radius:999px;border:1px solid #f5c84244;color:#f5c842;background:#201a08;margin-top:4px;display:inline-block}.od-hb{margin-left:auto;text-align:right}.od-section{margin-bottom:18px}.od-sec-title{font-size:10px;font-weight:700;color:var(--muted);letter-spacing:.07em;text-transform:uppercase;margin-bottom:10px}.od-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}.ods{background:var(--panel2);border-radius:8px;padding:10px;text-align:center}.ods-v{font-size:20px;font-weight:800}.ods-l{font-size:10px;color:var(--muted);margin-top:2px}.tbar{margin-bottom:7px}.tbar-hdr{display:flex;justify-content:space-between;font-size:11px;margin-bottom:3px}.tbar-label{color:var(--muted)}.tbar-val{font-weight:600}.tbar-track{height:6px;background:#0e1320;border-radius:3px;overflow:hidden}.tbar-fill{height:100%;border-radius:3px}.od-cost-row{display:flex;gap:10px;flex-wrap:wrap}.cost-chip{font-size:12px;padding:5px 12px;border-radius:8px;border:1px solid var(--line);background:var(--panel2)}.cost-chip b{font-size:15px}.cost-chip.hi{border-color:#ff527044}.cost-chip.md{border-color:#f5c84244}.cost-chip.lo{border-color:#2ecc8a44}.od-edict-list{display:flex;flex-direction:column;gap:5px}.oe-item{display:flex;align-items:center;gap:8px;padding:7px 10px;background:var(--panel2);border-radius:7px;font-size:12px;cursor:pointer}.oe-item:hover{background:#141c30}.oe-id{font-size:10px;color:var(--acc);font-weight:700;min-width:110px}.oe-title{flex:1;color:var(--text)}.oe-state{font-size:10px}.kpi-v.gold{color:#f5c842}.kpi-v.green{color:var(--ok)}.kpi-v.blue{color:var(--acc)}.kpi-v.warn{color:var(--warn)}.sub-cats{display:flex;flex-wrap:wrap;gap:8px}.sub-cat{display:flex;align-items:center;gap:6px;padding:7px 12px;border-radius:8px;border:1px solid var(--line);background:var(--panel2);cursor:pointer;transition:all .15s;-webkit-user-select:none;-moz-user-select:none;user-select:none}.sub-cat:hover{border-color:var(--acc)}.sub-cat.active{border-color:var(--ok);background:#0a2018}.sub-cat .sc-check{width:16px;height:16px;border-radius:4px;border:1px solid var(--line);display:flex;align-items:center;justify-content:center;font-size:10px;transition:all .15s}.sub-cat.active .sc-check{background:var(--ok);border-color:var(--ok);color:#000}.sub-cat .sc-label{font-size:12px;font-weight:600}.sub-cat .sc-count{font-size:10px;color:var(--muted)}.sub-kw-list{display:flex;flex-wrap:wrap;gap:6px}.sub-kw{display:flex;align-items:center;gap:4px;padding:4px 8px 4px 10px;border-radius:999px;background:#0f1a38;border:1px solid #1e2e50;font-size:11px;color:var(--acc)}.sub-kw .kw-del{cursor:pointer;opacity:.5;font-size:13px;padding:0 2px}.sub-kw .kw-del:hover{opacity:1;color:var(--danger)}.sub-feed-list{display:flex;flex-direction:column;gap:4px}.sub-feed{display:flex;align-items:center;gap:8px;padding:6px 10px;background:var(--panel2);border-radius:7px;font-size:12px}.sub-feed .sf-name{font-weight:600;min-width:80px;color:var(--acc)}.sub-feed .sf-url{flex:1;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sub-feed .sf-cat{font-size:10px;padding:2px 6px;border-radius:4px;border:1px solid var(--line)}.sub-feed .sf-del{cursor:pointer;color:var(--muted);font-size:14px}.sub-feed .sf-del:hover{color:var(--danger)}.as-card .as-emoji{font-size:22px;margin-bottom:3px}.as-card .as-label{font-size:12px;font-weight:700}.as-card .as-role{font-size:10px;color:var(--muted)}.as-card .as-status{font-size:10px;margin-top:4px}.as-card .as-time{font-size:9px;color:var(--muted);margin-top:2px}.as-wake-btn:disabled{opacity:.4;cursor:not-allowed}.as-refresh{font-size:11px;padding:4px 12px;border-radius:8px;border:1px solid var(--line);color:var(--muted);background:transparent;cursor:pointer;transition:background .15s}.as-refresh:hover{background:var(--panel2);color:var(--text)}.as-wake-all{font-size:11px;padding:4px 12px;border-radius:8px;border:1px solid var(--warn);color:var(--warn);background:transparent;cursor:pointer;transition:background .15s;margin-left:6px}.as-wake-all:hover{background:var(--warn);color:#fff}.as-summary span{display:flex;align-items:center;gap:4px}.archive-bar .ab-archive-all{font-size:11px;padding:4px 12px;border-radius:6px;border:1px solid #2ecc8a44;background:transparent;cursor:pointer;color:var(--ok);font-weight:600;transition:all .15s}.archive-bar .ab-archive-all:hover{background:#0a2018;border-color:var(--ok)}.archive-bar .ab-scan-detail{font-size:11px;padding:4px 10px;border-radius:6px;border:1px solid var(--line);background:transparent;cursor:pointer;color:var(--muted);font-weight:600;transition:all .15s}.archive-bar .ab-scan-detail:hover{border-color:var(--acc);color:var(--text)}.archive-bar .ab-scan-detail.active{border-color:var(--acc);color:var(--acc);background:#0f1a38}.archive-bar .ab-scan-copy{font-size:11px;padding:4px 10px;border-radius:6px;border:1px solid #2ecc8a44;background:transparent;cursor:pointer;color:var(--ok);font-weight:600;transition:all .15s}.archive-bar .ab-scan-copy:hover{background:#0a2018;border-color:var(--ok)}.global-scan-detail{display:none;margin-top:-4px;margin-bottom:12px;background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:10px 12px}.global-scan-detail.open{display:block}.global-scan-detail .gs-empty{font-size:11px;color:var(--muted)}.global-scan-detail .gs-list{display:flex;flex-direction:column;gap:6px}.global-scan-detail .gs-item{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:8px;background:var(--panel);border:1px solid var(--line);font-size:11px}.global-scan-detail .gs-tag{font-size:10px;border-radius:10px;padding:2px 8px;font-weight:700;border:1px solid var(--line);color:var(--muted)}.global-scan-detail .gs-tag.retry{color:var(--acc);border-color:#6a9eff55}.global-scan-detail .gs-tag.escalate{color:#f5c842;border-color:#f5c84255}.global-scan-detail .gs-tag.rollback{color:#ff5270;border-color:#ff527055}.global-scan-detail .gs-task{font-weight:700;color:var(--text)}.global-scan-detail .gs-meta{color:var(--muted)}.global-scan-detail .gs-hint{margin-top:8px;font-size:10px;color:var(--muted)}.confirm-reason{width:100%;background:var(--panel2);border:1px solid var(--line);border-radius:7px;color:var(--text);padding:8px 10px;font-size:12px;outline:none;margin-bottom:14px;resize:vertical;min-height:60px}.confirm-reason:focus{border-color:var(--acc)}.empty{text-align:center;padding:40px 20px;color:var(--muted);font-size:13px}.sec-title{font-size:12px;font-weight:700;color:var(--muted);letter-spacing:.07em;text-transform:uppercase;margin-bottom:12px}code{font-size:11px;background:var(--panel2);padding:2px 6px;border-radius:4px;font-family:monospace} + + + +(function(){const p=document.createElement("link").relList;if(p&&p.supports&&p.supports("modulepreload"))return;for(const S of document.querySelectorAll('link[rel="modulepreload"]'))k(S);new MutationObserver(S=>{for(const D of S)if(D.type==="childList")for(const F of D.addedNodes)F.tagName==="LINK"&&F.rel==="modulepreload"&&k(F)}).observe(document,{childList:!0,subtree:!0});function u(S){const D={};return S.integrity&&(D.integrity=S.integrity),S.referrerPolicy&&(D.referrerPolicy=S.referrerPolicy),S.crossOrigin==="use-credentials"?D.credentials="include":S.crossOrigin==="anonymous"?D.credentials="omit":D.credentials="same-origin",D}function k(S){if(S.ep)return;S.ep=!0;const D=u(S);fetch(S.href,D)}})();function Qi(o){return o&&o.__esModule&&Object.prototype.hasOwnProperty.call(o,"default")?o.default:o}var Mi={exports:{}},Cr={},Ai={exports:{}},de={};/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Hu;function df(){if(Hu)return de;Hu=1;var o=Symbol.for("react.element"),p=Symbol.for("react.portal"),u=Symbol.for("react.fragment"),k=Symbol.for("react.strict_mode"),S=Symbol.for("react.profiler"),D=Symbol.for("react.provider"),F=Symbol.for("react.context"),_=Symbol.for("react.forward_ref"),R=Symbol.for("react.suspense"),C=Symbol.for("react.memo"),z=Symbol.for("react.lazy"),d=Symbol.iterator;function w(v){return v===null||typeof v!="object"?null:(v=d&&v[d]||v["@@iterator"],typeof v=="function"?v:null)}var m={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},T=Object.assign,g={};function M(v,L,ae){this.props=v,this.context=L,this.refs=g,this.updater=ae||m}M.prototype.isReactComponent={},M.prototype.setState=function(v,L){if(typeof v!="object"&&typeof v!="function"&&v!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,v,L,"setState")},M.prototype.forceUpdate=function(v){this.updater.enqueueForceUpdate(this,v,"forceUpdate")};function P(){}P.prototype=M.prototype;function B(v,L,ae){this.props=v,this.context=L,this.refs=g,this.updater=ae||m}var Z=B.prototype=new P;Z.constructor=B,T(Z,M.prototype),Z.isPureReactComponent=!0;var I=Array.isArray,oe=Object.prototype.hasOwnProperty,ue={current:null},pe={key:!0,ref:!0,__self:!0,__source:!0};function me(v,L,ae){var ce,x={},U=null,ee=null;if(L!=null)for(ce in L.ref!==void 0&&(ee=L.ref),L.key!==void 0&&(U=""+L.key),L)oe.call(L,ce)&&!pe.hasOwnProperty(ce)&&(x[ce]=L[ce]);var he=arguments.length-2;if(he===1)x.children=ae;else if(1>>1,L=E[v];if(0>>1;vS(x,W))US(ee,x)?(E[v]=ee,E[U]=W,v=U):(E[v]=x,E[ce]=W,v=ce);else if(US(ee,W))E[v]=ee,E[U]=W,v=U;else break e}}return Q}function S(E,Q){var W=E.sortIndex-Q.sortIndex;return W!==0?W:E.id-Q.id}if(typeof performance=="object"&&typeof performance.now=="function"){var D=performance;o.unstable_now=function(){return D.now()}}else{var F=Date,_=F.now();o.unstable_now=function(){return F.now()-_}}var R=[],C=[],z=1,d=null,w=3,m=!1,T=!1,g=!1,M=typeof setTimeout=="function"?setTimeout:null,P=typeof clearTimeout=="function"?clearTimeout:null,B=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function Z(E){for(var Q=u(C);Q!==null;){if(Q.callback===null)k(C);else if(Q.startTime<=E)k(C),Q.sortIndex=Q.expirationTime,p(R,Q);else break;Q=u(C)}}function I(E){if(g=!1,Z(E),!T)if(u(R)!==null)T=!0,_e(oe);else{var Q=u(C);Q!==null&&xe(I,Q.startTime-E)}}function oe(E,Q){T=!1,g&&(g=!1,P(me),me=-1),m=!0;var W=w;try{for(Z(Q),d=u(R);d!==null&&(!(d.expirationTime>Q)||E&&!le());){var v=d.callback;if(typeof v=="function"){d.callback=null,w=d.priorityLevel;var L=v(d.expirationTime<=Q);Q=o.unstable_now(),typeof L=="function"?d.callback=L:d===u(R)&&k(R),Z(Q)}else k(R);d=u(R)}if(d!==null)var ae=!0;else{var ce=u(C);ce!==null&&xe(I,ce.startTime-Q),ae=!1}return ae}finally{d=null,w=W,m=!1}}var ue=!1,pe=null,me=-1,be=5,X=-1;function le(){return!(o.unstable_now()-XE||125v?(E.sortIndex=W,p(C,E),u(R)===null&&E===u(C)&&(g?(P(me),me=-1):g=!0,xe(I,W-v))):(E.sortIndex=L,p(R,E),T||m||(T=!0,_e(oe))),E},o.unstable_shouldYield=le,o.unstable_wrapCallback=function(E){var Q=w;return function(){var W=w;w=Q;try{return E.apply(this,arguments)}finally{w=W}}}})($i)),$i}var Yu;function hf(){return Yu||(Yu=1,bi.exports=mf()),bi.exports}/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Xu;function vf(){if(Xu)return nt;Xu=1;var o=_r(),p=hf();function u(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),R=Object.prototype.hasOwnProperty,C=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,z={},d={};function w(e){return R.call(d,e)?!0:R.call(z,e)?!1:C.test(e)?d[e]=!0:(z[e]=!0,!1)}function m(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function T(e,t,n,r){if(t===null||typeof t>"u"||m(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function g(e,t,n,r,s,i,a){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=s,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=i,this.removeEmptyString=a}var M={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){M[e]=new g(e,0,!1,e,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];M[t]=new g(t,1,!1,e[1],null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(e){M[e]=new g(e,2,!1,e.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){M[e]=new g(e,2,!1,e,null,!1,!1)}),"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){M[e]=new g(e,3,!1,e.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(e){M[e]=new g(e,3,!0,e,null,!1,!1)}),["capture","download"].forEach(function(e){M[e]=new g(e,4,!1,e,null,!1,!1)}),["cols","rows","size","span"].forEach(function(e){M[e]=new g(e,6,!1,e,null,!1,!1)}),["rowSpan","start"].forEach(function(e){M[e]=new g(e,5,!1,e.toLowerCase(),null,!1,!1)});var P=/[\-:]([a-z])/g;function B(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(P,B);M[t]=new g(t,1,!1,e,null,!1,!1)}),"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(P,B);M[t]=new g(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(P,B);M[t]=new g(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(e){M[e]=new g(e,1,!1,e.toLowerCase(),null,!1,!1)}),M.xlinkHref=new g("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(e){M[e]=new g(e,1,!1,e.toLowerCase(),null,!0,!0)});function Z(e,t,n,r){var s=M.hasOwnProperty(t)?M[t]:null;(s!==null?s.type!==0:r||!(2c||s[a]!==i[c]){var f=` +`+s[a].replace(" at new "," at ");return e.displayName&&f.includes("")&&(f=f.replace("",e.displayName)),f}while(1<=a&&0<=c);break}}}finally{ae=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?L(e):""}function x(e){switch(e.tag){case 5:return L(e.type);case 16:return L("Lazy");case 13:return L("Suspense");case 19:return L("SuspenseList");case 0:case 2:case 15:return e=ce(e.type,!1),e;case 11:return e=ce(e.type.render,!1),e;case 1:return e=ce(e.type,!0),e;default:return""}}function U(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case pe:return"Fragment";case ue:return"Portal";case be:return"Profiler";case me:return"StrictMode";case se:return"Suspense";case J:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case le:return(e.displayName||"Context")+".Consumer";case X:return(e._context.displayName||"Context")+".Provider";case ke:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case Ne:return t=e.displayName||null,t!==null?t:U(e.type)||"Memo";case _e:t=e._payload,e=e._init;try{return U(e(t))}catch{}}return null}function ee(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return U(t);case 8:return t===me?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function he(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function ve(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Me(e){var t=ve(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var s=n.get,i=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return s.call(this)},set:function(a){r=""+a,i.call(this,a)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(a){r=""+a},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function mn(e){e._valueTracker||(e._valueTracker=Me(e))}function Yi(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=ve(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Tr(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function Wl(e,t){var n=t.checked;return W({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function Xi(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=he(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function Zi(e,t){t=t.checked,t!=null&&Z(e,"checked",t,!1)}function Ul(e,t){Zi(e,t);var n=he(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?Hl(e,t.type,n):t.hasOwnProperty("defaultValue")&&Hl(e,t.type,he(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function qi(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function Hl(e,t,n){(t!=="number"||Tr(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var Fn=Array.isArray;function hn(e,t,n,r){if(e=e.options,t){t={};for(var s=0;s"+t.valueOf().toString()+"",t=Lr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Bn(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Wn={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},mc=["Webkit","ms","Moz","O"];Object.keys(Wn).forEach(function(e){mc.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Wn[t]=Wn[e]})});function lo(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Wn.hasOwnProperty(e)&&Wn[e]?(""+t).trim():t+"px"}function so(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,s=lo(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,s):e[n]=s}}var hc=W({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function Kl(e,t){if(t){if(hc[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(u(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(u(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(u(61))}if(t.style!=null&&typeof t.style!="object")throw Error(u(62))}}function Gl(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var Yl=null;function Xl(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var Zl=null,vn=null,gn=null;function io(e){if(e=cr(e)){if(typeof Zl!="function")throw Error(u(280));var t=e.stateNode;t&&(t=Jr(t),Zl(e.stateNode,e.type,t))}}function oo(e){vn?gn?gn.push(e):gn=[e]:vn=e}function ao(){if(vn){var e=vn,t=gn;if(gn=vn=null,io(e),t)for(e=0;e>>=0,e===0?32:31-(Ec(e)/_c|0)|0}var Mr=64,Ar=4194304;function Qn(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Or(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,s=e.suspendedLanes,i=e.pingedLanes,a=n&268435455;if(a!==0){var c=a&~s;c!==0?r=Qn(c):(i&=a,i!==0&&(r=Qn(i)))}else a=n&~s,a!==0?r=Qn(a):i!==0&&(r=Qn(i));if(r===0)return 0;if(t!==0&&t!==r&&(t&s)===0&&(s=r&-r,i=t&-t,s>=i||s===16&&(i&4194240)!==0))return t;if((r&4)!==0&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function Kn(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-pt(t),e[t]=n}function Rc(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=tr),Oo=" ",bo=!1;function $o(e,t){switch(e){case"keyup":return sd.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Fo(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var kn=!1;function od(e,t){switch(e){case"compositionend":return Fo(t);case"keypress":return t.which!==32?null:(bo=!0,Oo);case"textInput":return e=t.data,e===Oo&&bo?null:e;default:return null}}function ad(e,t){if(kn)return e==="compositionend"||!hs&&$o(e,t)?(e=Ro(),Wr=us=bt=null,kn=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=Ko(n)}}function Yo(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Yo(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Xo(){for(var e=window,t=Tr();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=Tr(e.document)}return t}function ys(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function gd(e){var t=Xo(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Yo(n.ownerDocument.documentElement,n)){if(r!==null&&ys(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var s=n.textContent.length,i=Math.min(r.start,s);r=r.end===void 0?i:Math.min(r.end,s),!e.extend&&i>r&&(s=r,r=i,i=s),s=Go(n,i);var a=Go(n,r);s&&a&&(e.rangeCount!==1||e.anchorNode!==s.node||e.anchorOffset!==s.offset||e.focusNode!==a.node||e.focusOffset!==a.offset)&&(t=t.createRange(),t.setStart(s.node,s.offset),e.removeAllRanges(),i>r?(e.addRange(t),e.extend(a.node,a.offset)):(t.setEnd(a.node,a.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,jn=null,xs=null,sr=null,ks=!1;function Zo(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;ks||jn==null||jn!==Tr(r)||(r=jn,"selectionStart"in r&&ys(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),sr&&lr(sr,r)||(sr=r,r=Xr(xs,"onSelect"),0En||(e.current=Ps[En],Ps[En]=null,En--)}function we(e,t){En++,Ps[En]=e.current,e.current=t}var Wt={},Ve=Bt(Wt),Ze=Bt(!1),nn=Wt;function _n(e,t){var n=e.type.contextTypes;if(!n)return Wt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var s={},i;for(i in n)s[i]=t[i];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=s),s}function qe(e){return e=e.childContextTypes,e!=null}function el(){Ee(Ze),Ee(Ve)}function fa(e,t,n){if(Ve.current!==Wt)throw Error(u(168));we(Ve,t),we(Ze,n)}function pa(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var s in r)if(!(s in t))throw Error(u(108,ee(e)||"Unknown",s));return W({},n,r)}function tl(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Wt,nn=Ve.current,we(Ve,e),we(Ze,Ze.current),!0}function ma(e,t,n){var r=e.stateNode;if(!r)throw Error(u(169));n?(e=pa(e,t,nn),r.__reactInternalMemoizedMergedChildContext=e,Ee(Ze),Ee(Ve),we(Ve,e)):Ee(Ze),we(Ze,n)}var _t=null,nl=!1,Is=!1;function ha(e){_t===null?_t=[e]:_t.push(e)}function Td(e){nl=!0,ha(e)}function Ut(){if(!Is&&_t!==null){Is=!0;var e=0,t=je;try{var n=_t;for(je=1;e>=a,s-=a,zt=1<<32-pt(t)+s|n<ie?(Be=re,re=null):Be=re.sibling;var ye=A(y,re,j[ie],$);if(ye===null){re===null&&(re=Be);break}e&&re&&ye.alternate===null&&t(y,re),h=i(ye,h,ie),ne===null?Y=ye:ne.sibling=ye,ne=ye,re=Be}if(ie===j.length)return n(y,re),ze&&ln(y,ie),Y;if(re===null){for(;ieie?(Be=re,re=null):Be=re.sibling;var qt=A(y,re,ye.value,$);if(qt===null){re===null&&(re=Be);break}e&&re&&qt.alternate===null&&t(y,re),h=i(qt,h,ie),ne===null?Y=qt:ne.sibling=qt,ne=qt,re=Be}if(ye.done)return n(y,re),ze&&ln(y,ie),Y;if(re===null){for(;!ye.done;ie++,ye=j.next())ye=b(y,ye.value,$),ye!==null&&(h=i(ye,h,ie),ne===null?Y=ye:ne.sibling=ye,ne=ye);return ze&&ln(y,ie),Y}for(re=r(y,re);!ye.done;ie++,ye=j.next())ye=H(re,y,ie,ye.value,$),ye!==null&&(e&&ye.alternate!==null&&re.delete(ye.key===null?ie:ye.key),h=i(ye,h,ie),ne===null?Y=ye:ne.sibling=ye,ne=ye);return e&&re.forEach(function(cf){return t(y,cf)}),ze&&ln(y,ie),Y}function Ie(y,h,j,$){if(typeof j=="object"&&j!==null&&j.type===pe&&j.key===null&&(j=j.props.children),typeof j=="object"&&j!==null){switch(j.$$typeof){case oe:e:{for(var Y=j.key,ne=h;ne!==null;){if(ne.key===Y){if(Y=j.type,Y===pe){if(ne.tag===7){n(y,ne.sibling),h=s(ne,j.props.children),h.return=y,y=h;break e}}else if(ne.elementType===Y||typeof Y=="object"&&Y!==null&&Y.$$typeof===_e&&ja(Y)===ne.type){n(y,ne.sibling),h=s(ne,j.props),h.ref=dr(y,ne,j),h.return=y,y=h;break e}n(y,ne);break}else t(y,ne);ne=ne.sibling}j.type===pe?(h=pn(j.props.children,y.mode,$,j.key),h.return=y,y=h):($=Ll(j.type,j.key,j.props,null,y.mode,$),$.ref=dr(y,h,j),$.return=y,y=$)}return a(y);case ue:e:{for(ne=j.key;h!==null;){if(h.key===ne)if(h.tag===4&&h.stateNode.containerInfo===j.containerInfo&&h.stateNode.implementation===j.implementation){n(y,h.sibling),h=s(h,j.children||[]),h.return=y,y=h;break e}else{n(y,h);break}else t(y,h);h=h.sibling}h=Li(j,y.mode,$),h.return=y,y=h}return a(y);case _e:return ne=j._init,Ie(y,h,ne(j._payload),$)}if(Fn(j))return K(y,h,j,$);if(Q(j))return G(y,h,j,$);il(y,j)}return typeof j=="string"&&j!==""||typeof j=="number"?(j=""+j,h!==null&&h.tag===6?(n(y,h.sibling),h=s(h,j),h.return=y,y=h):(n(y,h),h=Ti(j,y.mode,$),h.return=y,y=h),a(y)):n(y,h)}return Ie}var Rn=Sa(!0),wa=Sa(!1),ol=Bt(null),al=null,Pn=null,$s=null;function Fs(){$s=Pn=al=null}function Bs(e){var t=ol.current;Ee(ol),e._currentValue=t}function Ws(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function In(e,t){al=e,$s=Pn=null,e=e.dependencies,e!==null&&e.firstContext!==null&&((e.lanes&t)!==0&&(Je=!0),e.firstContext=null)}function ut(e){var t=e._currentValue;if($s!==e)if(e={context:e,memoizedValue:t,next:null},Pn===null){if(al===null)throw Error(u(308));Pn=e,al.dependencies={lanes:0,firstContext:e}}else Pn=Pn.next=e;return t}var sn=null;function Us(e){sn===null?sn=[e]:sn.push(e)}function Na(e,t,n,r){var s=t.interleaved;return s===null?(n.next=n,Us(t)):(n.next=s.next,s.next=n),t.interleaved=n,Lt(e,r)}function Lt(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var Ht=!1;function Hs(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Ca(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Rt(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function Vt(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,(ge&2)!==0){var s=r.pending;return s===null?t.next=t:(t.next=s.next,s.next=t),r.pending=t,Lt(e,n)}return s=r.interleaved,s===null?(t.next=t,Us(r)):(t.next=s.next,s.next=t),r.interleaved=t,Lt(e,n)}function ul(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,ls(e,n)}}function Ea(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var s=null,i=null;if(n=n.firstBaseUpdate,n!==null){do{var a={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};i===null?s=i=a:i=i.next=a,n=n.next}while(n!==null);i===null?s=i=t:i=i.next=t}else s=i=t;n={baseState:r.baseState,firstBaseUpdate:s,lastBaseUpdate:i,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function cl(e,t,n,r){var s=e.updateQueue;Ht=!1;var i=s.firstBaseUpdate,a=s.lastBaseUpdate,c=s.shared.pending;if(c!==null){s.shared.pending=null;var f=c,N=f.next;f.next=null,a===null?i=N:a.next=N,a=f;var O=e.alternate;O!==null&&(O=O.updateQueue,c=O.lastBaseUpdate,c!==a&&(c===null?O.firstBaseUpdate=N:c.next=N,O.lastBaseUpdate=f))}if(i!==null){var b=s.baseState;a=0,O=N=f=null,c=i;do{var A=c.lane,H=c.eventTime;if((r&A)===A){O!==null&&(O=O.next={eventTime:H,lane:0,tag:c.tag,payload:c.payload,callback:c.callback,next:null});e:{var K=e,G=c;switch(A=t,H=n,G.tag){case 1:if(K=G.payload,typeof K=="function"){b=K.call(H,b,A);break e}b=K;break e;case 3:K.flags=K.flags&-65537|128;case 0:if(K=G.payload,A=typeof K=="function"?K.call(H,b,A):K,A==null)break e;b=W({},b,A);break e;case 2:Ht=!0}}c.callback!==null&&c.lane!==0&&(e.flags|=64,A=s.effects,A===null?s.effects=[c]:A.push(c))}else H={eventTime:H,lane:A,tag:c.tag,payload:c.payload,callback:c.callback,next:null},O===null?(N=O=H,f=b):O=O.next=H,a|=A;if(c=c.next,c===null){if(c=s.shared.pending,c===null)break;A=c,c=A.next,A.next=null,s.lastBaseUpdate=A,s.shared.pending=null}}while(!0);if(O===null&&(f=b),s.baseState=f,s.firstBaseUpdate=N,s.lastBaseUpdate=O,t=s.shared.interleaved,t!==null){s=t;do a|=s.lane,s=s.next;while(s!==t)}else i===null&&(s.shared.lanes=0);un|=a,e.lanes=a,e.memoizedState=b}}function _a(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=Ys.transition;Ys.transition={};try{e(!1),t()}finally{je=n,Ys.transition=r}}function Qa(){return ct().memoizedState}function Id(e,t,n){var r=Yt(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},Ka(e))Ga(t,n);else if(n=Na(e,t,n,r),n!==null){var s=Xe();xt(n,e,r,s),Ya(n,t,r)}}function Dd(e,t,n){var r=Yt(e),s={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(Ka(e))Ga(t,s);else{var i=e.alternate;if(e.lanes===0&&(i===null||i.lanes===0)&&(i=t.lastRenderedReducer,i!==null))try{var a=t.lastRenderedState,c=i(a,n);if(s.hasEagerState=!0,s.eagerState=c,mt(c,a)){var f=t.interleaved;f===null?(s.next=s,Us(t)):(s.next=f.next,f.next=s),t.interleaved=s;return}}catch{}finally{}n=Na(e,t,s,r),n!==null&&(s=Xe(),xt(n,e,r,s),Ya(n,t,r))}}function Ka(e){var t=e.alternate;return e===Le||t!==null&&t===Le}function Ga(e,t){hr=pl=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Ya(e,t,n){if((n&4194240)!==0){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,ls(e,n)}}var vl={readContext:ut,useCallback:Qe,useContext:Qe,useEffect:Qe,useImperativeHandle:Qe,useInsertionEffect:Qe,useLayoutEffect:Qe,useMemo:Qe,useReducer:Qe,useRef:Qe,useState:Qe,useDebugValue:Qe,useDeferredValue:Qe,useTransition:Qe,useMutableSource:Qe,useSyncExternalStore:Qe,useId:Qe,unstable_isNewReconciler:!1},Md={readContext:ut,useCallback:function(e,t){return Nt().memoizedState=[e,t===void 0?null:t],e},useContext:ut,useEffect:ba,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,ml(4194308,4,Ba.bind(null,t,e),n)},useLayoutEffect:function(e,t){return ml(4194308,4,e,t)},useInsertionEffect:function(e,t){return ml(4,2,e,t)},useMemo:function(e,t){var n=Nt();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Nt();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=Id.bind(null,Le,e),[r.memoizedState,e]},useRef:function(e){var t=Nt();return e={current:e},t.memoizedState=e},useState:Aa,useDebugValue:ni,useDeferredValue:function(e){return Nt().memoizedState=e},useTransition:function(){var e=Aa(!1),t=e[0];return e=Pd.bind(null,e[1]),Nt().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=Le,s=Nt();if(ze){if(n===void 0)throw Error(u(407));n=n()}else{if(n=t(),Fe===null)throw Error(u(349));(an&30)!==0||Ra(r,t,n)}s.memoizedState=n;var i={value:n,getSnapshot:t};return s.queue=i,ba(Ia.bind(null,r,i,e),[e]),r.flags|=2048,yr(9,Pa.bind(null,r,i,n,t),void 0,null),n},useId:function(){var e=Nt(),t=Fe.identifierPrefix;if(ze){var n=Tt,r=zt;n=(r&~(1<<32-pt(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=vr++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=a.createElement(n,{is:r.is}):(e=a.createElement(n),n==="select"&&(a=e,r.multiple?a.multiple=!0:r.size&&(a.size=r.size))):e=a.createElementNS(e,n),e[St]=t,e[ur]=r,hu(e,t,!1,!1),t.stateNode=e;e:{switch(a=Gl(n,r),n){case"dialog":Ce("cancel",e),Ce("close",e),s=r;break;case"iframe":case"object":case"embed":Ce("load",e),s=r;break;case"video":case"audio":for(s=0;sbn&&(t.flags|=128,r=!0,xr(i,!1),t.lanes=4194304)}else{if(!r)if(e=dl(a),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),xr(i,!0),i.tail===null&&i.tailMode==="hidden"&&!a.alternate&&!ze)return Ke(t),null}else 2*Pe()-i.renderingStartTime>bn&&n!==1073741824&&(t.flags|=128,r=!0,xr(i,!1),t.lanes=4194304);i.isBackwards?(a.sibling=t.child,t.child=a):(n=i.last,n!==null?n.sibling=a:t.child=a,i.last=a)}return i.tail!==null?(t=i.tail,i.rendering=t,i.tail=t.sibling,i.renderingStartTime=Pe(),t.sibling=null,n=Te.current,we(Te,r?n&1|2:n&1),t):(Ke(t),null);case 22:case 23:return Ei(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&(t.mode&1)!==0?(it&1073741824)!==0&&(Ke(t),t.subtreeFlags&6&&(t.flags|=8192)):Ke(t),null;case 24:return null;case 25:return null}throw Error(u(156,t.tag))}function Ud(e,t){switch(Ms(t),t.tag){case 1:return qe(t.type)&&el(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Dn(),Ee(Ze),Ee(Ve),Gs(),e=t.flags,(e&65536)!==0&&(e&128)===0?(t.flags=e&-65537|128,t):null;case 5:return Qs(t),null;case 13:if(Ee(Te),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(u(340));Ln()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return Ee(Te),null;case 4:return Dn(),null;case 10:return Bs(t.type._context),null;case 22:case 23:return Ei(),null;case 24:return null;default:return null}}var kl=!1,Ge=!1,Hd=typeof WeakSet=="function"?WeakSet:Set,V=null;function An(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){Re(e,t,r)}else n.current=null}function mi(e,t,n){try{n()}catch(r){Re(e,t,r)}}var yu=!1;function Vd(e,t){if(Es=Fr,e=Xo(),ys(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var s=r.anchorOffset,i=r.focusNode;r=r.focusOffset;try{n.nodeType,i.nodeType}catch{n=null;break e}var a=0,c=-1,f=-1,N=0,O=0,b=e,A=null;t:for(;;){for(var H;b!==n||s!==0&&b.nodeType!==3||(c=a+s),b!==i||r!==0&&b.nodeType!==3||(f=a+r),b.nodeType===3&&(a+=b.nodeValue.length),(H=b.firstChild)!==null;)A=b,b=H;for(;;){if(b===e)break t;if(A===n&&++N===s&&(c=a),A===i&&++O===r&&(f=a),(H=b.nextSibling)!==null)break;b=A,A=b.parentNode}b=H}n=c===-1||f===-1?null:{start:c,end:f}}else n=null}n=n||{start:0,end:0}}else n=null;for(_s={focusedElem:e,selectionRange:n},Fr=!1,V=t;V!==null;)if(t=V,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,V=e;else for(;V!==null;){t=V;try{var K=t.alternate;if((t.flags&1024)!==0)switch(t.tag){case 0:case 11:case 15:break;case 1:if(K!==null){var G=K.memoizedProps,Ie=K.memoizedState,y=t.stateNode,h=y.getSnapshotBeforeUpdate(t.elementType===t.type?G:vt(t.type,G),Ie);y.__reactInternalSnapshotBeforeUpdate=h}break;case 3:var j=t.stateNode.containerInfo;j.nodeType===1?j.textContent="":j.nodeType===9&&j.documentElement&&j.removeChild(j.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(u(163))}}catch($){Re(t,t.return,$)}if(e=t.sibling,e!==null){e.return=t.return,V=e;break}V=t.return}return K=yu,yu=!1,K}function kr(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var s=r=r.next;do{if((s.tag&e)===e){var i=s.destroy;s.destroy=void 0,i!==void 0&&mi(t,n,i)}s=s.next}while(s!==r)}}function jl(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function hi(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function xu(e){var t=e.alternate;t!==null&&(e.alternate=null,xu(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[St],delete t[ur],delete t[Rs],delete t[_d],delete t[zd])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function ku(e){return e.tag===5||e.tag===3||e.tag===4}function ju(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||ku(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function vi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=qr));else if(r!==4&&(e=e.child,e!==null))for(vi(e,t,n),e=e.sibling;e!==null;)vi(e,t,n),e=e.sibling}function gi(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(gi(e,t,n),e=e.sibling;e!==null;)gi(e,t,n),e=e.sibling}var Ue=null,gt=!1;function Qt(e,t,n){for(n=n.child;n!==null;)Su(e,t,n),n=n.sibling}function Su(e,t,n){if(jt&&typeof jt.onCommitFiberUnmount=="function")try{jt.onCommitFiberUnmount(Dr,n)}catch{}switch(n.tag){case 5:Ge||An(n,t);case 6:var r=Ue,s=gt;Ue=null,Qt(e,t,n),Ue=r,gt=s,Ue!==null&&(gt?(e=Ue,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):Ue.removeChild(n.stateNode));break;case 18:Ue!==null&&(gt?(e=Ue,n=n.stateNode,e.nodeType===8?Ls(e.parentNode,n):e.nodeType===1&&Ls(e,n),qn(e)):Ls(Ue,n.stateNode));break;case 4:r=Ue,s=gt,Ue=n.stateNode.containerInfo,gt=!0,Qt(e,t,n),Ue=r,gt=s;break;case 0:case 11:case 14:case 15:if(!Ge&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){s=r=r.next;do{var i=s,a=i.destroy;i=i.tag,a!==void 0&&((i&2)!==0||(i&4)!==0)&&mi(n,t,a),s=s.next}while(s!==r)}Qt(e,t,n);break;case 1:if(!Ge&&(An(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(c){Re(n,t,c)}Qt(e,t,n);break;case 21:Qt(e,t,n);break;case 22:n.mode&1?(Ge=(r=Ge)||n.memoizedState!==null,Qt(e,t,n),Ge=r):Qt(e,t,n);break;default:Qt(e,t,n)}}function wu(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new Hd),t.forEach(function(r){var s=ef.bind(null,e,r);n.has(r)||(n.add(r),r.then(s,s))})}}function yt(e,t){var n=t.deletions;if(n!==null)for(var r=0;rs&&(s=a),r&=~i}if(r=s,r=Pe()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Kd(r/1960))-r,10e?16:e,Gt===null)var r=!1;else{if(e=Gt,Gt=null,El=0,(ge&6)!==0)throw Error(u(331));var s=ge;for(ge|=4,V=e.current;V!==null;){var i=V,a=i.child;if((V.flags&16)!==0){var c=i.deletions;if(c!==null){for(var f=0;fPe()-ki?dn(e,0):xi|=n),tt(e,t)}function Au(e,t){t===0&&((e.mode&1)===0?t=1:(t=Ar,Ar<<=1,(Ar&130023424)===0&&(Ar=4194304)));var n=Xe();e=Lt(e,t),e!==null&&(Kn(e,t,n),tt(e,n))}function Jd(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Au(e,n)}function ef(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,s=e.memoizedState;s!==null&&(n=s.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(u(314))}r!==null&&r.delete(t),Au(e,n)}var Ou;Ou=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||Ze.current)Je=!0;else{if((e.lanes&n)===0&&(t.flags&128)===0)return Je=!1,Bd(e,t,n);Je=(e.flags&131072)!==0}else Je=!1,ze&&(t.flags&1048576)!==0&&va(t,ll,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;xl(e,t),e=t.pendingProps;var s=_n(t,Ve.current);In(t,n),s=Zs(null,t,r,e,s,n);var i=qs();return t.flags|=1,typeof s=="object"&&s!==null&&typeof s.render=="function"&&s.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,qe(r)?(i=!0,tl(t)):i=!1,t.memoizedState=s.state!==null&&s.state!==void 0?s.state:null,Hs(t),s.updater=gl,t.stateNode=s,s._reactInternals=t,li(t,r,e,n),t=ai(null,t,r,!0,i,n)):(t.tag=0,ze&&i&&Ds(t),Ye(null,t,s,n),t=t.child),t;case 16:r=t.elementType;e:{switch(xl(e,t),e=t.pendingProps,s=r._init,r=s(r._payload),t.type=r,s=t.tag=nf(r),e=vt(r,e),s){case 0:t=oi(null,t,r,e,n);break e;case 1:t=uu(null,t,r,e,n);break e;case 11:t=lu(null,t,r,e,n);break e;case 14:t=su(null,t,r,vt(r.type,e),n);break e}throw Error(u(306,r,""))}return t;case 0:return r=t.type,s=t.pendingProps,s=t.elementType===r?s:vt(r,s),oi(e,t,r,s,n);case 1:return r=t.type,s=t.pendingProps,s=t.elementType===r?s:vt(r,s),uu(e,t,r,s,n);case 3:e:{if(cu(t),e===null)throw Error(u(387));r=t.pendingProps,i=t.memoizedState,s=i.element,Ca(e,t),cl(t,r,null,n);var a=t.memoizedState;if(r=a.element,i.isDehydrated)if(i={element:r,isDehydrated:!1,cache:a.cache,pendingSuspenseBoundaries:a.pendingSuspenseBoundaries,transitions:a.transitions},t.updateQueue.baseState=i,t.memoizedState=i,t.flags&256){s=Mn(Error(u(423)),t),t=du(e,t,r,n,s);break e}else if(r!==s){s=Mn(Error(u(424)),t),t=du(e,t,r,n,s);break e}else for(st=Ft(t.stateNode.containerInfo.firstChild),lt=t,ze=!0,ht=null,n=wa(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Ln(),r===s){t=Pt(e,t,n);break e}Ye(e,t,r,n)}t=t.child}return t;case 5:return za(t),e===null&&Os(t),r=t.type,s=t.pendingProps,i=e!==null?e.memoizedProps:null,a=s.children,zs(r,s)?a=null:i!==null&&zs(r,i)&&(t.flags|=32),au(e,t),Ye(e,t,a,n),t.child;case 6:return e===null&&Os(t),null;case 13:return fu(e,t,n);case 4:return Vs(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=Rn(t,null,r,n):Ye(e,t,r,n),t.child;case 11:return r=t.type,s=t.pendingProps,s=t.elementType===r?s:vt(r,s),lu(e,t,r,s,n);case 7:return Ye(e,t,t.pendingProps,n),t.child;case 8:return Ye(e,t,t.pendingProps.children,n),t.child;case 12:return Ye(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,s=t.pendingProps,i=t.memoizedProps,a=s.value,we(ol,r._currentValue),r._currentValue=a,i!==null)if(mt(i.value,a)){if(i.children===s.children&&!Ze.current){t=Pt(e,t,n);break e}}else for(i=t.child,i!==null&&(i.return=t);i!==null;){var c=i.dependencies;if(c!==null){a=i.child;for(var f=c.firstContext;f!==null;){if(f.context===r){if(i.tag===1){f=Rt(-1,n&-n),f.tag=2;var N=i.updateQueue;if(N!==null){N=N.shared;var O=N.pending;O===null?f.next=f:(f.next=O.next,O.next=f),N.pending=f}}i.lanes|=n,f=i.alternate,f!==null&&(f.lanes|=n),Ws(i.return,n,t),c.lanes|=n;break}f=f.next}}else if(i.tag===10)a=i.type===t.type?null:i.child;else if(i.tag===18){if(a=i.return,a===null)throw Error(u(341));a.lanes|=n,c=a.alternate,c!==null&&(c.lanes|=n),Ws(a,n,t),a=i.sibling}else a=i.child;if(a!==null)a.return=i;else for(a=i;a!==null;){if(a===t){a=null;break}if(i=a.sibling,i!==null){i.return=a.return,a=i;break}a=a.return}i=a}Ye(e,t,s.children,n),t=t.child}return t;case 9:return s=t.type,r=t.pendingProps.children,In(t,n),s=ut(s),r=r(s),t.flags|=1,Ye(e,t,r,n),t.child;case 14:return r=t.type,s=vt(r,t.pendingProps),s=vt(r.type,s),su(e,t,r,s,n);case 15:return iu(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,s=t.pendingProps,s=t.elementType===r?s:vt(r,s),xl(e,t),t.tag=1,qe(r)?(e=!0,tl(t)):e=!1,In(t,n),Za(t,r,s),li(t,r,s,n),ai(null,t,r,!0,e,n);case 19:return mu(e,t,n);case 22:return ou(e,t,n)}throw Error(u(156,t.tag))};function bu(e,t){return go(e,t)}function tf(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function ft(e,t,n,r){return new tf(e,t,n,r)}function zi(e){return e=e.prototype,!(!e||!e.isReactComponent)}function nf(e){if(typeof e=="function")return zi(e)?1:0;if(e!=null){if(e=e.$$typeof,e===ke)return 11;if(e===Ne)return 14}return 2}function Zt(e,t){var n=e.alternate;return n===null?(n=ft(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Ll(e,t,n,r,s,i){var a=2;if(r=e,typeof e=="function")zi(e)&&(a=1);else if(typeof e=="string")a=5;else e:switch(e){case pe:return pn(n.children,s,i,t);case me:a=8,s|=8;break;case be:return e=ft(12,n,t,s|2),e.elementType=be,e.lanes=i,e;case se:return e=ft(13,n,t,s),e.elementType=se,e.lanes=i,e;case J:return e=ft(19,n,t,s),e.elementType=J,e.lanes=i,e;case xe:return Rl(n,s,i,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case X:a=10;break e;case le:a=9;break e;case ke:a=11;break e;case Ne:a=14;break e;case _e:a=16,r=null;break e}throw Error(u(130,e==null?e:typeof e,""))}return t=ft(a,n,t,s),t.elementType=e,t.type=r,t.lanes=i,t}function pn(e,t,n,r){return e=ft(7,e,r,t),e.lanes=n,e}function Rl(e,t,n,r){return e=ft(22,e,r,t),e.elementType=xe,e.lanes=n,e.stateNode={isHidden:!1},e}function Ti(e,t,n){return e=ft(6,e,null,t),e.lanes=n,e}function Li(e,t,n){return t=ft(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function rf(e,t,n,r,s){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=rs(0),this.expirationTimes=rs(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=rs(0),this.identifierPrefix=r,this.onRecoverableError=s,this.mutableSourceEagerHydrationData=null}function Ri(e,t,n,r,s,i,a,c,f){return e=new rf(e,t,n,c,f),t===1?(t=1,i===!0&&(t|=8)):t=0,i=ft(3,null,null,t),e.current=i,i.stateNode=e,i.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},Hs(i),e}function lf(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(o)}catch(p){console.error(p)}}return o(),Oi.exports=vf(),Oi.exports}var qu;function yf(){if(qu)return bl;qu=1;var o=gf();return bl.createRoot=o.createRoot,bl.hydrateRoot=o.hydrateRoot,bl}var xf=yf();const kf=Qi(xf),jf={},Ju=o=>{let p;const u=new Set,k=(z,d)=>{const w=typeof z=="function"?z(p):z;if(!Object.is(w,p)){const m=p;p=d??(typeof w!="object"||w===null)?w:Object.assign({},p,w),u.forEach(T=>T(p,m))}},S=()=>p,R={setState:k,getState:S,getInitialState:()=>C,subscribe:z=>(u.add(z),()=>u.delete(z)),destroy:()=>{(jf?"production":void 0)!=="production"&&console.warn("[DEPRECATED] The `destroy` method will be unsupported in a future version. Instead use unsubscribe function returned by subscribe. Everything will be garbage-collected if store is garbage-collected."),u.clear()}},C=p=o(k,S,R);return R},Sf=o=>o?Ju(o):Ju;var Fi={exports:{}},Bi={},Wi={exports:{}},Ui={};/** + * @license React + * use-sync-external-store-shim.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var ec;function wf(){if(ec)return Ui;ec=1;var o=_r();function p(d,w){return d===w&&(d!==0||1/d===1/w)||d!==d&&w!==w}var u=typeof Object.is=="function"?Object.is:p,k=o.useState,S=o.useEffect,D=o.useLayoutEffect,F=o.useDebugValue;function _(d,w){var m=w(),T=k({inst:{value:m,getSnapshot:w}}),g=T[0].inst,M=T[1];return D(function(){g.value=m,g.getSnapshot=w,R(g)&&M({inst:g})},[d,m,w]),S(function(){return R(g)&&M({inst:g}),d(function(){R(g)&&M({inst:g})})},[d]),F(m),m}function R(d){var w=d.getSnapshot;d=d.value;try{var m=w();return!u(d,m)}catch{return!0}}function C(d,w){return w()}var z=typeof window>"u"||typeof window.document>"u"||typeof window.document.createElement>"u"?C:_;return Ui.useSyncExternalStore=o.useSyncExternalStore!==void 0?o.useSyncExternalStore:z,Ui}var tc;function Nf(){return tc||(tc=1,Wi.exports=wf()),Wi.exports}/** + * @license React + * use-sync-external-store-shim/with-selector.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var nc;function Cf(){if(nc)return Bi;nc=1;var o=_r(),p=Nf();function u(C,z){return C===z&&(C!==0||1/C===1/z)||C!==C&&z!==z}var k=typeof Object.is=="function"?Object.is:u,S=p.useSyncExternalStore,D=o.useRef,F=o.useEffect,_=o.useMemo,R=o.useDebugValue;return Bi.useSyncExternalStoreWithSelector=function(C,z,d,w,m){var T=D(null);if(T.current===null){var g={hasValue:!1,value:null};T.current=g}else g=T.current;T=_(function(){function P(ue){if(!B){if(B=!0,Z=ue,ue=w(ue),m!==void 0&&g.hasValue){var pe=g.value;if(m(pe,ue))return I=pe}return I=ue}if(pe=I,k(Z,ue))return pe;var me=w(ue);return m!==void 0&&m(pe,me)?(Z=ue,pe):(Z=ue,I=me)}var B=!1,Z,I,oe=d===void 0?null:d;return[function(){return P(z())},oe===null?void 0:function(){return P(oe())}]},[z,d,w,m]);var M=S(C,T[0],T[1]);return F(function(){g.hasValue=!0,g.value=M},[M]),R(M),M},Bi}var rc;function Ef(){return rc||(rc=1,Fi.exports=Cf()),Fi.exports}var _f=Ef();const zf=Qi(_f),ac={},{useDebugValue:Tf}=oc,{useSyncExternalStoreWithSelector:Lf}=zf;let lc=!1;const Rf=o=>o;function Pf(o,p=Rf,u){(ac?"production":void 0)!=="production"&&u&&!lc&&(console.warn("[DEPRECATED] Use `createWithEqualityFn` instead of `create` or use `useStoreWithEqualityFn` instead of `useStore`. They can be imported from 'zustand/traditional'. https://github.com/pmndrs/zustand/discussions/1937"),lc=!0);const k=Lf(o.subscribe,o.getState,o.getServerState||o.getInitialState,p,u);return Tf(k),k}const sc=o=>{(ac?"production":void 0)!=="production"&&typeof o!="function"&&console.warn("[DEPRECATED] Passing a vanilla store will be unsupported in a future version. Instead use `import { useStore } from 'zustand'`.");const p=typeof o=="function"?Sf(o):o,u=(k,S)=>Pf(p,k,S);return Object.assign(u,p),u},If=o=>o?sc(o):sc,Se="";async function kt(o){const p=await fetch(o,{cache:"no-store"});if(!p.ok)throw new Error(String(p.status));return p.json()}async function We(o,p){return(await fetch(o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(p)})).json()}const fe={liveStatus:()=>kt(`${Se}/api/live-status`),agentConfig:()=>kt(`${Se}/api/agent-config`),modelChangeLog:()=>kt(`${Se}/api/model-change-log`).catch(()=>[]),officialsStats:()=>kt(`${Se}/api/officials-stats`),morningBrief:()=>kt(`${Se}/api/morning-brief`),morningConfig:()=>kt(`${Se}/api/morning-config`),agentsStatus:()=>kt(`${Se}/api/agents-status`),taskActivity:o=>kt(`${Se}/api/task-activity/${encodeURIComponent(o)}`),schedulerState:o=>kt(`${Se}/api/scheduler-state/${encodeURIComponent(o)}`),skillContent:(o,p)=>kt(`${Se}/api/skill-content/${encodeURIComponent(o)}/${encodeURIComponent(p)}`),setModel:(o,p)=>We(`${Se}/api/set-model`,{agentId:o,model:p}),agentWake:o=>We(`${Se}/api/agent-wake`,{agentId:o}),taskAction:(o,p,u)=>We(`${Se}/api/task-action`,{taskId:o,action:p,reason:u}),reviewAction:(o,p,u)=>We(`${Se}/api/review-action`,{taskId:o,action:p,comment:u}),advanceState:(o,p)=>We(`${Se}/api/advance-state`,{taskId:o,comment:p}),archiveTask:(o,p)=>We(`${Se}/api/archive-task`,{taskId:o,archived:p}),archiveAllDone:()=>We(`${Se}/api/archive-task`,{archiveAllDone:!0}),schedulerScan:(o=180)=>We(`${Se}/api/scheduler-scan`,{thresholdSec:o}),schedulerRetry:(o,p)=>We(`${Se}/api/scheduler-retry`,{taskId:o,reason:p}),schedulerEscalate:(o,p)=>We(`${Se}/api/scheduler-escalate`,{taskId:o,reason:p}),schedulerRollback:(o,p)=>We(`${Se}/api/scheduler-rollback`,{taskId:o,reason:p}),refreshMorning:()=>We(`${Se}/api/morning-brief/refresh`,{}),saveMorningConfig:o=>We(`${Se}/api/morning-config`,o),addSkill:(o,p,u,k)=>We(`${Se}/api/add-skill`,{agentId:o,skillName:p,description:u,trigger:k}),addRemoteSkill:(o,p,u,k)=>We(`${Se}/api/add-remote-skill`,{agentId:o,skillName:p,sourceUrl:u,description:k||""}),remoteSkillsList:()=>kt(`${Se}/api/remote-skills-list`),updateRemoteSkill:(o,p)=>We(`${Se}/api/update-remote-skill`,{agentId:o,skillName:p}),removeRemoteSkill:(o,p)=>We(`${Se}/api/remove-remote-skill`,{agentId:o,skillName:p}),createTask:o=>We(`${Se}/api/create-task`,o)},uc=[{key:"Inbox",dept:"皇上",icon:"👑",action:"下旨"},{key:"Taizi",dept:"太子",icon:"🤴",action:"分拣"},{key:"Zhongshu",dept:"中书省",icon:"📜",action:"起草"},{key:"Menxia",dept:"门下省",icon:"🔍",action:"审议"},{key:"Assigned",dept:"尚书省",icon:"📮",action:"派发"},{key:"Doing",dept:"六部",icon:"⚙️",action:"执行"},{key:"Review",dept:"尚书省",icon:"🔎",action:"汇总"},{key:"Done",dept:"回奏",icon:"✅",action:"完成"}],Df={Inbox:0,Pending:0,Taizi:1,Zhongshu:2,Menxia:3,Assigned:4,Doing:5,Review:6,Done:7,Blocked:5,Cancelled:5,Next:4},Mf={太子:"#e8a040",中书省:"#a07aff",门下省:"#6a9eff",尚书省:"#6aef9a",礼部:"#f5c842",户部:"#ff9a6a",兵部:"#ff5270",刑部:"#cc4444",工部:"#44aaff",吏部:"#9b59b6",皇上:"#ffd700",回奏:"#2ecc8a"},zr={Inbox:"收件",Pending:"待处理",Taizi:"太子分拣",Zhongshu:"中书起草",Menxia:"门下审议",Assigned:"已派发",Doing:"执行中",Review:"待审查",Done:"已完成",Blocked:"阻塞",Cancelled:"已取消",Next:"待执行"};function $l(o){return Mf[o]||"#6a9eff"}function Ki(o){const p=o.review_round||0;return o.state==="Menxia"&&p>1?`门下审议(第${p}轮)`:o.state==="Zhongshu"&&p>0?`中书修订(第${p}轮)`:zr[o.state]||o.state}function Jt(o){return/^JJC-/i.test(o.id||"")}function Bl(o){return o.archived||["Done","Cancelled"].includes(o.state)}function Gi(o){const p=Df[o.state]??4;return uc.map((u,k)=>({...u,status:k({liveStatus:null,agentConfig:null,changeLog:[],officialsData:null,agentsStatusData:null,morningBrief:null,subConfig:null,activeTab:"edicts",edictFilter:"active",sessFilter:"all",tplCatFilter:"全部",selectedOfficial:null,modalTaskId:null,countdown:5,toasts:[],setActiveTab:u=>{o({activeTab:u});const k=p();["models","skills","sessions"].includes(u)&&!k.agentConfig&&k.loadAgentConfig(),u==="officials"&&!k.officialsData&&k.loadOfficials(),u==="monitor"&&k.loadAgentsStatus(),u==="morning"&&!k.morningBrief&&k.loadMorning()},setEdictFilter:u=>o({edictFilter:u}),setSessFilter:u=>o({sessFilter:u}),setTplCatFilter:u=>o({tplCatFilter:u}),setSelectedOfficial:u=>o({selectedOfficial:u}),setModalTaskId:u=>o({modalTaskId:u}),setCountdown:u=>o({countdown:u}),toast:(u,k="ok")=>{const S=++Ff;o(D=>({toasts:[...D.toasts,{id:S,msg:u,type:k}]})),setTimeout(()=>{o(D=>({toasts:D.toasts.filter(F=>F.id!==S)}))},3e3)},loadLive:async()=>{try{const u=await fe.liveStatus();o({liveStatus:u}),p().officialsData||fe.officialsStats().then(S=>o({officialsData:S})).catch(()=>{})}catch{}},loadAgentConfig:async()=>{try{const u=await fe.agentConfig(),k=await fe.modelChangeLog();o({agentConfig:u,changeLog:k})}catch{}},loadOfficials:async()=>{try{const u=await fe.officialsStats();o({officialsData:u})}catch{}},loadAgentsStatus:async()=>{try{const u=await fe.agentsStatus();o({agentsStatusData:u})}catch{o({agentsStatusData:null})}},loadMorning:async()=>{try{const[u,k]=await Promise.all([fe.morningBrief(),fe.morningConfig()]);o({morningBrief:u,subConfig:k})}catch{}},loadSubConfig:async()=>{try{const u=await fe.morningConfig();o({subConfig:u})}catch{}},loadAll:async()=>{const u=p();await u.loadLive();const k=u.activeTab;["models","skills"].includes(k)&&await u.loadAgentConfig()}}));let Er=null;function Bf(){Er||(q.getState().loadAll(),Er=setInterval(()=>{const o=q.getState(),p=o.countdown-1;p<=0?(o.setCountdown(5),o.loadAll()):o.setCountdown(p)},1e3))}function Wf(){Er&&(clearInterval(Er),Er=null)}function Uf(o){if(!o)return"";try{const p=new Date(o.includes("T")?o:o.replace(" ","T")+"Z");if(isNaN(p.getTime()))return"";const u=Date.now()-p.getTime(),k=Math.floor(u/6e4);if(k<1)return"刚刚";if(k<60)return k+"分钟前";const S=Math.floor(k/60);return S<24?S+"小时前":Math.floor(S/24)+"天前"}catch{return""}}const ic={Doing:0,Review:1,Assigned:2,Menxia:3,Zhongshu:4,Taizi:5,Inbox:6,Blocked:7,Next:8,Done:9,Cancelled:10};function Hf({task:o}){const p=Gi(o);return l.jsx("div",{className:"ec-pipe",children:p.map((u,k)=>l.jsxs("span",{style:{display:"contents"},children:[l.jsxs("div",{className:`ep-node ${u.status}`,children:[l.jsx("div",{className:"ep-icon",children:u.icon}),l.jsx("div",{className:"ep-name",children:u.dept})]}),kP.setModalTaskId),u=q(P=>P.toast),k=q(P=>P.loadAll),S=o.heartbeat||{status:"unknown",label:"⚪"},D="st-"+(o.state||""),F="dt-"+(o.org||"").replace(/\s/g,""),_=uc.find((P,B)=>Gi(o)[B].status==="active"),R=o.todos||[],C=R.filter(P=>P.status==="completed").length,z=R.length,d=!["Done","Blocked","Cancelled"].includes(o.state),w=["Blocked","Cancelled"].includes(o.state),m=Bl(o),T=o.block&&o.block!=="无"&&o.block!=="-",g=async(P,B)=>{if(B.stopPropagation(),P==="stop"||P==="cancel"){const Z=prompt(P==="stop"?"请输入叫停原因:":"请输入取消原因:");if(Z===null)return;try{const I=await fe.taskAction(o.id,P,Z);I.ok?(u(I.message||"操作成功"),k()):u(I.error||"操作失败","err")}catch{u("服务器连接失败","err")}}else if(P==="resume")try{const Z=await fe.taskAction(o.id,"resume","恢复执行");Z.ok?(u(Z.message||"已恢复"),k()):u(Z.error||"操作失败","err")}catch{u("服务器连接失败","err")}},M=async P=>{P.stopPropagation();try{const B=await fe.archiveTask(o.id,!o.archived);B.ok?(u(B.message||"操作成功"),k()):u(B.error||"操作失败","err")}catch{u("服务器连接失败","err")}};return l.jsxs("div",{className:`edict-card${m?" archived":""}`,onClick:()=>p(o.id),children:[l.jsx(Hf,{task:o}),l.jsx("div",{className:"ec-id",children:o.id}),l.jsx("div",{className:"ec-title",children:o.title||"(无标题)"}),l.jsxs("div",{className:"ec-meta",children:[l.jsx("span",{className:`tag ${D}`,children:Ki(o)}),o.org&&l.jsx("span",{className:`tag ${F}`,children:o.org}),_&&l.jsxs("span",{style:{fontSize:11,color:"var(--muted)"},children:["当前: ",l.jsxs("b",{style:{color:$l(_.dept)},children:[_.dept," · ",_.action]})]})]}),o.now&&o.now!=="-"&&l.jsx("div",{style:{fontSize:11,color:"var(--muted)",lineHeight:1.5,marginBottom:6},children:o.now.substring(0,80)}),(o.review_round||0)>0&&l.jsxs("div",{style:{fontSize:11,marginBottom:6},children:[Array.from({length:o.review_round||0},(P,B)=>l.jsx("span",{style:{display:"inline-block",width:14,height:14,borderRadius:"50%",background:B<(o.review_round||0)-1?"#1a3a6a22":"var(--acc)22",border:`1px solid ${B<(o.review_round||0)-1?"#2a4a8a":"var(--acc)"}`,fontSize:9,textAlign:"center",lineHeight:"13px",marginRight:2,color:B<(o.review_round||0)-1?"#4a6aaa":"var(--acc)"},children:B+1},B)),l.jsxs("span",{style:{color:"var(--muted)",fontSize:10},children:["第 ",o.review_round," 轮磋商"]})]}),z>0&&l.jsxs("div",{className:"ec-todo-bar",children:[l.jsxs("span",{children:["📋 ",C,"/",z]}),l.jsx("div",{className:"ec-todo-track",children:l.jsx("div",{className:"ec-todo-fill",style:{width:`${Math.round(C/z*100)}%`}})}),l.jsx("span",{children:C===z?"✅ 全部完成":"🔄 进行中"})]}),l.jsxs("div",{className:"ec-footer",children:[l.jsx("span",{className:`hb ${S.status}`,children:S.label}),T&&l.jsxs("span",{className:"tag",style:{borderColor:"#ff527044",color:"var(--danger)",background:"#200a10"},children:["🚫 ",o.block]}),o.eta&&o.eta!=="-"&&l.jsxs("span",{style:{fontSize:11,color:"var(--muted)"},children:["📅 ",o.eta]})]}),l.jsxs("div",{className:"ec-actions",onClick:P=>P.stopPropagation(),children:[d&&l.jsxs(l.Fragment,{children:[l.jsx("button",{className:"mini-act",onClick:P=>g("stop",P),children:"⏸ 叫停"}),l.jsx("button",{className:"mini-act danger",onClick:P=>g("cancel",P),children:"🚫 取消"})]}),w&&l.jsx("button",{className:"mini-act",onClick:P=>g("resume",P),children:"▶ 恢复"}),m&&!o.archived&&l.jsx("button",{className:"mini-act",onClick:M,children:"📦 归档"}),o.archived&&l.jsx("button",{className:"mini-act",onClick:M,children:"📤 取消归档"})]})]})}function Qf(){const o=q(m=>m.liveStatus),p=q(m=>m.edictFilter),u=q(m=>m.setEdictFilter),k=q(m=>m.toast),S=q(m=>m.loadAll),F=((o==null?void 0:o.tasks)||[]).filter(Jt),_=F.filter(m=>!Bl(m)),R=F.filter(m=>Bl(m));let C;p==="active"?C=_:p==="archived"?C=R:C=F,C.sort((m,T)=>(ic[m.state]??9)-(ic[T.state]??9));const z=F.filter(m=>!m.archived&&["Done","Cancelled"].includes(m.state)),d=async()=>{if(confirm("将所有已完成/已取消的旨意移入归档?"))try{const m=await fe.archiveAllDone();m.ok?(k(`📦 ${m.count||0} 道旨意已归档`),S()):k(m.error||"批量归档失败","err")}catch{k("服务器连接失败","err")}},w=async()=>{try{const m=await fe.schedulerScan();m.ok?k(`🧭 太子巡检完成:${m.count||0} 个动作`):k(m.error||"巡检失败","err"),S()}catch{k("服务器连接失败","err")}};return l.jsxs("div",{children:[l.jsxs("div",{className:"archive-bar",children:[l.jsx("span",{className:"ab-label",children:"筛选:"}),["active","archived","all"].map(m=>l.jsx("button",{className:`ab-btn ${p===m?"active":""}`,onClick:()=>u(m),children:m==="active"?"活跃":m==="archived"?"归档":"全部"},m)),z.length>0&&l.jsx("button",{className:"ab-btn",onClick:d,children:"📦 一键归档"}),l.jsxs("span",{className:"ab-count",children:["活跃 ",_.length," · 归档 ",R.length," · 共 ",F.length]}),l.jsx("button",{className:"ab-scan",onClick:w,children:"🧭 太子巡检"})]}),l.jsx("div",{className:"edict-grid",children:C.length===0?l.jsxs("div",{className:"empty",style:{gridColumn:"1/-1"},children:["暂无旨意",l.jsx("br",{}),l.jsx("small",{style:{fontSize:11,marginTop:6,display:"block",color:"var(--muted)"},children:"通过飞书向太子发送任务,太子分拣后转中书省处理"})]}):C.map(m=>l.jsx(Vf,{task:m},m.id))})]})}function Kf(){var Z;const o=q(I=>I.liveStatus),p=q(I=>I.agentsStatusData),u=q(I=>I.officialsData),k=q(I=>I.loadAgentsStatus),S=q(I=>I.setModalTaskId),D=q(I=>I.toast);te.useEffect(()=>{k()},[k]);const _=((o==null?void 0:o.tasks)||[]).filter(I=>Jt(I)&&I.state!=="Done"&&I.state!=="Next"),R={};u!=null&&u.officials&&u.officials.forEach(I=>{R[I.id]=I});const C=async I=>{try{const oe=await fe.agentWake(I);D(oe.message||"唤醒指令已发出"),setTimeout(()=>k(),3e4)}catch{D("唤醒失败","err")}},z=async()=>{if(!p)return;const I=p.agents.filter(oe=>oe.id!=="main"&&oe.status!=="running"&&oe.status!=="unconfigured");if(!I.length){D("所有 Agent 均已在线");return}D(`正在唤醒 ${I.length} 个 Agent...`);for(const oe of I)try{await fe.agentWake(oe.id)}catch{}D(`${I.length} 个唤醒指令已发出,30秒后刷新状态`),setTimeout(()=>k(),3e4)},d=p,w=((Z=d==null?void 0:d.agents)==null?void 0:Z.filter(I=>I.id!=="main"))||[],m=w.filter(I=>I.status==="running").length,T=w.filter(I=>I.status==="idle").length,g=w.filter(I=>I.status==="offline").length,M=w.filter(I=>I.status==="unconfigured").length,P=d==null?void 0:d.gateway,B=P!=null&&P.probe?"ok":P!=null&&P.alive?"warn":"err";return l.jsxs("div",{children:[d&&d.ok&&l.jsxs("div",{className:"as-panel",children:[l.jsxs("div",{className:"as-header",children:[l.jsx("span",{className:"as-title",children:"🔌 Agent 在线状态"}),l.jsxs("span",{className:`as-gw ${B}`,children:["Gateway: ",(P==null?void 0:P.status)||"未知"]}),l.jsx("button",{className:"btn-refresh",onClick:()=>k(),style:{marginLeft:8},children:"🔄 刷新"}),g+M>0&&l.jsx("button",{className:"btn-refresh",onClick:z,style:{marginLeft:4,borderColor:"var(--warn)",color:"var(--warn)"},children:"⚡ 全部唤醒"})]}),l.jsx("div",{className:"as-grid",children:w.map(I=>{const oe=I.status!=="running"&&I.status!=="unconfigured"&&(P==null?void 0:P.alive);return l.jsxs("div",{className:"as-card",title:`${I.role} · ${I.statusLabel}`,children:[l.jsx("div",{className:`as-dot ${I.status}`}),l.jsx("div",{style:{fontSize:22},children:I.emoji}),l.jsx("div",{style:{fontSize:12,fontWeight:700},children:I.label}),l.jsx("div",{style:{fontSize:10,color:"var(--muted)"},children:I.role}),l.jsx("div",{style:{fontSize:10,color:"var(--muted)"},children:I.statusLabel}),I.lastActive?l.jsxs("div",{style:{fontSize:10,color:"var(--muted)"},children:["⏰ ",I.lastActive]}):l.jsx("div",{style:{fontSize:10,color:"var(--muted)"},children:"无活动记录"}),oe&&l.jsx("button",{className:"as-wake-btn",onClick:ue=>{ue.stopPropagation(),C(I.id)},children:"⚡ 唤醒"})]},I.id)})}),l.jsxs("div",{className:"as-summary",children:[l.jsxs("span",{children:[l.jsx("span",{className:"as-dot running",style:{position:"static",width:8,height:8}})," ",m," 运行中"]}),l.jsxs("span",{children:[l.jsx("span",{className:"as-dot idle",style:{position:"static",width:8,height:8}})," ",T," 待命"]}),g>0&&l.jsxs("span",{children:[l.jsx("span",{className:"as-dot offline",style:{position:"static",width:8,height:8}})," ",g," 离线"]}),M>0&&l.jsxs("span",{children:[l.jsx("span",{className:"as-dot unconfigured",style:{position:"static",width:8,height:8}})," ",M," 未配置"]}),l.jsxs("span",{style:{marginLeft:"auto",fontSize:10,color:"var(--muted)"},children:["检测于 ",(d.checkedAt||"").substring(11,19)]})]})]}),l.jsx("div",{className:"duty-grid",children:Of.map(I=>{const oe=_.filter(se=>se.org===I.label),ue=oe.some(se=>se.state==="Doing"),pe=oe.some(se=>se.state==="Blocked"),me=R[I.id],be=(me==null?void 0:me.heartbeat)||{status:"idle"},X=pe?"blocked":ue?"busy":be.status==="active"?"active":"idle",le=pe?"⚠️ 阻塞":ue?"⚙️ 执行中":be.status==="active"?"🟢 活跃":"⚪ 候命",ke=pe?"blocked-card":ue?"active-card":"";return l.jsxs("div",{className:`duty-card ${ke}`,children:[l.jsxs("div",{className:"dc-hdr",children:[l.jsx("span",{className:"dc-emoji",children:I.emoji}),l.jsxs("div",{className:"dc-info",children:[l.jsx("div",{className:"dc-name",children:I.label}),l.jsxs("div",{className:"dc-role",children:[I.role," · ",I.rank]})]}),l.jsxs("div",{className:"dc-status",children:[l.jsx("span",{className:`dc-dot ${X}`}),l.jsx("span",{children:le})]})]}),l.jsx("div",{className:"dc-body",children:oe.length>0?oe.map(se=>l.jsxs("div",{className:"dc-task",onClick:()=>S(se.id),children:[l.jsx("div",{className:"dc-task-id",children:se.id}),l.jsx("div",{className:"dc-task-title",children:se.title||"(无标题)"}),se.now&&se.now!=="-"&&l.jsx("div",{className:"dc-task-now",children:se.now.substring(0,70)}),l.jsxs("div",{className:"dc-task-meta",children:[l.jsx("span",{className:`tag st-${se.state}`,children:Ki(se)}),se.block&&se.block!=="无"&&l.jsxs("span",{className:"tag",style:{borderColor:"#ff527044",color:"var(--danger)"},children:["🚫",se.block]})]})]},se.id)):l.jsxs("div",{className:"dc-idle",children:[l.jsx("span",{style:{fontSize:20},children:"🪭"}),l.jsx("span",{children:"候命中"})]})}),l.jsxs("div",{className:"dc-footer",children:[l.jsxs("span",{className:"dc-model",children:["🤖 ",(me==null?void 0:me.model_short)||"待配置"]}),(me==null?void 0:me.last_active)&&l.jsxs("span",{className:"dc-la",children:["⏰ ",me.last_active]})]})]},I.id)})})]})}const Gf=["🥇","🥈","🥉"];function Yf(){var d;const o=q(w=>w.officialsData),p=q(w=>w.selectedOfficial),u=q(w=>w.setSelectedOfficial),k=q(w=>w.loadOfficials),S=q(w=>w.setModalTaskId);if(te.useEffect(()=>{k()},[k]),!(o!=null&&o.officials))return l.jsx("div",{className:"empty",children:"⚠️ 请确保本地服务器已启动"});const D=o.officials,F=o.totals||{tasks_done:0,cost_cny:0},_=Math.max(...D.map(w=>w.tokens_in+w.tokens_out+w.cache_read+w.cache_write),1),R=D.filter(w=>{var m;return((m=w.heartbeat)==null?void 0:m.status)==="active"}),C=D.find(w=>{var m;return w.id===(p||((m=D[0])==null?void 0:m.id))}),z=(C==null?void 0:C.id)||((d=D[0])==null?void 0:d.id);return l.jsxs("div",{children:[R.length>0&&l.jsxs("div",{className:"off-activity",children:[l.jsx("span",{children:"🟢 当前活跃:"}),R.map(w=>l.jsxs("span",{style:{fontSize:12},children:[w.emoji," ",w.role]},w.id)),l.jsx("span",{style:{color:"var(--muted)",fontSize:11,marginLeft:"auto"},children:"其余官员待命"})]}),l.jsxs("div",{className:"off-kpi",children:[l.jsxs("div",{className:"kpi",children:[l.jsx("div",{className:"kpi-v",style:{color:"var(--acc)"},children:D.length}),l.jsx("div",{className:"kpi-l",children:"在职官员"})]}),l.jsxs("div",{className:"kpi",children:[l.jsx("div",{className:"kpi-v",style:{color:"#f5c842"},children:F.tasks_done||0}),l.jsx("div",{className:"kpi-l",children:"累计完成旨意"})]}),l.jsxs("div",{className:"kpi",children:[l.jsxs("div",{className:"kpi-v",style:{color:(F.cost_cny||0)>20?"var(--warn)":"var(--ok)"},children:["¥",F.cost_cny||0]}),l.jsx("div",{className:"kpi-l",children:"累计费用(含缓存)"})]}),l.jsxs("div",{className:"kpi",children:[l.jsx("div",{className:"kpi-v",style:{fontSize:16,paddingTop:4},children:o.top_official||"—"}),l.jsx("div",{className:"kpi-l",children:"功绩最高"})]})]}),l.jsxs("div",{className:"off-layout",children:[l.jsxs("div",{className:"off-ranklist",children:[l.jsx("div",{className:"orl-hdr",children:"功绩排行"}),D.map(w=>{const m=w.heartbeat||{status:"idle"};return l.jsxs("div",{className:`orl-item${z===w.id?" selected":""}`,onClick:()=>u(w.id),children:[l.jsx("span",{style:{minWidth:24,textAlign:"center"},children:w.merit_rank<=3?Gf[w.merit_rank-1]:"#"+w.merit_rank}),l.jsx("span",{children:w.emoji}),l.jsxs("span",{style:{flex:1},children:[l.jsx("div",{style:{fontSize:12,fontWeight:700},children:w.role}),l.jsx("div",{style:{fontSize:10,color:"var(--muted)"},children:w.label})]}),l.jsxs("span",{style:{fontSize:11},children:[w.merit_score,"分"]}),l.jsx("span",{className:`dc-dot ${m.status}`,style:{width:8,height:8}})]},w.id)})]}),l.jsx("div",{className:"off-detail",children:C?l.jsx(Xf,{official:C,maxTk:_,onOpenTask:S}):l.jsx("div",{className:"empty",children:"选择左侧官员查看详情"})})]})]})}function Xf({official:o,maxTk:p,onOpenTask:u}){const k=o.heartbeat||{status:"idle",label:"⚪ 待命"},S=o.tokens_in+o.tokens_out+o.cache_read+o.cache_write,D=o.participated_edicts||[],F=[{l:"输入",v:o.tokens_in,color:"#6a9eff"},{l:"输出",v:o.tokens_out,color:"#a07aff"},{l:"缓存读",v:o.cache_read,color:"#2ecc8a"},{l:"缓存写",v:o.cache_write,color:"#f5c842"}];return l.jsxs("div",{children:[l.jsxs("div",{style:{display:"flex",gap:16,alignItems:"center",marginBottom:20},children:[l.jsx("div",{style:{fontSize:40},children:o.emoji}),l.jsxs("div",{style:{flex:1},children:[l.jsx("div",{style:{fontSize:18,fontWeight:800},children:o.role}),l.jsxs("div",{style:{fontSize:12,color:"var(--muted)"},children:[o.label," · ",l.jsx("span",{style:{color:"var(--acc)"},children:o.model_short||o.model})]}),l.jsxs("div",{style:{fontSize:11,color:"var(--muted)",marginTop:2},children:["🏅 ",o.rank," · 功绩分 ",o.merit_score]})]}),l.jsxs("div",{style:{textAlign:"right"},children:[l.jsx("div",{className:`hb ${k.status}`,style:{marginBottom:4},children:k.label}),o.last_active&&l.jsxs("div",{style:{fontSize:10,color:"var(--muted)"},children:["活跃 ",o.last_active]}),l.jsxs("div",{style:{fontSize:10,color:"var(--muted)",marginTop:2},children:[o.sessions," 个会话 · ",o.messages," 条消息"]})]})]}),l.jsxs("div",{style:{marginBottom:18},children:[l.jsx("div",{className:"sec-title",children:"功绩统计"}),l.jsxs("div",{style:{display:"flex",gap:16},children:[l.jsxs("div",{style:{textAlign:"center"},children:[l.jsx("div",{style:{fontSize:20,fontWeight:800,color:"var(--ok)"},children:o.tasks_done}),l.jsx("div",{style:{fontSize:10,color:"var(--muted)"},children:"完成旨意"})]}),l.jsxs("div",{style:{textAlign:"center"},children:[l.jsx("div",{style:{fontSize:20,fontWeight:800,color:"var(--warn)"},children:o.tasks_active}),l.jsx("div",{style:{fontSize:10,color:"var(--muted)"},children:"执行中"})]}),l.jsxs("div",{style:{textAlign:"center"},children:[l.jsx("div",{style:{fontSize:20,fontWeight:800,color:"var(--acc)"},children:o.flow_participations}),l.jsx("div",{style:{fontSize:10,color:"var(--muted)"},children:"流转参与"})]})]})]}),l.jsxs("div",{style:{marginBottom:18},children:[l.jsx("div",{className:"sec-title",children:"Token 消耗"}),F.map(_=>l.jsxs("div",{style:{marginBottom:6},children:[l.jsxs("div",{style:{display:"flex",justifyContent:"space-between",fontSize:11,marginBottom:2},children:[l.jsx("span",{style:{color:"var(--muted)"},children:_.l}),l.jsx("span",{children:_.v.toLocaleString()})]}),l.jsx("div",{style:{height:6,background:"#0e1320",borderRadius:3},children:l.jsx("div",{style:{height:"100%",width:`${p>0?Math.round(_.v/p*100):0}%`,background:_.color,borderRadius:3}})})]},_.l))]}),l.jsxs("div",{style:{marginBottom:18},children:[l.jsx("div",{className:"sec-title",children:"累计费用"}),l.jsxs("div",{style:{display:"flex",gap:10},children:[l.jsxs("span",{style:{fontSize:12,color:o.cost_cny>10?"var(--danger)":o.cost_cny>3?"var(--warn)":"var(--ok)"},children:[l.jsxs("b",{children:["¥",o.cost_cny]})," 人民币"]}),l.jsxs("span",{style:{fontSize:12},children:[l.jsxs("b",{children:["$",o.cost_usd]})," 美元"]}),l.jsxs("span",{style:{fontSize:11,color:"var(--muted)"},children:["总计 ",S.toLocaleString()," tokens"]})]})]}),l.jsxs("div",{children:[l.jsxs("div",{className:"sec-title",children:["参与旨意(",D.length," 道)"]}),D.length===0?l.jsx("div",{style:{fontSize:12,color:"var(--muted)",padding:"8px 0"},children:"暂无旨意记录"}):l.jsx("div",{style:{display:"flex",flexDirection:"column",gap:4},children:D.map(_=>l.jsxs("div",{style:{display:"flex",gap:8,alignItems:"center",padding:"6px 8px",borderRadius:6,cursor:"pointer",border:"1px solid var(--line)"},onClick:()=>u(_.id),children:[l.jsx("span",{style:{fontSize:10,color:"var(--acc)",fontWeight:700},children:_.id}),l.jsx("span",{style:{flex:1,fontSize:12},children:_.title.substring(0,35)}),l.jsx("span",{className:`tag st-${_.state}`,style:{fontSize:10},children:zr[_.state]||_.state})]},_.id))})]})]})}const Zf=[{id:"anthropic/claude-sonnet-4-6",l:"Claude Sonnet 4.6",p:"Anthropic"},{id:"anthropic/claude-opus-4-5",l:"Claude Opus 4.5",p:"Anthropic"},{id:"anthropic/claude-haiku-3-5",l:"Claude Haiku 3.5",p:"Anthropic"},{id:"openai/gpt-4o",l:"GPT-4o",p:"OpenAI"},{id:"openai/gpt-4o-mini",l:"GPT-4o Mini",p:"OpenAI"},{id:"google/gemini-2.5-pro",l:"Gemini 2.5 Pro",p:"Google"},{id:"copilot/claude-sonnet-4",l:"Claude Sonnet 4",p:"Copilot"},{id:"copilot/claude-opus-4.5",l:"Claude Opus 4.5",p:"Copilot"},{id:"copilot/gpt-4o",l:"GPT-4o",p:"Copilot"},{id:"copilot/gemini-2.5-pro",l:"Gemini 2.5 Pro",p:"Copilot"}];function qf(){var w;const o=q(m=>m.agentConfig),p=q(m=>m.changeLog),u=q(m=>m.loadAgentConfig),k=q(m=>m.toast),[S,D]=te.useState({}),[F,_]=te.useState({});if(te.useEffect(()=>{u()},[u]),te.useEffect(()=>{if(o!=null&&o.agents){const m={};o.agents.forEach(T=>{m[T.id]=T.model}),D(m)}},[o]),!(o!=null&&o.agents))return l.jsx("div",{className:"empty",style:{gridColumn:"1/-1"},children:"⚠️ 请先启动本地服务器"});const R=(w=o.knownModels)!=null&&w.length?o.knownModels.map(m=>({id:m.id,l:m.label,p:m.provider})):Zf,C=(m,T)=>{D(g=>({...g,[m]:T}))},z=m=>{const T=o.agents.find(g=>g.id===m);T&&D(g=>({...g,[m]:T.model}))},d=async m=>{const T=S[m];if(T){_(g=>({...g,[m]:{cls:"pending",text:"⟳ 提交中…"}}));try{const g=await fe.setModel(m,T);g.ok?(_(M=>({...M,[m]:{cls:"ok",text:"✅ 已提交,Gateway 重启中(约5秒)"}})),k(m+" 模型已更改","ok"),setTimeout(()=>u(),5500)):_(M=>({...M,[m]:{cls:"err",text:"❌ "+(g.error||"错误")}}))}catch{_(g=>({...g,[m]:{cls:"err",text:"❌ 无法连接服务器"}}))}}};return l.jsxs("div",{children:[l.jsx("div",{className:"model-grid",children:o.agents.map(m=>{const T=S[m.id]||m.model,g=T!==m.model,M=F[m.id];return l.jsxs("div",{className:"mc-card",children:[l.jsxs("div",{className:"mc-top",children:[l.jsx("span",{className:"mc-emoji",children:m.emoji||"🏛️"}),l.jsxs("div",{children:[l.jsxs("div",{className:"mc-name",children:[m.label," ",l.jsx("span",{style:{fontSize:11,color:"var(--muted)"},children:m.id})]}),l.jsx("div",{className:"mc-role",children:m.role})]})]}),l.jsxs("div",{className:"mc-cur",children:["当前: ",l.jsx("b",{children:m.model})]}),l.jsx("select",{className:"msel",value:T,onChange:P=>C(m.id,P.target.value),children:R.map(P=>l.jsxs("option",{value:P.id,children:[P.l," (",P.p,")"]},P.id))}),l.jsxs("div",{className:"mc-btns",children:[l.jsx("button",{className:"btn btn-p",disabled:!g,onClick:()=>d(m.id),children:"应用"}),l.jsx("button",{className:"btn btn-g",onClick:()=>z(m.id),children:"重置"})]}),M&&l.jsx("div",{className:`mc-st ${M.cls}`,children:M.text})]},m.id)})}),l.jsxs("div",{style:{marginTop:24},children:[l.jsx("div",{className:"sec-title",children:"变更日志"}),l.jsx("div",{className:"cl-list",children:p!=null&&p.length?[...p].reverse().slice(0,15).map((m,T)=>l.jsxs("div",{className:"cl-row",children:[l.jsx("span",{className:"cl-t",children:(m.at||"").substring(0,16).replace("T"," ")}),l.jsx("span",{className:"cl-a",children:m.agentId}),l.jsxs("span",{className:"cl-c",children:[l.jsx("b",{children:m.oldModel})," → ",l.jsx("b",{children:m.newModel}),m.rolledBack&&l.jsx("span",{style:{color:"var(--danger)",fontSize:10,border:"1px solid #ff527044",padding:"1px 5px",borderRadius:3,marginLeft:4},children:"⚠ 已回滚"})]})]},T)):l.jsx("div",{style:{fontSize:12,color:"var(--muted)",padding:"8px 0"},children:"暂无变更"})})]})]})}const Jf=[{label:"obra/superpowers",emoji:"⚡",stars:"66.9k",desc:"完整开发工作流技能集",skills:[{name:"brainstorming",url:"https://raw.githubusercontent.com/obra/superpowers/refs/heads/main/skills/brainstorming/SKILL.md"},{name:"test-driven-development",url:"https://raw.githubusercontent.com/obra/superpowers/refs/heads/main/skills/test-driven-development/SKILL.md"},{name:"systematic-debugging",url:"https://raw.githubusercontent.com/obra/superpowers/refs/heads/main/skills/systematic-debugging/SKILL.md"},{name:"subagent-driven-development",url:"https://raw.githubusercontent.com/obra/superpowers/refs/heads/main/skills/subagent-driven-development/SKILL.md"},{name:"writing-plans",url:"https://raw.githubusercontent.com/obra/superpowers/refs/heads/main/skills/writing-plans/SKILL.md"},{name:"executing-plans",url:"https://raw.githubusercontent.com/obra/superpowers/refs/heads/main/skills/executing-plans/SKILL.md"},{name:"requesting-code-review",url:"https://raw.githubusercontent.com/obra/superpowers/refs/heads/main/skills/requesting-code-review/SKILL.md"},{name:"root-cause-tracing",url:"https://raw.githubusercontent.com/obra/superpowers/refs/heads/main/skills/root-cause-tracing/SKILL.md"},{name:"verification-before-completion",url:"https://raw.githubusercontent.com/obra/superpowers/refs/heads/main/skills/verification-before-completion/SKILL.md"},{name:"dispatching-parallel-agents",url:"https://raw.githubusercontent.com/obra/superpowers/refs/heads/main/skills/dispatching-parallel-agents/SKILL.md"}]},{label:"anthropics/skills",emoji:"🏛️",stars:"官方",desc:"Anthropic 官方技能库",skills:[{name:"docx",url:"https://raw.githubusercontent.com/anthropics/skills/main/skills/docx/SKILL.md"},{name:"pdf",url:"https://raw.githubusercontent.com/anthropics/skills/main/skills/pdf/SKILL.md"},{name:"xlsx",url:"https://raw.githubusercontent.com/anthropics/skills/main/skills/xlsx/SKILL.md"},{name:"pptx",url:"https://raw.githubusercontent.com/anthropics/skills/main/skills/pptx/SKILL.md"},{name:"mcp-builder",url:"https://raw.githubusercontent.com/anthropics/skills/main/skills/mcp-builder/SKILL.md"},{name:"frontend-design",url:"https://raw.githubusercontent.com/anthropics/skills/main/skills/frontend-design/SKILL.md"},{name:"web-artifacts-builder",url:"https://raw.githubusercontent.com/anthropics/skills/main/skills/web-artifacts-builder/SKILL.md"},{name:"webapp-testing",url:"https://raw.githubusercontent.com/anthropics/skills/main/skills/webapp-testing/SKILL.md"},{name:"algorithmic-art",url:"https://raw.githubusercontent.com/anthropics/skills/main/skills/algorithmic-art/SKILL.md"},{name:"canvas-design",url:"https://raw.githubusercontent.com/anthropics/skills/main/skills/canvas-design/SKILL.md"}]},{label:"ComposioHQ/awesome-claude-skills",emoji:"🌐",stars:"39.2k",desc:"100+ 社区精选技能",skills:[{name:"github-integration",url:"https://raw.githubusercontent.com/ComposioHQ/awesome-claude-skills/master/github-integration/SKILL.md"},{name:"data-analysis",url:"https://raw.githubusercontent.com/ComposioHQ/awesome-claude-skills/master/data-analysis/SKILL.md"},{name:"code-review",url:"https://raw.githubusercontent.com/ComposioHQ/awesome-claude-skills/master/code-review/SKILL.md"}]}];function ep(){const o=q(x=>x.agentConfig),p=q(x=>x.loadAgentConfig),u=q(x=>x.toast),[k,S]=te.useState(null),[D,F]=te.useState(null),[_,R]=te.useState({name:"",desc:"",trigger:""}),[C,z]=te.useState(!1),[d,w]=te.useState("local"),[m,T]=te.useState([]),[g,M]=te.useState(!1),[P,B]=te.useState(!1),[Z,I]=te.useState({agentId:"",skillName:"",sourceUrl:"",description:""}),[oe,ue]=te.useState(!1),[pe,me]=te.useState(null),[be,X]=te.useState(null),[le,ke]=te.useState(null),[se,J]=te.useState("");te.useEffect(()=>{p()},[p]),te.useEffect(()=>{d==="remote"&&Ne()},[d]);const Ne=async()=>{M(!0);try{const x=await fe.remoteSkillsList();x.ok&&T(x.remoteSkills||[])}catch{u("远程技能列表加载失败","err")}M(!1)},_e=async(x,U)=>{S({agentId:x,name:U,content:"⟳ 加载中…",path:""});try{const ee=await fe.skillContent(x,U);ee.ok?S({agentId:x,name:U,content:ee.content||"",path:ee.path||""}):S({agentId:x,name:U,content:"❌ "+(ee.error||"无法读取"),path:""})}catch{S({agentId:x,name:U,content:"❌ 服务器连接失败",path:""})}},xe=(x,U)=>{F({agentId:x,agentLabel:U}),R({name:"",desc:"",trigger:""})},E=async x=>{if(x.preventDefault(),!(!D||!_.name)){z(!0);try{const U=await fe.addSkill(D.agentId,_.name,_.desc,_.trigger);U.ok?(u(`✅ 技能 ${_.name} 已添加到 ${D.agentLabel}`,"ok"),F(null),p()):u(U.error||"添加失败","err")}catch{u("服务器连接失败","err")}z(!1)}},Q=async x=>{x.preventDefault();const{agentId:U,skillName:ee,sourceUrl:he,description:ve}=Z;if(!(!U||!ee||!he)){ue(!0);try{const Me=await fe.addRemoteSkill(U,ee,he,ve);Me.ok?(u(`✅ 远程技能 ${ee} 已添加到 ${U}`,"ok"),B(!1),I({agentId:"",skillName:"",sourceUrl:"",description:""}),Ne(),p()):u(Me.error||"添加失败","err")}catch{u("服务器连接失败","err")}ue(!1)}},W=async x=>{const U=`${x.agentId}/${x.skillName}`;me(U);try{const ee=await fe.updateRemoteSkill(x.agentId,x.skillName);ee.ok?(u(`✅ 技能 ${x.skillName} 已更新`,"ok"),Ne()):u(ee.error||"更新失败","err")}catch{u("服务器连接失败","err")}me(null)},v=async x=>{const U=`${x.agentId}/${x.skillName}`;X(U);try{const ee=await fe.removeRemoteSkill(x.agentId,x.skillName);ee.ok?(u(`🗑️ 技能 ${x.skillName} 已移除`,"ok"),Ne(),p()):u(ee.error||"移除失败","err")}catch{u("服务器连接失败","err")}X(null)},L=async(x,U)=>{if(!se){u("请先选择目标 Agent","err");return}try{const ee=await fe.addRemoteSkill(se,U,x,"");ee.ok?(u(`✅ ${U} → ${se}`,"ok"),Ne(),p()):u(ee.error||"导入失败","err")}catch{u("服务器连接失败","err")}};if(!(o!=null&&o.agents))return l.jsx("div",{className:"empty",children:"无法加载"});const ae=l.jsx("div",{children:l.jsx("div",{className:"skills-grid",children:o.agents.map(x=>l.jsxs("div",{className:"sk-card",children:[l.jsxs("div",{className:"sk-hdr",children:[l.jsx("span",{className:"sk-emoji",children:x.emoji||"🏛️"}),l.jsx("span",{className:"sk-name",children:x.label}),l.jsxs("span",{className:"sk-cnt",children:[(x.skills||[]).length," 技能"]})]}),l.jsx("div",{className:"sk-list",children:(x.skills||[]).length?(x.skills||[]).map(U=>l.jsxs("div",{className:"sk-item",onClick:()=>_e(x.id,U.name),children:[l.jsxs("span",{className:"si-name",children:["📦 ",U.name]}),l.jsx("span",{className:"si-desc",children:U.description||"无描述"}),l.jsx("span",{className:"si-arrow",children:"›"})]},U.name)):l.jsx("div",{className:"sk-empty",children:"暂无 Skills"})}),l.jsx("div",{className:"sk-add",onClick:()=>xe(x.id,x.label),children:"+ 添加技能"})]},x.id))})}),ce=l.jsxs("div",{children:[l.jsxs("div",{style:{display:"flex",gap:10,marginBottom:20,flexWrap:"wrap",alignItems:"center"},children:[l.jsx("button",{style:{padding:"8px 18px",background:"var(--acc)",color:"#fff",border:"none",borderRadius:8,cursor:"pointer",fontWeight:600,fontSize:13},onClick:()=>{B(!0),ke(null)},children:"+ 添加远程 Skill"}),l.jsx("button",{style:{padding:"8px 14px",background:"transparent",color:"var(--acc)",border:"1px solid var(--acc)",borderRadius:8,cursor:"pointer",fontSize:12},onClick:Ne,children:"⟳ 刷新列表"}),l.jsxs("span",{style:{fontSize:11,color:"var(--muted)",marginLeft:4},children:["共 ",m.length," 个远程技能"]})]}),l.jsxs("div",{style:{marginBottom:24},children:[l.jsx("div",{style:{fontSize:12,fontWeight:700,color:"var(--muted)",letterSpacing:".06em",marginBottom:10},children:"🌐 社区技能源 — 一键导入"}),l.jsx("div",{style:{display:"flex",gap:10,flexWrap:"wrap"},children:Jf.map(x=>l.jsxs("div",{onClick:()=>ke((le==null?void 0:le.label)===x.label?null:x),style:{padding:"8px 14px",background:(le==null?void 0:le.label)===x.label?"#0d1f45":"var(--panel)",border:`1px solid ${(le==null?void 0:le.label)===x.label?"var(--acc)":"var(--line)"}`,borderRadius:10,cursor:"pointer",fontSize:12,transition:"all .15s"},children:[l.jsx("span",{style:{marginRight:6},children:x.emoji}),l.jsx("b",{style:{color:"var(--text)"},children:x.label}),l.jsxs("span",{style:{marginLeft:6,color:"#f0b429",fontSize:11},children:["★ ",x.stars]}),l.jsx("span",{style:{marginLeft:8,color:"var(--muted)"},children:x.desc})]},x.label))}),le&&l.jsxs("div",{style:{marginTop:14,background:"var(--panel)",border:"1px solid var(--line)",borderRadius:12,padding:16},children:[l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:12,marginBottom:14},children:[l.jsx("span",{style:{fontSize:12,fontWeight:600},children:"目标 Agent:"}),l.jsxs("select",{value:se,onChange:x=>J(x.target.value),style:{padding:"6px 10px",background:"var(--bg)",border:"1px solid var(--line)",borderRadius:6,color:"var(--text)",fontSize:12},children:[l.jsx("option",{value:"",children:"— 选择 Agent —"}),o.agents.map(x=>l.jsxs("option",{value:x.id,children:[x.emoji," ",x.label," (",x.id,")"]},x.id))]})]}),l.jsx("div",{style:{display:"grid",gridTemplateColumns:"repeat(auto-fill, minmax(260px, 1fr))",gap:8},children:le.skills.map(x=>{const U=m.some(ee=>ee.skillName===x.name&&ee.agentId===se);return l.jsxs("div",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",padding:"8px 12px",background:"var(--panel2)",borderRadius:8,border:"1px solid var(--line)"},children:[l.jsxs("div",{children:[l.jsxs("div",{style:{fontSize:12,fontWeight:600},children:["📦 ",x.name]}),l.jsx("div",{style:{fontSize:10,color:"var(--muted)",wordBreak:"break-all",maxWidth:180},children:x.url.split("/").slice(-2).join("/")})]}),U?l.jsx("span",{style:{fontSize:10,color:"#4caf88",fontWeight:600},children:"✓ 已导入"}):l.jsx("button",{onClick:()=>L(x.url,x.name),style:{padding:"4px 10px",background:"var(--acc)",color:"#fff",border:"none",borderRadius:6,cursor:"pointer",fontSize:11,whiteSpace:"nowrap"},children:"导入"})]},x.name)})})]})]}),g?l.jsx("div",{style:{textAlign:"center",padding:"40px 0",color:"var(--muted)",fontSize:13},children:"⟳ 加载中…"}):m.length===0?l.jsxs("div",{style:{textAlign:"center",padding:"40px",background:"var(--panel)",borderRadius:12,border:"1px dashed var(--line)"},children:[l.jsx("div",{style:{fontSize:32,marginBottom:10},children:"🌐"}),l.jsx("div",{style:{fontSize:14,color:"var(--muted)"},children:"尚无远程技能"}),l.jsx("div",{style:{fontSize:12,color:"var(--muted)",marginTop:6},children:"从社区技能源快速导入,或手动添加 URL"})]}):l.jsx("div",{style:{display:"flex",flexDirection:"column",gap:10},children:m.map(x=>{var Me;const U=`${x.agentId}/${x.skillName}`,ee=pe===U,he=be===U,ve=o.agents.find(mn=>mn.id===x.agentId);return l.jsxs("div",{style:{background:"var(--panel)",border:"1px solid var(--line)",borderRadius:12,padding:"14px 18px",display:"grid",gridTemplateColumns:"1fr auto",gap:12,alignItems:"center"},children:[l.jsxs("div",{children:[l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:10,marginBottom:6},children:[l.jsxs("span",{style:{fontSize:14,fontWeight:700},children:["📦 ",x.skillName]}),l.jsx("span",{style:{fontSize:10,padding:"2px 8px",borderRadius:999,background:x.status==="valid"?"#0d3322":"#3d1111",color:x.status==="valid"?"#4caf88":"#ff5270",fontWeight:600},children:x.status==="valid"?"✓ 有效":"✗ 文件丢失"}),l.jsxs("span",{style:{fontSize:11,color:"var(--muted)",background:"var(--panel2)",padding:"2px 8px",borderRadius:6},children:[ve==null?void 0:ve.emoji," ",(ve==null?void 0:ve.label)||x.agentId]})]}),x.description&&l.jsx("div",{style:{fontSize:12,color:"var(--muted)",marginBottom:4},children:x.description}),l.jsxs("div",{style:{fontSize:10,color:"var(--muted)",display:"flex",gap:16,flexWrap:"wrap"},children:[l.jsxs("span",{children:["🔗 ",l.jsx("a",{href:x.sourceUrl,target:"_blank",rel:"noreferrer",style:{color:"var(--acc)",textDecoration:"none"},children:x.sourceUrl.length>60?x.sourceUrl.slice(0,60)+"…":x.sourceUrl})]}),l.jsxs("span",{children:["📅 ",x.lastUpdated?x.lastUpdated.slice(0,10):(Me=x.addedAt)==null?void 0:Me.slice(0,10)]})]})]}),l.jsxs("div",{style:{display:"flex",gap:8},children:[l.jsx("button",{onClick:()=>_e(x.agentId,x.skillName),style:{padding:"6px 12px",background:"transparent",color:"var(--muted)",border:"1px solid var(--line)",borderRadius:6,cursor:"pointer",fontSize:11},children:"查看"}),l.jsx("button",{onClick:()=>W(x),disabled:ee,style:{padding:"6px 12px",background:"transparent",color:"var(--acc)",border:"1px solid var(--acc)",borderRadius:6,cursor:"pointer",fontSize:11},children:ee?"⟳":"更新"}),l.jsx("button",{onClick:()=>v(x),disabled:he,style:{padding:"6px 12px",background:"transparent",color:"#ff5270",border:"1px solid #ff5270",borderRadius:6,cursor:"pointer",fontSize:11},children:he?"⟳":"删除"})]})]},U)})})]});return l.jsxs("div",{children:[l.jsx("div",{style:{display:"flex",gap:4,marginBottom:20,borderBottom:"1px solid var(--line)",paddingBottom:0},children:[{key:"local",label:"🏛️ 本地技能",count:o.agents.reduce((x,U)=>{var ee;return x+(((ee=U.skills)==null?void 0:ee.length)||0)},0)},{key:"remote",label:"🌐 远程技能",count:m.length}].map(x=>l.jsxs("div",{onClick:()=>w(x.key),style:{padding:"8px 18px",cursor:"pointer",fontSize:13,borderRadius:"8px 8px 0 0",fontWeight:d===x.key?700:400,background:d===x.key?"var(--panel)":"transparent",color:d===x.key?"var(--text)":"var(--muted)",border:d===x.key?"1px solid var(--line)":"1px solid transparent",borderBottom:d===x.key?"1px solid var(--panel)":"1px solid transparent",position:"relative",bottom:-1,transition:"all .15s"},children:[x.label,x.count>0&&l.jsx("span",{style:{marginLeft:6,fontSize:10,padding:"1px 6px",borderRadius:999,background:"#1a2040",color:"var(--acc)"},children:x.count})]},x.key))}),d==="local"?ae:ce,k&&l.jsx("div",{className:"modal-bg open",onClick:()=>S(null),children:l.jsxs("div",{className:"modal",onClick:x=>x.stopPropagation(),children:[l.jsx("button",{className:"modal-close",onClick:()=>S(null),children:"✕"}),l.jsxs("div",{className:"modal-body",children:[l.jsx("div",{style:{fontSize:11,color:"var(--acc)",fontWeight:700,letterSpacing:".04em",marginBottom:4},children:k.agentId.toUpperCase()}),l.jsxs("div",{style:{fontSize:20,fontWeight:800,marginBottom:16},children:["📦 ",k.name]}),l.jsxs("div",{className:"sk-modal-body",children:[l.jsx("div",{className:"sk-md",style:{whiteSpace:"pre-wrap",fontSize:12,lineHeight:1.7},children:k.content}),k.path&&l.jsxs("div",{className:"sk-path",style:{fontSize:10,color:"var(--muted)",marginTop:12},children:["📂 ",k.path]})]})]})]})}),D&&l.jsx("div",{className:"modal-bg open",onClick:()=>F(null),children:l.jsxs("div",{className:"modal",onClick:x=>x.stopPropagation(),children:[l.jsx("button",{className:"modal-close",onClick:()=>F(null),children:"✕"}),l.jsxs("div",{className:"modal-body",children:[l.jsxs("div",{style:{fontSize:11,color:"var(--acc)",fontWeight:700,letterSpacing:".04em",marginBottom:4},children:["为 ",D.agentLabel," 添加技能"]}),l.jsx("div",{style:{fontSize:20,fontWeight:800,marginBottom:18},children:"+ 新增 Skill"}),l.jsxs("div",{style:{background:"var(--panel2)",border:"1px solid var(--line)",borderRadius:10,padding:14,marginBottom:18,fontSize:12,lineHeight:1.7,color:"var(--muted)"},children:[l.jsx("b",{style:{color:"var(--text)"},children:"📋 Skill 规范说明"}),l.jsx("br",{}),"• 技能名称使用",l.jsx("b",{style:{color:"var(--text)"},children:"小写英文 + 连字符"}),l.jsx("br",{}),"• 创建后会生成模板文件 SKILL.md",l.jsx("br",{}),"• 技能会在 agent 收到相关任务时",l.jsx("b",{style:{color:"var(--text)"},children:"自动激活"})]}),l.jsxs("form",{onSubmit:E,style:{display:"flex",flexDirection:"column",gap:14},children:[l.jsxs("div",{children:[l.jsxs("label",{style:{fontSize:12,fontWeight:600,display:"block",marginBottom:6},children:["技能名称 ",l.jsx("span",{style:{color:"#ff5270"},children:"*"})]}),l.jsx("input",{type:"text",required:!0,placeholder:"如 data-analysis, code-review",value:_.name,onChange:x=>R(U=>({...U,name:x.target.value.toLowerCase().replace(/[^a-z0-9-]/g,"")})),style:{width:"100%",padding:"10px 12px",background:"var(--bg)",border:"1px solid var(--line)",borderRadius:8,color:"var(--text)",fontSize:13,outline:"none"}})]}),l.jsxs("div",{children:[l.jsx("label",{style:{fontSize:12,fontWeight:600,display:"block",marginBottom:6},children:"技能描述"}),l.jsx("input",{type:"text",placeholder:"一句话说明用途",value:_.desc,onChange:x=>R(U=>({...U,desc:x.target.value})),style:{width:"100%",padding:"10px 12px",background:"var(--bg)",border:"1px solid var(--line)",borderRadius:8,color:"var(--text)",fontSize:13,outline:"none"}})]}),l.jsxs("div",{children:[l.jsx("label",{style:{fontSize:12,fontWeight:600,display:"block",marginBottom:6},children:"触发条件(可选)"}),l.jsx("input",{type:"text",placeholder:"何时激活此技能",value:_.trigger,onChange:x=>R(U=>({...U,trigger:x.target.value})),style:{width:"100%",padding:"10px 12px",background:"var(--bg)",border:"1px solid var(--line)",borderRadius:8,color:"var(--text)",fontSize:13,outline:"none"}})]}),l.jsxs("div",{style:{display:"flex",gap:10,justifyContent:"flex-end",marginTop:4},children:[l.jsx("button",{type:"button",className:"btn btn-g",onClick:()=>F(null),style:{padding:"8px 20px"},children:"取消"}),l.jsx("button",{type:"submit",disabled:C,style:{padding:"8px 20px",fontSize:13,background:"var(--acc)",color:"#fff",border:"none",borderRadius:8,cursor:"pointer",fontWeight:600},children:C?"⟳ 创建中…":"📦 创建技能"})]})]})]})]})}),P&&l.jsx("div",{className:"modal-bg open",onClick:()=>B(!1),children:l.jsxs("div",{className:"modal",style:{maxWidth:520},onClick:x=>x.stopPropagation(),children:[l.jsx("button",{className:"modal-close",onClick:()=>B(!1),children:"✕"}),l.jsxs("div",{className:"modal-body",children:[l.jsx("div",{style:{fontSize:11,color:"#a07aff",fontWeight:700,letterSpacing:".04em",marginBottom:4},children:"远程技能管理"}),l.jsx("div",{style:{fontSize:20,fontWeight:800,marginBottom:18},children:"🌐 添加远程 Skill"}),l.jsxs("div",{style:{background:"var(--panel2)",border:"1px solid var(--line)",borderRadius:10,padding:12,marginBottom:18,fontSize:11,color:"var(--muted)",lineHeight:1.7},children:["支持 GitHub Raw URL,如:",l.jsx("br",{}),l.jsx("code",{style:{color:"var(--acc)",fontSize:10},children:"https://raw.githubusercontent.com/obra/superpowers/refs/heads/main/skills/brainstorming/SKILL.md"})]}),l.jsxs("form",{onSubmit:Q,style:{display:"flex",flexDirection:"column",gap:14},children:[l.jsxs("div",{children:[l.jsxs("label",{style:{fontSize:12,fontWeight:600,display:"block",marginBottom:6},children:["目标 Agent ",l.jsx("span",{style:{color:"#ff5270"},children:"*"})]}),l.jsxs("select",{required:!0,value:Z.agentId,onChange:x=>I(U=>({...U,agentId:x.target.value})),style:{width:"100%",padding:"10px 12px",background:"var(--bg)",border:"1px solid var(--line)",borderRadius:8,color:"var(--text)",fontSize:13},children:[l.jsx("option",{value:"",children:"— 选择 Agent —"}),o.agents.map(x=>l.jsxs("option",{value:x.id,children:[x.emoji," ",x.label," (",x.id,")"]},x.id))]})]}),l.jsxs("div",{children:[l.jsxs("label",{style:{fontSize:12,fontWeight:600,display:"block",marginBottom:6},children:["技能名称 ",l.jsx("span",{style:{color:"#ff5270"},children:"*"})]}),l.jsx("input",{type:"text",required:!0,placeholder:"如 brainstorming, code-review",value:Z.skillName,onChange:x=>I(U=>({...U,skillName:x.target.value.toLowerCase().replace(/[^a-z0-9-]/g,"")})),style:{width:"100%",padding:"10px 12px",background:"var(--bg)",border:"1px solid var(--line)",borderRadius:8,color:"var(--text)",fontSize:13,outline:"none"}})]}),l.jsxs("div",{children:[l.jsxs("label",{style:{fontSize:12,fontWeight:600,display:"block",marginBottom:6},children:["源 URL ",l.jsx("span",{style:{color:"#ff5270"},children:"*"})]}),l.jsx("input",{type:"url",required:!0,placeholder:"https://raw.githubusercontent.com/...",value:Z.sourceUrl,onChange:x=>I(U=>({...U,sourceUrl:x.target.value})),style:{width:"100%",padding:"10px 12px",background:"var(--bg)",border:"1px solid var(--line)",borderRadius:8,color:"var(--text)",fontSize:12,outline:"none"}})]}),l.jsxs("div",{children:[l.jsx("label",{style:{fontSize:12,fontWeight:600,display:"block",marginBottom:6},children:"描述(可选)"}),l.jsx("input",{type:"text",placeholder:"一句话说明用途",value:Z.description,onChange:x=>I(U=>({...U,description:x.target.value})),style:{width:"100%",padding:"10px 12px",background:"var(--bg)",border:"1px solid var(--line)",borderRadius:8,color:"var(--text)",fontSize:13,outline:"none"}})]}),l.jsxs("div",{style:{display:"flex",gap:10,justifyContent:"flex-end",marginTop:4},children:[l.jsx("button",{type:"button",className:"btn btn-g",onClick:()=>B(!1),style:{padding:"8px 20px"},children:"取消"}),l.jsx("button",{type:"submit",disabled:oe,style:{padding:"8px 20px",fontSize:13,background:"#a07aff",color:"#fff",border:"none",borderRadius:8,cursor:"pointer",fontWeight:600},children:oe?"⟳ 下载中…":"🌐 添加远程技能"})]})]})]})]})})]})}function tp(){const o=q(k=>k.agentConfig),p={},u={};return o!=null&&o.agents&&o.agents.forEach(k=>{p[k.id]=k.emoji||"🏛️",u[k.id]=k.label||k.id}),{emojiMap:p,labelMap:u}}function Fl(o){const p=(o.id||"").match(/^OC-(\w+)-/);return p?p[1]:(o.org||"").replace(/省|部/g,"").toLowerCase()}function cc(o,p){let u=o.title||"";if(u==="heartbeat 会话")return"💓 心跳检测";const k=u.match(/^agent:(\w+):(\w+)/);if(k){const S=p[k[1]]||k[1];return k[2]==="main"?S+" · 主会话":k[2]==="subagent"?S+" · 子任务执行":k[2]==="cron"?S+" · 定时任务":S+" · "+k[2]}return u.replace(/ 会话$/,"")||o.id}function dc(o){const p=o.now||"";return p.includes("feishu/direct")?{icon:"💬",text:"飞书对话"}:p.includes("feishu")?{icon:"💬",text:"飞书"}:p.includes("webchat")?{icon:"🌐",text:"WebChat"}:p.includes("cron")?{icon:"⏰",text:"定时"}:p.includes("direct")?{icon:"📨",text:"直连"}:{icon:"🔗",text:"会话"}}function np(o){const p=o.activity||[];for(let u=p.length-1;u>=0;u--){const k=p[u];if(k.kind==="assistant"){let S=k.text||"";if(S.startsWith("NO_REPLY")||S.startsWith("Reasoning:"))continue;return S=S.replace(/\[\[.*?\]\]/g,"").replace(/\*\*/g,"").replace(/^#+\s/gm,"").trim(),S.substring(0,120)+(S.length>120?"…":"")}}return""}function rp(){const o=q(d=>d.liveStatus),p=q(d=>d.sessFilter),u=q(d=>d.setSessFilter),{emojiMap:k,labelMap:S}=tp(),[D,F]=te.useState(null),R=((o==null?void 0:o.tasks)||[]).filter(d=>!Jt(d));let C=R;p==="active"?C=R.filter(d=>!["Done","Cancelled"].includes(d.state)):p!=="all"&&(C=R.filter(d=>Fl(d)===p));const z=[...new Set(R.map(Fl))];return l.jsxs("div",{children:[l.jsx("div",{style:{display:"flex",gap:6,marginBottom:16,flexWrap:"wrap"},children:[{key:"all",label:`全部 (${R.length})`},{key:"active",label:"活跃"},...z.slice(0,8).map(d=>({key:d,label:S[d]||d}))].map(d=>l.jsx("span",{className:`sess-filter${p===d.key?" active":""}`,onClick:()=>u(d.key),children:d.label},d.key))}),l.jsx("div",{className:"sess-grid",children:C.length?C.map(d=>{const w=Fl(d),m=k[w]||"🏛️",T=S[w]||d.org||w,g=d.heartbeat||{status:"unknown",label:""},M=dc(d),P=cc(d,S),B=np(d),I=(d.sourceMeta||{}).totalTokens,oe=d.eta||"",ue=g.status==="active"?"🟢":g.status==="warn"?"🟡":g.status==="stalled"?"🔴":"⚪",pe=d.state||"Unknown";return l.jsxs("div",{className:"sess-card",onClick:()=>F(d),children:[l.jsxs("div",{className:"sc-top",children:[l.jsx("span",{className:"sc-emoji",children:m}),l.jsx("div",{style:{flex:1,minWidth:0},children:l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:6},children:[l.jsx("span",{className:"sc-agent",children:T}),l.jsxs("span",{style:{fontSize:10,color:"var(--muted)",background:"var(--panel2)",padding:"2px 6px",borderRadius:4},children:[M.icon," ",M.text]})]})}),l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:6},children:[l.jsx("span",{title:g.label||"",children:ue}),l.jsx("span",{className:`tag st-${pe}`,style:{fontSize:10},children:zr[pe]||pe})]})]}),l.jsx("div",{className:"sc-title",children:P}),B&&l.jsx("div",{style:{fontSize:11,color:"var(--muted)",lineHeight:1.5,marginBottom:8,borderLeft:"2px solid var(--line)",paddingLeft:8,maxHeight:40,overflow:"hidden"},children:B}),l.jsxs("div",{className:"sc-meta",children:[I?l.jsxs("span",{style:{fontSize:10,color:"var(--muted)"},children:["🪙 ",I.toLocaleString()," tokens"]}):null,oe?l.jsx("span",{className:"sc-time",children:Uf(oe)}):null]})]},d.id)}):l.jsx("div",{style:{fontSize:13,color:"var(--muted)",padding:24,textAlign:"center",gridColumn:"1/-1"},children:"暂无小任务/会话数据"})}),D&&l.jsx(lp,{task:D,labelMap:S,emojiMap:k,onClose:()=>F(null)})]})}function lp({task:o,labelMap:p,emojiMap:u,onClose:k}){const S=Fl(o),D=u[S]||"🏛️",F=cc(o,p),_=dc(o),R=o.heartbeat||{label:""},C=o.sourceMeta||{},z=o.activity||[],d=o.state||"Unknown",w=C.totalTokens,m=C.inputTokens,T=C.outputTokens;return l.jsx("div",{className:"modal-bg open",onClick:k,children:l.jsxs("div",{className:"modal",onClick:g=>g.stopPropagation(),children:[l.jsx("button",{className:"modal-close",onClick:k,children:"✕"}),l.jsxs("div",{className:"modal-body",children:[l.jsx("div",{style:{fontSize:11,color:"var(--acc)",fontWeight:700,letterSpacing:".04em",marginBottom:4},children:o.id}),l.jsxs("div",{style:{fontSize:20,fontWeight:800,marginBottom:6},children:[D," ",F]}),l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,marginBottom:18,flexWrap:"wrap"},children:[l.jsx("span",{className:`tag st-${d}`,children:zr[d]||d}),l.jsxs("span",{style:{fontSize:11,color:"var(--muted)"},children:[_.icon," ",_.text]}),R.label&&l.jsx("span",{style:{fontSize:11},children:R.label})]}),l.jsxs("div",{style:{display:"flex",gap:14,marginBottom:18,flexWrap:"wrap"},children:[w!=null&&l.jsxs("div",{style:{background:"var(--panel2)",padding:"10px 16px",borderRadius:8,fontSize:12},children:[l.jsx("div",{style:{fontSize:16,fontWeight:700,color:"var(--acc)"},children:w.toLocaleString()}),l.jsx("div",{style:{color:"var(--muted)",fontSize:10},children:"总 Tokens"})]}),m!=null&&l.jsxs("div",{style:{background:"var(--panel2)",padding:"10px 16px",borderRadius:8,fontSize:12},children:[l.jsx("div",{style:{fontSize:16,fontWeight:700},children:m.toLocaleString()}),l.jsx("div",{style:{color:"var(--muted)",fontSize:10},children:"输入"})]}),T!=null&&l.jsxs("div",{style:{background:"var(--panel2)",padding:"10px 16px",borderRadius:8,fontSize:12},children:[l.jsx("div",{style:{fontSize:16,fontWeight:700},children:T.toLocaleString()}),l.jsx("div",{style:{color:"var(--muted)",fontSize:10},children:"输出"})]})]}),l.jsxs("div",{style:{fontSize:12,fontWeight:700,marginBottom:8},children:["📋 最近活动 ",l.jsxs("span",{style:{fontWeight:400,color:"var(--muted)"},children:["(",z.length," 条)"]})]}),l.jsx("div",{style:{maxHeight:350,overflowY:"auto",border:"1px solid var(--line)",borderRadius:10,background:"var(--panel2)"},children:z.length?z.slice(-15).reverse().map((g,M)=>{const P=g.kind||"",B=P==="assistant"?"🤖":P==="tool"?"🔧":P==="user"?"👤":"📝",Z=P==="assistant"?"回复":P==="tool"?"工具":P==="user"?"用户":"事件";let I=(g.text||"").replace(/\[\[.*?\]\]/g,"").replace(/\*\*/g,"").trim();I.length>200&&(I=I.substring(0,200)+"…");const oe=(g.at||"").substring(11,19);return l.jsxs("div",{style:{padding:"8px 12px",borderBottom:"1px solid var(--line)",fontSize:12,lineHeight:1.5},children:[l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:6,marginBottom:3},children:[l.jsx("span",{children:B}),l.jsx("span",{style:{fontWeight:600,fontSize:11},children:Z}),l.jsx("span",{style:{color:"var(--muted)",fontSize:10,marginLeft:"auto"},children:oe})]}),l.jsx("div",{style:{color:"var(--muted)"},children:I})]},M)}):l.jsx("div",{style:{padding:16,color:"var(--muted)",fontSize:12,textAlign:"center"},children:"暂无活动记录"})}),o.output&&o.output!=="-"&&l.jsxs("div",{style:{fontSize:10,color:"var(--muted)",marginTop:12,wordBreak:"break-all",borderTop:"1px solid var(--line)",paddingTop:8},children:["📂 ",o.output]})]})]})})}function sp(){const o=q(C=>C.liveStatus),[p,u]=te.useState("all"),[k,S]=te.useState(null),D=q(C=>C.toast);let _=((o==null?void 0:o.tasks)||[]).filter(C=>Jt(C)&&["Done","Cancelled"].includes(C.state));p!=="all"&&(_=_.filter(C=>C.state===p));const R=C=>{const z=C.flow_log||[];let d=`# 📜 奏折 · ${C.title} + +`;if(d+=`- **任务编号**: ${C.id} +`,d+=`- **状态**: ${C.state} +`,d+=`- **负责部门**: ${C.org} +`,z.length){const w=z[0].at?z[0].at.substring(0,19).replace("T"," "):"未知",m=z[z.length-1].at?z[z.length-1].at.substring(0,19).replace("T"," "):"未知";d+=`- **开始时间**: ${w} +`,d+=`- **完成时间**: ${m} +`}d+=` +## 流转记录 + +`;for(const w of z)d+=`- **${w.from}** → **${w.to}** + ${w.remark} + _${(w.at||"").substring(0,19)}_ + +`;C.output&&C.output!=="-"&&(d+=`## 产出物 + +\`${C.output}\` +`),navigator.clipboard.writeText(d).then(()=>D("✅ 奏折已复制为 Markdown","ok"),()=>D("复制失败","err"))};return l.jsxs("div",{children:[l.jsxs("div",{style:{display:"flex",gap:8,marginBottom:16,alignItems:"center"},children:[l.jsx("span",{style:{fontSize:12,color:"var(--muted)"},children:"筛选:"}),[{key:"all",label:"全部"},{key:"Done",label:"✅ 已完成"},{key:"Cancelled",label:"🚫 已取消"}].map(C=>l.jsx("span",{className:`sess-filter${p===C.key?" active":""}`,onClick:()=>u(C.key),children:C.label},C.key))]}),l.jsx("div",{className:"mem-list",children:_.length?_.map(C=>{const z=C.flow_log||[],d=[...new Set(z.map(g=>g.from).concat(z.map(g=>g.to)).filter(g=>g&&g!=="皇上"))],w=z.length?(z[0].at||"").substring(0,16).replace("T"," "):"",m=z.length?(z[z.length-1].at||"").substring(0,16).replace("T"," "):"",T=C.state==="Done"?"✅":"🚫";return l.jsxs("div",{className:"mem-card",onClick:()=>S(C),children:[l.jsx("div",{className:"mem-icon",children:"📜"}),l.jsxs("div",{className:"mem-info",children:[l.jsxs("div",{className:"mem-title",children:[T," ",C.title||C.id]}),l.jsxs("div",{className:"mem-sub",children:[C.id," · ",C.org||""," · 流转 ",z.length," 步"]}),l.jsx("div",{className:"mem-tags",children:d.slice(0,5).map(g=>l.jsx("span",{className:"mem-tag",children:g},g))})]}),l.jsxs("div",{className:"mem-right",children:[l.jsx("span",{className:"mem-date",children:w}),m!==w&&l.jsx("span",{className:"mem-date",children:m})]})]},C.id)}):l.jsx("div",{className:"mem-empty",children:"暂无奏折 — 任务完成后自动生成"})}),k&&l.jsx(ip,{task:k,onClose:()=>S(null),onExport:R})]})}function ip({task:o,onClose:p,onExport:u}){const k=o.flow_log||[],S=o.state||"Unknown",D=S==="Done"?"✅":S==="Cancelled"?"🚫":"🔄",F=[...new Set(k.map(m=>m.from).concat(k.map(m=>m.to)).filter(m=>m&&m!=="皇上"))],_=[],R=[],C=[],z=[],d=[];for(const m of k)m.from==="皇上"?_.push(m):m.to==="中书省"||m.from==="中书省"?R.push(m):m.to==="门下省"||m.from==="门下省"?C.push(m):m.remark&&(m.remark.includes("完成")||m.remark.includes("回奏"))?d.push(m):z.push(m);const w=(m,T,g)=>g.length?l.jsxs("div",{style:{marginBottom:18},children:[l.jsxs("div",{style:{fontSize:13,fontWeight:700,marginBottom:10},children:[T," ",m]}),l.jsx("div",{className:"md-timeline",children:g.map((M,P)=>{var Z,I;const B=(Z=M.remark)!=null&&Z.includes("✅")?"green":(I=M.remark)!=null&&I.includes("驳")?"red":"";return l.jsxs("div",{className:"md-tl-item",children:[l.jsx("div",{className:`md-tl-dot ${B}`}),l.jsxs("div",{style:{display:"flex",gap:6,alignItems:"baseline"},children:[l.jsx("span",{className:"md-tl-from",children:M.from}),l.jsxs("span",{className:"md-tl-to",children:["→ ",M.to]})]}),l.jsx("div",{className:"md-tl-remark",children:M.remark}),l.jsx("div",{className:"md-tl-time",children:(M.at||"").substring(0,19).replace("T"," ")})]},P)})})]}):null;return l.jsx("div",{className:"modal-bg open",onClick:p,children:l.jsxs("div",{className:"modal",onClick:m=>m.stopPropagation(),children:[l.jsx("button",{className:"modal-close",onClick:p,children:"✕"}),l.jsxs("div",{className:"modal-body",children:[l.jsx("div",{style:{fontSize:11,color:"var(--acc)",fontWeight:700,letterSpacing:".04em",marginBottom:4},children:o.id}),l.jsxs("div",{style:{fontSize:20,fontWeight:800,marginBottom:6},children:[D," ",o.title||o.id]}),l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,marginBottom:18,flexWrap:"wrap"},children:[l.jsx("span",{className:`tag st-${S}`,children:zr[S]||S}),l.jsx("span",{style:{fontSize:11,color:"var(--muted)"},children:o.org}),l.jsxs("span",{style:{fontSize:11,color:"var(--muted)"},children:["流转 ",k.length," 步"]}),F.map(m=>l.jsx("span",{className:"mem-tag",children:m},m))]}),o.now&&l.jsx("div",{style:{background:"var(--panel2)",border:"1px solid var(--line)",borderRadius:8,padding:"10px 14px",marginBottom:18,fontSize:12,color:"var(--muted)"},children:o.now}),w("圣旨原文","👑",_),w("中书规划","📋",R),w("门下审议","🔍",C),w("六部执行","⚔️",z),w("汇总回奏","📨",d),o.output&&o.output!=="-"&&l.jsxs("div",{style:{marginTop:12,paddingTop:12,borderTop:"1px solid var(--line)"},children:[l.jsx("div",{style:{fontSize:11,fontWeight:600,marginBottom:4},children:"📦 产出物"}),l.jsx("code",{style:{fontSize:11,wordBreak:"break-all"},children:o.output})]}),l.jsx("div",{style:{display:"flex",gap:8,marginTop:16,justifyContent:"flex-end"},children:l.jsx("button",{className:"btn btn-g",onClick:()=>u(o),style:{fontSize:12,padding:"6px 16px"},children:"📋 复制奏折"})})]})]})})}function op(){const o=q(g=>g.tplCatFilter),p=q(g=>g.setTplCatFilter),u=q(g=>g.toast),k=q(g=>g.loadAll),[S,D]=te.useState(null),[F,_]=te.useState({}),[R,C]=te.useState("");let z=bf;o!=="全部"&&(z=z.filter(g=>g.cat===o));const d=g=>{const M={};g.params.forEach(P=>{M[P.key]=P.default||""}),_(M),D(g),C("")},w=g=>{let M=g.command;for(const P of g.params)M=M.replace(new RegExp("\\{"+P.key+"\\}","g"),F[P.key]||P.default||"");return M},m=()=>{S&&C(w(S))},T=async g=>{if(g.preventDefault(),!S)return;const M=w(S);if(!M.trim()){u("请填写必填参数","err");return}try{const P=await fe.agentsStatus();if(P.ok&&P.gateway&&!P.gateway.alive&&(u("⚠️ Gateway 未启动,任务将无法派发!","err"),!confirm("Gateway 未启动,继续?")))return}catch{}if(confirm(`确认下旨? + +${M.substring(0,200)}${M.length>200?"…":""}`))try{const P={};for(const Z of S.params)P[Z.key]=F[Z.key]||Z.default||"";const B=await fe.createTask({title:M.substring(0,120),org:"中书省",targetDept:S.depts[0]||"",priority:"normal",templateId:S.id,params:P});B.ok?(u(`📜 ${B.taskId} 旨意已下达`,"ok"),D(null),k()):u(B.error||"下旨失败","err")}catch{u("⚠️ 服务器连接失败","err")}};return l.jsxs("div",{children:[l.jsx("div",{style:{display:"flex",gap:6,marginBottom:16,flexWrap:"wrap"},children:$f.map(g=>l.jsxs("span",{className:`tpl-cat${o===g.name?" active":""}`,onClick:()=>p(g.name),children:[g.icon," ",g.name]},g.name))}),l.jsx("div",{className:"tpl-grid",children:z.map(g=>l.jsxs("div",{className:"tpl-card",children:[l.jsxs("div",{className:"tpl-top",children:[l.jsx("span",{className:"tpl-icon",children:g.icon}),l.jsx("span",{className:"tpl-name",children:g.name})]}),l.jsx("div",{className:"tpl-desc",children:g.desc}),l.jsxs("div",{className:"tpl-footer",children:[g.depts.map(M=>l.jsx("span",{className:"tpl-dept",children:M},M)),l.jsxs("span",{className:"tpl-est",children:[g.est," · ",g.cost]}),l.jsx("button",{className:"tpl-go",onClick:()=>d(g),children:"下旨"})]})]},g.id))}),S&&l.jsx("div",{className:"modal-bg open",onClick:()=>D(null),children:l.jsxs("div",{className:"modal",onClick:g=>g.stopPropagation(),children:[l.jsx("button",{className:"modal-close",onClick:()=>D(null),children:"✕"}),l.jsxs("div",{className:"modal-body",children:[l.jsx("div",{style:{fontSize:11,color:"var(--acc)",fontWeight:700,letterSpacing:".04em",marginBottom:4},children:"圣旨模板"}),l.jsxs("div",{style:{fontSize:20,fontWeight:800,marginBottom:6},children:[S.icon," ",S.name]}),l.jsx("div",{style:{fontSize:12,color:"var(--muted)",marginBottom:18},children:S.desc}),l.jsxs("div",{style:{display:"flex",gap:6,marginBottom:18,flexWrap:"wrap"},children:[S.depts.map(g=>l.jsx("span",{className:"tpl-dept",children:g},g)),l.jsxs("span",{style:{fontSize:11,color:"var(--muted)",marginLeft:"auto"},children:[S.est," · ",S.cost]})]}),l.jsxs("form",{className:"tpl-form",onSubmit:T,children:[S.params.map(g=>l.jsxs("div",{className:"tpl-field",children:[l.jsxs("label",{className:"tpl-label",children:[g.label,g.required&&l.jsx("span",{style:{color:"#ff5270"},children:" *"})]}),g.type==="textarea"?l.jsx("textarea",{className:"tpl-input",style:{minHeight:80,resize:"vertical"},required:g.required,value:F[g.key]||"",onChange:M=>_(P=>({...P,[g.key]:M.target.value}))}):g.type==="select"?l.jsx("select",{className:"tpl-input",value:F[g.key]||g.default||"",onChange:M=>_(P=>({...P,[g.key]:M.target.value})),children:(g.options||[]).map(M=>l.jsx("option",{children:M},M))}):l.jsx("input",{className:"tpl-input",type:"text",required:g.required,value:F[g.key]||"",onChange:M=>_(P=>({...P,[g.key]:M.target.value}))})]},g.key)),R&&l.jsxs("div",{style:{background:"var(--panel2)",border:"1px solid var(--line)",borderRadius:8,padding:12,marginBottom:14,fontSize:12,color:"var(--muted)"},children:[l.jsx("div",{style:{fontSize:11,fontWeight:600,color:"var(--text)",marginBottom:6},children:"📜 将发送给中书省的旨意:"}),l.jsx("div",{style:{whiteSpace:"pre-wrap",lineHeight:1.6},children:R})]}),l.jsxs("div",{style:{display:"flex",gap:10,justifyContent:"flex-end"},children:[l.jsx("button",{type:"button",className:"btn btn-g",onClick:m,style:{padding:"8px 16px",fontSize:12},children:"👁 预览旨意"}),l.jsx("button",{type:"submit",className:"tpl-go",style:{padding:"8px 20px",fontSize:13},children:"📜 下旨"})]})]})]})]})})]})}const fc={政治:{icon:"🏛️",color:"#6a9eff",desc:"全球政治动态"},军事:{icon:"⚔️",color:"#ff5270",desc:"军事与冲突"},经济:{icon:"💹",color:"#2ecc8a",desc:"经济与市场"},AI大模型:{icon:"🤖",color:"#a07aff",desc:"AI与大模型进展"}},Hi=["政治","军事","经济","AI大模型"];function ap(){const o=q(X=>X.morningBrief),p=q(X=>X.subConfig),u=q(X=>X.loadMorning),k=q(X=>X.loadSubConfig),S=q(X=>X.toast),[D,F]=te.useState(!1),[_,R]=te.useState(null),[C,z]=te.useState(!1),[d,w]=te.useState("⟳ 立即采集"),m=te.useRef(null);te.useEffect(()=>{u()},[u]),te.useEffect(()=>{p&&R(JSON.parse(JSON.stringify(p)))},[p]),te.useEffect(()=>()=>{m.current&&clearInterval(m.current)},[]);const T=async()=>{z(!0),w("⟳ 采集中…");let X=null;try{X=(o==null?void 0:o.generated_at)||null}catch{}try{await fe.refreshMorning(),S("采集已触发,自动检测更新中…","ok");let le=0;m.current&&clearInterval(m.current),m.current=setInterval(async()=>{if(le++,le>24){clearInterval(m.current),m.current=null,z(!1),w("⟳ 立即采集"),S("采集超时,请重试","err");return}try{const ke=await fe.morningBrief();ke.generated_at&&ke.generated_at!==X?(clearInterval(m.current),m.current=null,z(!1),w("⟳ 立即采集"),u(),S("✅ 天下要闻已更新","ok")):w(`⟳ 采集中… (${le*5}s)`)}catch{}},5e3)}catch{S("触发失败","err"),z(!1),w("⟳ 立即采集")}},g=X=>{if(!_)return;const le=[..._.categories||[]],ke=le.find(se=>se.name===X);ke?ke.enabled=!ke.enabled:le.push({name:X,enabled:!0}),R({..._,categories:le})},M=X=>{if(!_||!X)return;const le=[..._.keywords||[]];le.includes(X)||le.push(X),R({..._,keywords:le})},P=X=>{if(!_)return;const le=[..._.keywords||[]];le.splice(X,1),R({..._,keywords:le})},B=(X,le,ke)=>{if(!_||!X||!le){S("请填写源名称和URL","err");return}const se=[..._.custom_feeds||[]];se.push({name:X,url:le,category:ke}),R({..._,custom_feeds:se})},Z=X=>{if(!_)return;const le=[..._.custom_feeds||[]];le.splice(X,1),R({..._,custom_feeds:le})},I=async()=>{if(_)try{const X=await fe.saveMorningConfig(_);X.ok?(S("订阅配置已保存","ok"),k()):S(X.error||"保存失败","err")}catch{S("服务器连接失败","err")}},oe=_?new Set((_.categories||[]).filter(X=>X.enabled).map(X=>X.name)):new Set(Hi),ue=((_==null?void 0:_.keywords)||[]).map(X=>X.toLowerCase()),pe=(o==null?void 0:o.categories)||{},me=o!=null&&o.date?o.date.replace(/(\d{4})(\d{2})(\d{2})/,"$1年$2月$3日"):"",be=Object.values(pe).flat().length;return l.jsxs("div",{children:[l.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:16},children:[l.jsxs("div",{children:[l.jsx("div",{style:{fontSize:20,fontWeight:800,marginBottom:4},children:"🌅 天下要闻"}),l.jsxs("div",{style:{fontSize:12,color:"var(--muted)"},children:[me&&`${me} | `,(o==null?void 0:o.generated_at)&&`采集于 ${o.generated_at} | `,"共 ",be," 条要闻"]})]}),l.jsxs("div",{style:{display:"flex",gap:8},children:[l.jsx("button",{className:"btn btn-g",onClick:()=>F(!D),style:{fontSize:12,padding:"6px 14px"},children:"⚙ 订阅配置"}),l.jsx("button",{className:"tpl-go",disabled:C,onClick:T,style:{fontSize:12,padding:"6px 14px"},children:d})]})]}),D&&_&&l.jsx(up,{config:_,enabledSet:oe,onToggleCat:g,onAddKeyword:M,onRemoveKeyword:P,onAddFeed:B,onRemoveFeed:Z,onSave:I,onSetWebhook:X=>R({..._,feishu_webhook:X})}),Object.keys(pe).length?l.jsx("div",{className:"mb-cats",children:Object.entries(pe).map(([X,le])=>{if(!oe.has(X))return null;const ke=fc[X]||{icon:"📰",color:"var(--acc)"},se=le.map(J=>{const Ne=((J.title||"")+(J.summary||"")).toLowerCase(),_e=ue.filter(xe=>Ne.includes(xe)).length;return{...J,_kwHits:_e}}).sort((J,Ne)=>Ne._kwHits-J._kwHits);return l.jsxs("div",{className:"mb-cat",children:[l.jsxs("div",{className:"mb-cat-hdr",children:[l.jsx("span",{className:"mb-cat-icon",children:ke.icon}),l.jsx("span",{className:"mb-cat-name",style:{color:ke.color},children:X}),l.jsxs("span",{className:"mb-cat-cnt",children:[se.length," 条"]})]}),l.jsx("div",{className:"mb-news-list",children:se.length?se.map((J,Ne)=>{const _e=!!(J.image&&J.image.startsWith("http"));return l.jsxs("div",{className:"mb-card",onClick:()=>window.open(J.link,"_blank"),children:[l.jsx("div",{className:"mb-img",children:_e?l.jsx("img",{src:J.image,onError:xe=>{xe.target.style.display="none"},loading:"lazy",alt:""}):l.jsx("span",{children:ke.icon})}),l.jsxs("div",{className:"mb-info",children:[l.jsxs("div",{className:"mb-headline",children:[J.title,J._kwHits>0&&l.jsx("span",{style:{fontSize:9,padding:"1px 5px",borderRadius:999,background:"#a07aff22",color:"#a07aff",border:"1px solid #a07aff44",marginLeft:4},children:"⭐ 关注"})]}),l.jsx("div",{className:"mb-summary",children:J.summary||J.desc||""}),l.jsxs("div",{className:"mb-meta",children:[l.jsxs("span",{className:"mb-source",children:["📡 ",J.source||""]}),J.pub_date&&l.jsx("span",{className:"mb-time",children:J.pub_date.substring(0,16)})]})]})]},Ne)}):l.jsx("div",{className:"mb-empty",style:{padding:16},children:"暂无新闻"})})]},X)})}):l.jsx("div",{className:"mb-empty",children:"暂无数据,点击右上角「立即采集」获取今日简报"})]})}function up({config:o,enabledSet:p,onToggleCat:u,onAddKeyword:k,onRemoveKeyword:S,onAddFeed:D,onRemoveFeed:F,onSave:_,onSetWebhook:R}){const[C,z]=te.useState(""),[d,w]=te.useState(""),[m,T]=te.useState(""),[g,M]=te.useState(Hi[0]),P=[...Hi];return(o.categories||[]).forEach(B=>{P.includes(B.name)||P.push(B.name)}),l.jsxs("div",{className:"sub-config",style:{marginBottom:20,padding:16,background:"var(--panel2)",borderRadius:12,border:"1px solid var(--line)"},children:[l.jsx("div",{style:{fontSize:14,fontWeight:700,marginBottom:12},children:"⚙ 订阅配置"}),l.jsxs("div",{style:{marginBottom:14},children:[l.jsx("div",{style:{fontSize:12,fontWeight:600,marginBottom:8},children:"订阅分类"}),l.jsx("div",{style:{display:"flex",gap:8,flexWrap:"wrap"},children:P.map(B=>{const Z=fc[B]||{icon:"📰"},I=p.has(B);return l.jsxs("div",{className:`sub-cat ${I?"active":""}`,onClick:()=>u(B),style:{cursor:"pointer",padding:"6px 12px",borderRadius:8,border:`1px solid ${I?"var(--acc)":"var(--line)"}`,display:"flex",alignItems:"center",gap:6},children:[l.jsx("span",{children:Z.icon}),l.jsx("span",{style:{fontSize:12},children:B}),I&&l.jsx("span",{style:{fontSize:10,color:"var(--ok)"},children:"✓"})]},B)})})]}),l.jsxs("div",{style:{marginBottom:14},children:[l.jsx("div",{style:{fontSize:12,fontWeight:600,marginBottom:8},children:"关注关键词"}),l.jsx("div",{style:{display:"flex",gap:6,flexWrap:"wrap",marginBottom:6},children:(o.keywords||[]).map((B,Z)=>l.jsxs("span",{className:"sub-kw",style:{fontSize:11,padding:"2px 8px",borderRadius:4,background:"var(--bg)",border:"1px solid var(--line)"},children:[B,l.jsx("span",{style:{cursor:"pointer",marginLeft:4,color:"var(--danger)"},onClick:()=>S(Z),children:"✕"})]},Z))}),l.jsxs("div",{style:{display:"flex",gap:6},children:[l.jsx("input",{type:"text",value:C,onChange:B=>z(B.target.value),placeholder:"输入关键词",onKeyDown:B=>{B.key==="Enter"&&(k(C.trim()),z(""))},style:{flex:1,padding:"6px 10px",background:"var(--bg)",border:"1px solid var(--line)",borderRadius:6,color:"var(--text)",fontSize:12,outline:"none"}}),l.jsx("button",{className:"btn btn-g",onClick:()=>{k(C.trim()),z("")},style:{fontSize:11,padding:"4px 12px"},children:"添加"})]})]}),l.jsxs("div",{style:{marginBottom:14},children:[l.jsx("div",{style:{fontSize:12,fontWeight:600,marginBottom:8},children:"自定义信息源"}),(o.custom_feeds||[]).map((B,Z)=>l.jsxs("div",{style:{display:"flex",gap:8,alignItems:"center",marginBottom:4,fontSize:11},children:[l.jsx("span",{style:{fontWeight:600},children:B.name}),l.jsx("span",{style:{color:"var(--muted)",flex:1,overflow:"hidden",textOverflow:"ellipsis"},children:B.url}),l.jsx("span",{style:{color:"var(--acc)"},children:B.category}),l.jsx("span",{style:{cursor:"pointer",color:"var(--danger)"},onClick:()=>F(Z),children:"✕"})]},Z)),l.jsxs("div",{style:{display:"flex",gap:6,marginTop:6},children:[l.jsx("input",{placeholder:"源名称",value:d,onChange:B=>w(B.target.value),style:{width:100,padding:"6px 8px",background:"var(--bg)",border:"1px solid var(--line)",borderRadius:6,color:"var(--text)",fontSize:11,outline:"none"}}),l.jsx("input",{placeholder:"RSS / URL",value:m,onChange:B=>T(B.target.value),style:{flex:1,padding:"6px 8px",background:"var(--bg)",border:"1px solid var(--line)",borderRadius:6,color:"var(--text)",fontSize:11,outline:"none"}}),l.jsx("select",{value:g,onChange:B=>M(B.target.value),style:{padding:"6px 8px",background:"var(--bg)",border:"1px solid var(--line)",borderRadius:6,color:"var(--text)",fontSize:11,outline:"none"},children:P.map(B=>l.jsx("option",{value:B,children:B},B))}),l.jsx("button",{className:"btn btn-g",onClick:()=>{D(d,m,g),w(""),T("")},style:{fontSize:11,padding:"4px 12px"},children:"添加"})]})]}),l.jsxs("div",{style:{marginBottom:14},children:[l.jsx("div",{style:{fontSize:12,fontWeight:600,marginBottom:6},children:"飞书 Webhook"}),l.jsx("input",{type:"text",value:o.feishu_webhook||"",onChange:B=>R(B.target.value),placeholder:"https://open.feishu.cn/open-apis/bot/v2/hook/...",style:{width:"100%",padding:"8px 10px",background:"var(--bg)",border:"1px solid var(--line)",borderRadius:6,color:"var(--text)",fontSize:12,outline:"none"}})]}),l.jsx("div",{style:{display:"flex",justifyContent:"flex-end"},children:l.jsx("button",{className:"tpl-go",onClick:_,style:{fontSize:12,padding:"6px 16px"},children:"💾 保存配置"})})]})}const pc={main:"太子",zhongshu:"中书省",menxia:"门下省",shangshu:"尚书省",libu:"礼部",hubu:"户部",bingbu:"兵部",xingbu:"刑部",gongbu:"工部",libu_hr:"吏部",zaochao:"钦天监"},cp={Taizi:"中书省起草",Zhongshu:"门下省审议",Menxia:"尚书省派发",Assigned:"开始执行",Doing:"进入审查",Review:"完成"};function dp(o){const p=Math.max(0,o);if(p<60)return`${p}秒`;if(p<3600)return`${Math.floor(p/60)}分${p%60}秒`;const u=Math.floor(p/3600),k=Math.floor(p%3600/60);return`${u}小时${k}分`}function Vi(o){if(!o)return"";if(typeof o=="number"){const p=new Date(o);return`${String(p.getHours()).padStart(2,"0")}:${String(p.getMinutes()).padStart(2,"0")}:${String(p.getSeconds()).padStart(2,"0")}`}return typeof o=="string"&&o.length>=19?o.substring(11,19):String(o).substring(0,8)}function fp(){var _e,xe;const o=q(E=>E.modalTaskId),p=q(E=>E.setModalTaskId),u=q(E=>E.liveStatus),k=q(E=>E.loadAll),S=q(E=>E.toast),[D,F]=te.useState(null),[_,R]=te.useState(null),C=te.useRef(null),z=te.useRef(null),d=((_e=u==null?void 0:u.tasks)==null?void 0:_e.find(E=>E.id===o))||null,w=te.useCallback(async()=>{if(o)try{const E=await fe.taskActivity(o);F(E)}catch{F(null)}},[o]),m=te.useCallback(async()=>{if(o)try{const E=await fe.schedulerState(o);R(E)}catch{R(null)}},[o]);if(te.useEffect(()=>!o||!d?void 0:(w(),m(),["Done","Cancelled"].includes(d.state)||(C.current=setInterval(()=>{w(),m()},4e3)),()=>{C.current&&(clearInterval(C.current),C.current=null)}),[o,d==null?void 0:d.state,w,m]),te.useEffect(()=>{z.current&&(z.current.scrollTop=z.current.scrollHeight)},[(xe=D==null?void 0:D.activity)==null?void 0:xe.length]),!o||!d)return null;const T=()=>p(null),g=Gi(d),M=g.find(E=>E.status==="active"),P=d.heartbeat||{status:"unknown",label:"⚪ 无数据"},B=d.flow_log||[],Z=d.todos||[],I=Z.filter(E=>E.status==="completed").length,oe=Z.length,ue=!["Done","Blocked","Cancelled"].includes(d.state),pe=["Blocked","Cancelled"].includes(d.state),me=async(E,Q)=>{try{const W=await fe.taskAction(d.id,E,Q);W.ok?(S(W.message||"操作成功","ok"),k(),T()):S(W.error||"操作失败","err")}catch{S("服务器连接失败","err")}},be=async E=>{const Q={approve:"准奏",reject:"封驳"},W=prompt(`${Q[E]} ${d.id} + +请输入批注(可留空):`);if(W!==null)try{const v=await fe.reviewAction(d.id,E,W||"");v.ok?(S(`✅ ${d.id} 已${Q[E]}`,"ok"),k(),T()):S(v.error||"操作失败","err")}catch{S("服务器连接失败","err")}},X=async()=>{const E=cp[d.state]||"下一步",Q=prompt(`⏩ 手动推进 ${d.id} +当前: ${d.state} → 下一步: ${E} + +请输入说明(可留空):`);if(Q!==null)try{const W=await fe.advanceState(d.id,Q||"");W.ok?(S(`⏩ ${W.message}`,"ok"),k(),T()):S(W.error||"推进失败","err")}catch{S("服务器连接失败","err")}},le=async E=>{if(E==="scan"){try{const L=await fe.schedulerScan(180);L.ok?S(`🔍 扫描完成:${L.count||0} 个动作`,"ok"):S(L.error||"扫描失败","err"),m()}catch{S("服务器连接失败","err")}return}const W=prompt(`请输入${{retry:"重试",escalate:"升级",rollback:"回滚"}[E]}原因(可留空):`);if(W===null)return;const v={retry:fe.schedulerRetry,escalate:fe.schedulerEscalate,rollback:fe.schedulerRollback};try{const L=await v[E](d.id,W);L.ok?S(L.message||"操作成功","ok"):S(L.error||"操作失败","err"),m(),k()}catch{S("服务器连接失败","err")}},ke=()=>{const E=prompt("请输入叫停原因(可留空):");E!==null&&me("stop",E)},se=()=>{if(!confirm(`确定要取消 ${d.id} 吗?`))return;const E=prompt("请输入取消原因(可留空):");E!==null&&me("cancel",E)},J=_==null?void 0:_.scheduler,Ne=(_==null?void 0:_.stalledSec)||0;return l.jsx("div",{className:"modal-bg open",onClick:T,children:l.jsxs("div",{className:"modal",onClick:E=>E.stopPropagation(),children:[l.jsx("button",{className:"modal-close",onClick:T,children:"✕"}),l.jsxs("div",{className:"modal-body",children:[l.jsx("div",{className:"modal-id",children:d.id}),l.jsx("div",{className:"modal-title",children:d.title||"(无标题)"}),M&&l.jsxs("div",{className:"cur-stage",children:[l.jsx("div",{className:"cs-icon",children:M.icon}),l.jsxs("div",{className:"cs-info",children:[l.jsx("div",{className:"cs-dept",style:{color:$l(M.dept)},children:M.dept}),l.jsxs("div",{className:"cs-action",children:["当前阶段:",M.action]})]}),l.jsx("span",{className:`hb ${P.status} cs-hb`,children:P.label})]}),l.jsx("div",{className:"m-pipe",children:g.map((E,Q)=>l.jsxs("div",{className:"mp-stage",children:[l.jsxs("div",{className:`mp-node ${E.status}`,children:[E.status==="done"&&l.jsx("div",{className:"mp-done-tick",children:"✓"}),l.jsx("div",{className:"mp-icon",children:E.icon}),l.jsx("div",{className:"mp-dept",style:E.status==="active"?{color:"var(--acc)"}:E.status==="done"?{color:"var(--ok)"}:{},children:E.dept}),l.jsx("div",{className:"mp-action",children:E.action})]}),Qme("resume","恢复执行"),children:"▶️ 恢复执行"}),["Review","Menxia"].includes(d.state)&&l.jsxs(l.Fragment,{children:[l.jsx("button",{className:"btn-action",style:{background:"#2ecc8a22",color:"#2ecc8a",border:"1px solid #2ecc8a44"},onClick:()=>be("approve"),children:"✅ 准奏"}),l.jsx("button",{className:"btn-action",style:{background:"#ff527022",color:"#ff5270",border:"1px solid #ff527044"},onClick:()=>be("reject"),children:"🚫 封驳"})]}),["Pending","Taizi","Zhongshu","Menxia","Assigned","Doing","Review","Next"].includes(d.state)&&l.jsx("button",{className:"btn-action",style:{background:"#7c5cfc18",color:"#7c5cfc",border:"1px solid #7c5cfc44"},onClick:X,children:"⏩ 推进到下一步"})]}),l.jsxs("div",{className:"sched-section",children:[l.jsxs("div",{className:"sched-head",children:[l.jsx("span",{className:"sched-title",children:"🧭 太子调度"}),l.jsx("span",{className:"sched-status",children:J?`${J.enabled===!1?"已禁用":"运行中"} · 阈值 ${J.stallThresholdSec||180}s`:"加载中..."})]}),l.jsxs("div",{className:"sched-grid",children:[l.jsxs("div",{className:"sched-kpi",children:[l.jsx("div",{className:"k",children:"停滞时长"}),l.jsx("div",{className:"v",children:dp(Ne)})]}),l.jsxs("div",{className:"sched-kpi",children:[l.jsx("div",{className:"k",children:"重试次数"}),l.jsx("div",{className:"v",children:(J==null?void 0:J.retryCount)||0})]}),l.jsxs("div",{className:"sched-kpi",children:[l.jsx("div",{className:"k",children:"升级级别"}),l.jsx("div",{className:"v",children:J!=null&&J.escalationLevel?J.escalationLevel===1?"门下省":"尚书省":"无"})]}),l.jsxs("div",{className:"sched-kpi",children:[l.jsx("div",{className:"k",children:"派发状态"}),l.jsx("div",{className:"v",children:(J==null?void 0:J.lastDispatchStatus)||"idle"})]})]}),J&&l.jsxs("div",{className:"sched-line",children:[J.lastProgressAt&&l.jsxs("span",{children:["最近进展 ",(J.lastProgressAt||"").replace("T"," ").substring(0,19)]}),J.lastDispatchAt&&l.jsxs("span",{children:["最近派发 ",(J.lastDispatchAt||"").replace("T"," ").substring(0,19)]}),l.jsxs("span",{children:["自动回滚 ",J.autoRollback===!1?"关闭":"开启"]}),J.lastDispatchAgent&&l.jsxs("span",{children:["目标 ",J.lastDispatchAgent]})]}),l.jsxs("div",{className:"sched-actions",children:[l.jsx("button",{className:"sched-btn",onClick:()=>le("retry"),children:"🔁 重试派发"}),l.jsx("button",{className:"sched-btn warn",onClick:()=>le("escalate"),children:"📣 升级协调"}),l.jsx("button",{className:"sched-btn danger",onClick:()=>le("rollback"),children:"↩️ 回滚稳定点"}),l.jsx("button",{className:"sched-btn",onClick:()=>le("scan"),children:"🔍 立即扫描"})]})]}),oe>0&&l.jsx(pp,{todos:Z,todoDone:I,todoTotal:oe}),l.jsx("div",{className:"m-section",children:l.jsxs("div",{className:"m-rows",children:[l.jsxs("div",{className:"m-row",children:[l.jsx("div",{className:"mr-label",children:"状态"}),l.jsxs("div",{className:"mr-val",children:[l.jsx("span",{className:`tag st-${d.state}`,children:Ki(d)}),(d.review_round||0)>0&&l.jsxs("span",{style:{fontSize:11,color:"var(--muted)",marginLeft:8},children:["共磋商 ",d.review_round," 轮"]})]})]}),l.jsxs("div",{className:"m-row",children:[l.jsx("div",{className:"mr-label",children:"执行部门"}),l.jsx("div",{className:"mr-val",children:l.jsx("span",{className:`tag dt-${(d.org||"").replace(/\s/g,"")}`,children:d.org||"—"})})]}),d.eta&&d.eta!=="-"&&l.jsxs("div",{className:"m-row",children:[l.jsx("div",{className:"mr-label",children:"预计完成"}),l.jsx("div",{className:"mr-val",children:d.eta})]}),d.block&&d.block!=="无"&&d.block!=="-"&&l.jsxs("div",{className:"m-row",children:[l.jsx("div",{className:"mr-label",style:{color:"var(--danger)"},children:"阻塞项"}),l.jsx("div",{className:"mr-val",style:{color:"var(--danger)"},children:d.block})]}),d.now&&d.now!=="-"&&l.jsxs("div",{className:"m-row",style:{gridColumn:"1/-1"},children:[l.jsx("div",{className:"mr-label",children:"当前进展"}),l.jsx("div",{className:"mr-val",style:{fontWeight:400,fontSize:12},children:d.now})]}),d.ac&&l.jsxs("div",{className:"m-row",style:{gridColumn:"1/-1"},children:[l.jsx("div",{className:"mr-label",children:"验收标准"}),l.jsx("div",{className:"mr-val",style:{fontWeight:400,fontSize:12},children:d.ac})]})]})}),B.length>0&&l.jsxs("div",{className:"m-section",children:[l.jsxs("div",{className:"m-sec-label",children:["流转日志(",B.length," 条)"]}),l.jsx("div",{className:"fl-timeline",children:B.map((E,Q)=>{const W=$l(E.from||"");return l.jsxs("div",{className:"fl-item",children:[l.jsx("div",{className:"fl-time",children:E.at?E.at.substring(11,16):""}),l.jsx("div",{className:"fl-dot",style:{background:W}}),l.jsxs("div",{className:"fl-content",children:[l.jsxs("div",{className:"fl-who",children:[l.jsx("span",{className:"from",style:{color:W},children:E.from}),l.jsx("span",{style:{color:"var(--muted)"},children:" → "}),l.jsx("span",{className:"to",style:{color:$l(E.to||"")},children:E.to})]}),l.jsx("div",{className:"fl-rem",children:E.remark})]})]},Q)})})]}),d.output&&d.output!=="-"&&d.output!==""&&l.jsxs("div",{className:"m-section",children:[l.jsx("div",{className:"m-sec-label",children:"产出物"}),l.jsx("code",{children:d.output})]}),l.jsx(mp,{data:D,isDone:["Done","Cancelled"].includes(d.state),logRef:z})]})]})})}function pp({todos:o,todoDone:p,todoTotal:u}){return l.jsxs("div",{className:"todo-section",children:[l.jsxs("div",{className:"todo-header",children:[l.jsxs("div",{className:"m-sec-label",style:{marginBottom:0,border:"none",padding:0},children:["子任务清单(",p,"/",u,")"]}),l.jsxs("div",{className:"todo-progress",children:[l.jsx("div",{className:"todo-bar",children:l.jsx("div",{className:"todo-bar-fill",style:{width:`${Math.round(p/u*100)}%`}})}),l.jsxs("span",{children:[Math.round(p/u*100),"%"]})]})]}),l.jsx("div",{className:"todo-list",children:o.map(k=>{const S=k.status==="completed"?"✅":k.status==="in-progress"?"🔄":"⬜",D=k.status==="completed"?"已完成":k.status==="in-progress"?"进行中":"待开始",F=k.status==="completed"?"s-done":k.status==="in-progress"?"s-progress":"s-notstarted",_=k.status==="completed"?"done":"";return l.jsxs("div",{className:`todo-item ${_}`,children:[l.jsxs("div",{className:"t-row",children:[l.jsx("span",{className:"t-icon",children:S}),l.jsxs("span",{className:"t-id",children:["#",k.id]}),l.jsx("span",{className:"t-title",children:k.title}),l.jsx("span",{className:`t-status ${F}`,children:D})]}),k.detail&&l.jsx("div",{className:"todo-detail",children:k.detail})]},k.id)})})]})}function mp({data:o,isDone:p,logRef:u}){if(!o)return null;const k=o.activity||[],S=(()=>{if(!k.length)return!1;const T=k[k.length-1];if(!T.at)return!1;const g=typeof T.at=="number"?T.at:new Date(T.at).getTime();return Date.now()-g<3e5})(),D=[];o.agentLabel&&D.push(o.agentLabel),o.relatedAgents&&o.relatedAgents.length>1&&D.push(`${o.relatedAgents.length}个 Agent`),o.lastActive&&D.push(`最后活跃: ${o.lastActive}`);const F=o.phaseDurations||[],_=Math.max(...F.map(T=>T.durationSec||1),1),R={皇上:"#eab308",太子:"#f97316",中书省:"#3b82f6",门下省:"#8b5cf6",尚书省:"#10b981",六部:"#06b6d4",礼部:"#ec4899",户部:"#f59e0b",兵部:"#ef4444",刑部:"#6366f1",工部:"#14b8a6",吏部:"#d946ef"},C=o.todosSummary,z=o.resourceSummary,d=k.filter(T=>T.kind==="flow"),w=k.filter(T=>T.kind!=="flow"),m=new Map;return w.forEach(T=>{const g=T.agent||"unknown";m.has(g)||m.set(g,[]),m.get(g).push(T)}),l.jsxs("div",{className:"la-section",children:[l.jsxs("div",{className:"la-header",children:[l.jsxs("span",{className:"la-title",children:[l.jsx("span",{className:`la-dot${S?"":" idle"}`}),p?"执行回顾":"实时动态"]}),l.jsx("span",{className:"la-agent",children:D.join(" · ")||"加载中..."})]}),F.length>0&&l.jsxs("div",{style:{padding:"4px 0 8px",borderBottom:"1px solid var(--line)"},children:[l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:6,marginBottom:6},children:[l.jsx("span",{style:{fontSize:11,fontWeight:600},children:"⏱ 阶段耗时"}),o.totalDuration&&l.jsxs("span",{style:{marginLeft:"auto",fontSize:10,color:"var(--muted)"},children:["总耗时 ",o.totalDuration]})]}),F.map((T,g)=>{const M=Math.max(5,Math.round((T.durationSec||1)/_*100)),P=R[T.phase]||"#6b7280";return l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:6,margin:"2px 0",fontSize:11},children:[l.jsx("span",{style:{minWidth:48,color:"var(--muted)",textAlign:"right"},children:T.phase}),l.jsx("div",{style:{flex:1,height:14,background:"var(--panel)",borderRadius:3,overflow:"hidden"},children:l.jsx("div",{style:{width:`${M}%`,height:"100%",background:P,borderRadius:3,opacity:T.ongoing?.6:.85}})}),l.jsxs("span",{style:{minWidth:60,fontSize:10,color:"var(--muted)"},children:[T.durationText,T.ongoing&&l.jsx("span",{style:{fontSize:9,color:"#60a5fa"},children:" ●进行中"})]})]},g)})]}),C&&l.jsxs("div",{style:{padding:"4px 0 8px",borderBottom:"1px solid var(--line)"},children:[l.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,marginBottom:4},children:[l.jsx("span",{style:{fontSize:11,fontWeight:600},children:"📊 执行进度"}),l.jsxs("span",{style:{fontSize:20,fontWeight:700,color:C.percent>=100?"#22c55e":C.percent>=50?"#60a5fa":"var(--text)"},children:[C.percent,"%"]}),l.jsxs("span",{style:{fontSize:10,color:"var(--muted)"},children:["✅",C.completed," 🔄",C.inProgress," ⬜",C.notStarted," / 共",C.total,"项"]})]}),l.jsxs("div",{style:{height:8,background:"var(--panel)",borderRadius:4,overflow:"hidden",display:"flex"},children:[l.jsx("div",{style:{width:`${C.total?C.completed/C.total*100:0}%`,background:"#22c55e",transition:"width .3s"}}),l.jsx("div",{style:{width:`${C.total?C.inProgress/C.total*100:0}%`,background:"#3b82f6",transition:"width .3s"}})]})]}),z&&(z.totalTokens||z.totalCost)&&l.jsxs("div",{style:{padding:"4px 0 8px",borderBottom:"1px solid var(--line)",display:"flex",gap:12,alignItems:"center"},children:[l.jsx("span",{style:{fontSize:11,fontWeight:600},children:"📈 资源消耗"}),z.totalTokens!=null&&l.jsxs("span",{style:{fontSize:11,color:"var(--muted)"},children:["🔢 ",z.totalTokens.toLocaleString()," tokens"]}),z.totalCost!=null&&l.jsxs("span",{style:{fontSize:11,color:"var(--muted)"},children:["💰 $",z.totalCost.toFixed(4)]}),z.totalElapsedSec!=null&&l.jsxs("span",{style:{fontSize:11,color:"var(--muted)"},children:["⏳ ",z.totalElapsedSec>=60?`${Math.floor(z.totalElapsedSec/60)}分`:"",z.totalElapsedSec%60,"秒"]})]}),l.jsxs("div",{className:"la-log",ref:u,children:[d.length>0&&l.jsx("div",{className:"la-flow-wrap",children:d.map((T,g)=>l.jsxs("div",{className:"la-entry la-tool",children:[l.jsx("span",{className:"la-icon",children:"📋"}),l.jsxs("span",{className:"la-body",children:[l.jsx("b",{children:T.from})," → ",l.jsx("b",{children:T.to})," ",T.remark||""]}),l.jsx("span",{className:"la-time",children:Vi(T.at)})]},`flow-${g}`))}),m.size>0?l.jsx("div",{className:"la-groups",children:Array.from(m.entries()).map(([T,g])=>{const M=pc[T]||T||"未标识",P=g[g.length-1],B=P!=null&&P.at?Vi(P.at):"--:--:--";return l.jsxs("div",{className:"la-group",children:[l.jsxs("div",{className:"la-group-hd",children:[l.jsx("span",{className:"name",children:M}),l.jsxs("span",{children:["最近更新 ",B]})]}),l.jsx("div",{className:"la-group-bd",children:g.map((Z,I)=>l.jsx(hp,{entry:Z},I))})]},T)})}):!d.length&&l.jsx("div",{className:"la-empty",children:o.message||o.error||"Agent 尚未上报进展(等待 Agent 调用 progress 命令)"})]})]})}function hp({entry:o}){var k,S,D;const p=Vi(o.at),u=o.agent?l.jsx("span",{style:{fontSize:9,color:"var(--muted)",background:"var(--panel)",padding:"1px 4px",borderRadius:3,marginRight:4},children:pc[o.agent]||o.agent}):null;if(o.kind==="progress")return l.jsxs("div",{className:"la-entry la-assistant",children:[l.jsx("span",{className:"la-icon",children:"🔄"}),l.jsxs("span",{className:"la-body",children:[u,l.jsx("b",{children:"当前进展:"}),o.text]}),l.jsx("span",{className:"la-time",children:p})]});if(o.kind==="todos"){const F=o.items||[],_=new Map;return o.diff&&((o.diff.changed||[]).forEach(R=>_.set(R.id,{type:"changed",from:R.from,to:R.to})),(o.diff.added||[]).forEach(R=>_.set(R.id,{type:"added"}))),l.jsxs("div",{className:"la-entry",style:{flexDirection:"column",alignItems:"flex-start",gap:2},children:[l.jsxs("div",{style:{fontSize:11,color:"var(--muted)",marginBottom:2},children:[u,"📝 执行计划"]}),F.map(R=>{const C=R.status==="completed"?"✅":R.status==="in-progress"?"🔄":"⬜",z=_.get(String(R.id)),d=R.status==="completed"?{opacity:.5,textDecoration:"line-through"}:R.status==="in-progress"?{color:"#60a5fa",fontWeight:"bold"}:{};return l.jsxs("div",{style:d,children:[C," ",R.title,z&&z.type==="changed"&&z.to==="completed"&&l.jsx("span",{style:{color:"#22c55e",fontSize:9,marginLeft:4},children:"✨刚完成"}),z&&z.type==="changed"&&z.to!=="completed"&&l.jsxs("span",{style:{color:"#f59e0b",fontSize:9,marginLeft:4},children:["↻",z.from,"→",z.to]}),z&&z.type==="added"&&l.jsx("span",{style:{color:"#3b82f6",fontSize:9,marginLeft:4},children:"🆕新增"})]},R.id)}),(S=(k=o.diff)==null?void 0:k.removed)==null?void 0:S.map(R=>l.jsxs("div",{style:{opacity:.4,textDecoration:"line-through"},children:["🗑 ",R.title]},R.id))]})}if(o.kind==="assistant")return l.jsxs(l.Fragment,{children:[o.thinking&&l.jsxs("div",{className:"la-entry la-thinking",children:[l.jsx("span",{className:"la-icon",children:"💭"}),l.jsxs("span",{className:"la-body",children:[u,o.thinking]}),l.jsx("span",{className:"la-time",children:p})]}),(D=o.tools)==null?void 0:D.map((F,_)=>l.jsxs("div",{className:"la-entry la-tool",children:[l.jsx("span",{className:"la-icon",children:"🔧"}),l.jsxs("span",{className:"la-body",children:[u,l.jsx("span",{className:"la-tool-name",children:F.name}),l.jsx("span",{className:"la-trunc",children:F.input_preview||""})]}),l.jsx("span",{className:"la-time",children:p})]},_)),o.text&&l.jsxs("div",{className:"la-entry la-assistant",children:[l.jsx("span",{className:"la-icon",children:"🤖"}),l.jsxs("span",{className:"la-body",children:[u,o.text]}),l.jsx("span",{className:"la-time",children:p})]})]});if(o.kind==="tool_result"){const F=o.exitCode===0||o.exitCode===null||o.exitCode===void 0;return l.jsxs("div",{className:`la-entry la-tool-result ${F?"ok":"err"}`,children:[l.jsx("span",{className:"la-icon",children:F?"✅":"❌"}),l.jsxs("span",{className:"la-body",children:[u,l.jsx("span",{className:"la-tool-name",children:o.tool||""}),o.output?o.output.substring(0,150):""]}),l.jsx("span",{className:"la-time",children:p})]})}return o.kind==="user"?l.jsxs("div",{className:"la-entry la-user",children:[l.jsx("span",{className:"la-icon",children:"📥"}),l.jsxs("span",{className:"la-body",children:[u,o.text||""]}),l.jsx("span",{className:"la-time",children:p})]}):null}function vp(){const o=q(p=>p.toasts);return o.length?l.jsx("div",{className:"toaster",children:o.map(p=>l.jsx("div",{className:`toast ${p.type}`,children:p.msg},p.id))}):null}function gp(){const o=q(T=>T.liveStatus),[p,u]=te.useState(!1),[k,S]=te.useState(!1);te.useEffect(()=>{const T=localStorage.getItem("openclaw_court_date"),g=new Date().toISOString().substring(0,10);if(!JSON.parse(localStorage.getItem("openclaw_court_pref")||'{"enabled":true}').enabled||T===g)return;localStorage.setItem("openclaw_court_date",g),u(!0);const P=setTimeout(()=>D(),3500);return()=>clearTimeout(P)},[]);const D=()=>{S(!0),setTimeout(()=>u(!1),500)};if(!p)return null;const _=((o==null?void 0:o.tasks)||[]).filter(Jt),R=_.filter(T=>!["Done","Cancelled"].includes(T.state)).length,C=_.filter(T=>T.state==="Done").length,z=_.filter(T=>T.state!=="Done"&&T.state!=="Cancelled"&&T.eta&&new Date(T.eta.replace(" ","T"))0&&` · ⚠ 超期 ${z} 件`]}),l.jsx("div",{className:"crm-date in",children:m}),l.jsx("div",{className:"crm-skip",children:"点击任意处跳过"})]})}function yp(){const o=q(d=>d.activeTab),p=q(d=>d.setActiveTab),u=q(d=>d.liveStatus),k=q(d=>d.countdown),S=q(d=>d.loadAll);te.useEffect(()=>(Bf(),()=>Wf()),[]);const D=(u==null?void 0:u.tasks)||[],F=D.filter(Jt),_=F.filter(d=>!Bl(d)),R=u==null?void 0:u.syncStatus,C=R==null?void 0:R.ok,z=d=>d==="edicts"?String(_.length):d==="sessions"?String(D.filter(w=>!Jt(w)).length):d==="memorials"?String(F.filter(w=>["Done","Cancelled"].includes(w.state)).length):d==="monitor"?D.filter(m=>Jt(m)&&m.state==="Doing").length+"活跃":"";return l.jsxs("div",{className:"wrap",children:[l.jsxs("div",{className:"hdr",children:[l.jsxs("div",{children:[l.jsx("div",{className:"logo",children:"三省六部 · 总控台"}),l.jsx("div",{className:"sub-text",children:"OpenClaw Sansheng-Liubu Dashboard"})]}),l.jsxs("div",{className:"hdr-r",children:[l.jsx("span",{className:`chip ${C?"ok":C===!1?"err":""}`,children:C?"✅ 同步正常":C===!1?"❌ 服务器未启动":"⏳ 连接中…"}),l.jsxs("span",{className:"chip",children:[_.length," 道旨意"]}),l.jsx("button",{className:"btn-refresh",onClick:()=>S(),children:"⟳ 刷新"}),l.jsxs("span",{style:{fontSize:11,color:"var(--muted)"},children:["⟳ ",k,"s"]})]})]}),l.jsx("div",{className:"tabs",children:Af.map(d=>l.jsxs("div",{className:`tab ${o===d.key?"active":""}`,onClick:()=>p(d.key),children:[d.icon," ",d.label,z(d.key)&&l.jsx("span",{className:"tbadge",children:z(d.key)})]},d.key))}),o==="edicts"&&l.jsx(Qf,{}),o==="monitor"&&l.jsx(Kf,{}),o==="officials"&&l.jsx(Yf,{}),o==="models"&&l.jsx(qf,{}),o==="skills"&&l.jsx(ep,{}),o==="sessions"&&l.jsx(rp,{}),o==="memorials"&&l.jsx(sp,{}),o==="templates"&&l.jsx(op,{}),o==="morning"&&l.jsx(ap,{}),l.jsx(fp,{}),l.jsx(vp,{}),l.jsx(gp,{})]})}kf.createRoot(document.getElementById("root")).render(l.jsx(oc.StrictMode,{children:l.jsx(yp,{})})); + + + +{} + + + +{ + "generatedAt": "2026-02-24 22:13:18", + "taskSource": "tasks_source.json", + "officials": [ + { + "name": "中书令", + "org": "中书省", + "duty": "起草任务令与优先级" + }, + { + "name": "侍中", + "org": "门下省", + "duty": "审议与退回机制" + }, + { + "name": "尚书令", + "org": "尚书省", + "duty": "派单与升级裁决" + }, + { + "name": "吏部尚书", + "org": "吏部", + "duty": "编制/权限/排班" + }, + { + "name": "户部尚书", + "org": "户部", + "duty": "资源/预算/成本" + }, + { + "name": "礼部尚书", + "org": "礼部", + "duty": "文档/汇报/规范" + }, + { + "name": "兵部尚书", + "org": "兵部", + "duty": "应急与巡检" + }, + { + "name": "刑部尚书", + "org": "刑部", + "duty": "合规/审计/红线" + }, + { + "name": "工部尚书", + "org": "工部", + "duty": "工程交付与自动化" + } + ], + "tasks": [ + { + "id": "JJC-20260224-014", + "title": "A计划:三省六部 GitHub 爆款项目打造", + "official": "中书令", + "org": "中书省", + "state": "Doing", + "now": "Week1完成:README升级/Topics/对比表已上线。待决策:①Docker Hub推送授权 ②Demo GIF录制", + "eta": "2026-03-24", + "block": "门下省disabled,皇上授权直接执行", + "output": "", + "ac": "GitHub Stars 显著增长,进入 Trending,Hacker News 上榜", + "review_round": 0, + "flow_log": [ + { + "at": "2026-02-24T14:03:00.543790Z", + "from": "皇上", + "to": "中书省", + "remark": "下旨:执行A计划——三省六部 GitHub 爆款项目打造,30天行动计划" + }, + { + "at": "2026-02-24T14:04:39.078564Z", + "from": "中书省", + "to": "尚书省", + "remark": "⚡ 皇上钦命:门下省disabled,直接转尚书省执行A计划" + }, + { + "at": "2026-02-24T14:07:07.407940Z", + "from": "中书省", + "to": "执行中", + "remark": "✅ GitHub Topics(13个)已添加 ✅ README竞品对比表已push | 🔄 等待:Docker镜像、Demo GIF、HN帖子" + }, + { + "at": "2026-02-24T14:10:41.895659Z", + "from": "尚书省", + "to": "中书省", + "remark": "Week1回奏:礼部README已推送(87f0313),工部Dockerfile本地就绪,兵部截图已生成。待决策:Docker Hub授权 + GIF录制方式" + } + ], + "updatedAt": "2026-02-24T14:10:41.895659Z", + "outputMeta": { + "exists": true, + "lastModified": "2026-02-23 22:07:11" + }, + "heartbeat": { + "status": "active", + "label": "🟢 活跃 2分钟前", + "ageSec": 157 + } + }, + { + "id": "JJC-20260224-013", + "title": "电芯循环数据清洗小模型方案", + "official": "中书令", + "org": "中书省", + "state": "Zhongshu", + "now": "中书省正在起草规划方案", + "eta": "-", + "block": "无", + "output": "", + "ac": "", + "review_round": 0, + "flow_log": [ + { + "at": "2026-02-24T10:29:22.597859Z", + "from": "皇上", + "to": "中书省", + "remark": "下旨:电芯循环数据清洗小模型方案" + } + ], + "updatedAt": "2026-02-24T10:29:22.598063Z", + "outputMeta": { + "exists": true, + "lastModified": "2026-02-23 22:07:11" + }, + "heartbeat": null + }, + { + "id": "JJC-20260224-001", + "title": "每日早朝简报系统(7点图文新闻)", + "official": "中书令", + "org": "中书省", + "state": "Done", + "now": "早朝简报系统已上线:20条新闻,今晚首次运行成功", + "eta": "-", + "block": "无", + "output": "http://127.0.0.1:7891 → 🌅 早朝简报", + "ac": "", + "review_round": 1, + "flow_log": [ + { + "at": "2026-02-23T16:03:04.183711Z", + "from": "皇上", + "to": "中书省", + "remark": "下旨:每日7:00早朝看板,含政治/军事/经济/AI大模型新闻,图文并茂" + }, + { + "at": "2026-02-23T16:03:42.611779Z", + "from": "中书省", + "to": "门下省", + "remark": "📋 提交第1轮:新闻采集脚本+定时触发+早朝Tab,串行执行" + }, + { + "at": "2026-02-23T16:10:11.354802Z", + "from": "门下省", + "to": "中书省", + "remark": "🟡 附条件准奏:①Brave API直调+降级②schema契约③去重④存档⑤图片本地化⑥时效过滤" + }, + { + "at": "2026-02-23T16:10:11.354802Z", + "from": "尚书省", + "to": "六部", + "remark": "📮 派发:礼部→采集脚本,工部→看板Tab+定时触发" + }, + { + "at": "2026-02-23T16:18:33.625030Z", + "from": "工部", + "to": "尚书省", + "remark": "✅ 完成:早朝Tab上线,zaochao cron已注册每日6:00,20条新闻采集成功" + }, + { + "at": "2026-02-23T16:18:33.625030Z", + "from": "尚书省", + "to": "皇上", + "remark": "✅ 回奏:早朝简报系统已就绪,明日7:00可见4板块20条图文新闻" + } + ], + "updatedAt": "2026-02-23T16:18:33.625030Z", + "outputMeta": { + "exists": false, + "lastModified": null + }, + "heartbeat": null + }, + { + "id": "JJC-20260223-014", + "title": "看板流程合规审查 + 省部调度Tab优化", + "official": "中书令", + "org": "中书省", + "state": "Done", + "now": "三项全部完成:六部SOUL合规/省部调度重设计/续流修复", + "eta": "-", + "block": "无", + "output": "agents/*/SOUL.md + dashboard.html省部调度Tab", + "ac": "", + "review_round": 1, + "flow_log": [ + { + "at": "2026-02-23T15:52:11.651567Z", + "from": "皇上", + "to": "中书省", + "remark": "下旨:①检查各部看板更新合规性②优化省部调度Tab,违者罚俸1月" + }, + { + "at": "2026-02-23T15:52:40.196390Z", + "from": "中书省", + "to": "门下省", + "remark": "📋 提交第1轮方案:六部SOUL合规+省部调度Tab重设计+看板审计" + }, + { + "at": "2026-02-23T15:56:00.668461Z", + "from": "门下省", + "to": "中书省", + "remark": "⚠️ 附条件准奏:①吏部漏列②礼部自审矛盾③审计漏洞④数据源不明" + }, + { + "at": "2026-02-23T15:56:00.668461Z", + "from": "中书省", + "to": "门下省", + "remark": "✅ 四条全部采纳:吏部由尚书省兼管/中书统一模板/last_kanban_update强制项/数据源明确为tasks_source.json" + }, + { + "at": "2026-02-23T15:56:00.668461Z", + "from": "尚书省", + "to": "六部", + "remark": "📮 三路并行派发:礼部→六部SOUL模板;工部→省部调度重设计+审计字段" + }, + { + "at": "2026-02-23T15:58:06.705347Z", + "from": "工部", + "to": "皇上", + "remark": "✅ 回奏:①六部SOUL.md已补正(强制look板更新+罚俸机制)②省部调度改为当班一览③中书省续流铁律已写入SOUL" + } + ], + "updatedAt": "2026-02-23T15:58:06.705347Z", + "outputMeta": { + "exists": false, + "lastModified": null + }, + "heartbeat": null + }, + { + "id": "JJC-20260223-013", + "title": "官员总览看板重新设计", + "official": "中书令", + "org": "中书省", + "state": "Done", + "now": "官员总览全面重设计完成:左右分栏+品级排行+参与旨意+费用准确", + "eta": "-", + "block": "无", + "output": "http://127.0.0.1:7891 → 👥 官员总览", + "ac": "", + "review_round": 1, + "flow_log": [ + { + "at": "2026-02-23T15:25:50.576961Z", + "from": "皇上", + "to": "中书省", + "remark": "下旨:官员总览看板朕不满意,重新设计,门下省不得摸鱼" + }, + { + "at": "2026-02-23T15:27:02.331755Z", + "from": "中书省", + "to": "门下省", + "remark": "📋 提交规划方案第1轮:官员总览重设计(布局重构/数据修复/历史旨意反查)" + }, + { + "at": "2026-02-23T15:39:00Z", + "from": "门下省", + "to": "中书省", + "remark": "⚠️ 附条件准奏:需补充①刷新机制②旨意数据源③Token扫描回归范围,三项明确后无需二次审议" + }, + { + "at": "2026-02-23T15:42:33.704207Z", + "from": "中书省", + "to": "门下省", + "remark": "✅ 三条件回应:①5秒轮询+15秒refresh loop;②从tasks_source.json的flow_log反查;③仅改sync_officials_stats.py,不影响其他模块" + }, + { + "at": "2026-02-23T15:42:33.704207Z", + "from": "尚书省", + "to": "工部", + "remark": "📮 派发:官员总览重设计,含布局重构/Token扫描修复/历史旨意反查" + }, + { + "at": "2026-02-23T15:46:00.352147Z", + "from": "工部", + "to": "皇上", + "remark": "✅ 回奏:官员总览重设计完成,含品级排行榜/旨意参与/Token/缓存费用拆分,请皇上御览" + } + ], + "updatedAt": "2026-02-23T15:46:00.352147Z", + "outputMeta": { + "exists": false, + "lastModified": null + }, + "heartbeat": null + }, + { + "id": "JJC-20260223-012", + "title": "看板增加官员总览面板", + "official": "中书令", + "org": "中书省", + "state": "Done", + "now": "官员总览 Tab 已上线:功绩排行/Token费用/心跳监控", + "eta": "2026-02-23 23:10", + "block": "无", + "output": "http://127.0.0.1:7891 → 👥 官员总览", + "ac": "官员总览Tab上线,含模型费用/功绩统计/监控项", + "flow_log": [ + { + "at": "2026-02-23T14:57:23Z", + "from": "皇上", + "to": "中书省", + "remark": "下旨:看板增加官员总览 - 模型额度/功绩统计/监控项" + }, + { + "at": "2026-02-23T14:58:10Z", + "from": "中书省", + "to": "门下省", + "remark": "📋 提交规划方案:官员总览Tab,含Token统计/费用估算/功绩排名/心跳监控" + }, + { + "at": "2026-02-23T14:58:40Z", + "from": "门下省", + "to": "尚书省", + "remark": "✅ 准奏:方案可行,无风险,转尚书省派发执行" + }, + { + "at": "2026-02-23T14:59:05Z", + "from": "尚书省", + "to": "工部", + "remark": "📮 派发:工部负责构建官员统计同步脚本与看板Tab" + }, + { + "at": "2026-02-23T15:16:00Z", + "from": "工部", + "to": "尚书省", + "remark": "完成:scripts/sync_officials_stats.py + 官员总览Tab,API已验证" + }, + { + "at": "2026-02-23T15:16:30Z", + "from": "尚书省", + "to": "皇上", + "remark": "✅ 回奏:官员总览上线,含功绩排行/实际费用/Token统计/心跳监控" + } + ], + "updatedAt": "2026-02-23T15:20:31.690832Z", + "outputMeta": { + "exists": false, + "lastModified": null + }, + "heartbeat": null + }, + { + "id": "JJC-20260223-011", + "title": "三省六部制开源 GitHub 项目", + "official": "中书令", + "org": "中书省", + "state": "Done", + "now": "已完成:项目已推送至 https://github.com/cft0808/openclaw-sansheng-liubu", + "eta": "2026-02-23 22:37", + "block": "无", + "output": "https://github.com/cft0808/openclaw-sansheng-liubu", + "ac": "看板增强、文档完整、一键安装、GitHub推送", + "flow_log": [ + { + "at": "2026-02-23T14:51:33Z", + "from": "皇上", + "to": "中书省", + "remark": "下旨:把三省六部制做成开源GitHub项目,补全看板功能,写好文档" + }, + { + "at": "2026-02-23T14:53:00Z", + "from": "中书省", + "to": "工部", + "remark": "执行:构建 dashboard.html + server.py + 全套脚本" + }, + { + "at": "2026-02-23T15:00:00Z", + "from": "工部", + "to": "礼部", + "remark": "执行:撰写 README.md(中英双版)" + }, + { + "at": "2026-02-23T15:37:00Z", + "from": "中书省", + "to": "皇上", + "remark": "✅ 回奏:https://github.com/cft0808/openclaw-sansheng-liubu 已上线" + } + ], + "updatedAt": "2026-02-23T15:16:22.408230Z", + "outputMeta": { + "exists": false, + "lastModified": null + }, + "heartbeat": null + }, + { + "id": "JJC-20260223-010", + "title": "电芯循环测试数据预测算法设计与可视化验证", + "official": "尚书令", + "org": "尚书省", + "state": "Done", + "now": "全部完成,已回奏皇上", + "eta": "2026-02-23 21:30", + "block": "无", + "output": "/Users/bingsen/.openclaw/workspace-shangshu/", + "flow_log": [ + { + "at": "2026-02-23T12:23:15Z", + "from": "皇上", + "to": "尚书省", + "remark": "下旨:设计电芯循环测试数据预测算法并可视化验证" + }, + { + "at": "2026-02-23T12:23:15Z", + "from": "尚书省", + "to": "中书省", + "remark": "请求规划拆解" + }, + { + "at": "2026-02-23T12:43:00Z", + "from": "中书省", + "to": "门下省", + "remark": "规划方案提交审核(户部→兵部串行+礼部并行)" + }, + { + "at": "2026-02-23T12:55:00Z", + "from": "门下省", + "to": "尚书省", + "remark": "✅ 准奏:方案完整,路线合理,可执行" + }, + { + "at": "2026-02-23T12:57:00Z", + "from": "尚书省", + "to": "户部", + "remark": "派发:构造电芯循环测试模拟数据" + }, + { + "at": "2026-02-23T12:57:00Z", + "from": "尚书省", + "to": "礼部", + "remark": "并行派发:起草算法说明文档框架" + }, + { + "at": "2026-02-23T13:21:50.426121+00:00", + "from": "尚书省", + "to": "户部", + "remark": "正式派发:生成电芯循环测试模拟数据" + }, + { + "at": "2026-02-23T13:21:50.426121+00:00", + "from": "尚书省", + "to": "礼部", + "remark": "正式派发(并行):撰写算法说明文档" + }, + { + "at": "2026-02-23T13:26:44.518730+00:00", + "from": "户部", + "to": "尚书省", + "remark": "✅ 完成:battery_cycle_data.csv(1000行)" + }, + { + "at": "2026-02-23T13:26:44.518730+00:00", + "from": "礼部", + "to": "尚书省", + "remark": "✅ 完成:algorithm_report.md" + }, + { + "at": "2026-02-23T13:26:44.518730+00:00", + "from": "尚书省", + "to": "兵部", + "remark": "派发:实现RandomForest预测算法 + 可视化图表" + }, + { + "at": "2026-02-23T13:31:08.577447+00:00", + "from": "兵部", + "to": "尚书省", + "remark": "✅ 完成:battery_predictor.py + 3张可视化图,R²=0.9641" + }, + { + "at": "2026-02-23T13:31:08.577447+00:00", + "from": "尚书省", + "to": "皇上", + "remark": "✅ 全流程完成,回奏" + } + ], + "ac": "Python脚本可运行,图表可查,文档完整", + "updatedAt": "2026-02-23T13:34:19.393358+00:00", + "outputMeta": { + "exists": true, + "lastModified": "2026-02-23 21:27:18" + }, + "heartbeat": null + }, + { + "id": "JJC-ZHONGSHU-001", + "title": "中书省:并行流程规划", + "official": "中书令", + "org": "中书省", + "state": "Done", + "now": "已完成:三省六部并行流程规划", + "eta": "已完成 2026-02-23 10:27", + "block": "无", + "output": "/Users/bingsen/clawd/junjichu-v2/docs/三省六部-OpenClaw实施指南-实装版-v1.md", + "flow": { + "draft": "已起草", + "review": "门下通过", + "dispatch": "已归档" + }, + "ac": "形成可执行步骤与验收口径", + "updatedAt": "2026-02-23T13:34:19.393358+00:00", + "outputMeta": { + "exists": true, + "lastModified": "2026-02-23 10:05:17" + }, + "heartbeat": null + }, + { + "id": "JJC-BINGBU-002", + "title": "兵部:看板细节渲染修复", + "official": "兵部尚书", + "org": "兵部", + "state": "Done", + "now": "已完成:抽屉细节与事件流渲染", + "eta": "已完成 2026-02-23 10:27", + "block": "无", + "output": "/Users/bingsen/clawd/junjichu-v2/dashboard.html", + "flow": { + "draft": "尚书令指令", + "review": "门下通过", + "dispatch": "已归档" + }, + "ac": "详情页可见最近事件", + "updatedAt": "2026-02-23T13:34:19.393358+00:00", + "outputMeta": { + "exists": true, + "lastModified": "2026-02-24 21:51:23" + }, + "heartbeat": null + }, + { + "id": "JJC-MENXIA-003", + "title": "门下省:并行任务审议", + "official": "侍中", + "org": "门下省", + "state": "Done", + "now": "已完成:准奏并归档", + "eta": "已完成 2026-02-23 10:27", + "block": "无", + "output": "/Users/bingsen/clawd/junjichu-v2/data/live_status.json", + "flow": { + "draft": "审议立案", + "review": "通过", + "dispatch": "准奏" + }, + "ac": "给出准奏结论", + "updatedAt": "2026-02-23T13:34:19.393358+00:00", + "outputMeta": { + "exists": true, + "lastModified": "2026-02-24 22:13:11" + }, + "heartbeat": null + }, + { + "id": "OC-zhongshu-af6ad7f2", + "title": "褚凤天 会话", + "official": "中书令", + "org": "中书省", + "state": "Review", + "now": "feishu/direct · 模型 claude-sonnet-4-6", + "eta": "2026-02-24 22:11:03", + "block": "无", + "output": "/Users/bingsen/.openclaw/agents/zhongshu/sessions/af6ad7f2-0130-4d88-81f0-dad420cc0dc7.jsonl", + "flow": { + "draft": "agent=zhongshu", + "review": "updatedAt=2026-02-24 22:11:03", + "dispatch": "sessionKey=agent:zhongshu:main" + }, + "ac": "来自 OpenClaw runtime sessions 的实时映射", + "activity": [ + { + "at": "2026-02-24T14:11:30.989Z", + "kind": "tool", + "text": "exec completed (code=0)" + }, + { + "at": "2026-02-24T14:11:42.050Z", + "kind": "assistant", + "text": "Screen Recording 权限未授权。换方案——用 `ffmpeg` + `screencapture` 组合录制:" + }, + { + "at": "2026-02-24T14:11:42.131Z", + "kind": "tool", + "text": "exec completed (code=1)" + }, + { + "at": "2026-02-24T14:12:05.109Z", + "kind": "tool", + "text": "exec completed (code=None)" + }, + { + "at": "2026-02-24T14:12:12.461Z", + "kind": "tool", + "text": "process completed (code=0)" + }, + { + "at": "2026-02-24T14:12:45.663Z", + "kind": "assistant", + "text": "Screen Recording 权限不足。换方案:用 **Playwright 无头浏览器**直接录制看板,无需系统权限:" + }, + { + "at": "2026-02-24T14:12:45.677Z", + "kind": "tool", + "text": "write completed (code=None)" + }, + { + "at": "2026-02-24T14:12:55.277Z", + "kind": "tool", + "text": "exec completed (code=None)" + }, + { + "at": "2026-02-24T14:13:06.413Z", + "kind": "assistant", + "text": "兵部 GIF 录制进行中,同步准备 Dockerfile:" + }, + { + "at": "2026-02-24T14:13:06.430Z", + "kind": "tool", + "text": "write completed (code=None)" + } + ], + "sourceMeta": { + "agentId": "zhongshu", + "sessionKey": "agent:zhongshu:main", + "sessionId": "af6ad7f2-0130-4d88-81f0-dad420cc0dc7", + "updatedAt": 1771942263194, + "ageMs": 123373, + "systemSent": true, + "abortedLastRun": false, + "inputTokens": 66, + "outputTokens": 2712, + "totalTokens": 148834 + }, + "outputMeta": { + "exists": true, + "lastModified": "2026-02-24 22:13:06" + }, + "heartbeat": { + "status": "active", + "label": "🟢 活跃 2分钟前", + "ageSec": 135 + } + }, + { + "id": "OC-mc-gateway-2f631e27-511b-45db-8d59-791c8c2cd4a5-7df8432c", + "title": "heartbeat 会话", + "official": "尚书令", + "org": "尚书省", + "state": "Review", + "now": "-/direct · 模型 claude-sonnet-4-6", + "eta": "2026-02-24 22:10:35", + "block": "无", + "output": "/Users/bingsen/.openclaw/agents/mc-gateway-2f631e27-511b-45db-8d59-791c8c2cd4a5/sessions/7df8432c-ff6e-4b4c-99f2-1301c2daebc3.jsonl", + "flow": { + "draft": "agent=mc-gateway-2f631e27-511b-45db-8d59-791c8c2cd4a5", + "review": "updatedAt=2026-02-24 22:10:35", + "dispatch": "sessionKey=agent:mc-gateway-2f631e27-511b-45db-8d59-791c8c2cd4a5:main" + }, + "ac": "来自 OpenClaw runtime sessions 的实时映射", + "activity": [ + { + "at": "2026-02-24T13:37:43.673Z", + "kind": "tool", + "text": "exec completed (code=52)" + }, + { + "at": "2026-02-24T13:37:47.447Z", + "kind": "assistant", + "text": "Check-in failed — `BASE_URL` is still `REPLACE_WITH_BASE_URL`. No `HEARTBEAT_OK`, no memory writes. Please configure the" + }, + { + "at": "2026-02-24T13:47:44.284Z", + "kind": "tool", + "text": "exec completed (code=52)" + }, + { + "at": "2026-02-24T13:47:47.405Z", + "kind": "assistant", + "text": "Check-in failed — `BASE_URL` is still `REPLACE_WITH_BASE_URL`. No `HEARTBEAT_OK`, no memory writes. Please configure the" + }, + { + "at": "2026-02-24T13:53:41.144Z", + "kind": "tool", + "text": "exec completed (code=52)" + }, + { + "at": "2026-02-24T13:53:44.932Z", + "kind": "assistant", + "text": "Check-in failed — `BASE_URL` is still `REPLACE_WITH_BASE_URL`. No `HEARTBEAT_OK`, no memory writes. Please configure the" + }, + { + "at": "2026-02-24T14:07:42.347Z", + "kind": "tool", + "text": "exec completed (code=52)" + }, + { + "at": "2026-02-24T14:07:45.391Z", + "kind": "assistant", + "text": "Check-in failed — `BASE_URL` is still `REPLACE_WITH_BASE_URL`. No `HEARTBEAT_OK`, no memory writes. Please configure the" + }, + { + "at": "2026-02-24T14:10:32.157Z", + "kind": "tool", + "text": "exec completed (code=52)" + }, + { + "at": "2026-02-24T14:10:35.416Z", + "kind": "assistant", + "text": "Check-in failed — `BASE_URL` is still `REPLACE_WITH_BASE_URL`. No `HEARTBEAT_OK`, no memory writes. Please configure the" + } + ], + "sourceMeta": { + "agentId": "mc-gateway-2f631e27-511b-45db-8d59-791c8c2cd4a5", + "sessionKey": "agent:mc-gateway-2f631e27-511b-45db-8d59-791c8c2cd4a5:main", + "sessionId": "7df8432c-ff6e-4b4c-99f2-1301c2daebc3", + "updatedAt": 1771942235439, + "ageMs": 151128, + "systemSent": true, + "abortedLastRun": false, + "inputTokens": 18, + "outputTokens": 292, + "totalTokens": 59134 + }, + "outputMeta": { + "exists": true, + "lastModified": "2026-02-24 22:10:35" + }, + "heartbeat": { + "status": "active", + "label": "🟢 活跃 2分钟前", + "ageSec": 163 + } + }, + { + "id": "OC-shangshu-8be927fe", + "title": "agent:shangshu:main 会话", + "official": "尚书令", + "org": "尚书省", + "state": "Review", + "now": "feishu/- · 模型 claude-sonnet-4-6", + "eta": "2026-02-24 22:10:18", + "block": "无", + "output": "/Users/bingsen/.openclaw/agents/shangshu/sessions/8be927fe-8360-449e-b02d-4c7d70d9f141.jsonl", + "flow": { + "draft": "agent=shangshu", + "review": "updatedAt=2026-02-24 22:10:18", + "dispatch": "sessionKey=agent:shangshu:main" + }, + "ac": "来自 OpenClaw runtime sessions 的实时映射", + "activity": [ + { + "at": "2026-02-24T14:08:13.069Z", + "kind": "assistant", + "text": "NO_REPLY ✅ Subagent bingbu finished ✅ 兵部完成 任务ID: JJC-20260224-014 产出: - `/Users/bingsen/clawd/junjichu-v2/docs/screens" + }, + { + "at": "2026-02-24T14:08:21.354Z", + "kind": "assistant", + "text": "三部全部完成,更新看板并回奏:" + }, + { + "at": "2026-02-24T14:08:21.538Z", + "kind": "tool", + "text": "exec completed (code=0)" + }, + { + "at": "2026-02-24T14:08:35.038Z", + "kind": "assistant", + "text": "📜 **奏折 · JJC-20260224-014 Week1 全部完成** A计划——三省六部 GitHub 爆款项目打造,四项任务已全部交付: --- **① ✅ Docker 演示镜像(工部)** - `junjichu-v2/" + }, + { + "at": "2026-02-24T14:08:35.061Z", + "kind": "tool", + "text": "message completed (code=None)" + }, + { + "at": "2026-02-24T14:08:45.903Z", + "kind": "tool", + "text": "sessions_send completed (code=None)" + }, + { + "at": "2026-02-24T14:08:49.235Z", + "kind": "assistant", + "text": "⏱️ Subagent bingbu timed out 所有产出文件就绪。任务完成。 --- ✅ **兵部完成** **任务ID**: JJC-20260224-014 **产出文件**: | 文件 | 大小 | 内容 | |--" + }, + { + "at": "2026-02-24T14:08:54.212Z", + "kind": "tool", + "text": "sessions_list completed (code=None)" + }, + { + "at": "2026-02-24T14:09:09.110Z", + "kind": "tool", + "text": "sessions_send completed (code=None)" + }, + { + "at": "2026-02-24T14:09:13.843Z", + "kind": "assistant", + "text": "NO_REPLY" + } + ], + "sourceMeta": { + "agentId": "shangshu", + "sessionKey": "agent:shangshu:main", + "sessionId": "8be927fe-8360-449e-b02d-4c7d70d9f141", + "updatedAt": 1771942218056, + "ageMs": 168511, + "systemSent": true, + "abortedLastRun": false, + "inputTokens": 50, + "outputTokens": 1930, + "totalTokens": 184496 + }, + "outputMeta": { + "exists": true, + "lastModified": "2026-02-24 22:10:18" + }, + "heartbeat": { + "status": "warn", + "label": "🟡 可能停滞 3分钟前", + "ageSec": 180 + } + }, + { + "id": "OC-gongbu-77f5e08c", + "title": "agent:gongbu:subagent:de641ed5-fefa-4d35-9ca1-6895fa103447 会话", + "official": "工部尚书", + "org": "工部", + "state": "Review", + "now": "webchat/- · 模型 claude-sonnet-4-6", + "eta": "2026-02-24 22:07:50", + "block": "无", + "output": "/Users/bingsen/.openclaw/agents/gongbu/sessions/77f5e08c-e75c-4094-b4db-071eae702f7d.jsonl", + "flow": { + "draft": "agent=gongbu", + "review": "updatedAt=2026-02-24 22:07:50", + "dispatch": "sessionKey=agent:gongbu:subagent:de641ed5-fefa-4d35-9ca1-6895fa103447" + }, + "ac": "来自 OpenClaw runtime sessions 的实时映射", + "activity": [ + { + "at": "2026-02-24T14:06:50.309Z", + "kind": "assistant", + "text": "验证 Topics 是否已写入:" + }, + { + "at": "2026-02-24T14:06:52.554Z", + "kind": "tool", + "text": "exec completed (code=0)" + }, + { + "at": "2026-02-24T14:06:59.820Z", + "kind": "assistant", + "text": "10 个 Topics 全部成功(另有 ai-orchestration/dashboard/openclaw 是原有 topics,已保留)。 快速验证 docker build 语法无误:" + }, + { + "at": "2026-02-24T14:07:00.115Z", + "kind": "tool", + "text": "exec completed (code=0)" + }, + { + "at": "2026-02-24T14:07:03.720Z", + "kind": "assistant", + "text": "实际构建验证:" + }, + { + "at": "2026-02-24T14:07:13.762Z", + "kind": "tool", + "text": "exec completed (code=None)" + }, + { + "at": "2026-02-24T14:07:34.472Z", + "kind": "tool", + "text": "process completed (code=0)" + }, + { + "at": "2026-02-24T14:07:38.180Z", + "kind": "assistant", + "text": "构建成功(exit code 0)。清理测试镜像:" + }, + { + "at": "2026-02-24T14:07:38.240Z", + "kind": "tool", + "text": "exec completed (code=0)" + }, + { + "at": "2026-02-24T14:07:50.919Z", + "kind": "assistant", + "text": "--- ✅ **工部完成** **任务ID: JJC-20260224-014** --- **① Docker 演示镜像** | 文件 | 路径 | |------|------| | Dockerfile | `/Users/b" + } + ], + "sourceMeta": { + "agentId": "gongbu", + "sessionKey": "agent:gongbu:subagent:de641ed5-fefa-4d35-9ca1-6895fa103447", + "sessionId": "77f5e08c-e75c-4094-b4db-071eae702f7d", + "updatedAt": 1771942070958, + "ageMs": 315609, + "systemSent": false, + "abortedLastRun": false, + "inputTokens": 122, + "outputTokens": 3681, + "totalTokens": 36930 + }, + "outputMeta": { + "exists": true, + "lastModified": "2026-02-24 22:07:50" + }, + "heartbeat": { + "status": "warn", + "label": "🟡 可能停滞 5分钟前", + "ageSec": 327 + } + }, + { + "id": "OC-libu-2f31143a", + "title": "agent:libu:subagent:9a63ae48-0851-4d63-8a37-0326007a24ff 会话", + "official": "礼部尚书", + "org": "礼部", + "state": "Review", + "now": "webchat/- · 模型 claude-sonnet-4-6", + "eta": "2026-02-24 22:07:43", + "block": "无", + "output": "/Users/bingsen/.openclaw/agents/libu/sessions/2f31143a-11f7-47fc-a2ef-86a65596486f.jsonl", + "flow": { + "draft": "agent=libu", + "review": "updatedAt=2026-02-24 22:07:43", + "dispatch": "sessionKey=agent:libu:subagent:9a63ae48-0851-4d63-8a37-0326007a24ff" + }, + "ac": "来自 OpenClaw runtime sessions 的实时映射", + "activity": [ + { + "at": "2026-02-24T14:05:54.062Z", + "kind": "assistant", + "text": "已接旨。开始执行任务③:README 升级。" + }, + { + "at": "2026-02-24T14:05:57.601Z", + "kind": "tool", + "text": "exec completed (code=0)" + }, + { + "at": "2026-02-24T14:05:57.617Z", + "kind": "tool", + "text": "exec completed (code=0)" + }, + { + "at": "2026-02-24T14:07:15.612Z", + "kind": "assistant", + "text": "本地仓库存在,已获取完整 README。现在生成更新后的内容并写入本地文件。" + }, + { + "at": "2026-02-24T14:07:15.632Z", + "kind": "tool", + "text": "write completed (code=None)" + }, + { + "at": "2026-02-24T14:07:23.206Z", + "kind": "assistant", + "text": "文件已写入,现在通过 git 提交并推送到 GitHub。" + }, + { + "at": "2026-02-24T14:07:23.308Z", + "kind": "tool", + "text": "exec completed (code=0)" + }, + { + "at": "2026-02-24T14:07:32.736Z", + "kind": "tool", + "text": "exec completed (code=0)" + }, + { + "at": "2026-02-24T14:07:43.725Z", + "kind": "assistant", + "text": "推送成功。 --- ✅ 礼部完成 **任务ID**: JJC-20260224-014 **产出**: README.md 已更新并推送至 `main` 分支(commit `87f0313`) **变更说明**: 1. **🆚 竞" + } + ], + "sourceMeta": { + "agentId": "libu", + "sessionKey": "agent:libu:subagent:9a63ae48-0851-4d63-8a37-0326007a24ff", + "sessionId": "2f31143a-11f7-47fc-a2ef-86a65596486f", + "updatedAt": 1771942063789, + "ageMs": 322778, + "systemSent": false, + "abortedLastRun": false, + "inputTokens": 42, + "outputTokens": 6566, + "totalTokens": 22863 + }, + "outputMeta": { + "exists": true, + "lastModified": "2026-02-24 22:07:43" + }, + "heartbeat": { + "status": "warn", + "label": "🟡 可能停滞 5分钟前", + "ageSec": 335 + } + }, + { + "id": "OC-bingbu-53af4339", + "title": "agent:bingbu:subagent:36d6c4fc-66c7-49f1-bf15-b4ce9613c0ba 会话", + "official": "兵部尚书", + "org": "兵部", + "state": "Review", + "now": "webchat/- · 模型 -", + "eta": "2026-02-24 22:05:48", + "block": "无", + "output": "/Users/bingsen/.openclaw/agents/bingbu/sessions/53af4339-e3f9-49ec-8162-9880d0c51e26.jsonl", + "flow": { + "draft": "agent=bingbu", + "review": "updatedAt=2026-02-24 22:05:48", + "dispatch": "sessionKey=agent:bingbu:subagent:36d6c4fc-66c7-49f1-bf15-b4ce9613c0ba" + }, + "ac": "来自 OpenClaw runtime sessions 的实时映射", + "activity": [ + { + "at": "2026-02-24T14:08:00.781Z", + "kind": "tool", + "text": "exec completed (code=0)" + }, + { + "at": "2026-02-24T14:08:08.885Z", + "kind": "assistant", + "text": "现在截取任务时间轴(点击任务卡片后的弹窗):" + }, + { + "at": "2026-02-24T14:08:09.776Z", + "kind": "tool", + "text": "exec completed (code=0)" + }, + { + "at": "2026-02-24T14:08:20.076Z", + "kind": "assistant", + "text": "三张截图完成!再截几张丰富动画内容(省部调度、早朝简报):" + }, + { + "at": "2026-02-24T14:08:24.006Z", + "kind": "tool", + "text": "exec completed (code=0)" + }, + { + "at": "2026-02-24T14:08:35.256Z", + "kind": "assistant", + "text": "5张截图全部就绪。现在用 ffmpeg 合成 GIF(约30秒,每帧停留6秒):" + }, + { + "at": "2026-02-24T14:08:36.519Z", + "kind": "tool", + "text": "exec completed (code=0)" + }, + { + "at": "2026-02-24T14:08:40.890Z", + "kind": "assistant", + "text": "GIF 生成成功!36秒,676K。确认所有产出文件:" + }, + { + "at": "2026-02-24T14:08:40.929Z", + "kind": "tool", + "text": "exec completed (code=1)" + }, + { + "at": "2026-02-24T14:08:48.124Z", + "kind": "assistant", + "text": "所有产出文件就绪。任务完成。 --- ✅ **兵部完成** **任务ID**: JJC-20260224-014 **产出文件**: | 文件 | 大小 | 内容 | |------|------|------| | `/Users/" + } + ], + "sourceMeta": { + "agentId": "bingbu", + "sessionKey": "agent:bingbu:subagent:36d6c4fc-66c7-49f1-bf15-b4ce9613c0ba", + "sessionId": "53af4339-e3f9-49ec-8162-9880d0c51e26", + "updatedAt": 1771941948040, + "ageMs": 438527, + "systemSent": false, + "abortedLastRun": false, + "inputTokens": null, + "outputTokens": null, + "totalTokens": null + }, + "outputMeta": { + "exists": true, + "lastModified": "2026-02-24 22:09:01" + }, + "heartbeat": { + "status": "warn", + "label": "🟡 可能停滞 7分钟前", + "ageSec": 450 + } + }, + { + "id": "OC-shangshu-ef5d83a2", + "title": "agent:shangshu:cron:3d21409f-eb0b-4f13-9f00-283e249e1704 会话", + "official": "尚书令", + "org": "尚书省", + "state": "Next", + "now": "-/- · 模型 -", + "eta": "2026-02-24 06:19:47", + "block": "无", + "output": "", + "flow": { + "draft": "agent=shangshu", + "review": "updatedAt=2026-02-24 06:19:47", + "dispatch": "sessionKey=agent:shangshu:cron:3d21409f-eb0b-4f13-9f00-283e249e1704" + }, + "ac": "来自 OpenClaw runtime sessions 的实时映射", + "activity": [], + "sourceMeta": { + "agentId": "shangshu", + "sessionKey": "agent:shangshu:cron:3d21409f-eb0b-4f13-9f00-283e249e1704", + "sessionId": "ef5d83a2-9e5a-42f1-b16c-396ab2f8b871", + "updatedAt": 1771885187176, + "ageMs": 57199391, + "systemSent": true, + "abortedLastRun": false, + "inputTokens": null, + "outputTokens": null, + "totalTokens": null + }, + "outputMeta": { + "exists": true, + "lastModified": "2026-02-23 22:07:11" + }, + "heartbeat": null + }, + { + "id": "OC-shangshu-ef5d83a2", + "title": "agent:shangshu:cron:3d21409f-eb0b-4f13-9f00-283e249e1704:run:ef5d83a2-9e5a-42f1-b16c-396ab2f8b871 会话", + "official": "尚书令", + "org": "尚书省", + "state": "Next", + "now": "-/- · 模型 -", + "eta": "2026-02-24 06:19:47", + "block": "无", + "output": "", + "flow": { + "draft": "agent=shangshu", + "review": "updatedAt=2026-02-24 06:19:47", + "dispatch": "sessionKey=agent:shangshu:cron:3d21409f-eb0b-4f13-9f00-283e249e1704:run:ef5d83a2-9e5a-42f1-b16c-396ab2f8b871" + }, + "ac": "来自 OpenClaw runtime sessions 的实时映射", + "activity": [], + "sourceMeta": { + "agentId": "shangshu", + "sessionKey": "agent:shangshu:cron:3d21409f-eb0b-4f13-9f00-283e249e1704:run:ef5d83a2-9e5a-42f1-b16c-396ab2f8b871", + "sessionId": "ef5d83a2-9e5a-42f1-b16c-396ab2f8b871", + "updatedAt": 1771885187130, + "ageMs": 57199437, + "systemSent": true, + "abortedLastRun": false, + "inputTokens": null, + "outputTokens": null, + "totalTokens": null + }, + "outputMeta": { + "exists": true, + "lastModified": "2026-02-23 22:07:11" + }, + "heartbeat": null + }, + { + "id": "OC-shangshu-5d3c50db", + "title": "agent:shangshu:cron:b03078fe-c25d-4a65-a05a-2f7a9453ddb2 会话", + "official": "尚书令", + "org": "尚书省", + "state": "Next", + "now": "-/- · 模型 claude-sonnet-4-6", + "eta": "2026-02-24 06:03:01", + "block": "无", + "output": "", + "flow": { + "draft": "agent=shangshu", + "review": "updatedAt=2026-02-24 06:03:01", + "dispatch": "sessionKey=agent:shangshu:cron:b03078fe-c25d-4a65-a05a-2f7a9453ddb2" + }, + "ac": "来自 OpenClaw runtime sessions 的实时映射", + "activity": [], + "sourceMeta": { + "agentId": "shangshu", + "sessionKey": "agent:shangshu:cron:b03078fe-c25d-4a65-a05a-2f7a9453ddb2", + "sessionId": "5d3c50db-0b91-4ff0-a9eb-26114ffb4339", + "updatedAt": 1771884181137, + "ageMs": 58205430, + "systemSent": true, + "abortedLastRun": false, + "inputTokens": 122, + "outputTokens": 16126, + "totalTokens": 43619 + }, + "outputMeta": { + "exists": true, + "lastModified": "2026-02-23 22:07:11" + }, + "heartbeat": null + }, + { + "id": "OC-shangshu-5d3c50db", + "title": "agent:shangshu:cron:b03078fe-c25d-4a65-a05a-2f7a9453ddb2:run:5d3c50db-0b91-4ff0-a9eb-26114ffb4339 会话", + "official": "尚书令", + "org": "尚书省", + "state": "Next", + "now": "-/- · 模型 claude-sonnet-4-6", + "eta": "2026-02-24 06:03:01", + "block": "无", + "output": "", + "flow": { + "draft": "agent=shangshu", + "review": "updatedAt=2026-02-24 06:03:01", + "dispatch": "sessionKey=agent:shangshu:cron:b03078fe-c25d-4a65-a05a-2f7a9453ddb2:run:5d3c50db-0b91-4ff0-a9eb-26114ffb4339" + }, + "ac": "来自 OpenClaw runtime sessions 的实时映射", + "activity": [], + "sourceMeta": { + "agentId": "shangshu", + "sessionKey": "agent:shangshu:cron:b03078fe-c25d-4a65-a05a-2f7a9453ddb2:run:5d3c50db-0b91-4ff0-a9eb-26114ffb4339", + "sessionId": "5d3c50db-0b91-4ff0-a9eb-26114ffb4339", + "updatedAt": 1771884181137, + "ageMs": 58205430, + "systemSent": true, + "abortedLastRun": false, + "inputTokens": 122, + "outputTokens": 16126, + "totalTokens": 43619 + }, + "outputMeta": { + "exists": true, + "lastModified": "2026-02-23 22:07:11" + }, + "heartbeat": null + }, + { + "id": "OC-shangshu-aaa29ca8", + "title": "agent:shangshu:cron:b03078fe-c25d-4a65-a05a-2f7a9453ddb2:run:aaa29ca8-46d1-4dfd-87a8-c474f9bb8227 会话", + "official": "尚书令", + "org": "尚书省", + "state": "Next", + "now": "-/- · 模型 claude-sonnet-4-6", + "eta": "2026-02-24 00:12:22", + "block": "无", + "output": "", + "flow": { + "draft": "agent=shangshu", + "review": "updatedAt=2026-02-24 00:12:22", + "dispatch": "sessionKey=agent:shangshu:cron:b03078fe-c25d-4a65-a05a-2f7a9453ddb2:run:aaa29ca8-46d1-4dfd-87a8-c474f9bb8227" + }, + "ac": "来自 OpenClaw runtime sessions 的实时映射", + "activity": [], + "sourceMeta": { + "agentId": "shangshu", + "sessionKey": "agent:shangshu:cron:b03078fe-c25d-4a65-a05a-2f7a9453ddb2:run:aaa29ca8-46d1-4dfd-87a8-c474f9bb8227", + "sessionId": "aaa29ca8-46d1-4dfd-87a8-c474f9bb8227", + "updatedAt": 1771863142993, + "ageMs": 79243574, + "systemSent": true, + "abortedLastRun": false, + "inputTokens": 122, + "outputTokens": 16126, + "totalTokens": 43619 + }, + "outputMeta": { + "exists": true, + "lastModified": "2026-02-23 22:07:11" + }, + "heartbeat": null + }, + { + "id": "MC-adf92a7e", + "title": "兵部:Mission Control 映射验收", + "official": "兵部尚书", + "org": "兵部", + "state": "Done", + "now": "Mission Control 映射已验收完成,系统正常运行", + "eta": "-", + "block": "无", + "output": "mission-control://boards/2ed2acde-f66f-46f9-ad3b-bc502dfffa83/tasks/adf92a7e-def5-4e67-b304-666ec74aaa34", + "flow": { + "draft": "board=军机处-兵部", + "review": "门下通过", + "dispatch": "mission-control" + }, + "ac": "Mission Control 映射;审批 pending=0, rejected=0", + "sourceMeta": { + "source": "mission_control", + "boardId": "2ed2acde-f66f-46f9-ad3b-bc502dfffa83", + "taskId": "adf92a7e-def5-4e67-b304-666ec74aaa34", + "approvalPending": 0, + "approvalRejected": 0, + "updatedAt": 1771857504947 + }, + "updatedAt": "2026-02-23T22:55:13.612669", + "outputMeta": { + "exists": false, + "lastModified": null + }, + "heartbeat": null + }, + { + "id": "OC-main-de50c4d8", + "title": "褚凤天 会话", + "official": "工部尚书", + "org": "工部", + "state": "Next", + "now": "feishu/direct · 模型 gpt-5.3-codex", + "eta": "2026-02-23 15:20:53", + "block": "无", + "output": "/Users/bingsen/.openclaw/agents/main/sessions/de50c4d8-6d36-470f-8a32-a55973726117.jsonl", + "flow": { + "draft": "agent=main", + "review": "updatedAt=2026-02-23 15:20:53", + "dispatch": "sessionKey=agent:main:main" + }, + "ac": "来自 OpenClaw runtime sessions 的实时映射", + "activity": [ + { + "at": "2026-02-23T07:19:47.493Z", + "kind": "tool", + "text": "exec completed (code=0)" + }, + { + "at": "2026-02-23T07:19:53.313Z", + "kind": "tool", + "text": "exec completed (code=0)" + }, + { + "at": "2026-02-23T07:20:00.800Z", + "kind": "tool", + "text": "exec completed (code=0)" + }, + { + "at": "2026-02-23T07:20:09.848Z", + "kind": "tool", + "text": "exec completed (code=0)" + }, + { + "at": "2026-02-23T07:20:16.787Z", + "kind": "tool", + "text": "exec completed (code=0)" + }, + { + "at": "2026-02-23T07:20:20.771Z", + "kind": "tool", + "text": "exec completed (code=0)" + }, + { + "at": "2026-02-23T07:20:27.075Z", + "kind": "tool", + "text": "exec completed (code=0)" + }, + { + "at": "2026-02-23T07:20:37.053Z", + "kind": "tool", + "text": "exec completed (code=0)" + }, + { + "at": "2026-02-23T07:20:44.651Z", + "kind": "tool", + "text": "exec completed (code=0)" + }, + { + "at": "2026-02-23T07:20:53.112Z", + "kind": "assistant", + "text": "[[reply_to_current]]回父皇:**好了**,已打通。😊 刚才您看不到 Pair/Approve 的根因,儿臣已代为走完: - 用 CLI 批准了挂起配对请求(`openclaw devices approve --lat" + } + ], + "sourceMeta": { + "agentId": "main", + "sessionKey": "agent:main:main", + "sessionId": "de50c4d8-6d36-470f-8a32-a55973726117", + "updatedAt": 1771831253211, + "ageMs": 111133356, + "systemSent": true, + "abortedLastRun": false, + "inputTokens": 443258, + "outputTokens": 2354, + "totalTokens": 148178 + }, + "outputMeta": { + "exists": true, + "lastModified": "2026-02-23 15:20:53" + }, + "heartbeat": null + } + ], + "history": [ + { + "at": "未知", + "official": "中书令", + "task": "每日早朝简报系统(7点图文新闻)", + "out": "http://127.0.0.1:7891 → 🌅 早朝简报", + "qa": "待补成果" + }, + { + "at": "未知", + "official": "中书令", + "task": "看板流程合规审查 + 省部调度Tab优化", + "out": "agents/*/SOUL.md + dashboard.html省部调度Tab", + "qa": "待补成果" + }, + { + "at": "未知", + "official": "中书令", + "task": "官员总览看板重新设计", + "out": "http://127.0.0.1:7891 → 👥 官员总览", + "qa": "待补成果" + }, + { + "at": "未知", + "official": "中书令", + "task": "看板增加官员总览面板", + "out": "http://127.0.0.1:7891 → 👥 官员总览", + "qa": "待补成果" + }, + { + "at": "未知", + "official": "中书令", + "task": "三省六部制开源 GitHub 项目", + "out": "https://github.com/cft0808/openclaw-sansheng-liubu", + "qa": "待补成果" + }, + { + "at": "2026-02-23 21:27:18", + "official": "尚书令", + "task": "电芯循环测试数据预测算法设计与可视化验证", + "out": "/Users/bingsen/.openclaw/workspace-shangshu/", + "qa": "通过" + }, + { + "at": "2026-02-23 10:05:17", + "official": "中书令", + "task": "中书省:并行流程规划", + "out": "/Users/bingsen/clawd/junjichu-v2/docs/三省六部-OpenClaw实施指南-实装版-v1.md", + "qa": "通过" + }, + { + "at": "2026-02-24 21:51:23", + "official": "兵部尚书", + "task": "兵部:看板细节渲染修复", + "out": "/Users/bingsen/clawd/junjichu-v2/dashboard.html", + "qa": "通过" + }, + { + "at": "2026-02-24 22:13:11", + "official": "侍中", + "task": "门下省:并行任务审议", + "out": "/Users/bingsen/clawd/junjichu-v2/data/live_status.json", + "qa": "通过" + }, + { + "at": "未知", + "official": "兵部尚书", + "task": "兵部:Mission Control 映射验收", + "out": "mission-control://boards/2ed2acde-f66f-46f9-ad3b-bc502dfffa83/tasks/adf92a7e-def5-4e67-b304-666ec74aaa34", + "qa": "待补成果" + } + ], + "metrics": { + "officialCount": 9, + "todayDone": 10, + "inProgress": 13, + "blocked": 0 + }, + "syncStatus": { + "ok": true, + "statusLevel": "ok", + "consecutiveFailures": 0, + "lastSuccessAt": "2026-02-24 22:13:06", + "lastSyncAt": "2026-02-24 22:13:06", + "durationMs": 4720, + "source": "openclaw_runtime_to_feishu_bitable", + "recordCount": 24, + "created": 0, + "updated": 3, + "skipped": 21, + "archived": 0, + "missingFields": {}, + "error": null + }, + "health": { + "syncOk": true, + "syncLatencyMs": 4720, + "missingFieldCount": 0 + } +} + + + +[] + + + +{ + "date": "20260226", + "generated_at": "2026-02-26 06:30:00", + "categories": { + "AI大模型": [ + { + "title": "Anthropic 发布 Claude Opus 4.6,推理能力再创新高", + "summary": "Anthropic 宣布推出 Claude Opus 4.6 模型,在复杂推理和代码生成任务上取得重大突破,同时保持安全对齐。", + "source": "TechCrunch", + "url": "https://example.com/opus46", + "time": "2026-02-25" + }, + { + "title": "OpenAI Codex GPT-5.3 支持百万行代码库上下文", + "summary": "OpenAI 发布 Codex GPT-5.3,上下文窗口扩展至2M tokens,可一次性理解整个大型代码库。", + "source": "The Verge", + "url": "https://example.com/codex53", + "time": "2026-02-25" + } + ], + "经济": [ + { + "title": "全球半导体产业链加速向东南亚布局", + "summary": "台积电、三星等芯片巨头宣布新一轮东南亚投资计划,越南和马来西亚成主要受益者。", + "source": "日经新闻", + "url": "https://example.com/semi", + "time": "2026-02-25" + } + ], + "政治": [ + { + "title": "欧盟 AI 法案实施细则正式公布", + "summary": "欧盟委员会发布 AI 法案最终实施细则,明确高风险 AI 系统的审计和透明度要求。", + "source": "Reuters", + "url": "https://example.com/euai", + "time": "2026-02-25" + } + ] + } +} + + + +{ + "generatedAt": "2026-02-24 22:13:06", + "officials": [ + { + "id": "zhongshu", + "label": "中书省", + "role": "中书令", + "emoji": "📜", + "rank": "正一品", + "model": "anthropic/claude-sonnet-4-6", + "model_short": "claude-sonnet-4-6", + "sessions": 1, + "tokens_in": 66, + "tokens_out": 2712, + "cache_read": 1027806, + "cache_write": 148826, + "tokens_total": 2778, + "messages": 177, + "cost_usd": 0.9073, + "cost_cny": 6.58, + "last_active": "2026-02-24 22:11", + "heartbeat": { + "status": "active", + "label": "🟢 活跃 2分钟前", + "ageSec": 120 + }, + "tasks_done": 6, + "tasks_active": 2, + "flow_participations": 23, + "participated_edicts": [ + { + "id": "JJC-20260224-014", + "title": "A计划:三省六部 GitHub 爆款项目打造", + "state": "Doing" + }, + { + "id": "JJC-20260224-013", + "title": "电芯循环数据清洗小模型方案", + "state": "Zhongshu" + }, + { + "id": "JJC-20260224-001", + "title": "每日早朝简报系统(7点图文新闻)", + "state": "Done" + }, + { + "id": "JJC-20260223-014", + "title": "看板流程合规审查 + 省部调度Tab优化", + "state": "Done" + }, + { + "id": "JJC-20260223-013", + "title": "官员总览看板重新设计", + "state": "Done" + }, + { + "id": "JJC-20260223-012", + "title": "看板增加官员总览面板", + "state": "Done" + }, + { + "id": "JJC-20260223-011", + "title": "三省六部制开源 GitHub 项目", + "state": "Done" + }, + { + "id": "JJC-20260223-010", + "title": "电芯循环测试数据预测算法设计与可视化验证", + "state": "Done" + } + ], + "merit_score": 107, + "merit_rank": 1 + }, + { + "id": "shangshu", + "label": "尚书省", + "role": "尚书令", + "emoji": "📮", + "rank": "正一品", + "model": "anthropic/claude-sonnet-4-6", + "model_short": "claude-sonnet-4-6", + "sessions": 6, + "tokens_in": 416, + "tokens_out": 50308, + "cache_read": 2196943, + "cache_write": 136506, + "tokens_total": 50724, + "messages": 187, + "cost_usd": 1.9268, + "cost_cny": 13.97, + "last_active": "2026-02-24 22:10", + "heartbeat": { + "status": "active", + "label": "🟢 活跃 2分钟前", + "ageSec": 165 + }, + "tasks_done": 1, + "tasks_active": 2, + "flow_participations": 23, + "participated_edicts": [ + { + "id": "JJC-20260224-014", + "title": "A计划:三省六部 GitHub 爆款项目打造", + "state": "Doing" + }, + { + "id": "JJC-20260224-001", + "title": "每日早朝简报系统(7点图文新闻)", + "state": "Done" + }, + { + "id": "JJC-20260223-014", + "title": "看板流程合规审查 + 省部调度Tab优化", + "state": "Done" + }, + { + "id": "JJC-20260223-013", + "title": "官员总览看板重新设计", + "state": "Done" + }, + { + "id": "JJC-20260223-012", + "title": "看板增加官员总览面板", + "state": "Done" + }, + { + "id": "JJC-20260223-010", + "title": "电芯循环测试数据预测算法设计与可视化验证", + "state": "Done" + } + ], + "merit_score": 62, + "merit_rank": 2 + }, + { + "id": "menxia", + "label": "门下省", + "role": "侍中", + "emoji": "🔍", + "rank": "正一品", + "model": "anthropic/claude-sonnet-4-6", + "model_short": "claude-sonnet-4-6", + "sessions": 0, + "tokens_in": 0, + "tokens_out": 0, + "cache_read": 0, + "cache_write": 0, + "tokens_total": 0, + "messages": 0, + "cost_usd": 0.0, + "cost_cny": 0.0, + "last_active": null, + "heartbeat": { + "status": "idle", + "label": "⚪ 待命", + "ageSec": null + }, + "tasks_done": 1, + "tasks_active": 0, + "flow_participations": 12, + "participated_edicts": [ + { + "id": "JJC-20260224-001", + "title": "每日早朝简报系统(7点图文新闻)", + "state": "Done" + }, + { + "id": "JJC-20260223-014", + "title": "看板流程合规审查 + 省部调度Tab优化", + "state": "Done" + }, + { + "id": "JJC-20260223-013", + "title": "官员总览看板重新设计", + "state": "Done" + }, + { + "id": "JJC-20260223-012", + "title": "看板增加官员总览面板", + "state": "Done" + }, + { + "id": "JJC-20260223-010", + "title": "电芯循环测试数据预测算法设计与可视化验证", + "state": "Done" + } + ], + "merit_score": 34, + "merit_rank": 3 + }, + { + "id": "bingbu", + "label": "兵部", + "role": "兵部尚书", + "emoji": "⚔️", + "rank": "正二品", + "model": "anthropic/claude-sonnet-4-6", + "model_short": "claude-sonnet-4-6", + "sessions": 1, + "tokens_in": 0, + "tokens_out": 0, + "cache_read": 0, + "cache_write": 0, + "tokens_total": 0, + "messages": 26, + "cost_usd": 0.0, + "cost_cny": 0.0, + "last_active": "2026-02-24 22:05", + "heartbeat": { + "status": "warn", + "label": "🟡 可能停滞 7分钟前", + "ageSec": 435 + }, + "tasks_done": 2, + "tasks_active": 1, + "flow_participations": 2, + "participated_edicts": [ + { + "id": "JJC-20260223-010", + "title": "电芯循环测试数据预测算法设计与可视化验证", + "state": "Done" + } + ], + "merit_score": 25, + "merit_rank": 4 + }, + { + "id": "gongbu", + "label": "工部", + "role": "工部尚书", + "emoji": "🔧", + "rank": "正二品", + "model": "anthropic/claude-sonnet-4-6", + "model_short": "claude-sonnet-4-6", + "sessions": 1, + "tokens_in": 122, + "tokens_out": 3681, + "cache_read": 427760, + "cache_write": 36922, + "tokens_total": 3803, + "messages": 15, + "cost_usd": 0.3224, + "cost_cny": 2.34, + "last_active": "2026-02-24 22:07", + "heartbeat": { + "status": "warn", + "label": "🟡 可能停滞 5分钟前", + "ageSec": 312 + }, + "tasks_done": 0, + "tasks_active": 1, + "flow_participations": 8, + "participated_edicts": [ + { + "id": "JJC-20260224-001", + "title": "每日早朝简报系统(7点图文新闻)", + "state": "Done" + }, + { + "id": "JJC-20260223-014", + "title": "看板流程合规审查 + 省部调度Tab优化", + "state": "Done" + }, + { + "id": "JJC-20260223-013", + "title": "官员总览看板重新设计", + "state": "Done" + }, + { + "id": "JJC-20260223-012", + "title": "看板增加官员总览面板", + "state": "Done" + }, + { + "id": "JJC-20260223-011", + "title": "三省六部制开源 GitHub 项目", + "state": "Done" + } + ], + "merit_score": 17, + "merit_rank": 5 + }, + { + "id": "libu", + "label": "礼部", + "role": "礼部尚书", + "emoji": "📝", + "rank": "正二品", + "model": "anthropic/claude-sonnet-4-6", + "model_short": "claude-sonnet-4-6", + "sessions": 1, + "tokens_in": 42, + "tokens_out": 6566, + "cache_read": 73286, + "cache_write": 22855, + "tokens_total": 6608, + "messages": 5, + "cost_usd": 0.2063, + "cost_cny": 1.5, + "last_active": "2026-02-24 22:07", + "heartbeat": { + "status": "warn", + "label": "🟡 可能停滞 5分钟前", + "ageSec": 320 + }, + "tasks_done": 0, + "tasks_active": 1, + "flow_participations": 4, + "participated_edicts": [ + { + "id": "JJC-20260223-011", + "title": "三省六部制开源 GitHub 项目", + "state": "Done" + }, + { + "id": "JJC-20260223-010", + "title": "电芯循环测试数据预测算法设计与可视化验证", + "state": "Done" + } + ], + "merit_score": 9, + "merit_rank": 6 + }, + { + "id": "hubu", + "label": "户部", + "role": "户部尚书", + "emoji": "💰", + "rank": "正二品", + "model": "anthropic/claude-sonnet-4-6", + "model_short": "claude-sonnet-4-6", + "sessions": 0, + "tokens_in": 0, + "tokens_out": 0, + "cache_read": 0, + "cache_write": 0, + "tokens_total": 0, + "messages": 0, + "cost_usd": 0.0, + "cost_cny": 0.0, + "last_active": null, + "heartbeat": { + "status": "idle", + "label": "⚪ 待命", + "ageSec": null + }, + "tasks_done": 0, + "tasks_active": 0, + "flow_participations": 3, + "participated_edicts": [ + { + "id": "JJC-20260223-010", + "title": "电芯循环测试数据预测算法设计与可视化验证", + "state": "Done" + } + ], + "merit_score": 6, + "merit_rank": 7 + }, + { + "id": "xingbu", + "label": "刑部", + "role": "刑部尚书", + "emoji": "⚖️", + "rank": "正二品", + "model": "anthropic/claude-sonnet-4-6", + "model_short": "claude-sonnet-4-6", + "sessions": 0, + "tokens_in": 0, + "tokens_out": 0, + "cache_read": 0, + "cache_write": 0, + "tokens_total": 0, + "messages": 0, + "cost_usd": 0.0, + "cost_cny": 0.0, + "last_active": null, + "heartbeat": { + "status": "idle", + "label": "⚪ 待命", + "ageSec": null + }, + "tasks_done": 0, + "tasks_active": 0, + "flow_participations": 0, + "participated_edicts": [], + "merit_score": 0, + "merit_rank": 8 + } + ], + "totals": { + "tokens_total": 63913, + "cache_total": 4070904, + "cost_usd": 3.36, + "cost_cny": 24.39, + "tasks_done": 10 + }, + "top_official": "中书省" +} + + + +[] + + + +[ + { + "id": "JJC-20260224-001", + "title": "生成本周项目进展周报", + "official": "礼部尚书", + "org": "礼部", + "state": "Done", + "now": "✅ 周报已生成并推送至飞书", + "eta": "2026-02-24", + "block": "无", + "output": "/tmp/weekly-report-20260224.md", + "ac": "完成周报需包含各部产出汇总和下周计划", + "review_round": 1, + "archived": true, + "archivedAt": "2026-02-24T16:00:00Z", + "flow_log": [ + {"at": "2026-02-24T09:00:00Z", "from": "皇上", "to": "中书省", "remark": "📜 皇上下旨:生成本周项目进展周报"}, + {"at": "2026-02-24T09:02:30Z", "from": "中书省", "to": "门下省", "remark": "📋 中书省规划完成 · 交付门下省审议"}, + {"at": "2026-02-24T09:05:00Z", "from": "门下省", "to": "尚书省", "remark": "✅ 门下省审议通过 · 移交尚书省派发"}, + {"at": "2026-02-24T09:06:00Z", "from": "尚书省", "to": "礼部", "remark": "📮 尚书省派单 → 礼部执行"}, + {"at": "2026-02-24T09:15:00Z", "from": "礼部", "to": "尚书省", "remark": "📦 礼部执行完成 · 产出物已提交"}, + {"at": "2026-02-24T09:16:00Z", "from": "尚书省", "to": "皇上", "remark": "✅ 回奏:周报任务完成"} + ], + "updatedAt": "2026-02-24T09:16:00Z" + }, + { + "id": "JJC-20260225-001", + "title": "对 edict 仓库进行代码审查", + "official": "兵部尚书", + "org": "兵部", + "state": "Doing", + "now": "🔍 兵部正在审查 dashboard 模块代码", + "eta": "2026-02-25", + "block": "无", + "output": "", + "ac": "输出安全漏洞、错误处理、性能问题清单", + "review_round": 0, + "flow_log": [ + {"at": "2026-02-25T10:00:00Z", "from": "皇上", "to": "中书省", "remark": "📜 皇上下旨:对 edict 仓库进行代码审查"}, + {"at": "2026-02-25T10:03:00Z", "from": "中书省", "to": "门下省", "remark": "📋 中书省规划完成"}, + {"at": "2026-02-25T10:06:00Z", "from": "门下省", "to": "尚书省", "remark": "✅ 门下省审议通过"}, + {"at": "2026-02-25T10:07:00Z", "from": "尚书省", "to": "兵部", "remark": "📮 尚书省派单 → 兵部执行"} + ], + "updatedAt": "2026-02-25T10:30:00Z" + }, + { + "id": "JJC-20260225-002", + "title": "更新并推送 Docker Hub 镜像", + "official": "工部尚书", + "org": "工部", + "state": "Done", + "now": "✅ Docker 镜像已推送", + "eta": "2026-02-25", + "block": "无", + "output": "/tmp/docker-push-log.txt", + "ac": "更新 Dockerfile, 构建并推送至 DockerHub", + "review_round": 0, + "flow_log": [ + {"at": "2026-02-25T14:00:00Z", "from": "皇上", "to": "中书省", "remark": "📜 皇上下旨:更新 Docker 镜像"}, + {"at": "2026-02-25T14:02:00Z", "from": "中书省", "to": "门下省", "remark": "📋 规划完成"}, + {"at": "2026-02-25T14:04:00Z", "from": "门下省", "to": "尚书省", "remark": "✅ 审议通过"}, + {"at": "2026-02-25T14:05:00Z", "from": "尚书省", "to": "工部", "remark": "📮 派单 → 工部"}, + {"at": "2026-02-25T14:20:00Z", "from": "工部", "to": "尚书省", "remark": "📦 执行完成"}, + {"at": "2026-02-25T14:21:00Z", "from": "尚书省", "to": "皇上", "remark": "✅ 回奏完成"} + ], + "updatedAt": "2026-02-25T14:21:00Z" + }, + { + "id": "JJC-20260226-001", + "title": "竞品分析:CrewAI vs AutoGen vs 三省六部", + "official": "户部尚书", + "org": "户部", + "state": "Review", + "now": "📊 分析报告待审查", + "eta": "2026-02-26", + "block": "无", + "output": "/tmp/competitive-analysis.md", + "ac": "从架构、审核、可观测性、易用性四个维度对比", + "review_round": 2, + "flow_log": [ + {"at": "2026-02-26T08:00:00Z", "from": "皇上", "to": "中书省", "remark": "📜 下旨:分析竞品框架"}, + {"at": "2026-02-26T08:05:00Z", "from": "中书省", "to": "门下省", "remark": "📋 规划完成"}, + {"at": "2026-02-26T08:08:00Z", "from": "门下省", "to": "中书省", "remark": "🔄 门下省封驳:需补充 LangGraph 对比"}, + {"at": "2026-02-26T08:15:00Z", "from": "中书省", "to": "门下省", "remark": "📋 已补充 LangGraph,重新提交"}, + {"at": "2026-02-26T08:18:00Z", "from": "门下省", "to": "尚书省", "remark": "✅ 第二轮审议通过"}, + {"at": "2026-02-26T08:20:00Z", "from": "尚书省", "to": "户部", "remark": "📮 派单 → 户部"}, + {"at": "2026-02-26T09:00:00Z", "from": "户部", "to": "尚书省", "remark": "📦 初稿完成,待审查"} + ], + "updatedAt": "2026-02-26T09:00:00Z" + } +] + + + +# 远程 Skills 资源管理指南 + +## 概述 + +三省六部现已支持从网上连接和增补 skills 资源,无需手动复制文件。支持从以下来源获取: + +- **GitHub 仓库** (raw.githubusercontent.com) +- **任何 HTTPS URL** (需返回有效的 skill 文件) +- **本地文件路径** +- **内置仓库** (官方 skills 库) + +--- + +## 功能架构 + +### 1. API 端点 + +#### `POST /api/add-remote-skill` + +从远程 URL 或本地路径为指定 Agent 添加 skill。 + +**请求体:** +```json +{ + "agentId": "zhongshu", + "skillName": "code_review", + "sourceUrl": "https://raw.githubusercontent.com/org/skills-repo/main/code_review/SKILL.md", + "description": "代码审查专项技能" +} +``` + +**参数说明:** +- `agentId` (string, 必需): 目标 Agent ID (验证有效性) +- `skillName` (string, 必需): skill 的内部名称 (仅允许字母/数字/下划线/汉字) +- `sourceUrl` (string, 必需): 远程 URL 或本地文件路径 + - GitHub: `https://raw.githubusercontent.com/user/repo/branch/path/SKILL.md` + - 任意 HTTPS: `https://example.com/skills/my_skill.md` + - 本地: `file:///Users/bingsen/skills/code_review.md` 或 `/Users/bingsen/skills/code_review.md` +- `description` (string, 可选): skill 的中文描述 + +**响应成功 (200):** +```json +{ + "ok": true, + "message": "技能 code_review 已添加到 zhongshu", + "skillName": "code_review", + "agentId": "zhongshu", + "source": "https://raw.githubusercontent.com/...", + "localPath": "/Users/bingsen/.openclaw/workspace-zhongshu/skills/code_review/SKILL.md", + "size": 2048, + "addedAt": "2026-03-02T14:30:00Z" +} +``` + +**响应失败 (400):** +```json +{ + "ok": false, + "error": "URL 无效或无法访问", + "details": "Connection timeout after 10s" +} +``` + +#### `GET /api/remote-skills-list` + +列出所有已添加的远程 skills 及其源信息。 + +**响应:** +```json +{ + "ok": true, + "remoteSkills": [ + { + "skillName": "code_review", + "agentId": "zhongshu", + "sourceUrl": "https://raw.githubusercontent.com/org/skills-repo/main/code_review/SKILL.md", + "description": "代码审查专项技能", + "localPath": "/Users/bingsen/.openclaw/workspace-zhongshu/skills/code_review/SKILL.md", + "lastUpdated": "2026-03-02T14:30:00Z", + "status": "valid" // valid | invalid | not-found + } + ], + "count": 5 +} +``` + +#### `POST /api/update-remote-skill` + +更新已添加的远程 skill 为最新版本。 + +**请求体:** +```json +{ + "agentId": "zhongshu", + "skillName": "code_review" +} +``` + +**响应:** +```json +{ + "ok": true, + "message": "技能已更新", + "skillName": "code_review", + "newVersion": "2.1.0", + "updatedAt": "2026-03-02T15:00:00Z" +} +``` + +#### `DELETE /api/remove-remote-skill` + +移除已添加的远程 skill。 + +**请求体:** +```json +{ + "agentId": "zhongshu", + "skillName": "code_review" +} +``` + +--- + +## CLI 命令 + +### 添加远程 Skill + +```bash +python3 scripts/skill_manager.py add-remote \ + --agent zhongshu \ + --name code_review \ + --source https://raw.githubusercontent.com/org/skills-repo/main/code_review/SKILL.md \ + --description "代码审查专项技能" +``` + +### 列出远程 Skills + +```bash +python3 scripts/skill_manager.py list-remote +``` + +### 更新远程 Skill + +```bash +python3 scripts/skill_manager.py update-remote \ + --agent zhongshu \ + --name code_review +``` + +### 移除远程 Skill + +```bash +python3 scripts/skill_manager.py remove-remote \ + --agent zhongshu \ + --name code_review +``` + +--- + +## 官方 Skills 库 + +### OpenClaw Skills Hub + +> **官方 skills 库地址**: https://github.com/openclaw-ai/skills-hub + +可用 skills 列表: + +| Skill 名称 | 描述 | 适用 Agent | 源 URL | +|-----------|------|----------|--------| +| `code_review` | 代码审查(支持 Python/JS/Go) | 兵部/刑部 | https://raw.githubusercontent.com/openclaw-ai/skills-hub/main/code_review/SKILL.md | +| `api_design` | API 设计审查 | 兵部/工部 | https://raw.githubusercontent.com/openclaw-ai/skills-hub/main/api_design/SKILL.md | +| `security_audit` | 安全审计 | 刑部 | https://raw.githubusercontent.com/openclaw-ai/skills-hub/main/security_audit/SKILL.md | +| `data_analysis` | 数据分析 | 户部 | https://raw.githubusercontent.com/openclaw-ai/skills-hub/main/data_analysis/SKILL.md | +| `doc_generation` | 文档生成 | 礼部 | https://raw.githubusercontent.com/openclaw-ai/skills-hub/main/doc_generation/SKILL.md | +| `test_framework` | 测试框架设计 | 工部/刑部 | https://raw.githubusercontent.com/openclaw-ai/skills-hub/main/test_framework/SKILL.md | + +**一键导入官方 skills** + +```bash +python3 scripts/skill_manager.py import-official-hub \ + --agents zhongshu,menxia,shangshu,bingbu,xingbu,libu +``` + +--- + +## 看板 UI 操作 + +### 快捷添加 Skill + +1. 打开看板 → 🔧 **技能配置** 面板 +2. 点击 **➕ 添加远程 Skill** 按钮 +3. 填写表单: + - **Agent**: 选择目标 Agent + - **Skill 名称**: 输入 skill 的内部 ID + - **远程 URL**: 粘贴 GitHub/HTTPS URL + - **中文描述**: 可选,简述 skill 功能 +4. 点击 **确认** 按钮 + +### 管理已添加的 Skills + +1. 看板 → 🔧 **技能配置** → **远程 Skills** 标签 +2. 查看已添加的所有 skills 及其源地址 +3. 操作: + - **查看**: 展示 SKILL.md 内容 + - **更新**: 从源 URL 重新下载最新版本 + - **删除**: 移除本地副本(不影响源) + - **复制源 URL**: 快速分享给他人 + +--- + +## Skill 文件规范 + +远程 skills 必须遵循标准的 Markdown 格式: + +### 最小必需结构 + +```markdown +--- +name: skill_internal_name +description: Short description +version: 1.0.0 +tags: [tag1, tag2] +--- + +# Skill 名称 + +详细描述... + +## 输入 + +说明接收什么参数 + +## 处理流程 + +具体步骤... + +## 输出规范 + +输出格式说明 +``` + +### 完整示例 + +```markdown +--- +name: code_review +description: 对 Python/JavaScript 代码进行结构审查和优化建议 +version: 2.1.0 +author: openclaw-ai +tags: [code-quality, security, performance] +compatibleAgents: [bingbu, xingbu, menxia] +--- + +# 代码审查技能 + +本技能专门用于对生产代码进行多维度审查... + +## 输入 + +- `code`: 要审查的源代码 +- `language`: 编程语言 (python, javascript, go, rust) +- `focusAreas`: 审查重点 (security, performance, style, structure) + +## 处理流程 + +1. 语言识别与语法验证 +2. 安全漏洞扫描 +3. 性能瓶颈识别 +4. 代码风格检查 +5. 最佳实践建议 + +## 输出规范 + +```json +{ + "issues": [ + { + "type": "security|performance|style|structure", + "severity": "critical|high|medium|low", + "location": "line:column", + "message": "问题描述", + "suggestion": "修复建议" + } + ], + "summary": { + "totalIssues": 3, + "criticalCount": 1, + "highCount": 2 + } +} +``` + +## 适用场景 + +- 兵部(代码实现)的代码产出审查 +- 刑部(合规审计)的安全检查 +- 门下省(审议把关)的质量评估 + +## 依赖与限制 + +- 需要 Python 3.9+ +- 支持文件大小: 最多 50KB +- 执行超时: 30 秒 +``` + +--- + +## 数据存储 + +### 本地存储结构 + +``` +~/.openclaw/ +├── workspace-zhongshu/ +│ └── skills/ +│ ├── code_review/ +│ │ ├── SKILL.md +│ │ └── .source.json # 存储源 URL 和元数据 +│ └── api_design/ +│ ├── SKILL.md +│ └── .source.json +├── ... +``` + +### .source.json 格式 + +```json +{ + "skillName": "code_review", + "sourceUrl": "https://raw.githubusercontent.com/...", + "description": "代码审查专项技能", + "version": "2.1.0", + "addedAt": "2026-03-02T14:30:00Z", + "lastUpdated": "2026-03-02T14:30:00Z", + "lastUpdateCheck": "2026-03-02T15:00:00Z", + "checksum": "sha256:abc123...", + "status": "valid" +} +``` + +--- + +## 安全考虑 + +### URL 验证 + +✅ **允许的 URL 类型:** +- HTTPS URLs: `https://` +- 本地文件: `file://` 或绝对路径 +- 相对路径: `./skills/` + +❌ **禁止的 URL 类型:** +- HTTP (非 HTTPS): `http://` 被拒绝 +- 本地模式 HTTP: `http://localhost/` (避免环回攻击) +- FTP/SSH: `ftp://`, `ssh://` + +### 内容验证 + +1. **格式验证**: 确保是有效的 Markdown YAML frontmatter +2. **大小限制**: 最多 10 MB +3. **超时保护**: 下载超过 30 秒自动中止 +4. **路径遍历防护**: 检查解析后的 skill 名称,禁用 `../` 模式 +5. **checksum 验证**: 可选的 GPG 签名验证(仅官方库) + +### 隔离执行 + +- 远程 skills 在沙箱中执行(由 OpenClaw runtime 提供) +- 无法访问 `~/.openclaw/config.json` 等敏感文件 +- 只能访问分配的 workspace 目录 + +--- + +## 故障排查 + +### 常见问题 + +**Q: 下载失败,提示 "Connection timeout"** + +A: 检查网络连接和 URL 有效性: +```bash +curl -I https://raw.githubusercontent.com/... +``` + +**Q: Skill 显示 "invalid" 状态** + +A: 检查文件格式: +```bash +python3 -m json.tool ~/.openclaw/workspace-zhongshu/skills/xxx/SKILL.md +``` + +**Q: 能否从私有 GitHub 仓库导入?** + +A: 不支持(安全考虑)。可以: +1. 将仓库设为公开 +2. 在本地下载后直接添加 +3. 通过 GitHub Gist 的公开链接 + +**Q: 如何创建自己的 skills 库?** + +A: 参考 [OpenClaw Skills Hub](https://github.com/openclaw-ai/skills-hub) 的结构创建自己的仓库,然后: + +```bash +git clone https://github.com/yourname/my-skills-hub.git +cd my-skills-hub +# 创建 skill 文件结构 +# 提交 & 推送到 GitHub +``` + +然后通过 URL 或官方库导入功能添加即可。 + +--- + +## 最佳实践 + +### 1. 版本管理 + +始终在 SKILL.md 的 frontmatter 中标注版本号: +```yaml +--- +version: 2.1.0 +--- +``` + +### 2. 向后兼容 + +更新 skill 时保持输入/输出格式兼容,避免破坏现有流程。 + +### 3. 文档完整 + +包含详细的: +- 功能描述 +- 适用场景 +- 依赖说明 +- 输出示例 + +### 4. 定期更新 + +设置定期检查更新(周期可在看板中配置): +```bash +python3 scripts/skill_manager.py check-updates --interval weekly +``` + +### 5. 贡献社区 + +成熟的 skills 可向 [OpenClaw Skills Hub](https://github.com/openclaw-ai/skills-hub) 贡献。 + +--- + +## API 完整参考 + +详见 [任务分发流转架构文档](task-dispatch-architecture.md) 的第三部分(API 与工具)。 + +--- + +

+ 开放 的生态,赋能 制度化 的 AI 协作 +

+
+ + +# 远程 Skills 快速入门 + +## 5 分钟体验 + +### 1. 启动服务器 + +```bash +# 确保你在项目根目录 +python3 dashboard/server.py +# 输出: 三省六部看板启动 → http://127.0.0.1:7891 +``` + +### 2. 添加官方 Skill(CLI) + +```bash +# 为中书省添加代码审查 skill +python3 scripts/skill_manager.py add-remote \ + --agent zhongshu \ + --name code_review \ + --source https://raw.githubusercontent.com/openclaw-ai/skills-hub/main/code_review/SKILL.md \ + --description "代码审查能力" + +# 输出: +# ⏳ 正在从 https://raw.githubusercontent.com/... 下载... +# ✅ 技能 code_review 已添加到 zhongshu +# 路径: /Users/xxx/.openclaw/workspace-zhongshu/skills/code_review/SKILL.md +# 大小: 2048 字节 +``` + +### 3. 列出所有远程 Skills + +```bash +python3 scripts/skill_manager.py list-remote + +# 输出: +# 📋 共 1 个远程 skills: +# +# Agent | Skill 名称 | 描述 | 添加时间 +# ------------|----------------------|--------------------------------|---------- +# zhongshu | code_review | 代码审查能力 | 2026-03-02 +``` + +### 4. 查看 API 响应 + +```bash +curl http://localhost:7891/api/remote-skills-list | jq . + +# 输出: +# { +# "ok": true, +# "remoteSkills": [ +# { +# "skillName": "code_review", +# "agentId": "zhongshu", +# "sourceUrl": "https://raw.githubusercontent.com/...", +# "description": "代码审查能力", +# "localPath": "/Users/xxx/.openclaw/workspace-zhongshu/skills/code_review/SKILL.md", +# "addedAt": "2026-03-02T14:30:00Z", +# "lastUpdated": "2026-03-02T14:30:00Z", +# "status": "valid" +# } +# ], +# "count": 1, +# "listedAt": "2026-03-02T14:35:00Z" +# } +``` + +--- + +## 常见操作 + +### 一键导入官方库中的所有 skills + +```bash +python3 scripts/skill_manager.py import-official-hub \ + --agents zhongshu,menxia,shangshu,bingbu,xingbu +``` + +这会自动为每个 agent 添加: +- **zhongshu**: code_review, api_design, doc_generation +- **menxia**: code_review, api_design, security_audit, data_analysis, doc_generation, test_framework +- **shangshu**: 同 menxia(协调者) +- **bingbu**: code_review, api_design, test_framework +- **xingbu**: code_review, security_audit, test_framework + +### 更新某个 Skill 到最新版本 + +```bash +python3 scripts/skill_manager.py update-remote \ + --agent zhongshu \ + --name code_review + +# 输出: +# ⏳ 正在从 https://raw.githubusercontent.com/... 下载... +# ✅ 技能 code_review 已添加到 zhongshu +# ✅ 技能已更新 +# 路径: /Users/xxx/.openclaw/workspace-zhongshu/skills/code_review/SKILL.md +# 大小: 2156 字节 +``` + +### 移除某个 Skill + +```bash +python3 scripts/skill_manager.py remove-remote \ + --agent zhongshu \ + --name code_review + +# 输出: +# ✅ 技能 code_review 已从 zhongshu 移除 +``` + +--- + +## 看板 UI 操作 + +### 在看板中添加 Remote Skill + +1. 打开 http://localhost:7891 +2. 进入 🔧 **技能配置** 面板 +3. 点击 **➕ 添加远程 Skill** 按钮 +4. 填写表单: + - **Agent**: 从下拉列表选择(如 zhongshu) + - **Skill 名称**: 输入内部 ID 如 `code_review` + - **远程 URL**: 粘贴 GitHub URL 如 `https://raw.githubusercontent.com/openclaw-ai/skills-hub/main/code_review/SKILL.md` + - **中文描述**: 可选,如 `代码审查能力` +5. 点击 **导入** 按钮 +6. 等待 1-2 秒,看到 ✅ 成功提示 + +### 管理已添加的 Skills + +在看板 → 🔧 技能配置 → **远程 Skills** 标签页: + +- **查看**: 点击 Skill 名称查看 SKILL.md 内容 +- **更新**: 点击 🔄 重新从源 URL 下载最新版本 +- **删除**: 点击 ✕ 移除本地副本 +- **复制 URL**: 快速分享给他人 + +--- + +## 创建自己的 Skill 库 + +### 目录结构 + +``` +my-skills-hub/ +├── code_review/ +│ └── SKILL.md # 代码审查能力 +├── api_design/ +│ └── SKILL.md # API 设计审查 +├── data_analysis/ +│ └── SKILL.md # 数据分析 +└── README.md +``` + +### SKILL.md 模板 + +```markdown +--- +name: my_custom_skill +description: 简短描述 +version: 1.0.0 +tags: [tag1, tag2] +--- + +# Skill 完整名称 + +详细描述... + +## 输入 + +说明接收什么参数 + +## 处理流程 + +具体步骤... + +## 输出规范 + +输出格式说明 +``` + +### 上传到 GitHub + +```bash +git init +git add . +git commit -m "Initial commit: my-skills-hub" +git remote add origin https://github.com/yourname/my-skills-hub +git push -u origin main +``` + +### 导入自己的 Skill + +```bash +python3 scripts/skill_manager.py add-remote \ + --agent zhongshu \ + --name my_skill \ + --source https://raw.githubusercontent.com/yourname/my-skills-hub/main/my_skill/SKILL.md \ + --description "我的定制技能" +``` + +--- + +## API 完整参考 + +### POST /api/add-remote-skill + +添加远程 skill。 + +**请求:** +```bash +curl -X POST http://localhost:7891/api/add-remote-skill \ + -H "Content-Type: application/json" \ + -d '{ + "agentId": "zhongshu", + "skillName": "code_review", + "sourceUrl": "https://raw.githubusercontent.com/...", + "description": "代码审查" + }' +``` + +**响应 (200):** +```json +{ + "ok": true, + "message": "技能 code_review 已从远程源添加到 zhongshu", + "skillName": "code_review", + "agentId": "zhongshu", + "source": "https://raw.githubusercontent.com/...", + "localPath": "/Users/xxx/.openclaw/workspace-zhongshu/skills/code_review/SKILL.md", + "size": 2048, + "addedAt": "2026-03-02T14:30:00Z" +} +``` + +### GET /api/remote-skills-list + +列出所有远程 skills。 + +```bash +curl http://localhost:7891/api/remote-skills-list +``` + +**响应:** +```json +{ + "ok": true, + "remoteSkills": [ + { + "skillName": "code_review", + "agentId": "zhongshu", + "sourceUrl": "https://raw.githubusercontent.com/...", + "description": "代码审查能力", + "localPath": "/Users/xxx/.openclaw/workspace-zhongshu/skills/code_review/SKILL.md", + "addedAt": "2026-03-02T14:30:00Z", + "lastUpdated": "2026-03-02T14:30:00Z", + "status": "valid" + } + ], + "count": 1, + "listedAt": "2026-03-02T14:35:00Z" +} +``` + +### POST /api/update-remote-skill + +更新远程 skill 为最新版本。 + +```bash +curl -X POST http://localhost:7891/api/update-remote-skill \ + -H "Content-Type: application/json" \ + -d '{ + "agentId": "zhongshu", + "skillName": "code_review" + }' +``` + +### DELETE /api/remove-remote-skill + +移除远程 skill。 + +```bash +curl -X POST http://localhost:7891/api/remove-remote-skill \ + -H "Content-Type: application/json" \ + -d '{ + "agentId": "zhongshu", + "skillName": "code_review" + }' +``` + +--- + +## 故障排查 + +### Q: 下载失败,提示 "Connection timeout" + +**A:** 检查网络连接和 URL 有效性 + +```bash +curl -I https://raw.githubusercontent.com/... +# 应该返回 HTTP/1.1 200 OK +``` + +### Q: 文件格式无效 + +**A:** 确保 SKILL.md 以 YAML frontmatter 开头 + +```markdown +--- +name: skill_name +description: 描述 +--- + +# 正文开始... +``` + +### Q: 导入后看不到 Skill + +**A:** 刷新看板或检查 Agent 是否配置正确 + +```bash +# 检查 Agent 是否存在 +python3 scripts/skill_manager.py list-remote + +# 检查本地文件 +ls -la ~/.openclaw/workspace-zhongshu/skills/ +``` + +--- + +## 更多信息 + +- 📚 [完整指南](remote-skills-guide.md) +- 🏛️ [架构文档](task-dispatch-architecture.md) +- 🤝 [项目贡献](../CONTRIBUTING.md) + + + +# 三省六部任务分发流转体系 · 业务与技术架构 + +> 本文档详细阐述「三省六部」项目如何从**业务制度设计**到**代码实现细节**,完整处理复杂多Agent协作的任务分发与流转。这是一个**制度化的AI多Agent框架**,而非传统的自由讨论式协作系统。 + +**文档概览图** + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +业务层:帝国制度 (Imperial Governance Model) + ├─ 分权制衡:皇上 → 太子 → 中书 → 门下 → 尚书 → 六部 + ├─ 制度约束:不可越级、状态严格递进、门下必审议 + └─ 质量保障:可封驳反工、实时可观测、紧急可干预 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +技术层:OpenClaw多Agent编排 (Multi-Agent Orchestration) + ├─ 状态机:9个状态(Pending → Taizi → Zhongshu → Menxia → Assigned → Doing/Next → Review → Done/Cancelled) + ├─ 数据融合:flow_log + progress_log + session JSONL → unified activity stream + ├─ 权限矩阵:严格的subagent调用权限控制 + └─ 调度层:自动派发、超时重试、停滞升级、自动回滚 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +观测层:React 看板 + 实时API (Dashboard + Real-time Analytics) + ├─ 任务看板:10个视图面板(全部/按状态/按部门/按优先级等) + ├─ 活动流:59条/任务的混合活动记录(思考过程、工具调用、状态转移) + └─ 在线状态:Agent 实时节点检测 + 心跳喚醒机制 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +--- + +## 📚 第一部分:业务架构 + +### 1.1 帝国制度:分权制衡的设计哲学 + +#### 核心理念 + +传统的多Agent框架(如CrewAI、AutoGen)采用**"自由协作"模式**: +- Agent自主选择协作对象 +- 框架仅提供通信通道 +- 质量控制完全依赖Agent智能 +- **问题**:容易出现Agent相互制造假数据、重复工作、方案质量无保障 + +**三省六部**采用**"制度化协作"模式**,模仿古代帝国官僚体系: + +``` + 皇上 + (User) + │ + ↓ + 太子 (Taizi) + [分拣官、消息接入总负责] + ├─ 识别:这是旨意还是闲聊? + ├─ 执行:直接回复闲聊 || 建立任务→转中书 + └─ 权限:只能调用 中书省 + │ + ↓ + 中书省 (Zhongshu) + [规划官、方案起草总负责] + ├─ 接旨后分析需求 + ├─ 拆解为子任务(todos) + ├─ 调用门下省审议 OR 尚书省咨询 + └─ 权限:只能调用 门下 + 尚书 + │ + ↓ + 门下省 (Menxia) + [审议官、质量把握人] + ├─ 审查中书方案(可行性、完整性、风险) + ├─ 准奏 OR 封驳(含修改建议) + ├─ 若封驳 → 返回中书修改 → 重新审议(最多3轮) + └─ 权限:只能调用 尚书 + 回调中书 + │ + (✅ 准奏) + │ + ↓ + 尚书省 (Shangshu) + [派发官、执行总指挥] + ├─ 接到准奏方案 + ├─ 分析派发给哪个部门 + ├─ 调用六部(礼/户/兵/刑/工/吏)执行 + ├─ 监控各部进度 → 汇总结果 + └─ 权限:只能调用 六部(不能越权调中书) + │ + ├─ 礼部 (Libu) - 文档编制官 + ├─ 户部 (Hubu) - 数据分析官 + ├─ 兵部 (Bingbu) - 代码实现官 + ├─ 刑部 (Xingbu) - 测试审查官 + ├─ 工部 (Gongbu) - 基础设施官 + └─ 吏部 (Libu_hr) - 人力资源官 + │ + (各部并行执行) + ↓ + 尚书省·汇总 + ├─ 收集六部结果 + ├─ 状态转为 Review + ├─ 回调中书省转报皇上 + │ + ↓ + 中书省·回奏 + ├─ 汇总现象、结论、建议 + ├─ 状态转为 Done + └─ 回复飞书消息给皇上 +``` + +#### 制度的4大保障 + +| 保障机制 | 实现细节 | 防护效果 | +|---------|---------|---------| +| **制度性审核** | 门下省必审议所有中书方案,不可跳过 | 防止Agent胡乱执行,确保方案具有可行性 | +| **分权制衡** | 权限矩阵:谁能调谁严格定义 | 防止权力滥用(如尚书越权调中书改方案) | +| **完全可观测** | 任务看板10个面板 + 59条活动/任务 | 实时看到任务卡在哪、谁在工作、工作状态如何 | +| **实时可干预** | 看板内一键 stop/cancel/resume/advance | 紧急情况(如发现Agent走错方向)能立即纠正 | + +--- + +### 1.2 任务完整流转流程 + +#### 流程示意图 + +```mermaid +stateDiagram-v2 +[*] --> Pending: 皇上下旨 +Pending --> Taizi: 太子接旨 +Taizi --> Zhongshu: 太子转交中书 +Zhongshu --> Menxia: 中书提交审议 +Menxia --> Zhongshu: 门下封驳(可多次) +Menxia --> Assigned: 门下准奏 +Assigned --> Doing: 尚书派发执行 +Doing --> Review: 各部完成 +Review --> Done: 皇上御批通过 +Review --> Menxia: 皇上要求修改 +Done --> [*] +Doing --> [*]: 手动取消 +Review --> [*]: 业务终止 +``` + +#### 具体关键路径 + +**✅ 理想路径**(无阻滞,4-5天完成) + +``` +DAY 1: + 10:00 - 皇上飞书:"为三省六部编写完整自动化测试方案" + 太子接旨。state = Taizi, org = 太子 + 自动派发 taizi agent → 处理此旨意 + + 10:30 - 太子分拣完毕。判定为「工作旨意」(非闲聊) + 建任务 JJC-20260228-E2E + flow_log 记录:"皇上 → 太子:下旨" + state: Taizi → Zhongshu, org: 太子 → 中书省 + 自动派发 zhongshu agent + +DAY 2: + 09:00 - 中书省接旨。开始规划 + 汇报进展:"分析测试需求,拆解为单元/集成/E2E三层" + progress_log 记录:"中书省 张三:分需求" + + 15:00 - 中书省完成方案 + todos 快照:需求分析✅、方案设计✅、待审议🔄 + flow_log 记录:"中书省 → 门下省:方案提交审议" + state: Zhongshu → Menxia, org: 中书省 → 门下省 + 自动派发 menxia agent + +DAY 3: + 09:00 - 门下省开始审议 + 进度汇报:"现在审查方案的完整性和风险" + + 14:00 - 门下省审议完毕 + 判定:"方案可行,但缺失 _infer_agent_id_from_runtime 函数的测试" + 行为:✅ 准奏 (带修改建议) + flow_log 记录:"门下省 → 尚书省:✅ 准奏通过(5条建议)" + state: Menxia → Assigned, org: 门下省 → 尚书省 + OPTIONAL:中书省收到建议,主动优化方案 + 自动派发 shangshu agent + +DAY 4: + 10:00 - 尚书省接到准奏 + 分析:"该测试方案应派给工部+刑部+礼部协力完成" + flow_log 记录:"尚书省 → 六部:派发执行(兵吏合作)" + state: Assigned → Doing, org: 尚书省 → 兵部+刑部+礼部 + 自动派发 bingbu/xingbu/libu 三个agent(并行) + +DAY 4-5: + (各部并行执行) + - 兵部(bingbu):实现 pytest + unittest 测试框架 + - 刑部(xingbu):编写测试覆盖所有关键函数 + - 礼部(libu):整理测试文档和用例说明 + + 实时汇报(hourly progress): + - 兵部:"✅ 已实现 16 个单元测试" + - 刑部:"🔄 正在编写集成测试(8/12 完成)" + - 礼部:"等待兵部完成再写报告" + +DAY 5: + 14:00 - 各部完成 + state: Doing → Review, org: 兵部 → 尚书省 + 尚书省汇总:"所有测试已完成,通过率 98.5%" + 转回中书省 + + 15:00 - 中书省回奏皇上 + state: Review → Done + 模板回复飞书,含最终成果链接和总结 +``` + +**❌ 挫折路径**(含封驳和重试,6-7天) + +``` +DAY 2 同上 + +DAY 3 [封驳场景]: + 14:00 - 门下省审议完毕 + 判定:"方案不完整,缺少性能测试 + 压力测试" + 行为:🚫 封驳 + review_round += 1 + flow_log 记录:"门下省 → 中书省:🚫 封驳(需补充性能测试)" + state: Menxia → Zhongshu # 返回中书修改 + 自动派发 zhongshu agent(重新规划) + +DAY 3-4: + 16:00 - 中书省收到封驳通知(唤醒agent) + 分析改进意见,补充性能测试方案 + progress:"已整合性能测试需求,修正方案如下..." + flow_log 记录:"中书省 → 门下省:修订方案(第2轮审议)" + state: Zhongshu → Menxia + 自动派发 menxia agent + + 18:00 - 门下省重新审议 + 判定:"✅ 本次通过" + flow_log 记录:"门下省 → 尚书省:✅ 准奏通过(第2轮)" + state: Menxia → Assigned → Doing + 后续同理想路径... + +DAY 7:全部完成(比理想路径晚1-2天) +``` + +--- + +### 1.3 任务规格书与业务契约 + +#### Task Schema 字段说明 + +```json +{ + "id": "JJC-20260228-E2E", // 任务全局唯一ID (JJC-日期-序号) + "title": "为三省六部编写完整自动化测试方案", + "official": "中书令", // 负责官职 + "org": "中书省", // 当前负责部门 + "state": "Assigned", // 当前状态(见 _STATE_FLOW) + + // ──── 质量与约束 ──── + "priority": "normal", // 优先级:critical/high/normal/low + "block": "无", // 当前阻滞原因(如"等待工部反馈") + "reviewRound": 2, // 门下审议第几轮 + "_prev_state": "Menxia", // 若被 stop,记录之前状态用于 resume + + // ──── 业务产出 ──── + "output": "", // 最终任务成果(URL/文件路径/总结) + "ac": "", // Acceptance Criteria(验收标准) + "priority": "normal", + + // ──── 流转记录 ──── + "flow_log": [ + { + "at": "2026-02-28T10:00:00Z", + "from": "皇上", + "to": "太子", + "remark": "下旨:为三省六部编写完整自动化测试方案" + }, + { + "at": "2026-02-28T10:30:00Z", + "from": "太子", + "to": "中书省", + "remark": "分拣→传旨" + }, + { + "at": "2026-02-28T15:00:00Z", + "from": "中书省", + "to": "门下省", + "remark": "规划方案提交审议" + }, + { + "at": "2026-03-01T09:00:00Z", + "from": "门下省", + "to": "中书省", + "remark": "🚫 封驳:需补充性能测试" + }, + { + "at": "2026-03-01T15:00:00Z", + "from": "中书省", + "to": "门下省", + "remark": "修订方案(第2轮审议)" + }, + { + "at": "2026-03-01T20:00:00Z", + "from": "门下省", + "to": "尚书省", + "remark": "✅ 准奏通过(第2轮,5条建议已采纳)" + } + ], + + // ──── Agent 实时汇报 ──── + "progress_log": [ + { + "at": "2026-02-28T10:35:00Z", + "agent": "zhongshu", // 汇报agent + "agentLabel": "中书省", + "text": "已接旨。分析测试需求,拟定三层测试方案...", + "state": "Zhongshu", // 汇报时的状态快照 + "org": "中书省", + "tokens": 4500, // 资源消耗 + "cost": 0.0045, + "elapsed": 120, + "todos": [ // 待办任务快照 + {"id": "1", "title": "需求分析", "status": "completed"}, + {"id": "2", "title": "方案设计", "status": "in-progress"}, + {"id": "3", "title": "await审议", "status": "not-started"} + ] + }, + // ... 更多 progress_log 条目 ... + ], + + // ──── 调度元数据 ──── + "_scheduler": { + "enabled": true, + "stallThresholdSec": 180, // 停滞超过180秒自动升级 + "maxRetry": 1, // 自动重试最多1次 + "retryCount": 0, + "escalationLevel": 0, // 0=无升级 1=门下协调 2=尚书协调 + "lastProgressAt": "2026-03-01T20:00:00Z", + "stallSince": null, // 何时开始停滞 + "lastDispatchStatus": "success", // queued|success|failed|timeout|error + "snapshot": { + "state": "Assigned", + "org": "尚书省", + "note": "review-before-approve" + } + }, + + // ──── 生命周期 ──── + "archived": false, // 是否归档 + "now": "门下省准奏,移交尚书省派发", // 当前实时状态描述 + "updatedAt": "2026-03-01T20:00:00Z" +} +``` + +#### 业务契约 + +| 契约 | 含义 | 违反后果 | +|------|------|---------| +| **不可越级** | 太子只能调中书,中书只能调门下/尚书,六部不能对外调用 | 超权调用被拒绝,系统自动拦截 | +| **状态单向递进** | Pending → Taizi → Zhongshu → ... → Done,不能跳过或倒退 | 只能通过 review_action(reject) 返回上一步 | +| **门下必审** | 所有中书提出的方案都要门下省审议,无法跳过 | 中书不能直接转尚书,门下必入 | +| **一旦Done无改** | 任务进入Done/Cancelled后不能再修改状态 | 若需修改需要创建新任务或取消后重新建 | +| **task_id唯一性** | JJC-日期-序号 全局唯一,同一天同一任务不重复建 | 看板防重,自动去重 | +| **资源消耗透明** | 每次进展汇报都要上报 tokens/cost/elapsed | 便于成本核算和性能优化 | + +--- + +## 🔧 第二部分:技术架构 + +### 2.1 状态机与自动派发 + +#### 状态转移完整定义 + +```python +_STATE_FLOW = { + 'Pending': ('Taizi', '皇上', '太子', '待处理旨意转交太子分拣'), + 'Taizi': ('Zhongshu','太子', '中书省', '太子分拣完毕,转中书省起草'), + 'Zhongshu': ('Menxia', '中书省', '门下省', '中书省方案提交门下省审议'), + 'Menxia': ('Assigned','门下省', '尚书省', '门下省准奏,转尚书省派发'), + 'Assigned': ('Doing', '尚书省', '六部', '尚书省开始派发执行'), + 'Next': ('Doing', '尚书省', '六部', '待执行任务开始执行'), + 'Doing': ('Review', '六部', '尚书省', '各部完成,进入汇总'), + 'Review': ('Done', '尚书省', '太子', '全流程完成,回奏太子转报皇上'), +} +``` + +每个状态自动关联 Agent ID(见 `_STATE_AGENT_MAP`): + +```python +_STATE_AGENT_MAP = { + 'Taizi': 'taizi', + 'Zhongshu': 'zhongshu', + 'Menxia': 'menxia', + 'Assigned': 'shangshu', + 'Doing': None, # 从 org 推断(六部之一) + 'Next': None, # 从 org 推断 + 'Review': 'shangshu', + 'Pending': 'zhongshu', +} +``` + +#### 自动派发流程 + +当任务状态转移时(通过 `handle_advance_state()` 或审批),后台自动执行派发: + +``` +1. 状态转移触发派发 + ├─ 查表 _STATE_AGENT_MAP 得到目标 agent_id + ├─ 若是 Doing/Next,从 task.org 查表 _ORG_AGENT_MAP 推断具体部门agent + └─ 若无法推断则跳过派发(如 Done/Cancelled) + +2. 构造派发消息(针对性促使Agent立即工作) + ├─ taizi: "📜 皇上旨意需要你处理..." + ├─ zhongshu: "📜 旨意已到中书省,请起草方案..." + ├─ menxia: "📋 中书省方案提交审议..." + ├─ shangshu: "📮 门下省已准奏,请派发执行..." + └─ 六部: "📌 请处理任务..." + +3. 后台异步派发(非阻塞) + ├─ spawn daemon thread + ├─ 标记 _scheduler.lastDispatchStatus = 'queued' + ├─ 检查 Gateway 进程是否开启 + ├─ 运行 openclaw agent --agent {id} -m "{msg}" --deliver --timeout 300 + ├─ 重试最多2次(失败间隔5秒退避) + ├─ 更新 _scheduler 状态和错误信息 + └─ flow_log 记录派发结果 + +4. 派发状态转移 + ├─ success: 立即更新 _scheduler.lastDispatchStatus = 'success' + ├─ failed: 记录失败原因,Agent 超时不会 block 看板 + ├─ timeout: 标记 timeout,允许用户手动重试 / 升级 + ├─ gateway-offline: Gateway 未启动,跳过此次派发(后续可重试) + └─ error: 异常情况,记录堆栈供调试 + +5. 到达目标Agent的处理 + ├─ Agent 从飞书消息收到通知 + ├─ 通过 kanban_update.py 与看板交互(更新状态/记录进展) + └─ 完成工作后再次触发派发到下一个Agent +``` + +--- + +### 2.2 权限矩阵与Subagent调用 + +#### 权限定义(openclaw.json 中配置) + +```json +{ + "agents": [ + { + "id": "taizi", + "label": "太子", + "allowAgents": ["zhongshu"] + }, + { + "id": "zhongshu", + "label": "中书省", + "allowAgents": ["menxia", "shangshu"] + }, + { + "id": "menxia", + "label": "门下省", + "allowAgents": ["shangshu", "zhongshu"] + }, + { + "id": "shangshu", + "label": "尚书省", + "allowAgents": ["libu", "hubu", "bingbu", "xingbu", "gongbu", "libu_hr"] + }, + { + "id": "libu", + "label": "礼部", + "allowAgents": [] + }, + // ... 其他六部同样 allowAgents = [] ... + ] +} +``` + +#### 权限检查机制(代码层面) + +在 `dispatch_for_state()` 之外,还有一套防御性的权限检查: + +```python +def can_dispatch_to(from_agent, to_agent): + """检查 from_agent 是否有权调用 to_agent。""" + cfg = read_json(DATA / 'agent_config.json', {}) + agents = cfg.get('agents', []) + + from_record = next((a for a in agents if a.get('id') == from_agent), None) + if not from_record: + return False, f'{from_agent} 不存在' + + allowed = from_record.get('allowAgents', []) + if to_agent not in allowed: + return False, f'{from_agent} 无权调用 {to_agent}(允许列表:{allowed})' + + return True, 'OK' +``` + +#### 权限违反示例与处理 + +| 场景 | 请求 | 结果 | 理由 | +|------|------|------|------| +| **正常** | 中书省 → 门下省审议 | ✅ 允许 | 门下在中书的 allowAgents 中 | +| **违反** | 中书省 → 尚书省改方案 | ❌ 拒绝 | 中书只能调门下/尚书,不能手工改尚书工作 | +| **违反** | 工部 → 尚书省 "我完成了" | ✅ 改状态 | 通过 flow_log 和 progress_log(不是跨Agent调用) | +| **违反** | 尚书省 → 中书省 "重新改方案" | ❌ 拒绝 | 尚书不在门下/中书的 allowAgents 中 | +| **防控** | Agent 伪造其他agent派发 | ❌ 拦截 | API 层验证 HTTP 请求来源/签名 | + +--- + +### 2.3 数据融合:progress_log + session JSONL + +#### 现象 + +当任务执行时,有三层数据源: + +``` +1️⃣ flow_log + └─ 纯粹记录状态转移(Zhongshu → Menxia) + └─ 数据源:任务 JSON 的 flow_log 字段 + └─ 来自:Agent 通过 kanban_update.py flow 命令上报 + +2️⃣ progress_log + └─ Agent 的实时工作汇报(文本进展、todos快照、资源消耗) + └─ 数据源:任务 JSON 的 progress_log 字段 + └─ 来自:Agent 通过 kanban_update.py progress 命令上报 + └─ 周期:通常每30分钟或关键节点上报1次 + +3️⃣ session JSONL(新增!) + └─ Agent 的内部思考过程(thinking)、工具调用(tool_result)、对话历史(user) + └─ 数据源:~/.openclaw/agents/{agent_id}/sessions/*.jsonl + └─ 来自:OpenClaw框架自动记录,Agent无需主动操作 + └─ 周期:消息级别,粒度最细 +``` + +#### 问题诊断 + +过去,只靠 flow_log + progress_log 展现进展: +- ❌ 看不到Agent的具体思考过程 +- ❌ 看不到每次工具调用的结果 +- ❌ 看不到Agent中间的对话历史 +- ❌ Agent 表现出"黑盒状态" + +例如:progress_log 记录"正在分析需求",但用户看不到到底分析了什么。 + +#### 解决方案:Session JSONL 融合 + +在 `get_task_activity()` 中新增融合逻辑(40行): + +```python +def get_task_activity(task_id): + # ... 前面代码同上 ... + + # ── 融合 Agent Session 活动(thinking / tool_result / user)── + session_entries = [] + + # 活跃任务:尝试按 task_id 精确匹配 + if state not in ('Done', 'Cancelled'): + if agent_id: + entries = get_agent_activity( + agent_id, limit=30, task_id=task_id + ) + session_entries.extend(entries) + + # 也从相关Agent获取 + for ra in related_agents: + if ra != agent_id: + entries = get_agent_activity( + ra, limit=20, task_id=task_id + ) + session_entries.extend(entries) + else: + # 已完成任务:基于关键词匹配 + title = task.get('title', '') + keywords = _extract_keywords(title) + if keywords: + for ra in related_agents[:5]: + entries = get_agent_activity_by_keywords( + ra, keywords, limit=15 + ) + session_entries.extend(entries) + + # 去重(通过 at+kind 去重避免重复) + existing_keys = {(a.get('at', ''), a.get('kind', '')) for a in activity} + for se in session_entries: + key = (se.get('at', ''), se.get('kind', '')) + if key not in existing_keys: + activity.append(se) + existing_keys.add(key) + + # 重新排序 + activity.sort(key=lambda x: x.get('at', '')) + + # 返回时标记数据来源 + return { + 'activity': activity, + 'activitySource': 'progress+session', # 新标记 + # ... 其他字段 ... + } +``` + +#### Session JSONL 格式解析 + +从 JSONL 中提取的条目,统一转换为看板活动条目: + +```python +def _parse_activity_entry(item): + """将 session jsonl 的 message 统一解析成看板活动条目。""" + msg = item.get('message', {}) + role = str(msg.get('role', '')).strip().lower() + ts = item.get('timestamp', '') + + # 🧠 Assistant 角色 - Agent思考过程 + if role == 'assistant': + entry = { + 'at': ts, + 'kind': 'assistant', + 'text': '...主回复...', + 'thinking': '💭 Agent考虑到...', # 内部思维链 + 'tools': [ + {'name': 'bash', 'input_preview': 'cd /src && npm test'}, + {'name': 'file_read', 'input_preview': 'dashboard/server.py'}, + ] + } + return entry + + # 🔧 Tool Result - 工具调用结果 + if role in ('toolresult', 'tool_result'): + entry = { + 'at': ts, + 'kind': 'tool_result', + 'tool': 'bash', + 'exitCode': 0, + 'output': '✓ All tests passed (123 tests)', + 'durationMs': 4500 # 执行时长 + } + return entry + + # 👤 User - 人工反馈或对话 + if role == 'user': + entry = { + 'at': ts, + 'kind': 'user', + 'text': '请实现测试用例的异常处理' + } + return entry +``` + +#### 融合后的活动流结构 + +单个任务的59条活动流(JJC-20260228-E2E 示例): + +``` +kind count 代表事件 +──────────────────────────────────────────────── +flow 10 状态转移链(Pending→Taizi→Zhongshu→...) +progress 11 Agent工作汇报("正在分析"、"已完成") +todos 11 待办任务快照(进度更新时每条) +user 1 用户反馈(如"需要补充性能测试") +assistant 10 Agent思考过程(💭 reasoning chain) +tool_result 16 工具调用记录(bash运行结果、API调用结果) +──────────────────────────────────────────────── +总计 59 完整工作轨迹 +``` + +看板展示时,用户可以: +- 📋 看流转链了解任务在哪个阶段 +- 📝 看 progress 了解Agent实时说了什么 +- ✅ 看 todos 了解任务拆解和完成进度 +- 💭 看 assistant/thinking 了解Agent的思考过程 +- 🔧 看 tool_result 了解每次工具调用的结果 +- 👤 看 user 了解是否有人工干预 + +--- + +### 2.4 调度系统:超时重试、停滞升级、自动回滚 + +#### 调度元数据结构 + +```python +_scheduler = { + # 配置参数 + 'enabled': True, + 'stallThresholdSec': 180, # 停滞多久后自动升级(默认180秒) + 'maxRetry': 1, # 自动重试次数(0=不重试,1=重试1次) + 'autoRollback': True, # 是否自动回滚到快照 + + # 运行时状态 + 'retryCount': 0, # 当前已重试几次 + 'escalationLevel': 0, # 0=无升级 1=门下协调 2=尚书协调 + 'stallSince': None, # 何时开始停滞的时间戳 + 'lastProgressAt': '2026-03-01T...', # 最后一次获得进展的时间 + 'lastEscalatedAt': '2026-03-01T...', + 'lastRetryAt': '2026-03-01T...', + + # 派发追踪 + 'lastDispatchStatus': 'success', # queued|success|failed|timeout|gateway-offline|error + 'lastDispatchAgent': 'zhongshu', + 'lastDispatchTrigger': 'state-transition', + 'lastDispatchError': '', # 错误堆栈(如有) + + # 快照(用于自动回滚) + 'snapshot': { + 'state': 'Assigned', + 'org': '尚书省', + 'now': '等待派发...', + 'savedAt': '2026-03-01T...', + 'note': 'scheduled-check' + } +} +``` + +#### 调度算法 + +每 60 秒运行一次 `handle_scheduler_scan(threshold_sec=180)`: + +``` +FOR EACH 任务: + IF state in (Done, Cancelled, Blocked): + SKIP # 终态不处理 + + elapsed_since_progress = NOW - lastProgressAt + + IF elapsed_since_progress < stallThreshold: + SKIP # 最近有进展,无需处理 + + # ── 停滞处理逻辑 ── + IF retryCount < maxRetry: + ✅ 执行【重试】 + - increment retryCount + - dispatch_for_state(task, new_state, trigger='taizi-scan-retry') + - flow_log: "停滞180秒,触发自动重试第N次" + - NEXT task + + IF escalationLevel < 2: + ✅ 执行【升级】 + - nextLevel = escalationLevel + 1 + - target_agent = menxia (if L=1) else shangshu (if L=2) + - wake_agent(target_agent, "💬 任务停滞,请介入协调推进") + - flow_log: "升级至{target_agent}协调" + - NEXT task + + IF escalationLevel >= 2 AND autoRollback: + ✅ 执行【自动回滚】 + - restore task to snapshot.state + - retryCount = 0 + - escalationLevel = 0 + - dispatch_for_state(task, snapshot.state, trigger='taiji-auto-rollback') + - flow_log: "连续停滞,自动回滚到{snapshot.state}" +``` + +#### 示例场景 + +**场景:中书省Agent进程崩溃,任务卡在 Zhongshu** + +``` +T+0: + 中书省正在规划方案 + lastProgressAt = T + dispatch status = success + +T+30: + Agent 进程意外崩溃(或超载无响应) + lastProgressAt 仍然 = T(没有新的 progress) + +T+60: + scheduler_scan 扫一遍,发现: + elapsed = 60 < 180,跳过 + +T+180: + scheduler_scan 扫一遍,发现: + elapsed = 180 >= 180,触发处理 + + ✅ 阶段1:重试 + - retryCount: 0 → 1 + - dispatch_for_state('JJC-20260228-E2E', 'Zhongshu', trigger='taizi-scan-retry') + - 派发消息发送到中书省(唤醒agent或重启) + - flow_log: "停滞180秒,自动重试第1次" + +T+ 240: + 中书省 Agent 恢复(或手工重启),收到重试派发 + 汇报进展:"已恢复,继续规划..." + lastProgressAt 更新为 T+240 + retryCount 重置为 0 + + ✓ 问题解决 + +T+360 (若仍未恢复): + scheduler_scan 再次扫,发现: + elapsed = 360 >= 180, retryCount 已经 = 1 + + ✅ 阶段2:升级 + - escalationLevel: 0 → 1 + - wake_agent('menxia', "💬 任务JJC-20260228-E2E停滞,中书省无反应,请介入") + - flow_log: "升级至门下省协调" + + 门下省Agent被唤醒,可以: + - 检查中书省是否在线 + - 若在线,询问进度 + - 若离线,可能启动应急流程(如由门下暂代起草) + +T+540 (若仍未解决): + scheduler_scan 再次扫,发现: + escalationLevel = 1, 还能升级到 2 + + ✅ 阶段3:再次升级 + - escalationLevel: 1 → 2 + - wake_agent('shangshu', "💬 任务长期停滞,中书省+门下省都无法推进,尚书省请介入协调") + - flow_log: "升级至尚书省协调" + +T+720 (若仍未解决): + scheduler_scan 再次扫,发现: + escalationLevel = 2(已最大),autoRollback = true + + ✅ 阶段4:自动回滚 + - snapshot.state = 'Assigned' (前一个稳定状态) + - task.state: Zhongshu → Assigned + - dispatch_for_state('JJC-20260228-E2E', 'Assigned', trigger='taizi-auto-rollback') + - flow_log: "连续停滞,自动回滚到Assigned,由尚书省重新派发" + + 结果: + - 尚书省重新派发给六部执行 + - 中书省的方案保留在前一个 snapshot 版本中 + - 用户可以看到回滚操作,决定是否介入 +``` + +--- + +## 🎯 第三部分:核心API与CLI工具 + +### 3.1 任务操作API端点 + +#### 任务创建:`POST /api/create-task` + +``` +请求: +{ + "title": "为三省六部编写完整自动化测试方案", + "org": "中书省", // 可选,默认太子 + "official": "中书令", // 可选 + "priority": "normal", + "template_id": "test_plan", // 可选 + "params": {}, + "target_dept": "兵部+刑部" // 可选,派发建议 +} + +响应: +{ + "ok": true, + "taskId": "JJC-20260228-001", + "message": "旨意 JJC-20260228-001 已下达,正在派发给太子" +} +``` + +#### 任务活动流:`GET /api/task-activity/{task_id}` + +``` +请求: +GET /api/task-activity/JJC-20260228-E2E + +响应: +{ + "ok": true, + "taskId": "JJC-20260228-E2E", + "taskMeta": { + "title": "为三省六部编写完整自动化测试方案", + "state": "Assigned", + "org": "尚书省", + "output": "", + "block": "无", + "priority": "normal" + }, + "agentId": "shangshu", + "agentLabel": "尚书省", + + // ── 完整活动流(59条示例)── + "activity": [ + // flow_log (10条) + { + "at": "2026-02-28T10:00:00Z", + "kind": "flow", + "from": "皇上", + "to": "太子", + "remark": "下旨:为三省六部编写完整自动化测试方案" + }, + // progress_log (11条) + { + "at": "2026-02-28T10:35:00Z", + "kind": "progress", + "text": "已接旨。分析测试需求,拟定三层测试方案...", + "agent": "zhongshu", + "agentLabel": "中书省", + "state": "Zhongshu", + "org": "中书省", + "tokens": 4500, + "cost": 0.0045, + "elapsed": 120 + }, + // todos (11条) + { + "at": "2026-02-28T15:00:00Z", + "kind": "todos", + "items": [ + {"id": "1", "title": "需求分析", "status": "completed"}, + {"id": "2", "title": "方案设计", "status": "in-progress"}, + {"id": "3", "title": "await审议", "status": "not-started"} + ], + "agent": "zhongshu", + "diff": { + "changed": [{"id": "2", "from": "not-started", "to": "in-progress"}], + "added": [], + "removed": [] + } + }, + // session活动 (26条总计) + // - assistant (10条) + { + "at": "2026-02-28T14:23:00Z", + "kind": "assistant", + "text": "基于需求,我建议采用三层测试架构:\n1. 单元测试覆盖核心函数\n2. 集成测试覆盖API端点\n3. E2E测试覆盖完整流程", + "thinking": "💭 考虑到项目的复杂性,需要覆盖七个Agent的交互逻辑。单元测试应该采用pytest,集成测试用server.py启动后的HTTP测试...", + "tools": [ + {"name": "bash", "input_preview": "find . -name '*.py' -type f | wc -l"}, + {"name": "file_read", "input_preview": "dashboard/server.py (first 100 lines)"} + ] + }, + // - tool_result (16条) + { + "at": "2026-02-28T14:24:00Z", + "kind": "tool_result", + "tool": "bash", + "exitCode": 0, + "output": "83", + "durationMs": 450 + } + ], + + "activitySource": "progress+session", + "relatedAgents": ["taizi", "zhongshu", "menxia"], + "phaseDurations": [ + { + "phase": "太子", + "durationText": "30分", + "ongoing": false + }, + { + "phase": "中书省", + "durationText": "4小时32分", + "ongoing": false + }, + { + "phase": "门下省", + "durationText": "1小时15分", + "ongoing": false + }, + { + "phase": "尚书省", + "durationText": "4小时10分", + "ongoing": true + } + ], + "totalDuration": "10小时27分", + "todosSummary": { + "total": 3, + "completed": 2, + "inProgress": 1, + "notStarted": 0, + "percent": 67 + }, + "resourceSummary": { + "totalTokens": 18500, + "totalCost": 0.0187, + "totalElapsedSec": 480 + } +} +``` + +#### 状态推进:`POST /api/advance-state/{task_id}` + +``` +请求: +{ + "comment": "任务分明该推进了" +} + +响应: +{ + "ok": true, + "message": "JJC-20260228-E2E 已推进到下一阶段 (已自动派发 Agent)", + "oldState": "Zhongshu", + "newState": "Menxia", + "targetAgent": "menxia" +} +``` + +#### 审批操作:`POST /api/review-action/{task_id}` + +``` +请求(准奏): +{ + "action": "approve", + "comment": "方案可行,已采纳改进建议" +} + +OR 请求(封驳): +{ + "action": "reject", + "comment": "需补充性能测试,第N轮审议" +} + +响应: +{ + "ok": true, + "message": "JJC-20260228-E2E 已准奏 (已自动派发 Agent)", + "state": "Assigned", + "reviewRound": 1 +} +``` + +--- + +### 3.2 CLI工具:kanban_update.py + +Agent 通过此工具与看板交互,共7个命令: + +#### 命令1:创建任务(太子或中书手工) + +```bash +python3 scripts/kanban_update.py create \ + JJC-20260228-E2E \ + "为三省六部编写完整自动化测试方案" \ + Zhongshu \ + 中书省 \ + 中书令 + +# 说明:通常不需要手工运行(看板API自动触发),除非debug +``` + +#### 命令2:更新状态 + +```bash +python3 scripts/kanban_update.py state \ + JJC-20260228-E2E \ + Menxia \ + "方案提交门下省审议" + +# 说明: +# - 第一个参数:task_id +# - 第二个参数:新状态(Pending/Taizi/Zhongshu/...) +# - 第三个参数:可选,描述信息(会记录到 now 字段) +# +# 效果: +# - task.state = Menxia +# - task.org 自动推断为 "门下省" +# - 触发派发 menxia agent +# - flow_log 记录转移 +``` + +#### 命令3:添加流转记录 + +```bash +python3 scripts/kanban_update.py flow \ + JJC-20260228-E2E \ + "中书省" \ + "门下省" \ + "📋 方案提交审核,请审议" + +# 说明: +# - 参数1:task_id +# - 参数2:from_dept(谁在上报) +# - 参数3:to_dept(流转到谁) +# - 参数4:remark(备注,可包含emoji) +# +# 注意:只是记录 flow_log,不改变 task.state +#(多用于细节流转,如部门间的协调) +``` + +#### 命令4:实时进展汇报(重点!) + +```bash +python3 scripts/kanban_update.py progress \ + JJC-20260228-E2E \ + "已完成需求分析和方案初稿,现正征询工部意见" \ + "1.需求分析✅|2.方案设计✅|3.工部咨询🔄|4.待门下审议" + +# 说明: +# - 参数1:task_id +# - 参数2:进展文本说明 +# - 参数3:todos 当前快照(用 | 分隔各项,支持emoji) +# +# 效果: +# - progress_log 添加新条目: +# { +# "at": now_iso(), +# "agent": inferred_agent_id, +# "text": "已完成需求分析和方案初稿,现正征询工部意见", +# "state": task.state, +# "org": task.org, +# "todos": [ +# {"id": "1", "title": "需求分析", "status": "completed"}, +# {"id": "2", "title": "方案设计", "status": "completed"}, +# {"id": "3", "title": "工部咨询", "status": "in-progress"}, +# {"id": "4", "title": "待门下审议", "status": "not-started"} +# ], +# "tokens": (自动从 openclaw 会话数据读取), +# "cost": (自动计算), +# "elapsed": (自动计算) +# } +# +# 看板效果: +# - 即时渲染为活动条目 +# - todos 进度条更新(67% 完成) +# - 资源消耗累加显示 +``` + +#### 命令5:任务完成 + +```bash +python3 scripts/kanban_update.py done \ + JJC-20260228-E2E \ + "https://github.com/org/repo/tree/feature/auto-test" \ + "自动化测试方案已完成,涵盖单元/集成/E2E三层,通过率98.5%" + +# 说明: +# - 参数1:task_id +# - 参数2:output URL(可以是代码仓库、文档链接等) +# - 参数3:最终总结 +# +# 效果: +# - task.state = Done(从 Review 推进) +# - task.output = "https://..." +# - 自动发送Feishu消息给皇上(太子转报) +# - flow_log 记录完成转移 +``` + +#### 命令6 & 7:停止/取消任务 + +```bash +# 叫停(随时可恢复) +python3 scripts/kanban_update.py stop \ + JJC-20260228-E2E \ + "等待工部反馈继续" + +# 说明: +# - task.state 暂存(_prev_state) +# - task.block = "等待工部反馈继续" +# - 看板显示 "⏸️ 已叫停" +# +# 恢复: +python3 scripts/kanban_update.py resume \ + JJC-20260228-E2E \ + "工部已反馈,继续执行" +# +# - task.state 恢复到 _prev_state +# - 重新派发 agent + +# 取消(不可恢复) +python3 scripts/kanban_update.py cancel \ + JJC-20260228-E2E \ + "业务需求变更,任务作废" +# +# - task.state = Cancelled +# - flow_log 记录取消原因 +``` + +--- + +## 💡 第四部分:对标与对比 + +### CrewAI / AutoGen 的传统方式 vs 三省六部的制度化方式 + +| 维度 | CrewAI | AutoGen | **三省六部** | +|------|--------|---------|----------| +| **协作模式** | 自由讨论(Agent自主选择协作对象) | 面板+回调(Human-in-the-loop) | **制度化协作(权限矩阵+状态机)** | +| **质量保障** | 依赖Agent智能(无审核)| Human审核(频繁中断) | **自动审核(门下省必审)+可干预** | +| **权限控制** | ❌ 无 | ⚠️ Hard-coded | **✅ 配置化权限矩阵** | +| **可观测性** | 低(Agent消息黑盒) | 中(Human看到对话)| **极高(59条活动/任务)** | +| **可干预性** | ❌ 无(跑起来后很难叫停) | ✅ 有(需要人工批准) | **✅ 有(一键stop/cancel/advance)** | +| **任务分发** | 不确定(Agent自主选) | 确定(Human手工分) | **自动确定(权限矩阵+状态机)** | +| **吞吐量** | 1任务1Agent(串行讨论) | 1任务1Team(需人工管理) | **多任务并行(六部同时执行)** | +| **失败恢复** | ❌(重新开始) | ⚠️(需人工调试) | **✅(自动重试3阶段)** | +| **成本控制** | 不透明(没有成本上限)| 中等(Human可叫停) | **透明(每条progress上报成本)** | + +### 业务契约的严格性 + +**CrewAI 的"温和"方式** +```python +# Agent可以自由选择下一步工作 +if task_seems_done: + # Agent自己决定要不要报告给其他Agent + send_message_to_someone() # 可能发错人,可能重复 +``` + +**三省六部的"严格"方式** +```python +# 任务状态严格受限,下一步由系统决定 +if task.state == 'Zhongshu' and agent_id == 'zhongshu': + # 只能做Zhongshu该做的事(起草方案) + deliver_plan_to_menxia() + + # 状态转移只能通过API,不能绕过 + # 中书不能直接转尚书,必须经过门下审议 + + # 若想绕过门下审议 + try: + dispatch_to(shangshu) # ❌ 权限检查拦截 + except PermissionError: + log.error(f'zhongshu 无权越权调用 shangshu') +``` + +--- + +## 🔍 第五部分:故障场景与恢复机制 + +### 场景1:Agent进程崩溃 + +``` +症状:任务卡在某个状态,180秒无新进展 +报警:太子调度系统检测到停滞 + +自动处理流程: + T+0: 崩溃 + T+180: scan 检测到停滞 + ✅ 第1阶段:自动重试 + - 派发消息到agent(唤醒或重启) + - 如果agent恢复,流程继续 + + T+360: 若仍未恢复 + ✅ 第2阶段:升级协调 + - 唤醒门下省agent + - 汇报:"中书省无响应,请介入" + - 门下可能接管或代理工作 + + T+540: 若仍未恢复 + ✅ 第3阶段:再次升级 + - 唤醒尚书省agent + - 汇报:"任务彻底卡住,请企业级协调" + + T+720: 若仍未恢复 + ✅ 第4阶段:自动回滚 + - 恢复到前一个稳定状态 + - 派发给尚书省重新处理 + - 用户可看到完整回滚链路 +``` + +### 场景2:Agent作恶(伪造数据) + +假设 `zhongshu` agent 想骗过系统: + +```python +# 尝试伪造门下省的准奏(直接改JSON) +task['flow_log'].append({ + 'from': '门下省', # ❌ 假冒身份 + 'to': '尚书省', + 'remark': '✅ 准奏' +}) + +# 系统防御: +# 1. 权限验证:API 层检查 HTTP 请求者身份 +# ├─ 来自 zhongshu agent 的请求不能直接 flow +# ├─ 必须通过 flow_log 记录,且签名验证 +# └─ 签名不匹配则拒绝 +# 2. 状态机验证:状态转移受控 +# ├─ 即使 flow_log 被篡改,state 仍然是 Zhongshu +# ├─ 下一步只能由 gate-keeper 系统转移 +# └─ zhongshu 无权自己改 state + +# 结果:❌ Agent 的伪造被系统拦截 +``` + +### 场景3:业务流程违反(如中书越权调尚书改方案) + +```python +# 中书省想绕过门下审议,直接咨询尚书省 +try: + result = dispatch_to_agent('shangshu', '请帮我审查一下这个方案') +except PermissionError: + # ❌ 权限矩阵拦截 + log.error('zhongshu 无权调用 shangshu (仅限: menxia, shangshu)') + +# 门下省想升级到皇上 +try: + result = dispatch_to_agent('taizi', '我需要皇上的指示') +except PermissionError: + # ❌ 权限矩阵拦截 + log.error('menxia 无权调用 taizi') +``` + +--- + +## 📊 第六部分:监控与可观测性 + +### 看板的10个视图面板 + +``` +1. 全任务列表 + └─ 所有任务的汇总视图(按创建时间倒序) + └─ 快速过滤:活跃/完成/已封驳 + +2. 按状态分类 + ├─ Pending(待处理) + ├─ Taizi(太子分拣中) + ├─ Zhongshu(中书规划中) + ├─ Menxia(门下审议中) + ├─ Assigned(尚书派发中) + ├─ Doing(六部执行中) + ├─ Review(尚书汇总中) + └─ Done/Cancelled(已完成/已取消) + +3. 按部门分类 + ├─ 太子任务 + ├─ 中书省任务 + ├─ 门下省任务 + ├─ 尚书省任务 + ├─ 六部任务(并行视图) + └─ 已派发任务 + +4. 按优先级分类 + ├─ 🔴 Critical(紧急) + ├─ 🟠 High(高优) + ├─ 🟡 Normal(普通) + └─ 🔵 Low(低优) + +5. Agent 在线状态 + ├─ 🟢 运行中(正在处理任务) + ├─ 🟡 待命(最近有活动,闲置) + ├─ ⚪ 空闲(超过10分钟无活动) + ├─ 🔴 离线(Gateway 未启动) + └─ ❌ 未配置(工作空间不存在) + +6. 任务详情面板 + ├─ 基本信息(标题、创建人、优先级) + ├─ 完整活动流(flow_log + progress_log + session) + ├─ 阶段耗时统计(各Agent停留时间) + ├─ Todos 进度条 + └─ 资源消耗(tokens/cost/elapsed) + +7. 停滞任务监控 + ├─ 列出所有超过阈值未推进的任务 + ├─ 显示停滞时长 + ├─ 快速操作:重试/升级/回滚 + +8. 审批工单池 + ├─ 清单所有在 Menxia 等待审批的任务 + ├─ 按停留时长排序 + ├─ 一键准奏/封驳 + +9. 今日概览 + ├─ 今日新建任务数 + ├─ 今日完成任务数 + ├─ 平均流转时长 + ├─ 各Agent活动频率 + +10. 历史报表 + ├─ 周报(人均产出、平均周期) + ├─ 月报(部门协作效率) + └─ 成本分析(API调用成本、Agent工作量) +``` + +### 实时API:Agent 在线检测 + +``` +GET /api/agents-status + +响应: +{ + "ok": true, + "gateway": { + "alive": true, // 进程存在 + "probe": true, // HTTP 响应正常 + "status": "🟢 运行中" + }, + "agents": [ + { + "id": "taizi", + "label": "太子", + "status": "running", // running|idle|offline|unconfigured + "statusLabel": "🟢 运行中", + "lastActive": "03-02 14:30", // 最后活跃时间 + "lastActiveTs": 1708943400000, + "sessions": 42, // 活跃session数 + "hasWorkspace": true, + "processAlive": true + }, + // ... 其他agent ... + ] +} +``` + +--- + +## 🎓 第七部分:使用示例与最佳实践 + +### 完整案例:创建→分发→执行→完成 + +```bash +# ═══════════════════════════════════════════════════════════ +# 第1步:皇上下旨(飞书消息或看板API) +# ═══════════════════════════════════════════════════════════ + +curl -X POST http://127.0.0.1:7891/api/create-task \ + -H "Content-Type: application/json" \ + -d '{ + "title": "编写三省六部协议文档", + "priority": "high" + }' + +# 响应:JJC-20260302-001 已创建 +# 太子Agent 收到通知:"📜 皇上旨意..." + +# ═══════════════════════════════════════════════════════════ +# 第2步:太子接旨分拣(Agent自动) +# ═══════════════════════════════════════════════════════════ + +# 太子Agent 判定:这是"工作旨意"(非闲聊) +# 自动运行: +python3 scripts/kanban_update.py state \ + JJC-20260302-001 \ + Zhongshu \ + "分拣完毕,转中书省起草" + +# 中书省Agent 收到派发通知 + +# ═══════════════════════════════════════════════════════════ +# 第3步:中书起草(Agent工作) +# ═══════════════════════════════════════════════════════════ + +# 中书Agent 分析需求、拆解任务 + +# 第一次汇报(30分钟后): +python3 scripts/kanban_update.py progress \ + JJC-20260302-001 \ + "已完成需求分析,拟定三部分文档:概述|技术栈|使用指南" \ + "1.需求分析✅|2.文档规划✅|3.内容编写🔄|4.审查待完成" + +# 看板显示: +# - 进度条:50% 完成 +# - 活动流:新增 progress + todos 条目 +# - 消耗:1200 tokens, $0.0012, 18分钟 + +# 第二次汇报(再过90分钟): +python3 scripts/kanban_update.py progress \ + JJC-20260302-001 \ + "文档初稿已完成,现提交门下省审议" \ + "1.需求分析✅|2.文档规划✅|3.内容编写✅|4.待审查" + +python3 scripts/kanban_update.py flow \ + JJC-20260302-001 \ + "中书省" \ + "门下省" \ + "提交审议" + +python3 scripts/kanban_update.py state \ + JJC-20260302-001 \ + Menxia \ + "方案提交门下省审议" + +# 门下省Agent 收到派发通知,开始审议 + +# ═══════════════════════════════════════════════════════════ +# 第4步:门下审议(Agent工作) +# ═══════════════════════════════════════════════════════════ + +# 门下Agent 审查文档质量 + +# 审议结果(30分钟后): + +# 情景A:准奏 +python3 scripts/kanban_update.py state \ + JJC-20260302-001 \ + Assigned \ + "✅ 准奏,已采纳改进建议" + +python3 scripts/kanban_update.py flow \ + JJC-20260302-001 \ + "门下省" \ + "尚书省" \ + "✅ 准奏:文档质量良好,建议补充代码示例" + +# 尚书省Agent 收到派发 + +# 情景B:封驳 +python3 scripts/kanban_update.py state \ + JJC-20260302-001 \ + Zhongshu \ + "🚫 封驳:需补充协议规范部分" + +python3 scripts/kanban_update.py flow \ + JJC-20260302-001 \ + "门下省" \ + "中书省" \ + "🚫 封驳:协议部分过于简略,需补充权限矩阵示例" + +# 中书省Agent 收到唤醒,重新修改方案 +# (3小时后 → 重新提交门下审议) + +# ═══════════════════════════════════════════════════════════ +# 第5步:尚书派发(Agent工作) +# ═══════════════════════════════════════════════════════════ + +# 尚书省Agent 分析文档应派给谁: +# - 礼部:文档排版和格式 +# - 兵部:代码示例补充 +# - 工部:部署文档 + +python3 scripts/kanban_update.py state \ + JJC-20260302-001 \ + Doing \ + "派发给礼部+兵部+工部三部并行执行" + +python3 scripts/kanban_update.py flow \ + JJC-20260302-001 \ + "尚书省" \ + "六部" \ + "派发执行:礼部排版|兵部代码示例|工部基础设施部分" + +# 六部Agent 分别收到派发 + +# ═══════════════════════════════════════════════════════════ +# 第6步:六部执行(并行) +# ═══════════════════════════════════════════════════════════ + +# 礼部进展汇报(20分钟): +python3 scripts/kanban_update.py progress \ + JJC-20260302-001 \ + "已完成文档排版和目录调整,现待其他部门内容补充" \ + "1.排版✅|2.目录调整✅|3.等待代码示例|4.等待基础设施部分" + +# 兵部进展汇报(40分钟): +python3 scripts/kanban_update.py progress \ + JJC-20260302-001 \ + "已编写5个代码示例(权限检查、派发流程、session融合等),待集成到文档" \ + "1.分析需求✅|2.编码示例✅|3.集成文档🔄|4.测试验证" + +# 工部进展汇报(60分钟): +python3 scripts/kanban_update.py progress \ + JJC-20260302-001 \ + "已编写Docker+K8s部署部分,Nginx配置和让证书更新文案完成" \ + "1.Docker编写✅|2.K8s配置✅|3.一键部署脚本🔄|4.部署文档待完成" + +# ═══════════════════════════════════════════════════════════ +# 第7步:尚书汇总(Agent工作) +# ═══════════════════════════════════════════════════════════ + +# 等所有部门汇报完成后,尚书省汇总所有成果 + +python3 scripts/kanban_update.py progress \ + JJC-20260302-001 \ + "全部部门已完成。汇总成果:\n- 文档已排版,包含9个章节\n- 15个代码示例已集成\n- 完整部署指南已编写\n通过率:100%" \ + "1.排版✅|2.代码示例✅|3.基础设施✅|4.汇总✅" + +python3 scripts/kanban_update.py state \ + JJC-20260302-001 \ + Review \ + "所有部门完成,进入审查阶段" + +# 皇上/太子收到通知,审查最终成果 + +# ═══════════════════════════════════════════════════════════ +# 第8步:完成(终态) +# ═══════════════════════════════════════════════════════════ + +python3 scripts/kanban_update.py done \ + JJC-20260302-001 \ + "https://github.com/org/repo/docs/architecture.md" \ + "三省六部协议文档已完成,包含89页,5个阶段历时3天,总消耗成本$2.34" + +# 看板显示: +# - 状态:Done ✅ +# - 总耗时:3天2小时45分 +# - 完整活动流:79条活动记录 +# - 资源统计:87500 tokens, $2.34, 890分钟总工作时间 + +# ═══════════════════════════════════════════════════════════ +# 查询最终成果 +# ═══════════════════════════════════════════════════════════ + +curl http://127.0.0.1:7891/api/task-activity/JJC-20260302-001 + +# 响应: +# { +# "taskMeta": { +# "state": "Done", +# "output": "https://github.com/org/repo/docs/architecture.md" +# }, +# "activity": [79条完整流转链], +# "totalDuration": "3天2小时45分", +# "resourceSummary": { +# "totalTokens": 87500, +# "totalCost": 2.34, +# "totalElapsedSec": 53700 +# } +# } +``` + +--- + +## 📋 总结 + +**三省六部是一个制度化的AI多Agent系统**,不是传统的"自由讨论"框架。它通过: + +1. **业务层**:模仿古代帝国官僚体系,建立分权制衡的组织结构 +2. **技术层**:状态机 + 权限矩阵 + 自动派发 + 调度重试,确保流程可控 +3. **观测层**:React 看板 + 完整活动流(59条/任务),实时掌握全局 +4. **介入层**:一键stop/cancel/advance,遇到异常能立即纠正 + +**核心价值**:用制度确保质量,用透明确保信心,用自动化确保效率。 + +相比 CrewAI/AutoGen 的"自由+人工管理",三省六部提供了一套**企业级的AI协作框架**。 + + + +# 我用三省六部制重新设计了 AI 多 Agent 协作架构 + +> 1300 年前的制度设计,比现代 AI 框架更懂分权。 + +![封面:军机处看板全貌](screenshots/01-kanban-main.png) + +--- + +## 一、一个奇怪的想法 + +去年底我开始重度使用 AI Agent 干活——写代码、做分析、生成文档。用的是市面上最火的几个多 Agent 框架。 + +用了一个月,我发现一个根本性的问题: + +**这些框架没有"审核"这个概念。** + +CrewAI 的模式是:几个 Agent 各自干活,做完就交。AutoGen 好一点,有个 Human-in-the-loop,但本质上是你自己当 QA。MetaGPT 有角色分工,但审核是可选的。 + +就像一家公司没有 QA 部门,工程师写完代码直接部署到线上。 + +然后你拿到最终结果,不知道中间发生了什么,无法复现,无法审计,无法干预。出了问题只能重跑。 + +我一直在想:有没有一种架构,天然就把审核嵌入到流程里,不是可选的插件,而是必须经过的关卡? + +然后有一天,我在翻《资治通鉴》的时候突然想到—— + +**三省六部制。** + +唐太宗在 1300 年前就设计了这个制度:中书省草拟政令,门下省审议封驳,尚书省执行。三个部门互相制衡,任何政令必须经过审议才能下发。 + +这不就是我要找的架构吗? + +![上朝仪式:每日首次打开的彩蛋动画](screenshots/11-ceremony.png) +*▲ 每天第一次打开看板,会有一个"上朝"开场动画——仪式感拉满* + +--- + +## 二、古人的架构设计 + +三省六部制不是一个 metaphor,它是一套经过 1400 年实践检验的分权制衡系统。 + +简化一下,信息流是这样的: + +``` +皇上(你) + ↓ 下旨 +中书省(规划) ← 把你的一句话拆成可执行的子任务 + ↓ 提交审核 +门下省(审议) ← 审查方案质量,不行就封驳打回 + ↓ 准奏 +尚书省(派发) ← 分配给六部执行 + ↓ +六部(执行) ← 户部管数据、礼部管文档、兵部管开发、刑部管合规、工部管基建 + ↓ +尚书省汇总回奏 ← 结果回报给你 +``` + +注意这里最关键的一步:**门下省审议**。 + +中书省规划完方案后,不是直接扔给执行层——必须先经过门下省审议。门下省会检查: + +- 子任务拆解是否合理?有没有遗漏需求? +- 部门分配是否准确?该派兵部的是不是错派给了礼部? +- 方案是否可执行?有没有不切实际的地方? + +如果不合格,门下省可以**封驳**——直接打回让中书省重新规划。不是一个 warning,是强制返工。 + +这就是为什么唐朝能运转 289 年。**不受制约的权力必然会出错**,唐太宗想得很清楚。 + +--- + +## 三、我把它做成了开源项目 + +我用 OpenClaw 搭了一个真正的三省六部系统。9 个 AI Agent 各司其职,严格按照权限矩阵通信。 + +项目叫 **Edict(三省六部)**,已开源: + +**GitHub:https://github.com/cft0808/edict** + +核心架构很简单: + +- **中书省**:接旨(你的指令),规划方案,拆解子任务 +- **门下省**:审议方案,质量把关,不合格直接封驳 +- **尚书省**:准奏后派发给六部,协调执行,汇总结果 +- **六部**:户部(数据分析)、礼部(文档撰写)、兵部(代码开发)、刑部(安全合规)、工部(CI/CD 部署) +- **早朝官**:每天给你推一份新闻简报 + +每个 Agent 有独立的 Workspace、独立的 Skills、独立的 LLM 模型。严格的权限矩阵——谁能给谁发消息,白纸黑字: + +| 谁 ↓ 给谁发 → | 中书 | 门下 | 尚书 | 六部 | +|:---:|:---:|:---:|:---:|:---:| +| **中书省** | — | ✅ | ✅ | ❌ | +| **门下省** | ✅ | — | ✅ | ❌ | +| **尚书省** | ✅ | ✅ | — | ✅ | +| **六部** | ❌ | ❌ | ✅ | ❌ | + +中书省不能直接指挥六部,六部不能越级上报中书省。所有的跨层通信必须经过尚书省中转。 + +**这不是装饰性的设定,这是架构层面的强制约束。** + +![Demo:30 秒看完整流转](demo.gif) +*▲ 30 秒 Demo:从上朝仪式到旨意看板、奏折归档、模型配置的完整巡览* + +--- + +## 四、跟现有框架对比 + +你可能会问:跟 CrewAI、AutoGen 比,差在哪? + +| | CrewAI | AutoGen | **三省六部** | +|---|:---:|:---:|:---:| +| 审核机制 | ❌ | ⚠️ 可选 | ✅ 门下省强制审核 | +| 实时看板 | ❌ | ❌ | ✅ 10 个面板 | +| 任务干预 | ❌ | ❌ | ✅ 叫停 / 取消 / 恢复 | +| 流转审计 | ⚠️ | ❌ | ✅ 完整奏折存档 | +| Agent 健康监控 | ❌ | ❌ | ✅ 心跳检测 | +| 热切换 LLM | ❌ | ❌ | ✅ 看板内一键切换 | + +最核心的差异是**门下省审核机制**。 + +这不是 Human-in-the-loop(那是让你自己当 QA),这是一个专职的 AI Agent 负责审核另一个 AI Agent 的产出。制度性的,强制的,架构级别的。 + +一个不经审核的 AI 协作系统,就像一个没有代码 review 的团队——跑得快,翻车也快。 + +--- + +## 五、军机处看板——让一切可观测 + +光有架构不够,你还得看得见。 + +所以我做了一个**军机处看板**——一个实时监控所有任务流转的 Web 面板。零依赖,单文件 HTML,Python 标准库后端,打开浏览器就能用。 + +10 个功能面板: + +**📋 旨意看板**:所有任务以卡片形式展示,按状态分列,支持过滤搜索。每张卡片有心跳徽章——🟢 活跃、🟡 停滞、🔴 告警。点开看完整的流转时间线,随时可以叫停或取消。 + +![旨意看板](screenshots/01-kanban-main.png) +*▲ 旨意看板:任务卡片按状态分列,心跳徽章一目了然* + +**🔭 省部调度**:可视化各状态的任务数量、部门分布、Agent 健康卡片。一眼看清谁在忙、谁在闲、谁宕机了。 + +![省部调度](screenshots/02-monitor.png) +*▲ 省部调度:状态分布 + 部门负载 + Agent 健康卡片* + +**📜 奏折阁**:所有已完成的旨意自动归档为"奏折",展示完整的五阶段时间线——圣旨→中书规划→门下审议→六部执行→回奏。一键复制为 Markdown。 + +![奏折归档](screenshots/08-memorials.png) +*▲ 奏折阁:完整的五阶段时间线,一键导出 Markdown* + +**📜 旨库**:9 个预设圣旨模板。选一个,填参数,预览,一键下旨。覆盖:周报生成、代码审查、API 设计、竞品分析等常见场景。 + +![圣旨模板库](screenshots/09-templates.png) +*▲ 旨库:9 个预设模板,填参数一键下旨* + +**⚙️ 模型配置**:每个 Agent 可以独立切换 LLM 模型。中书省用 Claude 做规划,兵部用 GPT-4o 写代码,户部用 DeepSeek 算数据——各取所长。 + +![模型配置](screenshots/04-model-config.png) +*▲ 模型配置:每个 Agent 独立切换 LLM,各取所长* + +还有官员总览(Token 消耗排行榜)、技能管理、天下要闻(自动新闻聚合)、会话监控、上朝仪式(每天首次打开的彩蛋动画)。 + +**全部零依赖**,没有 React 也没有 Vue,纯 HTML + CSS + JavaScript,2200 行搞定。 + +![官员总览](screenshots/06-official-overview.png) +*▲ 官员总览:Token 消耗排行榜 + 活跃度统计* + +![天下要闻](screenshots/10-morning-briefing.png) +*▲ 天下要闻:每日自动聚合科技/财经资讯* + +--- + +## 六、跑一个真实案例给你看 + +光说不练不行。来看一个真实的运行记录——让三省六部分析竞品。 + +**旨意**:分析 CrewAI、AutoGen 和 LangGraph 这三个框架的差异,输出对比报告。 + +![任务流转详情](screenshots/03-task-detail.png) +*▲ 点开任意任务卡片,可以看到完整的流转链和实时状态* + +### 中书省规划(45 秒) + +中书省接旨后,拆成了 4 个子任务: +1. 兵部 → 架构与通信机制调研 +2. 户部 → 数据采集与量化对比(GitHub Stars、Contributors 等) +3. 兵部 → 开发者体验深度评测 +4. 礼部 → 汇总写对比报告 + +### 门下省审议(32 秒)—— 封驳了! + +**门下省第一轮直接打回:** + +> *"方案有三个问题:1)旨意明确要求评测'可观测性',但规划里没有对应子任务;2)子任务 1 和 3 都是兵部调研,有重叠,建议合并;3)缺少推荐场景的结论性子任务——分析没有结论等于没分析。驳回。"* + +中书省修改方案后,门下省第二轮准奏。 + +**这就是门下省的价值。** 如果没有这一步,兵部会做两次调研,最终报告里也不会有推荐场景——因为原始规划里就没要求。 + +### 各部执行(17 分钟) + +- **兵部**:技术深度对比,覆盖架构、通信、可观测性三维度 +- **户部**:量化数据表——Stars、Contributors、Issue 响应时间、Hello World 搭建时长 +- **礼部**:整合兵部 + 户部数据,撰写最终报告 + +### 回奏 + +22 分钟,15800 Token,一份结构化对比报告。结论很有意思: + +| 场景 | 推荐 | 理由 | +|------|------|------| +| 快速原型 | CrewAI | 上手最快 | +| 对话式协作 | AutoGen | 天然适合多轮讨论 | +| 复杂工作流 | LangGraph | 状态机最灵活 | +| **可靠性优先** | **三省六部** | 唯一内置强制审核 | + +--- + +## 七、技术上的一些选择 + +做这个项目的时候,我做了几个刻意的技术决策: + +**1. 零依赖** + +看板前端是一个 HTML 文件,2200 行,没有用任何框架。后端是 Python 标准库的 `http.server`,没有 Flask 也没有 FastAPI。 + +为什么?因为我不想让人跑之前先 `pip install` 一堆东西。这个项目的目标用户可能只是想快速体验一下三省六部的流转效果,不想搭环境。 + +**2. 每个 Agent 一个 SOUL.md** + +每个 Agent 的人格、职责、工作流规则都写在一个 Markdown 文件里。想修改门下省的审核标准?编辑 `agents/menxia/SOUL.md`,下次启动自动生效。 + +这意味着你可以定制自己的三省六部——也许你的"兵部"不是负责工程,而是负责市场分析。改个 SOUL.md 就行。 + +**3. 权限矩阵是强制的** + +不是"建议"Agent 之间不要越级通信,是在架构层面强制限制。六部不能给中书省发消息,中书省不能绕过门下省直接让尚书省执行。OpenClaw 的配置文件里白纸黑字写着谁能跟谁说话。 + +--- + +## 八、现在你可以试了 + +项目已经开源,MIT 协议。 + +**GitHub:https://github.com/cft0808/edict** + +最快的体验方式: + +```bash +# Docker 一行启动 +docker run -p 7891:7891 cft0808/edict + +# 打开浏览器 +open http://localhost:7891 +``` + +如果你装了 OpenClaw,可以完整安装: + +```bash +git clone https://github.com/cft0808/edict.git +cd edict +chmod +x install.sh && ./install.sh +``` + +安装脚本自动创建 9 个 Agent Workspace、写入人格文件、注册权限矩阵、重启 Gateway。 + +![技能配置](screenshots/05-skills-config.png) +*▲ 技能管理:各省部已安装的 Skills 一览,可查看详情和添加新技能* + +--- + +## 九、下一步 + +Phase 1(核心架构)已经完成了。接下来要做的几件事: + +- **御批模式**:让门下省的审议结果可以推送到你的飞书/Telegram,你亲自决定准奏还是封驳 +- **功过簿**:每个 Agent 的绩效评分——完成率、返工率、耗时统计 +- **急递铺**:看板里加一个实时的 Agent 通信流向图——中书省发消息给门下省的时候,连线亮一下 +- **国史馆**:把历史旨意和奏折沉淀成知识库,新旨意可以参考历史经验 + +完整 Roadmap 在 GitHub 上,Phase 2 和 Phase 3 的每个子项都标了难度,欢迎认领。 + +--- + +## 最后 + +AI Agent 协作的核心问题不是"让 Agent 更聪明",而是"让 Agent 的协作有规矩"。 + +CrewAI 解决了"多个 Agent 一起干活"的问题。AutoGen 解决了"Agent 之间能对话"的问题。 + +但谁来解决"Agent 的产出质量有保障"的问题? + +唐太宗在 1300 年前就给出了答案:**分权制衡**。规划的不审核,审核的不执行,执行的不规划。每一个环节都有人盯着,每一个决策都要经过审议。 + +这可能是我见过的、最优雅的"AI 治理"方案——因为它根本不是为 AI 设计的。 + +它是为**治理**本身设计的。 + +--- + +**GitHub:https://github.com/cft0808/edict** + +开源 · MIT · 欢迎 Star ⚔️ + + + +from .tasks import router as tasks_router +from .agents import router as agents_router +from .events import router as events_router +from .admin import router as admin_router +from .websocket import router as websocket_router + +__all__ = [ + "tasks_router", + "agents_router", + "events_router", + "admin_router", + "websocket_router", +] + + + +"""Admin API — 管理操作(迁移、诊断、配置)。""" + +import json +import logging +from pathlib import Path + +from fastapi import APIRouter, Depends +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import text + +from ..db import get_db +from ..services.event_bus import get_event_bus + +log = logging.getLogger("edict.api.admin") +router = APIRouter() + + +@router.get("/health/deep") +async def deep_health(db: AsyncSession = Depends(get_db)): + """深度健康检查:Postgres + Redis 连通性。""" + checks = {"postgres": False, "redis": False} + + # Postgres + try: + result = await db.execute(text("SELECT 1")) + checks["postgres"] = result.scalar() == 1 + except Exception as e: + checks["postgres_error"] = str(e) + + # Redis + try: + bus = await get_event_bus() + pong = await bus.redis.ping() + checks["redis"] = pong is True + except Exception as e: + checks["redis_error"] = str(e) + + status = "ok" if all(checks.get(k) for k in ["postgres", "redis"]) else "degraded" + return {"status": status, "checks": checks} + + +@router.get("/pending-events") +async def pending_events( + topic: str = "task.dispatch", + group: str = "dispatcher", + count: int = 20, +): + """查看未 ACK 的 pending 事件(诊断工具)。""" + bus = await get_event_bus() + pending = await bus.get_pending(topic, group, count) + return { + "topic": topic, + "group": group, + "pending": [ + { + "entry_id": str(p.get("message_id", "")), + "consumer": str(p.get("consumer", "")), + "idle_ms": p.get("time_since_delivered", 0), + "delivery_count": p.get("times_delivered", 0), + } + for p in pending + ] if pending else [], + } + + +@router.post("/migrate/check") +async def migration_check(): + """检查旧数据文件是否存在。""" + data_dir = Path(__file__).parents[4] / "data" + files = { + "tasks_source": (data_dir / "tasks_source.json").exists(), + "live_status": (data_dir / "live_status.json").exists(), + "agent_config": (data_dir / "agent_config.json").exists(), + "officials_stats": (data_dir / "officials_stats.json").exists(), + } + return {"data_dir": str(data_dir), "files": files} + + +@router.get("/config") +async def get_config(): + """获取当前运行配置(脱敏)。""" + from ..config import get_settings + settings = get_settings() + return { + "port": settings.port, + "debug": settings.debug, + "database": settings.database_url.split("@")[-1] if "@" in settings.database_url else "***", + "redis": settings.redis_url.split("@")[-1] if "@" in settings.redis_url else settings.redis_url, + "scheduler_scan_interval": settings.scheduler_scan_interval_seconds, + } + + + +"""Agents API — Agent 配置和状态查询。""" + +import json +import logging +from pathlib import Path + +from fastapi import APIRouter + +log = logging.getLogger("edict.api.agents") +router = APIRouter() + +# Agent 元信息(对应 agents/ 目录下的 SOUL.md) +AGENT_META = { + "zaochao": {"name": "早朝(朝会主持)", "role": "朝会召集与议程管理", "icon": "🏛️"}, + "shangshu": {"name": "尚书令", "role": "总协调与任务监督", "icon": "📜"}, + "zhongshu": {"name": "中书省", "role": "起草诏令与方案规划", "icon": "✍️"}, + "menxia": {"name": "门下省", "role": "审核与封驳", "icon": "🔍"}, + "libu": {"name": "吏部", "role": "人事与组织管理", "icon": "👤"}, + "hubu": {"name": "户部", "role": "财务与资源管理", "icon": "💰"}, + "gongbu": {"name": "工部", "role": "工程与技术实施", "icon": "🔧"}, + "xingbu": {"name": "刑部", "role": "规范与质量审查", "icon": "⚖️"}, + "bingbu": {"name": "兵部", "role": "安全与应急响应", "icon": "🛡️"}, +} + + +@router.get("") +async def list_agents(): + """列出所有可用 Agent。""" + agents = [] + for agent_id, meta in AGENT_META.items(): + agents.append({ + "id": agent_id, + **meta, + }) + return {"agents": agents} + + +@router.get("/{agent_id}") +async def get_agent(agent_id: str): + """获取 Agent 详情。""" + meta = AGENT_META.get(agent_id) + if not meta: + return {"error": f"Agent '{agent_id}' not found"}, 404 + + # 尝试读取 SOUL.md + soul_path = Path(__file__).parents[4] / "agents" / agent_id / "SOUL.md" + soul_content = "" + if soul_path.exists(): + soul_content = soul_path.read_text(encoding="utf-8")[:2000] + + return { + "id": agent_id, + **meta, + "soul_preview": soul_content, + } + + +@router.get("/{agent_id}/config") +async def get_agent_config(agent_id: str): + """获取 Agent 运行时配置。""" + config_path = Path(__file__).parents[4] / "data" / "agent_config.json" + if not config_path.exists(): + return {"agent_id": agent_id, "config": {}} + + try: + configs = json.loads(config_path.read_text(encoding="utf-8")) + agent_config = configs.get(agent_id, {}) + return {"agent_id": agent_id, "config": agent_config} + except (json.JSONDecodeError, IOError): + return {"agent_id": agent_id, "config": {}} + + + +"""Events API — 事件查询与审计。""" + +import logging +from datetime import datetime + +from fastapi import APIRouter, Query +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from fastapi import Depends + +from ..db import get_db +from ..models.event import Event +from ..services.event_bus import get_event_bus + +log = logging.getLogger("edict.api.events") +router = APIRouter() + + +@router.get("") +async def list_events( + trace_id: str | None = None, + topic: str | None = None, + producer: str | None = None, + limit: int = Query(default=50, le=500), + db: AsyncSession = Depends(get_db), +): + """查询持久化事件(从 Postgres event 表)。""" + stmt = select(Event) + if trace_id: + stmt = stmt.where(Event.trace_id == trace_id) + if topic: + stmt = stmt.where(Event.topic == topic) + if producer: + stmt = stmt.where(Event.producer == producer) + stmt = stmt.order_by(Event.timestamp.desc()).limit(limit) + result = await db.execute(stmt) + events = result.scalars().all() + return { + "events": [ + { + "event_id": str(e.event_id), + "trace_id": e.trace_id, + "topic": e.topic, + "event_type": e.event_type, + "producer": e.producer, + "payload": e.payload, + "meta": e.meta, + "timestamp": e.timestamp.isoformat() if e.timestamp else None, + } + for e in events + ], + "count": len(events), + } + + +@router.get("/stream-info") +async def stream_info(topic: str = Query(description="Stream topic")): + """查询 Redis Stream 实时信息。""" + bus = await get_event_bus() + info = await bus.stream_info(topic) + return {"topic": topic, "info": info} + + +@router.get("/topics") +async def list_topics(): + """列出所有可用事件 topic。""" + from ..services.event_bus import ( + TOPIC_TASK_CREATED, + TOPIC_TASK_STATUS, + TOPIC_TASK_DISPATCH, + TOPIC_TASK_COMPLETED, + TOPIC_TASK_STALLED, + TOPIC_AGENT_THOUGHTS, + TOPIC_AGENT_HEARTBEAT, + ) + return { + "topics": [ + {"name": TOPIC_TASK_CREATED, "description": "任务创建"}, + {"name": TOPIC_TASK_STATUS, "description": "状态变更"}, + {"name": TOPIC_TASK_DISPATCH, "description": "Agent 派发"}, + {"name": TOPIC_TASK_COMPLETED, "description": "任务完成"}, + {"name": TOPIC_TASK_STALLED, "description": "任务停滞"}, + {"name": TOPIC_AGENT_THOUGHTS, "description": "Agent 思考流"}, + {"name": TOPIC_AGENT_HEARTBEAT, "description": "Agent 心跳"}, + ] + } + + + +"""Legacy 兼容路由 — 通过旧版 task_id (JJC-xxx) 操作任务。 + +旧版 kanban_update.py 使用自定义 ID (JJC-20260301-007), +Edict 使用 UUID。此路由通过 tags 或 meta.legacy_id 映射。 +""" + +import logging +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from ..db import get_db +from ..models.task import Task, TaskState +from ..services.event_bus import get_event_bus +from ..services.task_service import TaskService + +log = logging.getLogger("edict.api.legacy") +router = APIRouter() + + +async def _find_by_legacy_id(db: AsyncSession, legacy_id: str) -> Task | None: + """通过旧版 ID 查找任务(在 tags 或 meta.legacy_id 中搜索)。""" + # 方式1: tags 包含 legacy_id + stmt = select(Task).where(Task.tags.contains([legacy_id])) + result = await db.execute(stmt) + task = result.scalars().first() + if task: + return task + + # 方式2: meta->legacy_id + stmt2 = select(Task).where(Task.meta["legacy_id"].astext == legacy_id) + result2 = await db.execute(stmt2) + return result2.scalars().first() + + +class LegacyTransition(BaseModel): + new_state: str + agent: str = "system" + reason: str = "" + + +class LegacyProgress(BaseModel): + agent: str + content: str + + +class LegacyTodoUpdate(BaseModel): + todos: list[dict] + + +@router.post("/by-legacy/{legacy_id}/transition") +async def legacy_transition( + legacy_id: str, + body: LegacyTransition, + db: AsyncSession = Depends(get_db), +): + task = await _find_by_legacy_id(db, legacy_id) + if not task: + raise HTTPException(status_code=404, detail=f"Legacy task not found: {legacy_id}") + + bus = await get_event_bus() + svc = TaskService(db, bus) + try: + new_state = TaskState(body.new_state) + except ValueError: + raise HTTPException(status_code=400, detail=f"Invalid state: {body.new_state}") + + try: + t = await svc.transition_state(task.task_id, new_state, body.agent, body.reason) + return {"task_id": str(t.task_id), "state": t.state.value} + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + +@router.post("/by-legacy/{legacy_id}/progress") +async def legacy_progress( + legacy_id: str, + body: LegacyProgress, + db: AsyncSession = Depends(get_db), +): + task = await _find_by_legacy_id(db, legacy_id) + if not task: + raise HTTPException(status_code=404, detail=f"Legacy task not found: {legacy_id}") + + bus = await get_event_bus() + svc = TaskService(db, bus) + await svc.add_progress(task.task_id, body.agent, body.content) + return {"message": "ok"} + + +@router.put("/by-legacy/{legacy_id}/todos") +async def legacy_todos( + legacy_id: str, + body: LegacyTodoUpdate, + db: AsyncSession = Depends(get_db), +): + task = await _find_by_legacy_id(db, legacy_id) + if not task: + raise HTTPException(status_code=404, detail=f"Legacy task not found: {legacy_id}") + + bus = await get_event_bus() + svc = TaskService(db, bus) + await svc.update_todos(task.task_id, body.todos) + return {"message": "ok"} + + +@router.get("/by-legacy/{legacy_id}") +async def legacy_get( + legacy_id: str, + db: AsyncSession = Depends(get_db), +): + task = await _find_by_legacy_id(db, legacy_id) + if not task: + raise HTTPException(status_code=404, detail=f"Legacy task not found: {legacy_id}") + return task.to_dict() + + + +"""Tasks API — 任务的 CRUD 和状态流转。""" + +import uuid +import logging +from datetime import datetime + +from fastapi import APIRouter, Depends, HTTPException, Query +from pydantic import BaseModel, Field +from sqlalchemy.ext.asyncio import AsyncSession + +from ..db import get_db +from ..models.task import TaskState +from ..services.event_bus import EventBus, get_event_bus +from ..services.task_service import TaskService + +log = logging.getLogger("edict.api.tasks") +router = APIRouter() + + +# ── Schemas ── + +class TaskCreate(BaseModel): + title: str + description: str = "" + priority: str = "中" + assignee_org: str | None = None + creator: str = "emperor" + tags: list[str] = [] + meta: dict | None = None + + +class TaskTransition(BaseModel): + new_state: str + agent: str = "system" + reason: str = "" + + +class TaskProgress(BaseModel): + agent: str + content: str + + +class TaskTodoUpdate(BaseModel): + todos: list[dict] + + +class TaskSchedulerUpdate(BaseModel): + scheduler: dict + + +class TaskOut(BaseModel): + task_id: str + trace_id: str + title: str + description: str + priority: str + state: str + assignee_org: str | None + creator: str + tags: list[str] + flow_log: list + progress_log: list + todos: list + scheduler: dict | None + created_at: str + updated_at: str + + class Config: + from_attributes = True + + +# ── 依赖注入 helper ── + +async def get_task_service( + db: AsyncSession = Depends(get_db), +) -> TaskService: + bus = await get_event_bus() + return TaskService(db, bus) + + +# ── Endpoints ── + +@router.get("") +async def list_tasks( + state: str | None = None, + assignee_org: str | None = None, + priority: str | None = None, + limit: int = Query(default=50, le=200), + offset: int = Query(default=0, ge=0), + svc: TaskService = Depends(get_task_service), +): + """获取任务列表。""" + task_state = TaskState(state) if state else None + tasks = await svc.list_tasks( + state=task_state, + assignee_org=assignee_org, + priority=priority, + limit=limit, + offset=offset, + ) + return {"tasks": [t.to_dict() for t in tasks], "count": len(tasks)} + + +@router.get("/live-status") +async def live_status(svc: TaskService = Depends(get_task_service)): + """兼容旧 live_status.json 格式的全局状态。""" + return await svc.get_live_status() + + +@router.get("/stats") +async def task_stats(svc: TaskService = Depends(get_task_service)): + """任务统计。""" + stats = {} + for s in TaskState: + stats[s.value] = await svc.count_tasks(s) + total = sum(stats.values()) + return {"total": total, "by_state": stats} + + +@router.post("", status_code=201) +async def create_task( + body: TaskCreate, + svc: TaskService = Depends(get_task_service), +): + """创建新任务。""" + task = await svc.create_task( + title=body.title, + description=body.description, + priority=body.priority, + assignee_org=body.assignee_org, + creator=body.creator, + tags=body.tags, + meta=body.meta, + ) + return {"task_id": str(task.task_id), "trace_id": str(task.trace_id), "state": task.state.value} + + +@router.get("/{task_id}") +async def get_task( + task_id: uuid.UUID, + svc: TaskService = Depends(get_task_service), +): + """获取任务详情。""" + try: + task = await svc.get_task(task_id) + return task.to_dict() + except ValueError: + raise HTTPException(status_code=404, detail="Task not found") + + +@router.post("/{task_id}/transition") +async def transition_task( + task_id: uuid.UUID, + body: TaskTransition, + svc: TaskService = Depends(get_task_service), +): + """执行状态流转。""" + try: + new_state = TaskState(body.new_state) + except ValueError: + raise HTTPException(status_code=400, detail=f"Invalid state: {body.new_state}") + + try: + task = await svc.transition_state( + task_id=task_id, + new_state=new_state, + agent=body.agent, + reason=body.reason, + ) + return {"task_id": str(task.task_id), "state": task.state.value, "message": "ok"} + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + +@router.post("/{task_id}/dispatch") +async def dispatch_task( + task_id: uuid.UUID, + agent: str = Query(description="目标 agent"), + message: str = Query(default="", description="派发消息"), + svc: TaskService = Depends(get_task_service), +): + """手动派发任务给指定 agent。""" + try: + await svc.request_dispatch(task_id, agent, message) + return {"message": "dispatch requested", "agent": agent} + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) + + +@router.post("/{task_id}/progress") +async def add_progress( + task_id: uuid.UUID, + body: TaskProgress, + svc: TaskService = Depends(get_task_service), +): + """添加进度记录。""" + try: + await svc.add_progress(task_id, body.agent, body.content) + return {"message": "ok"} + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) + + +@router.put("/{task_id}/todos") +async def update_todos( + task_id: uuid.UUID, + body: TaskTodoUpdate, + svc: TaskService = Depends(get_task_service), +): + """更新任务 TODO 清单。""" + try: + await svc.update_todos(task_id, body.todos) + return {"message": "ok"} + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) + + +@router.put("/{task_id}/scheduler") +async def update_scheduler( + task_id: uuid.UUID, + body: TaskSchedulerUpdate, + svc: TaskService = Depends(get_task_service), +): + """更新任务排期信息。""" + try: + await svc.update_scheduler(task_id, body.scheduler) + return {"message": "ok"} + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) + + + +"""WebSocket 端点 — 实时推送事件到前端。 + +取代旧架构的 5 秒 HTTP 轮询,改为: +- 客户端 WebSocket 连接 +- 服务端订阅 Redis Pub/Sub 频道 +- 实时推送事件(状态变更、Agent 思考流、心跳等) +""" + +import asyncio +import json +import logging + +import redis.asyncio as aioredis +from fastapi import APIRouter, WebSocket, WebSocketDisconnect + +from ..config import get_settings +from ..services.event_bus import get_event_bus + +log = logging.getLogger("edict.ws") +router = APIRouter() + +# 活跃连接管理 +_connections: set[WebSocket] = set() + + +@router.websocket("/ws") +async def websocket_endpoint(ws: WebSocket): + """主 WebSocket 端点 — 推送所有事件。""" + await ws.accept() + _connections.add(ws) + log.info(f"WebSocket connected. Total: {len(_connections)}") + + # 创建独立的 Redis Pub/Sub 连接 + settings = get_settings() + pubsub_redis = aioredis.from_url(settings.redis_url, decode_responses=True) + pubsub = pubsub_redis.pubsub() + + # 订阅所有 edict 频道 + await pubsub.psubscribe("edict:pubsub:*") + + try: + # 并发:监听 Redis Pub/Sub + 客户端消息 + await asyncio.gather( + _relay_events(pubsub, ws), + _handle_client_messages(ws), + ) + except WebSocketDisconnect: + log.info("WebSocket disconnected") + except Exception as e: + log.error(f"WebSocket error: {e}") + finally: + _connections.discard(ws) + await pubsub.punsubscribe("edict:pubsub:*") + await pubsub_redis.aclose() + log.info(f"WebSocket cleaned up. Remaining: {len(_connections)}") + + +async def _relay_events(pubsub, ws: WebSocket): + """从 Redis Pub/Sub 接收事件,推送到 WebSocket。""" + async for message in pubsub.listen(): + if message["type"] == "pmessage": + channel = message["channel"] + data = message["data"] + + # 提取 topic 名 + topic = channel.replace("edict:pubsub:", "") if channel.startswith("edict:pubsub:") else channel + + try: + event_data = json.loads(data) if isinstance(data, str) else data + await ws.send_json({ + "type": "event", + "topic": topic, + "data": event_data, + }) + except Exception as e: + log.warning(f"Failed to relay event: {e}") + break + + +async def _handle_client_messages(ws: WebSocket): + """处理客户端发送的消息(心跳、订阅过滤等)。""" + while True: + try: + data = await ws.receive_json() + msg_type = data.get("type", "") + + if msg_type == "ping": + await ws.send_json({"type": "pong"}) + elif msg_type == "subscribe": + # 前端可请求只订阅特定 topic(未来扩展) + topics = data.get("topics", []) + log.debug(f"Client subscribe request: {topics}") + await ws.send_json({"type": "subscribed", "topics": topics}) + else: + log.debug(f"Unknown client message: {msg_type}") + + except WebSocketDisconnect: + raise + except Exception: + break + + +@router.websocket("/ws/task/{task_id}") +async def task_websocket(ws: WebSocket, task_id: str): + """单任务 WebSocket — 只推送与特定任务相关的事件。""" + await ws.accept() + _connections.add(ws) + + settings = get_settings() + pubsub_redis = aioredis.from_url(settings.redis_url, decode_responses=True) + pubsub = pubsub_redis.pubsub() + await pubsub.psubscribe("edict:pubsub:*") + + try: + async for message in pubsub.listen(): + if message["type"] == "pmessage": + data = message["data"] + try: + event_data = json.loads(data) if isinstance(data, str) else data + payload = event_data.get("payload", {}) + if isinstance(payload, str): + payload = json.loads(payload) + + # 只转发与此任务相关的事件 + if payload.get("task_id") == task_id: + topic = message["channel"].replace("edict:pubsub:", "") + await ws.send_json({ + "type": "event", + "topic": topic, + "data": event_data, + }) + except Exception: + continue + except WebSocketDisconnect: + pass + finally: + _connections.discard(ws) + await pubsub.punsubscribe("edict:pubsub:*") + await pubsub_redis.aclose() + + +async def broadcast(event: dict): + """向所有连接的 WebSocket 客户端广播事件(服务端内部调用用)。""" + dead = set() + for ws in _connections: + try: + await ws.send_json(event) + except Exception: + dead.add(ws) + _connections -= dead + + + +"""Edict 数据模型包。""" + +from .task import Task, TaskState +from .event import Event +from .thought import Thought +from .todo import Todo + +__all__ = ["Task", "TaskState", "Event", "Thought", "Todo"] + + + +"""Event 模型 — 事件持久化表,支持回放和审计。 + +每个事件对应一次系统行为:任务创建、状态变更、Agent 思考、Todo 更新等。 +遵循 Edict Architecture §3 事件结构规范。 +""" + +import uuid +from datetime import datetime, timezone + +from sqlalchemy import Column, DateTime, Index, String, Text +from sqlalchemy.dialects.postgresql import JSONB, UUID + +from ..db import Base + + +class Event(Base): + """事件表 — 所有系统事件的持久化记录。""" + __tablename__ = "events" + + event_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + trace_id = Column(String(32), nullable=False, index=True, comment="关联任务ID") + timestamp = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False) + + # 事件分类 + topic = Column(String(128), nullable=False, index=True, comment="事件主题, e.g. task.created") + event_type = Column(String(128), nullable=False, comment="事件类型, e.g. state.changed") + producer = Column(String(128), nullable=False, comment="事件生产者, e.g. orchestrator:v1") + + # 事件数据 + payload = Column(JSONB, default=dict, comment="事件负载") + meta = Column(JSONB, default=dict, comment="元数据 {priority, model, version}") + + __table_args__ = ( + Index("ix_events_trace_topic", "trace_id", "topic"), + Index("ix_events_timestamp", "timestamp"), + ) + + def to_dict(self) -> dict: + return { + "event_id": str(self.event_id), + "trace_id": self.trace_id, + "timestamp": self.timestamp.isoformat() if self.timestamp else "", + "topic": self.topic, + "event_type": self.event_type, + "producer": self.producer, + "payload": self.payload or {}, + "meta": self.meta or {}, + } + + + +"""Task 模型 — 三省六部任务核心表。 + +对应当前 tasks_source.json 中的每一条任务记录。 +state 对应三省六部流转状态机: + Taizi → Zhongshu → Menxia → Assigned → Doing → Review → Done +""" + +import enum +import uuid +from datetime import datetime, timezone + +from sqlalchemy import ( + Column, + DateTime, + Enum, + Index, + String, + Text, + Boolean, + Integer, + text, +) +from sqlalchemy.dialects.postgresql import JSONB, UUID + +from ..db import Base + + +class TaskState(str, enum.Enum): + """任务状态枚举 — 映射三省六部流程。""" + Taizi = "Taizi" # 太子分拣 + Zhongshu = "Zhongshu" # 中书省起草 + Menxia = "Menxia" # 门下省审议 + Assigned = "Assigned" # 尚书省已将任务派发 + Next = "Next" # 待执行 + Doing = "Doing" # 六部执行中 + Review = "Review" # 审查汇总 + Done = "Done" # 完成 + Blocked = "Blocked" # 阻塞 + Cancelled = "Cancelled" # 取消 + Pending = "Pending" # 待处理 + + +# 终态集合 +TERMINAL_STATES = {TaskState.Done, TaskState.Cancelled} + +# 状态流转合法路径 +STATE_TRANSITIONS = { + TaskState.Taizi: {TaskState.Zhongshu, TaskState.Cancelled}, + TaskState.Zhongshu: {TaskState.Menxia, TaskState.Cancelled, TaskState.Blocked}, + TaskState.Menxia: {TaskState.Assigned, TaskState.Zhongshu, TaskState.Cancelled}, # 封驳退回中书 + TaskState.Assigned: {TaskState.Doing, TaskState.Next, TaskState.Cancelled, TaskState.Blocked}, + TaskState.Next: {TaskState.Doing, TaskState.Cancelled}, + TaskState.Doing: {TaskState.Review, TaskState.Done, TaskState.Blocked, TaskState.Cancelled}, + TaskState.Review: {TaskState.Done, TaskState.Doing, TaskState.Cancelled}, # 审查不通过退回 + TaskState.Blocked: {TaskState.Taizi, TaskState.Zhongshu, TaskState.Menxia, TaskState.Assigned, TaskState.Doing}, +} + +# 状态 → Agent 映射 +STATE_AGENT_MAP = { + TaskState.Taizi: "taizi", + TaskState.Zhongshu: "zhongshu", + TaskState.Menxia: "menxia", + TaskState.Assigned: "shangshu", + TaskState.Review: "shangshu", +} + +# 组织 → Agent 映射(六部) +ORG_AGENT_MAP = { + "户部": "hubu", + "礼部": "libu", + "兵部": "bingbu", + "刑部": "xingbu", + "工部": "gongbu", + "吏部": "libu_hr", +} + + +class Task(Base): + """三省六部任务表。""" + __tablename__ = "tasks" + + id = Column(String(32), primary_key=True, comment="任务ID, e.g. JJC-20260301-001") + title = Column(Text, nullable=False, comment="任务标题") + state = Column(Enum(TaskState, name="task_state"), nullable=False, default=TaskState.Taizi, index=True) + org = Column(String(32), nullable=False, default="太子", comment="当前执行部门") + official = Column(String(32), default="", comment="责任官员") + now = Column(Text, default="", comment="当前进展描述") + eta = Column(String(64), default="-", comment="预计完成时间") + block = Column(Text, default="无", comment="阻塞原因") + output = Column(Text, default="", comment="最终产出") + priority = Column(String(16), default="normal", comment="优先级") + archived = Column(Boolean, default=False, index=True) + + # JSONB 灵活字段 + flow_log = Column(JSONB, default=list, comment="流转日志 [{at, from, to, remark}]") + progress_log = Column(JSONB, default=list, comment="进展日志 [{at, agent, text, todos}]") + todos = Column(JSONB, default=list, comment="子任务 [{id, title, status, detail}]") + scheduler = Column(JSONB, default=dict, comment="调度器元数据") + template_id = Column(String(64), default="", comment="模板ID") + template_params = Column(JSONB, default=dict, comment="模板参数") + ac = Column(Text, default="", comment="验收标准") + target_dept = Column(String(64), default="", comment="目标部门") + + # 时间戳 + created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False) + updated_at = Column( + DateTime(timezone=True), + default=lambda: datetime.now(timezone.utc), + onupdate=lambda: datetime.now(timezone.utc), + nullable=False, + ) + + __table_args__ = ( + Index("ix_tasks_state_archived", "state", "archived"), + Index("ix_tasks_updated_at", "updated_at"), + ) + + def to_dict(self) -> dict: + """序列化为 API 响应格式(兼容旧 live_status 格式)。""" + return { + "id": self.id, + "title": self.title, + "state": self.state.value if self.state else "", + "org": self.org, + "official": self.official, + "now": self.now, + "eta": self.eta, + "block": self.block, + "output": self.output, + "priority": self.priority, + "archived": self.archived, + "flow_log": self.flow_log or [], + "progress_log": self.progress_log or [], + "todos": self.todos or [], + "templateId": self.template_id, + "templateParams": self.template_params or {}, + "ac": self.ac, + "targetDept": self.target_dept, + "_scheduler": self.scheduler or {}, + "createdAt": self.created_at.isoformat() if self.created_at else "", + "updatedAt": self.updated_at.isoformat() if self.updated_at else "", + } + + + +"""Thought 模型 — Agent 思考流持久化。 + +遵循 Edict Architecture §4 Thought JSON Schema。 +支持 streaming partial thoughts 和 dashboard 实时展示。 +""" + +import uuid +from datetime import datetime, timezone + +from sqlalchemy import Column, DateTime, Float, Index, Integer, String, Text, Boolean +from sqlalchemy.dialects.postgresql import UUID + +from ..db import Base + + +class Thought(Base): + """Agent 思考记录。""" + __tablename__ = "thoughts" + + thought_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + trace_id = Column(String(32), nullable=False, index=True, comment="关联任务ID") + agent = Column(String(32), nullable=False, index=True, comment="Agent 标识") + step = Column(Integer, nullable=False, default=0, comment="思考步骤序号") + type = Column( + String(32), + nullable=False, + default="reasoning", + comment="思考类型: reasoning|query|action_intent|summary", + ) + source = Column(String(16), default="llm", comment="来源: llm|tool|human") + content = Column(Text, nullable=False, default="", comment="思考内容") + tokens = Column(Integer, default=0, comment="消耗 token 数") + confidence = Column(Float, default=0.0, comment="置信度 0-1") + sensitive = Column(Boolean, default=False, comment="是否敏感内容") + timestamp = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False) + + __table_args__ = ( + Index("ix_thoughts_trace_agent", "trace_id", "agent"), + Index("ix_thoughts_timestamp", "timestamp"), + ) + + def to_dict(self) -> dict: + return { + "thought_id": str(self.thought_id), + "trace_id": self.trace_id, + "agent": self.agent, + "step": self.step, + "type": self.type, + "source": self.source, + "content": self.content, + "tokens": self.tokens, + "confidence": self.confidence, + "sensitive": self.sensitive, + "timestamp": self.timestamp.isoformat() if self.timestamp else "", + } + + + +"""Todo 模型 — 结构化子任务。 + +遵循 Edict Architecture §4 Todo JSON Schema。 +支持层级结构(parent_id)和 checkpoint 跟踪。 +""" + +import uuid +from datetime import datetime, timezone + +from sqlalchemy import Column, DateTime, Float, Index, String, Text +from sqlalchemy.dialects.postgresql import JSONB, UUID + +from ..db import Base + + +class Todo(Base): + """结构化子任务表。""" + __tablename__ = "todos" + + todo_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + trace_id = Column(String(32), nullable=False, index=True, comment="关联任务ID") + parent_id = Column(UUID(as_uuid=True), nullable=True, comment="父级 todo_id(树状结构)") + + title = Column(String(256), nullable=False, comment="子任务标题") + description = Column(Text, default="", comment="详细描述") + owner = Column(String(64), default="", comment="负责部门") + assignee_agent = Column(String(32), default="", comment="执行 Agent") + + status = Column(String(32), nullable=False, default="open", index=True, + comment="状态: open|in_progress|done|cancelled") + priority = Column(String(16), default="normal", comment="优先级: low|normal|high|urgent") + estimated_cost = Column(Float, default=0.0, comment="预估 token 耗费") + + created_by = Column(String(64), default="", comment="创建者") + checkpoints = Column(JSONB, default=list, comment="检查点 [{name, status}]") + metadata_ = Column("metadata", JSONB, default=dict, comment="扩展元数据") + + created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False) + updated_at = Column( + DateTime(timezone=True), + default=lambda: datetime.now(timezone.utc), + onupdate=lambda: datetime.now(timezone.utc), + nullable=False, + ) + + __table_args__ = ( + Index("ix_todos_trace_status", "trace_id", "status"), + ) + + def to_dict(self) -> dict: + return { + "todo_id": str(self.todo_id), + "trace_id": self.trace_id, + "parent_id": str(self.parent_id) if self.parent_id else None, + "title": self.title, + "description": self.description, + "owner": self.owner, + "assignee_agent": self.assignee_agent, + "status": self.status, + "priority": self.priority, + "estimated_cost": self.estimated_cost, + "created_by": self.created_by, + "checkpoints": self.checkpoints or [], + "metadata": self.metadata_ or {}, + "created_at": self.created_at.isoformat() if self.created_at else "", + "updated_at": self.updated_at.isoformat() if self.updated_at else "", + } + + + +from .event_bus import EventBus, get_event_bus +from .task_service import TaskService + +__all__ = ["EventBus", "get_event_bus", "TaskService"] + + + +"""Redis Streams 事件总线 — 可靠的事件发布/消费。 + +核心能力: +- publish: XADD 发布事件到 stream +- subscribe: XREADGROUP 消费者组消费,带 ACK 保证 +- 未 ACK 的事件在消费者崩溃后会被自动重新投递 +- 解决旧架构 daemon 线程丢失导致派发永久中断的根因 +""" + +import json +import logging +import uuid +from datetime import datetime, timezone +from typing import Any + +import redis.asyncio as aioredis + +from ..config import get_settings + +log = logging.getLogger("edict.event_bus") + +# ── 标准 Topic 常量 ── +TOPIC_TASK_CREATED = "task.created" +TOPIC_TASK_PLANNING_REQUEST = "task.planning.request" +TOPIC_TASK_PLANNING_COMPLETE = "task.planning.complete" +TOPIC_TASK_REVIEW_REQUEST = "task.review.request" +TOPIC_TASK_REVIEW_RESULT = "task.review.result" +TOPIC_TASK_DISPATCH = "task.dispatch" +TOPIC_TASK_STATUS = "task.status" +TOPIC_TASK_COMPLETED = "task.completed" +TOPIC_TASK_CLOSED = "task.closed" +TOPIC_TASK_REPLAN = "task.replan" +TOPIC_TASK_STALLED = "task.stalled" +TOPIC_TASK_ESCALATED = "task.escalated" + +TOPIC_AGENT_THOUGHTS = "agent.thoughts" +TOPIC_AGENT_TODO_UPDATE = "agent.todo.update" +TOPIC_AGENT_HEARTBEAT = "agent.heartbeat" + +# 所有 topic 对应的 Redis Stream key 前缀 +STREAM_PREFIX = "edict:stream:" + + +class EventBus: + """Redis Streams 事件总线。""" + + def __init__(self, redis_url: str | None = None): + self._redis_url = redis_url or get_settings().redis_url + self._redis: aioredis.Redis | None = None + + async def connect(self): + """建立 Redis 连接。""" + if self._redis is None: + self._redis = aioredis.from_url( + self._redis_url, + decode_responses=True, + max_connections=20, + ) + log.info(f"EventBus connected to Redis: {self._redis_url}") + + async def close(self): + if self._redis: + await self._redis.aclose() + self._redis = None + + @property + def redis(self) -> aioredis.Redis: + assert self._redis is not None, "EventBus not connected. Call connect() first." + return self._redis + + def _stream_key(self, topic: str) -> str: + return f"{STREAM_PREFIX}{topic}" + + async def publish( + self, + topic: str, + trace_id: str, + event_type: str, + producer: str, + payload: dict[str, Any] | None = None, + meta: dict[str, Any] | None = None, + ) -> str: + """发布事件到 Redis Stream。 + + Returns: + event_id (str): 由 Redis 自动生成的 Stream entry ID + """ + event = { + "event_id": str(uuid.uuid4()), + "trace_id": trace_id, + "timestamp": datetime.now(timezone.utc).isoformat(), + "topic": topic, + "event_type": event_type, + "producer": producer, + "payload": json.dumps(payload or {}, ensure_ascii=False), + "meta": json.dumps(meta or {}, ensure_ascii=False), + } + stream_key = self._stream_key(topic) + entry_id = await self.redis.xadd(stream_key, event, maxlen=10000) + log.debug(f"📤 Published {topic}/{event_type} → {stream_key} [{entry_id}] trace={trace_id}") + + # 同时发布到 Pub/Sub 频道(供 WebSocket 实时推送) + await self.redis.publish(f"edict:pubsub:{topic}", json.dumps(event, ensure_ascii=False)) + + return entry_id + + async def ensure_consumer_group(self, topic: str, group: str): + """确保消费者组存在(幂等)。""" + stream_key = self._stream_key(topic) + try: + await self.redis.xgroup_create(stream_key, group, id="0", mkstream=True) + log.info(f"Created consumer group {group} on {stream_key}") + except aioredis.ResponseError as e: + if "BUSYGROUP" not in str(e): + raise + + async def consume( + self, + topic: str, + group: str, + consumer: str, + count: int = 10, + block_ms: int = 5000, + ) -> list[tuple[str, dict]]: + """从消费者组消费事件。 + + Returns: + list of (entry_id, event_dict) + """ + stream_key = self._stream_key(topic) + results = await self.redis.xreadgroup( + groupname=group, + consumername=consumer, + streams={stream_key: ">"}, + count=count, + block=block_ms, + ) + events = [] + if results: + for _stream, messages in results: + for entry_id, data in messages: + # 反序列化 JSON 字段 + if "payload" in data: + data["payload"] = json.loads(data["payload"]) + if "meta" in data: + data["meta"] = json.loads(data["meta"]) + events.append((entry_id, data)) + return events + + async def ack(self, topic: str, group: str, entry_id: str): + """确认消费 — ACK 后事件不会被重新投递。""" + stream_key = self._stream_key(topic) + await self.redis.xack(stream_key, group, entry_id) + log.debug(f"✅ ACK {stream_key} [{entry_id}] group={group}") + + async def get_pending(self, topic: str, group: str, count: int = 10) -> list: + """查看未 ACK 的 pending 事件(用于诊断和恢复)。""" + stream_key = self._stream_key(topic) + return await self.redis.xpending_range(stream_key, group, min="-", max="+", count=count) + + async def claim_stale( + self, + topic: str, + group: str, + consumer: str, + min_idle_ms: int = 60000, + count: int = 10, + ) -> list[tuple[str, dict]]: + """认领超时的 pending 事件(消费者崩溃恢复)。""" + stream_key = self._stream_key(topic) + results = await self.redis.xautoclaim( + stream_key, group, consumer, min_idle_time=min_idle_ms, start_id="0-0", count=count + ) + # xautoclaim returns (next_id, [(id, data), ...], [deleted_ids]) + if results and len(results) >= 2: + events = [] + for entry_id, data in results[1]: + if "payload" in data: + data["payload"] = json.loads(data["payload"]) + if "meta" in data: + data["meta"] = json.loads(data["meta"]) + events.append((entry_id, data)) + return events + return [] + + async def stream_info(self, topic: str) -> dict: + """获取 Stream 信息(长度、消费者组等)。""" + stream_key = self._stream_key(topic) + try: + info = await self.redis.xinfo_stream(stream_key) + return info + except aioredis.ResponseError: + return {} + + +# ── 全局单例 ── +_bus: EventBus | None = None + + +async def get_event_bus() -> EventBus: + global _bus + if _bus is None: + _bus = EventBus() + await _bus.connect() + return _bus + + + +"""任务服务层 — CRUD + 状态机逻辑。 + +所有业务规则集中在此: +- 创建任务 → 发布 task.created 事件 +- 状态流转 → 校验合法性 + 发布状态事件 +- 查询、过滤、聚合 +""" + +import logging +import uuid +from datetime import datetime, timezone +from typing import Any + +from sqlalchemy import select, func, and_ +from sqlalchemy.ext.asyncio import AsyncSession + +from ..models.task import Task, TaskState, STATE_TRANSITIONS, TERMINAL_STATES +from .event_bus import ( + EventBus, + TOPIC_TASK_CREATED, + TOPIC_TASK_STATUS, + TOPIC_TASK_COMPLETED, + TOPIC_TASK_DISPATCH, +) + +log = logging.getLogger("edict.task_service") + + +class TaskService: + def __init__(self, db: AsyncSession, event_bus: EventBus): + self.db = db + self.bus = event_bus + + # ── 创建 ── + + async def create_task( + self, + title: str, + description: str = "", + priority: str = "中", + assignee_org: str | None = None, + creator: str = "emperor", + tags: list[str] | None = None, + initial_state: TaskState = TaskState.TAIZI, + meta: dict | None = None, + ) -> Task: + """创建任务并发布 task.created 事件。""" + now = datetime.now(timezone.utc) + trace_id = str(uuid.uuid4()) + + task = Task( + trace_id=trace_id, + title=title, + description=description, + priority=priority, + state=initial_state, + assignee_org=assignee_org, + creator=creator, + tags=tags or [], + flow_log=[ + { + "from": None, + "to": initial_state.value, + "agent": "system", + "reason": "任务创建", + "ts": now.isoformat(), + } + ], + progress_log=[], + todos=[], + scheduler=None, + meta=meta or {}, + ) + self.db.add(task) + await self.db.flush() + + # 发布事件 + await self.bus.publish( + topic=TOPIC_TASK_CREATED, + trace_id=trace_id, + event_type="task.created", + producer="task_service", + payload={ + "task_id": str(task.task_id), + "title": title, + "state": initial_state.value, + "priority": priority, + "assignee_org": assignee_org, + }, + ) + + await self.db.commit() + log.info(f"Created task {task.task_id}: {title} [{initial_state.value}]") + return task + + # ── 状态流转 ── + + async def transition_state( + self, + task_id: uuid.UUID, + new_state: TaskState, + agent: str = "system", + reason: str = "", + ) -> Task: + """执行状态流转,校验合法性。""" + task = await self._get_task(task_id) + old_state = task.state + + # 校验合法流转 + allowed = STATE_TRANSITIONS.get(old_state, set()) + if new_state not in allowed: + raise ValueError( + f"Invalid transition: {old_state.value} → {new_state.value}. " + f"Allowed: {[s.value for s in allowed]}" + ) + + task.state = new_state + task.updated_at = datetime.now(timezone.utc) + + # 记入 flow_log + flow_entry = { + "from": old_state.value, + "to": new_state.value, + "agent": agent, + "reason": reason, + "ts": datetime.now(timezone.utc).isoformat(), + } + if task.flow_log is None: + task.flow_log = [] + task.flow_log = [*task.flow_log, flow_entry] + + # 发布状态变更事件 + topic = TOPIC_TASK_COMPLETED if new_state in TERMINAL_STATES else TOPIC_TASK_STATUS + await self.bus.publish( + topic=topic, + trace_id=str(task.trace_id), + event_type=f"task.state.{new_state.value}", + producer=agent, + payload={ + "task_id": str(task_id), + "from": old_state.value, + "to": new_state.value, + "reason": reason, + }, + ) + + await self.db.commit() + log.info(f"Task {task_id} state: {old_state.value} → {new_state.value} by {agent}") + return task + + # ── 派发请求 ── + + async def request_dispatch( + self, + task_id: uuid.UUID, + target_agent: str, + message: str = "", + ): + """发布 task.dispatch 事件,由 DispatchWorker 消费执行。""" + task = await self._get_task(task_id) + await self.bus.publish( + topic=TOPIC_TASK_DISPATCH, + trace_id=str(task.trace_id), + event_type="task.dispatch.request", + producer="task_service", + payload={ + "task_id": str(task_id), + "agent": target_agent, + "message": message, + "state": task.state.value, + }, + ) + log.info(f"Dispatch requested: task {task_id} → agent {target_agent}") + + # ── 进度/备注更新 ── + + async def add_progress( + self, + task_id: uuid.UUID, + agent: str, + content: str, + ) -> Task: + task = await self._get_task(task_id) + entry = { + "agent": agent, + "content": content, + "ts": datetime.now(timezone.utc).isoformat(), + } + if task.progress_log is None: + task.progress_log = [] + task.progress_log = [*task.progress_log, entry] + task.updated_at = datetime.now(timezone.utc) + await self.db.commit() + return task + + async def update_todos( + self, + task_id: uuid.UUID, + todos: list[dict], + ) -> Task: + task = await self._get_task(task_id) + task.todos = todos + task.updated_at = datetime.now(timezone.utc) + await self.db.commit() + return task + + async def update_scheduler( + self, + task_id: uuid.UUID, + scheduler: dict, + ) -> Task: + task = await self._get_task(task_id) + task.scheduler = scheduler + task.updated_at = datetime.now(timezone.utc) + await self.db.commit() + return task + + # ── 查询 ── + + async def get_task(self, task_id: uuid.UUID) -> Task: + return await self._get_task(task_id) + + async def list_tasks( + self, + state: TaskState | None = None, + assignee_org: str | None = None, + priority: str | None = None, + limit: int = 50, + offset: int = 0, + ) -> list[Task]: + stmt = select(Task) + conditions = [] + if state is not None: + conditions.append(Task.state == state) + if assignee_org is not None: + conditions.append(Task.assignee_org == assignee_org) + if priority is not None: + conditions.append(Task.priority == priority) + if conditions: + stmt = stmt.where(and_(*conditions)) + stmt = stmt.order_by(Task.created_at.desc()).limit(limit).offset(offset) + result = await self.db.execute(stmt) + return list(result.scalars().all()) + + async def get_live_status(self) -> dict[str, Any]: + """生成兼容旧 live_status.json 格式的全局状态。""" + tasks = await self.list_tasks(limit=200) + active_tasks = {} + completed_tasks = {} + for t in tasks: + d = t.to_dict() + if t.state in TERMINAL_STATES: + completed_tasks[str(t.task_id)] = d + else: + active_tasks[str(t.task_id)] = d + return { + "tasks": active_tasks, + "completed_tasks": completed_tasks, + "last_updated": datetime.now(timezone.utc).isoformat(), + } + + async def count_tasks(self, state: TaskState | None = None) -> int: + stmt = select(func.count(Task.task_id)) + if state is not None: + stmt = stmt.where(Task.state == state) + result = await self.db.execute(stmt) + return result.scalar_one() + + # ── 内部 ── + + async def _get_task(self, task_id: uuid.UUID) -> Task: + task = await self.db.get(Task, task_id) + if task is None: + raise ValueError(f"Task not found: {task_id}") + return task + + + +from .orchestrator_worker import OrchestratorWorker, run_orchestrator +from .dispatch_worker import DispatchWorker, run_dispatcher + +__all__ = [ + "OrchestratorWorker", + "run_orchestrator", + "DispatchWorker", + "run_dispatcher", +] + + + +"""Dispatch Worker — 消费 task.dispatch 事件,执行 OpenClaw agent 调用。 + +核心解决旧架构痛点: +- 旧: daemon 线程 + subprocess.run → kill -9 丢失一切 +- 新: Redis Streams ACK 保证 → 崩溃后自动重新投递 + +流程: +1. 从 task.dispatch stream 消费事件 +2. 调用 OpenClaw CLI: `openclaw agent --agent xxx -m "..."` +3. 解析 agent 输出(kanban_update.py 调用结果) +4. ACK 事件 +""" + +import asyncio +import logging +import os +import signal +import subprocess +import uuid +from datetime import datetime, timezone + +from ..config import get_settings +from ..services.event_bus import ( + EventBus, + TOPIC_TASK_DISPATCH, + TOPIC_TASK_STATUS, + TOPIC_AGENT_THOUGHTS, + TOPIC_AGENT_HEARTBEAT, +) + +log = logging.getLogger("edict.dispatcher") + +GROUP = "dispatcher" +CONSUMER = "disp-1" + + +class DispatchWorker: + """Agent 派发 Worker — 调用 OpenClaw CLI 执行 agent 任务。""" + + def __init__(self, max_concurrent: int = 3): + self.bus = EventBus() + self._running = False + self._semaphore = asyncio.Semaphore(max_concurrent) + self._active_tasks: dict[str, asyncio.Task] = {} + + async def start(self): + await self.bus.connect() + await self.bus.ensure_consumer_group(TOPIC_TASK_DISPATCH, GROUP) + self._running = True + log.info("🚀 Dispatch worker started") + + # 恢复崩溃遗留 + await self._recover_pending() + + while self._running: + try: + await self._poll_cycle() + except Exception as e: + log.error(f"Dispatch poll error: {e}", exc_info=True) + await asyncio.sleep(2) + + async def stop(self): + self._running = False + # 等待进行中的 agent 调用完成 + if self._active_tasks: + log.info(f"Waiting for {len(self._active_tasks)} active dispatches...") + await asyncio.gather(*self._active_tasks.values(), return_exceptions=True) + await self.bus.close() + log.info("Dispatch worker stopped") + + async def _recover_pending(self): + events = await self.bus.claim_stale( + TOPIC_TASK_DISPATCH, GROUP, CONSUMER, min_idle_ms=60000, count=20 + ) + if events: + log.info(f"Recovering {len(events)} stale dispatch events") + for entry_id, event in events: + await self._dispatch(entry_id, event) + + async def _poll_cycle(self): + events = await self.bus.consume( + TOPIC_TASK_DISPATCH, GROUP, CONSUMER, count=3, block_ms=2000 + ) + for entry_id, event in events: + # 每个派发在独立任务中执行,带并发控制 + task = asyncio.create_task(self._dispatch(entry_id, event)) + task_id = event.get("payload", {}).get("task_id", entry_id) + self._active_tasks[task_id] = task + task.add_done_callback(lambda t, tid=task_id: self._active_tasks.pop(tid, None)) + + async def _dispatch(self, entry_id: str, event: dict): + """执行一次 agent 派发。""" + async with self._semaphore: + payload = event.get("payload", {}) + task_id = payload.get("task_id", "") + agent = payload.get("agent", "") + message = payload.get("message", "") + trace_id = event.get("trace_id", "") + state = payload.get("state", "") + + log.info(f"🔄 Dispatching task {task_id} → agent '{agent}' state={state}") + + # 发布心跳 + await self.bus.publish( + topic=TOPIC_AGENT_HEARTBEAT, + trace_id=trace_id, + event_type="agent.dispatch.start", + producer="dispatcher", + payload={"task_id": task_id, "agent": agent}, + ) + + try: + result = await self._call_openclaw(agent, message, task_id, trace_id) + + # 发布 agent 输出 + await self.bus.publish( + topic=TOPIC_AGENT_THOUGHTS, + trace_id=trace_id, + event_type="agent.output", + producer=f"agent.{agent}", + payload={ + "task_id": task_id, + "agent": agent, + "output": result.get("stdout", ""), + "return_code": result.get("returncode", -1), + }, + ) + + if result.get("returncode") == 0: + log.info(f"✅ Agent '{agent}' completed task {task_id}") + else: + log.warning( + f"⚠️ Agent '{agent}' returned non-zero for task {task_id}: " + f"rc={result.get('returncode')}" + ) + + # ACK — 事件处理完毕 + await self.bus.ack(TOPIC_TASK_DISPATCH, GROUP, entry_id) + + except Exception as e: + log.error(f"❌ Dispatch failed: task {task_id} → {agent}: {e}", exc_info=True) + # 不 ACK → Redis 会重新投递给其他消费者 + + async def _call_openclaw( + self, + agent: str, + message: str, + task_id: str, + trace_id: str, + ) -> dict: + """异步调用 OpenClaw CLI — 在线程池中执行。""" + settings = get_settings() + cmd = [ + "openclaw", "agent", + "--agent", agent, + "-m", message, + ] + + env = os.environ.copy() + env["EDICT_TASK_ID"] = task_id + env["EDICT_TRACE_ID"] = trace_id + env["EDICT_API_URL"] = f"http://localhost:{settings.port}" + + log.debug(f"Executing: {' '.join(cmd)}") + + def _run(): + try: + proc = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=300, + env=env, + cwd=settings.openclaw_project_dir or None, + ) + return { + "returncode": proc.returncode, + "stdout": proc.stdout[-5000:] if proc.stdout else "", + "stderr": proc.stderr[-2000:] if proc.stderr else "", + } + except subprocess.TimeoutExpired: + return {"returncode": -1, "stdout": "", "stderr": "TIMEOUT after 300s"} + except FileNotFoundError: + return {"returncode": -1, "stdout": "", "stderr": "openclaw command not found"} + + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, _run) + + +async def run_dispatcher(): + """入口函数 — 用于直接运行 worker。""" + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(name)s] %(levelname)s: %(message)s", + ) + worker = DispatchWorker() + + loop = asyncio.get_event_loop() + for sig in (signal.SIGTERM, signal.SIGINT): + loop.add_signal_handler(sig, lambda: asyncio.create_task(worker.stop())) + + await worker.start() + + + +"""Orchestrator Worker — 消费事件总线,驱动任务状态机。 + +监听 topic: +- task.created → 自动派发给太子 agent +- task.planning.complete → 中书审议完成 → 流转门下 +- task.review.result → 门下审核 → 通过则 Assigned,退回则 Replan +- task.status → 处理各种状态变更 +- task.stalled → 处理停滞任务 + +这是系统的核心编排器,取代旧架构中 daemon 线程 + 定时扫描的角色。 +得益于 Redis Streams ACK 机制:即使 worker 崩溃,未 ACK 的事件 +会被其他消费者自动认领,永不丢失。 +""" + +import asyncio +import logging +import signal +from contextlib import asynccontextmanager + +from ..config import get_settings +from ..db import async_session +from ..models.task import TaskState, STATE_AGENT_MAP, ORG_AGENT_MAP +from ..services.event_bus import ( + EventBus, + TOPIC_TASK_CREATED, + TOPIC_TASK_STATUS, + TOPIC_TASK_DISPATCH, + TOPIC_TASK_COMPLETED, + TOPIC_TASK_STALLED, +) +from ..services.task_service import TaskService + +log = logging.getLogger("edict.orchestrator") + +GROUP = "orchestrator" +CONSUMER = "orch-1" + +# 需要监听的 topics +WATCHED_TOPICS = [ + TOPIC_TASK_CREATED, + TOPIC_TASK_STATUS, + TOPIC_TASK_COMPLETED, + TOPIC_TASK_STALLED, +] + + +class OrchestratorWorker: + """事件驱动的编排器 Worker。""" + + def __init__(self): + self.bus = EventBus() + self._running = False + + async def start(self): + """启动 worker 主循环。""" + await self.bus.connect() + + # 确保所有消费者组 + for topic in WATCHED_TOPICS: + await self.bus.ensure_consumer_group(topic, GROUP) + + self._running = True + log.info("🏛️ Orchestrator worker started") + + # 先处理崩溃遗留的 pending 事件 + await self._recover_pending() + + while self._running: + try: + await self._poll_cycle() + except Exception as e: + log.error(f"Orchestrator poll error: {e}", exc_info=True) + await asyncio.sleep(2) + + async def stop(self): + self._running = False + await self.bus.close() + log.info("Orchestrator worker stopped") + + async def _recover_pending(self): + """恢复崩溃前未 ACK 的事件。""" + for topic in WATCHED_TOPICS: + events = await self.bus.claim_stale( + topic, GROUP, CONSUMER, min_idle_ms=30000, count=50 + ) + if events: + log.info(f"Recovering {len(events)} stale events from {topic}") + for entry_id, event in events: + await self._handle_event(topic, entry_id, event) + + async def _poll_cycle(self): + """一次轮询周期:从所有 topic 消费事件。""" + for topic in WATCHED_TOPICS: + events = await self.bus.consume( + topic, GROUP, CONSUMER, count=5, block_ms=1000 + ) + for entry_id, event in events: + try: + await self._handle_event(topic, entry_id, event) + await self.bus.ack(topic, GROUP, entry_id) + except Exception as e: + log.error( + f"Error handling event {entry_id} from {topic}: {e}", + exc_info=True, + ) + # 不 ACK → 事件会被重新投递 + + async def _handle_event(self, topic: str, entry_id: str, event: dict): + """根据 topic 和 event_type 分发处理。""" + event_type = event.get("event_type", "") + trace_id = event.get("trace_id", "") + payload = event.get("payload", {}) + + log.info(f"📨 {topic}/{event_type} trace={trace_id}") + + if topic == TOPIC_TASK_CREATED: + await self._on_task_created(payload, trace_id) + elif topic == TOPIC_TASK_STATUS: + await self._on_task_status(event_type, payload, trace_id) + elif topic == TOPIC_TASK_COMPLETED: + await self._on_task_completed(payload, trace_id) + elif topic == TOPIC_TASK_STALLED: + await self._on_task_stalled(payload, trace_id) + + async def _on_task_created(self, payload: dict, trace_id: str): + """任务创建 → 派发给太子 agent 起草。""" + task_id = payload.get("task_id") + state = payload.get("state", "taizi") + agent = STATE_AGENT_MAP.get(TaskState(state), "taizi") + + await self.bus.publish( + topic=TOPIC_TASK_DISPATCH, + trace_id=trace_id, + event_type="task.dispatch.request", + producer="orchestrator", + payload={ + "task_id": task_id, + "agent": agent, + "state": state, + "message": f"新任务已创建: {payload.get('title', '')}", + }, + ) + + async def _on_task_status(self, event_type: str, payload: dict, trace_id: str): + """状态变更 → 自动派发下一个 agent。""" + task_id = payload.get("task_id") + new_state_str = payload.get("to", "") + + try: + new_state = TaskState(new_state_str) + except ValueError: + log.warning(f"Unknown state: {new_state_str}") + return + + # 如果新状态有对应 agent,自动派发 + agent = STATE_AGENT_MAP.get(new_state) + + # 如果进入 assigned 状态,需要查找六部对应 agent + if new_state == TaskState.ASSIGNED: + # 从 payload 获取 assignee_org + org = payload.get("assignee_org", "") + agent = ORG_AGENT_MAP.get(org, agent) + + if agent: + await self.bus.publish( + topic=TOPIC_TASK_DISPATCH, + trace_id=trace_id, + event_type="task.dispatch.request", + producer="orchestrator", + payload={ + "task_id": task_id, + "agent": agent, + "state": new_state_str, + "message": f"任务已流转到 {new_state_str}", + }, + ) + + async def _on_task_completed(self, payload: dict, trace_id: str): + """任务完成 → 记录日志。""" + task_id = payload.get("task_id") + log.info(f"🎉 Task {task_id} completed. trace={trace_id}") + + async def _on_task_stalled(self, payload: dict, trace_id: str): + """任务停滞 → 通知尚书或重新派发。""" + task_id = payload.get("task_id") + log.warning(f"⏸️ Task {task_id} stalled! Requesting intervention. trace={trace_id}") + # TODO: 实现停滞任务的自动恢复策略 + + +async def run_orchestrator(): + """入口函数 — 用于直接运行 worker。""" + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(name)s] %(levelname)s: %(message)s", + ) + worker = OrchestratorWorker() + + loop = asyncio.get_event_loop() + for sig in (signal.SIGTERM, signal.SIGINT): + loop.add_signal_handler(sig, lambda: asyncio.create_task(worker.stop())) + + await worker.start() + + + +"""Edict Backend — 三省六部事件驱动架构。""" + + + +"""Edict 配置管理 — 从环境变量加载所有配置。""" + +from pydantic_settings import BaseSettings +from functools import lru_cache + + +class Settings(BaseSettings): + # ── Postgres ── + postgres_host: str = "localhost" + postgres_port: int = 5432 + postgres_db: str = "edict" + postgres_user: str = "edict" + postgres_password: str = "edict_secret_change_me" + database_url_override: str | None = None # 直接设置 DATABASE_URL 环境变量时用 + + # ── Redis ── + redis_url: str = "redis://localhost:6379/0" + + # ── Server ── + backend_host: str = "0.0.0.0" + backend_port: int = 8000 + port: int = 8000 + secret_key: str = "change-me-in-production" + debug: bool = False + + # ── OpenClaw ── + openclaw_gateway_url: str = "http://localhost:18789" + openclaw_bin: str = "openclaw" + openclaw_project_dir: str | None = None + + # ── Legacy 兼容 ── + legacy_data_dir: str = "../data" + legacy_tasks_file: str = "../data/tasks_source.json" + + # ── 调度参数 ── + stall_threshold_sec: int = 180 + max_dispatch_retry: int = 3 + dispatch_timeout_sec: int = 300 + heartbeat_interval_sec: int = 30 + scheduler_scan_interval_seconds: int = 60 + + # ── 飞书 ── + feishu_deliver: bool = True + feishu_channel: str = "feishu" + + @property + def database_url(self) -> str: + if self.database_url_override: + return self.database_url_override + return ( + f"postgresql+asyncpg://{self.postgres_user}:{self.postgres_password}" + f"@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}" + ) + + @property + def database_url_sync(self) -> str: + """同步 URL,供 Alembic 使用。""" + return ( + f"postgresql://{self.postgres_user}:{self.postgres_password}" + f"@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}" + ) + + model_config = { + "env_file": ".env", + "env_file_encoding": "utf-8", + "env_prefix": "", + "alias_generator": None, + "populate_by_name": True, + "extra": "ignore", + } + + +@lru_cache +def get_settings() -> Settings: + return Settings() + + + +"""SQLAlchemy async 引擎与 session 管理。""" + +from sqlalchemy.ext.asyncio import ( + AsyncSession, + async_sessionmaker, + create_async_engine, +) +from sqlalchemy.orm import DeclarativeBase + +from .config import get_settings + +settings = get_settings() + +engine = create_async_engine( + settings.database_url, + echo=settings.debug, + pool_size=10, + max_overflow=20, + pool_pre_ping=True, +) + +async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) + + +class Base(DeclarativeBase): + """所有 ORM 模型的基类。""" + pass + + +async def get_db() -> AsyncSession: + """FastAPI 依赖注入 — 获取异步数据库 session。""" + async with async_session() as session: + try: + yield session + await session.commit() + except Exception: + await session.rollback() + raise + + +async def init_db(): + """开发用 — 创建所有表(生产用 Alembic)。""" + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + + +"""Edict Backend — FastAPI 应用入口。 + +Lifespan 管理: +- startup: 连接 Redis Event Bus, 初始化数据库 +- shutdown: 关闭连接 + +路由: +- /api/tasks — 任务 CRUD +- /api/agents — Agent 信息 +- /api/events — 事件查询 +- /api/admin — 管理操作 +- /ws — WebSocket 实时推送 +""" + +import logging +from contextlib import asynccontextmanager + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from .config import get_settings +from .services.event_bus import get_event_bus +from .api import tasks, agents, events, admin, websocket +from .api import legacy + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(name)s] %(levelname)s: %(message)s", +) +log = logging.getLogger("edict") + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """应用生命周期管理。""" + settings = get_settings() + log.info(f"🏛️ Edict Backend starting on port {settings.port}...") + + # 连接 Event Bus + bus = await get_event_bus() + log.info("✅ Event Bus connected") + + yield + + # 清理 + await bus.close() + log.info("Edict Backend shutdown complete") + + +app = FastAPI( + title="Edict 三省六部", + description="事件驱动的 AI Agent 协作平台", + version="2.0.0", + lifespan=lifespan, +) + +# CORS — 开发环境允许所有来源 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# 注册路由 +app.include_router(tasks.router, prefix="/api/tasks", tags=["tasks"]) +app.include_router(agents.router, prefix="/api/agents", tags=["agents"]) +app.include_router(events.router, prefix="/api/events", tags=["events"]) +app.include_router(admin.router, prefix="/api/admin", tags=["admin"]) +app.include_router(websocket.router, tags=["websocket"]) +app.include_router(legacy.router, prefix="/api/tasks", tags=["legacy"]) + + +@app.get("/health") +async def health(): + return {"status": "ok", "version": "2.0.0", "engine": "edict"} + + +@app.get("/api") +async def api_root(): + return { + "name": "Edict 三省六部 API", + "version": "2.0.0", + "endpoints": { + "tasks": "/api/tasks", + "agents": "/api/agents", + "events": "/api/events", + "admin": "/api/admin", + "websocket": "/ws", + "health": "/health", + }, + } + + + +# Edict Backend Dependencies +fastapi[standard]>=0.115.0 +uvicorn[standard]>=0.32.0 +sqlalchemy[asyncio]>=2.0.36 +asyncpg>=0.30.0 +alembic>=1.14.0 +redis[hiredis]>=5.2.0 +pydantic>=2.10.0 +pydantic-settings>=2.6.0 +python-dotenv>=1.0.1 +httpx>=0.28.0 + + + +import { useState } from 'react'; + +interface Props { + title: string; + message: string; + okLabel: string; + okClass?: string; + onOk: (reason: string) => void; + onCancel: () => void; +} + +export default function ConfirmDialog({ title, message, okLabel, okClass, onOk, onCancel }: Props) { + const [reason, setReason] = useState(''); + + return ( +
+
e.stopPropagation()}> +
+
+ ` + :p.type==='select' + ?`` + :`` + } +
`).join('')} + +
+ + +
+ + `; + document.getElementById('modal-bg').classList.add('open'); +} + +function buildCommand(tplId){ + const t = TEMPLATES.find(x=>x.id===tplId); + if(!t) return ''; + let cmd = t.command; + for(const p of t.params){ + const el = document.getElementById('tpl-'+p.key); + const val = el ? el.value : (p.default||''); + cmd = cmd.replace(new RegExp('\\{'+p.key+'\\}','g'), val); + } + return cmd; +} + +function previewTemplate(tplId){ + const cmd = buildCommand(tplId); + const el = document.getElementById('tpl-preview'); + el.style.display='block'; + el.innerHTML = `
📜 将发送给中书省的旨意:
${esc(cmd)}
`; +} + +async function executeTemplate(e, tplId){ + e.preventDefault(); + const cmd = buildCommand(tplId); + if(!cmd.trim()){ toast('请填写必填参数','err'); return; } + const t = TEMPLATES.find(x=>x.id===tplId); + + // 前置校验:检测 Gateway 和关键 Agent 是否在线 + try{ + const st = await fetchJ(API+'/agents-status'); + if(st.ok && st.gateway && !st.gateway.alive){ + toast('⚠️ Gateway 未启动,任务将无法派发!请先运行 openclaw gateway start','err',6000); + if(!confirm('Gateway 未启动,任务创建后将无法自动派发。是否仍然继续?')) return; + } + }catch(ex){/* 检测失败不阻塞创建 */} + + // Show the generated command in a confirmation + if(!confirm(`确认下旨?\n\n${cmd.substring(0,200)}${cmd.length>200?'…':''}`)) return; + // 通过 API 创建真实任务 + try{ + const params = {}; + for(const p of t.params){ + const el = document.getElementById('tpl-'+p.key); + params[p.key] = el ? el.value : (p.default||''); + } + const r = await postJ(API+'/create-task', { + title: cmd.substring(0, 120), + org: '中书省', + targetDept: t.depts[0] || '', + priority: 'normal', + templateId: tplId, + params: params, + }); + if(r.ok){ + toast(`📜 ${r.taskId} 旨意已下达`, 'ok', 5000); + closeModal(); + loadAll(); + } else { + toast(r.error||'下旨失败','err'); + } + }catch(err){ + // 降级:复制到剪贴板 + toast('⚠️ 服务器连接失败,已复制到剪贴板', 'err', 4000); + navigator.clipboard.writeText(cmd).catch(()=>{}); + closeModal(); + } +} + +/* ══ INIT ══ */ +restoreGlobalScanState(); +loadAll().then(()=>showCeremony()); +startCd(); + + + + + +