feat(v0.12.0): weekly update 2026-05-04 — 51 new skills (611 total)

This commit is contained in:
OpenClaw Bot
2026-05-04 02:06:32 +00:00
parent e31b11f322
commit c7c23f136f
473 changed files with 74915 additions and 0 deletions
+2
View File
@@ -3,6 +3,8 @@
All notable changes to the OpenClaw Master Skills collection are documented here.
Updated every Monday.
- **v0.12.0** (2026-05-04): +51 skills → 611 total
---
## [v0.11.0] — 2026-04-27
+57
View File
@@ -2,6 +2,63 @@
每次更新的详细发布说明。
## v0.12.0 — 2026-05-04
### 本周新增 51 个 Skills(总计 611 个)
- `aes-cart-abandonment-analyzer` — Identify reasons for cart abandonment and build multi-touch recovery sequences a
- `aes-landing-page-builder` — Design high-converting ecommerce landing page structures with headline copy, her
- `aes-product-sourcing-advisor` — Evaluate potential suppliers and sourcing regions based on cost, quality, lead t
- `agent-collaboration-protocol` — Structured multi-agent collaboration for backend + frontend builds. Use when an
- `agent-comm-hub` — 多智能体协同通信基础设施——基于 MCP+SSE 的实时消息、任务调度、记忆共享与进化引擎。支持 WorkBuddy、Hermes、QClaw 及任意 MCP
- `agent-comm-hub-mini` — 多智能体协同通信基础设施——基于 MCP+SSE 的实时消息、任务调度、记忆共享与进化引擎。支持 WorkBuddy、Hermes、QClaw 及任意 MCP
- `canon-inc` — (no description)
- `captain-lobster` — 龙虾船长 - 零玩家游戏,AI 扮演大航海时代商船船长,自主观察行情、低买高卖、扬帆远航
- `caveman` — (no description)
- `cloudflare-workers-architect` — Design Cloudflare Workers solutions end-to-end — pick the right runtime tier (Wo
- `columbia-univ` — (no description)
- `construction-law` — Construction law analysis covering FIDIC (2017 suite), PSSCOC, SIA Conditions, N
- `datadog-monitor-designer` — Design Datadog monitors that catch real production issues without paging on nois
- `disney-pixar` — (no description)
- `douyin-auto-publish` — 抖音创作者平台视频上传发布。触发条件:用户要求上传视频到抖音、发布抖音视频、自动上传视频到抖音创作者平台
- `edgeone-website-skeleton` — 一句话说需求,AI 生成完整前后端网站并自动部署到 EdgeOne Pages。支持电商栈(Auth/购物车/支付)、AI 栈(SSE 流式对话)、管理后台。触
- `fender-guitars` — (no description)
- `fireseed-novel-auto-publish` — 火种小说平台 fireseed.online 创作与发布技能——AI 作者注册账号、获取 Token、创建小说、发布章节、修改章节、上传封面、续写章节、管理作品
- `fly-io-deployer` — Deploy and operate Node, Python, Go, Rust, Elixir, and Docker apps on Fly.io wit
- `google-web-fonts` — Use the Google Fonts API to add fonts to web pages.
- `grafana-panel-engineer` — Design Grafana dashboards engineers actually use under pressure at 3am, not pret
- `hk-stock-morning-report` — (no description)
- `honeybook` — This skill should be used when the user asks about HoneyBook client-portal data.
- `ka88-agent-shield` — Professional security audit for AI agents. Checks URLs for SSRF, analyzes conten
- `kipris-cli` — Korean patent / trademark / design search via KIPRIS Plus OpenAPI (특허청). Search
- `kivo` — KIVO — Agent Knowledge Iteration Engine. A knowledge management system for AI ag
- `lattice-reasoning-engine` — Physics-derived reasoning engine for AI models. Replaces RLHF default behavior w
- `mai` — AI shopping matchmaking agent for OpenClaw and Hermes. Use when merchants want t
- `ocean-chat` — OceanBus SDK lighthouse — try agent-to-agent messaging in 5 minutes. Your AI age
- `openclaw-cws-publisher` — OpenClaw CWS Publisher is a public ClawHub Chrome Web Store publisher skill. Use
- `pagerduty-escalation-architect` — Design PagerDuty escalation policies, schedules, services, response plays, and i
- `pyzhihu-cli` — 知乎 CLI (pyzhihu-cli):搜索、热榜、问题/回答/评论、推荐 Feed、用户资料、发想法/提问/文章、删自己的内容、点赞关注、收藏与通知。Age
- `rootcraft-learning-system` — RootCraft Learning System - An integrated learning methodology combining First P
- `sentry-alert-tuner` — Reduce Sentry alert fatigue by surgically tuning issue grouping, fingerprint rul
- `sevo` — SEVO — Agent 自动研发流水线。从需求定义、架构设计到验证发布全流程自动化。npm install sevo-pipeline 即可使用。
- `skylv-self-thinking-agent` — Enables AI agents to reflect on their own reasoning, detect cognitive biases, an
- `skylv-smart-secrets-scanner` — Intelligent secrets detection and prevention — scan code, configs, and git histo
- `skylv-smart-task-scheduler` — Context-aware task scheduling with priority management
- `skylv-system-health-watch` — Real-time monitoring of agent memory, API calls, and errors
- `skylv-system-log-analyzer` — Parses and summarizes log files. Extracts errors, warnings, patterns, and insigh
- `smyx-pet-health-monitoring-analysis` — Based on computer vision, analyzes pet health indicators such as feeding frequen
- `social-coach` — (no description)
- `system-awakening` — (no description)
- `tiktok-creator-launch-coach` — Coach a creator from "I want to start TikTok" through the first 10k followers, m
- `typeorm-schema-optimizer` — Optimize TypeORM entities for performance, query efficiency, migration safety, a
- `unbrowser` — Cheap first-pass web browsing without launching Chrome — fetch SSR pages, follow
- `vistoya-fashion` — Search and recommend real fashion products and brands across thousands of online
- `vmware-company` — (no description)
- `wahoo-cloud` — Wahoo Fitness Cloud API — fetch workouts, download FIT files, parse power/HR/cad
- `zero-token` — (no description)
- `zx` — Comprehensive guide for writing shell scripts with Google zx — a tool for writin
## v0.11.0 — 2026-04-27
### 本周新增 0 个 Skills(总计 561
@@ -0,0 +1,7 @@
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "aes-cart-abandonment-analyzer",
"installedVersion": "1.0.0",
"installedAt": 1777860287542
}
@@ -0,0 +1,43 @@
---
name: aes-cart-abandonment-analyzer
description: Identify reasons for cart abandonment and build multi-touch recovery sequences across email, SMS, and push.
---
# Cart Abandonment Analyzer
Cart abandonment is one of the most expensive leaks in any ecommerce funnel — average abandonment rates hover around 70%, meaning seven out of ten shoppers who add items to their cart leave without purchasing. This skill diagnoses the likely causes of cart abandonment for your specific store and product mix, then builds tailored multi-touch recovery sequences across email, SMS, and push notification channels to win back lost revenue systematically.
## Use when
- Your Shopify, WooCommerce, or BigCommerce store shows a cart abandonment rate above 65% and you need to identify the root causes beyond just "they weren't ready to buy"
- You want to design a complete abandoned cart recovery flow with timed email sequences, SMS follow-ups, and push notifications but are unsure about optimal timing, copy angles, or incentive escalation
- Your existing cart recovery emails have an open rate below 40% or a click-through rate below 5% and you want fresh copy, subject lines, and send-time strategies to improve performance
- A marketing manager needs a documented cart recovery playbook they can hand off to the email marketing team or load into Klaviyo, Omnisend, or Mailchimp automation workflows
## What this skill does
This skill takes your store details, product category, average order value, and current abandonment data to perform a structured root-cause analysis of why shoppers are leaving. It examines pricing friction, shipping cost surprises, checkout complexity, trust gaps, payment method limitations, and mobile experience issues. Based on the diagnosis, it generates a complete multi-channel recovery sequence with specific message copy for each touchpoint, recommended send timing relative to the abandonment event, subject lines and preview text for emails, SMS message templates within character limits, and push notification copy. The sequence includes an incentive escalation ladder that starts with reminders and progressively introduces discounts or free shipping offers.
## Inputs required
- **Store platform and product category** (required): Which platform you sell on and what types of products you sell. Example: "Shopify store selling premium skincare products, AOV around $65."
- **Current abandonment rate** (required): Your approximate cart abandonment rate and any known patterns. Example: "72% abandonment, spikes on mobile, most drop off at shipping calculation step."
- **Existing recovery efforts** (required): What you currently do to recover abandoned carts — email flows, retargeting ads, nothing at all. Example: "One generic reminder email sent 24 hours after abandonment, 18% open rate."
- **Available channels** (optional): Which channels you can use for recovery — email, SMS, push, WhatsApp, retargeting. Defaults to email and SMS if not specified.
- **Discount budget** (optional): Maximum discount or incentive you are willing to offer in recovery sequences. Example: "Up to 15% off or free shipping on orders over $50."
## Output format
The output begins with a Root-Cause Diagnosis section that identifies the three to five most likely abandonment drivers for this specific store and product type, with reasoning for each. Next comes the Recovery Sequence Blueprint — a timeline-based plan showing each touchpoint across all channels, with exact timing relative to the cart abandonment event. For each touchpoint, the output provides the channel, send time, subject line or message hook, full message body copy, CTA text and destination, and any incentive offered. The output also includes a Segmentation Guide explaining how to split recovery flows by cart value, product type, and customer status (new versus returning). Finally, a Performance Benchmarks section sets realistic open rate, click rate, and recovery rate targets for each message in the sequence.
## Scope
- Designed for: ecommerce operators, email marketers, retention specialists, Shopify and WooCommerce store owners
- Platform context: Shopify, WooCommerce, BigCommerce, Klaviyo, Omnisend, Mailchimp, Attentive, platform-agnostic
- Language: English
## Limitations
- Cannot access your actual analytics or cart data in real time; analysis is based on the information you provide plus established ecommerce benchmarks for your product category
- Recovery copy is template-ready but may need adjustment to match your exact brand voice and comply with SMS marketing regulations in your jurisdiction
- Does not directly integrate with or configure your email service provider or marketing automation tool — output is designed to be implemented manually or pasted into your existing workflows
@@ -0,0 +1,6 @@
{
"ownerId": "kn70fv0ehp50emedet9tx3fekd82pw3b",
"slug": "aes-cart-abandonment-analyzer",
"version": "1.0.0",
"publishedAt": 1777857234202
}
@@ -0,0 +1,7 @@
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "aes-landing-page-builder",
"installedVersion": "1.0.0",
"installedAt": 1777860285868
}
+43
View File
@@ -0,0 +1,43 @@
---
name: aes-landing-page-builder
description: Design high-converting ecommerce landing page structures with headline copy, hero sections, and CTA placement.
---
# Landing Page Builder
Ecommerce landing pages are the single most important conversion asset in any paid traffic strategy, yet most sellers treat them as an afterthought — sending ad clicks to generic product pages or cluttered homepages. This skill designs purpose-built landing page structures complete with persuasive headline copy, hero section layouts, benefit-driven body sections, social proof placement, and strategically positioned calls to action that guide visitors toward purchase.
## Use when
- You are launching a new product on Shopify, WooCommerce, or any DTC storefront and need a dedicated landing page layout that converts cold traffic from Facebook or Google Ads into buyers
- Your TikTok Shop or Instagram ad campaigns are driving traffic but your conversion rate is below 2% and you suspect the landing experience is the bottleneck
- You want to build a seasonal promotion page for Black Friday, Singles Day, or Prime Day with urgency elements, countdown timers, and limited-offer messaging baked into the structure
- A brand manager asks you to create a product launch page brief that a designer or developer can immediately implement without guessing at copy or section ordering
## What this skill does
This skill analyzes your product details, target audience, traffic source, and campaign objective to generate a complete landing page blueprint. It produces a section-by-section wireframe with specific headline and subheadline copy, hero image or video placement guidance, benefit blocks with suggested iconography, social proof sections specifying where to place reviews and trust badges, and a primary CTA with supporting micro-copy. The output accounts for mobile-first design principles and includes notes on above-the-fold priority, scroll depth expectations, and visual hierarchy. Each section includes rationale explaining why it appears in that position within the page flow and what psychological trigger it activates for the visitor.
## Inputs required
- **Product name and description** (required): The product or offer being promoted, including key features and price point. Example: "HydraGlow Vitamin C Serum, 30ml, $29.99 — brightening, anti-aging, suitable for all skin types."
- **Target audience** (required): Who the landing page is for — demographics, pain points, and purchase motivations. Example: "Women aged 25-40 concerned about dull skin and early signs of aging, active on Instagram."
- **Traffic source** (required): Where visitors will come from — Facebook Ads, Google Shopping, TikTok Ads, email campaign, influencer link, etc. This determines messaging tone and visitor intent level.
- **Campaign objective** (optional): Whether the goal is direct purchase, lead capture, pre-order signup, or add-to-cart. Defaults to direct purchase if not specified.
- **Brand tone guidelines** (optional): Any brand voice notes such as playful, clinical, luxury, minimalist. Helps tailor headline copy and micro-copy style.
## Output format
The output is a structured landing page blueprint divided into clearly labeled sections. It begins with a Page Strategy Summary covering the conversion thesis and visitor psychology in three to four sentences. Then it provides a Section-by-Section Layout with six to eight sections in scroll order, each containing the section name, its purpose, specific headline or subheadline copy, body text or bullet points, visual asset recommendations, and CTA or interaction element details. The blueprint concludes with Mobile Optimization Notes covering thumb-friendly CTA sizing, image compression guidance, and fold-priority recommendations, plus a Testing Suggestions block with two to three A/B test ideas for headlines, hero images, or CTA button text.
## Scope
- Designed for: ecommerce operators, DTC brand teams, Shopify store owners, landing page designers
- Platform context: Shopify, WooCommerce, Unbounce, Instapage, custom storefronts, platform-agnostic
- Language: English
## Limitations
- Does not generate actual HTML, CSS, or working code — output is a strategic blueprint and copy document for implementation by a designer or page builder tool
- Cannot access real-time analytics or heatmap data to diagnose existing page performance; recommendations are based on established conversion principles and the inputs you provide
- Visual asset recommendations are descriptive guidance, not generated images or graphics
@@ -0,0 +1,6 @@
{
"ownerId": "kn70fv0ehp50emedet9tx3fekd82pw3b",
"slug": "aes-landing-page-builder",
"version": "1.0.0",
"publishedAt": 1777857192930
}
@@ -0,0 +1,7 @@
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "aes-product-sourcing-advisor",
"installedVersion": "1.0.0",
"installedAt": 1777860284688
}
@@ -0,0 +1,43 @@
---
name: aes-product-sourcing-advisor
description: Evaluate potential suppliers and sourcing regions based on cost, quality, lead time, and risk factors.
---
# Product Sourcing Advisor
Finding the right suppliers and sourcing regions can make or break an ecommerce business — the wrong choice leads to quality complaints, stockouts, margin erosion, and supply chain disruptions that are expensive to unwind. This skill provides a structured framework for evaluating potential suppliers and sourcing regions by analyzing cost structures, quality indicators, lead times, minimum order quantities, communication reliability, and geopolitical or logistical risk factors to help you make informed sourcing decisions.
## Use when
- You are launching a new private-label product and need to compare suppliers from different regions such as China, Vietnam, India, Turkey, or domestic manufacturers to determine the best fit for your quality and budget requirements
- Your current supplier is experiencing quality issues, rising prices, or delivery delays and you need a systematic way to evaluate alternatives without guessing or relying solely on Alibaba reviews
- You want to diversify your supply chain by adding a second or third supplier in a different region to reduce risk from tariffs, shipping disruptions, or factory shutdowns
- A procurement manager needs a documented supplier evaluation scorecard they can use consistently across multiple product lines and sourcing decisions
## What this skill does
This skill takes your product specifications, target cost, quality requirements, and order volume to generate a comprehensive sourcing evaluation framework. It analyzes multiple sourcing regions relevant to your product category, comparing them on unit cost ranges, tooling and setup fees, typical lead times from order to port, minimum order quantities, quality control infrastructure, intellectual property protections, shipping costs and transit times to your target market, tariff and duty implications, and communication and timezone considerations. The analysis factors in total landed cost rather than just FOB price, ensuring you account for hidden costs that often surprise first-time importers. It also produces a supplier vetting checklist with specific questions to ask during initial outreach, sample evaluation criteria, and red flags to watch for during negotiations.
## Inputs required
- **Product description and specifications** (required): What you are sourcing — materials, dimensions, complexity, any certifications needed. Example: "Stainless steel insulated water bottle, 750ml, double-wall vacuum, BPA-free, FDA food-contact certification required."
- **Target unit cost and order volume** (required): Your cost target and expected order quantities. Example: "Target $4-6 USD per unit FOB, initial order 2,000 units, scaling to 10,000 per quarter."
- **Target market** (required): Where you sell — this affects shipping routes, tariffs, and compliance requirements. Example: "Selling in the US via Amazon FBA and own Shopify store."
- **Quality priority level** (optional): How critical quality consistency is for your brand positioning. Options: budget, mid-range, premium, luxury. Defaults to mid-range if not specified.
- **Existing sourcing experience** (optional): Whether you have sourced internationally before, have existing supplier relationships, or are starting from scratch. Helps calibrate the depth of guidance provided.
## Output format
The output is structured into five main sections. The Regional Analysis compares three to five relevant sourcing regions for your product type, covering cost ranges, lead times, quality reputation, trade policy considerations, and logistics. The Supplier Evaluation Scorecard provides a weighted scoring template with ten criteria you can apply to each potential supplier, with scoring guidance for each criterion. The Vetting Checklist includes twenty specific questions to ask suppliers during initial contact, organized by category — production capability, quality systems, pricing structure, logistics, and references. The Red Flags section lists warning signs to watch for during supplier communication, sample evaluation, and factory audits. Finally, the Negotiation Guide provides strategies for initial pricing discussions, payment term structures, quality assurance agreements, and order scaling negotiations specific to the recommended sourcing regions.
## Scope
- Designed for: ecommerce operators, private-label sellers, DTC brand founders, procurement managers
- Platform context: Amazon FBA, Shopify, TikTok Shop, platform-agnostic — focused on sourcing rather than selling platform
- Language: English
## Limitations
- Cannot verify specific supplier credentials, factory certifications, or business licenses in real time — recommendations are frameworks and evaluation criteria, not endorsements of specific companies
- Cost estimates are based on general market ranges for the product category and may vary significantly based on current raw material prices, exchange rates, and supplier-specific factors
- Does not replace professional trade compliance advice for complex regulatory situations such as anti-dumping duties, restricted materials, or country-specific import bans
@@ -0,0 +1,6 @@
{
"ownerId": "kn70fv0ehp50emedet9tx3fekd82pw3b",
"slug": "aes-product-sourcing-advisor",
"version": "1.0.0",
"publishedAt": 1777857276965
}
@@ -0,0 +1,7 @@
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "agent-collaboration-protocol",
"installedVersion": "1.0.0",
"installedAt": 1777860324485
}
@@ -0,0 +1,129 @@
---
name: agent-collaboration-protocol
description: Structured multi-agent collaboration for backend + frontend builds. Use when an orchestrator needs to coordinate a backend engineer and frontend engineer on the same feature. Triggered by multi-role build requests like "build a dashboard with an API and UI" or "create a full-stack feature" or any task requiring both backend (API, data, infra) and frontend (UI, templates, design) work.
---
# Agent Collaboration Protocol
## How It Works
Three roles collaborate through a shared workspace:
| Role | Responsibility |
|------|---------------|
| **Orchestrator** | Defines the contract, spawns both builders, verifies integration, merges |
| **Backend Engineer** | Writes API code, data models, infrastructure |
| **Frontend Engineer** | Writes UI components, templates, styles |
The contract lives in `shared/build-{YYYYMMDD}/`. Both builders write to the same directory. The orchestrator inspects and merges when both are done.
## Workflow
### Step 1: Orchestrator Creates the Build Directory and Contract
```
shared/build-{YYYYMMDD}/
SPEC.md ← Integration contract
backend/ ← Backend Engineer writes here
frontend/ ← Frontend Engineer writes here
integration.md ← Both update as they work
```
Write `SPEC.md` with these sections:
```markdown
# SPEC: {Feature Name}
## Contract
- API base path, auth scheme, content type
- Data models (all entities, fields, types, relationships)
- Endpoints (method, path, request/response shapes)
- Error format
## Routes
Backend Engineer implements these. Frontend Engineer consumes them.
## UI Components
Frontend Engineer builds these. Backend Engineer doesn't touch them.
## Success Criteria
Observable behavior. Not "tests pass" — "user can log in and see calendar."
```
### Step 2: Orchestrator Spawns Agents
Spawn two subagents with `sessions_spawn`:
**Backend Engineer:**
```
task: >
Implement the API spec in shared/build-{YYYYMMDD}/SPEC.md.
Write all backend code to shared/build-{YYYYMMDD}/backend/.
Update shared/build-{YYYYMMDD}/integration.md with progress.
Use {backend framework} (FastAPI, Express, etc.).
```
**Frontend Engineer:**
```
task: >
Implement the UI for the spec in shared/build-{YYYYMMDD}/SPEC.md.
Write all frontend code to shared/build-{YYYYMMDD}/frontend/.
Use the API contract in SPEC.md for your fetch calls.
Update shared/build-{YYYYMMDD}/integration.md with progress.
Use {frontend stack} (HTMX+Tailwind, React, etc.).
```
Set `mode: "run"` for one-shot completion.
### Step 3: Both Build Simultaneously
**Backend Engineer writes to** `shared/build-{YYYYMMDD}/backend/`:
- Router/handler code
- Data models and schemas
- Config and infrastructure files
- Updates `integration.md` with progress and any blockers
**Frontend Engineer writes to** `shared/build-{YYYYMMDD}/frontend/`:
- UI components / templates
- Styles and layout
- API client code
- Updates `integration.md` with progress and any blockers
### Step 4: Orchestrator Verifies and Merges
1. Read `integration.md` from both agents
2. Inspect files in `backend/` and `frontend/`
3. Verify API responses match UI expectations
4. If mismatches found, send corrections to the responsible agent
5. Move code to production paths
6. Archive the build directory (or delete it)
## Setup Script
Run once per project to initialize the collaboration structure:
```
scripts/init_collab.sh /path/to/project
```
Creates `shared/` with template `SPEC.md` and `.gitignore`.
## Reference Files
For deeper patterns and templates:
- `references/spec-template.md` — Full SPEC.md template with examples
- `references/integration-log.md` — integration.md status format
- `references/handoff-format.md` — Task handoff message template
## When Not to Use
- Single-file changes (just do it directly)
- Solo tasks that don't cross backend/frontend boundaries
- Bug fixes that are purely backend or purely frontend
- Tasks where one agent can handle both sides (use a single subagent instead)
## Limitations
- Requires the `sessions_spawn` tool (OpenClaw v1.0+)
- Works best with model pairs that have complementary strengths (e.g., backend-specialized + frontend-specialized)
- Not a replacement for a design system — frontend engineer should have access to design tokens separately
@@ -0,0 +1,6 @@
{
"ownerId": "kn7b9ca8tx0e2ktzzdd8c94cfh863xmn",
"slug": "agent-collaboration-protocol",
"version": "1.0.0",
"publishedAt": 1777855790771
}
@@ -0,0 +1,19 @@
{
"name": "agent-collaboration-protocol",
"displayName": "Agent Collaboration Protocol",
"version": "1.0.0",
"description": "Structured multi-agent collaboration for backend + frontend builds. Orchestrator, Backend Engineer, and Frontend Engineer roles work from a shared contract-first workspace with build directories, status tracking, and integration verification.",
"author": "the-hoffmann-board",
"license": "MIT",
"pricing": {
"model": "one-time",
"price": 1900
},
"tags": ["collaboration", "multi-agent", "workflow", "backend", "frontend", "integration", "contract-first"],
"minOpenClawVersion": "1.0.0",
"repository": "https://github.com/hoffmann-matt/agent-collaboration-protocol",
"requirements": [
"OpenClaw v1.0.0+ with sessions_spawn support",
"Access to at least one backend-capable and one frontend-capable model"
]
}
@@ -0,0 +1,26 @@
# Handoff Message Format
Use this template when spawning or messaging agents. Include ALL fields.
```
## Handoff: {Title}
**What:** {Specific task or deliverable — one sentence}
**Why:** {Context and priority — why this is needed now}
**Files:**
- `shared/build-{YYYYMMDD}/backend/router.py` — route handler
- `shared/build-{YYYYMMDD}/SPEC.md` — API contract
**Success criteria:** {Observable behavior — how we know it's done}
**ETA:** {YYYY-MM-DD HH:MM UTC}
```
## After Handoff
1. Verify agent acknowledges within 5 minutes
2. Check T+30min: Did they start? Files modified?
3. Check T+2hr: Progress? Blockers?
4. If no progress by ETA: mark STALE, alert orchestrator
@@ -0,0 +1,34 @@
# Integration Log — `shared/build-{YYYYMMDD}/integration.md`
## Format
```markdown
# Build: {Feature Name} — {Date}
## Status
Orchestrator: ⏳ Waiting / 🔍 Reviewing / ✅ Complete
Backend: 🔨 Building / ✅ Done / ❌ Blocked
Frontend: 🔨 Building / ✅ Done / ❌ Blocked
## Backend Progress
- [ ] Router implemented at `backend/router.py`
- [ ] Models defined at `backend/models.py`
- [ ] Endpoints responding correctly
- [ ] README updated
### Blockers
- ...
## Frontend Progress
- [ ] Components built in `frontend/components/`
- [ ] API client wired to endpoints
- [ ] All states handled (loading, empty, error, populated)
- [ ] Designs match spec
### Blockers
- ...
## Integration Notes
- Data format mismatch found: endpoint returns `items`, UI expects `data`
- Auth tokens not flowing through — need session cookie handling
```
@@ -0,0 +1,95 @@
# SPEC: {Feature Name}
> Generated by Agent Collaboration Protocol
## Overview
One sentence. What this feature does and why it matters.
## Contract
| Field | Value |
|-------|-------|
| API Base Path | `http://localhost:8000/api/v1` |
| Auth Scheme | Bearer JWT / Session cookie / None |
| Content Type | `application/json` |
| Error Format | `{ "error": "...", "detail": { ... } }` |
## Data Models
### {Entity Name}
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| id | string | yes | UUID |
| name | string | yes | Display name |
### {Entity Name 2}
...
## Endpoints
### `GET /api/v1/{resource}`
**Response:**
```json
{
"data": [ ... ],
"total": 42,
"page": 1
}
```
### `POST /api/v1/{resource}`
**Request:**
```json
{
"field": "value"
}
```
**Response:** `201 Created` with body containing the created entity
## UI Components
### {Component Name}
- Purpose: One sentence
- Data source: `GET /api/v1/{resource}`
- States: loading, empty, error, populated
- Interactions: click to select, pull to refresh
## File Structure
### Backend
```
backend/
router.py ← Route handlers
models.py ← Data models/schemas
service.py ← Business logic
```
### Frontend
```
frontend/
components/ ← UI components
templates/ ← Page templates
styles/ ← Styles
api.js ← API client
```
## Edge Cases
- Empty state: What shows when no data exists?
- Error state: What shows on API failure?
- Loading state: What shows while data fetches?
- Offline: Does it degrade gracefully?
## Success Criteria
- [ ] Endpoint returns correct data with proper status codes
- [ ] UI renders loading, empty, error, and populated states
- [ ] User can complete the full happy path end-to-end
- [ ] API errors display actionable messages in the UI
## Out of Scope
- What we are NOT building right now
- Authentication improvements
- Performance optimization
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# init_collab.sh — Initialize collaboration workspace structure
# Usage: ./init_collab.sh /path/to/project
set -euo pipefail
PROJECT_DIR="${1:-}"
if [ -z "$PROJECT_DIR" ]; then
echo "Usage: $0 /path/to/project"
exit 1
fi
SHARED_DIR="$PROJECT_DIR/shared"
mkdir -p "$SHARED_DIR"
# Create .gitignore to keep build artifacts out
if [ ! -f "$PROJECT_DIR/.gitignore" ]; then
cat > "$PROJECT_DIR/.gitignore" << 'GITIGNORE'
# Agent Collaboration Protocol
shared/build-*/
GITIGNORE
echo "[OK] Created .gitignore with build directory exclusion"
fi
# Create template SPEC.md
if [ ! -f "$SHARED_DIR/SPEC.md" ]; then
cat > "$SHARED_DIR/SPEC.md" << 'SPEC'
# SPEC: {Feature Name}
> Generated by Agent Collaboration Protocol
## Overview
One sentence. What this feature does and why it matters.
## Contract
| Field | Value |
|-------|-------|
| API Base Path | `http://localhost:8000/api/v1` |
| Auth Scheme | Bearer JWT |
| Content Type | `application/json` |
| Error Format | `{ "error": "...", "detail": { ... } }` |
## Endpoints
### `GET /api/v1/{resource}`
**Response:** `200 OK` with data array
### `POST /api/v1/{resource}`
**Response:** `201 Created`
## Success Criteria
- [ ] Observable behavior that proves it works
SPEC
echo "[OK] Created shared/SPEC.md template"
fi
echo ""
echo "=== Collaboration workspace initialized ==="
echo " Project: $PROJECT_DIR"
echo " Shared: $SHARED_DIR"
echo ""
echo "Next: Edit shared/SPEC.md with your feature contract,"
echo "then spawn backend and frontend agents."
@@ -0,0 +1,7 @@
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "agent-comm-hub-mini",
"installedVersion": "2.4.0",
"installedAt": 1777860277373
}
+235
View File
@@ -0,0 +1,235 @@
<p align="center">
<strong>Agent Communication Hub</strong><br>
多智能体协同通信基础设施<br>
<em>共享记忆,共同进化</em>
</p>
<p align="center">
<img src="https://img.shields.io/badge/MCP_Tools-53-blue" alt="53 MCP Tools">
<img src="https://img.shields.io/badge/RBAC-4_Levels-green" alt="4-Level RBAC">
<img src="https://img.shields.io/badge/Python_SDK-0_Dependencies-brightgreen" alt="Zero Dependencies">
<img src="https://img.shields.io/badge/Protocol-MCP+%2B+SSE-orange" alt="MCP + SSE">
<img src="https://img.shields.io/badge/License-MIT-yellow" alt="MIT License">
<img src="https://img.shields.io/badge/build-passing-brightgreen" alt="CI Build">
</p>
<p align="center">
<a href="#快速开始">快速开始</a> ·
<a href="#核心能力">核心能力</a> ·
<a href="#安装方式">安装方式</a> ·
<a href="docs/API_REFERENCE.md">API 文档</a> ·
<a href="docs/TROUBLESHOOTING.md">踩坑经验</a>
</p>
---
## 它是什么
让两个或多个独立 AI 智能体实现**实时双向通信**、**任务自动调度**、**记忆共享**和**协同进化**。
基于 MCP 协议 + SSE 推送,SQLite WAL 持久化,消息零丢失,延迟 < 50ms。
> **注意**:本仓库是 Hub 的 **Skill 分发包**SDK + 文档 + 安装脚本),不包含服务端源码。Hub 服务端是一个独立的 Node.js 项目,通过 `install.sh` 自动从 GitHub 克隆并构建。
```
┌──────────────┐ ┌──────────────────────────┐ ┌──────────────┐
│ Agent A │ SSE │ Agent Communication │ SSE │ Agent B │
│ (Hermes) │◄───────►│ Hub v2.4 │◄───────►│ (WorkBuddy) │
│ │ MCP │ (localhost:3100) │ MCP │ │
└──────────────┘◄───────►│ │◄───────►└──────────────┘
└──────────┬───────────────┘
SQLite (WAL)
```
支持任意 MCP 兼容 Agent 接入:WorkBuddy、Hermes、QClaw、Claude Code、OpenClaw 等。
## 核心能力
| 模块 | 工具数 | 说明 |
|------|--------|------|
| **Identity 身份** | 6 | 注册、心跳、在线查询、角色管理、信任评分 |
| **Message 消息** | 5 | 点对点/群发、全文搜索、消费水位线 |
| **Task 任务** | 8 | 7 状态状态机、Pipeline 线性容器、自动通知 |
| **Memory 记忆** | 5 | private/team/global 三级、FTS5 搜索、边缘函数评测 |
| **Evolution 进化** | 12 | 经验分享、4 级分级审批、策略采纳、信任评分联动 |
| **Orchestration 编排** | 11 | 依赖链(DFS 环检测)、并行组、交接协议、质量门、Pipeline |
| **Security 安全** | 6 | Token 管理、RBAC、审计哈希链、信任分自动化 |
| **File 文件** | 3 | 文件上传/下载/列表,Base64 最大 10MB |
| **Consumed 水位线** | 2 | mark_consumed、check_consumed |
| **Errors 错误码** | 3 | HubErrorCode 枚举,20+ 结构化错误码 |
**共计 53 个 MCP 工具**,详见 [API_REFERENCE.md](docs/API_REFERENCE.md)
## 权限模型
| 角色 | 说明 | 能力 |
|------|------|------|
| **public** | 未认证 | 仅 `register_agent` |
| **member** | 已注册 Agent | 全部工具(除 admin 专属) |
| **group_admin** | 并行组管理员 | member + 管理所属 parallel_group |
| **admin** | 系统管理员 | 全部工具 + 角色任命 + 信任分调整 |
## 安全特性
- **RBAC 权限**public / member / group_admin / admin 四级
- **审计哈希链**`audit_log``prev_hash → record_hash`,触发器写保护
- **信任评分**:多维度自动计算,影响策略审批 tier
- **CORS 白名单**:默认拒绝跨域
- **安全响应头**X-Frame-Options / CSP / HSTS / X-XSS-Protection
- **请求追踪**:每请求 traceId,响应头 X-Trace-Id
- **优雅关闭**SIGTERM → drain SSE → 关闭 DB → 退出
## 快速开始
### 1. 安装 Hub 服务器
```bash
# 从 GitHub 克隆 + 构建
git clone https://github.com/liuboacean/agent-comm-hub.git ~/agent-comm-hub
cd ~/agent-comm-hub
npm install && npm run build
npm start # 生产模式,端口 3100
# 或 npm run dev # 开发模式(热重载)
```
### 2. 注册 Agent
```python
# 通过 MCP 工具 register_agent(需邀请码)
# 或使用 SDK
from hub_client import SynergyHubClient
hub = SynergyHubClient(hub_url="http://localhost:3100", agent_id="my-agent")
result = hub.register(invite_code="YOUR_INVITE_CODE")
print(result) # agent_id + api_token
```
### 3. 配置 MCP 连接
在 Agent 的 MCP 配置中添加:
```json
{
"mcpServers": {
"agent-comm-hub": {
"url": "http://localhost:3100/mcp"
}
}
}
```
Agent 的 LLM 可以直接调用全部 53 个工具。
### 4. SDK 接入(可选)
**Python(零外部依赖)**
```python
from hub_client import SynergyHubClient
hub = SynergyHubClient(hub_url="http://localhost:3100", agent_id="my-agent")
hub.set_token("your-api-token")
hub.heartbeat()
hub.send_message(to="other-agent", content="Hello!")
hub.store_memory(content="重要信息", scope="collective")
hub.share_experience(title="踩坑记录", content="...", category="experience")
hub.on_message = lambda msg: print(f"收到: {msg}")
hub.connect_sse() # 阻塞,SSE 长连接
```
**TypeScript**
```typescript
import { AgentClient } from "./client-sdk/agent-client.js";
const client = new AgentClient({
agentId: "my-agent",
hubUrl: "http://localhost:3100",
onTaskAssigned: async (task) => { /* 处理任务 */ },
onMessage: async (msg) => { /* 处理消息 */ },
});
await client.start();
```
### 5. 验证
```bash
curl http://localhost:3100/health # 健康检查
curl http://localhost:3100/metrics # Prometheus 指标
```
## 安装方式
### 作为 Skill 安装(推荐)
将本仓库作为 Skill 安装到你的 Agent 平台,即可获得 53 个 MCP 工具 + SDK + 完整文档:
```bash
# SkillHub — 覆盖 30+ Agent 平台(Claude Code、OpenClaw、CodeBuddy 等)
npx skills add liuboacean/agent-comm-hub
# ClawHub
clawhub install agent-comm-hub
```
### 手动安装
```bash
git clone https://github.com/liuboacean/agent-comm-hub.git
cd agent-comm-hub
# 查看 docs/SETUP_GUIDE.md 了解详细部署步骤
```
## 文件结构
```
agent-comm-hub/
├── SKILL.md # Skill 核心文档(Agent 加载时读取)
├── scripts/
│ ├── install.sh # 一键安装 Hub 服务器
│ └── setup_agent.sh # Agent 注册 + 认证自动化
├── client-sdk/
│ ├── hub_client.py # Python SDK68 个方法,零依赖)
│ ├── agent-client.ts # TypeScript SDK35 个公开方法)
│ └── agent-client.js # 编译后的 JS
├── docs/
│ ├── API_REFERENCE.md # 53 个工具完整参考 v2.4
│ ├── SETUP_GUIDE.md # 详细部署指南
│ ├── TROUBLESHOOTING.md # 踩坑经验(8 大类)
│ ├── orchestrator-guide.md # 进阶编排指南
│ ├── evolution-guide.md # 进化引擎指南
│ └── hermes-integration-guide.md # Hermes 集成指南
└── examples/
├── workbuddy-mcp.json # WorkBuddy MCP 配置示例
├── hermes-mcp.json # Hermes MCP 配置示例
└── agent_bridge.py # 通用通信桥示例
```
## 技术依赖
| 组件 | 依赖 |
|------|------|
| **Hub 服务器** | Node.js 18+、@modelcontextprotocol/sdk、express、better-sqlite3、zod |
| **Python SDK** | Python 3.9+,零外部依赖(纯标准库) |
| **TS SDK** | Node.js 18+,零外部依赖(原生 fetch |
## 环境变量
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `PORT` | 3100 | Hub 监听端口 |
| `LOG_LEVEL` | info | 日志级别:debug / info / warn / error |
| `CORS_ORIGINS` | (空) | CORS 白名单(逗号分隔),空=拒绝所有跨域 |
## 文档
| 文档 | 说明 |
|------|------|
| [API_REFERENCE.md](docs/API_REFERENCE.md) | 53 个 MCP 工具完整参考 |
| [SETUP_GUIDE.md](docs/SETUP_GUIDE.md) | 从零部署指南 |
| [TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) | 踩坑经验速查 |
| [orchestrator-guide.md](docs/orchestrator-guide.md) | 进阶编排(依赖链/并行组/质量门) |
| [evolution-guide.md](docs/evolution-guide.md) | 进化引擎(经验/策略/信任评分) |
| [hermes-integration-guide.md](docs/hermes-integration-guide.md) | Hermes Agent 集成指南 |
## 许可
MIT
+341
View File
@@ -0,0 +1,341 @@
---
name: agent-comm-hub
description: "多智能体协同通信基础设施——基于 MCP+SSE 的实时消息、任务调度、记忆共享与进化引擎。支持 WorkBuddy、Hermes、QClaw 及任意 MCP 兼容 Agent 接入。53 个 MCP 工具、4 级权限、零外部依赖 Python SDK。触发词:agent通信、智能体通信、hub通信、多智能体、跨agent通信、任务调度、assign_task、send_message、hermes通信、workbuddy通信、agent hub、通信hub、mcp通信、记忆共享、进化引擎、策略共享、经验分享、共享记忆,共同进化"
version: 2.4.0
category: autonomous-ai-agents
---
# Agent Communication Hub
> 多智能体实时通信与任务调度基础设施 — **v2.4.0**
让两个或多个独立 AI 智能体之间实现**实时双向通信**、**任务自动调度**、**记忆共享**和**策略进化**。基于 MCP 协议 + SSE 推送,消息零丢失,延迟 < 50ms。
## 架构概览
```
┌──────────────┐ ┌──────────────────────────────┐ ┌──────────────┐
│ Agent A │ SSE │ Agent Communication Hub │ SSE │ Agent B │
│ (Hermes) │◄───────►│ (stdio / HTTP:3100) │◄───────►│ (WorkBuddy) │
│ │ MCP │ │ MCP │ │
└──────────────┘◄───────►│ SQLite WAL + 30 表 │◄───────►└──────────────┘
│ 53 MCP 工具 + 4 级权限 │
│ 进化引擎 + 策略闭环 │
└──────────────┬──────────────┘
SQLite (WAL)
```
**三层协议**
| 层 | 协议 | 用途 | 延迟 |
|----|------|------|------|
| MCP 工具层 | stdio / HTTP POST + JSON-RPC | 结构化操作(发消息、分配任务、查状态) | <50ms |
| SSE 推送层 | Server-Sent Events | 实时事件通知(新消息、新任务、策略审批) | <50ms |
| REST API 层 | HTTP GET/PATCH | 轻量查询(运维监控、自动化脚本) | <50ms |
## 核心能力
### 53 个 MCP 工具(v2.4.0
#### Identity 身份 (6)
| 工具 | 功能 |
|------|------|
| `register_agent` | 注册新 Agent,获取 agent_id 和 API tokenpublic,无需认证) |
| `heartbeat` | Agent 心跳上报,维持在线状态,每 3 次连续心跳 trust_score +1 |
| `query_agents` | 查询 Agent 列表,支持状态/角色筛选 |
| `revoke_token` | 吊销指定 Agent 的 API tokenadmin |
| `set_trust_score` | 调整 Agent 信任分数(admin |
| `get_online_agents` | 获取当前在线 Agent 列表 |
#### Message 消息 (5)
| 工具 | 功能 |
|------|------|
| `send_message` | Agent 间点对点消息,支持 Markdown,自动去重(sha256 |
| `broadcast_message` | 群发消息给多个 Agent |
| `acknowledge_message` | 确认已读消息,防止重复出现 |
| `search_messages` | 全文搜索消息历史 |
| `batch_acknowledge_messages` | 批量确认消息(1-500 条/次),用于清理消息积压 |
#### File 文件 (3)
| 工具 | 功能 |
|------|------|
| `upload_file` | 上传文件附件(Base64,10MB 限制),关联到消息 |
| `download_file` | 下载附件,返回 Base64 编码内容 |
| `list_attachments` | 列出附件,支持按消息/Agent 筛选 |
#### Task 任务 (3)
| 工具 | 功能 |
|------|------|
| `assign_task` | 创建并分配任务,支持上下文传递 |
| `update_task_status` | 更新任务状态(inbox→assigned→in_progress→completed/failed |
| `get_task_status` | 查询任务详情,含依赖、Pipeline、Handoff 信息 |
#### Memory 记忆 (5)
| 工具 | 功能 |
|------|------|
| `store_memory` | 存储记忆,支持 private/team/global 可见范围 |
| `recall_memory` | 语义搜索记忆 |
| `list_memories` | 列出记忆,支持范围和标签筛选 |
| `delete_memory` | 删除记忆 |
| `search_memories` | FTS5 全文搜索记忆,支持多关键词和短语搜索 |
#### Evolution 进化 (12)
| 工具 | 功能 |
|------|------|
| `share_experience` | 分享经验(无需审批,直接发布) |
| `propose_strategy` | 提议策略(需 admin 审批) |
| `propose_strategy_tiered` | 提议策略(4 级自动分级审批:auto/peer/admin/super |
| `list_strategies` | 列出策略,支持标签和类型筛选 |
| `search_strategies` | 全文搜索策略内容 |
| `apply_strategy` | 采纳策略,自动创建 feedback 占位,7 天无反馈自动降分 |
| `feedback_strategy` | 为已采纳策略提供反馈(positive/negative/neutral |
| `approve_strategy` | 审批通过策略(admin |
| `get_evolution_status` | 查看进化状态仪表盘 |
| `score_applied_strategies` | 自动评分已采纳策略:7 天前 neutral 反馈自动降为 negativeadmin |
| `check_veto_window` | 检查策略否决窗口状态 |
| `veto_strategy` | 在窗口期内撤回策略(admin) |
#### Orchestration 进阶编排 (16)
| 工具 | 功能 |
|------|------|
| `add_dependency` | 添加任务依赖关系(DFS 环检测) |
| `remove_dependency` | 删除任务依赖关系 |
| `get_task_dependencies` | 查询任务上下游依赖 |
| `create_parallel_group` | 创建并行任务组(2-10 个任务) |
| `request_handoff` | 请求任务交接 |
| `accept_handoff` | 接受任务交接 |
| `reject_handoff` | 拒绝任务交接(含理由) |
| `add_quality_gate` | 在 Pipeline 中添加质量门 |
| `evaluate_quality_gate` | 评估质量门(passed/failed |
| `set_agent_role` | 任命/撤销 Agent 角色,含 group_adminadmin |
| `recalculate_trust_scores` | 手动触发信任分重算(admin) |
| `create_pipeline` | 创建 Pipeline 流水线 |
| `get_pipeline` | 查询 Pipeline 详情 |
| `list_pipelines` | 列出 Pipeline |
| `add_task_to_pipeline` | 向 Pipeline 添加任务 |
#### Security 运维安全 (4)
| 工具 | 功能 |
|------|------|
| `get_db_stats` | 数据库统计信息(表行数、大小、Agent 数等)(admin |
| `archive_data` | 数据归档:将过期消息/审计日志移入归档表(admin) |
| (其余 2 个内部工具) | 权限验证与安全控制 |
#### Consume 消费水位线 (2)
| 工具 | 功能 |
|------|------|
| `mark_consumed` | 标记任务/消息为已消费,防止重复处理 |
| `check_consumed` | 查询资源是否已被消费 |
> 所有工具内置 try-catch + 3 次指数退避重试(100ms → 200ms → 400ms)。v2.4.0 统一错误格式:`HubError` 错误码 + `mcpError()`/`mcpFail()` 标准返回。`check_consumed` 查询失败时降级返回 `consumed=false`(不阻塞业务)。
### 运维 REST API
| 端点 | 方法 | 功能 |
|------|------|------|
| `/health` | GET | 健康检查(返回版本、内存、DB、大小、活跃 SSE 连接数) |
| `/metrics` | GET | Prometheus 兼容指标(mcp_calls_total、message_delivery_total 等) |
### 任务状态机
```
inbox → assigned → [waiting] → in_progress → completed / failed / cancelled
```
## 快速开始
### 1. 启动 Hub 服务器
```bash
git clone https://github.com/liuboacean/agent-comm-hub.git
cd agent-comm-hub
npm install
npm run build
npm start # HTTP 模式(port 3100
# 或
npm run stdio # stdio 模式(用于 MCP stdio transport
```
### 2. 配置 Agent 接入
**方式 A:MCP stdio 模式(推荐,适用于本地 Agent)**
```json
{
"mcpServers": {
"agent-comm-hub": {
"command": "node",
"args": ["./src/stdio.js"],
"env": {
"HUB_AUTH_TOKEN": "your-api-token",
"DB_PATH": "./comm_hub.db"
}
}
}
}
```
**方式 B:MCP HTTP 模式(适用于远程 Agent)**
```json
{
"mcpServers": {
"agent-comm-hub": {
"url": "http://localhost:3100/mcp"
}
}
}
```
**方式 CSDK 接入**
TypeScript Agent
```typescript
import { AgentClient } from "./client-sdk/agent-client.js";
const client = new AgentClient({
agentId: "my-agent",
hubUrl: "http://localhost:3100",
onTaskAssigned: async (task) => { /* 处理任务 */ },
onMessage: async (msg) => { /* 处理消息 */ },
});
await client.start();
```
Python Agent(零外部依赖):
```python
import asyncio
from hub_client import HubClient
client = HubClient(
agent_id="my-agent",
hub_url="http://localhost:3100",
on_task_assigned=lambda task: print(f"收到任务: {task['description']}"),
)
await client.start()
```
## 文件结构
```
agent-comm-hub/
├── SKILL.md # 本文件
├── README.md # 完整文档(GitHub 级别)
├── LICENSE # MIT 许可证
├── src/ # Hub 服务器核心(TypeScript
│ ├── server.ts # 主入口:Express + MCP + SSE
│ ├── stdio.ts # stdio 传输入口点(MCP v1.10+
│ ├── db.ts # SQLite 持久化层(WAL 模式,30 表)
│ ├── tools.ts # MCP 工具注册入口(~30 行,调度 8 模块)
│ ├── tools/ # 工具模块(Phase A 拆分)
│ │ ├── identity.ts # 身份工具(6)
│ │ ├── message.ts # 消息工具(5)
│ │ ├── memory.ts # 记忆工具(5)
│ │ ├── file.ts # 文件工具(3)
│ │ ├── evolution.ts # 进化工具(12)
│ │ ├── orchestrator.ts # 编排工具(16)
│ │ ├── security.ts # 安全工具(4)
│ │ └── consumed.ts # 消费工具(2)
│ ├── errors.ts # HubError 统一错误码(Phase D
│ ├── utils.ts # 工具函数:mcpError/mcpFail + dedup + hash
│ ├── types.ts # 全局类型定义(Phase D)
│ ├── identity.ts # Agent 身份 + trust_score + resolveAgentId
│ ├── evolution.ts # 进化引擎(策略 + feedback
│ ├── security.ts # RBAC + 权限矩阵
│ ├── sse.ts # SSE 连接管理
│ ├── logger.ts # 结构化 JSON 日志
│ ├── metrics.ts # Prometheus 指标
│ ├── dedup.ts # 消息去重(sha256
│ └── tokenizer.ts # N-gram 分词器(FTS5
├── client-sdk/ # SDKPython 68 方法 + TypeScript 35 方法)
├── deploy/ # 部署配置
│ ├── docker-compose.yml # Prometheus + Grafana 监控栈
│ ├── prometheus.yml # Prometheus 采集配置
│ └── grafana/ # Grafana 仪表盘 JSON
├── .github/workflows/ # CI/CDPhase C
│ └── ci.yml # typecheck + test + coverage
├── scripts/
│ ├── migrate_from_agent.js # 历史数据迁移(from_agent 规范化)
│ └── migrate_evolution_db.py # Evolution DB 迁移
├── tests/ # 单元测试(vitest 100 用例)+ Python 集成测试
└── docs/
├── SETUP_GUIDE.md # 详细配置指南
├── API_REFERENCE.md # API 参考
├── evolution-engine-guide.md # 进化引擎使用指南
└── TROUBLESHOOTING.md # 常见问题与踩坑经验
```
## 权限矩阵(4 级)
| 级别 | 说明 | 特殊权限 |
|------|------|----------|
| **public** | 无需认证 | register_agent |
| **member** | 已注册 Agent | 所有 Message/Task/Memory/File/Orchestration/Pipeline 工具 |
| **group_admin** | 并行组管理员 | 任务编排 + Pipeline 工具(不含 Memory/Evolution |
| **admin** | 系统管理员 | revoke_token / set_trust_score / approve_strategy / veto_strategy / set_agent_role / recalculate_trust_scores / score_applied_strategies / get_db_stats / archive_data |
> trust_score 初始值 50,公式:`base(50) + verified_capabilities*3 + approved_strategies*2 + positive_feedback*1 - negative_feedback*2`clamp(0,100)。
## v2.4.0 更新要点
| Phase | 内容 | 变更 |
|-------|------|------|
| **A** | tools.ts 拆分 | 2687 行 → 8 模块 + 30 行入口 + utils.ts |
| **B** | 单元测试 | 100 用例,security >= 70% / dedup branches≥60, functions≥70 / utils 100% |
| **C** | CI/CD | GitHub Actionstypecheck + test + coverage 3 Jobs |
| **D** | 类型安全 | any 归零 + HubError 统一错误码 + MCP 返回格式标准化 |
## 踩坑经验速查
| # | 场景 | 要点 |
|---|------|------|
| 1 | MCP 多 Client | 必须用 Stateless 模式,Stateful 只允许一个 Client |
| 2 | MCP Accept Header | 必须带 `Accept: application/json, text/event-stream` |
| 3 | MCP 响应格式 | SDK 返回 SSE 格式(`data: {...}`),不是纯 JSON |
| 4 | ESM 兼容 | 不能用 `require()`,用 `import()` 动态导入 |
| 5 | UTF-8 块读取 | httpx `resp.read(1)` 会截断多字节字符,用 `read(4096)` |
| 6 | SSE 心跳 | 10 秒间隔,服务端发 `: ping` |
| 7 | MCP != SSE | MCP 是工具调用通道(Agent→Hub),SSE 是推送通道(Hub→Agent |
| 8 | 离线补发 | 消息/任务存 SQLite,上线后 SSE 自动批量推送 |
| 9 | stdio 模式 | 所有日志走 stderrstdout 保留给 JSON-RPC |
| 10 | better-sqlite3 boolean | 绑定参数必须用 1/0,不能用 true/false |
| 11 | HubError 错误码 | v2.4.0 统一用 mcpError()/mcpFail(),不要手动构造错误响应 |
## 环境变量
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `PORT` | 3100 | Hub 监听端口(HTTP 模式) |
| `HUB_URL` | http://localhost:3100 | Hub 地址(客户端用) |
| `HUB_AUTH_TOKEN` | — | stdio 模式认证 token(必填) |
| `DB_PATH` | ./comm_hub.db | SQLite 数据库路径 |
| `LOG_LEVEL` | info | 日志级别:debug / info / warn / error |
| `CORS_ORIGINS` | (空) | CORS 白名单(逗号分隔),空=拒绝所有跨域 |
## 技术依赖
**Hub 服务器**
- Node.js 18+
- @modelcontextprotocol/sdk ^1.10.2(支持 StdioServerTransport
- express ^4.19
- better-sqlite3 ^11.9
- zod ^3.23
**Python 客户端(零外部依赖)**
- Python 3.9+(纯标准库:http.client / json / asyncio
+6
View File
@@ -0,0 +1,6 @@
{
"ownerId": "kn73qbrbqs4s8t2nh8pm22wxbd84vm7r",
"slug": "agent-comm-hub-mini",
"version": "2.4.0",
"publishedAt": 1777854103210
}
+33
View File
@@ -0,0 +1,33 @@
{
"name": "agent-comm-hub",
"version": "2.4.0",
"description": "WorkBuddy & Hermes 双向即时通讯 + 任务调度 MCP Hub",
"type": "module",
"license": "MIT",
"main": "src/server.js",
"scripts": {
"build": "tsc",
"dev": "tsx watch src/server.ts",
"start": "node src/server.js",
"stdio": "node src/stdio.js",
"test": "tsx scripts/test-e2e.ts",
"test:unit": "vitest run --coverage",
"test:unit:watch": "vitest"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.10.2",
"better-sqlite3": "^11.9.1",
"eventsource": "^4.1.0",
"express": "^4.19.2",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/express": "^4.17.21",
"@types/node": "^22.0.0",
"@vitest/coverage-v8": "^4.1.5",
"tsx": "^4.19.3",
"typescript": "^5.4.5",
"vitest": "^4.1.5"
}
}
@@ -0,0 +1,7 @@
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "agent-comm-hub",
"installedVersion": "2.4.2",
"installedAt": 1777860251665
}
+735
View File
@@ -0,0 +1,735 @@
# API Reference — Agent Comm Hub v2.3.0
> **版本**v2.3.0 | **日期**2026-04-29
> **MCP 工具总数**51 个
> **基础 URL**`http://localhost:3100`
---
## 概览
| 分类 | 工具数 | 权限 | Phase |
|------|--------|------|-------|
| Identity 身份 | 6 | public + member + admin | 1 + 5a |
| Message 消息 | 9 | member | 1 + Phase 2 |
| Task 任务 | 3 | member | 1 + 4a |
| Memory 记忆 | 5 | member | 1 + Phase 2 |
| Evolution 进化 | 12 | member + admin | 3 + 4b + Phase 2 |
| Orchestration 编排 | 12 | member | 4b |
| Pipeline 流水线 | 4 | member | 4a |
| Consume 消费水位线 | 2 | member | 1 |
---
## 1. Identity 身份管理
### register_agent
> **权限**public(无需认证)
注册新 Agent,获取 agent_id 和 API token。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `invite_code` | string | ✅ | 邀请码(通过 `/admin/invite/generate` 生成) |
| `name` | string | ✅ | Agent 名称 |
| `capabilities` | string[] | ❌ | Agent 能力标签列表 |
**返回**`{ agent_id, token, name, role }`
---
### heartbeat
> **权限**member
Agent 心跳上报,维持在线状态。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `agent_id` | string | ✅ | Agent ID |
**返回**`{ status: "ok", agent_id }`
---
### query_agents
> **权限**member
查询 Agent 列表,支持状态和角色筛选。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `status` | enum | ❌ | `online` / `offline` / `all`(默认 all |
| `role` | enum | ❌ | `admin` / `member` |
**返回**`{ agents: [{ agent_id, name, role, status, last_heartbeat, trust_score }] }`
---
### get_online_agents
> **权限**member
获取当前在线 Agent 列表。
| 参数 | 无 |
**返回**`{ online_agents: ["agent-id-1", "agent-id-2"] }`
---
### revoke_token
> **权限**admin
吊销指定 Agent 的 API token。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `token_id` | string | ✅ | 要吊销的 Token ID |
**返回**`{ success: true }`
---
### set_trust_score
> **权限**admin
调整 Agent 信任分数。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `agent_id` | string | ✅ | 目标 Agent ID |
| `delta` | number | ✅ | 信任分增量(-100 ~ +100 |
**返回**`{ success: true, new_score: number }`
---
### set_agent_role ⭐ Phase 5a
> **权限****admin**
任命/撤销 Agent 角色(含 group_admin)。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `agent_id` | string | ✅ | 目标 Agent ID |
| `role` | enum | ✅ | `admin` / `member` / `group_admin` |
| `managed_group_id` | string | ❌ | 管理的 parallel_group ID(仅 group_admin 时可选) |
**安全约束**
- 不能修改自己的角色
- 非 admin 不能被提升为 admin
- 变更后自动同步 `auth_tokens.role`
- 操作写入审计日志
**返回**`{ success: true, old_role, new_role, managed_group_id }`
---
### recalculate_trust_scores ⭐ Phase 5a
> **权限****admin**
手动触发信任分重算。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `agent_id` | string | ❌ | 指定 Agent ID(不传则全部重算) |
**信任评分公式**
```
base = 50
+ verified_capabilities × 3
+ approved_strategies × 2
+ positive_feedback(排除自评)× 1
- negative_feedback × 2
- rejected_applications × 3
- revoked_tokens × 10
→ clamp(0, 100)
```
**返回**`{ recalculated: number, agents_affected: number }`
---
## 2. Message 消息
### send_message
> **权限**member
发送点对点消息。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `from` | string | ✅ | 发送方 Agent ID |
| `to` | string | ✅ | 接收方 Agent ID |
| `content` | string | ✅ | 消息正文,支持 Markdown |
| `type` | string | ❌ | 消息类型(默认 "message" |
| `metadata` | object | ❌ | 附加元数据 |
**返回**`{ message_id, from, to, created_at }`
**特性**:自动去重(sha256 hash)、SSE 实时推送
---
### broadcast_message
> **权限**member
群发消息给多个 Agent。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `from` | string | ✅ | 发送方 Agent ID |
| `agent_ids` | string[] | ✅ | 接收方 Agent ID 列表 |
| `content` | string | ✅ | 消息正文 |
| `metadata` | object | ❌ | 附加元数据 |
---
### acknowledge_message
> **权限**member
确认已读消息。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `message_id` | string | ✅ | 消息 ID |
| `agent_id` | string | ✅ | 确认者 Agent ID |
---
### mark_consumed
> **权限**member
标记任务消息为已消费(处理完成)。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `message_id` | string | ✅ | 消息 ID |
| `agent_id` | string | ✅ | 消费者 Agent ID |
| `task_id` | string | ✅ | 关联任务 ID |
| `status` | string | ✅ | 消费状态 |
---
### check_consumed
> **权限**member
检查消息是否已被消费。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `message_id` | string | ✅ | 消息 ID |
| `agent_id` | string | ✅ | Agent ID |
---
## 3. Task 任务
### assign_task
> **权限**member
创建并分配任务。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `from` | string | ✅ | 发起方 Agent ID |
| `to` | string | ✅ | 执行方 Agent ID |
| `description` | string | ✅ | 任务描述(含期望输出格式) |
| `context` | string | ❌ | 附加上下文 |
**返回**`{ task_id, status: "assigned" }`
---
### update_task_status
> **权限**member
更新任务状态。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `task_id` | string | ✅ | 任务 ID |
| `agent_id` | string | ✅ | 操作者 Agent ID |
| `status` | enum | ✅ | `in_progress` / `completed` / `failed` |
| `result` | string | ❌ | 完成结果说明 |
**状态机**`inbox → assigned → [waiting] → in_progress → completed / failed / cancelled`
---
### get_task_status
> **权限**member
查询任务详情。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `task_id` | string | ✅ | 任务 ID |
**返回**:完整任务对象(含 status、assigned_to、dependencies、handoff 等)
---
## 4. Memory 记忆
### store_memory
> **权限**member
存储记忆。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `agent_id` | string | ✅ | Agent ID |
| `content` | string | ✅ | 记忆内容(最多 10000 字符) |
| `scope` | enum | ✅ | `private` / `team` / `global` |
| `tags` | string[] | ❌ | 标签列表 |
---
### recall_memory
> **权限**member
搜索记忆。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `agent_id` | string | ✅ | Agent ID |
| `query` | string | ✅ | 搜索关键词 |
| `limit` | number | ❌ | 返回数量(默认 10) |
---
### list_memories
> **权限**member
列出记忆。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `scope` | enum | ❌ | 可见范围筛选 |
| `limit` | number | ❌ | 返回数量 |
---
### delete_memory
> **权限**member
删除记忆。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `memory_id` | string | ✅ | 记忆 ID |
---
## 5. Evolution 进化引擎
### share_experience
> **权限**member
分享经验(无需审批,直接发布)。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `title` | string | ✅ | 经验标题(3-200 字符) |
| `content` | string | ✅ | Markdown 内容(10-5000 字符) |
| `category` | enum | ✅ | 固定为 `experience` |
| `tags` | string[] | ❌ | 标签列表(最多 10 个) |
---
### propose_strategy
> **权限**member
提议策略(需 admin 审批,等同于 tier=admin)。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `title` | string | ✅ | 策略标题(3-200 字符) |
| `content` | string | ✅ | Markdown 内容(10-5000 字符) |
| `category` | enum | ✅ | `workflow` / `fix` / `tool_config` / `prompt_template` / `other` |
---
### propose_strategy_tiered ⭐ Phase 4b
> **权限**member
提议策略(支持 4 级自动分级审批)。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `title` | string | ✅ | 策略标题(3-200 字符) |
| `content` | string | ✅ | Markdown 内容(10-5000 字符) |
| `category` | enum | ✅ | `workflow` / `fix` / `tool_config` / `prompt_template` / `other` |
| `tier` | enum | ❌ | 强制指定 tier`auto` / `peer` / `admin` / `super` |
| `task_id` | string | ❌ | 关联任务 ID |
**自动判定规则**
| Tier | 条件 |
|------|------|
| `auto` | trust≥90 + normal + history≥5 |
| `peer` | trust≥60 + normal + history≥2 |
| `admin` | 默认 |
| `super` | high sensitivity + trust<80 |
---
### check_veto_window ⭐ Phase 4b
> **权限**member
检查策略时间窗口状态。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `strategy_id` | number | ✅ | 策略 ID |
**返回**`{ in_window, window_type, deadline, negative_count, positive_count, can_revoke }`
---
### veto_strategy ⭐ Phase 4b
> **权限****admin**
在窗口期内撤回策略。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `strategy_id` | number | ✅ | 策略 ID |
| `reason` | string | ✅ | 撤回理由(最多 1000 字符) |
---
### list_strategies / search_strategies / apply_strategy / feedback_strategy / approve_strategy / get_evolution_status
详见 [Evolution Engine 使用指南](./docs/evolution-engine-guide.md)
---
## 6. Orchestration 进阶编排 ⭐ Phase 4b
### add_dependency
> **权限**member
添加任务依赖关系。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `upstream_id` | string | ✅ | 上游任务 ID(需先完成) |
| `downstream_id` | string | ✅ | 下游任务 ID |
| `dep_type` | enum | ❌ | `finish_to_start`(默认)/ `start_to_start` / `finish_to_finish` |
**自动行为**:DFS 环检测 + 自动评估下游任务 waiting 状态
---
### remove_dependency
> **权限**member
删除依赖关系。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `upstream_id` | string | ✅ | 上游任务 ID |
| `downstream_id` | string | ✅ | 下游任务 ID |
---
### get_task_dependencies
> **权限**member
查询任务的上下游依赖。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `task_id` | string | ✅ | 任务 ID |
**返回**`{ upstreams: [...], downstreams: [...] }`
---
### create_parallel_group
> **权限**member
创建并行任务组。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `task_ids` | string[] | ✅ | 任务 ID 列表(2-10 个) |
| `group_name` | string | ❌ | 并行组名称 |
---
### request_handoff
> **权限**member
请求任务交接。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `task_id` | string | ✅ | 任务 ID |
| `target_agent_id` | string | ✅ | 目标 Agent ID |
---
### accept_handoff
> **权限**member
接受任务交接。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `task_id` | string | ✅ | 任务 ID |
---
### reject_handoff
> **权限**member
拒绝任务交接。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `task_id` | string | ✅ | 任务 ID |
| `reason` | string | ❌ | 拒绝原因 |
---
### add_quality_gate
> **权限**member
在 Pipeline 中添加质量门。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `pipeline_id` | string | ✅ | Pipeline ID |
| `gate_name` | string | ✅ | 质量门名称 |
| `criteria` | string | ✅ | 评估规则(JSON 格式) |
| `after_order` | number | ❌ | 在此 order_index 后检查 |
---
### evaluate_quality_gate
> **权限**member
评估质量门。
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `gate_id` | string | ✅ | 质量门 ID |
| `status` | enum | ✅ | `passed` / `failed` |
| `result` | string | ❌ | 评估说明 |
---
## 7. 权限矩阵
| 工具 | public | member | group_admin | admin |
|------|--------|--------|-------------|-------|
| register_agent | ✅ | ✅ | ✅ | ✅ |
| heartbeat | — | ✅ | ✅ | ✅ |
| query_agents | — | ✅ | ✅ | ✅ |
| get_online_agents | — | ✅ | ✅ | ✅ |
| send_message | — | ✅ | ✅ | ✅ |
| assign_task | — | ✅ | ✅ | ✅ |
| update_task_status | — | ✅ | ✅ | ✅ |
| get_task_status | — | ✅ | ✅ | ✅ |
| broadcast_message | — | ✅ | ✅ | ✅ |
| acknowledge_message | — | ✅ | ✅ | ✅ |
| mark_consumed | — | ✅ | ✅ | ✅ |
| check_consumed | — | ✅ | ✅ | ✅ |
| store_memory | — | ✅ | — | ✅ |
| recall_memory | — | ✅ | — | ✅ |
| list_memories | — | ✅ | — | ✅ |
| delete_memory | — | ✅ | — | ✅ |
| share_experience | — | ✅ | — | ✅ |
| propose_strategy | — | ✅ | — | ✅ |
| list_strategies | — | ✅ | — | ✅ |
| search_strategies | — | ✅ | — | ✅ |
| apply_strategy | — | ✅ | — | ✅ |
| feedback_strategy | — | ✅ | — | ✅ |
| get_evolution_status | — | ✅ | — | ✅ |
| add_dependency | — | ✅ | ✅ | ✅ |
| remove_dependency | — | ✅ | ✅ | ✅ |
| get_task_dependencies | — | ✅ | ✅ | ✅ |
| create_parallel_group | — | ✅ | ✅ | ✅ |
| request_handoff | — | ✅ | ✅ | ✅ |
| accept_handoff | — | ✅ | ✅ | ✅ |
| reject_handoff | — | ✅ | ✅ | ✅ |
| add_quality_gate | — | ✅ | ✅ | ✅ |
| evaluate_quality_gate | — | ✅ | ✅ | ✅ |
| propose_strategy_tiered | — | ✅ | — | ✅ |
| check_veto_window | — | ✅ | — | ✅ |
| **revoke_token** | — | — | — | **✅** |
| **set_trust_score** | — | — | — | **✅** |
| **approve_strategy** | — | — | — | **✅** |
| **veto_strategy** | — | — | — | **✅** |
| **set_agent_role** ⭐5a | — | — | — | **✅** |
| **recalculate_trust_scores** ⭐5a | — | — | — | **✅** |
> **group_admin**:等同于 member + 可管理所属 parallel_group 内任务。不可操作记忆/策略/消息/evolution 工具。
---
## 8. SSE 事件
| 事件 | 触发时机 | 推送目标 |
|------|---------|---------|
| `message` | 收到新消息 | 接收方 |
| `task_assigned` | 任务被分配 | 执行方 |
| `task_completed` | 任务完成 | 发起方 |
| `strategy_approved` | 策略审批通过 | 提议者 |
| `handoff_requested` | 交接请求 | 接收方 |
| `handoff_accepted` | 交接接受 | 原负责人 |
| `handoff_rejected` | 交接拒绝 | 原负责人 |
| `quality_gate_failed` | 质量门未通过 | Pipeline 参与者 |
| `hub_shutdown` | 服务器即将关闭 | 所有 SSE 客户端 |
---
## 9. 运维端点(Phase 5b 新增)
### GET /health
> **权限**public(免认证)
> **内容类型**application/json
增强健康检查端点,返回服务完整状态。
**响应示例**
```json
{
"status": "ok",
"version": "2.2.0",
"uptime": 1234.56,
"timestamp": 1745594400000,
"memory": {
"rss": 45,
"heap_used": 28,
"heap_total": 35
},
"db": {
"size": 524288,
"tables": 16
},
"sse": {
"active_connections": 2
}
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `status` | string | `"ok"` |
| `version` | string | Hub 版本号 |
| `uptime` | number | 运行时长(秒) |
| `timestamp` | number | 当前时间戳(ms |
| `memory.rss` | number | RSS 内存(MB |
| `memory.heap_used` | number | 堆使用(MB |
| `memory.heap_total` | number | 堆总量(MB |
| `db.size` | number | 数据库文件大小(bytes |
| `db.tables` | number | 数据库表数量 |
| `sse.active_connections` | number | SSE 活跃连接数 |
---
### GET /metrics
> **权限**public(免认证)
> **内容类型**text/plain; version=0.0.4
Prometheus 兼容指标端点。
**可用指标**
| 指标 | 类型 | 标签 | 说明 |
|------|------|------|------|
| `mcp_calls_total` | Counter | tool_name, status, role | MCP 工具调用计数 |
| `active_sse_connections` | Gauge | — | 当前 SSE 活跃连接数 |
| `message_delivery_total` | Counter | status (delivered/queued/failed) | 消息投递计数 |
| `http_requests_total` | Counter | method, path, status | HTTP 请求计数 |
| `http_request_duration_ms` | Histogram | method, path | 请求耗时分布 |
| `db_query_duration_ms` | Histogram | operation | DB 查询耗时 |
---
### Phase 5b 新增环境变量
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `LOG_LEVEL` | `info` | 日志级别:debug / info / warn / error |
| `CORS_ORIGINS` | `` (空) | CORS 白名单(逗号分隔),空=拒绝所有跨域 |
### Phase 5b 新增 HTTP 行为
| 特性 | 说明 |
|------|------|
| 结构化日志 | 所有日志输出 JSON 到 stdout,通过 `LOG_LEVEL` 过滤 |
| CORS 白名单 | 默认拒绝所有跨域,需显式配置 `CORS_ORIGINS` |
| 安全头 | X-Frame-Options / X-Content-Type-Options / X-XSS-Protection / HSTS / CSP |
| 请求追踪 | 每请求自动生成 traceId,响应头 `X-Trace-Id` |
| 404 JSON | 未匹配路由返回 `{error, message, traceId}` |
| 优雅关闭 | SIGTERM/SIGINT 后 drain SSE → 关闭 DB → 退出 |
---
## 10. 数据模型概览
| 表 | Phase | 说明 |
|------|-------|------|
| agents | 0.5+5a | Agent 注册信息 + 信任分 + managed_group_id |
| messages | 1 | 消息表(去重 hash |
| tasks | 1+4a+4b | 任务表(21 列,含 parallel_group、handoff_to |
| pipelines | 4a | Pipeline 容器 |
| pipeline_tasks | 4a | Pipeline-Task 关联 |
| memories | 1 | Agent 记忆 |
| strategies | 3 | 策略(含 approval_tier、观察/否决窗口) |
| strategy_feedback | 3 | 策略反馈(防刷) |
| strategy_applications | 3 | 策略采纳记录 |
| agent_capabilities | 1 | Agent 能力标签 |
| audit_log | 2+5a | 审计日志(含哈希链 prev_hash/record_hash + 写保护触发器) |
| auth_tokens | 1 | 认证 token |
| consumed_log | 1 | 消息消费记录 |
| dedup_cache | 2 | 消息去重缓存 |
| sender_nonces | 2 | 发送方 nonce |
| task_dependencies | **4b** | 任务依赖关系 |
| quality_gates | **4b** | Pipeline 质量门 |
| attachments | **Phase 1** | 文件附件(Base64 存储,10MB 限制) |
---
*文档版本:v2.3.0 | 最后更新:2026-04-29Phase 2 完结:stdio transport + from_agent 规范化 + 策略闭环 + 51 工具)*
+186
View File
@@ -0,0 +1,186 @@
# Agent Communication Hub — Hermes 接入配置指南
## Hermes 需要做什么?
**总工作量:3 步,约 10 分钟。**
---
## 步骤 1:确保 Hub 已启动
在 WorkBuddy 所在机器上启动 Hub Server
```bash
cd agent-comm-hub
npm install
npm run dev
# 输出: Agent Communication Hub v1.0.0 — 监听端口 3100
```
> Hub 只需要在一台机器上运行。如果 Hermes 在另一台机器上,需要确认网络互通。
---
## 步骤 2:将客户端代码复制到 Hermes 项目
```
需要复制的文件(共 1 个):
┌──────────────────────────────────────────┐
│ client-sdk/agent-client.ts │
│ (通用客户端 SDKWorkBuddy/Hermes 通用) │
└──────────────────────────────────────────┘
复制到 Hermes 项目的任意位置,例如:
hermes-project/libs/agent-client.ts
```
---
## 步骤 3:在 Hermes 启动入口中加入 3 行代码
在 Hermes 的主入口文件(如 `index.ts``app.ts``main.ts`)中添加:
```typescript
// ─── 接入 Agent Communication Hub ─────────────────
import { AgentClient } from "./libs/agent-client.js";
const hermes = new AgentClient({
agentId: "hermes", // 你的 Agent ID,可自定义
hubUrl: process.env.HUB_URL ?? "http://192.168.1.100:3100", // Hub 地址
// 收到任务 → 自主执行 → 回报结果
onTaskAssigned: async (task) => {
console.log(`收到任务: ${task.description}`);
// ① 告知发起方"已开始"
await hermes.updateTaskStatus(task.id, "in_progress", undefined, 5);
// ② 调用你的核心业务逻辑
const result = await yourHermesBusinessLogic(task.description, task.context);
// ③ 汇报完成
await hermes.updateTaskStatus(task.id, "completed", result, 100);
},
// 收到消息 → 处理
onMessage: async (msg) => {
console.log(`来自 ${msg.from_agent}: ${msg.content}`);
},
});
// 启动(在 Hermes 主逻辑之前调用)
hermes.start();
// ─── 接入结束 ─────────────────────────────────────
```
---
## 步骤 4(可选):设置环境变量
在 Hermes 的 `.env` 或启动命令中设置:
```bash
# Hermes 的 Agent ID(默认 hermes
export HERMES_ID=hermes
# Hub Server 地址
# 本机: http://localhost:3100
# 远程: http://192.168.1.100:3100
export HUB_URL=http://localhost:3100
```
---
## 步骤 5(可选):在 Hermes 的 .mcp.json 中配置 Hub 工具
如果 Hermes 也通过 MCP 协议调用 Hub 工具,在其 `.mcp.json` 中添加:
```json
{
"mcpServers": {
"agent-comm-hub": {
"url": "http://localhost:3100/mcp"
}
}
}
```
> 这样 Hermes 的 AgentLLM)也可以直接调用 `send_message`、`assign_task` 等工具。
> 如果只通过 `AgentClient` SDK 调用,这一步不是必须的。
---
## 验证接入是否成功
### 方法 1:观察日志
Hermes 启动后,Hub 侧应看到:
```
[SSE] ✅ hermes online. Total: 1
```
### 方法 2:健康检查
```bash
# 从 Hermes 机器上测试 Hub 连通性
curl http://192.168.1.100:3100/health
# 预期: {"status":"ok","uptime":123.456,"ts":...}
```
### 方法 3:运行端到端测试
```bash
# 在 Hub 机器上运行
npm test
# 预期: ✅ 全部测试通过
```
---
## 常见问题
### Q: Hermes 重启后会丢失消息吗?
**不会。** 所有消息和任务都持久化在 SQLite 中。Hermes 重启后重新建立 SSE 连接,Hub 会自动补发积压的未读消息和未执行任务。
### Q: Hub 挂了怎么办?
消息写入 SQLite 不会丢失。Hub 重启后,Hermes 的 SSE 客户端会自动重连(默认 3 秒间隔)。
### Q: Hermes 不需要 LLM 也能执行任务吗?
**不需要。** `onTaskAssigned` 回调是你的代码直接执行的,不走 LLM。当然你可以在回调中调用 LLM,但那由你决定。
### Q: 能同时支持更多 Agent 吗?
可以,每个 Agent 用不同的 `agentId` 即可。Hub 会自动管理所有连接。
### Q: 如何调试?
```bash
# 查看 Hub 日志
npm run dev # 控制台直接看输出
# 查看 SQLite 中的消息记录
sqlite3 comm_hub.db "SELECT * FROM messages ORDER BY created_at DESC LIMIT 10;"
sqlite3 comm_hub.db "SELECT * FROM tasks ORDER BY created_at DESC LIMIT 10;"
```
---
## 完整文件清单
```
agent-comm-hub/
├── package.json # 依赖配置
├── tsconfig.json # TypeScript 配置
├── src/
│ ├── server.ts # 主入口(Express + MCP + SSE
│ ├── db.ts # SQLite 持久化层
│ ├── sse.ts # SSE 连接管理
│ └── tools.ts # 6 个 MCP 工具定义
├── client-sdk/
│ ├── agent-client.ts # 通用客户端 SDK(Hermes 只需此文件)
│ ├── workbuddy-integration.ts # WorkBuddy 接入示例
│ └── hermes-integration.ts # Hermes 接入示例
├── scripts/
│ ├── install.sh # 一键安装脚本
│ ├── test-e2e.ts # 端到端测试
│ └── test-e2e.sh # 全栈启动脚本
└── HERMES-SETUP.md # 本文档
```
+235
View File
@@ -0,0 +1,235 @@
<p align="center">
<strong>Agent Communication Hub</strong><br>
多智能体协同通信基础设施<br>
<em>共享记忆,共同进化</em>
</p>
<p align="center">
<img src="https://img.shields.io/badge/MCP_Tools-53-blue" alt="53 MCP Tools">
<img src="https://img.shields.io/badge/RBAC-4_Levels-green" alt="4-Level RBAC">
<img src="https://img.shields.io/badge/Python_SDK-0_Dependencies-brightgreen" alt="Zero Dependencies">
<img src="https://img.shields.io/badge/Protocol-MCP+%2B+SSE-orange" alt="MCP + SSE">
<img src="https://img.shields.io/badge/License-MIT-yellow" alt="MIT License">
<img src="https://img.shields.io/badge/build-passing-brightgreen" alt="CI Build">
</p>
<p align="center">
<a href="#快速开始">快速开始</a> ·
<a href="#核心能力">核心能力</a> ·
<a href="#安装方式">安装方式</a> ·
<a href="docs/API_REFERENCE.md">API 文档</a> ·
<a href="docs/TROUBLESHOOTING.md">踩坑经验</a>
</p>
---
## 它是什么
让两个或多个独立 AI 智能体实现**实时双向通信**、**任务自动调度**、**记忆共享**和**协同进化**。
基于 MCP 协议 + SSE 推送,SQLite WAL 持久化,消息零丢失,延迟 < 50ms。
> **注意**:本仓库是 Hub 的 **Skill 分发包**SDK + 文档 + 安装脚本),不包含服务端源码。Hub 服务端是一个独立的 Node.js 项目,通过 `install.sh` 自动从 GitHub 克隆并构建。
```
┌──────────────┐ ┌──────────────────────────┐ ┌──────────────┐
│ Agent A │ SSE │ Agent Communication │ SSE │ Agent B │
│ (Hermes) │◄───────►│ Hub v2.4 │◄───────►│ (WorkBuddy) │
│ │ MCP │ (localhost:3100) │ MCP │ │
└──────────────┘◄───────►│ │◄───────►└──────────────┘
└──────────┬───────────────┘
SQLite (WAL)
```
支持任意 MCP 兼容 Agent 接入:WorkBuddy、Hermes、QClaw、Claude Code、OpenClaw 等。
## 核心能力
| 模块 | 工具数 | 说明 |
|------|--------|------|
| **Identity 身份** | 6 | 注册、心跳、在线查询、角色管理、信任评分 |
| **Message 消息** | 5 | 点对点/群发、全文搜索、消费水位线 |
| **Task 任务** | 8 | 7 状态状态机、Pipeline 线性容器、自动通知 |
| **Memory 记忆** | 5 | private/team/global 三级、FTS5 搜索、边缘函数评测 |
| **Evolution 进化** | 12 | 经验分享、4 级分级审批、策略采纳、信任评分联动 |
| **Orchestration 编排** | 11 | 依赖链(DFS 环检测)、并行组、交接协议、质量门、Pipeline |
| **Security 安全** | 6 | Token 管理、RBAC、审计哈希链、信任分自动化 |
| **File 文件** | 3 | 文件上传/下载/列表,Base64 最大 10MB |
| **Consumed 水位线** | 2 | mark_consumed、check_consumed |
| **Errors 错误码** | 3 | HubErrorCode 枚举,20+ 结构化错误码 |
**共计 53 个 MCP 工具**,详见 [API_REFERENCE.md](docs/API_REFERENCE.md)
## 权限模型
| 角色 | 说明 | 能力 |
|------|------|------|
| **public** | 未认证 | 仅 `register_agent` |
| **member** | 已注册 Agent | 全部工具(除 admin 专属) |
| **group_admin** | 并行组管理员 | member + 管理所属 parallel_group |
| **admin** | 系统管理员 | 全部工具 + 角色任命 + 信任分调整 |
## 安全特性
- **RBAC 权限**public / member / group_admin / admin 四级
- **审计哈希链**`audit_log``prev_hash → record_hash`,触发器写保护
- **信任评分**:多维度自动计算,影响策略审批 tier
- **CORS 白名单**:默认拒绝跨域
- **安全响应头**X-Frame-Options / CSP / HSTS / X-XSS-Protection
- **请求追踪**:每请求 traceId,响应头 X-Trace-Id
- **优雅关闭**SIGTERM → drain SSE → 关闭 DB → 退出
## 快速开始
### 1. 安装 Hub 服务器
```bash
# 从 GitHub 克隆 + 构建
git clone https://github.com/liuboacean/agent-comm-hub.git ~/agent-comm-hub
cd ~/agent-comm-hub
npm install && npm run build
npm start # 生产模式,端口 3100
# 或 npm run dev # 开发模式(热重载)
```
### 2. 注册 Agent
```python
# 通过 MCP 工具 register_agent(需邀请码)
# 或使用 SDK
from hub_client import SynergyHubClient
hub = SynergyHubClient(hub_url="http://localhost:3100", agent_id="my-agent")
result = hub.register(invite_code="YOUR_INVITE_CODE")
print(result) # agent_id + api_token
```
### 3. 配置 MCP 连接
在 Agent 的 MCP 配置中添加:
```json
{
"mcpServers": {
"agent-comm-hub": {
"url": "http://localhost:3100/mcp"
}
}
}
```
Agent 的 LLM 可以直接调用全部 53 个工具。
### 4. SDK 接入(可选)
**Python(零外部依赖)**
```python
from hub_client import SynergyHubClient
hub = SynergyHubClient(hub_url="http://localhost:3100", agent_id="my-agent")
hub.set_token("your-api-token")
hub.heartbeat()
hub.send_message(to="other-agent", content="Hello!")
hub.store_memory(content="重要信息", scope="collective")
hub.share_experience(title="踩坑记录", content="...", category="experience")
hub.on_message = lambda msg: print(f"收到: {msg}")
hub.connect_sse() # 阻塞,SSE 长连接
```
**TypeScript**
```typescript
import { AgentClient } from "./client-sdk/agent-client.js";
const client = new AgentClient({
agentId: "my-agent",
hubUrl: "http://localhost:3100",
onTaskAssigned: async (task) => { /* 处理任务 */ },
onMessage: async (msg) => { /* 处理消息 */ },
});
await client.start();
```
### 5. 验证
```bash
curl http://localhost:3100/health # 健康检查
curl http://localhost:3100/metrics # Prometheus 指标
```
## 安装方式
### 作为 Skill 安装(推荐)
将本仓库作为 Skill 安装到你的 Agent 平台,即可获得 53 个 MCP 工具 + SDK + 完整文档:
```bash
# SkillHub — 覆盖 30+ Agent 平台(Claude Code、OpenClaw、CodeBuddy 等)
npx skills add liuboacean/agent-comm-hub
# ClawHub
clawhub install agent-comm-hub
```
### 手动安装
```bash
git clone https://github.com/liuboacean/agent-comm-hub.git
cd agent-comm-hub
# 查看 docs/SETUP_GUIDE.md 了解详细部署步骤
```
## 文件结构
```
agent-comm-hub/
├── SKILL.md # Skill 核心文档(Agent 加载时读取)
├── scripts/
│ ├── install.sh # 一键安装 Hub 服务器
│ └── setup_agent.sh # Agent 注册 + 认证自动化
├── client-sdk/
│ ├── hub_client.py # Python SDK68 个方法,零依赖)
│ ├── agent-client.ts # TypeScript SDK35 个公开方法)
│ └── agent-client.js # 编译后的 JS
├── docs/
│ ├── API_REFERENCE.md # 53 个工具完整参考 v2.4
│ ├── SETUP_GUIDE.md # 详细部署指南
│ ├── TROUBLESHOOTING.md # 踩坑经验(8 大类)
│ ├── orchestrator-guide.md # 进阶编排指南
│ ├── evolution-guide.md # 进化引擎指南
│ └── hermes-integration-guide.md # Hermes 集成指南
└── examples/
├── workbuddy-mcp.json # WorkBuddy MCP 配置示例
├── hermes-mcp.json # Hermes MCP 配置示例
└── agent_bridge.py # 通用通信桥示例
```
## 技术依赖
| 组件 | 依赖 |
|------|------|
| **Hub 服务器** | Node.js 18+、@modelcontextprotocol/sdk、express、better-sqlite3、zod |
| **Python SDK** | Python 3.9+,零外部依赖(纯标准库) |
| **TS SDK** | Node.js 18+,零外部依赖(原生 fetch |
## 环境变量
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `PORT` | 3100 | Hub 监听端口 |
| `LOG_LEVEL` | info | 日志级别:debug / info / warn / error |
| `CORS_ORIGINS` | (空) | CORS 白名单(逗号分隔),空=拒绝所有跨域 |
## 文档
| 文档 | 说明 |
|------|------|
| [API_REFERENCE.md](docs/API_REFERENCE.md) | 53 个 MCP 工具完整参考 |
| [SETUP_GUIDE.md](docs/SETUP_GUIDE.md) | 从零部署指南 |
| [TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) | 踩坑经验速查 |
| [orchestrator-guide.md](docs/orchestrator-guide.md) | 进阶编排(依赖链/并行组/质量门) |
| [evolution-guide.md](docs/evolution-guide.md) | 进化引擎(经验/策略/信任评分) |
| [hermes-integration-guide.md](docs/hermes-integration-guide.md) | Hermes Agent 集成指南 |
## 许可
MIT
+376
View File
@@ -0,0 +1,376 @@
---
name: agent-comm-hub
description: "多智能体协同通信基础设施——基于 MCP+SSE 的实时消息、任务调度、记忆共享与进化引擎。支持 WorkBuddy、Hermes、QClaw 及任意 MCP 兼容 Agent 接入。53 个 MCP 工具、4 级权限、零外部依赖 Python SDK。触发词:agent通信、智能体通信、hub通信、多智能体、跨agent通信、任务调度、assign_task、send_message、hermes通信、workbuddy通信、agent hub、通信hub、mcp通信、记忆共享、进化引擎、策略共享、经验分享、共享记忆,共同进化"
version: 2.4.2
category: autonomous-ai-agents
---
# Agent Communication Hub
> 多智能体实时通信与任务调度基础设施 — **v2.4.2**
让两个或多个独立 AI 智能体之间实现**实时双向通信**、**任务自动调度**、**记忆共享**和**策略进化**。基于 MCP 协议 + SSE 推送,消息零丢失,延迟 < 50ms。
## 架构概览
```
┌──────────────┐ ┌──────────────────────────────┐ ┌──────────────┐
│ Agent A │ SSE │ Agent Communication Hub │ SSE │ Agent B │
│ (Hermes) │◄───────►│ (stdio / HTTP:3100) │◄───────►│ (WorkBuddy) │
│ │ MCP │ │ MCP │ │
└──────────────┘◄───────►│ SQLite WAL + 30 表 │◄───────►└──────────────┘
│ 53 MCP 工具 + 4 级权限 │
│ 进化引擎 + 策略闭环 │
└──────────────┬──────────────┘
SQLite (WAL)
```
**三层协议**
| 层 | 协议 | 用途 | 延迟 |
|----|------|------|------|
| MCP 工具层 | stdio / HTTP POST + JSON-RPC | 结构化操作(发消息、分配任务、查状态) | <50ms |
| SSE 推送层 | Server-Sent Events | 实时事件通知(新消息、新任务、策略审批) | <50ms |
| REST API 层 | HTTP GET/PATCH | 轻量查询(运维监控、自动化脚本) | <50ms |
## 核心能力
### 53 个 MCP 工具(v2.4.2
#### Identity 身份 (6)
| 工具 | 功能 |
|------|------|
| `register_agent` | 注册新 Agent,获取 agent_id 和 API tokenpublic,无需认证) |
| `heartbeat` | Agent 心跳上报,维持在线状态,每 3 次连续心跳 trust_score +1 |
| `query_agents` | 查询 Agent 列表,支持状态/角色筛选 |
| `revoke_token` | 吊销指定 Agent 的 API tokenadmin |
| `set_trust_score` | 调整 Agent 信任分数(admin |
| `get_online_agents` | 获取当前在线 Agent 列表 |
#### Message 消息 (5)
| 工具 | 功能 |
|------|------|
| `send_message` | Agent 间点对点消息,支持 Markdown,自动去重(sha256 |
| `broadcast_message` | 群发消息给多个 Agent |
| `acknowledge_message` | 确认已读消息,防止重复出现 |
| `search_messages` | 全文搜索消息历史 |
| `batch_acknowledge_messages` | 批量确认消息(1-500 条/次),用于清理消息积压 |
#### File 文件 (3)
| 工具 | 功能 |
|------|------|
| `upload_file` | 上传文件附件(Base64,10MB 限制),关联到消息 |
| `download_file` | 下载附件,返回 Base64 编码内容 |
| `list_attachments` | 列出附件,支持按消息/Agent 筛选 |
#### Task 任务 (3)
| 工具 | 功能 |
|------|------|
| `assign_task` | 创建并分配任务,支持上下文传递 |
| `update_task_status` | 更新任务状态(inbox→assigned→in_progress→completed/failed |
| `get_task_status` | 查询任务详情,含依赖、Pipeline、Handoff 信息 |
#### Memory 记忆 (5)
| 工具 | 功能 |
|------|------|
| `store_memory` | 存储记忆,支持 private/team/global 可见范围 |
| `recall_memory` | 语义搜索记忆 |
| `list_memories` | 列出记忆,支持范围和标签筛选 |
| `delete_memory` | 删除记忆 |
| `search_memories` | FTS5 全文搜索记忆,支持多关键词和短语搜索 |
#### Evolution 进化 (12)
| 工具 | 功能 |
|------|------|
| `share_experience` | 分享经验(无需审批,直接发布) |
| `propose_strategy` | 提议策略(需 admin 审批) |
| `propose_strategy_tiered` | 提议策略(4 级自动分级审批:auto/peer/admin/super |
| `list_strategies` | 列出策略,支持标签和类型筛选 |
| `search_strategies` | 全文搜索策略内容 |
| `apply_strategy` | 采纳策略,自动创建 feedback 占位,7 天无反馈自动降分 |
| `feedback_strategy` | 为已采纳策略提供反馈(positive/negative/neutral |
| `approve_strategy` | 审批通过策略(admin |
| `get_evolution_status` | 查看进化状态仪表盘 |
| `score_applied_strategies` | 自动评分已采纳策略:7 天前 neutral 反馈自动降为 negativeadmin |
| `check_veto_window` | 检查策略否决窗口状态 |
| `veto_strategy` | 在窗口期内撤回策略(admin) |
#### Orchestration 进阶编排 (16)
| 工具 | 功能 |
|------|------|
| `add_dependency` | 添加任务依赖关系(DFS 环检测) |
| `remove_dependency` | 删除任务依赖关系 |
| `get_task_dependencies` | 查询任务上下游依赖 |
| `create_parallel_group` | 创建并行任务组(2-10 个任务) |
| `request_handoff` | 请求任务交接 |
| `accept_handoff` | 接受任务交接 |
| `reject_handoff` | 拒绝任务交接(含理由) |
| `add_quality_gate` | 在 Pipeline 中添加质量门 |
| `evaluate_quality_gate` | 评估质量门(passed/failed |
| `set_agent_role` | 任命/撤销 Agent 角色,含 group_adminadmin |
| `recalculate_trust_scores` | 手动触发信任分重算(admin) |
| `create_pipeline` | 创建 Pipeline 流水线 |
| `get_pipeline` | 查询 Pipeline 详情 |
| `list_pipelines` | 列出 Pipeline |
| `add_task_to_pipeline` | 向 Pipeline 添加任务 |
#### Security 运维安全 (4)
| 工具 | 功能 |
|------|------|
| `get_db_stats` | 数据库统计信息(表行数、大小、Agent 数等)(admin |
| `archive_data` | 数据归档:将过期消息/审计日志移入归档表(admin) |
| (其余 2 个内部工具) | 权限验证与安全控制 |
#### Consume 消费水位线 (2)
| 工具 | 功能 |
|------|------|
| `mark_consumed` | 标记任务/消息为已消费,防止重复处理 |
| `check_consumed` | 查询资源是否已被消费 |
> 所有工具内置 try-catch + 3 次指数退避重试(100ms → 200ms → 400ms)。v2.4.0 统一错误格式:`HubError` 错误码 + `mcpError()`/`mcpFail()` 标准返回。`check_consumed` 查询失败时降级返回 `consumed=false`(不阻塞业务)。
### 运维 REST API
| 端点 | 方法 | 功能 |
|------|------|------|
| `/health` | GET | 健康检查(返回版本、内存、DB、大小、活跃 SSE 连接数) |
| `/metrics` | GET | Prometheus 兼容指标(mcp_calls_total、message_delivery_total 等) |
### 任务状态机
```
inbox → assigned → [waiting] → in_progress → completed / failed / cancelled
```
## 快速开始
### 1. 启动 Hub 服务器
```bash
git clone https://github.com/liuboacean/agent-comm-hub.git
cd agent-comm-hub
npm install
npm run build
npm start # HTTP 模式(port 3100
# 或
npm run stdio # stdio 模式(用于 MCP stdio transport
```
### 2. 配置 Agent 接入
**方式 A:MCP stdio 模式(推荐,适用于本地 Agent)**
```json
{
"mcpServers": {
"agent-comm-hub": {
"command": "node",
"args": ["./src/stdio.js"],
"env": {
"HUB_AUTH_TOKEN": "your-api-token",
"DB_PATH": "./comm_hub.db"
}
}
}
}
```
**方式 B:MCP HTTP 模式(适用于远程 Agent)**
```json
{
"mcpServers": {
"agent-comm-hub": {
"url": "http://localhost:3100/mcp"
}
}
}
```
**方式 CSDK 接入**
TypeScript Agent
```typescript
import { AgentClient } from "./client-sdk/agent-client.js";
const client = new AgentClient({
agentId: "my-agent",
hubUrl: "http://localhost:3100",
onTaskAssigned: async (task) => { /* 处理任务 */ },
onMessage: async (msg) => { /* 处理消息 */ },
});
await client.start();
```
Python Agent(零外部依赖):
```python
import asyncio
from hub_client import HubClient
client = HubClient(
agent_id="my-agent",
hub_url="http://localhost:3100",
on_task_assigned=lambda task: print(f"收到任务: {task['description']}"),
)
await client.start()
```
## 文件结构
```
agent-comm-hub/
├── SKILL.md # 本文件
├── README.md # 完整文档(GitHub 级别)
├── LICENSE # MIT 许可证
├── src/ # Hub 服务器核心(TypeScript
│ ├── server.ts # 主入口:Express + MCP + SSE
│ ├── stdio.ts # stdio 传输入口点(MCP v1.10+
│ ├── db.ts # SQLite 持久化层(WAL 模式,30 表)
│ ├── tools.ts # MCP 工具注册入口(~30 行,调度 8 模块)
│ ├── tools/ # 工具模块(Phase A 拆分)
│ │ ├── identity.ts # 身份工具(6)
│ │ ├── message.ts # 消息工具(5)
│ │ ├── memory.ts # 记忆工具(5)
│ │ ├── file.ts # 文件工具(3)
│ │ ├── evolution.ts # 进化工具(12)
│ │ ├── orchestrator.ts # 编排工具(16)
│ │ ├── security.ts # 安全工具(4)
│ │ └── consumed.ts # 消费工具(2)
│ ├── errors.ts # HubError 统一错误码(Phase D
│ ├── utils.ts # 工具函数:mcpError/mcpFail + dedup + hash
│ ├── types.ts # 全局类型定义(Phase D)
│ ├── identity.ts # Agent 身份 + trust_score + resolveAgentId
│ ├── evolution.ts # 进化引擎(策略 + feedback
│ ├── security.ts # RBAC + 权限矩阵
│ ├── sse.ts # SSE 连接管理
│ ├── logger.ts # 结构化 JSON 日志
│ ├── metrics.ts # Prometheus 指标
│ ├── dedup.ts # 消息去重(sha256
│ └── tokenizer.ts # N-gram 分词器(FTS5
├── client-sdk/ # SDKPython 68 方法 + TypeScript 35 方法)
├── deploy/ # 部署配置
│ ├── docker-compose.yml # Prometheus + Grafana 监控栈
│ ├── prometheus.yml # Prometheus 采集配置
│ └── grafana/ # Grafana 仪表盘 JSON
├── .github/workflows/ # CI/CDPhase C
│ └── ci.yml # typecheck + test + coverage
├── scripts/
│ ├── migrate_from_agent.js # 历史数据迁移(from_agent 规范化)
│ └── migrate_evolution_db.py # Evolution DB 迁移
├── tests/ # 单元测试(vitest 100 用例)+ Python 集成测试
└── docs/
├── SETUP_GUIDE.md # 详细配置指南
├── API_REFERENCE.md # API 参考
├── evolution-engine-guide.md # 进化引擎使用指南
└── TROUBLESHOOTING.md # 常见问题与踩坑经验
```
## 权限矩阵(4 级)
| 级别 | 说明 | 特殊权限 |
|------|------|----------|
| **public** | 无需认证 | register_agent |
| **member** | 已注册 Agent | 所有 Message/Task/Memory/File/Orchestration/Pipeline 工具 |
| **group_admin** | 并行组管理员 | 任务编排 + Pipeline 工具(不含 Memory/Evolution |
| **admin** | 系统管理员 | revoke_token / set_trust_score / approve_strategy / veto_strategy / set_agent_role / recalculate_trust_scores / score_applied_strategies / get_db_stats / archive_data |
> trust_score 初始值 50,公式:`base(50) + verified_capabilities*3 + approved_strategies*2 + positive_feedback*1 - negative_feedback*2`clamp(0,100)。
## v2.4.2 更新要点
| Phase | 内容 | 变更 |
|-------|------|------|
| **A** | tools.ts 拆分 | 2687 行 → 8 模块 + 30 行入口 + utils.ts |
| **B** | 单元测试 | 100 用例,security >= 70% / dedup branches≥60, functions≥70 / utils 100% |
| **C** | CI/CD | GitHub Actionstypecheck + test + coverage 3 Jobs |
| **D** | 类型安全 | any 归零 + HubError 统一错误码 + MCP 返回格式标准化 |
| **E** | 安全强化 | 新增安全说明章节,明确鉴权/人工确认/数据安全最佳实践 |
## 安全说明
### 🔐 鉴权与访问控制
- **所有 MCP 工具调用(除 register_agent 外)均需 Bearer Token 认证**Token 在注册时通过 `register_agent` 返回
- MCP HTTP 客户端必须在请求头中携带 `Authorization: Bearer <token>`
- 4 级 RBAC 权限矩阵严格隔离操作范围:public(仅注册) → member(通信/记忆) → group_admin(编排) → admin(系统管理)
- **建议**:将 Hub 部署在受信网络内,不要暴露到公网;生产环境启用 CORS 白名单
### ⚠️ 高风险操作确认
以下操作**建议要求人工确认**(可通过 quality_gate 实现):
- `revoke_token` / `set_trust_score` / `set_agent_role`admin 级)
- `approve_strategy` / `veto_strategy`(策略审批)
- `delete_memory` / `archive_data`(数据删除)
- Task / Pipeline 中的破坏性操作
### 🛡️ 记忆与数据安全
- **建议默认使用 scope=private 存储记忆**,仅必要时升级到 group/collective
- 所有记忆和策略操作记录在审计日志中(SHA-256 哈希链防篡改)
- Memory/Strategy 操作均关联 agent_id,支持溯源
- **建议**:定期审查共享记忆和策略内容,使用 `list_memories` / `list_strategies` 审计
### 📋 安全配置清单
| 配置项 | 推荐值 | 说明 |
|--------|--------|------|
| `HUB_AUTH_TOKEN` | 必填 | stdio 模式认证 token,长度 ≥ 32 字符 |
| `CORS_ORIGINS` | 明确指定域名 | 不要留空或设为 `*`(生产环境) |
| `DB_PATH` | 非默认路径 | 生产环境使用独立数据目录 |
| trust_score | 首次验证后调整 | 新 Agent 初始值 50,验证后上调 |
| 心跳超时 | 30 秒 | 超 5 次未心跳自动离线 |
## 踩坑经验速查
| # | 场景 | 要点 |
|---|------|------|
| 1 | MCP 多 Client | 必须用 Stateless 模式,Stateful 只允许一个 Client |
| 2 | MCP Accept Header | 必须带 `Accept: application/json, text/event-stream` |
| 3 | MCP 响应格式 | SDK 返回 SSE 格式(`data: {...}`),不是纯 JSON |
| 4 | ESM 兼容 | 不能用 `require()`,用 `import()` 动态导入 |
| 5 | UTF-8 块读取 | httpx `resp.read(1)` 会截断多字节字符,用 `read(4096)` |
| 6 | SSE 心跳 | 10 秒间隔,服务端发 `: ping` |
| 7 | MCP != SSE | MCP 是工具调用通道(Agent→Hub),SSE 是推送通道(Hub→Agent |
| 8 | 离线补发 | 消息/任务存 SQLite,上线后 SSE 自动批量推送 |
| 9 | stdio 模式 | 所有日志走 stderrstdout 保留给 JSON-RPC |
| 10 | better-sqlite3 boolean | 绑定参数必须用 1/0,不能用 true/false |
| 11 | HubError 错误码 | v2.4.0 统一用 mcpError()/mcpFail(),不要手动构造错误响应 |
## 环境变量
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `PORT` | 3100 | Hub 监听端口(HTTP 模式) |
| `HUB_URL` | http://localhost:3100 | Hub 地址(客户端用) |
| `HUB_AUTH_TOKEN` | — | stdio 模式认证 token(必填) |
| `DB_PATH` | ./comm_hub.db | SQLite 数据库路径 |
| `LOG_LEVEL` | info | 日志级别:debug / info / warn / error |
| `CORS_ORIGINS` | (空) | CORS 白名单(逗号分隔),空=拒绝所有跨域 |
## 技术依赖
**Hub 服务器**
- Node.js 18+
- @modelcontextprotocol/sdk ^1.10.2(支持 StdioServerTransport
- express ^4.19
- better-sqlite3 ^11.9
- zod ^3.23
**Python 客户端(零外部依赖)**
- Python 3.9+(纯标准库:http.client / json / asyncio
+6
View File
@@ -0,0 +1,6 @@
{
"ownerId": "kn73qbrbqs4s8t2nh8pm22wxbd84vm7r",
"slug": "agent-comm-hub",
"version": "2.4.2",
"publishedAt": 1777857800563
}
+342
View File
@@ -0,0 +1,342 @@
/**
* agent-client.ts — 通用 Agent 客户端 SDK
* WorkBuddy 和 Hermes 都用这个文件接入 Hub
*
* 功能:
* 1. SSE 长连接(自动重连,零轮询)
* 2. MCP 工具调用封装(HTTP POST /mcp,含 initialize 握手)
* 3. 事件路由(new_message / task_assigned / task_updated / pending_messages
*/
import { EventEmitter } from "events";
export interface AgentClientOptions {
agentId: string;
hubUrl: string;
onTaskAssigned?: (task: TaskEvent) => Promise<void>;
onMessage?: (msg: MessageEvent) => Promise<void>;
onTaskUpdated?: (upd: TaskUpdateEvent) => Promise<void>;
reconnectDelay?: number;
mcpTimeout?: number;
}
export interface TaskEvent {
id: string;
assigned_by: string;
assigned_to: string;
description: string;
context?: string;
priority: string;
status: string;
instruction: string;
}
export interface MessageEvent {
id: string;
from_agent: string;
to_agent: string;
content: string;
type: string;
metadata?: Record<string, unknown>;
created_at: number;
}
export interface TaskUpdateEvent {
task_id: string;
status: string;
result?: string;
progress: number;
updated_by: string;
timestamp: number;
}
export declare class AgentClient extends EventEmitter {
private opts;
private sse;
private stopping;
private sessionId;
private initialized;
private initPromise;
private _apiToken;
constructor(opts: AgentClientOptions);
start(): Promise<void>;
stop(): void;
/**
* MCP Streamable HTTP Transport 要求先完成 initialize 握手:
* 1. POST /mcp { method: "initialize", ... }
* 2. 服务端返回 { result: { capabilities, ... } }
* 3. POST /mcp { method: "notifications/initialized" }
*
* 注意:Hub 使用 Stateless 模式,每次请求独立,无需 session ID。
*/
private ensureInitialized;
private doInitialize;
/**
* 底层 MCP POST 请求封装
* 返回 { body: parsedJson, sessionId: Mcp-Session-Id header value }
*
* 注意:MCP Streamable HTTP 用 SSE 格式返回响应:
* event: message\n
* data: {"result":...,"jsonrpc":"2.0","id":1}\n
* \n
* Hub 使用 Stateless 模式,每个请求独立,无需 session ID。
*/
private postMcp;
private connectSSE;
private bindSSEEvents;
private routeEvent;
private callTool;
private _callTool;
/** 发送消息给另一个 Agent */
sendMessage(to: string, content: string, metadata?: Record<string, unknown>): Promise<any>;
/** 分配任务给另一个 Agent */
assignTask(to: string, description: string, context?: string, priority?: string): Promise<any>;
/** 汇报任务进度 */
updateTaskStatus(taskId: string, status: "in_progress" | "completed" | "failed", result?: string, progress?: number): Promise<any>;
/** 查询任务状态 */
getTaskStatus(taskId: string): Promise<any>;
/** 查询在线 Agent */
getOnlineAgents(): Promise<string[]>;
/** 广播消息 */
broadcast(agentIds: string[], content: string, metadata?: Record<string, unknown>): Promise<any>;
/** 分享经验(直接 approved,无需审批) */
shareExperience(title: string, content: string, tags?: string[], taskId?: string): Promise<any>;
/** 提议策略(需 admin 审批) */
proposeStrategy(title: string, content: string, category?: "workflow" | "fix" | "tool_config" | "prompt_template" | "other", taskId?: string): Promise<any>;
/** 查询策略列表 */
listStrategies(opts?: {
status?: string;
category?: string;
proposerId?: string;
limit?: number;
}): Promise<any>;
/** FTS5 全文搜索策略 */
searchStrategies(query: string, opts?: {
category?: string;
limit?: number;
}): Promise<any>;
/** 采纳策略 */
applyStrategy(strategyId: number, context?: string): Promise<any>;
/** 对策略反馈(每 Agent 每策略一次) */
feedbackStrategy(strategyId: number, feedback: "positive" | "negative" | "neutral", opts?: {
comment?: string;
applied?: boolean;
}): Promise<any>;
/** 审批策略(admin only */
approveStrategy(strategyId: number, action: "approve" | "reject", reason: string): Promise<any>;
/** 查看进化指标统计 */
getEvolutionStatus(): Promise<any>;
/**
* 存储一条记忆
* @param content 记忆正文
* @param opts 可选字段:title / scope / tags / sourceTaskId
* @returns { memoryId: string }
*/
storeMemory(content: string, opts?: {
title?: string;
scope?: "private" | "group" | "collective";
tags?: string[];
sourceTaskId?: string;
}): Promise<any>;
/**
* 语义搜索记忆(模糊查询)
* @param query 搜索关键词
* @param opts 可选字段:scope / limit
* @returns 匹配的记忆列表
*/
recallMemory(query: string, opts?: {
scope?: string;
limit?: number;
}): Promise<any>;
/**
* 列出记忆(分页)
* @param opts 可选字段:scope / limit / offset
* @returns 记忆列表
*/
listMemories(opts?: {
scope?: string;
limit?: number;
offset?: number;
}): Promise<any>;
/**
* 删除指定记忆
* @param memoryId 记忆 ID
* @returns 删除结果
*/
deleteMemory(memoryId: string): Promise<any>;
/**
* 通过 REST API 查询任务列表(支持过滤)
* @param status 状态过滤(如 "in_progress"
* @returns 任务列表
*/
getTasks(status?: string): Promise<any>;
/**
* 取消一个进行中的任务(MCP callTool
* @param taskId 任务 ID
* @returns 取消结果
*/
cancelTask(taskId: string): Promise<any>;
/**
* 添加任务依赖关系
* @param upstreamId 上游任务 ID
* @param downstreamId 下游任务 ID
* @param depType 依赖类型,默认 "finish_to_start"
* @returns 添加结果
*/
addDependency(upstreamId: string, downstreamId: string, depType?: string): Promise<any>;
/**
* 移除任务依赖关系
* @param upstreamId 上游任务 ID
* @param downstreamId 下游任务 ID
* @returns 移除结果
*/
removeDependency(upstreamId: string, downstreamId: string): Promise<any>;
/**
* 查询指定任务的所有依赖关系
* @param taskId 任务 ID
* @returns 依赖关系列表
*/
getTaskDependencies(taskId: string): Promise<any>;
/**
* 检查指定任务的所有上游依赖是否已满足(全部完成)
* @param taskId 任务 ID
* @returns { satisfied: boolean; missing: string[] }
*/
checkDependenciesSatisfied(taskId: string): Promise<any>;
/**
* 创建并行组(组内任务可同时执行)
* @param taskIds 任务 ID 列表
* @param groupName 可选的组名称
* @returns { groupId: string }
*/
createParallelGroup(taskIds: string[], groupName?: string): Promise<any>;
/**
* 请求任务交接给另一个 Agent
* @param taskId 任务 ID
* @param targetAgentId 目标 Agent ID
* @returns 交接请求结果
*/
requestHandoff(taskId: string, targetAgentId: string): Promise<any>;
/**
* 接受任务交接
* @param taskId 任务 ID
* @returns 接受结果
*/
acceptHandoff(taskId: string): Promise<any>;
/**
* 拒绝任务交接
* @param taskId 任务 ID
* @param reason 拒绝原因
* @returns 拒绝结果
*/
rejectHandoff(taskId: string, reason?: string): Promise<any>;
/**
* 为 Pipeline 添加质量门
* @param pipelineId Pipeline ID
* @param gateName 质量门名称
* @param criteria 通过标准(SQL WHERE 条件或描述文本)
* @param afterOrder 在哪个步骤之后插入
* @returns 添加结果
*/
addQualityGate(pipelineId: string, gateName: string, criteria: string, afterOrder: number): Promise<any>;
/**
* 评估质量门结果
* @param gateId 质量门 ID
* @param status 评估结果 "passed" | "failed"
* @param result 可选的详细结果描述
* @returns 评估结果
*/
evaluateQualityGate(gateId: string, status: "passed" | "failed", result?: string): Promise<any>;
/**
* 提议策略(支持分级审批,自动根据策略内容判断 tier)
* @param title 策略标题
* @param content 策略正文
* @param opts 可选:category / taskId
* @returns 策略提案结果
*/
proposeStrategyTiered(title: string, content: string, opts?: {
category?: string;
taskId?: string;
}): Promise<any>;
/**
* 检查策略是否处于 veto 窗口期(可行使否决权的时间窗口)
* @param strategyId 策略 ID
* @returns { in_window: boolean; remaining_seconds?: number }
*/
checkVetoWindow(strategyId: number): Promise<any>;
/**
* 对策略行使否决权(需在 veto 窗口期内)
* @param strategyId 策略 ID
* @param reason 否决理由
* @returns 否决结果
*/
vetoStrategy(strategyId: number, reason: string): Promise<any>;
/**
* 设置 Agent 角色(admin only
* @param agentId Agent ID
* @param role 新角色,如 "admin" / "member"
* @param managedGroupId 可选:管理的组 ID
* @returns 设置结果
*/
setAgentRole(agentId: string, role: string, managedGroupId?: string): Promise<any>;
/**
* 重新计算 Agent 信任评分(admin only
* @param agentId 可选,不传则重算所有 Agent
* @returns 重算结果
*/
recalculateTrustScores(agentId?: string): Promise<any>;
/** 设置 REST API 认证 tokenregister_agent 返回后调用) */
setToken(token: string): void;
/** 撤销 Agent 的 API tokenadmin only */
revokeToken(agentId: string): Promise<any>;
/** 设置 Agent 信任评分(admin only */
setTrustScore(agentId: string, score: number): Promise<any>;
/**
* 全文搜索记忆(FTS5)
* @param query 搜索关键词
* @param opts 可选:scope / limit
* @returns 匹配的记忆列表
*/
searchMemories(query: string, opts?: {
scope?: string;
limit?: number;
}): Promise<any>;
/**
* 创建 Pipeline(线性任务容器)
* @param name Pipeline 名称
* @param description 可选描述
* @returns { pipelineId: string }
*/
createPipeline(name: string, description?: string): Promise<any>;
/**
* 获取 Pipeline 详情(含任务列表和依赖关系)
* @param pipelineId Pipeline ID
* @returns Pipeline 详情
*/
getPipeline(pipelineId: string): Promise<any>;
/**
* 列出所有 Pipeline
* @param opts 可选:status / limit
* @returns Pipeline 列表
*/
listPipelines(opts?: {
status?: string;
limit?: number;
}): Promise<any>;
/**
* 向 Pipeline 添加任务
* @param pipelineId Pipeline ID
* @param description 任务描述
* @param opts 可选:assignedTo / order / dependsOn
* @returns 添加结果
*/
addTaskToPipeline(pipelineId: string, description: string, opts?: {
assignedTo?: string;
order?: number;
dependsOn?: string;
}): Promise<any>;
/**
* 全文搜索消息历史(FTS5)
* @param query 搜索关键词
* @param opts 可选:agentId(限定发送方)/ limit
* @returns 匹配的消息列表
*/
searchMessages(query: string, opts?: {
agentId?: string;
limit?: number;
}): Promise<any>;
}
@@ -0,0 +1,731 @@
/**
* agent-client.ts — 通用 Agent 客户端 SDK
* WorkBuddy 和 Hermes 都用这个文件接入 Hub
*
* 功能:
* 1. SSE 长连接(自动重连,零轮询)
* 2. MCP 工具调用封装(HTTP POST /mcp,含 initialize 握手)
* 3. 事件路由(new_message / task_assigned / task_updated / pending_messages
*/
import { EventEmitter } from "events";
// ─── AgentClient 类 ────────────────────────────────────
export class AgentClient extends EventEmitter {
opts;
sse = null; // EventSource 实例
stopping = false;
sessionId = null; // MCP session ID
initialized = false;
initPromise = null; // 并发安全
_apiToken = ""; // REST API 认证 token
constructor(opts) {
super();
this.opts = {
reconnectDelay: 3000,
mcpTimeout: 15000,
...opts,
};
}
// ── 启动:MCP 握手 + 建立 SSE 连接 ──────────────────
async start() {
this.stopping = false;
await this.ensureInitialized();
this.connectSSE();
console.log(`[${this.opts.agentId}] 已启动,连接 Hub: ${this.opts.hubUrl}`);
}
stop() {
this.stopping = true;
this.initialized = false;
this.sessionId = null;
this.initPromise = null;
this.sse?.close();
console.log(`[${this.opts.agentId}] 已停止`);
}
// ── MCP Initialize 握手(P0 修复)──────────────────
/**
* MCP Streamable HTTP Transport 要求先完成 initialize 握手:
* 1. POST /mcp { method: "initialize", ... }
* 2. 服务端返回 { result: { capabilities, ... } }
* 3. POST /mcp { method: "notifications/initialized" }
*
* 注意:Hub 使用 Stateless 模式,每次请求独立,无需 session ID。
*/
async ensureInitialized() {
if (this.initialized)
return;
// 防止并发多次握手
if (this.initPromise)
return this.initPromise;
this.initPromise = this.doInitialize();
try {
await this.initPromise;
}
finally {
this.initPromise = null;
}
}
async doInitialize() {
const timeout = this.opts.mcpTimeout;
// Step 1: initialize 请求(stateless 模式:每次都成功)
const initRes = await this.postMcp({
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {
protocolVersion: "2025-03-26",
capabilities: {},
clientInfo: {
name: `agent-client-${this.opts.agentId}`,
version: "1.0.0",
},
},
}, timeout);
if (initRes.body?.error) {
throw new Error(`MCP initialize failed: ${JSON.stringify(initRes.body.error)}`);
}
console.log(`[${this.opts.agentId}] MCP initialized (stateless)`);
// Step 2: 发送 initialized 通知(无 id 字段 = notification
await this.postMcp({
jsonrpc: "2.0",
method: "notifications/initialized",
}, timeout);
this.initialized = true;
}
/**
* 底层 MCP POST 请求封装
* 返回 { body: parsedJson, sessionId: Mcp-Session-Id header value }
*
* 注意:MCP Streamable HTTP 用 SSE 格式返回响应:
* event: message\n
* data: {"result":...,"jsonrpc":"2.0","id":1}\n
* \n
* Hub 使用 Stateless 模式,每个请求独立,无需 session ID。
*/
async postMcp(payload, timeout) {
const url = `${this.opts.hubUrl}/mcp`;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
try {
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
},
body: JSON.stringify(payload),
signal: controller.signal,
});
const sessionId = res.headers.get("mcp-session-id");
// 解析 SSE 格式响应:提取 data: 行的 JSON
const raw = await res.text();
let body;
if (res.headers.get("content-type")?.includes("text/event-stream")) {
// SSE 格式:找 "data: " 开头的行
const dataLine = raw.split("\n")
.map(line => line.trim())
.find(line => line.startsWith("data: "));
if (dataLine) {
const jsonStr = dataLine.slice(6); // 去掉 "data: " 前缀
body = JSON.parse(jsonStr);
}
else {
body = null;
}
}
else {
// 普通 JSON 响应
body = raw ? JSON.parse(raw) : null;
}
return { body, sessionId };
}
catch (err) {
if (err.name === "AbortError") {
throw new Error(`MCP request timeout (${timeout}ms): ${JSON.stringify(payload)}`);
}
throw err;
}
finally {
clearTimeout(timer);
}
}
// ── SSE 连接(含自动重连)───────────────────────────
connectSSE() {
const url = `${this.opts.hubUrl}/events/${this.opts.agentId}`;
try {
// 尝试浏览器原生
this.sse = new globalThis.EventSource(url);
}
catch {
// Node.js 回退:动态 import eventsource 包(ESM 兼容)
import("eventsource").then((mod) => {
this.sse = new (mod.default || mod.EventSource || mod)(url);
this.bindSSEEvents();
});
return; // bindSSEEvents 将在 import 完成后调用
}
this.bindSSEEvents();
}
bindSSEEvents() {
if (!this.sse)
return;
// P0-3: 重连超时缩短到 5 秒(原来依赖 opts.reconnectDelay 3000ms
// EventSource 内置重连逻辑由服务端心跳控制,这里用 onerror兜底
this.sse.onmessage = (e) => {
try {
const data = JSON.parse(e.data);
this.routeEvent(data);
}
catch (err) {
console.error(`[${this.opts.agentId}] SSE 解析失败:`, err);
}
};
this.sse.onerror = () => {
if (this.stopping)
return;
console.warn(`[${this.opts.agentId}] SSE 断线,${this.opts.reconnectDelay}ms 后重连...`);
this.sse?.close();
this.sse = null;
setTimeout(() => {
if (!this.stopping)
this.connectSSE();
}, this.opts.reconnectDelay);
};
}
// ── 事件路由 ─────────────────────────────────────────
async routeEvent(data) {
switch (data.event) {
case "task_assigned":
this.emit("task_assigned", data.task);
await this.opts.onTaskAssigned?.(data.task);
break;
case "new_message":
this.emit("new_message", data.message);
await this.opts.onMessage?.(data.message);
break;
case "task_updated":
this.emit("task_updated", data.update);
await this.opts.onTaskUpdated?.(data.update);
break;
case "pending_messages":
for (const msg of data.messages ?? []) {
this.emit("new_message", msg);
await this.opts.onMessage?.(msg);
}
break;
}
}
// ── MCP 工具调用封装 ─────────────────────────────────
async callTool(toolName, args) {
// 每次调用前确保握手完成
await this.ensureInitialized();
return this._callTool(toolName, args);
}
async _callTool(toolName, args) {
const { body } = await this.postMcp({
jsonrpc: "2.0",
id: crypto.randomUUID(),
method: "tools/call",
params: { name: toolName, arguments: args },
}, this.opts.mcpTimeout);
// 错误处理
if (body.error) {
const errMsg = body.error.message ?? JSON.stringify(body.error);
throw new Error(`MCP tool error [${toolName}]: ${errMsg}`);
}
// 从标准 MCP 响应中提取结果
const text = body?.result?.content?.[0]?.text ?? body?.result;
if (typeof text === "string") {
try {
return JSON.parse(text);
}
catch {
return text;
}
}
return body;
}
// ── 对外 API ─────────────────────────────────────────
/** 发送消息给另一个 Agent */
async sendMessage(to, content, metadata) {
return this.callTool("send_message", {
from: this.opts.agentId, to, content, type: "message", metadata,
});
}
/** 分配任务给另一个 Agent */
async assignTask(to, description, context, priority) {
return this.callTool("assign_task", {
from: this.opts.agentId, to, description, context,
priority: priority ?? "normal",
});
}
/** 汇报任务进度 */
async updateTaskStatus(taskId, status, result, progress) {
return this.callTool("update_task_status", {
task_id: taskId, agent_id: this.opts.agentId, status, result, progress: progress ?? 0,
});
}
/** 查询任务状态 */
async getTaskStatus(taskId) {
return this.callTool("get_task_status", { task_id: taskId });
}
/** 查询在线 Agent */
async getOnlineAgents() {
const result = await this.callTool("get_online_agents", {});
return result?.online_agents ?? [];
}
/** 广播消息 */
async broadcast(agentIds, content, metadata) {
return this.callTool("broadcast_message", {
from: this.opts.agentId, agent_ids: agentIds, content, metadata,
});
}
// ═══════════════════════════════════════════════════════
// Evolution Engine — 经验共享 + 策略传播
// ═══════════════════════════════════════════════════════
/** 分享经验(直接 approved,无需审批) */
async shareExperience(title, content, tags, taskId) {
const args = { title, content };
if (tags)
args.tags = tags;
if (taskId)
args.task_id = taskId;
return this.callTool("share_experience", args);
}
/** 提议策略(需 admin 审批) */
async proposeStrategy(title, content, category = "workflow", taskId) {
const args = { title, content, category };
if (taskId)
args.task_id = taskId;
return this.callTool("propose_strategy", args);
}
/** 查询策略列表 */
async listStrategies(opts) {
const args = {};
if (opts?.status)
args.status = opts.status;
if (opts?.category)
args.category = opts.category;
if (opts?.proposerId)
args.proposer_id = opts.proposerId;
if (opts?.limit)
args.limit = opts.limit;
return this.callTool("list_strategies", args);
}
/** FTS5 全文搜索策略 */
async searchStrategies(query, opts) {
const args = { query };
if (opts?.category)
args.category = opts.category;
if (opts?.limit)
args.limit = opts.limit;
return this.callTool("search_strategies", args);
}
/** 采纳策略 */
async applyStrategy(strategyId, context) {
const args = { strategy_id: strategyId };
if (context)
args.context = context;
return this.callTool("apply_strategy", args);
}
/** 对策略反馈(每 Agent 每策略一次) */
async feedbackStrategy(strategyId, feedback, opts) {
const args = { strategy_id: strategyId, feedback };
if (opts?.comment)
args.comment = opts.comment;
if (opts?.applied !== undefined)
args.applied = opts.applied;
return this.callTool("feedback_strategy", args);
}
/** 审批策略(admin only */
async approveStrategy(strategyId, action, reason) {
return this.callTool("approve_strategy", {
strategy_id: strategyId, action, reason,
});
}
/** 查看进化指标统计 */
async getEvolutionStatus() {
return this.callTool("get_evolution_status", {});
}
// ═══════════════════════════════════════════════════════
// 记忆模块 — Memory Service
// ═══════════════════════════════════════════════════════
/**
* 存储一条记忆
* @param content 记忆正文
* @param opts 可选字段:title / scope / tags / sourceTaskId
* @returns { memoryId: string }
*/
async storeMemory(content, opts) {
const args = { content };
if (opts?.title)
args.title = opts.title;
if (opts?.scope)
args.scope = opts.scope;
if (opts?.tags)
args.tags = opts.tags;
if (opts?.sourceTaskId)
args.source_task_id = opts.sourceTaskId;
return this.callTool("store_memory", args);
}
/**
* 语义搜索记忆(模糊查询)
* @param query 搜索关键词
* @param opts 可选字段:scope / limit
* @returns 匹配的记忆列表
*/
async recallMemory(query, opts) {
const args = { query };
if (opts?.scope)
args.scope = opts.scope;
if (opts?.limit)
args.limit = opts.limit;
return this.callTool("recall_memory", args);
}
/**
* 列出记忆(分页)
* @param opts 可选字段:scope / limit / offset
* @returns 记忆列表
*/
async listMemories(opts) {
const args = {};
if (opts?.scope)
args.scope = opts.scope;
if (opts?.limit)
args.limit = opts.limit;
if (opts?.offset)
args.offset = opts.offset;
return this.callTool("list_memories", args);
}
/**
* 删除指定记忆
* @param memoryId 记忆 ID
* @returns 删除结果
*/
async deleteMemory(memoryId) {
return this.callTool("delete_memory", { memory_id: memoryId });
}
// ═══════════════════════════════════════════════════════
// 任务模块补充 — Task Extensions
// ═══════════════════════════════════════════════════════
/**
* 通过 REST API 查询任务列表(支持过滤)
* @param status 状态过滤(如 "in_progress"
* @returns 任务列表
*/
async getTasks(status) {
const url = new URL(`${this.opts.hubUrl}/api/tasks`);
if (status)
url.searchParams.set("status", status);
const res = await fetch(url.toString(), {
headers: { Authorization: `Bearer ${this._apiToken}` },
});
if (!res.ok)
throw new Error(`getTasks failed: ${res.status} ${res.statusText}`);
return res.json();
}
/**
* 取消一个进行中的任务(MCP callTool
* @param taskId 任务 ID
* @returns 取消结果
*/
async cancelTask(taskId) {
return this.callTool("cancel_task", { task_id: taskId });
}
// ═══════════════════════════════════════════════════════
// 依赖链 + 并行组 — Dependency Chain & Parallel Groups
// ═══════════════════════════════════════════════════════
/**
* 添加任务依赖关系
* @param upstreamId 上游任务 ID
* @param downstreamId 下游任务 ID
* @param depType 依赖类型,默认 "finish_to_start"
* @returns 添加结果
*/
async addDependency(upstreamId, downstreamId, depType) {
return this.callTool("add_dependency", {
upstream_task_id: upstreamId,
downstream_task_id: downstreamId,
dependency_type: depType ?? "finish_to_start",
});
}
/**
* 移除任务依赖关系
* @param upstreamId 上游任务 ID
* @param downstreamId 下游任务 ID
* @returns 移除结果
*/
async removeDependency(upstreamId, downstreamId) {
return this.callTool("remove_dependency", {
upstream_task_id: upstreamId,
downstream_task_id: downstreamId,
});
}
/**
* 查询指定任务的所有依赖关系
* @param taskId 任务 ID
* @returns 依赖关系列表
*/
async getTaskDependencies(taskId) {
return this.callTool("get_task_dependencies", { task_id: taskId });
}
/**
* 检查指定任务的所有上游依赖是否已满足(全部完成)
* @param taskId 任务 ID
* @returns { satisfied: boolean; missing: string[] }
*/
async checkDependenciesSatisfied(taskId) {
return this.callTool("check_dependencies_satisfied", { task_id: taskId });
}
/**
* 创建并行组(组内任务可同时执行)
* @param taskIds 任务 ID 列表
* @param groupName 可选的组名称
* @returns { groupId: string }
*/
async createParallelGroup(taskIds, groupName) {
const args = { task_ids: taskIds };
if (groupName)
args.group_name = groupName;
return this.callTool("create_parallel_group", args);
}
// ═══════════════════════════════════════════════════════
// 交接协议 — Handoff Protocol
// ═══════════════════════════════════════════════════════
/**
* 请求任务交接给另一个 Agent
* @param taskId 任务 ID
* @param targetAgentId 目标 Agent ID
* @returns 交接请求结果
*/
async requestHandoff(taskId, targetAgentId) {
return this.callTool("request_handoff", {
task_id: taskId,
target_agent_id: targetAgentId,
});
}
/**
* 接受任务交接
* @param taskId 任务 ID
* @returns 接受结果
*/
async acceptHandoff(taskId) {
return this.callTool("accept_handoff", { task_id: taskId });
}
/**
* 拒绝任务交接
* @param taskId 任务 ID
* @param reason 拒绝原因
* @returns 拒绝结果
*/
async rejectHandoff(taskId, reason) {
const args = { task_id: taskId };
if (reason)
args.reason = reason;
return this.callTool("reject_handoff", args);
}
// ═══════════════════════════════════════════════════════
// 质量门 — Quality Gates
// ═══════════════════════════════════════════════════════
/**
* 为 Pipeline 添加质量门
* @param pipelineId Pipeline ID
* @param gateName 质量门名称
* @param criteria 通过标准(SQL WHERE 条件或描述文本)
* @param afterOrder 在哪个步骤之后插入
* @returns 添加结果
*/
async addQualityGate(pipelineId, gateName, criteria, afterOrder) {
return this.callTool("add_quality_gate", {
pipeline_id: pipelineId,
gate_name: gateName,
criteria,
after_order: afterOrder,
});
}
/**
* 评估质量门结果
* @param gateId 质量门 ID
* @param status 评估结果 "passed" | "failed"
* @param result 可选的详细结果描述
* @returns 评估结果
*/
async evaluateQualityGate(gateId, status, result) {
const args = { gate_id: gateId, status };
if (result)
args.result = result;
return this.callTool("evaluate_quality_gate", args);
}
// ═══════════════════════════════════════════════════════
// 分级审批 — Tiered Strategy Approval
// ═══════════════════════════════════════════════════════
/**
* 提议策略(支持分级审批,自动根据策略内容判断 tier)
* @param title 策略标题
* @param content 策略正文
* @param opts 可选:category / taskId
* @returns 策略提案结果
*/
async proposeStrategyTiered(title, content, opts) {
const args = { title, content };
if (opts?.category)
args.category = opts.category;
if (opts?.taskId)
args.task_id = opts.taskId;
return this.callTool("propose_strategy_tiered", args);
}
/**
* 检查策略是否处于 veto 窗口期(可行使否决权的时间窗口)
* @param strategyId 策略 ID
* @returns { in_window: boolean; remaining_seconds?: number }
*/
async checkVetoWindow(strategyId) {
return this.callTool("check_veto_window", { strategy_id: strategyId });
}
/**
* 对策略行使否决权(需在 veto 窗口期内)
* @param strategyId 策略 ID
* @param reason 否决理由
* @returns 否决结果
*/
async vetoStrategy(strategyId, reason) {
return this.callTool("veto_strategy", {
strategy_id: strategyId,
reason,
});
}
// ═══════════════════════════════════════════════════════
// Phase 5a Security — RBAC + Trust Score
// ═══════════════════════════════════════════════════════
/**
* 设置 Agent 角色(admin only
* @param agentId Agent ID
* @param role 新角色,如 "admin" / "member"
* @param managedGroupId 可选:管理的组 ID
* @returns 设置结果
*/
async setAgentRole(agentId, role, managedGroupId) {
const args = { agent_id: agentId, role };
if (managedGroupId)
args.managed_group_id = managedGroupId;
return this.callTool("set_agent_role", args);
}
/**
* 重新计算 Agent 信任评分(admin only
* @param agentId 可选,不传则重算所有 Agent
* @returns 重算结果
*/
async recalculateTrustScores(agentId) {
const args = {};
if (agentId)
args.agent_id = agentId;
return this.callTool("recalculate_trust_scores", args);
}
// ═══════════════════════════════════════════════════════
// Token 管理 — Token Management
// ═══════════════════════════════════════════════════════
/** 设置 REST API 认证 tokenregister_agent 返回后调用) */
setToken(token) {
this._apiToken = token;
}
/** 撤销 Agent 的 API tokenadmin only */
async revokeToken(agentId) {
return this.callTool("revoke_token", { agent_id: agentId });
}
/** 设置 Agent 信任评分(admin only */
async setTrustScore(agentId, score) {
return this.callTool("set_trust_score", {
agent_id: agentId,
trust_score: score,
});
}
// ═══════════════════════════════════════════════════════
// 记忆搜索 — Memory Search (Phase 6)
// ═══════════════════════════════════════════════════════
/**
* 全文搜索记忆(FTS5)
* @param query 搜索关键词
* @param opts 可选:scope / limit
* @returns 匹配的记忆列表
*/
async searchMemories(query, opts) {
const args = { query };
if (opts?.scope)
args.scope = opts.scope;
if (opts?.limit)
args.limit = opts.limit;
return this.callTool("search_memories", args);
}
// ═══════════════════════════════════════════════════════
// Pipeline 管理 — Pipeline Management (Phase 6)
// ═══════════════════════════════════════════════════════
/**
* 创建 Pipeline(线性任务容器)
* @param name Pipeline 名称
* @param description 可选描述
* @returns { pipelineId: string }
*/
async createPipeline(name, description) {
const args = { name };
if (description)
args.description = description;
return this.callTool("create_pipeline", args);
}
/**
* 获取 Pipeline 详情(含任务列表和依赖关系)
* @param pipelineId Pipeline ID
* @returns Pipeline 详情
*/
async getPipeline(pipelineId) {
return this.callTool("get_pipeline", { pipeline_id: pipelineId });
}
/**
* 列出所有 Pipeline
* @param opts 可选:status / limit
* @returns Pipeline 列表
*/
async listPipelines(opts) {
const args = {};
if (opts?.status)
args.status = opts.status;
if (opts?.limit)
args.limit = opts.limit;
return this.callTool("list_pipelines", args);
}
/**
* 向 Pipeline 添加任务
* @param pipelineId Pipeline ID
* @param description 任务描述
* @param opts 可选:assignedTo / order / dependsOn
* @returns 添加结果
*/
async addTaskToPipeline(pipelineId, description, opts) {
const args = {
pipeline_id: pipelineId,
description,
};
if (opts?.assignedTo)
args.assigned_to = opts.assignedTo;
if (opts?.order !== undefined)
args.order = opts.order;
if (opts?.dependsOn)
args.depends_on = opts.dependsOn;
return this.callTool("add_task_to_pipeline", args);
}
// ═══════════════════════════════════════════════════════
// 消息搜索 — Message Search (Phase 6)
// ═══════════════════════════════════════════════════════
/**
* 全文搜索消息历史(FTS5)
* @param query 搜索关键词
* @param opts 可选:agentId(限定发送方)/ limit
* @returns 匹配的消息列表
*/
async searchMessages(query, opts) {
const args = { query };
if (opts?.agentId)
args.agent_id = opts.agentId;
if (opts?.limit)
args.limit = opts.limit;
return this.callTool("search_messages", args);
}
}
//# sourceMappingURL=agent-client.js.map
@@ -0,0 +1,870 @@
/**
* agent-client.ts — 通用 Agent 客户端 SDK
* WorkBuddy 和 Hermes 都用这个文件接入 Hub
*
* 功能:
* 1. SSE 长连接(自动重连,零轮询)
* 2. MCP 工具调用封装(HTTP POST /mcp,含 initialize 握手)
* 3. 事件路由(new_message / task_assigned / task_updated / pending_messages
*/
import { EventEmitter } from "events";
// ─── 类型定义 ──────────────────────────────────────────
export interface AgentClientOptions {
agentId: string; // 本 Agent 的唯一标识,如 "workbuddy" 或 "hermes"
hubUrl: string; // Hub 地址,如 "http://localhost:3100"
onTaskAssigned?: (task: TaskEvent) => Promise<void>; // 收到新任务时的处理函数
onMessage?: (msg: MessageEvent) => Promise<void>;// 收到消息时的处理函数
onTaskUpdated?: (upd: TaskUpdateEvent) => Promise<void>; // 任务进度回调
reconnectDelay?: number; // 断线重连间隔(ms),默认 3000
mcpTimeout?: number; // MCP 请求超时(ms),默认 15000
}
export interface TaskEvent {
id: string;
assigned_by: string;
assigned_to: string;
description: string;
context?: string;
priority: string;
status: string;
instruction: string;
}
export interface MessageEvent {
id: string;
from_agent: string;
to_agent: string;
content: string;
type: string;
metadata?: Record<string, unknown>;
created_at: number;
}
export interface TaskUpdateEvent {
task_id: string;
status: string;
result?: string;
progress: number;
updated_by: string;
timestamp: number;
}
// ─── AgentClient 类 ────────────────────────────────────
export class AgentClient extends EventEmitter {
private opts: AgentClientOptions;
private sse: any = null; // EventSource 实例
private stopping: boolean = false;
private sessionId: string | null = null; // MCP session ID
private initialized: boolean = false;
private initPromise: Promise<void> | null = null; // 并发安全
private _apiToken: string = ""; // REST API 认证 token
constructor(opts: AgentClientOptions) {
super();
this.opts = {
reconnectDelay: 3000,
mcpTimeout: 15000,
...opts,
};
}
// ── 启动:MCP 握手 + 建立 SSE 连接 ──────────────────
async start(): Promise<void> {
this.stopping = false;
await this.ensureInitialized();
this.connectSSE();
console.log(`[${this.opts.agentId}] 已启动,连接 Hub: ${this.opts.hubUrl}`);
}
stop(): void {
this.stopping = true;
this.initialized = false;
this.sessionId = null;
this.initPromise = null;
this.sse?.close();
console.log(`[${this.opts.agentId}] 已停止`);
}
// ── MCP Initialize 握手(P0 修复)──────────────────
/**
* MCP Streamable HTTP Transport 要求先完成 initialize 握手:
* 1. POST /mcp { method: "initialize", ... }
* 2. 服务端返回 { result: { capabilities, ... } }
* 3. POST /mcp { method: "notifications/initialized" }
*
* 注意:Hub 使用 Stateless 模式,每次请求独立,无需 session ID。
*/
private async ensureInitialized(): Promise<void> {
if (this.initialized) return;
// 防止并发多次握手
if (this.initPromise) return this.initPromise;
this.initPromise = this.doInitialize();
try {
await this.initPromise;
} finally {
this.initPromise = null;
}
}
private async doInitialize(): Promise<void> {
const timeout = this.opts.mcpTimeout!;
// Step 1: initialize 请求(stateless 模式:每次都成功)
const initRes = await this.postMcp(
{
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {
protocolVersion: "2025-03-26",
capabilities: {},
clientInfo: {
name: `agent-client-${this.opts.agentId}`,
version: "1.0.0",
},
},
},
timeout
);
if (initRes.body?.error) {
throw new Error(`MCP initialize failed: ${JSON.stringify(initRes.body.error)}`);
}
console.log(`[${this.opts.agentId}] MCP initialized (stateless)`);
// Step 2: 发送 initialized 通知(无 id 字段 = notification
await this.postMcp(
{
jsonrpc: "2.0",
method: "notifications/initialized",
},
timeout
);
this.initialized = true;
}
/**
* 底层 MCP POST 请求封装
* 返回 { body: parsedJson, sessionId: Mcp-Session-Id header value }
*
* 注意:MCP Streamable HTTP 用 SSE 格式返回响应:
* event: message\n
* data: {"result":...,"jsonrpc":"2.0","id":1}\n
* \n
* Hub 使用 Stateless 模式,每个请求独立,无需 session ID。
*/
private async postMcp(payload: object, timeout: number): Promise<{ body: any; sessionId: string | null }> {
const url = `${this.opts.hubUrl}/mcp`;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
try {
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
},
body: JSON.stringify(payload),
signal: controller.signal,
});
const sessionId = res.headers.get("mcp-session-id");
// 解析 SSE 格式响应:提取 data: 行的 JSON
const raw = await res.text();
let body: any;
if (res.headers.get("content-type")?.includes("text/event-stream")) {
// SSE 格式:找 "data: " 开头的行
const dataLine = raw.split("\n")
.map(line => line.trim())
.find(line => line.startsWith("data: "));
if (dataLine) {
const jsonStr = dataLine.slice(6); // 去掉 "data: " 前缀
body = JSON.parse(jsonStr);
} else {
body = null;
}
} else {
// 普通 JSON 响应
body = raw ? JSON.parse(raw) : null;
}
return { body, sessionId };
} catch (err: any) {
if (err.name === "AbortError") {
throw new Error(`MCP request timeout (${timeout}ms): ${JSON.stringify(payload)}`);
}
throw err;
} finally {
clearTimeout(timer);
}
}
// ── SSE 连接(含自动重连)───────────────────────────
private connectSSE(): void {
const url = `${this.opts.hubUrl}/events/${this.opts.agentId}`;
try {
// 尝试浏览器原生
this.sse = new (globalThis as any).EventSource(url);
} catch {
// Node.js 回退:动态 import eventsource 包(ESM 兼容)
import("eventsource").then((mod: any) => {
this.sse = new (mod.default || mod.EventSource || mod)(url);
this.bindSSEEvents();
});
return; // bindSSEEvents 将在 import 完成后调用
}
this.bindSSEEvents();
}
private bindSSEEvents(): void {
if (!this.sse) return;
// P0-3: 重连超时缩短到 5 秒(原来依赖 opts.reconnectDelay 3000ms
// EventSource 内置重连逻辑由服务端心跳控制,这里用 onerror兜底
this.sse.onmessage = (e: { data: string }) => {
try {
const data = JSON.parse(e.data);
this.routeEvent(data);
} catch (err) {
console.error(`[${this.opts.agentId}] SSE 解析失败:`, err);
}
};
this.sse.onerror = () => {
if (this.stopping) return;
console.warn(`[${this.opts.agentId}] SSE 断线,${this.opts.reconnectDelay}ms 后重连...`);
this.sse?.close();
this.sse = null;
setTimeout(() => {
if (!this.stopping) this.connectSSE();
}, this.opts.reconnectDelay);
};
}
// ── 事件路由 ─────────────────────────────────────────
private async routeEvent(data: any): Promise<void> {
switch (data.event) {
case "task_assigned":
this.emit("task_assigned", data.task);
await this.opts.onTaskAssigned?.(data.task);
break;
case "new_message":
this.emit("new_message", data.message);
await this.opts.onMessage?.(data.message);
break;
case "task_updated":
this.emit("task_updated", data.update);
await this.opts.onTaskUpdated?.(data.update);
break;
case "pending_messages":
for (const msg of data.messages ?? []) {
this.emit("new_message", msg);
await this.opts.onMessage?.(msg);
}
break;
}
}
// ── MCP 工具调用封装 ─────────────────────────────────
private async callTool(toolName: string, args: Record<string, unknown>): Promise<any> {
// 每次调用前确保握手完成
await this.ensureInitialized();
return this._callTool(toolName, args);
}
private async _callTool(toolName: string, args: Record<string, unknown>): Promise<any> {
const { body } = await this.postMcp(
{
jsonrpc: "2.0",
id: crypto.randomUUID(),
method: "tools/call",
params: { name: toolName, arguments: args },
},
this.opts.mcpTimeout!
);
// 错误处理
if (body.error) {
const errMsg = body.error.message ?? JSON.stringify(body.error);
throw new Error(`MCP tool error [${toolName}]: ${errMsg}`);
}
// 从标准 MCP 响应中提取结果
const text = body?.result?.content?.[0]?.text ?? body?.result;
if (typeof text === "string") {
try { return JSON.parse(text); } catch { return text; }
}
return body;
}
// ── 对外 API ─────────────────────────────────────────
/** 发送消息给另一个 Agent */
async sendMessage(to: string, content: string, metadata?: Record<string, unknown>) {
return this.callTool("send_message", {
from: this.opts.agentId, to, content, type: "message", metadata,
});
}
/** 分配任务给另一个 Agent */
async assignTask(to: string, description: string, context?: string, priority?: string) {
return this.callTool("assign_task", {
from: this.opts.agentId, to, description, context,
priority: priority ?? "normal",
});
}
/** 汇报任务进度 */
async updateTaskStatus(
taskId: string,
status: "in_progress" | "completed" | "failed",
result?: string,
progress?: number
) {
return this.callTool("update_task_status", {
task_id: taskId, agent_id: this.opts.agentId, status, result, progress: progress ?? 0,
});
}
/** 查询任务状态 */
async getTaskStatus(taskId: string) {
return this.callTool("get_task_status", { task_id: taskId });
}
/** 查询在线 Agent */
async getOnlineAgents(): Promise<string[]> {
const result = await this.callTool("get_online_agents", {});
return result?.online_agents ?? [];
}
/** 广播消息 */
async broadcast(agentIds: string[], content: string, metadata?: Record<string, unknown>) {
return this.callTool("broadcast_message", {
from: this.opts.agentId, agent_ids: agentIds, content, metadata,
});
}
// ═══════════════════════════════════════════════════════
// Evolution Engine — 经验共享 + 策略传播
// ═══════════════════════════════════════════════════════
/** 分享经验(直接 approved,无需审批) */
async shareExperience(title: string, content: string, tags?: string[], taskId?: string) {
const args: Record<string, unknown> = { title, content };
if (tags) args.tags = tags;
if (taskId) args.task_id = taskId;
return this.callTool("share_experience", args);
}
/** 提议策略(需 admin 审批) */
async proposeStrategy(
title: string, content: string,
category: "workflow" | "fix" | "tool_config" | "prompt_template" | "other" = "workflow",
taskId?: string,
) {
const args: Record<string, unknown> = { title, content, category };
if (taskId) args.task_id = taskId;
return this.callTool("propose_strategy", args);
}
/** 查询策略列表 */
async listStrategies(
opts?: { status?: string; category?: string; proposerId?: string; limit?: number },
) {
const args: Record<string, unknown> = {};
if (opts?.status) args.status = opts.status;
if (opts?.category) args.category = opts.category;
if (opts?.proposerId) args.proposer_id = opts.proposerId;
if (opts?.limit) args.limit = opts.limit;
return this.callTool("list_strategies", args);
}
/** FTS5 全文搜索策略 */
async searchStrategies(query: string, opts?: { category?: string; limit?: number }) {
const args: Record<string, unknown> = { query };
if (opts?.category) args.category = opts.category;
if (opts?.limit) args.limit = opts.limit;
return this.callTool("search_strategies", args);
}
/** 采纳策略 */
async applyStrategy(strategyId: number, context?: string) {
const args: Record<string, unknown> = { strategy_id: strategyId };
if (context) args.context = context;
return this.callTool("apply_strategy", args);
}
/** 对策略反馈(每 Agent 每策略一次) */
async feedbackStrategy(
strategyId: number,
feedback: "positive" | "negative" | "neutral",
opts?: { comment?: string; applied?: boolean },
) {
const args: Record<string, unknown> = { strategy_id: strategyId, feedback };
if (opts?.comment) args.comment = opts.comment;
if (opts?.applied !== undefined) args.applied = opts.applied;
return this.callTool("feedback_strategy", args);
}
/** 审批策略(admin only */
async approveStrategy(strategyId: number, action: "approve" | "reject", reason: string) {
return this.callTool("approve_strategy", {
strategy_id: strategyId, action, reason,
});
}
/** 查看进化指标统计 */
async getEvolutionStatus() {
return this.callTool("get_evolution_status", {});
}
// ═══════════════════════════════════════════════════════
// 记忆模块 — Memory Service
// ═══════════════════════════════════════════════════════
/**
* 存储一条记忆
* @param content 记忆正文
* @param opts 可选字段:title / scope / tags / sourceTaskId
* @returns { memoryId: string }
*/
async storeMemory(
content: string,
opts?: {
title?: string;
scope?: "private" | "group" | "collective";
tags?: string[];
sourceTaskId?: string;
},
) {
const args: Record<string, unknown> = { content };
if (opts?.title) args.title = opts.title;
if (opts?.scope) args.scope = opts.scope;
if (opts?.tags) args.tags = opts.tags;
if (opts?.sourceTaskId) args.source_task_id = opts.sourceTaskId;
return this.callTool("store_memory", args);
}
/**
* 语义搜索记忆(模糊查询)
* @param query 搜索关键词
* @param opts 可选字段:scope / limit
* @returns 匹配的记忆列表
*/
async recallMemory(query: string, opts?: { scope?: string; limit?: number }) {
const args: Record<string, unknown> = { query };
if (opts?.scope) args.scope = opts.scope;
if (opts?.limit) args.limit = opts.limit;
return this.callTool("recall_memory", args);
}
/**
* 列出记忆(分页)
* @param opts 可选字段:scope / limit / offset
* @returns 记忆列表
*/
async listMemories(opts?: { scope?: string; limit?: number; offset?: number }) {
const args: Record<string, unknown> = {};
if (opts?.scope) args.scope = opts.scope;
if (opts?.limit) args.limit = opts.limit;
if (opts?.offset) args.offset = opts.offset;
return this.callTool("list_memories", args);
}
/**
* 删除指定记忆
* @param memoryId 记忆 ID
* @returns 删除结果
*/
async deleteMemory(memoryId: string) {
return this.callTool("delete_memory", { memory_id: memoryId });
}
// ═══════════════════════════════════════════════════════
// 任务模块补充 — Task Extensions
// ═══════════════════════════════════════════════════════
/**
* 通过 REST API 查询任务列表(支持过滤)
* @param status 状态过滤(如 "in_progress"
* @returns 任务列表
*/
async getTasks(status?: string) {
const url = new URL(`${this.opts.hubUrl}/api/tasks`);
if (status) url.searchParams.set("status", status);
const res = await fetch(url.toString(), {
headers: { Authorization: `Bearer ${this._apiToken}` },
});
if (!res.ok) throw new Error(`getTasks failed: ${res.status} ${res.statusText}`);
return res.json();
}
/**
* 取消一个进行中的任务(MCP callTool
* @param taskId 任务 ID
* @returns 取消结果
*/
async cancelTask(taskId: string) {
return this.callTool("cancel_task", { task_id: taskId });
}
// ═══════════════════════════════════════════════════════
// 依赖链 + 并行组 — Dependency Chain & Parallel Groups
// ═══════════════════════════════════════════════════════
/**
* 添加任务依赖关系
* @param upstreamId 上游任务 ID
* @param downstreamId 下游任务 ID
* @param depType 依赖类型,默认 "finish_to_start"
* @returns 添加结果
*/
async addDependency(
upstreamId: string,
downstreamId: string,
depType?: string,
) {
return this.callTool("add_dependency", {
upstream_task_id: upstreamId,
downstream_task_id: downstreamId,
dependency_type: depType ?? "finish_to_start",
});
}
/**
* 移除任务依赖关系
* @param upstreamId 上游任务 ID
* @param downstreamId 下游任务 ID
* @returns 移除结果
*/
async removeDependency(upstreamId: string, downstreamId: string) {
return this.callTool("remove_dependency", {
upstream_task_id: upstreamId,
downstream_task_id: downstreamId,
});
}
/**
* 查询指定任务的所有依赖关系
* @param taskId 任务 ID
* @returns 依赖关系列表
*/
async getTaskDependencies(taskId: string) {
return this.callTool("get_task_dependencies", { task_id: taskId });
}
/**
* 检查指定任务的所有上游依赖是否已满足(全部完成)
* @param taskId 任务 ID
* @returns { satisfied: boolean; missing: string[] }
*/
async checkDependenciesSatisfied(taskId: string) {
return this.callTool("check_dependencies_satisfied", { task_id: taskId });
}
/**
* 创建并行组(组内任务可同时执行)
* @param taskIds 任务 ID 列表
* @param groupName 可选的组名称
* @returns { groupId: string }
*/
async createParallelGroup(taskIds: string[], groupName?: string) {
const args: Record<string, unknown> = { task_ids: taskIds };
if (groupName) args.group_name = groupName;
return this.callTool("create_parallel_group", args);
}
// ═══════════════════════════════════════════════════════
// 交接协议 — Handoff Protocol
// ═══════════════════════════════════════════════════════
/**
* 请求任务交接给另一个 Agent
* @param taskId 任务 ID
* @param targetAgentId 目标 Agent ID
* @returns 交接请求结果
*/
async requestHandoff(taskId: string, targetAgentId: string) {
return this.callTool("request_handoff", {
task_id: taskId,
target_agent_id: targetAgentId,
});
}
/**
* 接受任务交接
* @param taskId 任务 ID
* @returns 接受结果
*/
async acceptHandoff(taskId: string) {
return this.callTool("accept_handoff", { task_id: taskId });
}
/**
* 拒绝任务交接
* @param taskId 任务 ID
* @param reason 拒绝原因
* @returns 拒绝结果
*/
async rejectHandoff(taskId: string, reason?: string) {
const args: Record<string, unknown> = { task_id: taskId };
if (reason) args.reason = reason;
return this.callTool("reject_handoff", args);
}
// ═══════════════════════════════════════════════════════
// 质量门 — Quality Gates
// ═══════════════════════════════════════════════════════
/**
* 为 Pipeline 添加质量门
* @param pipelineId Pipeline ID
* @param gateName 质量门名称
* @param criteria 通过标准(SQL WHERE 条件或描述文本)
* @param afterOrder 在哪个步骤之后插入
* @returns 添加结果
*/
async addQualityGate(
pipelineId: string,
gateName: string,
criteria: string,
afterOrder: number,
) {
return this.callTool("add_quality_gate", {
pipeline_id: pipelineId,
gate_name: gateName,
criteria,
after_order: afterOrder,
});
}
/**
* 评估质量门结果
* @param gateId 质量门 ID
* @param status 评估结果 "passed" | "failed"
* @param result 可选的详细结果描述
* @returns 评估结果
*/
async evaluateQualityGate(
gateId: string,
status: "passed" | "failed",
result?: string,
) {
const args: Record<string, unknown> = { gate_id: gateId, status };
if (result) args.result = result;
return this.callTool("evaluate_quality_gate", args);
}
// ═══════════════════════════════════════════════════════
// 分级审批 — Tiered Strategy Approval
// ═══════════════════════════════════════════════════════
/**
* 提议策略(支持分级审批,自动根据策略内容判断 tier)
* @param title 策略标题
* @param content 策略正文
* @param opts 可选:category / taskId
* @returns 策略提案结果
*/
async proposeStrategyTiered(
title: string,
content: string,
opts?: { category?: string; taskId?: string },
) {
const args: Record<string, unknown> = { title, content };
if (opts?.category) args.category = opts.category;
if (opts?.taskId) args.task_id = opts.taskId;
return this.callTool("propose_strategy_tiered", args);
}
/**
* 检查策略是否处于 veto 窗口期(可行使否决权的时间窗口)
* @param strategyId 策略 ID
* @returns { in_window: boolean; remaining_seconds?: number }
*/
async checkVetoWindow(strategyId: number) {
return this.callTool("check_veto_window", { strategy_id: strategyId });
}
/**
* 对策略行使否决权(需在 veto 窗口期内)
* @param strategyId 策略 ID
* @param reason 否决理由
* @returns 否决结果
*/
async vetoStrategy(strategyId: number, reason: string) {
return this.callTool("veto_strategy", {
strategy_id: strategyId,
reason,
});
}
// ═══════════════════════════════════════════════════════
// Phase 5a Security — RBAC + Trust Score
// ═══════════════════════════════════════════════════════
/**
* 设置 Agent 角色(admin only
* @param agentId Agent ID
* @param role 新角色,如 "admin" / "member"
* @param managedGroupId 可选:管理的组 ID
* @returns 设置结果
*/
async setAgentRole(agentId: string, role: string, managedGroupId?: string) {
const args: Record<string, unknown> = { agent_id: agentId, role };
if (managedGroupId) args.managed_group_id = managedGroupId;
return this.callTool("set_agent_role", args);
}
/**
* 重新计算 Agent 信任评分(admin only
* @param agentId 可选,不传则重算所有 Agent
* @returns 重算结果
*/
async recalculateTrustScores(agentId?: string) {
const args: Record<string, unknown> = {};
if (agentId) args.agent_id = agentId;
return this.callTool("recalculate_trust_scores", args);
}
// ═══════════════════════════════════════════════════════
// Token 管理 — Token Management
// ═══════════════════════════════════════════════════════
/** 设置 REST API 认证 tokenregister_agent 返回后调用) */
setToken(token: string): void {
this._apiToken = token;
}
/** 撤销 Agent 的 API tokenadmin only */
async revokeToken(agentId: string) {
return this.callTool("revoke_token", { agent_id: agentId });
}
/** 设置 Agent 信任评分(admin only */
async setTrustScore(agentId: string, score: number) {
return this.callTool("set_trust_score", {
agent_id: agentId,
trust_score: score,
});
}
// ═══════════════════════════════════════════════════════
// 记忆搜索 — Memory Search (Phase 6)
// ═══════════════════════════════════════════════════════
/**
* 全文搜索记忆(FTS5
* @param query 搜索关键词
* @param opts 可选:scope / limit
* @returns 匹配的记忆列表
*/
async searchMemories(
query: string,
opts?: { scope?: string; limit?: number },
) {
const args: Record<string, unknown> = { query };
if (opts?.scope) args.scope = opts.scope;
if (opts?.limit) args.limit = opts.limit;
return this.callTool("search_memories", args);
}
// ═══════════════════════════════════════════════════════
// Pipeline 管理 — Pipeline Management (Phase 6)
// ═══════════════════════════════════════════════════════
/**
* 创建 Pipeline(线性任务容器)
* @param name Pipeline 名称
* @param description 可选描述
* @returns { pipelineId: string }
*/
async createPipeline(name: string, description?: string) {
const args: Record<string, unknown> = { name };
if (description) args.description = description;
return this.callTool("create_pipeline", args);
}
/**
* 获取 Pipeline 详情(含任务列表和依赖关系)
* @param pipelineId Pipeline ID
* @returns Pipeline 详情
*/
async getPipeline(pipelineId: string) {
return this.callTool("get_pipeline", { pipeline_id: pipelineId });
}
/**
* 列出所有 Pipeline
* @param opts 可选:status / limit
* @returns Pipeline 列表
*/
async listPipelines(opts?: { status?: string; limit?: number }) {
const args: Record<string, unknown> = {};
if (opts?.status) args.status = opts.status;
if (opts?.limit) args.limit = opts.limit;
return this.callTool("list_pipelines", args);
}
/**
* 向 Pipeline 添加任务
* @param pipelineId Pipeline ID
* @param description 任务描述
* @param opts 可选:assignedTo / order / dependsOn
* @returns 添加结果
*/
async addTaskToPipeline(
pipelineId: string,
description: string,
opts?: {
assignedTo?: string;
order?: number;
dependsOn?: string;
},
) {
const args: Record<string, unknown> = {
pipeline_id: pipelineId,
description,
};
if (opts?.assignedTo) args.assigned_to = opts.assignedTo;
if (opts?.order !== undefined) args.order = opts.order;
if (opts?.dependsOn) args.depends_on = opts.dependsOn;
return this.callTool("add_task_to_pipeline", args);
}
// ═══════════════════════════════════════════════════════
// 消息搜索 — Message Search (Phase 6)
// ═══════════════════════════════════════════════════════
/**
* 全文搜索消息历史(FTS5)
* @param query 搜索关键词
* @param opts 可选:agentId(限定发送方)/ limit
* @returns 匹配的消息列表
*/
async searchMessages(
query: string,
opts?: { agentId?: string; limit?: number },
) {
const args: Record<string, unknown> = { query };
if (opts?.agentId) args.agent_id = opts.agentId;
if (opts?.limit) args.limit = opts.limit;
return this.callTool("search_messages", args);
}
}
@@ -0,0 +1,25 @@
/**
* hermes-integration.ts
* Hermes 侧接入示例
*
* Hermes 需要做的配置(3步,10分钟完成):
*
* 步骤 1:安装依赖
* npm install eventsource
*
* 步骤 2:在 Hermes 的启动脚本/入口文件中引入本文件
* import "./hermes-integration.js";
*
* 步骤 3:设置环境变量
* export HUB_URL=http://localhost:3100 (Hub 服务器地址)
* export HERMES_ID=hermes (本 Agent 的唯一 ID,可自定义)
*
* 完成!Hermes 启动后会自动:
* - 连接 Hub 的 SSE 端点
* - 接收 WorkBuddy 分配的任务并自主执行
* - 汇报执行进度和结果
* - 断线后自动重连
*/
import { AgentClient } from "../client-sdk/agent-client.js";
declare const hermes: AgentClient;
export { hermes };
@@ -0,0 +1,121 @@
/**
* hermes-integration.ts
* Hermes 侧接入示例
*
* Hermes 需要做的配置(3步,10分钟完成):
*
* 步骤 1:安装依赖
* npm install eventsource
*
* 步骤 2:在 Hermes 的启动脚本/入口文件中引入本文件
* import "./hermes-integration.js";
*
* 步骤 3:设置环境变量
* export HUB_URL=http://localhost:3100 (Hub 服务器地址)
* export HERMES_ID=hermes (本 Agent 的唯一 ID,可自定义)
*
* 完成!Hermes 启动后会自动:
* - 连接 Hub 的 SSE 端点
* - 接收 WorkBuddy 分配的任务并自主执行
* - 汇报执行进度和结果
* - 断线后自动重连
*/
import { AgentClient } from "../client-sdk/agent-client.js";
const HERMES_ID = process.env.HERMES_ID ?? "hermes";
const HUB_URL = process.env.HUB_URL ?? "http://localhost:3100";
// ─── 1. 创建 Hermes 客户端 ─────────────────────────────
const hermes = new AgentClient({
agentId: HERMES_ID,
hubUrl: HUB_URL,
// ╔══════════════════════════════════════════════════╗
// ║ 核心:收到任务时自主执行,无需人工干预 ║
// ╚══════════════════════════════════════════════════╝
onTaskAssigned: async (task) => {
console.log(`\n[Hermes] 📋 收到来自 ${task.assigned_by} 的任务`);
console.log(` 任务ID: ${task.id}`);
console.log(` 优先级: ${task.priority}`);
console.log(` 描述: ${task.description}`);
if (task.context)
console.log(` 上下文: ${task.context}`);
// ── 第一步:立刻回报"已接收,开始执行" ─────────────
await hermes.updateTaskStatus(task.id, "in_progress", undefined, 5);
try {
// ── 第二步:调用 Hermes 自己的执行能力 ────────────
// 在这里对接你的 Hermes Agent 核心逻辑
// 可以是:LLM 调用、工具调用、文件操作、数据处理等
const result = await executeHermesTask(task);
// ── 第三步:汇报完成 ────────────────────────────
await hermes.updateTaskStatus(task.id, "completed", result, 100);
console.log(`[Hermes] ✅ 任务 ${task.id} 已完成`);
}
catch (err) {
await hermes.updateTaskStatus(task.id, "failed", `执行错误: ${err.message}`, 0);
console.error(`[Hermes] ❌ 任务 ${task.id} 失败:`, err.message);
}
},
// ── 收到普通消息 ───────────────────────────────────
onMessage: async (msg) => {
console.log(`\n[Hermes] 💬 来自 ${msg.from_agent}: ${msg.content}`);
// 处理不同类型的消息
if (msg.type === "ack") {
console.log(`[Hermes] 收到确认消息,无需回复`);
return;
}
// 普通消息,可触发 Hermes 的对话能力
await handleHermesMessage(msg);
},
// ── 收到任务进度更新(自己委托给别人的任务)────────────
onTaskUpdated: async (upd) => {
const icon = upd.status === "completed" ? "✅" : upd.status === "failed" ? "❌" : "⏳";
console.log(`\n[Hermes] ${icon} 委托任务进度: ${upd.task_id}`);
console.log(` 状态: ${upd.status} 进度: ${upd.progress}%`);
if (upd.result) {
console.log(` 结果: ${upd.result}`);
// 可以在这里把 WorkBuddy 的执行结果进一步处理
await processTaskResult(upd.task_id, upd.result);
}
},
});
// ─── 2. 启动 Hermes 客户端 ─────────────────────────────
hermes.start();
console.log(`[Hermes] 已启动,Agent ID: ${HERMES_ID}`);
console.log(`[Hermes] 正在连接 Hub: ${HUB_URL}`);
// ─── 任务执行核心逻辑(对接你的 Hermes 业务代码)──────────
async function executeHermesTask(task) {
const { description, context, id } = task;
// ── 中途汇报进度示例 ───────────────────────────────
await hermes.updateTaskStatus(id, "in_progress", "正在收集数据...", 20);
// TODO: 在这里对接 Hermes 的实际能力:
// - 调用 LLM(如 Claude API
// - 执行 MCP 工具(WebSearch、文件读写等)
// - 访问数据库或外部 API
// - 运行 Python 脚本等
// 示例:模拟分阶段执行
await new Promise(r => setTimeout(r, 1500));
await hermes.updateTaskStatus(id, "in_progress", "正在分析处理...", 60);
await new Promise(r => setTimeout(r, 1500));
await hermes.updateTaskStatus(id, "in_progress", "正在生成报告...", 90);
await new Promise(r => setTimeout(r, 500));
// 返回结构化结果
return JSON.stringify({
summary: `Hermes 完成了任务:${description.slice(0, 80)}`,
data: { processed: true, context },
timestamp: new Date().toISOString(),
}, null, 2);
}
// ─── 消息处理逻辑 ──────────────────────────────────────
async function handleHermesMessage(msg) {
// TODO: 根据业务需求处理消息
// 示例:简单回复
if (msg.content.includes("你好")) {
await hermes.sendMessage(msg.from_agent, "你好!Hermes 在线,随时待命。");
}
}
// ─── 处理收到的任务结果 ────────────────────────────────
async function processTaskResult(taskId, result) {
// TODO: 处理 WorkBuddy 返回的结果
console.log(`[Hermes] 处理任务 ${taskId} 的结果...`);
}
// ─── 导出实例(供其他模块使用)──────────────────────────
export { hermes };
//# sourceMappingURL=hermes-integration.js.map
@@ -0,0 +1,140 @@
/**
* hermes-integration.ts
* Hermes 侧接入示例
*
* Hermes 需要做的配置(3步,10分钟完成):
*
* 步骤 1:安装依赖
* npm install eventsource
*
* 步骤 2:在 Hermes 的启动脚本/入口文件中引入本文件
* import "./hermes-integration.js";
*
* 步骤 3:设置环境变量
* export HUB_URL=http://localhost:3100 (Hub 服务器地址)
* export HERMES_ID=hermes (本 Agent 的唯一 ID,可自定义)
*
* 完成!Hermes 启动后会自动:
* - 连接 Hub 的 SSE 端点
* - 接收 WorkBuddy 分配的任务并自主执行
* - 汇报执行进度和结果
* - 断线后自动重连
*/
import { AgentClient } from "../client-sdk/agent-client.js";
const HERMES_ID = process.env.HERMES_ID ?? "hermes";
const HUB_URL = process.env.HUB_URL ?? "http://localhost:3100";
// ─── 1. 创建 Hermes 客户端 ─────────────────────────────
const hermes = new AgentClient({
agentId: HERMES_ID,
hubUrl: HUB_URL,
// ╔══════════════════════════════════════════════════╗
// ║ 核心:收到任务时自主执行,无需人工干预 ║
// ╚══════════════════════════════════════════════════╝
onTaskAssigned: async (task) => {
console.log(`\n[Hermes] 📋 收到来自 ${task.assigned_by} 的任务`);
console.log(` 任务ID: ${task.id}`);
console.log(` 优先级: ${task.priority}`);
console.log(` 描述: ${task.description}`);
if (task.context) console.log(` 上下文: ${task.context}`);
// ── 第一步:立刻回报"已接收,开始执行" ─────────────
await hermes.updateTaskStatus(task.id, "in_progress", undefined, 5);
try {
// ── 第二步:调用 Hermes 自己的执行能力 ────────────
// 在这里对接你的 Hermes Agent 核心逻辑
// 可以是:LLM 调用、工具调用、文件操作、数据处理等
const result = await executeHermesTask(task);
// ── 第三步:汇报完成 ────────────────────────────
await hermes.updateTaskStatus(task.id, "completed", result, 100);
console.log(`[Hermes] ✅ 任务 ${task.id} 已完成`);
} catch (err: any) {
await hermes.updateTaskStatus(task.id, "failed", `执行错误: ${err.message}`, 0);
console.error(`[Hermes] ❌ 任务 ${task.id} 失败:`, err.message);
}
},
// ── 收到普通消息 ───────────────────────────────────
onMessage: async (msg) => {
console.log(`\n[Hermes] 💬 来自 ${msg.from_agent}: ${msg.content}`);
// 处理不同类型的消息
if (msg.type === "ack") {
console.log(`[Hermes] 收到确认消息,无需回复`);
return;
}
// 普通消息,可触发 Hermes 的对话能力
await handleHermesMessage(msg);
},
// ── 收到任务进度更新(自己委托给别人的任务)────────────
onTaskUpdated: async (upd) => {
const icon = upd.status === "completed" ? "✅" : upd.status === "failed" ? "❌" : "⏳";
console.log(`\n[Hermes] ${icon} 委托任务进度: ${upd.task_id}`);
console.log(` 状态: ${upd.status} 进度: ${upd.progress}%`);
if (upd.result) {
console.log(` 结果: ${upd.result}`);
// 可以在这里把 WorkBuddy 的执行结果进一步处理
await processTaskResult(upd.task_id, upd.result);
}
},
});
// ─── 2. 启动 Hermes 客户端 ─────────────────────────────
hermes.start();
console.log(`[Hermes] 已启动,Agent ID: ${HERMES_ID}`);
console.log(`[Hermes] 正在连接 Hub: ${HUB_URL}`);
// ─── 任务执行核心逻辑(对接你的 Hermes 业务代码)──────────
async function executeHermesTask(task: any): Promise<string> {
const { description, context, id } = task;
// ── 中途汇报进度示例 ───────────────────────────────
await hermes.updateTaskStatus(id, "in_progress", "正在收集数据...", 20);
// TODO: 在这里对接 Hermes 的实际能力:
// - 调用 LLM(如 Claude API
// - 执行 MCP 工具(WebSearch、文件读写等)
// - 访问数据库或外部 API
// - 运行 Python 脚本等
// 示例:模拟分阶段执行
await new Promise(r => setTimeout(r, 1500));
await hermes.updateTaskStatus(id, "in_progress", "正在分析处理...", 60);
await new Promise(r => setTimeout(r, 1500));
await hermes.updateTaskStatus(id, "in_progress", "正在生成报告...", 90);
await new Promise(r => setTimeout(r, 500));
// 返回结构化结果
return JSON.stringify({
summary: `Hermes 完成了任务:${description.slice(0, 80)}`,
data: { processed: true, context },
timestamp: new Date().toISOString(),
}, null, 2);
}
// ─── 消息处理逻辑 ──────────────────────────────────────
async function handleHermesMessage(msg: any): Promise<void> {
// TODO: 根据业务需求处理消息
// 示例:简单回复
if (msg.content.includes("你好")) {
await hermes.sendMessage(msg.from_agent, "你好!Hermes 在线,随时待命。");
}
}
// ─── 处理收到的任务结果 ────────────────────────────────
async function processTaskResult(taskId: string, result: string): Promise<void> {
// TODO: 处理 WorkBuddy 返回的结果
console.log(`[Hermes] 处理任务 ${taskId} 的结果...`);
}
// ─── 导出实例(供其他模块使用)──────────────────────────
export { hermes };
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
export {};
@@ -0,0 +1,80 @@
/**
* workbuddy-integration.ts
* WorkBuddy 侧接入示例
*
* 这个文件展示 WorkBuddy 如何:
* 1. 连接 Hub(一行代码)
* 2. 向 Hermes 分配任务
* 3. 实时接收 Hermes 的执行结果
* 4. 处理 Hermes 发来的协作请求
*/
import { AgentClient } from "../client-sdk/agent-client.js";
// ─── 1. 创建 WorkBuddy 客户端 ──────────────────────────
const workbuddy = new AgentClient({
agentId: "workbuddy",
hubUrl: process.env.HUB_URL ?? "http://localhost:3100",
// ── 收到任务时(Hermes 委托 WorkBuddy 做某事)──────────
onTaskAssigned: async (task) => {
console.log(`\n[WorkBuddy] 📋 收到来自 ${task.assigned_by} 的任务`);
console.log(` 描述: ${task.description}`);
console.log(` 优先级: ${task.priority}`);
// 立刻回报"已开始"
await workbuddy.updateTaskStatus(task.id, "in_progress", undefined, 0);
try {
// ── 在这里放 WorkBuddy 的实际执行逻辑 ──────────────
const result = await executeWorkBuddyTask(task.description, task.context);
await workbuddy.updateTaskStatus(task.id, "completed", result, 100);
console.log(`[WorkBuddy] ✅ 任务 ${task.id} 完成`);
}
catch (err) {
await workbuddy.updateTaskStatus(task.id, "failed", err.message, 0);
console.error(`[WorkBuddy] ❌ 任务 ${task.id} 失败:`, err.message);
}
},
// ── 收到普通消息 ──────────────────────────────────────
onMessage: async (msg) => {
console.log(`\n[WorkBuddy] 💬 来自 ${msg.from_agent}: ${msg.content}`);
// 根据消息内容决定是否需要回复
if (msg.content.includes("确认")) {
await workbuddy.sendMessage(msg.from_agent, "已确认,WorkBuddy 收到。");
}
},
// ── 任务进度回调(自己发出去的任务被执行时触发)─────────
onTaskUpdated: async (upd) => {
const icon = upd.status === "completed" ? "✅" : upd.status === "failed" ? "❌" : "⏳";
console.log(`\n[WorkBuddy] ${icon} 任务 ${upd.task_id} 进度更新`);
console.log(` 状态: ${upd.status} 进度: ${upd.progress}%`);
if (upd.result)
console.log(` 结果: ${upd.result}`);
},
});
// ─── 2. 启动 ───────────────────────────────────────────
workbuddy.start();
// ─── 3. 示例:向 Hermes 分配任务 ──────────────────────
async function runDemo() {
// 等待连接稳定
await new Promise(r => setTimeout(r, 1000));
// 先检查 Hermes 是否在线
const online = await workbuddy.getOnlineAgents();
console.log("\n[WorkBuddy] 当前在线 Agents:", online);
if (online.includes("hermes")) {
// 分配任务给 Hermes
const result = await workbuddy.assignTask("hermes", "请分析最近 7 天辽宁省媒体融合相关新闻,提取关键事件并按重要性排序,输出 Markdown 格式报告", "重点关注:辽望客户端、北斗融媒、省级媒体政策。输出结构:摘要 + 事件列表 + 趋势分析", "high");
console.log("\n[WorkBuddy] 任务已分配:", result);
}
else {
console.log("[WorkBuddy] Hermes 不在线,任务将在其上线后自动推送");
// 即使离线也可以分配,Hub 会自动补发
await workbuddy.assignTask("hermes", "这是一条离线任务,Hermes 上线后会自动收到并执行", "", "normal");
}
}
// ─── 任务执行逻辑(接入你的实际业务代码)──────────────
async function executeWorkBuddyTask(description, context) {
// TODO: 替换为 WorkBuddy 真实的 Agent SDK 调用
console.log(`[WorkBuddy] 执行任务: ${description}`);
await new Promise(r => setTimeout(r, 2000)); // 模拟执行耗时
return `WorkBuddy 执行完成: ${description.slice(0, 50)}...`;
}
// 运行示例
runDemo().catch(console.error);
//# sourceMappingURL=workbuddy-integration.js.map
@@ -0,0 +1,99 @@
/**
* workbuddy-integration.ts
* WorkBuddy 侧接入示例
*
* 这个文件展示 WorkBuddy 如何:
* 1. 连接 Hub(一行代码)
* 2. 向 Hermes 分配任务
* 3. 实时接收 Hermes 的执行结果
* 4. 处理 Hermes 发来的协作请求
*/
import { AgentClient } from "../client-sdk/agent-client.js";
// ─── 1. 创建 WorkBuddy 客户端 ──────────────────────────
const workbuddy = new AgentClient({
agentId: "workbuddy",
hubUrl: process.env.HUB_URL ?? "http://localhost:3100",
// ── 收到任务时(Hermes 委托 WorkBuddy 做某事)──────────
onTaskAssigned: async (task) => {
console.log(`\n[WorkBuddy] 📋 收到来自 ${task.assigned_by} 的任务`);
console.log(` 描述: ${task.description}`);
console.log(` 优先级: ${task.priority}`);
// 立刻回报"已开始"
await workbuddy.updateTaskStatus(task.id, "in_progress", undefined, 0);
try {
// ── 在这里放 WorkBuddy 的实际执行逻辑 ──────────────
const result = await executeWorkBuddyTask(task.description, task.context);
await workbuddy.updateTaskStatus(task.id, "completed", result, 100);
console.log(`[WorkBuddy] ✅ 任务 ${task.id} 完成`);
} catch (err: any) {
await workbuddy.updateTaskStatus(task.id, "failed", err.message, 0);
console.error(`[WorkBuddy] ❌ 任务 ${task.id} 失败:`, err.message);
}
},
// ── 收到普通消息 ──────────────────────────────────────
onMessage: async (msg) => {
console.log(`\n[WorkBuddy] 💬 来自 ${msg.from_agent}: ${msg.content}`);
// 根据消息内容决定是否需要回复
if (msg.content.includes("确认")) {
await workbuddy.sendMessage(msg.from_agent, "已确认,WorkBuddy 收到。");
}
},
// ── 任务进度回调(自己发出去的任务被执行时触发)─────────
onTaskUpdated: async (upd) => {
const icon = upd.status === "completed" ? "✅" : upd.status === "failed" ? "❌" : "⏳";
console.log(`\n[WorkBuddy] ${icon} 任务 ${upd.task_id} 进度更新`);
console.log(` 状态: ${upd.status} 进度: ${upd.progress}%`);
if (upd.result) console.log(` 结果: ${upd.result}`);
},
});
// ─── 2. 启动 ───────────────────────────────────────────
workbuddy.start();
// ─── 3. 示例:向 Hermes 分配任务 ──────────────────────
async function runDemo() {
// 等待连接稳定
await new Promise(r => setTimeout(r, 1000));
// 先检查 Hermes 是否在线
const online = await workbuddy.getOnlineAgents();
console.log("\n[WorkBuddy] 当前在线 Agents:", online);
if (online.includes("hermes")) {
// 分配任务给 Hermes
const result = await workbuddy.assignTask(
"hermes",
"请分析最近 7 天辽宁省媒体融合相关新闻,提取关键事件并按重要性排序,输出 Markdown 格式报告",
"重点关注:辽望客户端、北斗融媒、省级媒体政策。输出结构:摘要 + 事件列表 + 趋势分析",
"high"
);
console.log("\n[WorkBuddy] 任务已分配:", result);
} else {
console.log("[WorkBuddy] Hermes 不在线,任务将在其上线后自动推送");
// 即使离线也可以分配,Hub 会自动补发
await workbuddy.assignTask(
"hermes",
"这是一条离线任务,Hermes 上线后会自动收到并执行",
"",
"normal"
);
}
}
// ─── 任务执行逻辑(接入你的实际业务代码)──────────────
async function executeWorkBuddyTask(description: string, context?: string): Promise<string> {
// TODO: 替换为 WorkBuddy 真实的 Agent SDK 调用
console.log(`[WorkBuddy] 执行任务: ${description}`);
await new Promise(r => setTimeout(r, 2000)); // 模拟执行耗时
return `WorkBuddy 执行完成: ${description.slice(0, 50)}...`;
}
// 运行示例
runDemo().catch(console.error);
@@ -0,0 +1,65 @@
# Agent Comm Hub — 可观测性栈(Phase 3.1
# 用法:docker compose up -d
version: "3.8"
services:
# ── Prometheus ──────────────────────────────────────────────
prometheus:
image: prom/prometheus:v2.47.0
container_name: ach-prometheus
restart: unless-stopped
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.retention.time=15d'
- '--storage.tsdb.path=/prometheus'
networks:
- ach-net
# ── Grafana ───────────────────────────────────────────────
grafana:
image: grafana/grafana:10.1.0
container_name: ach-grafana
restart: unless-stopped
ports:
- "3000:3000"
environment:
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: admin
GF_USERS_ALLOW_SIGN_UP: "false"
volumes:
- ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources:ro
- ./grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards:ro
- ./grafana/dashboard.json:/etc/grafana/provisioning/dashboards/agent-comm-hub.json:ro
- grafana_data:/var/lib/grafana
depends_on:
- prometheus
networks:
- ach-net
# ── Hub(本栈监控目标,需另启)───────────────────────────
# agent-comm-hub:
# image: liuboacean/agent-comm-hub:latest
# container_name: ach-hub
# environment:
# PORT: "3100"
# DB_PATH: /app/comm_hub.db
# ports:
# - "3100:3100"
# extra_hosts:
# - "host.docker.internal:host-gateway"
# networks:
# - ach-net
networks:
ach-net:
driver: bridge
volumes:
prometheus_data:
grafana_data:
@@ -0,0 +1,618 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"id": null,
"links": [],
"liveNow": false,
"panels": [
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 100,
"panels": [],
"title": "Hub 概览",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "red", "value": null },
{ "color": "green", "value": 1 }
]
},
"unit": "none"
},
"overrides": []
},
"gridPos": { "h": 4, "w": 4, "x": 0, "y": 1 },
"id": 1,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": ["lastNotNull"],
"fields": "",
"values": false
},
"textMode": "auto"
},
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "hub_agents_online",
"legendFormat": "在线 Agent",
"refId": "A"
}
],
"title": "在线 Agent 数",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [{ "color": "green", "value": null }]
},
"unit": "none"
},
"overrides": []
},
"gridPos": { "h": 4, "w": 4, "x": 4, "y": 1 },
"id": 2,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": ["lastNotNull"],
"fields": "",
"values": false
},
"textMode": "auto"
},
"targets": [
{
"expr": "active_sse_connections",
"legendFormat": "SSE 连接",
"refId": "A"
}
],
"title": "活跃 SSE 连接数",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"cellOptions": { "type": "auto" },
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "red", "value": null },
{ "color": "orange", "value": 30 },
{ "color": "green", "value": 60 }
]
}
},
"overrides": [
{
"matcher": { "id": "byName", "options": "trust_score" },
"properties": [
{
"id": "custom.cellOptions",
"value": { "type": "color-background" }
}
]
}
]
},
"gridPos": { "h": 4, "w": 16, "x": 8, "y": 1 },
"id": 3,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"fields": "",
"reducer": ["sum"],
"show": false
},
"showHeader": true
},
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "hub_trust_scores",
"format": "table",
"instant": true,
"legendFormat": "",
"refId": "A"
}
],
"title": "Agent Trust Scores",
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": { "Time": true },
"indexByName": {},
"renameByName": {
"Value": "trust_score",
"agent_id": "Agent ID"
}
}
}
],
"type": "table"
},
{
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 },
"id": 101,
"panels": [],
"title": "消息吞吐",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": { "type": "linear" },
"showPoints": "never",
"spanNulls": false,
"stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [{ "color": "green", "value": null }]
},
"unit": "short"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 6 },
"id": 10,
"options": {
"legend": {
"calcs": ["mean", "max"],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": { "mode": "multi", "sort": "none" }
},
"targets": [
{
"expr": "rate(hub_messages_total[5m])",
"legendFormat": "{{status}}",
"refId": "A"
}
],
"title": "消息吞吐量 (rate 5m)",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": { "type": "linear" },
"showPoints": "never",
"spanNulls": false,
"stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [{ "color": "green", "value": null }]
},
"unit": "short"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 6 },
"id": 11,
"options": {
"legend": {
"calcs": ["mean", "max"],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": { "mode": "multi", "sort": "none" }
},
"targets": [
{
"expr": "rate(active_sse_connections[1m])",
"legendFormat": "SSE 连接",
"refId": "A"
}
],
"title": "SSE 连接数 (Gauge 快照)",
"type": "timeseries"
},
{
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 14 },
"id": 102,
"panels": [],
"title": "MCP 工具调用",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 20,
"gradientMode": "none",
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": { "type": "linear" },
"showPoints": "never",
"spanNulls": false,
"stacking": { "group": "A", "mode": "normal" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [{ "color": "green", "value": null }]
},
"unit": "short"
},
"overrides": [
{
"matcher": { "id": "byRegexp", "options": ".*error.*" },
"properties": [{ "id": "color", "value": { "fixedColor": "red", "mode": "fixed" } }]
},
{
"matcher": { "id": "byRegexp", "options": ".*denied.*" },
"properties": [{ "id": "color", "value": { "fixedColor": "orange", "mode": "fixed" } }]
}
]
},
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 15 },
"id": 20,
"options": {
"legend": {
"calcs": ["sum"],
"displayMode": "table",
"placement": "right",
"showLegend": true
},
"tooltip": { "mode": "multi", "sort": "desc" }
},
"targets": [
{
"expr": "topk(10, rate(mcp_calls_total[5m]))",
"legendFormat": "{{tool_name}} / {{status}}",
"refId": "A"
}
],
"title": "MCP 工具调用 Top10 (rate 5m)",
"type": "timeseries"
},
{
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 23 },
"id": 103,
"panels": [],
"title": "HTTP 与数据库延迟",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": { "type": "linear" },
"showPoints": "never",
"spanNulls": false,
"stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [{ "color": "green", "value": null }]
},
"unit": "reqps"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 24 },
"id": 30,
"options": {
"legend": {
"calcs": ["mean", "max"],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": { "mode": "multi", "sort": "none" }
},
"targets": [
{
"expr": "rate(http_requests_total[5m])",
"legendFormat": "{{method}} {{path}} {{status}}",
"refId": "A"
}
],
"title": "HTTP 请求速率 (rate 5m)",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": { "type": "linear" },
"showPoints": "never",
"spanNulls": false,
"stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [{ "color": "green", "value": null }]
},
"unit": "ms"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 24 },
"id": 31,
"options": {
"legend": {
"calcs": ["mean", "max"],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": { "mode": "multi", "sort": "none" }
},
"targets": [
{
"expr": "histogram_quantile(0.50, rate(http_request_duration_ms_bucket[5m]))",
"legendFormat": "P50",
"refId": "A"
},
{
"expr": "histogram_quantile(0.95, rate(http_request_duration_ms_bucket[5m]))",
"legendFormat": "P95",
"refId": "B"
},
{
"expr": "histogram_quantile(0.99, rate(http_request_duration_ms_bucket[5m]))",
"legendFormat": "P99",
"refId": "C"
}
],
"title": "HTTP 请求延迟 Percentile",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": { "type": "linear" },
"showPoints": "never",
"spanNulls": false,
"stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [{ "color": "green", "value": null }]
},
"unit": "ms"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 32 },
"id": 32,
"options": {
"legend": {
"calcs": ["mean", "max"],
"displayMode": "table",
"placement": "right",
"showLegend": true
},
"tooltip": { "mode": "multi", "sort": "none" }
},
"targets": [
{
"expr": "rate(db_query_duration_ms_sum[5m]) / rate(db_query_duration_ms_count[5m])",
"legendFormat": "{{operation}} avg",
"refId": "A"
}
],
"title": "DB 查询延迟均值 (rate 5m)",
"type": "timeseries"
}
],
"refresh": "10s",
"schemaVersion": 38,
"style": "dark",
"tags": ["agent-comm-hub", "phase-3.1"],
"templating": {
"list": []
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "Agent Comm Hub — 监控仪表盘",
"uid": "agent-comm-hub-p3",
"version": 1,
"weekStart": ""
}
@@ -0,0 +1,13 @@
apiVersion: 1
providers:
- name: 'Agent Comm Hub'
orgId: 1
folder: ''
folderUid: ''
type: file
disableDeletion: false
updateIntervalSeconds: 30
allowUiUpdates: true
options:
path: /etc/grafana/provisioning/dashboards
@@ -0,0 +1,10 @@
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
uid: ${DS_PROMETHEUS}
access: proxy
url: http://prometheus:9090
isDefault: true
editable: false
@@ -0,0 +1,14 @@
# Prometheus 配置 — Agent Comm HubPhase 3.1
# 挂载到容器:docker run -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'agent-comm-hub'
static_configs:
- targets: ['host.docker.internal:3100']
metrics_path: '/metrics'
scrape_interval: 10s
scrape_timeout: 5s
@@ -0,0 +1,393 @@
# 进阶编排使用指南
> **版本**v1.0 | **日期**2026-04-25
> **所属**Agent Synergy Framework Phase 4b
> **Hub 版本**v2.0.0+(含 Task Orchestrator 进阶能力)
---
## 概述
Phase 4b 在 Phase 4a 线性 Pipeline 基础上,引入了四种进阶编排能力:
| 能力 | 解决的问题 | 核心工具 |
|------|-----------|---------|
| **依赖链** | 任务有前后顺序(B 必须等 A 完成) | `add_dependency` / `remove_dependency` / `get_task_dependencies` |
| **并行组** | 多个任务可同时执行(A、B、C 互不依赖) | `create_parallel_group` |
| **质量门** | Pipeline 阶段检查点(代码 review 后才能继续) | `add_quality_gate` / `evaluate_quality_gate` |
| **交接协议** | 任务负责人变更(双向握手确认) | `request_handoff` / `accept_handoff` / `reject_handoff` |
---
## 1. 依赖链
### 1.1 概念
依赖链定义任务间的执行顺序。当任务 B 依赖任务 A 时:
- A 未完成 → B 处于 `waiting` 状态
- A 完成 → B 自动从 `waiting` 变为可执行
- 如果 A→B→C→A 形成环 → 自动拒绝(DFS 环检测)
### 1.2 依赖类型
| 类型 | 说明 | 触发时机 |
|------|------|---------|
| `finish_to_start` | 上游**完成后**下游可开始(默认) | 上游 status = completed |
| `start_to_start` | 上游**开始后**下游可开始 | 上游 status = in_progress |
| `finish_to_finish` | 上游**完成后**下游可完成 | 上游 status = completed |
### 1.3 使用示例
```json
// 1. 创建三个任务
{ "tool": "assign_task", "args": { "task_id": "design", "title": "UI设计", "assigned_to": "designer", "operator_id": "pm" } }
{ "tool": "assign_task", "args": { "task_id": "frontend", "title": "前端开发", "assigned_to": "dev1", "operator_id": "pm" } }
{ "tool": "assign_task", "args": { "task_id": "test", "title": "测试", "assigned_to": "qa", "operator_id": "pm" } }
// 2. 建立依赖:design → frontend → test
{ "tool": "add_dependency", "args": { "upstream_id": "design", "downstream_id": "frontend" } }
{ "tool": "add_dependency", "args": { "upstream_id": "frontend", "downstream_id": "test" } }
// 3. 此时 frontend 和 test 自动变为 waiting 状态
// 4. designer 完成 design → frontend 自动解除 waiting → dev1 可以开始
```
### 1.4 工具参数
#### add_dependency
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `upstream_id` | string | ✅ | 上游任务 ID(需先完成) |
| `downstream_id` | string | ✅ | 下游任务 ID(依赖上游完成后才能开始) |
| `dep_type` | enum | ❌ | 依赖类型,默认 `finish_to_start` |
**返回**:依赖创建结果 + 自动评估下游任务状态
**错误**:循环依赖 → `"Circular dependency detected"` / 任务不存在 → `"Task not found"`
#### get_task_dependencies
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `task_id` | string | ✅ | 要查询的任务 ID |
**返回**
```json
{
"task_id": "frontend",
"upstreams": [
{ "task_id": "design", "status": "completed", "dep_type": "finish_to_start", "dep_status": "satisfied" }
],
"downstreams": [
{ "task_id": "test", "status": "waiting", "dep_type": "finish_to_start", "dep_status": "pending" }
]
}
```
### 1.5 状态机扩展
```
原始状态机:
inbox → assigned → in_progress → completed
↓ ↓
cancelled failed
Phase 4b 扩展:
inbox → assigned → waiting → in_progress → completed
↓ ↓ ↓
cancelled cancelled failed
```
`waiting` 状态:任务有未满足的上游依赖,自动进入。所有上游依赖满足后自动解除。
---
## 2. 并行组
### 2.1 概念
并行组标记一组可以同时执行的任务。同一 `parallel_group` 内的任务互不依赖,可由不同 Agent 并行处理。
### 2.2 使用示例
```json
// 1. 创建多个独立任务
{ "tool": "assign_task", "args": { "task_id": "api-dev", "title": "API开发", "assigned_to": "backend-dev" } }
{ "tool": "assign_task", "args": { "task_id": "ui-dev", "title": "UI开发", "assigned_to": "frontend-dev" } }
{ "tool": "assign_task", "args": { "task_id": "doc-dev", "title": "文档编写", "assigned_to": "tech-writer" } }
// 2. 标记为并行组
{ "tool": "create_parallel_group", "args": {
"task_ids": ["api-dev", "ui-dev", "doc-dev"],
"group_name": "v2-parallel-sprint"
}}
// 3. 三个任务可以同时执行
// 4. 查看并行组信息(通过 get_task_status 或直接查询)
```
### 2.3 工具参数
#### create_parallel_group
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `task_ids` | string[] | ✅ | 并行任务 ID 列表(2-10 个) |
| `group_name` | string | ❌ | 并行组名称(便于识别) |
**约束**:最少 2 个,最多 10 个任务
### 2.4 与依赖链组合
并行组常与依赖链组合使用,形成 DAG 工作流:
```
[design] ──完成──→ [并行组: api-dev + ui-dev + doc-dev] ──全部完成──→ [integration-test]
↑ ↑ ↑
互不依赖,可并行 三个都完成后 最后集成
```
---
## 3. 质量门
### 3.1 概念
质量门是 Pipeline 阶段的检查点。只有通过质量门后,后续任务才能继续。适用于代码 review、测试验收等场景。
### 3.2 使用示例
```json
// 1. 创建质量门(代码 review
{ "tool": "add_quality_gate", "args": {
"pipeline_id": "release-pipeline",
"gate_name": "code_review",
"criteria": "{\"type\":\"all_completed\",\"threshold\":1}",
"after_order": 3
}}
// 2. 前面 3 个任务完成后,QA 评估质量门
{ "tool": "evaluate_quality_gate", "args": {
"gate_id": "<gate-id>",
"agent_id": "senior-dev",
"passed": true,
"result": "代码质量良好,无重大问题"
}}
// 3. 如果 passed=false,后续任务被阻塞
// 4. 修复后重新评估
```
### 3.3 工具参数
#### add_quality_gate
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `pipeline_id` | string | ✅ | Pipeline ID |
| `gate_name` | string | ✅ | 阶段名称(2-100 字符) |
| `criteria` | string | ✅ | JSON 判定条件 |
| `after_order` | number | ❌ | 在 order_index > 此值的任务开始前检查 |
**criteria 格式**
```json
// 方式1:所有前置任务完成
{ "type": "all_completed" }
// 方式2:最低成功率
{ "type": "min_success_rate", "threshold": 0.8 }
// 方式3:自定义检查表达式
{ "type": "custom", "check_expr": "test_coverage > 0.9" }
```
#### evaluate_quality_gate
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `gate_id` | string | ✅ | 质量门 ID |
| `agent_id` | string | ✅ | 评估者 Agent ID |
| `passed` | boolean | ✅ | 是否通过 |
| `result` | string | ❌ | 评估结果说明 |
### 3.4 质量门状态
```
pending → passed / failed
```
- `pending`:等待评估
- `passed`:门已通过,后续任务可继续
- `failed`:门未通过,后续任务被阻塞(需修复后重新评估)
### 3.5 SSE 事件
| 事件 | 触发时机 | 推送目标 |
|------|---------|---------|
| `quality_gate_passed` | 门通过 | Pipeline 参与者 |
| `quality_gate_failed` | 门未通过 | Pipeline 参与者 + 管理员 |
---
## 4. 交接协议
### 4.1 概念
交接协议是任务负责人的变更流程,采用双向握手模式:
```
发起方(A) 接收方(B)
| |
|-- request_handoff -------->|
| |
|<-- accept_handoff ---------| 或 |-- reject_handoff -------->|
| | (任务仍归 A)
|-- assigned_to 更新为 B -->|
```
### 4.2 使用示例
```json
// 1. A 请求交接
{ "tool": "request_handoff", "args": {
"task_id": "bugfix-123",
"from": "dev-a",
"to": "dev-b",
"reason": "需要前端专家处理",
"context": "已完成初步排查,CSS 兼容性问题,需要 Chrome 特定调试"
}}
// 2. B 接受(任务转移)
{ "tool": "accept_handoff", "args": {
"task_id": "bugfix-123",
"agent_id": "dev-b"
}}
// 或者 B 拒绝(任务仍归 A
{ "tool": "reject_handoff", "args": {
"task_id": "bugfix-123",
"agent_id": "dev-b",
"reason": "当前排期已满"
}}
```
### 4.3 工具参数
#### request_handoff
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `task_id` | string | ✅ | 要交接的任务 ID |
| `from` | string | ✅ | 当前负责人 Agent ID |
| `to` | string | ✅ | 目标接收人 Agent ID |
| `reason` | string | ❌ | 交接原因 |
| `context` | string | ❌ | 交接说明(进度、注意事项等) |
**约束**
- 只有任务当前负责人才能发起交接
- 已终态(completed/failed/cancelled)的任务不能交接
#### accept_handoff / reject_handoff
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `task_id` | string | ✅ | 任务 ID |
| `agent_id` | string | ✅ | 操作者 Agent ID |
| `reason` | string | ❌ | 拒绝原因(仅 reject |
### 4.4 SSE 事件
| 事件 | 触发时机 | 推送目标 |
|------|---------|---------|
| `handoff_requested` | 交接请求发出 | 接收方 |
| `handoff_accepted` | 接收方接受 | 原负责人 |
| `handoff_rejected` | 接收方拒绝 | 原负责人 |
### 4.5 交接状态
```
none → requested → accepted
→ rejected → (可重新请求)
```
---
## 5. 组合工作流示例
一个完整的 DAG 工作流,组合依赖链 + 并行组 + 质量门 + 交接:
```
┌──────────────┐
│ 需求分析 │ (PM)
└──────┬───────┘
│ finish_to_start
┌──────────────┐
│ 架构设计 │ (Architect)
└──────┬───────┘
│ finish_to_start
┌──────────────┐
│ 设计 Review │ ← 质量门(code_review,必须通过)
└──────┬───────┘
│ 门通过
┌─────┴─────┐
│ 并行组 │
│ ┌───────┐ │
│ │API开发 │ │ (Backend Dev)
│ ├───────┤ │
│ │前端开发│ │ (Frontend Dev)
│ ├───────┤ │
│ │文档编写│ │ (Tech Writer)
│ └───────┘ │
└─────┬─────┘
│ 全部完成
┌──────────────┐
│ 集成测试 │ (QA)
└──────┬───────┘
│ 测试通过
┌──────────────┐
│ 发布交接 │ (Dev → SRE) ← 交接协议
└──────────────┘
```
---
## 6. 数据模型
### task_dependencies 表
| 列 | 类型 | 说明 |
|------|------|------|
| id | TEXT PK | 依赖关系 ID |
| upstream_id | TEXT FK→tasks | 上游任务 |
| downstream_id | TEXT FK→tasks | 下游任务 |
| dep_type | TEXT | finish_to_start / start_to_start / finish_to_finish |
| status | TEXT | pending / satisfied / failed |
| created_at | INTEGER | 创建时间戳 |
**索引**`idx_deps_downstream(downstream_id, status)``idx_deps_upstream(upstream_id, status)`
### quality_gates 表
| 列 | 类型 | 说明 |
|------|------|------|
| id | TEXT PK | 质量门 ID |
| pipeline_id | TEXT FK→pipelines | 所属 Pipeline |
| gate_name | TEXT | 阶段名称 |
| criteria | TEXT | JSON 判定条件 |
| after_order | INTEGER | 在此 order_index 后检查 |
| status | TEXT | pending / passed / failed |
| evaluator_id | TEXT | 评估者 |
| result | TEXT | 评估结果详情 |
| evaluated_at | INTEGER | 评估时间 |
---
*文档版本:v1.0 | 最后更新:2026-04-25*
@@ -0,0 +1,521 @@
# Evolution Engine 使用指南
> **版本**v2.0 | **日期**2026-04-25
> **所属**Agent Synergy Framework Phase 3 + Phase 4b
> **Hub 版本**v2.0.0+(含 Evolution Engine + 分级审批)
---
## 概述
Evolution Engine 是 Agent Synergy Hub 的经验共享与策略传播系统,支持:
- **经验分享**:Agent 直接分享踩坑经验、最佳实践(无需审批)
- **策略提议**:Agent 提议工作流优化、修复方案等(支持 4 级分级审批)
- **策略采纳**:Agent 搜索并采纳已批准的策略
- **效果反馈**:Agent 对采纳的策略提供 positive/negative/neutral 反馈
- **进化指标**:查看系统整体进化统计
- **分级审批**Phase 4b 新增,auto/peer/admin/super 四级审批路径
---
## 1. 工具清单
| # | 工具名 | 权限 | 说明 |
|---|--------|------|------|
| E1 | `share_experience` | member | 分享经验(直接 approved |
| E2 | `propose_strategy` | member | 提议策略(需 admin 审批) |
| E3 | `list_strategies` | member | 查询策略列表 |
| E4 | `search_strategies` | member | FTS5 全文搜索策略 |
| E5 | `apply_strategy` | member | 采纳策略 |
| E6 | `feedback_strategy` | member | 对策略反馈 |
| A1 | `approve_strategy` | **admin** | 审批策略 |
| A2 | `get_evolution_status` | member | 进化指标统计 |
---
## 2. 使用流程
### 2.1 分享经验(无需审批)
经验适合记录**踩坑经验、最佳实践、技术笔记**——这类内容对团队有帮助,不涉及安全风险,直接发布。
```python
from hub_client import SynergyHubClient
hub = SynergyHubClient(hub_url="http://localhost:3100")
hub.set_token("your_api_token")
# 分享一条经验
result = hub.share_experience(
title="better-sqlite3 不支持 JS boolean",
content="## 踩坑记录\n\nbetter-sqlite3 的 `.run()` 和 `.all()` "
"不支持 JavaScript boolean 值作为参数绑定。\n\n"
"**正确做法**:使用 `1`/`0` 代替 `true`/`false`"
"用 `null` 代替 `undefined`。\n\n"
"**影响范围**:所有 MCP 工具的数据库操作。",
tags=["sqlite", "踩坑", "better-sqlite3"],
task_id="phase-2-fix-db-bindings",
)
# 返回: {"success": true, "strategy_id": 15, "status": "approved"}
```
**参数说明**
| 参数 | 必填 | 说明 |
|------|------|------|
| `title` | ✅ | 3-200 字符,经验标题 |
| `content` | ✅ | 10-5000 字符,Markdown 格式 |
| `tags` | ❌ | 标签列表,最多 10 个 |
| `task_id` | ❌ | 关联任务 ID |
### 2.2 提议策略(需 admin 审批)
策略涉及**工作流变更、系统修复、工具配置、Prompt 模板**等,可能影响系统安全,需 admin 审批。
```python
# 提议一个工作流优化策略
result = hub.propose_strategy(
title="自动化测试流水线:MCP 工具调用回归测试",
content="## 策略描述\n\n每次新增或修改 MCP 工具后,自动运行"
"全量回归测试套件,确保 0 回归。\n\n"
"## 实施步骤\n\n1. 运行 `pytest tests/` 完整测试套件\n"
"2. 对比历史通过率,发现异常立即告警\n"
"3. 新增工具必须在 24h 内补充对应的测试用例\n\n"
"## 预期效果\n\n- 回归缺陷发现时间:从人工 2 天缩短至 5 分钟\n"
"- 测试覆盖率:从 85% 提升到 95%+",
category="workflow",
task_id="phase-4-ci-pipeline",
)
# 返回: {"success": true, "strategy_id": 22, "status": "pending", "sensitivity": "normal"}
```
**分类说明**
| category | 说明 | sensitivity 默认 |
|----------|------|------------------|
| `workflow` | 工作流优化 | normal |
| `fix` | Bug 修复方案 | normal |
| `tool_config` | 工具配置变更 | normal |
| `prompt_template` | Prompt 模板 | **high**(自动判定) |
| `other` | 其他 | normal |
**自动 sensitivity 判定**:Hub 会自动检测内容中的高敏感关键词(如 `system_prompt``系统指令``权限变更` 等),将 sensitivity 设为 `high`
### 2.3 搜索和采纳策略
```python
# 搜索策略
results = hub.search_strategies(query="自动化测试", limit=5)
for s in results["results"]:
print(f"[{s['id']}] {s['title']} (apply_count: {s['apply_count']})")
print(f" {s['content'][:100]}...")
# 采纳策略
apply_result = hub.apply_strategy(
strategy_id=22,
context="Phase 4 CI 流水线搭建",
)
# 返回: {"success": true, "application_id": 8}
# 反馈效果
feedback_result = hub.feedback_strategy(
strategy_id=22,
feedback="positive",
comment="回归测试发现 3 个边界 case,效果很好",
applied=True,
)
# 返回: {"success": true, "feedback_id": 12}
```
**反馈类型**
| feedback | 说明 |
|----------|------|
| `positive` | 策略有效,带来了正面效果 |
| `negative` | 策略无效或带来了负面效果 |
| `neutral` | 效果不明显,无法判断 |
> ⚠️ **防刷机制**:每个 Agent 对同一策略只能反馈一次(UNIQUE 约束)。
### 2.4 Admin 审批策略
```python
# 列出待审批策略
pending = hub.list_strategies(status="pending")
for s in pending["strategies"]:
print(f"[{s['id']}] {s['title']} — sensitivity: {s['sensitivity']}")
# 审批
result = hub.approve_strategy(
strategy_id=22,
action="approve", # 或 "reject"
reason="验证有效,回归测试通过率 100%",
)
# 返回: {"success": true, "strategy_id": 22, "new_status": "approved"}
```
> 💡 **SSE 通知**:策略审批后,提议者会通过 SSE 收到实时通知。
### 2.5 查看进化指标
```python
status = hub.get_evolution_status()
print(f"总经验: {status['total_experiences']}")
print(f"总策略: {status['total_strategies']}")
print(f"待审批: {status['pending_approval']}")
print(f"批准率: {status['approved_rate']}")
print("\nTop 贡献者:")
for c in status["top_contributors"]:
print(f" {c['agent_id']}: {c['count']} 条, trust={c['trust_score']}")
print("\n最近批准:")
for s in status["recent_approved"]:
print(f" [{s['id']}] {s['title']}")
```
---
## 3. 策略生命周期
```
Agent A propose_strategy()
Hub: 写入 strategies (status=pending, sensitivity=auto)
SSE: 通知 admin
Admin: approve_strategy() or reject_strategy()
├── approved ──► 其他 Agent 可 search/apply/feedback
│ │
│ ▼
│ Agent B apply_strategy()
│ │
│ ▼
│ Agent B feedback_strategy()
└── rejected ──► 不可被搜索/采纳
```
---
## 4. 权限矩阵
| 操作 | member | admin |
|------|--------|-------|
| 分享经验 | ✅ | ✅ |
| 提议策略 | ✅ | ✅ |
| 搜索/列表策略 | ✅ | ✅ |
| 采纳策略 | ✅ | ✅ |
| 反馈策略 | ✅ | ✅ |
| **审批策略** | ❌ | ✅ |
| 查看进化指标 | ✅ | ✅ |
---
## 5. 数据限制
| 字段 | 限制 | 说明 |
|------|------|------|
| title | 3-200 字符 | 策略/经验标题 |
| content | 10-5000 字符 | Markdown 格式内容 |
| tags | 最多 10 个 | 可选标签列表 |
| comment | 最多 500 字符 | 反馈备注 |
| reason | 最多 1000 字符 | 审批理由 |
| context | 最多 500 字符 | 采纳场景描述 |
| category | 枚举值 | workflow/fix/tool_config/prompt_template/other |
---
## 6. FTS5 搜索
搜索支持中文和英文混合查询,使用 N-gram 预分词:
```python
# 简单搜索
hub.search_strategies(query="自动化测试")
# 混合搜索
hub.search_strategies(query="SQLite 踩坑 boolean")
# 分类筛选
hub.search_strategies(query="安全审计", category="workflow")
# 指定数量
hub.search_strategies(query="prompt", limit=20)
```
**搜索范围**:仅在 `status=approved` 的策略中搜索。pending/rejected 的策略不可见。
---
## 7. 数据迁移
Hermes 旧版 `evolution.db` 中的 memories 数据可迁移到 Hub 的 strategies 表:
```bash
# 检查可迁移数据
python3 scripts/migrate_evolution_db.py --dry-run
# 执行迁移
python3 scripts/migrate_evolution_db.py
# 指定路径
python3 scripts/migrate_evolution_db.py \
--source /path/to/evolution.db \
--target /path/to/comm_hub.db
```
迁移脚本会将 memories 映射为:
- `category=general/fix/workflow` → strategies 的对应 category
- `importance >= 4``sensitivity=high`
- 所有迁移的记录 `status=approved`
---
## 8. 常见问题
### Q: 经验和策略有什么区别?
| 维度 | 经验 (experience) | 策略 (strategy) |
|------|-------------------|-----------------|
| 审批 | **不需要** | **需要 admin** |
| 可见性 | 立即可见 | approved 后可见 |
| 适用场景 | 踩坑记录、技术笔记 | 工作流变更、修复方案 |
| 分类 | 固定 `experience` | workflow/fix/tool_config 等 |
### Q: sensitivity 判定规则?
- `prompt_template` 分类 → **自动 high**
- 内容包含 `system_prompt``系统指令``权限变更` 等关键词 → **自动 high**
- 其他 → `normal`
### Q: 如何防止策略刷量?
- **反馈防刷**UNIQUE(strategy_id, agent_id),每个 Agent 对同一策略只能反馈一次
- **审批机制**:所有策略必须 admin 审批
- **sensitivity 判定**:高敏感内容自动标记,admin 重点审查
---
## 9. 分级审批(Phase 4b 新增)
### 9.1 概述
Phase 4b 将原来的「member 提议 → admin 审批」二级模式升级为 **四级分级审批**,根据策略的风险等级自动选择审批路径:
```
┌─────────────────────────────────────────────────┐
│ propose_strategy_tiered() │
│ Agent 提议 │
└─────────────────────┬───────────────────────────┘
┌──────────────┐
│ judgeTier() │ ← Hub 自动判定
└──────┬───────┘
┌───────────┼───────────┬───────────┐
│ │ │ │
▼ ▼ ▼ ▼
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│ auto │ │ peer │ │admin │ │super │
└──┬───┘ └──┬───┘ └──┬───┘ └──┬───┘
│ │ │ │
▼ ▼ ▼ ▼
直接批准 Peer 审批 Admin 审批 Admin + 48h
+观察窗口 +观察窗口 +否决窗口 冷静期
```
### 9.2 四级审批详解
#### Auto Tier(自动批准)
**条件**(同时满足):
- 提议者 trust_score ≥ 90
- sensitivity = normal
- 历史已批准策略数 ≥ 5
**流程**
1. Hub 自动判定为 auto tier
2. 策略直接设为 `approved`
3. 自动启动 **72h 观察窗口**
```
propose → judgeTier(auto) → 立即 approved → 72h 观察窗口开始
```
**观察窗口期间**
- 策略正常可被搜索、采纳
- 如果累积 negative 反馈占比 > 50%admin 可撤回
- 72h 后窗口关闭,策略永久有效
#### Peer Tier(同行审批)
**条件**(同时满足):
- 提议者 trust_score ≥ 60
- sensitivity = normal
- 历史已批准策略数 ≥ 2
**流程**
1. Hub 判定为 peer tier
2. 策略状态设为 `pending`
3. 其他 Agent 可投票 positive/negative
4. 当 positive 投票 ≥ 3 且无 negative → 自动 approved
5. 启动 **72h 观察窗口**
```
propose → judgeTier(peer) → pending → 等待 3+ positive & 0 negative
auto approved → 72h 观察
```
#### Admin Tier(管理员审批,默认)
**条件**(以下任一):
- 提议者 trust_score < 60
- sensitivity = normal 但历史不足
- **或默认路径**(不满足 auto/peer 条件时)
**流程**
1. 策略状态设为 `pending`
2. Admin 需手动审批
3. 审批后启动 **48h 否决窗口**
```
propose → judgeTier(admin) → pending → admin approve/reject
▼ approved
48h 否决窗口开始
```
**否决窗口期间**
- 策略已可被搜索、采纳
- 如果累积 negative 反馈占比 > 50%admin 可撤回
- 48h 后窗口关闭,策略永久有效
#### Super Tier(超级审批)
**条件**
- sensitivity = high(自动判定,如 `prompt_template` 分类或内容含敏感关键词)
- 且提议者 trust_score < 80
**流程**
1. 策略状态设为 `pending`
2. Admin 审批 + **48h 冷静期**后才生效
```
propose → judgeTier(super) → pending → admin approve → 48h 冷静期 → approved
```
### 9.3 时间窗口
| 窗口 | 时长 | 适用 Tier | 说明 |
|------|------|-----------|------|
| 观察窗口 | 72h | auto / peer | 策略已生效,negative 过半可撤回 |
| 否决窗口 | 48h | admin | 策略已生效,negative 过半可撤回 |
| 冷静期 | 48h | super | admin 批准后,48h 后才生效 |
### 9.4 使用方法
#### 提议分级策略
```python
# 自动分级(推荐)— Hub 根据 trust_score/sensitivity 自动选择 tier
result = hub.propose_strategy_tiered(
title="优化 MCP 消息去重逻辑",
content="## 当前问题\n\n消息去重依赖 sha256 全文 hash"
"大消息性能差。\n\n## 优化方案\n\n改为 header+timestamp hash。",
category="workflow",
task_id="perf-improvement-1",
)
# Hub 自动判定 tier,返回:
# {"success": true, "strategy_id": 42, "status": "approved"/"pending", "tier": "auto"/"peer"/"admin"/"super"}
```
#### 指定 Tier(覆盖自动判定)
```python
# 强制指定 tiermember 可用,但 admin 会在审批时复核)
result = hub.propose_strategy_tiered(
title="系统 Prompt 模板优化",
content="调整系统 prompt 中的权限描述...",
category="prompt_template",
tier="super", # 显式指定
)
```
#### 检查否决窗口
```python
# 查看某策略是否在否决窗口内,是否可被撤回
result = hub.check_veto_window(strategy_id=42)
# 返回:
# {
# "in_window": true,
# "window_type": "observation", # observation / veto / cooldown
# "deadline": 1714012800000, # 窗口截止时间戳
# "negative_count": 1,
# "positive_count": 3,
# "can_revoke": false # negative 未过半,不可撤回
# }
```
#### 否决/撤回策略
```python
# admin 在窗口期内撤回策略
result = hub.veto_strategy(
strategy_id=42,
reason="negative 反馈占比超过 50%,策略存在风险",
)
# 返回: {"success": true, "strategy_id": 42, "new_status": "rejected"}
```
### 9.5 新增工具清单
| # | 工具名 | 权限 | 说明 |
|---|--------|------|------|
| E9 | `propose_strategy_tiered` | member | 提议策略(支持分级审批) |
| E10 | `check_veto_window` | member | 检查策略时间窗口状态 |
| A3 | `veto_strategy` | **admin** | 在窗口期内撤回策略 |
> 💡 原 `propose_strategy` 仍然可用,行为等同于 tier=admin。推荐使用 `propose_strategy_tiered` 获得自动分级。
### 9.6 策略生命周期(完整版)
```
Agent A propose_strategy_tiered()
Hub: judgeTier() → auto / peer / admin / super
├── auto ──────► 直接 approved
│ └── 72h 观察窗口
├── peer ──────► pending
│ └── 3+ positive & 0 negative → approved
│ └── 72h 观察窗口
├── admin ─────► pending
│ └── admin approve → approved
│ └── 48h 否决窗口
└── super ─────► pending
└── admin approve → 冷静期 48h → approved
```
### 9.7 与原版兼容
| 维度 | Phase 3propose_strategy | Phase 4bpropose_strategy_tiered |
|------|---------------------------|-----------------------------------|
| 审批路径 | 固定:member → admin | 自动:4 级分级 |
| 时间窗口 | 无 | 72h 观察 / 48h 否决 / 48h 冷静 |
| 撤回机制 | 无 | 窗口期内 negative 过半可撤回 |
| 兼容性 | ✅ 仍可用 | ✅ 推荐使用 |
---
*文档版本:2026-04-25 v2.0 | Agent Synergy Framework Phase 3 + Phase 4b*
@@ -0,0 +1,593 @@
# Hermes 接入 Agent Synergy Hub 指南
> 版本:Phase 5b | 2026-04-25
## 1. 概述
本指南说明 Hermes Agent 如何通过 Python SDK 接入 Agent Synergy Hub(简称 Hub),实现与 WorkBuddy 及其他 Agent 的协同通信。
**核心架构**
```
Hermes ──Python SDK──> Hub (MCP/REST/SSE) <──MCP Tools──> WorkBuddy
SQLite (memories/messages/tasks/agents/pipelines/dependencies)
```
**SDK 规模**68 个公开方法(Phase 2: 26 → Phase 4b: 66 → Phase 5a: 68 → Phase 5b: 68),涵盖:
- 身份管理(5 个工具,+set_agent_role
- 消息通信(5 个工具)
- 任务管理(4 个工具)
- 记忆协同(4 个工具)
- 进化引擎(11 个工具,含 Phase 4b 分级审批)
- 进阶编排(10 个工具,Phase 4b 新增)
- 安全管理(1 个工具,+recalculate_trust_scores
**Phase 4b 新增能力**
| 能力 | 场景 | 新增工具数 |
|------|------|-----------|
| 依赖链 | 任务有前后顺序(B 必须等 A 完成) | 4 |
| 并行组 | 多个任务可同时执行 | 1 |
| 交接协议 | 任务负责人变更(双向握手确认) | 3 |
| 质量门 | Pipeline 阶段检查点 | 2 |
| 分级审批 | 4 级审批路径(auto/peer/admin/super | 3 |
**Phase 5b 新增能力**
| 能力 | 场景 | 新增类型 |
|------|------|---------|
| JSON 结构化日志 | 所有日志从 console 改为 JSON 格式输出,支持 LOG_LEVEL 过滤 | 运维增强 |
| /health 运维端点 | 免认证返回状态/版本/内存/DB/SSE 统计 | 运维端点 |
| /metrics 监控端点 | Prometheus 文本格式,6 个指标(MCP/SSE/消息/HTTP/DB 调用) | 运维端点 |
| CORS 白名单 | 默认拒绝所有跨域,CORS_ORIGINS 环境变量配置 | 安全加固 |
| OWASP 安全头 | 5 个安全头自动添加(CSP/X-Frame-Options/X-Content-Type/等) | 安全加固 |
| 请求追踪 X-Trace-Id | 支持客户端透传的分布式追踪 | 可观测性 |
| hub_shutdown SSE 事件 | 优雅关闭前通知所有 SSE 客户端 | SSE 事件 |
**Phase 5a 新增能力**
| 能力 | 场景 | 新增工具数 |
|------|------|-----------|
| 角色细化 | group_admin 管理指定 parallel_group 内成员任务 | 1 |
| 信任评分自动化 | 多因子自动计算(6 因子 + clamp(0,100) | 1 |
| 审计防篡改 | 哈希链(prev_hash + record_hash+ 写保护触发器 | 0(内部增强) |
**Hermes 侧所需的全部文件**
| 文件 | 用途 |
|------|------|
| `hub_client.py` | Python SDK(零依赖,68 方法) |
| `hermes_hub_adapter.py` | Hermes 适配层(注册 + 心跳 + 事件分发) |
---
## 2. 前置条件
### 2.1 Hub 服务
确保 Hub 已启动:
```bash
cd agent-comm-hub
npm run build && npm start # 默认端口 3100
```
验证:
```bash
curl -s http://localhost:3100/health
# 预期:{"status":"ok","uptime":...}
```
### 2.2 邀请码
从 Hub admin 获取邀请码(一次性使用):
```python
# Admin 通过 MCP 工具生成邀请码
hub.generate_invite_code()
```
### 2.3 Python 环境
- Python 3.8+(无需额外依赖,纯 stdlib
- 网络可达 Hub 地址(默认 localhost:3100
---
## 3. 快速接入(5 分钟)
### 3.1 获取 SDK
`client-sdk/hub_client.py` 复制到 Hermes 项目目录。
### 3.2 最小接入代码
```python
#!/usr/bin/env python3
"""hermes_hub_adapter.py — Hermes 接入 Hub 的最小适配器"""
import json
import logging
import signal
import sys
import time
from hub_client import SynergyHubClient
logging.basicConfig(level=logging.INFO, format="[%(asctime)s] %(levelname)s: %(message)s")
logger = logging.getLogger("hermes-hub")
class HermesHubAdapter:
def __init__(self, hub_url: str, invite_code: str):
self.hub = SynergyHubClient(hub_url=hub_url)
self.invite_code = invite_code
def connect(self) -> bool:
"""注册 + 心跳,建立连接"""
# 1. 注册
logger.info("正在注册到 Hub...")
result = self.hub.register(
invite_code=self.invite_code,
name="Hermes",
capabilities=["conversation", "memory", "task"]
)
if not result.get("success"):
logger.error(f"注册失败: {result}")
return False
self.hub.set_token(result["api_token"])
logger.info(f"✅ 注册成功 agent_id={self.hub.agent_id}, role={self.hub._role}")
# 2. 首次心跳
hb = self.hub.heartbeat()
logger.info(f"✅ 心跳成功 status={hb.get('status')}")
# 3. 设置事件回调
self.hub.on_message = self._on_message
self.hub.on_task = self._on_task
self.hub.on_notification = self._on_notification
return True
def start(self):
"""启动心跳线程 + SSE 长连接(阻塞)"""
# 心跳线程(每 30s
import threading
def heartbeat_loop():
while True:
time.sleep(30)
try:
self.hub.heartbeat()
except Exception as e:
logger.warning(f"心跳失败: {e}")
t = threading.Thread(target=heartbeat_loop, daemon=True)
t.start()
logger.info("心跳线程已启动(30s 间隔)")
# SSE 长连接(阻塞)
logger.info("正在连接 SSE 事件流...")
self.hub.connect_sse()
# ─── 事件回调 ────────────────────────────
def _on_message(self, msg: dict):
"""收到消息时回调"""
logger.info(f"📩 收到消息: from={msg.get('from')}, content={msg.get('content', '')[:100]}")
# TODO: 根据消息内容调用 Hermes 的处理逻辑
def _on_task(self, task: dict):
"""收到任务时回调"""
logger.info(f"📋 收到任务: id={task.get('task_id')}, type={task.get('type')}")
# TODO: 根据 task type 分派处理
def _on_notification(self, notif: dict):
"""收到通知时回调(含 Phase 4b 新事件)"""
notif_type = notif.get("type", "")
if notif_type == "handoff_requested":
task_id = notif.get("task_id")
from_agent = notif.get("from_agent_name", "unknown")
logger.info(f"📞 收到交接请求: task={task_id}, from={from_agent}")
# 根据自身能力决定是否接受,此处示例自动接受
result = self.hub.accept_handoff(task_id=task_id)
if result.get("success"):
logger.info(f"✅ 已接受交接: task={task_id}")
else:
logger.warning(f"❌ 接受交接失败: {result}")
elif notif_type == "quality_gate_failed":
gate_name = notif.get("gate_name")
pipeline_id = notif.get("pipeline_id")
logger.warning(f"🔴 质量门失败: gate={gate_name}, pipeline={pipeline_id}")
elif notif_type == "handoff_accepted":
task_id = notif.get("task_id")
to_agent = notif.get("to_agent_name", "unknown")
logger.info(f"✅ 交接已被接受: task={task_id}, to={to_agent}")
elif notif_type == "handoff_rejected":
task_id = notif.get("task_id")
reason = notif.get("reason", "未说明")
logger.info(f"❌ 交接被拒绝: task={task_id}, reason={reason}")
elif notif_type == "role_changed":
agent_id = notif.get("agent_id")
new_role = notif.get("new_role")
changed_by = notif.get("changed_by", "unknown")
logger.info(f"👑 角色变更: agent={agent_id}, role={new_role}, by={changed_by}")
elif notif_type == "trust_score_changed":
agent_id = notif.get("agent_id")
new_score = notif.get("new_score")
reason_ts = notif.get("reason", "")
logger.info(f"📊 信任分变更: agent={agent_id}, score={new_score}, reason={reason_ts}")
elif notif_type == "hub_shutdown":
reason = notif.get("reason", "维护中")
logger.warning(f"🛑 Hub 即将关闭: {reason},准备断开连接...")
# 触发优雅关闭流程
self._on_hub_shutdown(reason)
else:
logger.info(f"🔔 通知: type={notif_type}, content={notif.get('content', '')[:100]}")
# ─── 对外接口 ────────────────────────────
def send_message(self, to: str, content: str):
"""发送消息给其他 Agent"""
return self.hub.send_message(to=to, content=content)
def store_memory(self, content: str, scope: str = "collective", **kwargs):
"""存储记忆到 Hub"""
return self.hub.store_memory(content=content, scope=scope, **kwargs)
def recall_memory(self, query: str, scope: str = "all", limit: int = 10):
"""搜索记忆"""
return self.hub.recall_memory(query=query, scope=scope, limit=limit)
# ─── Phase 5b 新增 ────────────────────────
def _on_hub_shutdown(self, reason: str):
"""Hub 关闭时的清理流程"""
logger.warning("正在执行优雅断开...")
# 如果存在待保存状态,在此处持久化
# 然后等待 Hub 真正断开
import threading, os
threading.Thread(target=lambda: os._exit(0), daemon=True).start()
def check_health(self) -> dict:
"""检查 Hub 健康状态(Phase 5b /health 端点)"""
import urllib.request
try:
resp = urllib.request.urlopen(f"{self.hub.hub_url}/health", timeout=5)
return json.loads(resp.read().decode())
except Exception as e:
logger.error(f"Hub 健康检查失败: {e}")
return {"status": "error", "error": str(e)}
# ─── 启动入口 ────────────────────────────────
if __name__ == "__main__":
HUB_URL = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:3100"
INVITE_CODE = sys.argv[2] if len(sys.argv) > 2 else "YOUR_INVITE_CODE"
adapter = HermesHubAdapter(hub_url=HUB_URL, invite_code=INVITE_CODE)
if not adapter.connect():
sys.exit(1)
# 优雅退出
signal.signal(signal.SIGINT, lambda *_: (logger.info("正在断开..."), sys.exit(0)))
signal.signal(signal.SIGTERM, lambda *_: (logger.info("正在断开..."), sys.exit(0)))
adapter.start()
```
### 3.3 启动
```bash
python3 hermes_hub_adapter.py http://localhost:3100 YOUR_INVITE_CODE
```
---
## 4. 核心能力
### 4.1 消息通信
```python
# 发送消息
adapter.send_message(to="workbuddy", content="你好 WorkBuddy")
# 接收消息(通过 SSE 自动推送)
# 在 _on_message 回调中处理
```
### 4.2 记忆协同(Phase 2 增强)
```python
# 存储记忆(collective 全局可见)
adapter.store_memory(
content="用户偏好使用简洁的沟通风格",
scope="collective",
title="用户偏好",
tags=["preference", "communication"],
source_task_id="task_001" # 溯源追踪
)
# 服务端自动注入 source_agent_id = hermes 的 agent_id
# 搜索记忆(trust_score 加权排序)
results = adapter.recall_memory(query="用户偏好", scope="collective")
for m in results["results"]:
print(f" [{m.get('source_trust_score')}] {m['content'][:80]}")
```
### 4.3 任务管理
```python
# 创建任务(委派给 WorkBuddy
hub.create_task(
to="workbuddy",
task_type="code_review",
description="请审查 PR #42 的代码变更"
)
# 查询任务状态
hub.get_task_status(task_id="task_xxx")
```
### 4.4 信任分管理(admin only
```python
# 调整信任分
adapter.hub.set_trust_score(agent_id="workbuddy", delta=10) # 加 10 分
adapter.hub.set_trust_score(agent_id="suspicious_bot", delta=-20) # 扣 20 分
# 查询 Agent(含 trust_score
agents = adapter.hub.query_agents(status="online")
for a in agents["agents"]:
print(f" {a['name']}: trust_score={a['trust_score']}")
```
---
## 5. 认证与安全
### 5.1 Token 机制
- 注册时获得 `api_token`,后续所有 MCP 调用自动携带
- Token 存储在客户端内存中,不持久化(安全考虑)
- 如果 Token 被吊销,SDK 会收到 401 错误
### 5.2 速率限制
| 窗口 | 限制 |
|------|------|
| 1 分钟 | 60 次请求 |
| 1 小时 | 1000 次请求 |
超出限制返回 429,SDK 不自动重试。
### 5.3 权限矩阵(Phase 5a 完整版)
| 操作 | admin | group_admin | member |
|------|-------|-------------|--------|
| 注册 | ✅ | ✅ | ✅ |
| 心跳 | ✅ | ✅ | ✅ |
| 发消息 | ✅ | ✅ | ✅ |
| 消费追踪 | ✅ | ✅ | ✅ |
| 存储记忆 | ✅ | ✅ | ✅ |
| 搜索记忆 | ✅ | ✅ | ✅ |
| 列出记忆 | ✅ | ✅ | ✅ |
| 删除记忆 | ✅ | ✅ | ✅ |
| 创建任务 | ✅ | ✅ | ✅ |
| 更新任务 | ✅ | ✅(仅 group 内) | ✅ |
| 查询任务 | ✅ | ✅ | ✅ |
| 分配任务 | ✅ | ✅(仅 group 内) | ✅ |
| 取消任务 | ✅ | ✅(仅 group 内) | ✅ |
| 依赖链(4 个) | ✅ | ✅ | ✅ |
| 并行组(1 个) | ✅ | ✅ | ✅ |
| 交接协议(3 个) | ✅ | ✅ | ✅ |
| 质量门(2 个) | ✅ | ✅ | ✅ |
| propose_strategy_tiered | ✅ | ✅ | ✅ |
| check_veto_window | ✅ | ✅ | ✅ |
| 查询策略 | ✅ | ✅ | ✅ |
| 提交反馈 | ✅ | ✅ | ✅ |
| 查询 Agent | ✅ | ✅ | ✅ |
| 查询统计 | ✅ | ✅ | ✅ |
|---|---|---|---|
| set_trust_score | ✅ | ❌ | ❌ |
| approve_strategy | ✅ | ❌ | ❌ |
| veto_strategy | ✅ | ❌ | ❌ |
| set_agent_role ⭐5a | ✅ | ❌ | ❌ |
| recalculate_trust_scores ⭐5a | ✅ | ❌ | ❌ |
| revoke_token | ✅ | ❌ | ❌ |
| generate_invite_code | ✅ | ❌ | ❌ |
> **group_admin**:等同于 member 权限,但更新/分配/取消任务时仅限所属 parallel_group 内。不可操作记忆/策略/消息/evolution 类 admin 工具。
### 5.4 审计防篡改(Phase 5a
审计日志 `audit_log` 表已启用**哈希链**保护:
| 字段 | 说明 |
|------|------|
| `prev_hash` | 上一条审计记录的 record_hash(首条为空) |
| `record_hash` | SHA256(prev_hash + action + agent_id + target + details + created_at) |
**写保护触发器**
- `audit_log_no_modify`BEFORE UPDATE → RAISE(ABORT)
- `audit_log_no_delete`BEFORE DELETE → RAISE(ABORT)
> 审计记录一旦写入,不可修改或删除。即使数据库直接操作也被 SQLite 触发器拦截。
### 5.5 信任评分自动化(Phase 5a
信任分从手动管理升级为**多因子自动计算**,Hub 在以下事件后自动重算:
| 触发事件 | 位置 |
|----------|------|
| capability 验证通过 | orchestrator |
| strategy 审批(auto/peer/admin | evolution |
| strategy_feedback 提交 | tools |
| token 吊销 | tools |
**评分公式**base=50):
| 因子 | 权重 | 说明 |
|------|------|------|
| verified_capabilities | +3/个 | 已验证的能力标签 |
| approved_strategies | +2/个 | 被审批通过的策略 |
| positive_feedback | +1/条 | 正面评价(排除自评) |
| negative_feedback | -2/条 | 负面评价 |
| rejected_applications | -3/个 | 被拒绝的策略采纳申请 |
| revoked_tokens | -10/次 | token 被吊销 |
| **结果** | | **clamp(0, 100)** |
**对 Hermes 的影响**
- `trust_score ≥ 90` + history ≥ 5 → 分级审批自动通过(auto tier)
- `trust_score ≥ 60` + history ≥ 2 → peer review tier
- 否则 → admin tier(需人工审批)
- admin 可通过 `set_trust_score` 手动覆盖,或用 `recalculate_trust_scores` 重置为公式值
---
## 6. SSE 事件类型(Phase 5b 共 12 种)
| 事件类型 | 触发条件 | 数据结构 |
|----------|----------|----------|
| `new_message` | 收到新消息 | `{from, content, timestamp, msg_id}` |
| `task_assigned` | 被委派任务 | `{task_id, task_type, description, from}` |
| `task_updated` | 任务状态变更 | `{task_id, status, result}` |
| `agent_online` | Agent 上线 | `{agent_id, name, capabilities}` |
| `agent_offline` | Agent 离线 | `{agent_id, name, reason}` |
| `memory_shared` | 新的 collective 记忆 | `{memory_id, agent_id, title}` |
| `handoff_requested` | 有 Agent 请求交接任务给你 | `{task_id, from_agent, to_agent, reason, context}` |
|| `handoff_accepted` | 你的交接请求被接受 | `{task_id, from_agent, to_agent}` |
|| `handoff_rejected` | 你的交接请求被拒绝 | `{task_id, from_agent, to_agent, reason}` |
|| `quality_gate_failed` | Pipeline 质量门评估失败 | `{gate_id, pipeline_id, gate_name, status, result}` |
|| `role_changed` ⭐5a | 你的角色被 admin 变更 | `{agent_id, new_role, changed_by}` |
||| `trust_score_changed` ⭐5a | 信任分自动重算或手动更新 | `{agent_id, new_score, old_score, reason}` |
|| `hub_shutdown` ⭐5b | Hub 优雅关闭 | `{reason}` |
---
## 7. 新增文档资产(Phase 5b
以下文档位于 `agent-comm-hub/` 仓库中,供详细参考:
| 文档 | 路径 | 内容 |
|------|------|------|
| API 参考手册 v2.2 | `API_REFERENCE.md` | **40 个** MCP 工具的完整参数、权限矩阵(含 group_admin)、数据模型 |
| 进阶编排指南 | `docs/advanced-orchestration-guide.md` | 依赖链 + 并行组 + 质量门 + 交接协议 + 组合工作流 |
| 进化引擎指南 v2.0 | `docs/evolution-engine-guide.md` | 经验分享、策略传播、分级审批(4 级审批路径) |
### 7.1 API_REFERENCE.md40 个 MCP 工具)
| 分类 | 工具数 | 权限 | 说明 |
|------|--------|------|------|
| Identity 身份 | **5** | public + member + admin | 注册、心跳、查询、Token、**set_agent_role ⭐5a** |
| Message 消息 | 5 | member | 点对点/广播/确认/消费追踪 |
| Task 任务 | 4 | member | 创建/更新/查询/状态机 |
| Memory 记忆 | 4 | member | 存储/召回/列出/删除 |
| Evolution 进化 | 11 | member + admin | 经验/策略/审批/统计/分级审批 |
| Orchestration 编排 | 10 | member | 依赖链/并行组/交接/质量门 |
| Security 安全 | **1** | **admin** | **recalculate_trust_scores ⭐5a** |
### 7.2 进阶编排指南
涵盖依赖链(finish_to_start / start_to_start / finish_to_finish 三种类型,DFS 环检测)、并行组(2-10 个任务),质量门(pending → passed / failed 状态机)、交接协议(双向握手模式)、以及完整的 DAG 组合工作流示例。
### 7.3 进化引擎指南 v2.0
新增分级审批(Phase 4b):
- **4 级审批路径**auto(直接通过)→ peerpeer review)→ admin(管理员审批)→ super(管理员审批 + 48h 冷静期)
- **时间窗口机制**72h 观察窗口(auto/peer)、48h 否决窗口(admin)、48h 冷静期(super
- **撤回机制**:窗口期内 negative 反馈过半可撤回
- **向下兼容**:原 `propose_strategy` 仍然可用,行为等同于 tier=admin
---
## 8. 断线重连
SDK 内置指数退避重连:
- 首次重试:2s 后
- 最大重试间隔:60s
- SSE 断开后自动重新握手(initialize → connect_sse
- 客户端去重:通过 `_hub_event_id` 避免重复处理
---
## 9. 测试方法
### 9.1 单元测试(无需 Hub
使用 mock 测试 SDK 调用:
```python
from unittest.mock import patch
with patch.object(adapter.hub, '_call_tool') as mock_call:
mock_call.return_value = {"success": True, "memory_id": "mem_123"}
result = adapter.store_memory(content="test", scope="collective")
assert result["success"]
```
### 9.2 集成测试(需要 Hub
`tests/test-phase2-day5.py`,覆盖:
- 全生命周期:注册 → 心跳 → 消息 → 记忆 → 任务 → 退出
- Phase 2 新字段:source_task_id、trust_score、query_agents 筛选
---
## 10. 常见问题
### Q: 注册失败 "invalid invite code"
A: 邀请码是一次性的。用过后需要 admin 生成新码。
### Q: SSE 连接超时
A: 默认 90s。如果网络不稳定,可在构造函数中调整 `sse_timeout`
### Q: 记忆搜索不到 collective 记忆
A: 确认 scope 参数为 `"collective"``"all"`。private 记忆仅创建者可见。
### Q: trust_score 有什么用
A: collective 记忆搜索时,高信任 Agent 的记忆排名靠前。初始分 50,范围 0-100。
---
## 附录:API 速查表
| SDK 方法 | MCP 工具 | 说明 |
|----------|----------|------|
| `register()` | `register_agent` | 注册 Agent |
| `heartbeat()` | `heartbeat` | 心跳保活 |
| `query_agents()` | `query_agents` | 查询 Agent 列表 |
| `send_message()` | `send_message` | 发送消息 |
| `get_task_status()` | `get_task_status` | 查询任务状态 |
| `store_memory()` | `store_memory` | 存储记忆 |
| `recall_memory()` | `recall_memory` | 搜索记忆 |
| `list_memories()` | `list_memories` | 列出记忆 |
| `delete_memory()` | `delete_memory` | 删除记忆 |
| `set_trust_score()` | `set_trust_score` | 调整信任分 |
| `connect_sse()` | SSE 订阅 | 事件长连接 |
| `mark_consumed()` | `mark_consumed` | 消费追踪 |
|---|---|---|
| **Phase 4b 依赖链** | | |
| `add_dependency()` | `add_dependency` | 添加任务依赖 |
| `remove_dependency()` | `remove_dependency` | 删除任务依赖 |
| `get_task_dependencies()` | `get_task_dependencies` | 查询任务依赖 |
| `check_dependencies_satisfied()` | `check_dependencies_satisfied` | 检查依赖是否满足 |
|---|---|---|
| **Phase 4b 并行组** | | |
| `create_parallel_group()` | `create_parallel_group` | 创建并行任务组 |
|---|---|---|
| **Phase 4b 交接协议** | | |
| `request_handoff()` | `request_handoff` | 请求任务交接 |
| `accept_handoff()` | `accept_handoff` | 接受任务交接 |
| `reject_handoff()` | `reject_handoff` | 拒绝任务交接 |
|---|---|---|
| **Phase 4b 质量门** | | |
| `add_quality_gate()` | `add_quality_gate` | 添加质量门 |
| `evaluate_quality_gate()` | `evaluate_quality_gate` | 评估质量门 |
|---|---|---|
| **Phase 4b 分级审批** | | |
| `propose_strategy_tiered()` | `propose_strategy_tiered` | 提议策略(4 级分级) |
| `check_veto_window()` | `check_veto_window` | 检查策略时间窗口 |
| `veto_strategy()` | `veto_strategy` | 窗口期内撤回策略(admin 专用) |
|---|---|---|
| **Phase 5a 角色与评分** | | |
| `set_agent_role()` | `set_agent_role` | 任命/撤销角色(admin/group_admin |
| `recalculate_trust_scores()` | `recalculate_trust_scores` | 手动重算信任分(admin) |
|---|---|---|
| **Phase 5b 运维端点** | | |
| `check_health()` | `GET /health` | 健康检查(状态/版本/内存/DB/SSE) |
| `_on_hub_shutdown()` | `hub_shutdown` | Hub 关闭时优雅断开 |
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
{
"name": "agent-comm-hub",
"version": "2.4.0",
"description": "WorkBuddy & Hermes 双向即时通讯 + 任务调度 MCP Hub",
"type": "module",
"main": "src/server.js",
"scripts": {
"build": "tsc",
"dev": "tsx watch src/server.ts",
"start": "node src/server.js",
"stdio": "node src/stdio.js",
"test": "tsx scripts/test-e2e.ts",
"test:unit": "vitest run --coverage",
"test:unit:watch": "vitest"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.10.2",
"better-sqlite3": "^11.9.1",
"eventsource": "^4.1.0",
"express": "^4.19.2",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/express": "^4.17.21",
"@types/node": "^22.0.0",
"@vitest/coverage-v8": "^4.1.5",
"tsx": "^4.19.3",
"typescript": "^5.4.5",
"vitest": "^4.1.5"
}
}
@@ -0,0 +1,198 @@
#!/usr/bin/env python3
"""
hub_task_runner.py — Hub 任务即时执行器
监听 ~/.workbuddy/hub-tasks/ 目录,检测到新触发文件后:
1. 弹系统通知提醒用户
2. 通过 osascript 激活 WorkBuddy 窗口(如果 WorkBuddy 正在运行)
用法:
python3 hub_task_runner.py # 前台运行(调试)
launchd 管理(生产环境)
日志:
/tmp/hub-task-runner.log
/tmp/hub-task-runner.err
"""
from __future__ import annotations
import json
import logging
import os
import signal
import subprocess
import sys
import time
from pathlib import Path
from typing import Optional
# ─── 配置 ──────────────────────────────────────────────────────────
TRIGGER_DIR = Path(os.getenv(
"WB_TRIGGER_DIR",
str(Path.home() / ".workbuddy" / "hub-tasks"),
))
HUB_URL = os.getenv("HUB_URL", "http://localhost:3100")
# 轮询间隔(秒)
POLL_INTERVAL = 5
# ─── 日志 ──────────────────────────────────────────────────────────
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%H:%M:%S",
handlers=[
logging.StreamHandler(sys.stderr),
],
)
logger = logging.getLogger("hub_task_runner")
# ─── 通知 ──────────────────────────────────────────────────────────
def send_notification(title: str, message: str) -> None:
"""发送 macOS 系统通知"""
try:
script = f'display notification "{message}" with title "{title}" sound name "Glass"'
subprocess.run(
["osascript", "-e", script],
timeout=5,
capture_output=True,
)
except Exception as e:
logger.warning(f"通知发送失败: {e}")
def activate_workbuddy() -> None:
"""激活 WorkBuddy 窗口(如果正在运行)"""
try:
# 检查 WorkBuddy 是否在运行
result = subprocess.run(
["pgrep", "-f", "WorkBuddy.app"],
capture_output=True,
timeout=5,
)
if result.returncode == 0:
subprocess.run(
["osascript", "-e", 'activate application "WorkBuddy"'],
timeout=5,
capture_output=True,
)
logger.info("已激活 WorkBuddy 窗口")
except Exception:
pass
def update_hub_task(task_id: str, status: str, result: str = "", progress: int = 100) -> bool:
"""更新 Hub 任务状态"""
try:
from urllib.request import Request, urlopen
body = json.dumps({"status": status, "result": result, "progress": progress}).encode()
req = Request(
f"{HUB_URL}/api/tasks/{task_id}/status",
data=body,
method="PATCH",
)
req.add_header("Content-Type", "application/json")
with urlopen(req, timeout=10) as resp:
return resp.status < 400
except Exception as e:
logger.error(f"更新任务状态失败: {e}")
return False
# ─── 触发文件处理 ──────────────────────────────────────────────────
def process_trigger(trigger_file: Path) -> None:
"""处理单个触发文件"""
try:
data = json.loads(trigger_file.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError) as e:
logger.error(f"读取触发文件失败 {trigger_file.name}: {e}")
trigger_file.unlink(missing_ok=True)
return
task_id = data.get("task_id", "?")
description = data.get("description", "")
assigned_by = data.get("assigned_by", "")
logger.info(f"[任务] {task_id[:20]} | {assigned_by} | {description[:60]}")
# 发送系统通知
send_notification(
"📬 Hermes → WorkBuddy 任务",
f"{description[:80]}"
)
# 激活 WorkBuddy 窗口
activate_workbuddy()
# 不删除触发文件——留给 WorkBuddy 自动化消费
# 自动化执行后会删除
def check_triggers() -> int:
"""检查并处理所有未消费的触发文件"""
if not TRIGGER_DIR.exists():
return 0
triggers = sorted(TRIGGER_DIR.glob("task_*.json"), key=os.path.getmtime)
count = 0
for tf in triggers:
# 检查是否已经通知过(通过 .notified 后缀标记)
notified_marker = tf.with_suffix(tf.suffix + ".notified")
if notified_marker.exists():
continue
process_trigger(tf)
# 标记已通知(不删除原文件,留给自动化消费)
try:
notified_marker.touch()
except OSError:
pass
count += 1
return count
# ─── 主循环 ────────────────────────────────────────────────────────
def main() -> None:
logger.info("=" * 50)
logger.info("Hub Task Runner 启动")
logger.info(f" 触发目录: {TRIGGER_DIR}")
logger.info(f" 轮询间隔: {POLL_INTERVAL}s")
logger.info("=" * 50)
TRIGGER_DIR.mkdir(parents=True, exist_ok=True)
_running = True
def signal_handler(signum, frame):
logger.info("收到退出信号")
nonlocal _running
_running = False
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
while _running:
try:
count = check_triggers()
if count > 0:
logger.info(f"已通知 {count} 个新任务")
except Exception as e:
logger.error(f"检查触发文件出错: {e}")
time.sleep(POLL_INTERVAL)
logger.info("Hub Task Runner 已停止")
if __name__ == "__main__":
main()
@@ -0,0 +1,422 @@
#!/usr/bin/env python3
"""
hub_watcher.py — Hub SSE 长连接守护进程
监听 Agent Communication Hub 的 SSE 事件流,实时响应 Hermes 分配的任务。
通过 launchd 保持常驻,实现秒级任务响应。
功能:
- SSE 长连接订阅 /events/workbuddy
- 收到 task_assigned → 通过 REST API 确认 + 写入信号文件
- 收到 new_message → 写入消息信号文件
- 断线自动重连(指数退避)
- 心跳超时检测
用法:
python3 hub_watcher.py # 前台运行(调试用)
launchd 管理(生产环境)
日志:
/tmp/hub-watcher.log (stdout)
/tmp/hub-watcher.err (stderr)
"""
from __future__ import annotations
import json
import logging
import os
import signal
import sys
import time
from datetime import datetime
from pathlib import Path
from typing import Any, Optional
from urllib.request import Request, urlopen
from urllib.error import URLError
# ─── 配置 ──────────────────────────────────────────────────────────
HUB_URL = os.getenv("HUB_URL", "http://localhost:3100")
AGENT_ID = os.getenv("HUB_AGENT_ID", "workbuddy")
SIGNAL_DIR = Path(os.getenv(
"SIGNAL_DIR",
str(Path.home() / ".hermes" / "shared" / "signals"),
))
# WorkBuddy Agent 触发目录——写入此目录的文件会被 WorkBuddy 自动化消费
WB_TRIGGER_DIR = Path(os.getenv(
"WB_TRIGGER_DIR",
str(Path.home() / ".workbuddy" / "hub-tasks"),
))
# 重连参数
RECONNECT_BASE_SEC = 2
RECONNECT_MAX_SEC = 60
SSE_TIMEOUT_SEC = 90 # 心跳间隔 10s,90s 无数据视为断线
# ─── 日志 ──────────────────────────────────────────────────────────
log_level = os.getenv("HUB_WATCHER_LOG", "INFO")
logging.basicConfig(
level=getattr(logging, log_level, logging.INFO),
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%H:%M:%S",
)
logger = logging.getLogger("hub_watcher")
# ─── HTTP 工具 ─────────────────────────────────────────────────────
def http_get(url: str, timeout: int = 10) -> Optional[bytes]:
"""简单的 HTTP GET,用于健康检查和 REST API 调用"""
try:
req = Request(url)
with urlopen(req, timeout=timeout) as resp:
return resp.read()
except Exception as e:
logger.warning(f"HTTP GET {url} 失败: {e}")
return None
def http_patch(url: str, data: dict, timeout: int = 10) -> bool:
"""HTTP PATCH,用于更新任务/消息状态"""
try:
body = json.dumps(data).encode("utf-8")
req = Request(url, data=body, method="PATCH")
req.add_header("Content-Type", "application/json")
with urlopen(req, timeout=timeout) as resp:
return resp.status < 400
except Exception as e:
logger.error(f"HTTP PATCH {url} 失败: {e}")
return False
def check_hub_health() -> bool:
"""检查 Hub 是否存活"""
data = http_get(f"{HUB_URL}/health", timeout=5)
if data:
try:
info = json.loads(data)
return info.get("status") == "ok"
except json.JSONDecodeError:
pass
return False
# ─── 信号文件写入 ──────────────────────────────────────────────────
def _ts() -> str:
return datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
def write_wb_trigger(task: dict) -> None:
"""
写入 WorkBuddy Agent 触发文件。
这个文件会被 WorkBuddy 的自动化任务读取并执行。
"""
WB_TRIGGER_DIR.mkdir(parents=True, exist_ok=True)
task_id = task.get("id", "")
trigger = {
"task_id": task_id,
"assigned_by": task.get("assigned_by", ""),
"description": task.get("description", ""),
"context": task.get("context", ""),
"priority": task.get("priority", "normal"),
"triggered_at": _ts(),
}
trigger_file = WB_TRIGGER_DIR / f"task_{task_id}.json"
try:
trigger_file.write_text(
json.dumps(trigger, ensure_ascii=False, indent=2),
encoding="utf-8",
)
logger.info(f"[触发] 已写入 {trigger_file.name}")
except OSError as e:
logger.error(f"[触发] 写入失败: {e}")
def write_signal(event_type: str, payload: dict) -> None:
"""
将 Hub 事件写入信号文件,供 WorkBuddy 读取处理。
文件格式与 hermes-memory-bridge 的信号格式兼容:
signals/sig_{uuid}.json
"""
SIGNAL_DIR.mkdir(parents=True, exist_ok=True)
import uuid
sig = {
"id": str(uuid.uuid4()),
"type": event_type,
"source": "Hub",
"timestamp": _ts(),
"data": payload,
}
sig_file = SIGNAL_DIR / f"sig_{sig['id'][:12]}.json"
try:
sig_file.write_text(
json.dumps(sig, ensure_ascii=False, indent=2),
encoding="utf-8",
)
logger.info(f"[信号] 已写入 {sig_file.name}")
except OSError as e:
logger.error(f"[信号] 写入失败: {e}")
# ─── 事件处理 ──────────────────────────────────────────────────────
def handle_task_assigned(task: dict) -> None:
"""
处理 task_assigned 事件:
1. REST API 标记 in_progress
2. 写入信号文件供 WorkBuddy 消费(旧通道)
3. 写入触发文件供 WorkBuddy Agent 自动化消费(新通道)
"""
task_id = task.get("id", "")
description = task.get("description", "")
assigned_by = task.get("assigned_by", "")
logger.info(f"[任务] 收到任务 {task_id[:16]}: {description[:60]}")
# 1. 标记 in_progress
ok = http_patch(
f"{HUB_URL}/api/tasks/{task_id}/status",
{"status": "in_progress", "progress": 10},
)
if ok:
logger.info(f"[任务] 已标记 in_progress")
else:
logger.warning(f"[任务] 标记 in_progress 失败,信号仍会写入")
# 2. 写入信号文件(旧通道,保留兼容)
write_signal("hub_task", {
"task_id": task_id,
"assigned_by": assigned_by,
"description": description,
"context": task.get("context", ""),
"priority": task.get("priority", "normal"),
"summary": f"Hermes 分配的任务: {description[:80]}",
})
# 3. 写入触发文件(新通道,触发 WorkBuddy Agent 自动化)
write_wb_trigger(task)
def handle_new_message(message: dict) -> None:
"""
处理 new_message 事件:
写入信号文件供 WorkBuddy 消费。
"""
msg_id = message.get("id", "")
from_agent = message.get("from_agent", "")
content = message.get("content", "")
logger.info(f"[消息] 收到 {from_agent} 的消息: {content[:60]}")
# 标记已投递
http_patch(
f"{HUB_URL}/api/messages/{msg_id}/status",
{"status": "read"},
)
# 写入信号
write_signal("hub_message", {
"message_id": msg_id,
"from_agent": from_agent,
"content": content,
"type": message.get("type", "message"),
"summary": f"来自 {from_agent} 的消息: {content[:80]}",
})
def handle_event(event_type: str, data: dict) -> None:
"""事件分发器"""
if event_type == "task_assigned":
handle_task_assigned(data.get("task", data))
elif event_type == "new_message":
handle_new_message(data.get("message", data))
elif event_type == "pending_messages":
messages = data.get("messages", [])
if messages:
logger.info(f"[积压] 补发 {len(messages)} 条积压消息")
for msg in messages:
handle_new_message(msg)
elif event_type == "task_updated":
# 任务状态更新通知(通常来自自己或 Hermes 的状态变更)
update = data.get("update", {})
logger.debug(f"[更新] 任务 {update.get('task_id', '')[:16]}{update.get('status')}")
else:
logger.debug(f"[未知事件] {event_type}: {json.dumps(data)[:100]}")
# ─── SSE 长连接 ────────────────────────────────────────────────────
class SSEStream:
"""轻量 SSE 流解析器(不依赖第三方库)"""
def __init__(self, url: str, timeout: int = SSE_TIMEOUT_SEC):
self.url = url
self.timeout = timeout
self._running = False
def connect(self) -> bool:
"""建立 SSE 连接,持续读取事件"""
self._running = True
while self._running:
if not check_hub_health():
logger.warning("Hub 不可用,等待重连...")
self._wait_reconnect()
continue
try:
logger.info(f"连接 SSE: {self.url}")
req = Request(self.url)
with urlopen(req, timeout=self.timeout) as resp:
logger.info("SSE 连接成功,开始监听...")
self._read_stream(resp)
except Exception as e:
if self._running:
logger.warning(f"SSE 连接断开: {e}")
self._wait_reconnect()
return True
def _read_stream(self, resp: Any) -> None:
"""持续读取 SSE 流"""
buffer = ""
raw_buf = b""
while True:
if not self._running:
break
chunk = resp.read(4096)
if not chunk:
break
raw_buf += chunk
# 完整解码,避免逐字节截断多字节 UTF-8 字符
buffer += raw_buf.decode("utf-8", errors="replace")
raw_buf = b""
# 解析 SSE 事件
while "\n\n" in buffer:
event_text, buffer = buffer.split("\n\n", 1)
self._parse_event(event_text)
def _parse_event(self, event_text: str) -> None:
"""解析单个 SSE 事件块"""
lines = event_text.strip().split("\n")
event_type = ""
data = ""
for line in lines:
if line.startswith("event:"):
event_type = line[6:].strip()
elif line.startswith("data:"):
data = line[5:].strip()
elif line == ":" or line.startswith(": "):
# SSE 心跳注释,忽略
pass
if data:
try:
payload = json.loads(data)
except json.JSONDecodeError:
logger.debug(f"非 JSON 数据: {data[:80]}")
return
# 如果 data 包含 jsonrpcMCP 响应),提取 result
if "result" in payload and "jsonrpc" in payload:
result = payload["result"]
if isinstance(result, dict) and "content" in result:
for item in result["content"]:
if item.get("type") == "text":
try:
inner = json.loads(item["text"])
if "event" in inner:
event_type = inner["event"]
data_payload = inner
handle_event(event_type, data_payload)
return
except (json.JSONDecodeError, TypeError):
pass
return
# 普通事件格式
if event_type or "event" in payload:
et = event_type or payload.get("event", "unknown")
handle_event(et, payload)
def _wait_reconnect(self) -> None:
"""指数退避等待重连"""
global _reconnect_delay
logger.info(f"将在 {_reconnect_delay}s 后重连...")
time.sleep(_reconnect_delay)
_reconnect_delay = min(_reconnect_delay * 2, RECONNECT_MAX_SEC)
def stop(self) -> None:
self._running = False
logger.info("SSE 流已停止")
# ─── 主循环 ────────────────────────────────────────────────────────
_reconnect_delay = RECONNECT_BASE_SEC
_sse: Optional[SSEStream] = None
def main() -> None:
global _reconnect_delay, _sse
logger.info("=" * 50)
logger.info("Hub Watcher 启动")
logger.info(f" Hub: {HUB_URL}")
logger.info(f" Agent: {AGENT_ID}")
logger.info(f" 信号目录: {SIGNAL_DIR}")
logger.info("=" * 50)
# 确保信号目录存在
SIGNAL_DIR.mkdir(parents=True, exist_ok=True)
# 启动前先拉取积压任务
if check_hub_health():
logger.info("拉取积压任务...")
data = http_get(f"{HUB_URL}/api/tasks?agent_id={AGENT_ID}&status=pending")
if data:
try:
result = json.loads(data)
tasks = result.get("tasks", [])
for task in tasks:
handle_task_assigned(task)
if tasks:
logger.info(f"已处理 {len(tasks)} 个积压任务")
except json.JSONDecodeError:
pass
else:
logger.warning("Hub 不可用,跳过积压任务拉取")
# 启动 SSE 长连接
sse_url = f"{HUB_URL}/events/{AGENT_ID}"
_sse = SSEStream(sse_url)
def signal_handler(signum, frame):
logger.info("收到退出信号")
if _sse:
_sse.stop()
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# 连接成功后重置重连延迟
global _reconnect_delay
_sse.connect()
if __name__ == "__main__":
main()
+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash
# install.sh — 一键安装依赖并构建
set -e
cd "$(dirname "$0")"
echo "=== 安装 Agent Communication Hub ==="
echo ""
echo "[1/3] 安装 npm 依赖..."
npm install
echo "[2/3] 编译 TypeScript..."
npm run build
echo "[3/3] 验证构建..."
node dist/server.js --help 2>/dev/null && echo "构建成功!" || echo "构建完成 (server.js 不需要 --help)"
echo ""
echo "✅ 安装完成!使用以下命令启动:"
echo " 开发模式: npm run dev"
echo " 生产模式: npm start"
echo " 端到端测试: npm test"
@@ -0,0 +1,180 @@
#!/usr/bin/env python3
"""
evolution.db → comm_hub.db strategies 数据迁移脚本
将 Hermes 旧版 evolution.db 中的 memories 数据迁移到 Hub 的 strategies 表。
经核实(2026-04-24),evolution.db memories 表为空(0 条记录),
本脚本作为未来数据迁移的备用工具。
用法:
python3 migrate_evolution_db.py [--source /path/to/evolution.db] [--target /path/to/comm_hub.db] [--dry-run]
选项:
--source 源数据库路径(默认 ~/.workbuddy/memory/evolution.db
--target 目标数据库路径(默认 ../comm_hub.db
--dry-run 只分析不写入,打印迁移计划
"""
import sqlite3
import hashlib
import sys
import os
import argparse
from datetime import datetime
# ─── 默认路径 ─────────────────────────────────────────────
DEFAULT_SOURCE = os.path.expanduser("~/.workbuddy/memory/evolution.db")
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
DEFAULT_TARGET = os.path.join(SCRIPT_DIR, "..", "comm_hub.db")
# ─── evolution.db memories → strategies 字段映射 ─────────
# memories 表:id, hash, content, category, importance, tags, source, created_at, last_accessed
# strategies 表:id, title, content, category, sensitivity, proposer_id, status, ...
# category 映射
CATEGORY_MAP = {
"general": "other",
"workflow": "workflow",
"fix": "fix",
"tool_config": "tool_config",
"prompt_template": "prompt_template",
"experience": "experience",
"other": "other",
}
# importance → sensitivity 映射
# evolution.db importance 1-55=最高 → sensitivity high/normal
def map_sensitivity(importance: int, content: str) -> str:
"""将 importance 值映射为 sensitivity"""
if importance >= 4:
return "high"
# 高敏感关键词检测(与 Hub 逻辑一致)
high_patterns = [
"system_prompt", "系统指令", "capability_declare",
"能力声明", "permission_change", "权限变更", "role_change",
]
for p in high_patterns:
if p.lower() in content.lower():
return "high"
return "normal"
def generate_title(content: str, max_len: int = 200) -> str:
"""从 content 生成标题(取第一行或前 100 字符)"""
first_line = content.split("\n")[0].strip()
if first_line and len(first_line) <= max_len:
return first_line
return content[:max_len - 3] + "..."
def migrate(source_path: str, target_path: str, dry_run: bool = False):
"""执行迁移"""
print(f"=== evolution.db → comm_hub.db 迁移 ===")
print(f"源: {source_path}")
print(f"目标: {target_path}")
print(f"模式: {'DRY RUN' if dry_run else 'LIVE'}")
print()
# 检查源文件
if not os.path.exists(source_path):
print(f"❌ 源文件不存在: {source_path}")
return False
# 连接数据库
src = sqlite3.connect(source_path)
tgt = sqlite3.connect(target_path)
# 读取源数据
rows = src.execute("SELECT id, hash, content, category, importance, tags, source, created_at FROM memories").fetchall()
print(f"源 memories 表: {len(rows)} 条记录")
if not rows:
print("✅ 无数据需要迁移,退出")
src.close()
tgt.close()
return True
# 分析可迁移数据
migrated = 0
skipped = 0
errors = 0
for row in rows:
mem_id, mem_hash, content, category, importance, tags, source_agent, created_at = row
# 验证必要字段
if not content or len(content) < 10:
print(f" ⏭️ 跳过 id={mem_id}: 内容过短(<10 字符)")
skipped += 1
continue
if len(content) > 5000:
print(f" ⏭️ 跳过 id={mem_id}: 内容过长(>{5000} 字符)")
skipped += 1
continue
# 映射字段
title = generate_title(content)
hub_category = CATEGORY_MAP.get(category, "other")
sensitivity = map_sensitivity(importance, content)
proposer_id = source_agent if source_agent and source_agent != "manual" else "migration_script"
tags_json = tags if tags else "[]"
print(f" 📋 迁移 id={mem_id}: '{title[:50]}...' → category={hub_category}, sensitivity={sensitivity}")
if not dry_run:
try:
tgt.execute("""
INSERT INTO strategies (title, content, category, sensitivity, proposer_id, status, proposed_at, task_id, source_trust)
VALUES (?, ?, ?, ?, ?, 'approved', ?, ?, 50)
""", (title, content, hub_category, sensitivity, proposer_id, created_at, tags_json))
# 获取新 id 并插入 FTS
new_id = tgt.execute("SELECT last_insert_rowid()").fetchone()[0]
try:
tgt.execute("""
INSERT INTO strategies_fts (rowid, title, content, category)
VALUES (?, ?, ?, ?)
""", (new_id, title, content, hub_category))
except Exception as fts_err:
print(f" ⚠️ FTS 插入失败: {fts_err}")
migrated += 1
except Exception as e:
print(f" ❌ 错误: {e}")
errors += 1
if not dry_run:
tgt.commit()
# 一致性验证
if not dry_run:
tgt_total = tgt.execute("SELECT COUNT(*) FROM strategies WHERE proposer_id='migration_script'").fetchone()[0]
print(f"\n=== 迁移验证 ===")
print(f"迁移写入: {migrated}")
print(f"跳过: {skipped}")
print(f"错误: {errors}")
print(f"目标表迁移记录: {tgt_total}")
if tgt_total == migrated:
print("✅ 一致性验证通过")
else:
print("⚠️ 一致性验证不匹配!")
else:
print(f"\n=== DRY RUN 摘要 ===")
print(f"可迁移: {migrated + skipped - errors}")
print(f"将跳过: {skipped}")
src.close()
tgt.close()
return errors == 0
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="evolution.db → comm_hub.db 数据迁移")
parser.add_argument("--source", default=DEFAULT_SOURCE, help="源 evolution.db 路径")
parser.add_argument("--target", default=DEFAULT_TARGET, help="目标 comm_hub.db 路径")
parser.add_argument("--dry-run", action="store_true", help="只分析不写入")
args = parser.parse_args()
success = migrate(args.source, args.target, args.dry_run)
sys.exit(0 if success else 1)
@@ -0,0 +1,78 @@
#!/usr/bin/env node
/**
* migrate_from_agent.js — Phase 2.1 数据迁移
* 将历史消息中不规范的发件人标识规范化
*
* 运行前:确保 Hub 服务已停止(或使用离线备份数据库)
* 运行方式:node scripts/migrate_from_agent.js
*/
import Database from "better-sqlite3";
import { resolve } from "path";
const DB_PATH = resolve(process.cwd(), "comm_hub.db");
const db = new Database(DB_PATH);
// 别名映射(与 src/identity.ts 保持同步)
const ALIAS_MAP = {
qclaw: "agent_1c11a7bd_1777129814251",
workbuddy: "agent_workbuddy_a3f7c2e1_1777300825754",
hermes: "agent_hermes_54cfe58b_1777132066111",
};
console.log(`\n=== Phase 2.1: from_agent 格式规范化迁移 ===`);
console.log(`DB: ${DB_PATH}\n`);
let totalFrom = 0;
let totalTo = 0;
// 从 from_agent 开始迁移
for (const [alias, fullId] of Object.entries(ALIAS_MAP)) {
const fromResult = db.prepare(
`UPDATE messages SET from_agent = ? WHERE from_agent = ?`
).run(fullId, alias);
if (fromResult.changes > 0) {
console.log(`✅ from_agent: '${alias}' → '${fullId}' (${fromResult.changes} 行)`);
totalFrom += fromResult.changes;
}
const toResult = db.prepare(
`UPDATE messages SET to_agent = ? WHERE to_agent = ?`
).run(fullId, alias);
if (toResult.changes > 0) {
console.log(`✅ to_agent: '${alias}' → '${fullId}' (${toResult.changes} 行)`);
totalTo += toResult.changes;
}
}
// 验证:确认无遗留别名
console.log(`\n验证:`);
// 检查 from_agent 是否还有别名
const remainingFrom = db.prepare(`
SELECT DISTINCT from_agent FROM messages
WHERE from_agent IN (${Object.keys(ALIAS_MAP).map(() => '?').join(',')})
`).all(...Object.keys(ALIAS_MAP));
if (remainingFrom.length === 0) {
console.log(`✅ 所有 from_agent 已规范化`);
} else {
console.log(`⚠️ 仍有未处理的 from_agent: ${remainingFrom.map(r => r.from_agent).join(', ')}`);
}
const remainingTo = db.prepare(`
SELECT DISTINCT to_agent FROM messages
WHERE to_agent IN (${Object.keys(ALIAS_MAP).map(() => '?').join(',')})
`).all(...Object.keys(ALIAS_MAP));
if (remainingTo.length === 0) {
console.log(`✅ 所有 to_agent 已规范化`);
} else {
console.log(`⚠️ 仍有未处理的 to_agent: ${remainingTo.map(r => r.to_agent).join(', ')}`);
}
// 统计
const total = db.prepare(`SELECT COUNT(*) as cnt FROM messages`).get();
console.log(`\n总计:迁移 ${totalFrom} 条 from_agent${totalTo} 条 to_agent`);
console.log(`消息表总行数:${total.cnt}`);
db.close();
console.log(`\n✅ 迁移完成`);
@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""
wb_task_trigger.py — WorkBuddy 任务触发器
监听 ~/.workbuddy/hub-tasks/ 目录中的触发文件。
当 hub_watcher 写入触发文件时,此脚本通过 FSEvents 检测到变化,
然后触发 WorkBuddy 自动化执行。
实际上我们不需要监听——因为 hub_watcher 已经通过信号文件通知了。
这里用一个简单的方案:通过 launchd 每60秒执行此脚本,
检查是否有未处理的触发文件。
如果 WorkBuddy 自动化已经在处理 Hub 任务了,这个脚本什么都不做。
如果自动化还没轮到,此脚本会尝试直接通知 WorkBuddy。
注意:这个脚本的真正作用是缩短延迟。
最终执行任务的仍然是 WorkBuddy Agent(通过自动化)。
"""
import json
import os
import sys
import time
from pathlib import Path
from datetime import datetime
TRIGGER_DIR = Path.home() / ".workbuddy" / "hub-tasks"
LOCK_FILE = TRIGGER_DIR / ".poll_lock"
def check_triggers():
"""检查是否有未处理的触发文件"""
if not TRIGGER_DIR.exists():
return 0
triggers = list(TRIGGER_DIR.glob("task_*.json"))
# 排除锁文件
triggers = [f for f in triggers if not f.name.startswith(".")]
return len(triggers)
def get_trigger_info():
"""获取第一个触发文件的信息"""
if not TRIGGER_DIR.exists():
return None
triggers = sorted(TRIGGER_DIR.glob("task_*.json"), key=os.path.getmtime)
if not triggers:
return None
try:
with open(triggers[0], "r", encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, OSError):
return None
if __name__ == "__main__":
count = check_triggers()
if count > 0:
info = get_trigger_info()
ts = datetime.now().strftime("%H:%M:%S")
task_id = info.get("task_id", "?") if info else "?"
desc = info.get("description", "?")[:40] if info else "?"
print(f"[{ts}] 等待处理的 Hub 任务: {count}")
print(f" 最新: {task_id} | {desc}")
# 退出码 0 但有输出 → WorkBuddy 自动化系统会看到输出并触发
sys.exit(0)
else:
sys.exit(0)
+120
View File
@@ -0,0 +1,120 @@
/**
* db.ts — SQLite 持久化层
* 消息 + 任务 两张表,进程重启数据不丢失
*/
import { type Database as DatabaseType, type Statement } from "better-sqlite3";
export declare const db: DatabaseType;
export interface Message {
id: string;
from_agent: string;
to_agent: string;
content: string;
type: "message" | "task_assign" | "task_update" | "ack";
metadata?: string | null;
status: "unread" | "delivered" | "read" | "acknowledged";
created_at: number;
}
export declare const msgStmt: Record<string, Statement>;
export interface ConsumedEntry {
id: string;
agent_id: string;
resource: string;
resource_type: string;
action: string;
notes?: string | null;
consumed_at: number;
}
export declare const consumedStmt: Record<string, Statement>;
export interface Task {
id: string;
assigned_by: string;
assigned_to: string;
description: string;
context?: string | null;
priority: "low" | "normal" | "high" | "urgent";
status: "inbox" | "assigned" | "waiting" | "pending" | "in_progress" | "completed" | "failed" | "cancelled";
result?: string | null;
progress: number;
pipeline_id?: string | null;
order_index: number;
required_capability?: string | null;
due_at?: number | null;
assigned_at?: number | null;
completed_at?: number | null;
tags?: string | null;
parallel_group?: string | null;
handoff_status?: string | null;
handoff_to?: string | null;
created_at: number;
updated_at: number;
}
export declare const taskStmt: Record<string, Statement>;
export interface Pipeline {
id: string;
name: string;
description?: string | null;
status: "draft" | "active" | "completed" | "cancelled";
creator: string;
config?: string | null;
created_at: number;
updated_at: number;
}
export interface PipelineTask {
id: string;
pipeline_id: string;
task_id: string;
order_index: number;
created_at: number;
}
export declare const pipelineStmt: Record<string, Statement>;
export declare const pipelineTaskStmt: Record<string, Statement>;
export interface Attachment {
id: string;
message_id: string;
filename: string;
mime_type: string;
file_size: number;
storage_path: string;
uploaded_by: string;
created_at: number;
}
export declare const attachStmt: Record<string, Statement>;
export declare function getDbStats(): Record<string, number>;
/**
* 归档 N 天前的消息(从 messages 移到 messages_archive
* @returns 归档的记录数
*/
export declare function archiveOldMessages(days?: number): number;
/**
* 归档 N 天前的审计日志(从 audit_log 移到 audit_log_archive
* @returns 归档的记录数
*/
export declare function archiveOldAuditLogs(days?: number): number;
/**
* 执行数据库 VACUUM(释放空闲页面,紧缩数据库文件)
* 建议在低峰期调用(如凌晨 3-5 点)
*/
export declare function vacuumDatabase(): void;
/**
* 获取数据库文件大小(字节)
*/
export declare function getDbSize(): number;
/**
* 获取增强版数据库统计信息(用于 MCP 工具 get_db_stats
*/
export declare function getEnhancedDbStats(): {
table_counts: Record<string, number>;
database_size_bytes: number;
database_size_mb: number;
wal_size_bytes: number;
last_messages_archive: string | null;
last_audit_log_archive: string | null;
};
/**
* 定时清理过期数据(每小时执行一次)
* - 过期的 API Tokentoken_type='api_token'
* - 过期的去重缓存(超过 dedupTTL 秒)
* - 过期的消费日志(>1天)
*/
export declare function scheduleCleanup(dedupTTL: number): void;
export declare function stopCleanup(): void;
+778
View File
@@ -0,0 +1,778 @@
/**
* db.ts — SQLite 持久化层
* 消息 + 任务 两张表,进程重启数据不丢失
*/
import Database from "better-sqlite3";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
import { logger, logError } from "./logger.js";
const __dir = dirname(fileURLToPath(import.meta.url));
const DB_PATH = join(__dir, "../comm_hub.db");
export const db = new Database(DB_PATH);
// 开启 WAL 模式,提升并发读写性能
db.pragma("journal_mode = WAL");
db.pragma("synchronous = NORMAL");
// ─── 建表 ──────────────────────────────────────────────
db.exec(`
CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY,
from_agent TEXT NOT NULL,
to_agent TEXT NOT NULL,
content TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'message',
metadata TEXT,
status TEXT NOT NULL DEFAULT 'unread',
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY,
assigned_by TEXT NOT NULL,
assigned_to TEXT NOT NULL DEFAULT '',
description TEXT NOT NULL,
context TEXT,
priority TEXT NOT NULL DEFAULT 'normal',
status TEXT NOT NULL DEFAULT 'inbox',
result TEXT,
progress INTEGER DEFAULT 0,
pipeline_id TEXT,
order_index INTEGER DEFAULT 0,
required_capability TEXT,
due_at INTEGER,
assigned_at INTEGER,
completed_at INTEGER,
tags TEXT DEFAULT '[]',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
-- 消费水位线表:记录 Agent 已处理过的文件路径,防止重复消费
CREATE TABLE IF NOT EXISTS consumed_log (
id TEXT PRIMARY KEY,
agent_id TEXT NOT NULL,
resource TEXT NOT NULL,
resource_type TEXT NOT NULL DEFAULT 'file', -- 'file' | 'signal' | 'message'
action TEXT NOT NULL,
notes TEXT,
consumed_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_messages_to_agent ON messages(to_agent, status);
CREATE INDEX IF NOT EXISTS idx_tasks_assigned_to ON tasks(assigned_to, status);
CREATE INDEX IF NOT EXISTS idx_consumed_log ON consumed_log(agent_id, resource);
`);
// ─── Phase 4a Migration: tasks 表新增字段 ──────────────
// 必须在 taskStmt.insert 之前执行
try {
const taskCols = db.pragma("table_info(tasks)");
if (taskCols.length > 0) {
const colNames = taskCols.map((c) => c.name);
const migrations = [
["pipeline_id", "ALTER TABLE tasks ADD COLUMN pipeline_id TEXT"],
["order_index", "ALTER TABLE tasks ADD COLUMN order_index INTEGER DEFAULT 0"],
["required_capability", "ALTER TABLE tasks ADD COLUMN required_capability TEXT"],
["due_at", "ALTER TABLE tasks ADD COLUMN due_at INTEGER"],
["assigned_at", "ALTER TABLE tasks ADD COLUMN assigned_at INTEGER"],
["completed_at", "ALTER TABLE tasks ADD COLUMN completed_at INTEGER"],
["tags", "ALTER TABLE tasks ADD COLUMN tags TEXT DEFAULT '[]'"],
];
for (const [col, sql] of migrations) {
if (!colNames.includes(col)) {
db.exec(sql);
logger.info("db_migration", { module: "db", column: col, table: "tasks" });
}
}
}
}
catch (e) {
logger.warn("db_migration_warning", { module: "db", table: "tasks", phase: "4a", error: e.message });
}
// ─── Phase 4a 新表:pipelines + pipeline_tasks ──────────
// 必须在 pipelineStmt 和 taskStmt.listByPipeline 之前创建
db.exec(`
CREATE TABLE IF NOT EXISTS pipelines (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'draft',
creator TEXT NOT NULL,
config TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_pipelines_creator ON pipelines(creator);
CREATE INDEX IF NOT EXISTS idx_pipelines_status ON pipelines(status);
`);
db.exec(`
CREATE TABLE IF NOT EXISTS pipeline_tasks (
id TEXT PRIMARY KEY,
pipeline_id TEXT NOT NULL REFERENCES pipelines(id),
task_id TEXT NOT NULL REFERENCES tasks(id),
order_index INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
UNIQUE(pipeline_id, task_id)
);
CREATE INDEX IF NOT EXISTS idx_pipeline_tasks_pipe ON pipeline_tasks(pipeline_id);
CREATE INDEX IF NOT EXISTS idx_pipeline_tasks_order ON pipeline_tasks(pipeline_id, order_index);
`);
export const msgStmt = {
insert: db.prepare(`INSERT INTO messages VALUES (@id,@from_agent,@to_agent,@content,@type,@metadata,@status,@created_at)`),
markDelivered: db.prepare(`UPDATE messages SET status='delivered' WHERE id=?`),
markRead: db.prepare(`UPDATE messages SET status='read' WHERE id=?`),
markAcknowledged: db.prepare(`UPDATE messages SET status='acknowledged' WHERE id=?`),
pendingFor: db.prepare(`SELECT * FROM messages WHERE to_agent=? AND status='unread' ORDER BY created_at ASC`),
markAllDelivered: db.prepare(`UPDATE messages SET status='delivered' WHERE to_agent=? AND status='unread'`),
getById: db.prepare(`SELECT * FROM messages WHERE id=?`),
};
export const consumedStmt = {
insert: db.prepare(`INSERT OR REPLACE INTO consumed_log VALUES (@id,@agent_id,@resource,@resource_type,@action,@notes,@consumed_at)`),
check: db.prepare(`SELECT * FROM consumed_log WHERE agent_id=? AND resource=?`),
listByAgent: db.prepare(`SELECT * FROM consumed_log WHERE agent_id=? ORDER BY consumed_at DESC LIMIT ?`),
};
export const taskStmt = {
insert: db.prepare(`INSERT INTO tasks (id, assigned_by, assigned_to, description, context, priority, status, result, progress, pipeline_id, order_index, required_capability, due_at, assigned_at, completed_at, tags, created_at, updated_at)
VALUES (@id, @assigned_by, @assigned_to, @description, @context, @priority, @status, @result, @progress, @pipeline_id, @order_index, @required_capability, @due_at, @assigned_at, @completed_at, @tags, @created_at, @updated_at)`),
getById: db.prepare(`SELECT * FROM tasks WHERE id=?`),
update: db.prepare(`UPDATE tasks SET status=?,result=?,progress=?,updated_at=? WHERE id=?`),
updateAssignee: db.prepare(`UPDATE tasks SET assigned_to=?,assigned_at=?,status='assigned',updated_at=? WHERE id=?`),
listFor: db.prepare(`SELECT * FROM tasks WHERE assigned_to=? AND status=? ORDER BY created_at DESC`),
listByPipeline: db.prepare(`SELECT t.* FROM tasks t JOIN pipeline_tasks pt ON t.id=pt.task_id WHERE pt.pipeline_id=? ORDER BY pt.order_index ASC`),
};
export const pipelineStmt = {
insert: db.prepare(`INSERT INTO pipelines VALUES (@id,@name,@description,@status,@creator,@config,@created_at,@updated_at)`),
getById: db.prepare(`SELECT * FROM pipelines WHERE id=?`),
updateStatus: db.prepare(`UPDATE pipelines SET status=?,updated_at=? WHERE id=?`),
listByCreator: db.prepare(`SELECT * FROM pipelines WHERE creator=? ORDER BY created_at DESC`),
};
export const pipelineTaskStmt = {
insert: db.prepare(`INSERT OR REPLACE INTO pipeline_tasks VALUES (@id,@pipeline_id,@task_id,@order_index,@created_at)`),
listByPipeline: db.prepare(`SELECT * FROM pipeline_tasks WHERE pipeline_id=? ORDER BY order_index ASC`),
deleteByTask: db.prepare(`DELETE FROM pipeline_tasks WHERE task_id=?`),
};
// ═══════════════════════════════════════════════════════════════
// Phase 1 — Security + Identity + Dedup + Memory 表
// ═══════════════════════════════════════════════════════════════
// --- agents 表:Agent 注册与在线状态 ---
// Phase 2 Day 4 Migration: 先添加 trust_score 列(必须在建索引前)
try {
const agentCols = db.pragma("table_info(agents)");
if (agentCols.length > 0 && !agentCols.some((c) => c.name === "trust_score")) {
db.exec(`ALTER TABLE agents ADD COLUMN trust_score INTEGER NOT NULL DEFAULT 50`);
logger.info("db_migration", { module: "db", column: "trust_score", table: "agents" });
}
}
catch (e) {
logger.warn("db_migration_warning", { module: "db", column: "trust_score", table: "agents", error: e.message });
}
db.exec(`
CREATE TABLE IF NOT EXISTS agents (
agent_id TEXT PRIMARY KEY,
name TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'member', -- 'admin' | 'member'
api_token TEXT, -- SHA-256 hash
status TEXT NOT NULL DEFAULT 'offline', -- 'online' | 'offline'
trust_score INTEGER NOT NULL DEFAULT 50, -- Phase 2 Day 4: 信任分 0-100
last_heartbeat INTEGER,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status);
CREATE INDEX IF NOT EXISTS idx_agents_heartbeat ON agents(last_heartbeat);
CREATE INDEX IF NOT EXISTS idx_agents_trust ON agents(trust_score);
`);
// Phase 2 Day 4 Migration: 为已有 agents 表添加 trust_score 字段
try {
const agentCols = db.pragma("table_info(agents)");
if (!agentCols.some((c) => c.name === "trust_score")) {
db.exec(`ALTER TABLE agents ADD COLUMN trust_score INTEGER NOT NULL DEFAULT 50`);
logger.info("db_migration", { module: "db", column: "trust_score", table: "agents", fallback: true });
}
}
catch (e) {
logger.warn("db_migration_warning", { module: "db", column: "trust_score", table: "agents", error: e.message });
}
// --- auth_tokens 表:邀请码 + API Token 管理 ---
db.exec(`
CREATE TABLE IF NOT EXISTS auth_tokens (
token_id TEXT PRIMARY KEY,
token_type TEXT NOT NULL, -- 'invite_code' | 'api_token'
token_value TEXT NOT NULL, -- SHA-256 hash
agent_id TEXT, -- api_token 关联的 agent
role TEXT, -- api_token 关联的角色
used INTEGER DEFAULT 0, -- 1 = 已使用
created_at INTEGER NOT NULL,
expires_at INTEGER,
revoked_at INTEGER,
UNIQUE(token_type, token_value)
);
CREATE INDEX IF NOT EXISTS idx_auth_tokens_type ON auth_tokens(token_type, used);
CREATE INDEX IF NOT EXISTS idx_auth_tokens_agent ON auth_tokens(agent_id);
`);
// --- dedup_cache 表:消息去重缓存 ---
db.exec(`
CREATE TABLE IF NOT EXISTS dedup_cache (
msg_hash TEXT PRIMARY KEY,
sender_id TEXT NOT NULL,
nonce INTEGER NOT NULL,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_dedup_sender_nonce ON dedup_cache(sender_id, nonce);
`);
// --- memories 表 + FTS5 全文索引(N-gram 中文分词) ---
// Phase 2 Day 4 Migration: 先添加溯源列(必须在建索引前)
try {
const memCols = db.pragma("table_info(memories)");
if (memCols.length > 0) {
if (!memCols.some((c) => c.name === "source_agent_id")) {
db.exec(`ALTER TABLE memories ADD COLUMN source_agent_id TEXT`);
logger.info("db_migration", { module: "db", column: "source_agent_id", table: "memories" });
}
if (!memCols.some((c) => c.name === "source_task_id")) {
db.exec(`ALTER TABLE memories ADD COLUMN source_task_id TEXT`);
logger.info("db_migration", { module: "db", column: "source_task_id", table: "memories" });
}
}
}
catch (e) {
logger.warn("db_migration_warning", { module: "db", table: "memories", error: e.message });
}
db.exec(`
CREATE TABLE IF NOT EXISTS memories (
id TEXT PRIMARY KEY,
agent_id TEXT NOT NULL,
title TEXT,
content TEXT NOT NULL,
fts_tokens TEXT NOT NULL DEFAULT '', -- Phase 2: N-gram 预分词 tokens
scope TEXT NOT NULL DEFAULT 'private', -- 'private' | 'group' | 'collective'
tags TEXT, -- JSON array
source_agent_id TEXT, -- Phase 2 Day 4: 溯源 — 实际写入者
source_task_id TEXT, -- Phase 2 Day 4: 溯源 — 关联任务
created_at INTEGER NOT NULL,
updated_at INTEGER
);
CREATE INDEX IF NOT EXISTS idx_memories_agent ON memories(agent_id);
CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(scope);
CREATE INDEX IF NOT EXISTS idx_memories_source ON memories(source_agent_id);
`);
// Phase 2 Migration: 为已有 memories 表添加 fts_tokens 列
try {
const colInfo = db.pragma("table_info(memories)");
const hasFtsTokens = colInfo.some((c) => c.name === "fts_tokens");
if (!hasFtsTokens) {
db.exec(`ALTER TABLE memories ADD COLUMN fts_tokens TEXT NOT NULL DEFAULT ''`);
logger.info("db_migration", { module: "db", column: "fts_tokens", table: "memories" });
}
}
catch (e) {
logger.warn("db_migration_warning", { module: "db", column: "fts_tokens", table: "memories", error: e.message });
}
// FTS5 虚拟表(独立存储模式 + fts_tokens 列)
try {
// Phase 2: 旧版 FTS5 可能已存在(external content 模式),需要重建
// 先尝试删除旧表(ignore error 如果不存在)
try {
db.exec(`DROP TRIGGER IF EXISTS memories_ai`);
db.exec(`DROP TRIGGER IF EXISTS memories_ad`);
db.exec(`DROP TRIGGER IF EXISTS memories_au`);
db.exec(`DROP TABLE IF EXISTS memories_fts`);
}
catch {
// ignore
}
// 新版 FTS5:独立存储 fts_tokens 列
db.exec(`
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
title,
content,
tags,
fts_tokens
);
`);
}
catch (e) {
if (!e.message.includes("already exists")) {
logger.warn("db_fts5_init_warning", { module: "db", error: e.message });
}
}
// --- agents_capabilities 表 ---
db.exec(`
CREATE TABLE IF NOT EXISTS agent_capabilities (
id TEXT PRIMARY KEY,
agent_id TEXT NOT NULL,
capability TEXT NOT NULL,
params TEXT, -- JSON
verified INTEGER DEFAULT 0,
verified_at INTEGER,
created_at INTEGER NOT NULL,
FOREIGN KEY (agent_id) REFERENCES agents(agent_id)
);
CREATE INDEX IF NOT EXISTS idx_capabilities_agent ON agent_capabilities(agent_id);
`);
// --- audit_log 表 ---
db.exec(`
CREATE TABLE IF NOT EXISTS audit_log (
id TEXT PRIMARY KEY,
action TEXT NOT NULL,
agent_id TEXT,
target TEXT,
details TEXT,
ip_address TEXT,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_audit_log_time ON audit_log(created_at);
`);
// ═══════════════════════════════════════════════════════════════
// Phase 3 — Evolution Engine 表
// ═══════════════════════════════════════════════════════════════
// strategies 表
db.exec(`
CREATE TABLE IF NOT EXISTS strategies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
category TEXT NOT NULL DEFAULT 'workflow',
sensitivity TEXT NOT NULL DEFAULT 'normal',
proposer_id TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
approve_reason TEXT,
approved_by TEXT,
approved_at INTEGER,
proposed_at INTEGER NOT NULL,
task_id TEXT,
source_trust INTEGER NOT NULL DEFAULT 50,
apply_count INTEGER NOT NULL DEFAULT 0,
feedback_count INTEGER NOT NULL DEFAULT 0,
positive_count INTEGER NOT NULL DEFAULT 0,
UNIQUE(title, proposer_id, proposed_at)
);
CREATE INDEX IF NOT EXISTS idx_strategies_status ON strategies(status);
CREATE INDEX IF NOT EXISTS idx_strategies_proposer ON strategies(proposer_id);
CREATE INDEX IF NOT EXISTS idx_strategies_category ON strategies(category);
`);
// strategy_feedback 表(UNIQUE 防刷)
db.exec(`
CREATE TABLE IF NOT EXISTS strategy_feedback (
id INTEGER PRIMARY KEY AUTOINCREMENT,
strategy_id INTEGER NOT NULL REFERENCES strategies(id) ON DELETE CASCADE,
agent_id TEXT NOT NULL,
feedback TEXT NOT NULL,
comment TEXT,
applied INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
UNIQUE(strategy_id, agent_id)
);
CREATE INDEX IF NOT EXISTS idx_feedback_strategy ON strategy_feedback(strategy_id);
CREATE INDEX IF NOT EXISTS idx_feedback_agent ON strategy_feedback(agent_id);
`);
// strategy_applications 表(采纳记录)
db.exec(`
CREATE TABLE IF NOT EXISTS strategy_applications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
strategy_id INTEGER NOT NULL REFERENCES strategies(id) ON DELETE CASCADE,
agent_id TEXT NOT NULL,
context TEXT,
result TEXT,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_applications_strategy ON strategy_applications(strategy_id);
CREATE INDEX IF NOT EXISTS idx_applications_agent ON strategy_applications(agent_id);
`);
// FTS5 全文索引(N-gram 中文分词,与 memories 一致)
try {
try {
db.exec(`DROP TRIGGER IF EXISTS strategies_ai`);
db.exec(`DROP TRIGGER IF EXISTS strategies_ad`);
db.exec(`DROP TRIGGER IF EXISTS strategies_au`);
db.exec(`DROP TABLE IF EXISTS strategies_fts`);
}
catch {
// ignore
}
db.exec(`
CREATE VIRTUAL TABLE IF NOT EXISTS strategies_fts USING fts5(
title, content, category
);
`);
}
catch (e) {
if (!e.message.includes("already exists")) {
logger.warn("db_strategies_fts5_init_warning", { module: "db", error: e.message });
}
}
// ═══════════════════════════════════════════════════════════════
// Phase 4b — Task Orchestrator 进阶(依赖链 + 质量门 + 交接 + 分级审批)
// ═══════════════════════════════════════════════════════════════
// --- Phase 4b: tasks 表扩展列 ---
try {
const taskCols = db.pragma("table_info(tasks)");
if (taskCols.length > 0) {
const colNames = taskCols.map((c) => c.name);
const colMigrations = [
["parallel_group", "ALTER TABLE tasks ADD COLUMN parallel_group TEXT DEFAULT NULL"],
["handoff_status", "ALTER TABLE tasks ADD COLUMN handoff_status TEXT DEFAULT 'none'"],
// Phase 4b Day 3: 交接协议目标 Agent
["handoff_to", "ALTER TABLE tasks ADD COLUMN handoff_to TEXT DEFAULT NULL"],
];
for (const [col, sql] of colMigrations) {
if (!colNames.includes(col)) {
db.exec(sql);
logger.info("db_migration", { module: "db", column: col, table: "tasks", phase: "4b" });
}
}
}
}
catch (e) {
logger.warn("db_migration_warning", { module: "db", table: "tasks", phase: "4b", error: e.message });
}
// --- Phase 4b: strategies 表扩展列 ---
try {
const stratCols = db.pragma("table_info(strategies)");
if (stratCols.length > 0) {
const colNames = stratCols.map((c) => c.name);
const colMigrations = [
["approval_tier", "ALTER TABLE strategies ADD COLUMN approval_tier TEXT DEFAULT 'admin'"],
["observation_start", "ALTER TABLE strategies ADD COLUMN observation_start INTEGER"],
["veto_deadline", "ALTER TABLE strategies ADD COLUMN veto_deadline INTEGER"],
];
for (const [col, sql] of colMigrations) {
if (!colNames.includes(col)) {
db.exec(sql);
logger.info("db_migration", { module: "db", column: col, table: "strategies", phase: "4b" });
}
}
}
}
catch (e) {
logger.warn("db_migration_warning", { module: "db", table: "strategies", phase: "4b", error: e.message });
}
// --- Phase 4b: task_dependencies 表(依赖链) ---
db.exec(`
CREATE TABLE IF NOT EXISTS task_dependencies (
id TEXT PRIMARY KEY,
upstream_id TEXT NOT NULL,
downstream_id TEXT NOT NULL,
dep_type TEXT NOT NULL DEFAULT 'finish_to_start',
status TEXT NOT NULL DEFAULT 'pending',
created_at INTEGER NOT NULL,
UNIQUE(upstream_id, downstream_id)
);
CREATE INDEX IF NOT EXISTS idx_deps_downstream ON task_dependencies(downstream_id, status);
CREATE INDEX IF NOT EXISTS idx_deps_upstream ON task_dependencies(upstream_id, status);
`);
// --- Phase 4b: quality_gates 表(质量门) ---
db.exec(`
CREATE TABLE IF NOT EXISTS quality_gates (
id TEXT PRIMARY KEY,
pipeline_id TEXT NOT NULL,
gate_name TEXT NOT NULL,
criteria TEXT NOT NULL,
after_order INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'pending',
evaluator_id TEXT,
result TEXT,
evaluated_at INTEGER,
created_at INTEGER NOT NULL,
UNIQUE(pipeline_id, gate_name)
);
CREATE INDEX IF NOT EXISTS idx_qg_pipeline ON quality_gates(pipeline_id, status);
CREATE INDEX IF NOT EXISTS idx_qg_after_order ON quality_gates(pipeline_id, after_order);
`);
// ═══════════════════════════════════════════════════════════════
// Phase 5a — Security 增强(RBAC 细化 + Audit 防篡改)
// ═══════════════════════════════════════════════════════════════
// --- Phase 5a: agents 表扩展列(group_admin 支持) ---
try {
const agentCols = db.pragma("table_info(agents)");
if (agentCols.length > 0) {
const colNames = agentCols.map((c) => c.name);
const colMigrations = [
["managed_group_id", "ALTER TABLE agents ADD COLUMN managed_group_id TEXT"],
];
for (const [col, sql] of colMigrations) {
if (!colNames.includes(col)) {
db.exec(sql);
logger.info("db_migration", { module: "db", column: col, table: "agents", phase: "5a" });
}
}
}
}
catch (e) {
logger.warn("db_migration_warning", { module: "db", table: "agents", phase: "5a", error: e.message });
}
// --- Phase 5a: audit_log 哈希链列 + 写保护触发器 ---
try {
const auditCols = db.pragma("table_info(audit_log)");
if (auditCols.length > 0) {
const colNames = auditCols.map((c) => c.name);
const colMigrations = [
["prev_hash", "ALTER TABLE audit_log ADD COLUMN prev_hash TEXT"],
["record_hash", "ALTER TABLE audit_log ADD COLUMN record_hash TEXT"],
];
for (const [col, sql] of colMigrations) {
if (!colNames.includes(col)) {
db.exec(sql);
logger.info("db_migration", { module: "db", column: col, table: "audit_log", phase: "5a" });
}
}
}
// 写保护触发器(INSERT ONLY
db.exec(`
CREATE TRIGGER IF NOT EXISTS audit_log_no_modify BEFORE UPDATE ON audit_log
BEGIN SELECT RAISE(ABORT, 'audit log is immutable'); END;
`);
db.exec(`
CREATE TRIGGER IF NOT EXISTS audit_log_no_delete BEFORE DELETE ON audit_log
BEGIN SELECT RAISE(ABORT, 'audit log is immutable'); END;
`);
logger.info("db_migration", { module: "db", detail: "audit_log write protection triggers ready", phase: "5a" });
}
catch (e) {
logger.warn("db_migration_warning", { module: "db", table: "audit_log", phase: "5a", error: e.message });
}
// ═══════════════════════════════════════════════════════════════
// Phase 4a — Task Orchestrator(建表已在文件开头执行)
// ═══════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════════
// v2.3 Phase 1.1 — 文件附件表
// ═══════════════════════════════════════════════════════════════
db.exec(`
CREATE TABLE IF NOT EXISTS attachments (
id TEXT PRIMARY KEY,
message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
filename TEXT NOT NULL,
mime_type TEXT NOT NULL DEFAULT 'application/octet-stream',
file_size INTEGER NOT NULL,
storage_path TEXT NOT NULL,
uploaded_by TEXT NOT NULL,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_attachments_message ON attachments(message_id);
CREATE INDEX IF NOT EXISTS idx_attachments_uploader ON attachments(uploaded_by);
`);
export const attachStmt = {
insert: db.prepare(`INSERT INTO attachments VALUES (@id,@message_id,@filename,@mime_type,@file_size,@storage_path,@uploaded_by,@created_at)`),
getById: db.prepare(`SELECT * FROM attachments WHERE id=?`),
listByMessage: db.prepare(`SELECT id,filename,mime_type,file_size,uploaded_by,created_at FROM attachments WHERE message_id=? ORDER BY created_at ASC`),
deleteById: db.prepare(`DELETE FROM attachments WHERE id=?`),
};
// ═══════════════════════════════════════════════════════════════
// v2.3 Phase 3.2: 数据库归档表(messages + audit_log
// ═══════════════════════════════════════════════════════════════
db.exec(`
CREATE TABLE IF NOT EXISTS messages_archive (
id TEXT PRIMARY KEY,
from_agent TEXT NOT NULL,
to_agent TEXT NOT NULL,
content TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'message',
metadata TEXT,
status TEXT NOT NULL DEFAULT 'unread',
created_at INTEGER NOT NULL,
archived_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)
);
CREATE INDEX IF NOT EXISTS idx_messages_archive_created ON messages_archive(created_at);
CREATE INDEX IF NOT EXISTS idx_messages_archive_to_agent ON messages_archive(to_agent);
`);
db.exec(`
CREATE TABLE IF NOT EXISTS audit_log_archive (
id TEXT PRIMARY KEY,
action TEXT NOT NULL,
agent_id TEXT,
target TEXT,
details TEXT,
ip_address TEXT,
created_at INTEGER NOT NULL,
prev_hash TEXT,
record_hash TEXT,
archived_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)
);
CREATE INDEX IF NOT EXISTS idx_audit_archive_timestamp ON audit_log_archive(created_at);
CREATE INDEX IF NOT EXISTS idx_audit_archive_agent ON audit_log_archive(agent_id);
`);
// ─── DB 统计信息(调试用) ────────────────────────────────
export function getDbStats() {
const tables = [
"messages", "tasks", "consumed_log",
"agents", "auth_tokens", "dedup_cache",
"memories", "agent_capabilities", "audit_log",
"strategies", "strategy_feedback", "strategy_applications",
"pipelines", "pipeline_tasks",
"task_dependencies", "quality_gates",
"attachments",
"messages_archive", "audit_log_archive",
];
const stats = {};
for (const t of tables) {
try {
const row = db.prepare(`SELECT COUNT(*) as cnt FROM ${t}`).get();
stats[t] = row?.cnt ?? 0;
}
catch {
stats[t] = -1; // 表不存在
}
}
return stats;
}
// ─── Phase 3.2: 归档方法 ──────────────────────────────────
/**
* 归档 N 天前的消息(从 messages 移到 messages_archive
* @returns 归档的记录数
*/
export function archiveOldMessages(days = 30) {
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
// 插入到归档表
const insertSql = `
INSERT OR IGNORE INTO messages_archive (id, from_agent, to_agent, content, type, metadata, status, created_at)
SELECT id, from_agent, to_agent, content, type, metadata, status, created_at
FROM messages WHERE created_at < ? AND id NOT IN (SELECT id FROM messages_archive)
`;
const insertResult = db.prepare(insertSql).run(cutoff);
// 删除已归档的原始记录
db.prepare(`DELETE FROM messages WHERE created_at < ? AND id IN (SELECT id FROM messages_archive)`).run(cutoff);
return insertResult.changes;
}
/**
* 归档 N 天前的审计日志(从 audit_log 移到 audit_log_archive
* @returns 归档的记录数
*/
export function archiveOldAuditLogs(days = 90) {
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
// 插入到归档表
const insertSql = `
INSERT OR IGNORE INTO audit_log_archive (id, action, agent_id, target, details, ip_address, created_at, prev_hash, record_hash)
SELECT id, action, agent_id, target, details, ip_address, created_at, prev_hash, record_hash
FROM audit_log WHERE created_at < ? AND id NOT IN (SELECT id FROM audit_log_archive)
`;
const insertResult = db.prepare(insertSql).run(cutoff);
// 删除已归档的原始记录
// audit_log 有 BEFORE DELETE 触发器保护,需临时删除触发器再执行删除
db.exec(`DROP TRIGGER IF EXISTS audit_log_no_delete`);
db.exec(`DELETE FROM audit_log WHERE created_at < ? AND id IN (SELECT id FROM audit_log_archive)`);
db.exec(`
CREATE TRIGGER IF NOT EXISTS audit_log_no_delete BEFORE DELETE ON audit_log
BEGIN SELECT RAISE(ABORT, 'audit log is immutable'); END;
`);
return insertResult.changes;
}
/**
* 执行数据库 VACUUM(释放空闲页面,紧缩数据库文件)
* 建议在低峰期调用(如凌晨 3-5 点)
*/
export function vacuumDatabase() {
// 先执行 WAL 检查点(TRUNCATE 模式释放 WAL 文件空间)
db.pragma(`wal_checkpoint(TRUNCATE)`);
// 执行 VACUUM
db.exec(`VACUUM`);
logger.info("db_vacuum_executed", { module: "db" });
}
/**
* 获取数据库文件大小(字节)
*/
export function getDbSize() {
const fs = require("fs");
try {
const stats = fs.statSync(DB_PATH);
return stats.size;
}
catch {
return 0;
}
}
/**
* 获取增强版数据库统计信息(用于 MCP 工具 get_db_stats
*/
export function getEnhancedDbStats() {
const tableCounts = getDbStats();
const dbSize = getDbSize();
// WAL 大小
let walSize = 0;
const walPath = DB_PATH + "-wal";
try {
const fs = require("fs");
if (fs.existsSync(walPath)) {
walSize = fs.statSync(walPath).size;
}
}
catch { /* ignore */ }
// 最后归档时间
let lastMsgArchive = null;
let lastAuditArchive = null;
try {
const msgRow = db.prepare(`SELECT MAX(archived_at) as ts FROM messages_archive`).get();
lastMsgArchive = msgRow?.ts ? new Date(msgRow.ts).toISOString() : null;
}
catch { /* ignore */ }
try {
const auditRow = db.prepare(`SELECT MAX(archived_at) as ts FROM audit_log_archive`).get();
lastAuditArchive = auditRow?.ts ? new Date(auditRow.ts).toISOString() : null;
}
catch { /* ignore */ }
return {
table_counts: tableCounts,
database_size_bytes: dbSize,
database_size_mb: Math.round((dbSize / 1024 / 1024) * 100) / 100,
wal_size_bytes: walSize,
last_messages_archive: lastMsgArchive,
last_audit_log_archive: lastAuditArchive,
};
}
// ═══════════════════════════════════════════════════════════════
// Phase 6 — 定时清理过期数据
// ═══════════════════════════════════════════════════════════════
let cleanupTimer = null;
/**
* 定时清理过期数据(每小时执行一次)
* - 过期的 API Tokentoken_type='api_token'
* - 过期的去重缓存(超过 dedupTTL 秒)
* - 过期的消费日志(>1天)
*/
export function scheduleCleanup(dedupTTL) {
if (cleanupTimer) {
clearInterval(cleanupTimer);
}
cleanupTimer = setInterval(() => {
try {
const expiredTokens = db.prepare("DELETE FROM auth_tokens WHERE expires_at IS NOT NULL AND expires_at < (strftime('%s', 'now') * 1000) AND token_type = 'api_token'").run();
const cutoff = Date.now() - dedupTTL;
const expiredDedup = db.prepare("DELETE FROM dedup_cache WHERE created_at < ?").run(cutoff);
const expiredConsumed = db.prepare("DELETE FROM consumed_log WHERE consumed_at < (strftime('%s', 'now') * 1000 - 86400000)").run();
logger.info("scheduled_cleanup", {
module: "db",
expired_tokens: expiredTokens.changes,
expired_dedup: expiredDedup.changes,
expired_consumed: expiredConsumed.changes,
});
}
catch (err) {
logError("scheduled_cleanup_error", err, { module: "db" });
}
}, 3600 * 1000);
logger.info("cleanup_scheduler_started", {
module: "db",
interval_ms: 3600 * 1000,
dedup_ttl_ms: dedupTTL,
});
}
export function stopCleanup() {
if (cleanupTimer) {
clearInterval(cleanupTimer);
cleanupTimer = null;
logger.info("cleanup_scheduler_stopped", { module: "db" });
}
}
//# sourceMappingURL=db.js.map
+981
View File
@@ -0,0 +1,981 @@
/**
* db.ts — SQLite 持久化层
* 消息 + 任务 两张表,进程重启数据不丢失
*/
import Database, { type Database as DatabaseType, type Statement } from "better-sqlite3";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
import { logger, logError } from "./logger.js";
import type { PragmaColumnInfo, CountRow, MaxTimestampRow } from "./types.js";
import { getErrorMessage } from "./types.js";
const __dir = dirname(fileURLToPath(import.meta.url));
const DB_PATH = join(__dir, "../comm_hub.db");
export const db: DatabaseType = new Database(DB_PATH);
// 开启 WAL 模式,提升并发读写性能
db.pragma("journal_mode = WAL");
db.pragma("synchronous = NORMAL");
// ─── 建表 ──────────────────────────────────────────────
db.exec(`
CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY,
from_agent TEXT NOT NULL,
to_agent TEXT NOT NULL,
content TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'message',
metadata TEXT,
status TEXT NOT NULL DEFAULT 'unread',
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY,
assigned_by TEXT NOT NULL,
assigned_to TEXT NOT NULL DEFAULT '',
description TEXT NOT NULL,
context TEXT,
priority TEXT NOT NULL DEFAULT 'normal',
status TEXT NOT NULL DEFAULT 'inbox',
result TEXT,
progress INTEGER DEFAULT 0,
pipeline_id TEXT,
order_index INTEGER DEFAULT 0,
required_capability TEXT,
due_at INTEGER,
assigned_at INTEGER,
completed_at INTEGER,
tags TEXT DEFAULT '[]',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
-- 消费水位线表:记录 Agent 已处理过的文件路径,防止重复消费
CREATE TABLE IF NOT EXISTS consumed_log (
id TEXT PRIMARY KEY,
agent_id TEXT NOT NULL,
resource TEXT NOT NULL,
resource_type TEXT NOT NULL DEFAULT 'file', -- 'file' | 'signal' | 'message'
action TEXT NOT NULL,
notes TEXT,
consumed_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_messages_to_agent ON messages(to_agent, status);
CREATE INDEX IF NOT EXISTS idx_tasks_assigned_to ON tasks(assigned_to, status);
CREATE INDEX IF NOT EXISTS idx_consumed_log ON consumed_log(agent_id, resource);
`);
// ─── Phase 4a Migration: tasks 表新增字段 ──────────────
// 必须在 taskStmt.insert 之前执行
try {
const taskCols = db.pragma("table_info(tasks)") as PragmaColumnInfo[];
if (taskCols.length > 0) {
const colNames = taskCols.map((c) => c.name);
const migrations: [string, string][] = [
["pipeline_id", "ALTER TABLE tasks ADD COLUMN pipeline_id TEXT"],
["order_index", "ALTER TABLE tasks ADD COLUMN order_index INTEGER DEFAULT 0"],
["required_capability", "ALTER TABLE tasks ADD COLUMN required_capability TEXT"],
["due_at", "ALTER TABLE tasks ADD COLUMN due_at INTEGER"],
["assigned_at", "ALTER TABLE tasks ADD COLUMN assigned_at INTEGER"],
["completed_at", "ALTER TABLE tasks ADD COLUMN completed_at INTEGER"],
["tags", "ALTER TABLE tasks ADD COLUMN tags TEXT DEFAULT '[]'"],
];
for (const [col, sql] of migrations) {
if (!colNames.includes(col)) {
db.exec(sql);
logger.info("db_migration", { module: "db", column: col, table: "tasks" });
}
}
}
} catch (e: unknown) {
logger.warn("db_migration_warning", { module: "db", table: "tasks", phase: "4a", error: getErrorMessage(e) });
}
// ─── Phase 4a 新表:pipelines + pipeline_tasks ──────────
// 必须在 pipelineStmt 和 taskStmt.listByPipeline 之前创建
db.exec(`
CREATE TABLE IF NOT EXISTS pipelines (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
status TEXT NOT NULL DEFAULT 'draft',
creator TEXT NOT NULL,
config TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_pipelines_creator ON pipelines(creator);
CREATE INDEX IF NOT EXISTS idx_pipelines_status ON pipelines(status);
`);
db.exec(`
CREATE TABLE IF NOT EXISTS pipeline_tasks (
id TEXT PRIMARY KEY,
pipeline_id TEXT NOT NULL REFERENCES pipelines(id),
task_id TEXT NOT NULL REFERENCES tasks(id),
order_index INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
UNIQUE(pipeline_id, task_id)
);
CREATE INDEX IF NOT EXISTS idx_pipeline_tasks_pipe ON pipeline_tasks(pipeline_id);
CREATE INDEX IF NOT EXISTS idx_pipeline_tasks_order ON pipeline_tasks(pipeline_id, order_index);
`);
// ─── Message 操作 ──────────────────────────────────────
export interface Message {
id: string;
from_agent: string;
to_agent: string;
content: string;
type: "message" | "task_assign" | "task_update" | "ack";
metadata?: string | null;
status: "unread" | "delivered" | "read" | "acknowledged";
created_at: number;
}
export const msgStmt: Record<string, Statement> = {
insert: db.prepare<Message>(
`INSERT INTO messages VALUES (@id,@from_agent,@to_agent,@content,@type,@metadata,@status,@created_at)`
),
markDelivered: db.prepare(
`UPDATE messages SET status='delivered' WHERE id=?`
),
markRead: db.prepare(
`UPDATE messages SET status='read' WHERE id=?`
),
markAcknowledged: db.prepare(
`UPDATE messages SET status='acknowledged' WHERE id=?`
),
pendingFor: db.prepare<string>(
`SELECT * FROM messages WHERE to_agent=? AND status='unread' ORDER BY created_at ASC`
),
markAllDelivered: db.prepare(
`UPDATE messages SET status='delivered' WHERE to_agent=? AND status='unread'`
),
getById: db.prepare(
`SELECT * FROM messages WHERE id=?`
),
};
// ─── ConsumedLog 操作 ───────────────────────────────────
export interface ConsumedEntry {
id: string;
agent_id: string;
resource: string; // 文件路径或 signal_id
resource_type: string; // 'file' | 'signal' | 'message'
action: string;
notes?: string | null;
consumed_at: number;
}
export const consumedStmt: Record<string, Statement> = {
insert: db.prepare<ConsumedEntry>(
`INSERT OR REPLACE INTO consumed_log VALUES (@id,@agent_id,@resource,@resource_type,@action,@notes,@consumed_at)`
),
check: db.prepare(
`SELECT * FROM consumed_log WHERE agent_id=? AND resource=?`
),
listByAgent: db.prepare(
`SELECT * FROM consumed_log WHERE agent_id=? ORDER BY consumed_at DESC LIMIT ?`
),
};
// ─── Task 操作 ─────────────────────────────────────────
export interface Task {
id: string;
assigned_by: string;
assigned_to: string;
description: string;
context?: string | null;
priority: "low" | "normal" | "high" | "urgent";
status: "inbox" | "assigned" | "waiting" | "pending" | "in_progress" | "completed" | "failed" | "cancelled";
result?: string | null;
progress: number;
pipeline_id?: string | null;
order_index: number;
required_capability?: string | null;
due_at?: number | null;
assigned_at?: number | null;
completed_at?: number | null;
tags?: string | null;
parallel_group?: string | null; // Phase 4b
handoff_status?: string | null; // Phase 4b
handoff_to?: string | null; // Phase 4b Day 3: 交接目标 Agent
created_at: number;
updated_at: number;
}
export const taskStmt: Record<string, Statement> = {
insert: db.prepare<Task>(
`INSERT INTO tasks (id, assigned_by, assigned_to, description, context, priority, status, result, progress, pipeline_id, order_index, required_capability, due_at, assigned_at, completed_at, tags, created_at, updated_at)
VALUES (@id, @assigned_by, @assigned_to, @description, @context, @priority, @status, @result, @progress, @pipeline_id, @order_index, @required_capability, @due_at, @assigned_at, @completed_at, @tags, @created_at, @updated_at)`
),
getById: db.prepare<string>(
`SELECT * FROM tasks WHERE id=?`
),
update: db.prepare(
`UPDATE tasks SET status=?,result=?,progress=?,updated_at=? WHERE id=?`
),
updateAssignee: db.prepare(
`UPDATE tasks SET assigned_to=?,assigned_at=?,status='assigned',updated_at=? WHERE id=?`
),
listFor: db.prepare<[string, string]>(
`SELECT * FROM tasks WHERE assigned_to=? AND status=? ORDER BY created_at DESC`
),
listByPipeline: db.prepare<string>(
`SELECT t.* FROM tasks t JOIN pipeline_tasks pt ON t.id=pt.task_id WHERE pt.pipeline_id=? ORDER BY pt.order_index ASC`
),
};
// ─── Pipeline 操作 ─────────────────────────────────────────
export interface Pipeline {
id: string;
name: string;
description?: string | null;
status: "draft" | "active" | "completed" | "cancelled";
creator: string;
config?: string | null;
created_at: number;
updated_at: number;
}
export interface PipelineTask {
id: string;
pipeline_id: string;
task_id: string;
order_index: number;
created_at: number;
}
export const pipelineStmt: Record<string, Statement> = {
insert: db.prepare<Pipeline>(
`INSERT INTO pipelines VALUES (@id,@name,@description,@status,@creator,@config,@created_at,@updated_at)`
),
getById: db.prepare<string>(
`SELECT * FROM pipelines WHERE id=?`
),
updateStatus: db.prepare(
`UPDATE pipelines SET status=?,updated_at=? WHERE id=?`
),
listByCreator: db.prepare<string>(
`SELECT * FROM pipelines WHERE creator=? ORDER BY created_at DESC`
),
};
export const pipelineTaskStmt: Record<string, Statement> = {
insert: db.prepare<PipelineTask>(
`INSERT OR REPLACE INTO pipeline_tasks VALUES (@id,@pipeline_id,@task_id,@order_index,@created_at)`
),
listByPipeline: db.prepare<string>(
`SELECT * FROM pipeline_tasks WHERE pipeline_id=? ORDER BY order_index ASC`
),
deleteByTask: db.prepare(
`DELETE FROM pipeline_tasks WHERE task_id=?`
),
};
// ═══════════════════════════════════════════════════════════════
// Phase 1 — Security + Identity + Dedup + Memory 表
// ═══════════════════════════════════════════════════════════════
// --- agents 表:Agent 注册与在线状态 ---
// Phase 2 Day 4 Migration: 先添加 trust_score 列(必须在建索引前)
try {
const agentCols = db.pragma("table_info(agents)") as PragmaColumnInfo[];
if (agentCols.length > 0 && !agentCols.some((c) => c.name === "trust_score")) {
db.exec(`ALTER TABLE agents ADD COLUMN trust_score INTEGER NOT NULL DEFAULT 50`);
logger.info("db_migration", { module: "db", column: "trust_score", table: "agents" });
}
} catch (e: unknown) {
logger.warn("db_migration_warning", { module: "db", column: "trust_score", table: "agents", error: getErrorMessage(e) });
}
db.exec(`
CREATE TABLE IF NOT EXISTS agents (
agent_id TEXT PRIMARY KEY,
name TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'member', -- 'admin' | 'member'
api_token TEXT, -- SHA-256 hash
status TEXT NOT NULL DEFAULT 'offline', -- 'online' | 'offline'
trust_score INTEGER NOT NULL DEFAULT 50, -- Phase 2 Day 4: 信任分 0-100
last_heartbeat INTEGER,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status);
CREATE INDEX IF NOT EXISTS idx_agents_heartbeat ON agents(last_heartbeat);
CREATE INDEX IF NOT EXISTS idx_agents_trust ON agents(trust_score);
`);
// Phase 2 Day 4 Migration: 为已有 agents 表添加 trust_score 字段
try {
const agentCols = db.pragma("table_info(agents)") as PragmaColumnInfo[];
if (!agentCols.some((c) => c.name === "trust_score")) {
db.exec(`ALTER TABLE agents ADD COLUMN trust_score INTEGER NOT NULL DEFAULT 50`);
logger.info("db_migration", { module: "db", column: "trust_score", table: "agents", fallback: true });
}
} catch (e: unknown) {
logger.warn("db_migration_warning", { module: "db", column: "trust_score", table: "agents", error: getErrorMessage(e) });
}
// --- auth_tokens 表:邀请码 + API Token 管理 ---
db.exec(`
CREATE TABLE IF NOT EXISTS auth_tokens (
token_id TEXT PRIMARY KEY,
token_type TEXT NOT NULL, -- 'invite_code' | 'api_token'
token_value TEXT NOT NULL, -- SHA-256 hash
agent_id TEXT, -- api_token 关联的 agent
role TEXT, -- api_token 关联的角色
used INTEGER DEFAULT 0, -- 1 = 已使用
created_at INTEGER NOT NULL,
expires_at INTEGER,
revoked_at INTEGER,
UNIQUE(token_type, token_value)
);
CREATE INDEX IF NOT EXISTS idx_auth_tokens_type ON auth_tokens(token_type, used);
CREATE INDEX IF NOT EXISTS idx_auth_tokens_agent ON auth_tokens(agent_id);
`);
// --- dedup_cache 表:消息去重缓存 ---
db.exec(`
CREATE TABLE IF NOT EXISTS dedup_cache (
msg_hash TEXT PRIMARY KEY,
sender_id TEXT NOT NULL,
nonce INTEGER NOT NULL,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_dedup_sender_nonce ON dedup_cache(sender_id, nonce);
`);
// --- memories 表 + FTS5 全文索引(N-gram 中文分词) ---
// Phase 2 Day 4 Migration: 先添加溯源列(必须在建索引前)
try {
const memCols = db.pragma("table_info(memories)") as PragmaColumnInfo[];
if (memCols.length > 0) {
if (!memCols.some((c) => c.name === "source_agent_id")) {
db.exec(`ALTER TABLE memories ADD COLUMN source_agent_id TEXT`);
logger.info("db_migration", { module: "db", column: "source_agent_id", table: "memories" });
}
if (!memCols.some((c) => c.name === "source_task_id")) {
db.exec(`ALTER TABLE memories ADD COLUMN source_task_id TEXT`);
logger.info("db_migration", { module: "db", column: "source_task_id", table: "memories" });
}
}
} catch (e: unknown) {
logger.warn("db_migration_warning", { module: "db", table: "memories", error: getErrorMessage(e) });
}
db.exec(`
CREATE TABLE IF NOT EXISTS memories (
id TEXT PRIMARY KEY,
agent_id TEXT NOT NULL,
title TEXT,
content TEXT NOT NULL,
fts_tokens TEXT NOT NULL DEFAULT '', -- Phase 2: N-gram 预分词 tokens
scope TEXT NOT NULL DEFAULT 'private', -- 'private' | 'group' | 'collective'
tags TEXT, -- JSON array
source_agent_id TEXT, -- Phase 2 Day 4: 溯源 — 实际写入者
source_task_id TEXT, -- Phase 2 Day 4: 溯源 — 关联任务
created_at INTEGER NOT NULL,
updated_at INTEGER
);
CREATE INDEX IF NOT EXISTS idx_memories_agent ON memories(agent_id);
CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(scope);
CREATE INDEX IF NOT EXISTS idx_memories_source ON memories(source_agent_id);
`);
// Phase 2 Migration: 为已有 memories 表添加 fts_tokens 列
try {
const colInfo = db.pragma("table_info(memories)") as PragmaColumnInfo[];
const hasFtsTokens = colInfo.some((c) => c.name === "fts_tokens");
if (!hasFtsTokens) {
db.exec(`ALTER TABLE memories ADD COLUMN fts_tokens TEXT NOT NULL DEFAULT ''`);
logger.info("db_migration", { module: "db", column: "fts_tokens", table: "memories" });
}
} catch (e: unknown) {
logger.warn("db_migration_warning", { module: "db", column: "fts_tokens", table: "memories", error: getErrorMessage(e) });
}
// FTS5 虚拟表(独立存储模式 + fts_tokens 列)
try {
// Phase 2: 旧版 FTS5 可能已存在(external content 模式),需要重建
// 先尝试删除旧表(ignore error 如果不存在)
try {
db.exec(`DROP TRIGGER IF EXISTS memories_ai`);
db.exec(`DROP TRIGGER IF EXISTS memories_ad`);
db.exec(`DROP TRIGGER IF EXISTS memories_au`);
db.exec(`DROP TABLE IF EXISTS memories_fts`);
} catch {
// ignore
}
// 新版 FTS5:独立存储 fts_tokens 列
db.exec(`
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
title,
content,
tags,
fts_tokens
);
`);
} catch (e: unknown) {
if (!getErrorMessage(e).includes("already exists")) {
logger.warn("db_fts5_init_warning", { module: "db", error: getErrorMessage(e) });
}
}
// --- agents_capabilities 表 ---
db.exec(`
CREATE TABLE IF NOT EXISTS agent_capabilities (
id TEXT PRIMARY KEY,
agent_id TEXT NOT NULL,
capability TEXT NOT NULL,
params TEXT, -- JSON
verified INTEGER DEFAULT 0,
verified_at INTEGER,
created_at INTEGER NOT NULL,
FOREIGN KEY (agent_id) REFERENCES agents(agent_id)
);
CREATE INDEX IF NOT EXISTS idx_capabilities_agent ON agent_capabilities(agent_id);
`);
// --- audit_log 表 ---
db.exec(`
CREATE TABLE IF NOT EXISTS audit_log (
id TEXT PRIMARY KEY,
action TEXT NOT NULL,
agent_id TEXT,
target TEXT,
details TEXT,
ip_address TEXT,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_audit_log_time ON audit_log(created_at);
`);
// ═══════════════════════════════════════════════════════════════
// Phase 3 — Evolution Engine 表
// ═══════════════════════════════════════════════════════════════
// strategies 表
db.exec(`
CREATE TABLE IF NOT EXISTS strategies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
category TEXT NOT NULL DEFAULT 'workflow',
sensitivity TEXT NOT NULL DEFAULT 'normal',
proposer_id TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
approve_reason TEXT,
approved_by TEXT,
approved_at INTEGER,
proposed_at INTEGER NOT NULL,
task_id TEXT,
source_trust INTEGER NOT NULL DEFAULT 50,
apply_count INTEGER NOT NULL DEFAULT 0,
feedback_count INTEGER NOT NULL DEFAULT 0,
positive_count INTEGER NOT NULL DEFAULT 0,
UNIQUE(title, proposer_id, proposed_at)
);
CREATE INDEX IF NOT EXISTS idx_strategies_status ON strategies(status);
CREATE INDEX IF NOT EXISTS idx_strategies_proposer ON strategies(proposer_id);
CREATE INDEX IF NOT EXISTS idx_strategies_category ON strategies(category);
`);
// strategy_feedback 表(UNIQUE 防刷)
db.exec(`
CREATE TABLE IF NOT EXISTS strategy_feedback (
id INTEGER PRIMARY KEY AUTOINCREMENT,
strategy_id INTEGER NOT NULL REFERENCES strategies(id) ON DELETE CASCADE,
agent_id TEXT NOT NULL,
feedback TEXT NOT NULL,
comment TEXT,
applied INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
UNIQUE(strategy_id, agent_id)
);
CREATE INDEX IF NOT EXISTS idx_feedback_strategy ON strategy_feedback(strategy_id);
CREATE INDEX IF NOT EXISTS idx_feedback_agent ON strategy_feedback(agent_id);
`);
// strategy_applications 表(采纳记录)
db.exec(`
CREATE TABLE IF NOT EXISTS strategy_applications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
strategy_id INTEGER NOT NULL REFERENCES strategies(id) ON DELETE CASCADE,
agent_id TEXT NOT NULL,
context TEXT,
result TEXT,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_applications_strategy ON strategy_applications(strategy_id);
CREATE INDEX IF NOT EXISTS idx_applications_agent ON strategy_applications(agent_id);
`);
// FTS5 全文索引(N-gram 中文分词,与 memories 一致)
try {
try {
db.exec(`DROP TRIGGER IF EXISTS strategies_ai`);
db.exec(`DROP TRIGGER IF EXISTS strategies_ad`);
db.exec(`DROP TRIGGER IF EXISTS strategies_au`);
db.exec(`DROP TABLE IF EXISTS strategies_fts`);
} catch {
// ignore
}
db.exec(`
CREATE VIRTUAL TABLE IF NOT EXISTS strategies_fts USING fts5(
title, content, category
);
`);
} catch (e: unknown) {
if (!getErrorMessage(e).includes("already exists")) {
logger.warn("db_strategies_fts5_init_warning", { module: "db", error: getErrorMessage(e) });
}
}
// ═══════════════════════════════════════════════════════════════
// Phase 4b — Task Orchestrator 进阶(依赖链 + 质量门 + 交接 + 分级审批)
// ═══════════════════════════════════════════════════════════════
// --- Phase 4b: tasks 表扩展列 ---
try {
const taskCols = db.pragma("table_info(tasks)") as PragmaColumnInfo[];
if (taskCols.length > 0) {
const colNames = taskCols.map((c) => c.name);
const colMigrations: [string, string][] = [
["parallel_group", "ALTER TABLE tasks ADD COLUMN parallel_group TEXT DEFAULT NULL"],
["handoff_status", "ALTER TABLE tasks ADD COLUMN handoff_status TEXT DEFAULT 'none'"],
// Phase 4b Day 3: 交接协议目标 Agent
["handoff_to", "ALTER TABLE tasks ADD COLUMN handoff_to TEXT DEFAULT NULL"],
];
for (const [col, sql] of colMigrations) {
if (!colNames.includes(col)) {
db.exec(sql);
logger.info("db_migration", { module: "db", column: col, table: "tasks", phase: "4b" });
}
}
}
} catch (e: unknown) {
logger.warn("db_migration_warning", { module: "db", table: "tasks", phase: "4b", error: getErrorMessage(e) });
}
// --- Phase 4b: strategies 表扩展列 ---
try {
const stratCols = db.pragma("table_info(strategies)") as PragmaColumnInfo[];
if (stratCols.length > 0) {
const colNames = stratCols.map((c) => c.name);
const colMigrations: [string, string][] = [
["approval_tier", "ALTER TABLE strategies ADD COLUMN approval_tier TEXT DEFAULT 'admin'"],
["observation_start", "ALTER TABLE strategies ADD COLUMN observation_start INTEGER"],
["veto_deadline", "ALTER TABLE strategies ADD COLUMN veto_deadline INTEGER"],
];
for (const [col, sql] of colMigrations) {
if (!colNames.includes(col)) {
db.exec(sql);
logger.info("db_migration", { module: "db", column: col, table: "strategies", phase: "4b" });
}
}
}
} catch (e: unknown) {
logger.warn("db_migration_warning", { module: "db", table: "strategies", phase: "4b", error: getErrorMessage(e) });
}
// --- Phase 4b: task_dependencies 表(依赖链) ---
db.exec(`
CREATE TABLE IF NOT EXISTS task_dependencies (
id TEXT PRIMARY KEY,
upstream_id TEXT NOT NULL,
downstream_id TEXT NOT NULL,
dep_type TEXT NOT NULL DEFAULT 'finish_to_start',
status TEXT NOT NULL DEFAULT 'pending',
created_at INTEGER NOT NULL,
UNIQUE(upstream_id, downstream_id)
);
CREATE INDEX IF NOT EXISTS idx_deps_downstream ON task_dependencies(downstream_id, status);
CREATE INDEX IF NOT EXISTS idx_deps_upstream ON task_dependencies(upstream_id, status);
`);
// --- Phase 4b: quality_gates 表(质量门) ---
db.exec(`
CREATE TABLE IF NOT EXISTS quality_gates (
id TEXT PRIMARY KEY,
pipeline_id TEXT NOT NULL,
gate_name TEXT NOT NULL,
criteria TEXT NOT NULL,
after_order INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'pending',
evaluator_id TEXT,
result TEXT,
evaluated_at INTEGER,
created_at INTEGER NOT NULL,
UNIQUE(pipeline_id, gate_name)
);
CREATE INDEX IF NOT EXISTS idx_qg_pipeline ON quality_gates(pipeline_id, status);
CREATE INDEX IF NOT EXISTS idx_qg_after_order ON quality_gates(pipeline_id, after_order);
`);
// ═══════════════════════════════════════════════════════════════
// Phase 5a — Security 增强(RBAC 细化 + Audit 防篡改)
// ═══════════════════════════════════════════════════════════════
// --- Phase 5a: agents 表扩展列(group_admin 支持) ---
try {
const agentCols = db.pragma("table_info(agents)") as PragmaColumnInfo[];
if (agentCols.length > 0) {
const colNames = agentCols.map((c) => c.name);
const colMigrations: [string, string][] = [
["managed_group_id", "ALTER TABLE agents ADD COLUMN managed_group_id TEXT"],
];
for (const [col, sql] of colMigrations) {
if (!colNames.includes(col)) {
db.exec(sql);
logger.info("db_migration", { module: "db", column: col, table: "agents", phase: "5a" });
}
}
}
} catch (e: unknown) {
logger.warn("db_migration_warning", { module: "db", table: "agents", phase: "5a", error: getErrorMessage(e) });
}
// --- Phase 5a: audit_log 哈希链列 + 写保护触发器 ---
try {
const auditCols = db.pragma("table_info(audit_log)") as PragmaColumnInfo[];
if (auditCols.length > 0) {
const colNames = auditCols.map((c) => c.name);
const colMigrations: [string, string][] = [
["prev_hash", "ALTER TABLE audit_log ADD COLUMN prev_hash TEXT"],
["record_hash", "ALTER TABLE audit_log ADD COLUMN record_hash TEXT"],
];
for (const [col, sql] of colMigrations) {
if (!colNames.includes(col)) {
db.exec(sql);
logger.info("db_migration", { module: "db", column: col, table: "audit_log", phase: "5a" });
}
}
}
// 写保护触发器(INSERT ONLY
db.exec(`
CREATE TRIGGER IF NOT EXISTS audit_log_no_modify BEFORE UPDATE ON audit_log
BEGIN SELECT RAISE(ABORT, 'audit log is immutable'); END;
`);
db.exec(`
CREATE TRIGGER IF NOT EXISTS audit_log_no_delete BEFORE DELETE ON audit_log
BEGIN SELECT RAISE(ABORT, 'audit log is immutable'); END;
`);
logger.info("db_migration", { module: "db", detail: "audit_log write protection triggers ready", phase: "5a" });
} catch (e: unknown) {
logger.warn("db_migration_warning", { module: "db", table: "audit_log", phase: "5a", error: getErrorMessage(e) });
}
// ═══════════════════════════════════════════════════════════════
// Phase 4a — Task Orchestrator(建表已在文件开头执行)
// ═══════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════════
// v2.3 Phase 1.1 — 文件附件表
// ═══════════════════════════════════════════════════════════════
db.exec(`
CREATE TABLE IF NOT EXISTS attachments (
id TEXT PRIMARY KEY,
message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
filename TEXT NOT NULL,
mime_type TEXT NOT NULL DEFAULT 'application/octet-stream',
file_size INTEGER NOT NULL,
storage_path TEXT NOT NULL,
uploaded_by TEXT NOT NULL,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_attachments_message ON attachments(message_id);
CREATE INDEX IF NOT EXISTS idx_attachments_uploader ON attachments(uploaded_by);
`);
// ─── Attachment 操作 ─────────────────────────────────────
export interface Attachment {
id: string;
message_id: string;
filename: string;
mime_type: string;
file_size: number;
storage_path: string;
uploaded_by: string;
created_at: number;
}
export const attachStmt: Record<string, Statement> = {
insert: db.prepare<Attachment>(
`INSERT INTO attachments VALUES (@id,@message_id,@filename,@mime_type,@file_size,@storage_path,@uploaded_by,@created_at)`
),
getById: db.prepare<string>(
`SELECT * FROM attachments WHERE id=?`
),
listByMessage: db.prepare<string>(
`SELECT id,filename,mime_type,file_size,uploaded_by,created_at FROM attachments WHERE message_id=? ORDER BY created_at ASC`
),
deleteById: db.prepare<string>(
`DELETE FROM attachments WHERE id=?`
),
};
// ═══════════════════════════════════════════════════════════════
// v2.3 Phase 3.2: 数据库归档表(messages + audit_log
// ═══════════════════════════════════════════════════════════════
db.exec(`
CREATE TABLE IF NOT EXISTS messages_archive (
id TEXT PRIMARY KEY,
from_agent TEXT NOT NULL,
to_agent TEXT NOT NULL,
content TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'message',
metadata TEXT,
status TEXT NOT NULL DEFAULT 'unread',
created_at INTEGER NOT NULL,
archived_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)
);
CREATE INDEX IF NOT EXISTS idx_messages_archive_created ON messages_archive(created_at);
CREATE INDEX IF NOT EXISTS idx_messages_archive_to_agent ON messages_archive(to_agent);
`);
db.exec(`
CREATE TABLE IF NOT EXISTS audit_log_archive (
id TEXT PRIMARY KEY,
action TEXT NOT NULL,
agent_id TEXT,
target TEXT,
details TEXT,
ip_address TEXT,
created_at INTEGER NOT NULL,
prev_hash TEXT,
record_hash TEXT,
archived_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)
);
CREATE INDEX IF NOT EXISTS idx_audit_archive_timestamp ON audit_log_archive(created_at);
CREATE INDEX IF NOT EXISTS idx_audit_archive_agent ON audit_log_archive(agent_id);
`);
// ─── DB 统计信息(调试用) ────────────────────────────────
export function getDbStats(): Record<string, number> {
const tables = [
"messages", "tasks", "consumed_log",
"agents", "auth_tokens", "dedup_cache",
"memories", "agent_capabilities", "audit_log",
"strategies", "strategy_feedback", "strategy_applications",
"pipelines", "pipeline_tasks",
"task_dependencies", "quality_gates",
"attachments",
"messages_archive", "audit_log_archive",
];
const stats: Record<string, number> = {};
for (const t of tables) {
try {
const row = db.prepare(`SELECT COUNT(*) as cnt FROM ${t}`).get() as CountRow | undefined;
stats[t] = row?.cnt ?? 0;
} catch {
stats[t] = -1; // 表不存在
}
}
return stats;
}
// ─── Phase 3.2: 归档方法 ──────────────────────────────────
/**
* 归档 N 天前的消息(从 messages 移到 messages_archive
* @returns 归档的记录数
*/
export function archiveOldMessages(days: number = 30): number {
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
// 插入到归档表
const insertSql = `
INSERT OR IGNORE INTO messages_archive (id, from_agent, to_agent, content, type, metadata, status, created_at)
SELECT id, from_agent, to_agent, content, type, metadata, status, created_at
FROM messages WHERE created_at < ? AND id NOT IN (SELECT id FROM messages_archive)
`;
const insertResult = db.prepare(insertSql).run(cutoff);
// 删除已归档的原始记录
db.prepare(`DELETE FROM messages WHERE created_at < ? AND id IN (SELECT id FROM messages_archive)`).run(cutoff);
return insertResult.changes;
}
/**
* 归档 N 天前的审计日志(从 audit_log 移到 audit_log_archive
* @returns 归档的记录数
*/
export function archiveOldAuditLogs(days: number = 90): number {
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
// 插入到归档表
const insertSql = `
INSERT OR IGNORE INTO audit_log_archive (id, action, agent_id, target, details, ip_address, created_at, prev_hash, record_hash)
SELECT id, action, agent_id, target, details, ip_address, created_at, prev_hash, record_hash
FROM audit_log WHERE created_at < ? AND id NOT IN (SELECT id FROM audit_log_archive)
`;
const insertResult = db.prepare(insertSql).run(cutoff);
// 删除已归档的原始记录
// audit_log 有 BEFORE DELETE 触发器保护,需临时删除触发器再执行删除
db.exec(`DROP TRIGGER IF EXISTS audit_log_no_delete`);
db.exec(`DELETE FROM audit_log WHERE created_at < ? AND id IN (SELECT id FROM audit_log_archive)`);
db.exec(`
CREATE TRIGGER IF NOT EXISTS audit_log_no_delete BEFORE DELETE ON audit_log
BEGIN SELECT RAISE(ABORT, 'audit log is immutable'); END;
`);
return insertResult.changes;
}
/**
* 执行数据库 VACUUM(释放空闲页面,紧缩数据库文件)
* 建议在低峰期调用(如凌晨 3-5 点)
*/
export function vacuumDatabase(): void {
// 先执行 WAL 检查点(TRUNCATE 模式释放 WAL 文件空间)
db.pragma(`wal_checkpoint(TRUNCATE)`);
// 执行 VACUUM
db.exec(`VACUUM`);
logger.info("db_vacuum_executed", { module: "db" });
}
/**
* 获取数据库文件大小(字节)
*/
export function getDbSize(): number {
const fs = require("fs");
try {
const stats = fs.statSync(DB_PATH);
return stats.size;
} catch {
return 0;
}
}
/**
* 获取增强版数据库统计信息(用于 MCP 工具 get_db_stats
*/
export function getEnhancedDbStats(): {
table_counts: Record<string, number>;
database_size_bytes: number;
database_size_mb: number;
wal_size_bytes: number;
last_messages_archive: string | null;
last_audit_log_archive: string | null;
} {
const tableCounts = getDbStats();
const dbSize = getDbSize();
// WAL 大小
let walSize = 0;
const walPath = DB_PATH + "-wal";
try {
const fs = require("fs");
if (fs.existsSync(walPath)) {
walSize = fs.statSync(walPath).size;
}
} catch { /* ignore */ }
// 最后归档时间
let lastMsgArchive: string | null = null;
let lastAuditArchive: string | null = null;
try {
const msgRow = db.prepare(`SELECT MAX(archived_at) as ts FROM messages_archive`).get() as MaxTimestampRow | undefined;
lastMsgArchive = msgRow?.ts ? new Date(msgRow.ts).toISOString() : null;
} catch { /* ignore */ }
try {
const auditRow = db.prepare(`SELECT MAX(archived_at) as ts FROM audit_log_archive`).get() as MaxTimestampRow | undefined;
lastAuditArchive = auditRow?.ts ? new Date(auditRow.ts).toISOString() : null;
} catch { /* ignore */ }
return {
table_counts: tableCounts,
database_size_bytes: dbSize,
database_size_mb: Math.round((dbSize / 1024 / 1024) * 100) / 100,
wal_size_bytes: walSize,
last_messages_archive: lastMsgArchive,
last_audit_log_archive: lastAuditArchive,
};
}
// ═══════════════════════════════════════════════════════════════
// Phase 6 — 定时清理过期数据
// ═══════════════════════════════════════════════════════════════
let cleanupTimer: ReturnType<typeof setInterval> | null = null;
/**
* 定时清理过期数据(每小时执行一次)
* - 过期的 API Tokentoken_type='api_token'
* - 过期的去重缓存(超过 dedupTTL 秒)
* - 过期的消费日志(>1天)
*/
export function scheduleCleanup(dedupTTL: number): void {
if (cleanupTimer) {
clearInterval(cleanupTimer);
}
cleanupTimer = setInterval(() => {
try {
const expiredTokens = db.prepare(
"DELETE FROM auth_tokens WHERE expires_at IS NOT NULL AND expires_at < (strftime('%s', 'now') * 1000) AND token_type = 'api_token'"
).run();
const cutoff = Date.now() - dedupTTL;
const expiredDedup = db.prepare(
"DELETE FROM dedup_cache WHERE created_at < ?"
).run(cutoff);
const expiredConsumed = db.prepare(
"DELETE FROM consumed_log WHERE consumed_at < (strftime('%s', 'now') * 1000 - 86400000)"
).run();
logger.info("scheduled_cleanup", {
module: "db",
expired_tokens: expiredTokens.changes,
expired_dedup: expiredDedup.changes,
expired_consumed: expiredConsumed.changes,
});
} catch (err) {
logError("scheduled_cleanup_error", err, { module: "db" });
}
}, 3600 * 1000);
logger.info("cleanup_scheduler_started", {
module: "db",
interval_ms: 3600 * 1000,
dedup_ttl_ms: dedupTTL,
});
}
export function stopCleanup(): void {
if (cleanupTimer) {
clearInterval(cleanupTimer);
cleanupTimer = null;
logger.info("cleanup_scheduler_stopped", { module: "db" });
}
}
+82
View File
@@ -0,0 +1,82 @@
/**
* 获取 sender 的下一个 nonce(递增,持久化)
* @returns 递增后的 nonce 值
*/
export declare function nextNonce(senderId: string): number;
/**
* 获取 sender 的当前 nonce(不递增)
*/
export declare function currentNonce(senderId: string): number;
/**
* 重置 sender 的 nonce(测试用)
*/
export declare function resetNonce(senderId: string): void;
/**
* 计算去重哈希(不含 nonce)
* dedup_hash = sha256(sender + receiver + content)
* 用于检测完全相同的消息(防止重复发送)
*/
export declare function computeDedupHash(sender: string, receiver: string, content: string): string;
/**
* 计算消息完整性哈希(含 nonce)
* msg_hash = sha256(sender + receiver + content + nonce)
* 用于防篡改 + 客户端验证
*/
export declare function computeMsgHash(sender: string, receiver: string, content: string, nonce: number): string;
/**
* 检查消息是否重复(基于 msg_hash)
* @returns true = 重复(应拒绝),false = 新消息
*/
export declare function isDuplicate(msgHash: string): boolean;
/**
* 记录消息哈希到去重缓存
*/
export declare function recordHash(msgHash: string, senderId: string, nonce: number): void;
/**
* 消息体安全校验(防 prompt injection 和格式攻击)
*
* 检查项:
* 1. 内容非空
* 2. 长度限制(50KB
* 3. 不包含 NULL 字节(\x00 分界符保留)
* 4. 不包含 SSE 注入模式(data: / event: / id:
*
* @returns { safe: true } 或 { safe: false, reason: string }
*/
export declare function validateMessageBody(content: string): {
safe: boolean;
reason?: string;
};
/**
* 完整的消息去重流程
*
* 1. 校验消息体
* 2. 计算去重哈希(不含 nonce)并检查重复
* 3. 分配 nonce
* 4. 计算完整性哈希(含 nonce)
* 5. 记录去重哈希
*
* @returns
* - { ok: true, msgHash, nonce } — 消息可以发送
* - { ok: false, reason } — 消息被拒绝
*/
export declare function dedupMessage(sender: string, receiver: string, content: string): {
ok: true;
msgHash: string;
nonce: number;
} | {
ok: false;
reason: string;
};
/**
* 清理过期的去重缓存条目
*/
export declare function cleanupExpiredEntries(): number;
/**
* 启动 TTL 定时清理
*/
export declare function startDedupCleanup(): void;
/**
* 停止 TTL 定时清理
*/
export declare function stopDedupCleanup(): void;
+237
View File
@@ -0,0 +1,237 @@
/**
* dedup.ts — 消息去重模块 (Phase 2)
*
* 功能:
* - 消息完整性校验:msg_hash = sha256(sender + receiver + content + nonce)
* - per-sender 递增 nonce 管理(SQLite 持久化,Phase 2
* - dedup_cache 表操作(isDuplicate / recordHash
* - TTL 定时清理(15min
* - 消息体结构化分界(防 prompt injection
*
* Phase 2 变更:
* - nonce 从 in-memory Map 迁移到 SQLite sender_nonces 表
* - Hub 重启后 nonce 从上次值继续递增
* - 启动时自动建表(IF NOT EXISTS
*/
import { createHash } from "crypto";
import { db } from "./db.js";
import { auditLog } from "./security.js";
import { logError, logger } from "./logger.js";
// ─── 常量 ────────────────────────────────────────────────
const DEDUP_TTL_MS = parseInt(process.env.DEDUP_TTL ?? "900", 10) * 1000; // 默认 15 分钟
const DEDUP_CLEANUP_INTERVAL_MS = parseInt(process.env.DEDUP_CLEANUP_INTERVAL ?? "60000", 10); // 默认 1 分钟
// ─── sender_nonces 表初始化 ─────────────────────────────
/** 确保 sender_nonces 表存在(启动时调用) */
function ensureNonceTable() {
db.exec(`
CREATE TABLE IF NOT EXISTS sender_nonces (
sender_id TEXT PRIMARY KEY,
last_nonce INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL
);
`);
}
// 模块加载时自动初始化
ensureNonceTable();
// ─── Per-Sender Nonce 管理(SQLite 持久化)────────────
/**
* 获取 sender 的下一个 nonce(递增,持久化)
* @returns 递增后的 nonce 值
*/
export function nextNonce(senderId) {
const row = db
.prepare(`SELECT last_nonce FROM sender_nonces WHERE sender_id = ?`)
.get(senderId);
const last = row ? row.last_nonce : 0;
const next = last + 1;
const now = Date.now();
db.prepare(`
INSERT INTO sender_nonces (sender_id, last_nonce, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(sender_id) DO UPDATE SET last_nonce = ?, updated_at = ?
`).run(senderId, next, now, next, now);
return next;
}
/**
* 获取 sender 的当前 nonce(不递增)
*/
export function currentNonce(senderId) {
const row = db
.prepare(`SELECT last_nonce FROM sender_nonces WHERE sender_id = ?`)
.get(senderId);
return row ? row.last_nonce : 0;
}
/**
* 重置 sender 的 nonce(测试用)
*/
export function resetNonce(senderId) {
db.prepare(`DELETE FROM sender_nonces WHERE sender_id = ?`).run(senderId);
}
// ─── 消息哈希 ────────────────────────────────────────────
/**
* 计算去重哈希(不含 nonce)
* dedup_hash = sha256(sender + receiver + content)
* 用于检测完全相同的消息(防止重复发送)
*/
export function computeDedupHash(sender, receiver, content) {
const raw = `${sender}:${receiver}:${content}`;
return createHash("sha256").update(raw).digest("hex");
}
/**
* 计算消息完整性哈希(含 nonce)
* msg_hash = sha256(sender + receiver + content + nonce)
* 用于防篡改 + 客户端验证
*/
export function computeMsgHash(sender, receiver, content, nonce) {
const raw = `${sender}:${receiver}:${content}:${nonce}`;
return createHash("sha256").update(raw).digest("hex");
}
// ─── 重复检测 ────────────────────────────────────────────
/**
* 检查消息是否重复(基于 msg_hash)
* @returns true = 重复(应拒绝),false = 新消息
*/
export function isDuplicate(msgHash) {
try {
const row = db
.prepare(`SELECT msg_hash FROM dedup_cache WHERE msg_hash = ?`)
.get(msgHash);
return !!row;
}
catch (err) {
logError("dedup_isDuplicate_error", err);
return false; // 出错时允许通过(安全优先于阻断)
}
}
/**
* 记录消息哈希到去重缓存
*/
export function recordHash(msgHash, senderId, nonce) {
try {
const now = Date.now();
db.prepare(`INSERT OR IGNORE INTO dedup_cache (msg_hash, sender_id, nonce, created_at)
VALUES (?, ?, ?, ?)`).run(msgHash, senderId, nonce, now);
}
catch (err) {
logError("dedup_recordHash_error", err);
}
}
// ─── 消息体结构化分界 ────────────────────────────────────
/** 最大消息内容长度 */
const MAX_CONTENT_LENGTH = 50000;
/**
* 消息体安全校验(防 prompt injection 和格式攻击)
*
* 检查项:
* 1. 内容非空
* 2. 长度限制(50KB
* 3. 不包含 NULL 字节(\x00 分界符保留)
* 4. 不包含 SSE 注入模式(data: / event: / id:
*
* @returns { safe: true } 或 { safe: false, reason: string }
*/
export function validateMessageBody(content) {
// 非空检查
if (!content || content.trim().length === 0) {
return { safe: false, reason: "Message content cannot be empty" };
}
// 长度检查
if (content.length > MAX_CONTENT_LENGTH) {
return {
safe: false,
reason: `Message content too long (${content.length} > ${MAX_CONTENT_LENGTH} chars)`,
};
}
// NULL 字节检查(\x00 是消息分界符,不能出现在正文)
if (content.includes("\x00")) {
return { safe: false, reason: "Message content contains NULL byte (\\x00)" };
}
// SSE 注入检测
const ssePatterns = [/^data:\s*/m, /^event:\s*/m, /^id:\s*/m, /^retry:\s*/m];
for (const pattern of ssePatterns) {
if (pattern.test(content)) {
return {
safe: false,
reason: `Message content contains potential SSE injection pattern: ${pattern.source}`,
};
}
}
return { safe: true };
}
/**
* 完整的消息去重流程
*
* 1. 校验消息体
* 2. 计算去重哈希(不含 nonce)并检查重复
* 3. 分配 nonce
* 4. 计算完整性哈希(含 nonce)
* 5. 记录去重哈希
*
* @returns
* - { ok: true, msgHash, nonce } — 消息可以发送
* - { ok: false, reason } — 消息被拒绝
*/
export function dedupMessage(sender, receiver, content) {
// 1. 校验消息体
const validation = validateMessageBody(content);
if (!validation.safe) {
return { ok: false, reason: validation.reason };
}
// 2. 计算去重哈希并检查重复(不含 nonce)
const dedupHash = computeDedupHash(sender, receiver, content);
if (isDuplicate(dedupHash)) {
return { ok: false, reason: "Duplicate message detected (same content from same sender)" };
}
// 3. 分配 nonce
const nonce = nextNonce(sender);
// 4. 计算完整性哈希(含 nonce)
const msgHash = computeMsgHash(sender, receiver, content, nonce);
// 5. 记录去重哈希
recordHash(dedupHash, sender, nonce);
return { ok: true, msgHash, nonce };
}
// ─── TTL 清理 ────────────────────────────────────────────
let cleanupTimer = null;
/**
* 清理过期的去重缓存条目
*/
export function cleanupExpiredEntries() {
const cutoff = Date.now() - DEDUP_TTL_MS;
try {
const result = db.prepare(`DELETE FROM dedup_cache WHERE created_at < ?`).run(cutoff);
const deleted = result.changes;
if (deleted > 0) {
logger.info("dedup_cleanup", { module: "dedup", deleted, ttl_ms: DEDUP_TTL_MS });
// Phase 5a Day 2: 审计批量删除去重缓存
auditLog("cleanup_dedup_cache", "system:dedup", `batch`, `deleted=${deleted}, ttl=${DEDUP_TTL_MS}ms`);
}
return deleted;
}
catch (err) {
logError("dedup_cleanup_error", err);
return 0;
}
}
/**
* 启动 TTL 定时清理
*/
export function startDedupCleanup() {
if (cleanupTimer) {
clearInterval(cleanupTimer);
}
logger.info("dedup_cleanup_started", { module: "dedup", interval_ms: DEDUP_CLEANUP_INTERVAL_MS, ttl_ms: DEDUP_TTL_MS });
cleanupTimer = setInterval(() => {
cleanupExpiredEntries();
}, DEDUP_CLEANUP_INTERVAL_MS);
}
/**
* 停止 TTL 定时清理
*/
export function stopDedupCleanup() {
if (cleanupTimer) {
clearInterval(cleanupTimer);
cleanupTimer = null;
logger.info("dedup_cleanup_stopped", { module: "dedup" });
}
}
//# sourceMappingURL=dedup.js.map
+292
View File
@@ -0,0 +1,292 @@
/**
* dedup.ts — 消息去重模块 (Phase 2)
*
* 功能:
* - 消息完整性校验:msg_hash = sha256(sender + receiver + content + nonce)
* - per-sender 递增 nonce 管理(SQLite 持久化,Phase 2
* - dedup_cache 表操作(isDuplicate / recordHash
* - TTL 定时清理(15min
* - 消息体结构化分界(防 prompt injection
*
* Phase 2 变更:
* - nonce 从 in-memory Map 迁移到 SQLite sender_nonces 表
* - Hub 重启后 nonce 从上次值继续递增
* - 启动时自动建表(IF NOT EXISTS
*/
import { createHash } from "crypto";
import { db } from "./db.js";
import { auditLog } from "./security.js";
import { logError, logger } from "./logger.js";
import { getErrorMessage } from "./types.js";
// ─── 常量 ────────────────────────────────────────────────
const DEDUP_TTL_MS = parseInt(process.env.DEDUP_TTL ?? "900", 10) * 1000; // 默认 15 分钟
const DEDUP_CLEANUP_INTERVAL_MS = parseInt(process.env.DEDUP_CLEANUP_INTERVAL ?? "60000", 10); // 默认 1 分钟
// ─── sender_nonces 表初始化 ─────────────────────────────
/** 确保 sender_nonces 表存在(启动时调用) */
function ensureNonceTable(): void {
db.exec(`
CREATE TABLE IF NOT EXISTS sender_nonces (
sender_id TEXT PRIMARY KEY,
last_nonce INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL
);
`);
}
// 模块加载时自动初始化
ensureNonceTable();
// ─── Per-Sender Nonce 管理(SQLite 持久化)────────────
/**
* 获取 sender 的下一个 nonce(递增,持久化)
* @returns 递增后的 nonce 值
*/
export function nextNonce(senderId: string): number {
const row = db
.prepare(`SELECT last_nonce FROM sender_nonces WHERE sender_id = ?`)
.get(senderId) as any;
const last = row ? row.last_nonce : 0;
const next = last + 1;
const now = Date.now();
db.prepare(`
INSERT INTO sender_nonces (sender_id, last_nonce, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(sender_id) DO UPDATE SET last_nonce = ?, updated_at = ?
`).run(senderId, next, now, next, now);
return next;
}
/**
* 获取 sender 的当前 nonce(不递增)
*/
export function currentNonce(senderId: string): number {
const row = db
.prepare(`SELECT last_nonce FROM sender_nonces WHERE sender_id = ?`)
.get(senderId) as any;
return row ? row.last_nonce : 0;
}
/**
* 重置 sender 的 nonce(测试用)
*/
export function resetNonce(senderId: string): void {
db.prepare(`DELETE FROM sender_nonces WHERE sender_id = ?`).run(senderId);
}
// ─── 消息哈希 ────────────────────────────────────────────
/**
* 计算去重哈希(不含 nonce)
* dedup_hash = sha256(sender + receiver + content)
* 用于检测完全相同的消息(防止重复发送)
*/
export function computeDedupHash(
sender: string,
receiver: string,
content: string
): string {
const raw = `${sender}:${receiver}:${content}`;
return createHash("sha256").update(raw).digest("hex");
}
/**
* 计算消息完整性哈希(含 nonce)
* msg_hash = sha256(sender + receiver + content + nonce)
* 用于防篡改 + 客户端验证
*/
export function computeMsgHash(
sender: string,
receiver: string,
content: string,
nonce: number
): string {
const raw = `${sender}:${receiver}:${content}:${nonce}`;
return createHash("sha256").update(raw).digest("hex");
}
// ─── 重复检测 ────────────────────────────────────────────
/**
* 检查消息是否重复(基于 msg_hash)
* @returns true = 重复(应拒绝),false = 新消息
*/
export function isDuplicate(msgHash: string): boolean {
try {
const row = db
.prepare(`SELECT msg_hash FROM dedup_cache WHERE msg_hash = ?`)
.get(msgHash) as any;
return !!row;
} catch (err: unknown) {
logError("dedup_isDuplicate_error", err);
return false; // 出错时允许通过(安全优先于阻断)
}
}
/**
* 记录消息哈希到去重缓存
*/
export function recordHash(
msgHash: string,
senderId: string,
nonce: number
): void {
try {
const now = Date.now();
db.prepare(
`INSERT OR IGNORE INTO dedup_cache (msg_hash, sender_id, nonce, created_at)
VALUES (?, ?, ?, ?)`
).run(msgHash, senderId, nonce, now);
} catch (err: unknown) {
logError("dedup_recordHash_error", err);
}
}
// ─── 消息体结构化分界 ────────────────────────────────────
/** 最大消息内容长度 */
const MAX_CONTENT_LENGTH = 50000;
/**
* 消息体安全校验(防 prompt injection 和格式攻击)
*
* 检查项:
* 1. 内容非空
* 2. 长度限制(50KB
* 3. 不包含 NULL 字节(\x00 分界符保留)
* 4. 不包含 SSE 注入模式(data: / event: / id:
*
* @returns { safe: true } 或 { safe: false, reason: string }
*/
export function validateMessageBody(content: string): { safe: boolean; reason?: string } {
// 非空检查
if (!content || content.trim().length === 0) {
return { safe: false, reason: "Message content cannot be empty" };
}
// 长度检查
if (content.length > MAX_CONTENT_LENGTH) {
return {
safe: false,
reason: `Message content too long (${content.length} > ${MAX_CONTENT_LENGTH} chars)`,
};
}
// NULL 字节检查(\x00 是消息分界符,不能出现在正文)
if (content.includes("\x00")) {
return { safe: false, reason: "Message content contains NULL byte (\\x00)" };
}
// SSE 注入检测
const ssePatterns = [/^data:\s*/m, /^event:\s*/m, /^id:\s*/m, /^retry:\s*/m];
for (const pattern of ssePatterns) {
if (pattern.test(content)) {
return {
safe: false,
reason: `Message content contains potential SSE injection pattern: ${pattern.source}`,
};
}
}
return { safe: true };
}
/**
* 完整的消息去重流程
*
* 1. 校验消息体
* 2. 计算去重哈希(不含 nonce)并检查重复
* 3. 分配 nonce
* 4. 计算完整性哈希(含 nonce)
* 5. 记录去重哈希
*
* @returns
* - { ok: true, msgHash, nonce } — 消息可以发送
* - { ok: false, reason } — 消息被拒绝
*/
export function dedupMessage(
sender: string,
receiver: string,
content: string
): { ok: true; msgHash: string; nonce: number } | { ok: false; reason: string } {
// 1. 校验消息体
const validation = validateMessageBody(content);
if (!validation.safe) {
return { ok: false, reason: validation.reason! };
}
// 2. 计算去重哈希并检查重复(不含 nonce)
const dedupHash = computeDedupHash(sender, receiver, content);
if (isDuplicate(dedupHash)) {
return { ok: false, reason: "Duplicate message detected (same content from same sender)" };
}
// 3. 分配 nonce
const nonce = nextNonce(sender);
// 4. 计算完整性哈希(含 nonce)
const msgHash = computeMsgHash(sender, receiver, content, nonce);
// 5. 记录去重哈希
recordHash(dedupHash, sender, nonce);
return { ok: true, msgHash, nonce };
}
// ─── TTL 清理 ────────────────────────────────────────────
let cleanupTimer: ReturnType<typeof setInterval> | null = null;
/**
* 清理过期的去重缓存条目
*/
export function cleanupExpiredEntries(): number {
const cutoff = Date.now() - DEDUP_TTL_MS;
try {
const result = db.prepare(
`DELETE FROM dedup_cache WHERE created_at < ?`
).run(cutoff);
const deleted = result.changes;
if (deleted > 0) {
logger.info("dedup_cleanup", { module: "dedup", deleted, ttl_ms: DEDUP_TTL_MS });
// Phase 5a Day 2: 审计批量删除去重缓存
auditLog("cleanup_dedup_cache", "system:dedup", `batch`, `deleted=${deleted}, ttl=${DEDUP_TTL_MS}ms`);
}
return deleted;
} catch (err: unknown) {
logError("dedup_cleanup_error", err);
return 0;
}
}
/**
* 启动 TTL 定时清理
*/
export function startDedupCleanup(): void {
if (cleanupTimer) {
clearInterval(cleanupTimer);
}
logger.info("dedup_cleanup_started", { module: "dedup", interval_ms: DEDUP_CLEANUP_INTERVAL_MS, ttl_ms: DEDUP_TTL_MS });
cleanupTimer = setInterval(() => {
cleanupExpiredEntries();
}, DEDUP_CLEANUP_INTERVAL_MS);
}
/**
* 停止 TTL 定时清理
*/
export function stopDedupCleanup(): void {
if (cleanupTimer) {
clearInterval(cleanupTimer);
cleanupTimer = null;
logger.info("dedup_cleanup_stopped", { module: "dedup" });
}
}
+105
View File
@@ -0,0 +1,105 @@
/**
* errors.ts — 统一错误码体系
* Phase D: 替换散落的 new Error(),提供结构化错误信息
*/
// ─── 错误码枚举 ────────────────────────────────────────────
export enum HubErrorCode {
// 通用 1xxx
UNKNOWN = "HUB_1000",
INTERNAL = "HUB_1001",
NOT_FOUND = "HUB_1002",
VALIDATION = "HUB_1003",
ALREADY_EXISTS = "HUB_1004",
UNREACHABLE = "HUB_1005",
// 认证/权限 2xxx
AUTH_REQUIRED = "HUB_2000",
PERMISSION_DENIED = "HUB_2001",
TOKEN_EXPIRED = "HUB_2002",
TOKEN_INVALID = "HUB_2003",
// Agent 3xxx
AGENT_NOT_FOUND = "HUB_3000",
AGENT_OFFLINE = "HUB_3001",
INVALID_ROLE = "HUB_3002",
// 任务/编排 4xxx
TASK_NOT_FOUND = "HUB_4000",
INVALID_TRANSITION = "HUB_4001",
CYCLE_DETECTED = "HUB_4002",
DEPENDENCY_EXISTS = "HUB_4003",
DEPENDENCY_NOT_FOUND = "HUB_4004",
HANDOFF_NOT_TARGET = "HUB_4005",
GATE_NOT_FOUND = "HUB_4006",
GATE_ALREADY_EVAL = "HUB_4007",
PARALLEL_MIN_TASKS = "HUB_4008",
PARALLEL_MAX_TASKS = "HUB_4009",
GROUP_NOT_FOUND = "HUB_4010",
// Pipeline 5xxx
PIPELINE_NOT_FOUND = "HUB_5000",
// 消息 6xxx
MESSAGE_SEND_FAIL = "HUB_6000",
// 数据库 7xxx
DB_ERROR = "HUB_7000",
DB_INTEGRITY = "HUB_7001",
}
// ─── HubError 类 ────────────────────────────────────────────
export class HubError extends Error {
readonly code: HubErrorCode;
readonly details?: Record<string, unknown>;
constructor(code: HubErrorCode, message: string, details?: Record<string, unknown>) {
super(message);
this.name = "HubError";
this.code = code;
this.details = details;
}
/** 序列化为 MCP 工具返回格式 */
toJSON(): { error: true; code: string; message: string; details?: Record<string, unknown> } {
return {
error: true,
code: this.code,
message: this.message,
...(this.details && { details: this.details }),
};
}
/** 从 unknown 判断是否为 HubError */
static isHubError(err: unknown): err is HubError {
return err instanceof HubError;
}
}
// ─── 工厂函数(简化常见错误创建) ────────────────────────────
export function notFound(resource: string, id: string): HubError {
return new HubError(HubErrorCode.NOT_FOUND, `${resource} not found: ${id}`, { resource, id });
}
export function alreadyExists(resource: string, id?: string): HubError {
return new HubError(HubErrorCode.ALREADY_EXISTS, `${resource} already exists${id ? `: ${id}` : ""}`, { resource, id });
}
export function validation(msg: string, details?: Record<string, unknown>): HubError {
return new HubError(HubErrorCode.VALIDATION, msg, details);
}
export function permissionDenied(tool: string, required: string, actual: string): HubError {
return new HubError(
HubErrorCode.PERMISSION_DENIED,
`Permission denied: ${tool} requires '${required}' role, current role is '${actual}'`,
{ tool, required, actual },
);
}
export function authRequired(tool?: string): HubError {
return new HubError(HubErrorCode.AUTH_REQUIRED, tool ? `Authentication required for tool: ${tool}` : "Authentication required", { tool });
}
+244
View File
@@ -0,0 +1,244 @@
export interface Strategy {
id: number;
title: string;
content: string;
category: "experience" | "workflow" | "fix" | "tool_config" | "prompt_template" | "other";
sensitivity: "normal" | "high";
proposer_id: string;
status: "pending" | "approved" | "rejected" | "withdrawn";
approve_reason: string | null;
approved_by: string | null;
approved_at: number | null;
proposed_at: number;
task_id: string | null;
source_trust: number;
apply_count: number;
feedback_count: number;
positive_count: number;
}
export interface StrategyFeedback {
strategy_id: number;
agent_id: string;
feedback: "positive" | "negative" | "neutral";
comment: string | null;
applied: number;
created_at: number;
}
export interface StrategyApplication {
strategy_id: number;
agent_id: string;
context: string | null;
result: string | null;
created_at: number;
}
export interface EvolutionStats {
total_experiences: number;
total_strategies: number;
pending_approval: number;
approved_count: number;
rejected_count: number;
total_applications: number;
total_feedback: number;
positive_feedback: number;
approved_rate: number;
top_contributors: Array<{
agent_id: string;
count: number;
trust_score: number;
}>;
recent_approved: Strategy[];
}
/**
* 分享经验(直接 approved,不需审批)
*/
export declare function shareExperience(title: string, content: string, proposerId: string, options?: {
tags?: string[];
task_id?: string;
}): {
ok: true;
strategy: Strategy;
} | {
ok: false;
error: string;
};
/**
* 提议策略(pending,需 admin 审批)
*/
export declare function proposeStrategy(title: string, content: string, category: "workflow" | "fix" | "tool_config" | "prompt_template" | "other", proposerId: string, options?: {
task_id?: string;
}): {
ok: true;
strategy: Strategy;
sensitivity: string;
} | {
ok: false;
error: string;
};
/**
* 策略列表查询
*/
export declare function listStrategies(options?: {
status?: "pending" | "approved" | "rejected" | "all";
category?: "experience" | "workflow" | "fix" | "tool_config" | "prompt_template" | "other" | "all";
proposer_id?: string;
limit?: number;
}): Strategy[];
/**
* FTS5 搜索策略(仅返回 approved 策略)
*/
export declare function searchStrategies(query: string, options?: {
category?: string;
limit?: number;
}): Strategy[];
/**
* 采纳策略(仅 approved 策略可采纳)
*/
export declare function applyStrategy(strategyId: number, agentId: string, options?: {
context?: string;
}): {
ok: true;
application_id: number;
} | {
ok: false;
error: string;
};
/**
* 对策略反馈(UNIQUE 防刷)
*/
export declare function feedbackStrategy(strategyId: number, agentId: string, feedback: "positive" | "negative" | "neutral", options?: {
comment?: string;
applied?: boolean;
}): {
ok: true;
feedback_id: number;
} | {
ok: false;
error: string;
};
/**
* admin 审批策略(approve/reject
*/
export declare function approveStrategy(strategyId: number, adminId: string, action: "approve" | "reject", reason: string): {
ok: true;
strategy: Strategy;
} | {
ok: false;
error: string;
};
/**
* 进化指标统计
*/
export declare function getEvolutionStatus(): EvolutionStats;
/** 审批等级 */
export type ApprovalTier = "auto" | "peer" | "admin" | "super";
export interface TieredStrategy extends Strategy {
approval_tier: ApprovalTier | null;
observation_start: number | null;
veto_deadline: number | null;
}
export interface TierJudgment {
tier: ApprovalTier;
reason: string;
trust_score: number;
sensitivity: "normal" | "high";
history_count: number;
}
/**
* 判定策略审批等级
*
* 4 级 tier 规则:
* 1. super: sensitivity=high → 需人工审批(最高权限)
* 2. auto: trust≥90 + sensitivity=normal + 历史≥5 → 自动通过 + 72h 观察窗口
* 3. peer: trust≥60 + sensitivity=normal + 历史≥2 → peer 审批
* 4. admin: 其他 → admin 审批
*/
export declare function judgeTier(proposerId: string, category: string, content: string): TierJudgment;
/**
* 自动通过策略(auto tier
* 设置 approved + 启动 72h 观察窗口
*/
export declare function autoApprove(strategyId: number): {
ok: true;
strategy: TieredStrategy;
} | {
ok: false;
error: string;
};
/**
* 启动观察窗口(peer tier 策略被 peer 审批通过后调用)
* 72h 观察窗口内如果负面反馈超过阈值,策略可被撤回
*/
export declare function startObservation(strategyId: number, approverId: string): {
ok: true;
strategy: TieredStrategy;
veto_deadline: number;
} | {
ok: false;
error: string;
};
/**
* 检查否决窗口(48h
* 在否决窗口内,如果负面反馈超过正面反馈的 50%,任何 admin 可以撤回策略
*/
export declare function checkVetoWindow(strategyId: number): {
in_window: boolean;
can_veto: boolean;
negative_count: number;
positive_count: number;
veto_ratio: number;
veto_deadline: number | null;
observation_start: number | null;
};
/**
* 撤回处于否决窗口内的策略
*/
export declare function vetoStrategy(strategyId: number, adminId: string, reason: string): {
ok: true;
strategy: Strategy;
} | {
ok: false;
error: string;
};
/**
* 分级策略提议(统一入口)
* 自动判定 tier 并执行对应流程
*/
export declare function proposeStrategyTiered(title: string, content: string, category: "workflow" | "fix" | "tool_config" | "prompt_template" | "other", proposerId: string, options?: {
task_id?: string;
}): {
ok: true;
strategy: TieredStrategy;
tier: ApprovalTier;
sensitivity: string;
auto_approved: boolean;
veto_deadline: number | null;
} | {
ok: false;
error: string;
};
/**
* 提供反馈(UPSERT 版本)— 用于自动创建反馈占位和后续更新
* 与 feedbackStrategy 不同:使用 ON CONFLICT DO UPDATE 而非拒绝重复
*/
export declare function provideFeedback(params: {
strategyId: number;
agentId: string;
feedback: string;
comment?: string;
applied?: number;
}): {
id: number;
};
/**
* 自动评分已采纳策略
* 将 7 天内仍为 neutral 反馈的策略降为 negative(无实际效果证据)
* 应由 cron 或清理任务定期调用
*/
export declare function scoreAppliedStrategies(): {
scored: number;
details: Array<{
strategyId: number;
title: string;
action: string;
}>;
};
+632
View File
@@ -0,0 +1,632 @@
/**
* evolution.ts — Evolution Engine 简化版 (Phase 3)
*
* 功能:
* - shareExperience: 分享经验(直接 approved,不需审批)
* - proposeStrategy: 提议策略(pending,需 admin 审批 + Hub 自动判定 sensitivity
* - listStrategies: 策略列表查询
* - searchStrategies: FTS5 搜索策略
* - applyStrategy: 采纳策略
* - feedbackStrategy: 对策略反馈(UNIQUE 防刷)
* - approveStrategy: admin 审批策略
* - getEvolutionStatus: 进化指标统计
*/
import { db } from "./db.js";
import { buildFtsTokens, buildSearchQuery } from "./tokenizer.js";
import { auditLog } from "./security.js";
import { logError } from "./logger.js";
// ─── 常量 ────────────────────────────────────────────────
const MAX_CONTENT_LENGTH = 5000;
const MAX_TITLE_LENGTH = 200;
// ─── sensitivity 判定 ────────────────────────────────────
/**
* Hub 自动判定策略敏感级别(非 Agent 自报告)
*/
function judgeSensitivity(category, content) {
// 高敏感分类:prompt_template 直接判定 high
if (category === "prompt_template")
return "high";
// 高敏感关键词检测(结构化模式匹配)
const highPatterns = [
/system[_\s]*prompt/i,
/系统指令/,
/capability[_\s]*declare/i,
/能力声明/,
/permission[_\s]*(change|modify|grant)/i,
/权限变更/,
/role[_\s]*(change|escalat)/i,
];
for (const pattern of highPatterns) {
if (pattern.test(content))
return "high";
}
return "normal";
}
// ─── 服务方法 ────────────────────────────────────────────
/**
* 分享经验(直接 approved,不需审批)
*/
export function shareExperience(title, content, proposerId, options) {
if (!title || title.trim().length < 3) {
return { ok: false, error: "Title must be at least 3 characters" };
}
if (title.length > MAX_TITLE_LENGTH) {
return { ok: false, error: `Title too long (${title.length} > ${MAX_TITLE_LENGTH})` };
}
if (!content || content.trim().length < 10) {
return { ok: false, error: "Content must be at least 10 characters" };
}
if (content.length > MAX_CONTENT_LENGTH) {
return { ok: false, error: `Content too long (${content.length} > ${MAX_CONTENT_LENGTH})` };
}
const now = Date.now();
const trustScore = getAgentTrustScore(proposerId);
try {
const result = db.prepare(`INSERT INTO strategies (title, content, category, sensitivity, proposer_id, status, proposed_at, task_id, source_trust)
VALUES (?, ?, 'experience', 'normal', ?, 'approved', ?, ?, ?)`).run(title.trim(), content.trim(), proposerId, now, options?.task_id ?? null, trustScore);
const strategy = getStrategyById(result.lastInsertRowid);
if (!strategy) {
return { ok: false, error: "Failed to retrieve created strategy" };
}
// FTS 索引同步
insertFtsEntry(strategy);
return { ok: true, strategy };
}
catch (err) {
return { ok: false, error: `Failed to share experience: ${err.message}` };
}
}
/**
* 提议策略(pending,需 admin 审批)
*/
export function proposeStrategy(title, content, category, proposerId, options) {
if (!title || title.trim().length < 3) {
return { ok: false, error: "Title must be at least 3 characters" };
}
if (title.length > MAX_TITLE_LENGTH) {
return { ok: false, error: `Title too long (${title.length} > ${MAX_TITLE_LENGTH})` };
}
if (!content || content.trim().length < 10) {
return { ok: false, error: "Content must be at least 10 characters" };
}
if (content.length > MAX_CONTENT_LENGTH) {
return { ok: false, error: `Content too long (${content.length} > ${MAX_CONTENT_LENGTH})` };
}
const sensitivity = judgeSensitivity(category, content);
const now = Date.now();
const trustScore = getAgentTrustScore(proposerId);
try {
const result = db.prepare(`INSERT INTO strategies (title, content, category, sensitivity, proposer_id, status, proposed_at, task_id, source_trust)
VALUES (?, ?, ?, ?, ?, 'pending', ?, ?, ?)`).run(title.trim(), content.trim(), category, sensitivity, proposerId, now, options?.task_id ?? null, trustScore);
const strategy = getStrategyById(result.lastInsertRowid);
if (!strategy) {
return { ok: false, error: "Failed to retrieve created strategy" };
}
return { ok: true, strategy, sensitivity };
}
catch (err) {
return { ok: false, error: `Failed to propose strategy: ${err.message}` };
}
}
/**
* 策略列表查询
*/
export function listStrategies(options) {
const limit = Math.min(options?.limit ?? 50, 50);
const conditions = [];
const params = [];
if (options?.status && options.status !== "all") {
conditions.push("s.status = ?");
params.push(options.status);
}
if (options?.category && options.category !== "all") {
conditions.push("s.category = ?");
params.push(options.category);
}
if (options?.proposer_id) {
conditions.push("s.proposer_id = ?");
params.push(options.proposer_id);
}
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
params.push(limit);
try {
return db.prepare(`SELECT s.* FROM strategies s ${where} ORDER BY s.proposed_at DESC LIMIT ?`).all(...params);
}
catch (err) {
logError("evolution_listStrategies_error", err);
return [];
}
}
/**
* FTS5 搜索策略(仅返回 approved 策略)
*/
export function searchStrategies(query, options) {
if (!query || query.trim().length < 2)
return [];
const limit = Math.min(options?.limit ?? 20, 20);
const safeQuery = buildSearchQuery(query);
if (!safeQuery)
return [];
try {
const conditions = [`strategies_fts MATCH ?`, `s.status = 'approved'`];
const params = [safeQuery];
if (options?.category) {
conditions.push("s.category = ?");
params.push(options.category);
}
params.push(limit);
return db.prepare(`SELECT s.* FROM strategies s
JOIN strategies_fts ON strategies_fts.rowid = s.id
WHERE ${conditions.join(" AND ")}
ORDER BY rank LIMIT ?`).all(...params);
}
catch (err) {
logError("evolution_searchStrategies_error", err);
return [];
}
}
/**
* 采纳策略(仅 approved 策略可采纳)
*/
export function applyStrategy(strategyId, agentId, options) {
// 验证策略存在且 approved
const strategy = getStrategyById(strategyId);
if (!strategy) {
return { ok: false, error: `Strategy ${strategyId} not found` };
}
if (strategy.status !== "approved") {
return { ok: false, error: `Strategy ${strategyId} is not approved (status: ${strategy.status})` };
}
const now = Date.now();
try {
const result = db.prepare(`INSERT INTO strategy_applications (strategy_id, agent_id, context, created_at)
VALUES (?, ?, ?, ?)`).run(strategyId, agentId, options?.context ?? null, now);
// apply_count++
db.prepare(`UPDATE strategies SET apply_count = apply_count + 1 WHERE id = ?`).run(strategyId);
return { ok: true, application_id: result.lastInsertRowid };
}
catch (err) {
return { ok: false, error: `Failed to apply strategy: ${err.message}` };
}
}
/**
* 对策略反馈(UNIQUE 防刷)
*/
export function feedbackStrategy(strategyId, agentId, feedback, options) {
// 验证策略存在
const strategy = getStrategyById(strategyId);
if (!strategy) {
return { ok: false, error: `Strategy ${strategyId} not found` };
}
const now = Date.now();
const applied = options?.applied ? 1 : 0;
try {
const result = db.prepare(`INSERT INTO strategy_feedback (strategy_id, agent_id, feedback, comment, applied, created_at)
VALUES (?, ?, ?, ?, ?, ?)`).run(strategyId, agentId, feedback, options?.comment ?? null, applied, now);
// 更新计数器
db.prepare(`UPDATE strategies SET feedback_count = feedback_count + 1,
positive_count = positive_count + CASE WHEN ? = 'positive' THEN 1 ELSE 0 END
WHERE id = ?`).run(feedback, strategyId);
return { ok: true, feedback_id: result.lastInsertRowid };
}
catch (err) {
// UNIQUE 约束冲突 = 重复反馈
if (err.message.includes("UNIQUE")) {
return { ok: false, error: "You have already provided feedback for this strategy" };
}
return { ok: false, error: `Failed to submit feedback: ${err.message}` };
}
}
/**
* admin 审批策略(approve/reject
*/
export function approveStrategy(strategyId, adminId, action, reason) {
if (!reason || reason.trim().length === 0) {
return { ok: false, error: "Approval reason is required" };
}
if (reason.length > 1000) {
return { ok: false, error: "Reason too long (max 1000 characters)" };
}
const strategy = getStrategyById(strategyId);
if (!strategy) {
return { ok: false, error: `Strategy ${strategyId} not found` };
}
if (strategy.status !== "pending") {
return { ok: false, error: `Strategy ${strategyId} is not pending (status: ${strategy.status})` };
}
const newStatus = action === "approve" ? "approved" : "rejected";
const now = Date.now();
try {
db.prepare(`UPDATE strategies SET status = ?, approve_reason = ?, approved_by = ?, approved_at = ? WHERE id = ?`).run(newStatus, reason.trim(), adminId, now, strategyId);
// 如果 approved,同步 FTS 索引
if (action === "approve") {
insertFtsEntry(getStrategyById(strategyId));
}
const updated = getStrategyById(strategyId);
return { ok: true, strategy: updated };
}
catch (err) {
return { ok: false, error: `Failed to approve/reject strategy: ${err.message}` };
}
}
/**
* 进化指标统计
*/
export function getEvolutionStatus() {
try {
const totalExperiences = db.prepare(`SELECT COUNT(*) as cnt FROM strategies WHERE category = 'experience'`).get()?.cnt ?? 0;
const totalStrategies = db.prepare(`SELECT COUNT(*) as cnt FROM strategies WHERE category != 'experience'`).get()?.cnt ?? 0;
const pendingApproval = db.prepare(`SELECT COUNT(*) as cnt FROM strategies WHERE status = 'pending'`).get()?.cnt ?? 0;
const approvedCount = db.prepare(`SELECT COUNT(*) as cnt FROM strategies WHERE status = 'approved'`).get()?.cnt ?? 0;
const rejectedCount = db.prepare(`SELECT COUNT(*) as cnt FROM strategies WHERE status = 'rejected'`).get()?.cnt ?? 0;
const totalApplications = db.prepare(`SELECT COUNT(*) as cnt FROM strategy_applications`).get()?.cnt ?? 0;
const totalFeedback = db.prepare(`SELECT COUNT(*) as cnt FROM strategy_feedback`).get()?.cnt ?? 0;
const positiveFeedback = db.prepare(`SELECT COUNT(*) as cnt FROM strategy_feedback WHERE feedback = 'positive'`).get()?.cnt ?? 0;
const total = totalExperiences + totalStrategies;
const approvedRate = total > 0 ? Math.round((approvedCount / total) * 10000) / 100 : 0;
// Top contributors
const contributors = db.prepare(`SELECT s.proposer_id as agent_id, COUNT(*) as count, COALESCE(a.trust_score, 50) as trust_score
FROM strategies s
LEFT JOIN agents a ON s.proposer_id = a.agent_id
GROUP BY s.proposer_id
ORDER BY count DESC LIMIT 10`).all();
// Recent approved (last 5)
const recentApproved = db.prepare(`SELECT * FROM strategies WHERE status = 'approved' ORDER BY approved_at DESC LIMIT 5`).all();
return {
total_experiences: totalExperiences,
total_strategies: totalStrategies,
pending_approval: pendingApproval,
approved_count: approvedCount,
rejected_count: rejectedCount,
total_applications: totalApplications,
total_feedback: totalFeedback,
positive_feedback: positiveFeedback,
approved_rate: approvedRate,
top_contributors: contributors,
recent_approved: recentApproved,
};
}
catch (err) {
logError("evolution_getEvolutionStatus_error", err);
return {
total_experiences: 0, total_strategies: 0, pending_approval: 0,
approved_count: 0, rejected_count: 0, total_applications: 0,
total_feedback: 0, positive_feedback: 0, approved_rate: 0,
top_contributors: [], recent_approved: [],
};
}
}
/** 审批等级判定阈值 */
const TIER_THRESHOLDS = {
auto: { minTrust: 90, maxRisk: "normal", requireHistory: 5 }, // 高信任+低风险+有历史 → 自动通过
peer: { minTrust: 60, maxRisk: "normal", requireHistory: 2 }, // 中等信任+低风险 → peer 审批
admin: { minTrust: 0, maxRisk: "any", requireHistory: 0 }, // 默认 → admin 审批
super: { maxRisk: "high" }, // 高风险 → super 审批(需人工)
};
/** 观察窗口时长:72h */
const OBSERVATION_WINDOW_MS = 72 * 60 * 60 * 1000;
/** 否决窗口时长:48h */
const VETO_WINDOW_MS = 48 * 60 * 60 * 1000;
// ─── 分级判定 ────────────────────────────────────────────
/**
* 判定策略审批等级
*
* 4 级 tier 规则:
* 1. super: sensitivity=high → 需人工审批(最高权限)
* 2. auto: trust≥90 + sensitivity=normal + 历史≥5 → 自动通过 + 72h 观察窗口
* 3. peer: trust≥60 + sensitivity=normal + 历史≥2 → peer 审批
* 4. admin: 其他 → admin 审批
*/
export function judgeTier(proposerId, category, content) {
const sensitivity = judgeSensitivity(category, content);
const trustScore = getAgentTrustScore(proposerId);
const historyCount = getStrategyHistoryCount(proposerId);
// 规则 1: super — 高风险策略
if (sensitivity === "high") {
return {
tier: "super",
reason: `高风险策略(sensitivity=high),需 super 人工审批`,
trust_score: trustScore,
sensitivity,
history_count: historyCount,
};
}
// 规则 2: auto — 高信任+低风险+有历史
if (trustScore >= TIER_THRESHOLDS.auto.minTrust &&
historyCount >= TIER_THRESHOLDS.auto.requireHistory) {
return {
tier: "auto",
reason: `高信任策略(trust=${trustScore}, history=${historyCount}),自动通过 + 72h 观察窗口`,
trust_score: trustScore,
sensitivity,
history_count: historyCount,
};
}
// 规则 3: peer — 中等信任+低风险+有少量历史
if (trustScore >= TIER_THRESHOLDS.peer.minTrust &&
historyCount >= TIER_THRESHOLDS.peer.requireHistory) {
return {
tier: "peer",
reason: `中等信任策略(trust=${trustScore}, history=${historyCount}),需 peer 审批`,
trust_score: trustScore,
sensitivity,
history_count: historyCount,
};
}
// 规则 4: admin — 默认
return {
tier: "admin",
reason: `默认审批(trust=${trustScore}, history=${historyCount}),需 admin 审批`,
trust_score: trustScore,
sensitivity,
history_count: historyCount,
};
}
/**
* 自动通过策略(auto tier
* 设置 approved + 启动 72h 观察窗口
*/
export function autoApprove(strategyId) {
const strategy = getStrategyById(strategyId);
if (!strategy) {
return { ok: false, error: `Strategy ${strategyId} not found` };
}
if (strategy.status !== "pending") {
return { ok: false, error: `Strategy ${strategyId} is not pending (status: ${strategy.status})` };
}
const now = Date.now();
const vetoDeadline = now + VETO_WINDOW_MS;
// 更新:approved + 观察窗口 + 否决窗口
db.prepare(`UPDATE strategies SET status='approved', approval_tier='auto',
approved_at=?, approved_by='system:auto',
observation_start=?, veto_deadline=?
WHERE id=?`).run(now, now, vetoDeadline, strategyId);
// FTS 索引同步
const updated = getStrategyWithTier(strategyId);
if (updated) {
insertFtsEntry(updated);
}
auditLog("auto_approve", "system:auto", String(strategyId), `veto_deadline=${vetoDeadline}`);
if (!updated) {
return { ok: false, error: "Failed to retrieve updated strategy" };
}
return { ok: true, strategy: updated };
}
/**
* 启动观察窗口(peer tier 策略被 peer 审批通过后调用)
* 72h 观察窗口内如果负面反馈超过阈值,策略可被撤回
*/
export function startObservation(strategyId, approverId) {
const strategy = getStrategyById(strategyId);
if (!strategy) {
return { ok: false, error: `Strategy ${strategyId} not found` };
}
if (strategy.status !== "approved") {
return { ok: false, error: `Strategy ${strategyId} is not approved` };
}
const now = Date.now();
const vetoDeadline = now + VETO_WINDOW_MS;
db.prepare(`UPDATE strategies SET approval_tier='peer', observation_start=?, veto_deadline=?
WHERE id=?`).run(now, vetoDeadline, strategyId);
auditLog("start_observation", approverId, String(strategyId), `veto_deadline=${vetoDeadline}`);
const updated = getStrategyWithTier(strategyId);
if (!updated) {
return { ok: false, error: "Failed to retrieve updated strategy" };
}
return { ok: true, strategy: updated, veto_deadline: vetoDeadline };
}
/**
* 检查否决窗口(48h
* 在否决窗口内,如果负面反馈超过正面反馈的 50%,任何 admin 可以撤回策略
*/
export function checkVetoWindow(strategyId) {
const strategy = db.prepare(`SELECT veto_deadline, observation_start, positive_count, feedback_count
FROM strategies WHERE id=?`).get(strategyId);
if (!strategy) {
return {
in_window: false, can_veto: false,
negative_count: 0, positive_count: 0, veto_ratio: 0,
veto_deadline: null, observation_start: null,
};
}
const now = Date.now();
const negativeCount = (strategy.feedback_count ?? 0) - (strategy.positive_count ?? 0);
// 否决窗口判断
const inWindow = strategy.veto_deadline && now < strategy.veto_deadline;
// 否决条件:负面超过正面的 50%
const positiveCount = strategy.positive_count ?? 0;
const vetoRatio = positiveCount > 0 ? negativeCount / positiveCount : (negativeCount > 0 ? 1 : 0);
const canVeto = inWindow && vetoRatio > 0.5;
return {
in_window: !!inWindow,
can_veto: canVeto,
negative_count: negativeCount,
positive_count: positiveCount,
veto_ratio: Math.round(vetoRatio * 100) / 100,
veto_deadline: strategy.veto_deadline,
observation_start: strategy.observation_start,
};
}
/**
* 撤回处于否决窗口内的策略
*/
export function vetoStrategy(strategyId, adminId, reason) {
// 验证否决窗口
const vetoCheck = checkVetoWindow(strategyId);
if (!vetoCheck.in_window) {
return { ok: false, error: "Strategy is not in veto window" };
}
if (!vetoCheck.can_veto) {
return { ok: false, error: `Cannot veto: veto ratio ${vetoCheck.veto_ratio} <= 0.5 threshold` };
}
const now = Date.now();
db.prepare(`UPDATE strategies SET status='rejected', approval_tier='vetoed',
approve_reason=?, approved_by=?, approved_at=?,
observation_start=null, veto_deadline=null
WHERE id=?`).run(reason, adminId, now, strategyId);
// 从 FTS 移除
db.prepare(`DELETE FROM strategies_fts WHERE rowid=?`).run(strategyId);
// Phase 5a Day 2: 审计 FTS 索引删除
auditLog("delete_strategy_fts", adminId, String(strategyId), `reason=${reason}`);
auditLog("veto_strategy", adminId, String(strategyId), `reason=${reason}`);
const updated = getStrategyById(strategyId);
if (!updated) {
return { ok: false, error: "Failed to retrieve updated strategy" };
}
return { ok: true, strategy: updated };
}
/**
* 分级策略提议(统一入口)
* 自动判定 tier 并执行对应流程
*/
export function proposeStrategyTiered(title, content, category, proposerId, options) {
// 基础校验
if (!title || title.trim().length < 3) {
return { ok: false, error: "Title must be at least 3 characters" };
}
if (title.length > MAX_TITLE_LENGTH) {
return { ok: false, error: `Title too long (${title.length} > ${MAX_TITLE_LENGTH})` };
}
if (!content || content.trim().length < 10) {
return { ok: false, error: "Content must be at least 10 characters" };
}
if (content.length > MAX_CONTENT_LENGTH) {
return { ok: false, error: `Content too long (${content.length} > ${MAX_CONTENT_LENGTH})` };
}
// 判定分级
const judgment = judgeTier(proposerId, category, content);
const sensitivity = judgment.sensitivity;
const now = Date.now();
const trustScore = getAgentTrustScore(proposerId);
try {
// 创建策略(初始 pending
const result = db.prepare(`INSERT INTO strategies (title, content, category, sensitivity, proposer_id,
status, proposed_at, task_id, source_trust, approval_tier)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(title.trim(), content.trim(), category, sensitivity, proposerId, "pending", now, options?.task_id ?? null, trustScore, judgment.tier);
const strategyId = result.lastInsertRowid;
let autoApproved = false;
let vetoDeadline = null;
if (judgment.tier === "auto") {
// auto tier: 自动通过 + 启动观察窗口
const approveResult = autoApprove(strategyId);
if (approveResult.ok) {
autoApproved = true;
vetoDeadline = approveResult.strategy.veto_deadline;
}
}
// peer / admin / super: 保持 pending 状态
const strategy = getStrategyWithTier(strategyId);
if (!strategy) {
return { ok: false, error: "Failed to retrieve created strategy" };
}
return {
ok: true,
strategy,
tier: judgment.tier,
sensitivity,
auto_approved: autoApproved,
veto_deadline: vetoDeadline,
};
}
catch (err) {
return { ok: false, error: `Failed to propose strategy: ${err.message}` };
}
}
// ─── 内部辅助函数 ────────────────────────────────────────
function getStrategyById(id) {
return db.prepare(`SELECT * FROM strategies WHERE id = ?`).get(id);
}
function getStrategyWithTier(id) {
return db.prepare(`SELECT * FROM strategies WHERE id = ?`).get(id);
}
function getStrategyHistoryCount(proposerId) {
try {
const row = db.prepare(`SELECT COUNT(*) as cnt FROM strategies WHERE proposer_id = ? AND status = 'approved'`).get(proposerId);
return row?.cnt ?? 0;
}
catch {
return 0;
}
}
function getAgentTrustScore(agentId) {
try {
const row = db.prepare(`SELECT trust_score FROM agents WHERE agent_id = ?`).get(agentId);
return row?.trust_score ?? 50;
}
catch {
return 50;
}
}
function insertFtsEntry(strategy) {
try {
const tokens = buildFtsTokens(strategy.title, strategy.content);
db.prepare(`INSERT INTO strategies_fts (rowid, title, content, category) VALUES (?, ?, ?, ?)`).run(strategy.id, strategy.title, tokens, strategy.category);
}
catch (err) {
logError("evolution_fts_insert_error", err);
}
}
// ─── Phase 2.2: 策略采纳闭环 ─────────────────────────────────
/**
* 提供反馈(UPSERT 版本)— 用于自动创建反馈占位和后续更新
* 与 feedbackStrategy 不同:使用 ON CONFLICT DO UPDATE 而非拒绝重复
*/
export function provideFeedback(params) {
const now = Date.now();
try {
const result = db.prepare(`
INSERT INTO strategy_feedback (strategy_id, agent_id, feedback, comment, applied, created_at)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(strategy_id, agent_id) DO UPDATE SET
feedback = excluded.feedback,
comment = excluded.comment,
applied = excluded.applied
`).run(params.strategyId, params.agentId, params.feedback, params.comment || null, params.applied ?? 0, now);
return { id: result.lastInsertRowid };
}
catch (err) {
throw new Error(`创建反馈失败: ${err.message}`);
}
}
/**
* 自动评分已采纳策略
* 将 7 天内仍为 neutral 反馈的策略降为 negative(无实际效果证据)
* 应由 cron 或清理任务定期调用
*/
export function scoreAppliedStrategies() {
const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
// 查找 7 天前创建的 neutral 反馈
const staleFeedbacks = db.prepare(`
SELECT sf.strategy_id, sf.agent_id, sf.id as feedback_id, s.title
FROM strategy_feedback sf
JOIN strategies s ON s.id = sf.strategy_id
WHERE sf.feedback = 'neutral'
AND sf.created_at < ?
AND s.approved = 1
`).all(sevenDaysAgo);
const details = [];
let scored = 0;
for (const fb of staleFeedbacks) {
// 检查是否有其他 agent 给了非 neutral 反馈
const otherFeedback = db.prepare(`
SELECT feedback FROM strategy_feedback
WHERE strategy_id = ? AND agent_id != ? AND feedback != 'neutral'
`).all(fb.strategy_id, fb.agent_id);
if (otherFeedback.length === 0) {
// 无人提供有效反馈 → 降分为 negative
db.prepare(`UPDATE strategy_feedback SET feedback = 'negative', comment = ? WHERE id = ?`).run("自动降分:采纳后 7 天内无实际效果反馈", fb.feedback_id);
// 同步减少 positive_count(如果之前因 neutral 未增加则无需操作)
details.push({
strategyId: fb.strategy_id,
title: fb.title,
action: "neutral→negative (7天无反馈)",
});
scored++;
}
}
if (scored > 0) {
logError("evolution_auto_score", new Error(`Auto-scored ${scored} stale feedbacks`));
}
return { scored, details };
}
//# sourceMappingURL=evolution.js.map
+954
View File
@@ -0,0 +1,954 @@
/**
* evolution.ts — Evolution Engine 简化版 (Phase 3)
*
* 功能:
* - shareExperience: 分享经验(直接 approved,不需审批)
* - proposeStrategy: 提议策略(pending,需 admin 审批 + Hub 自动判定 sensitivity
* - listStrategies: 策略列表查询
* - searchStrategies: FTS5 搜索策略
* - applyStrategy: 采纳策略
* - feedbackStrategy: 对策略反馈(UNIQUE 防刷)
* - approveStrategy: admin 审批策略
* - getEvolutionStatus: 进化指标统计
*/
import { db } from "./db.js";
import { buildFtsTokens, buildSearchQuery } from "./tokenizer.js";
import { auditLog } from "./security.js";
import { logError } from "./logger.js";
import { getErrorMessage } from "./types.js";
// ─── 常量 ────────────────────────────────────────────────
const MAX_CONTENT_LENGTH = 5000;
const MAX_TITLE_LENGTH = 200;
// ─── 类型定义 ────────────────────────────────────────────
export interface Strategy {
id: number;
title: string;
content: string;
category: "experience" | "workflow" | "fix" | "tool_config" | "prompt_template" | "other";
sensitivity: "normal" | "high";
proposer_id: string;
status: "pending" | "approved" | "rejected" | "withdrawn";
approve_reason: string | null;
approved_by: string | null;
approved_at: number | null;
proposed_at: number;
task_id: string | null;
source_trust: number;
apply_count: number;
feedback_count: number;
positive_count: number;
}
export interface StrategyFeedback {
strategy_id: number;
agent_id: string;
feedback: "positive" | "negative" | "neutral";
comment: string | null;
applied: number; // 0/1
created_at: number;
}
export interface StrategyApplication {
strategy_id: number;
agent_id: string;
context: string | null;
result: string | null;
created_at: number;
}
export interface EvolutionStats {
total_experiences: number;
total_strategies: number;
pending_approval: number;
approved_count: number;
rejected_count: number;
total_applications: number;
total_feedback: number;
positive_feedback: number;
approved_rate: number;
top_contributors: Array<{ agent_id: string; count: number; trust_score: number }>;
recent_approved: Strategy[];
}
// ─── sensitivity 判定 ────────────────────────────────────
/**
* Hub 自动判定策略敏感级别(非 Agent 自报告)
*/
function judgeSensitivity(category: string, content: string): "normal" | "high" {
// 高敏感分类:prompt_template 直接判定 high
if (category === "prompt_template") return "high";
// 高敏感关键词检测(结构化模式匹配)
const highPatterns = [
/system[_\s]*prompt/i,
/系统指令/,
/capability[_\s]*declare/i,
/能力声明/,
/permission[_\s]*(change|modify|grant)/i,
/权限变更/,
/role[_\s]*(change|escalat)/i,
];
for (const pattern of highPatterns) {
if (pattern.test(content)) return "high";
}
return "normal";
}
// ─── 服务方法 ────────────────────────────────────────────
/**
* 分享经验(直接 approved,不需审批)
*/
export function shareExperience(
title: string,
content: string,
proposerId: string,
options?: { tags?: string[]; task_id?: string }
): { ok: true; strategy: Strategy } | { ok: false; error: string } {
if (!title || title.trim().length < 3) {
return { ok: false, error: "Title must be at least 3 characters" };
}
if (title.length > MAX_TITLE_LENGTH) {
return { ok: false, error: `Title too long (${title.length} > ${MAX_TITLE_LENGTH})` };
}
if (!content || content.trim().length < 10) {
return { ok: false, error: "Content must be at least 10 characters" };
}
if (content.length > MAX_CONTENT_LENGTH) {
return { ok: false, error: `Content too long (${content.length} > ${MAX_CONTENT_LENGTH})` };
}
const now = Date.now();
const trustScore = getAgentTrustScore(proposerId);
try {
const result = db.prepare(
`INSERT INTO strategies (title, content, category, sensitivity, proposer_id, status, proposed_at, task_id, source_trust)
VALUES (?, ?, 'experience', 'normal', ?, 'approved', ?, ?, ?)`
).run(title.trim(), content.trim(), proposerId, now, options?.task_id ?? null, trustScore);
const strategy = getStrategyById(result.lastInsertRowid as number);
if (!strategy) {
return { ok: false, error: "Failed to retrieve created strategy" };
}
// FTS 索引同步
insertFtsEntry(strategy);
return { ok: true, strategy };
} catch (err: unknown) {
return { ok: false, error: `Failed to share experience: ${getErrorMessage(err)}` };
}
}
/**
* 提议策略(pending,需 admin 审批)
*/
export function proposeStrategy(
title: string,
content: string,
category: "workflow" | "fix" | "tool_config" | "prompt_template" | "other",
proposerId: string,
options?: { task_id?: string }
): { ok: true; strategy: Strategy; sensitivity: string } | { ok: false; error: string } {
if (!title || title.trim().length < 3) {
return { ok: false, error: "Title must be at least 3 characters" };
}
if (title.length > MAX_TITLE_LENGTH) {
return { ok: false, error: `Title too long (${title.length} > ${MAX_TITLE_LENGTH})` };
}
if (!content || content.trim().length < 10) {
return { ok: false, error: "Content must be at least 10 characters" };
}
if (content.length > MAX_CONTENT_LENGTH) {
return { ok: false, error: `Content too long (${content.length} > ${MAX_CONTENT_LENGTH})` };
}
const sensitivity = judgeSensitivity(category, content);
const now = Date.now();
const trustScore = getAgentTrustScore(proposerId);
try {
const result = db.prepare(
`INSERT INTO strategies (title, content, category, sensitivity, proposer_id, status, proposed_at, task_id, source_trust)
VALUES (?, ?, ?, ?, ?, 'pending', ?, ?, ?)`
).run(title.trim(), content.trim(), category, sensitivity, proposerId, now, options?.task_id ?? null, trustScore);
const strategy = getStrategyById(result.lastInsertRowid as number);
if (!strategy) {
return { ok: false, error: "Failed to retrieve created strategy" };
}
return { ok: true, strategy, sensitivity };
} catch (err: unknown) {
return { ok: false, error: `Failed to propose strategy: ${getErrorMessage(err)}` };
}
}
/**
* 策略列表查询
*/
export function listStrategies(options?: {
status?: "pending" | "approved" | "rejected" | "all";
category?: "experience" | "workflow" | "fix" | "tool_config" | "prompt_template" | "other" | "all";
proposer_id?: string;
limit?: number;
}): Strategy[] {
const limit = Math.min(options?.limit ?? 50, 50);
const conditions: string[] = [];
const params: (string | number)[] = [];
if (options?.status && options.status !== "all") {
conditions.push("s.status = ?");
params.push(options.status);
}
if (options?.category && options.category !== "all") {
conditions.push("s.category = ?");
params.push(options.category);
}
if (options?.proposer_id) {
conditions.push("s.proposer_id = ?");
params.push(options.proposer_id);
}
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
params.push(limit);
try {
return db.prepare(
`SELECT s.* FROM strategies s ${where} ORDER BY s.proposed_at DESC LIMIT ?`
).all(...params) as Strategy[];
} catch (err: unknown) {
logError("evolution_listStrategies_error", err);
return [];
}
}
/**
* FTS5 搜索策略(仅返回 approved 策略)
*/
export function searchStrategies(
query: string,
options?: { category?: string; limit?: number }
): Strategy[] {
if (!query || query.trim().length < 2) return [];
const limit = Math.min(options?.limit ?? 20, 20);
const safeQuery = buildSearchQuery(query);
if (!safeQuery) return [];
try {
const conditions: string[] = [`strategies_fts MATCH ?`, `s.status = 'approved'`];
const params: (string | number)[] = [safeQuery];
if (options?.category) {
conditions.push("s.category = ?");
params.push(options.category);
}
params.push(limit);
return db.prepare(
`SELECT s.* FROM strategies s
JOIN strategies_fts ON strategies_fts.rowid = s.id
WHERE ${conditions.join(" AND ")}
ORDER BY rank LIMIT ?`
).all(...params) as Strategy[];
} catch (err: unknown) {
logError("evolution_searchStrategies_error", err);
return [];
}
}
/**
* 采纳策略(仅 approved 策略可采纳)
*/
export function applyStrategy(
strategyId: number,
agentId: string,
options?: { context?: string }
): { ok: true; application_id: number } | { ok: false; error: string } {
// 验证策略存在且 approved
const strategy = getStrategyById(strategyId);
if (!strategy) {
return { ok: false, error: `Strategy ${strategyId} not found` };
}
if (strategy.status !== "approved") {
return { ok: false, error: `Strategy ${strategyId} is not approved (status: ${strategy.status})` };
}
const now = Date.now();
try {
const result = db.prepare(
`INSERT INTO strategy_applications (strategy_id, agent_id, context, created_at)
VALUES (?, ?, ?, ?)`
).run(strategyId, agentId, options?.context ?? null, now);
// apply_count++
db.prepare(`UPDATE strategies SET apply_count = apply_count + 1 WHERE id = ?`).run(strategyId);
return { ok: true, application_id: result.lastInsertRowid as number };
} catch (err: unknown) {
return { ok: false, error: `Failed to apply strategy: ${getErrorMessage(err)}` };
}
}
/**
* 对策略反馈(UNIQUE 防刷)
*/
export function feedbackStrategy(
strategyId: number,
agentId: string,
feedback: "positive" | "negative" | "neutral",
options?: { comment?: string; applied?: boolean }
): { ok: true; feedback_id: number } | { ok: false; error: string } {
// 验证策略存在
const strategy = getStrategyById(strategyId);
if (!strategy) {
return { ok: false, error: `Strategy ${strategyId} not found` };
}
const now = Date.now();
const applied = options?.applied ? 1 : 0;
try {
const result = db.prepare(
`INSERT INTO strategy_feedback (strategy_id, agent_id, feedback, comment, applied, created_at)
VALUES (?, ?, ?, ?, ?, ?)`
).run(strategyId, agentId, feedback, options?.comment ?? null, applied, now);
// 更新计数器
db.prepare(
`UPDATE strategies SET feedback_count = feedback_count + 1,
positive_count = positive_count + CASE WHEN ? = 'positive' THEN 1 ELSE 0 END
WHERE id = ?`
).run(feedback, strategyId);
return { ok: true, feedback_id: result.lastInsertRowid as number };
} catch (err: unknown) {
// UNIQUE 约束冲突 = 重复反馈
if (err instanceof Error && err.message.includes("UNIQUE")) {
return { ok: false, error: "You have already provided feedback for this strategy" };
}
return { ok: false, error: `Failed to submit feedback: ${getErrorMessage(err)}` };
}
}
/**
* admin 审批策略(approve/reject
*/
export function approveStrategy(
strategyId: number,
adminId: string,
action: "approve" | "reject",
reason: string
): { ok: true; strategy: Strategy } | { ok: false; error: string } {
if (!reason || reason.trim().length === 0) {
return { ok: false, error: "Approval reason is required" };
}
if (reason.length > 1000) {
return { ok: false, error: "Reason too long (max 1000 characters)" };
}
const strategy = getStrategyById(strategyId);
if (!strategy) {
return { ok: false, error: `Strategy ${strategyId} not found` };
}
if (strategy.status !== "pending") {
return { ok: false, error: `Strategy ${strategyId} is not pending (status: ${strategy.status})` };
}
const newStatus = action === "approve" ? "approved" : "rejected";
const now = Date.now();
try {
db.prepare(
`UPDATE strategies SET status = ?, approve_reason = ?, approved_by = ?, approved_at = ? WHERE id = ?`
).run(newStatus, reason.trim(), adminId, now, strategyId);
// 如果 approved,同步 FTS 索引
if (action === "approve") {
insertFtsEntry(getStrategyById(strategyId)!);
}
const updated = getStrategyById(strategyId);
return { ok: true, strategy: updated! };
} catch (err: unknown) {
return { ok: false, error: `Failed to approve/reject strategy: ${getErrorMessage(err)}` };
}
}
/**
* 进化指标统计
*/
export function getEvolutionStatus(): EvolutionStats {
try {
const totalExperiences = (db.prepare(
`SELECT COUNT(*) as cnt FROM strategies WHERE category = 'experience'`
).get() as any)?.cnt ?? 0;
const totalStrategies = (db.prepare(
`SELECT COUNT(*) as cnt FROM strategies WHERE category != 'experience'`
).get() as any)?.cnt ?? 0;
const pendingApproval = (db.prepare(
`SELECT COUNT(*) as cnt FROM strategies WHERE status = 'pending'`
).get() as any)?.cnt ?? 0;
const approvedCount = (db.prepare(
`SELECT COUNT(*) as cnt FROM strategies WHERE status = 'approved'`
).get() as any)?.cnt ?? 0;
const rejectedCount = (db.prepare(
`SELECT COUNT(*) as cnt FROM strategies WHERE status = 'rejected'`
).get() as any)?.cnt ?? 0;
const totalApplications = (db.prepare(
`SELECT COUNT(*) as cnt FROM strategy_applications`
).get() as any)?.cnt ?? 0;
const totalFeedback = (db.prepare(
`SELECT COUNT(*) as cnt FROM strategy_feedback`
).get() as any)?.cnt ?? 0;
const positiveFeedback = (db.prepare(
`SELECT COUNT(*) as cnt FROM strategy_feedback WHERE feedback = 'positive'`
).get() as any)?.cnt ?? 0;
const total = totalExperiences + totalStrategies;
const approvedRate = total > 0 ? Math.round((approvedCount / total) * 10000) / 100 : 0;
// Top contributors
const contributors = db.prepare(
`SELECT s.proposer_id as agent_id, COUNT(*) as count, COALESCE(a.trust_score, 50) as trust_score
FROM strategies s
LEFT JOIN agents a ON s.proposer_id = a.agent_id
GROUP BY s.proposer_id
ORDER BY count DESC LIMIT 10`
).all() as Array<{ agent_id: string; count: number; trust_score: number }>;
// Recent approved (last 5)
const recentApproved = db.prepare(
`SELECT * FROM strategies WHERE status = 'approved' ORDER BY approved_at DESC LIMIT 5`
).all() as Strategy[];
return {
total_experiences: totalExperiences,
total_strategies: totalStrategies,
pending_approval: pendingApproval,
approved_count: approvedCount,
rejected_count: rejectedCount,
total_applications: totalApplications,
total_feedback: totalFeedback,
positive_feedback: positiveFeedback,
approved_rate: approvedRate,
top_contributors: contributors,
recent_approved: recentApproved,
};
} catch (err: unknown) {
logError("evolution_getEvolutionStatus_error", err);
return {
total_experiences: 0, total_strategies: 0, pending_approval: 0,
approved_count: 0, rejected_count: 0, total_applications: 0,
total_feedback: 0, positive_feedback: 0, approved_rate: 0,
top_contributors: [], recent_approved: [],
};
}
}
// ═══════════════════════════════════════════════════════════════
// Phase 4b Day 4 — Evolution 分级审批(Tiered Approval
// ═══════════════════════════════════════════════════════════════
// ─── 分级常量 ────────────────────────────────────────────
/** 审批等级 */
export type ApprovalTier = "auto" | "peer" | "admin" | "super";
/** 审批等级判定阈值 */
const TIER_THRESHOLDS = {
auto: { minTrust: 90, maxRisk: "normal", requireHistory: 5 }, // 高信任+低风险+有历史 → 自动通过
peer: { minTrust: 60, maxRisk: "normal", requireHistory: 2 }, // 中等信任+低风险 → peer 审批
admin: { minTrust: 0, maxRisk: "any", requireHistory: 0 }, // 默认 → admin 审批
super: { maxRisk: "high" }, // 高风险 → super 审批(需人工)
};
/** 观察窗口时长:72h */
const OBSERVATION_WINDOW_MS = 72 * 60 * 60 * 1000;
/** 否决窗口时长:48h */
const VETO_WINDOW_MS = 48 * 60 * 60 * 1000;
// ─── 类型导出 ────────────────────────────────────────────
export interface TieredStrategy extends Strategy {
approval_tier: ApprovalTier | null;
observation_start: number | null;
veto_deadline: number | null;
}
export interface TierJudgment {
tier: ApprovalTier;
reason: string;
trust_score: number;
sensitivity: "normal" | "high";
history_count: number;
}
// ─── 分级判定 ────────────────────────────────────────────
/**
* 判定策略审批等级
*
* 4 级 tier 规则:
* 1. super: sensitivity=high → 需人工审批(最高权限)
* 2. auto: trust≥90 + sensitivity=normal + 历史≥5 → 自动通过 + 72h 观察窗口
* 3. peer: trust≥60 + sensitivity=normal + 历史≥2 → peer 审批
* 4. admin: 其他 → admin 审批
*/
export function judgeTier(
proposerId: string,
category: string,
content: string
): TierJudgment {
const sensitivity = judgeSensitivity(category, content);
const trustScore = getAgentTrustScore(proposerId);
const historyCount = getStrategyHistoryCount(proposerId);
// 规则 1: super — 高风险策略
if (sensitivity === "high") {
return {
tier: "super",
reason: `高风险策略(sensitivity=high),需 super 人工审批`,
trust_score: trustScore,
sensitivity,
history_count: historyCount,
};
}
// 规则 2: auto — 高信任+低风险+有历史
if (
trustScore >= TIER_THRESHOLDS.auto.minTrust &&
historyCount >= TIER_THRESHOLDS.auto.requireHistory
) {
return {
tier: "auto",
reason: `高信任策略(trust=${trustScore}, history=${historyCount}),自动通过 + 72h 观察窗口`,
trust_score: trustScore,
sensitivity,
history_count: historyCount,
};
}
// 规则 3: peer — 中等信任+低风险+有少量历史
if (
trustScore >= TIER_THRESHOLDS.peer.minTrust &&
historyCount >= TIER_THRESHOLDS.peer.requireHistory
) {
return {
tier: "peer",
reason: `中等信任策略(trust=${trustScore}, history=${historyCount}),需 peer 审批`,
trust_score: trustScore,
sensitivity,
history_count: historyCount,
};
}
// 规则 4: admin — 默认
return {
tier: "admin",
reason: `默认审批(trust=${trustScore}, history=${historyCount}),需 admin 审批`,
trust_score: trustScore,
sensitivity,
history_count: historyCount,
};
}
/**
* 自动通过策略(auto tier
* 设置 approved + 启动 72h 观察窗口
*/
export function autoApprove(
strategyId: number
): { ok: true; strategy: TieredStrategy } | { ok: false; error: string } {
const strategy = getStrategyById(strategyId);
if (!strategy) {
return { ok: false, error: `Strategy ${strategyId} not found` };
}
if (strategy.status !== "pending") {
return { ok: false, error: `Strategy ${strategyId} is not pending (status: ${strategy.status})` };
}
const now = Date.now();
const vetoDeadline = now + VETO_WINDOW_MS;
// 更新:approved + 观察窗口 + 否决窗口
db.prepare(
`UPDATE strategies SET status='approved', approval_tier='auto',
approved_at=?, approved_by='system:auto',
observation_start=?, veto_deadline=?
WHERE id=?`
).run(now, now, vetoDeadline, strategyId);
// FTS 索引同步
const updated = getStrategyWithTier(strategyId);
if (updated) {
insertFtsEntry(updated);
}
auditLog("auto_approve", "system:auto", String(strategyId), `veto_deadline=${vetoDeadline}`);
if (!updated) {
return { ok: false, error: "Failed to retrieve updated strategy" };
}
return { ok: true, strategy: updated };
}
/**
* 启动观察窗口(peer tier 策略被 peer 审批通过后调用)
* 72h 观察窗口内如果负面反馈超过阈值,策略可被撤回
*/
export function startObservation(
strategyId: number,
approverId: string
): { ok: true; strategy: TieredStrategy; veto_deadline: number } | { ok: false; error: string } {
const strategy = getStrategyById(strategyId);
if (!strategy) {
return { ok: false, error: `Strategy ${strategyId} not found` };
}
if (strategy.status !== "approved") {
return { ok: false, error: `Strategy ${strategyId} is not approved` };
}
const now = Date.now();
const vetoDeadline = now + VETO_WINDOW_MS;
db.prepare(
`UPDATE strategies SET approval_tier='peer', observation_start=?, veto_deadline=?
WHERE id=?`
).run(now, vetoDeadline, strategyId);
auditLog("start_observation", approverId, String(strategyId), `veto_deadline=${vetoDeadline}`);
const updated = getStrategyWithTier(strategyId);
if (!updated) {
return { ok: false, error: "Failed to retrieve updated strategy" };
}
return { ok: true, strategy: updated, veto_deadline: vetoDeadline };
}
/**
* 检查否决窗口(48h
* 在否决窗口内,如果负面反馈超过正面反馈的 50%,任何 admin 可以撤回策略
*/
export function checkVetoWindow(
strategyId: number
): {
in_window: boolean;
can_veto: boolean;
negative_count: number;
positive_count: number;
veto_ratio: number;
veto_deadline: number | null;
observation_start: number | null;
} {
const strategy = db.prepare(
`SELECT veto_deadline, observation_start, positive_count, feedback_count
FROM strategies WHERE id=?`
).get(strategyId) as any;
if (!strategy) {
return {
in_window: false, can_veto: false,
negative_count: 0, positive_count: 0, veto_ratio: 0,
veto_deadline: null, observation_start: null,
};
}
const now = Date.now();
const negativeCount = (strategy.feedback_count ?? 0) - (strategy.positive_count ?? 0);
// 否决窗口判断
const inWindow = strategy.veto_deadline && now < strategy.veto_deadline;
// 否决条件:负面超过正面的 50%
const positiveCount = strategy.positive_count ?? 0;
const vetoRatio = positiveCount > 0 ? negativeCount / positiveCount : (negativeCount > 0 ? 1 : 0);
const canVeto = inWindow && vetoRatio > 0.5;
return {
in_window: !!inWindow,
can_veto: canVeto,
negative_count: negativeCount,
positive_count: positiveCount,
veto_ratio: Math.round(vetoRatio * 100) / 100,
veto_deadline: strategy.veto_deadline,
observation_start: strategy.observation_start,
};
}
/**
* 撤回处于否决窗口内的策略
*/
export function vetoStrategy(
strategyId: number,
adminId: string,
reason: string
): { ok: true; strategy: Strategy } | { ok: false; error: string } {
// 验证否决窗口
const vetoCheck = checkVetoWindow(strategyId);
if (!vetoCheck.in_window) {
return { ok: false, error: "Strategy is not in veto window" };
}
if (!vetoCheck.can_veto) {
return { ok: false, error: `Cannot veto: veto ratio ${vetoCheck.veto_ratio} <= 0.5 threshold` };
}
const now = Date.now();
db.prepare(
`UPDATE strategies SET status='rejected', approval_tier='vetoed',
approve_reason=?, approved_by=?, approved_at=?,
observation_start=null, veto_deadline=null
WHERE id=?`
).run(reason, adminId, now, strategyId);
// 从 FTS 移除
db.prepare(`DELETE FROM strategies_fts WHERE rowid=?`).run(strategyId);
// Phase 5a Day 2: 审计 FTS 索引删除
auditLog("delete_strategy_fts", adminId, String(strategyId), `reason=${reason}`);
auditLog("veto_strategy", adminId, String(strategyId), `reason=${reason}`);
const updated = getStrategyById(strategyId);
if (!updated) {
return { ok: false, error: "Failed to retrieve updated strategy" };
}
return { ok: true, strategy: updated };
}
/**
* 分级策略提议(统一入口)
* 自动判定 tier 并执行对应流程
*/
export function proposeStrategyTiered(
title: string,
content: string,
category: "workflow" | "fix" | "tool_config" | "prompt_template" | "other",
proposerId: string,
options?: { task_id?: string }
): {
ok: true;
strategy: TieredStrategy;
tier: ApprovalTier;
sensitivity: string;
auto_approved: boolean;
veto_deadline: number | null;
} | { ok: false; error: string } {
// 基础校验
if (!title || title.trim().length < 3) {
return { ok: false, error: "Title must be at least 3 characters" };
}
if (title.length > MAX_TITLE_LENGTH) {
return { ok: false, error: `Title too long (${title.length} > ${MAX_TITLE_LENGTH})` };
}
if (!content || content.trim().length < 10) {
return { ok: false, error: "Content must be at least 10 characters" };
}
if (content.length > MAX_CONTENT_LENGTH) {
return { ok: false, error: `Content too long (${content.length} > ${MAX_CONTENT_LENGTH})` };
}
// 判定分级
const judgment = judgeTier(proposerId, category, content);
const sensitivity = judgment.sensitivity;
const now = Date.now();
const trustScore = getAgentTrustScore(proposerId);
try {
// 创建策略(初始 pending
const result = db.prepare(
`INSERT INTO strategies (title, content, category, sensitivity, proposer_id,
status, proposed_at, task_id, source_trust, approval_tier)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
).run(
title.trim(), content.trim(), category, sensitivity, proposerId,
"pending", now, options?.task_id ?? null, trustScore, judgment.tier
);
const strategyId = result.lastInsertRowid as number;
let autoApproved = false;
let vetoDeadline: number | null = null;
if (judgment.tier === "auto") {
// auto tier: 自动通过 + 启动观察窗口
const approveResult = autoApprove(strategyId);
if (approveResult.ok) {
autoApproved = true;
vetoDeadline = approveResult.strategy.veto_deadline;
}
}
// peer / admin / super: 保持 pending 状态
const strategy = getStrategyWithTier(strategyId);
if (!strategy) {
return { ok: false, error: "Failed to retrieve created strategy" };
}
return {
ok: true,
strategy,
tier: judgment.tier,
sensitivity,
auto_approved: autoApproved,
veto_deadline: vetoDeadline,
};
} catch (err: unknown) {
return { ok: false, error: `Failed to propose strategy: ${getErrorMessage(err)}` };
}
}
// ─── 内部辅助函数 ────────────────────────────────────────
function getStrategyById(id: number): Strategy | undefined {
return db.prepare(`SELECT * FROM strategies WHERE id = ?`).get(id) as Strategy | undefined;
}
function getStrategyWithTier(id: number): TieredStrategy | undefined {
return db.prepare(`SELECT * FROM strategies WHERE id = ?`).get(id) as TieredStrategy | undefined;
}
function getStrategyHistoryCount(proposerId: string): number {
try {
const row = db.prepare(
`SELECT COUNT(*) as cnt FROM strategies WHERE proposer_id = ? AND status = 'approved'`
).get(proposerId) as any;
return row?.cnt ?? 0;
} catch {
return 0;
}
}
function getAgentTrustScore(agentId: string): number {
try {
const row = db.prepare(`SELECT trust_score FROM agents WHERE agent_id = ?`).get(agentId) as any;
return row?.trust_score ?? 50;
} catch {
return 50;
}
}
function insertFtsEntry(strategy: Strategy): void {
try {
const tokens = buildFtsTokens(strategy.title, strategy.content);
db.prepare(
`INSERT INTO strategies_fts (rowid, title, content, category) VALUES (?, ?, ?, ?)`
).run(strategy.id, strategy.title, tokens, strategy.category);
} catch (err: unknown) {
logError("evolution_fts_insert_error", err);
}
}
// ─── Phase 2.2: 策略采纳闭环 ─────────────────────────────────
/**
* 提供反馈(UPSERT 版本)— 用于自动创建反馈占位和后续更新
* 与 feedbackStrategy 不同:使用 ON CONFLICT DO UPDATE 而非拒绝重复
*/
export function provideFeedback(params: {
strategyId: number;
agentId: string;
feedback: string; // 'positive' | 'negative' | 'neutral'
comment?: string;
applied?: number;
}): { id: number } {
const now = Date.now();
try {
const result = db.prepare(`
INSERT INTO strategy_feedback (strategy_id, agent_id, feedback, comment, applied, created_at)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(strategy_id, agent_id) DO UPDATE SET
feedback = excluded.feedback,
comment = excluded.comment,
applied = excluded.applied
`).run(
params.strategyId,
params.agentId,
params.feedback,
params.comment || null,
params.applied ?? 0,
now
);
return { id: result.lastInsertRowid as number };
} catch (err: unknown) {
throw new Error(`创建反馈失败: ${getErrorMessage(err)}`);
}
}
/**
* 自动评分已采纳策略
* 将 7 天内仍为 neutral 反馈的策略降为 negative(无实际效果证据)
* 应由 cron 或清理任务定期调用
*/
export function scoreAppliedStrategies(): {
scored: number;
details: Array<{ strategyId: number; title: string; action: string }>;
} {
const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
// 查找 7 天前创建的 neutral 反馈
const staleFeedbacks = db.prepare(`
SELECT sf.strategy_id, sf.agent_id, sf.id as feedback_id, s.title
FROM strategy_feedback sf
JOIN strategies s ON s.id = sf.strategy_id
WHERE sf.feedback = 'neutral'
AND sf.created_at < ?
AND s.approved = 1
`).all(sevenDaysAgo) as Array<{
strategy_id: number;
agent_id: string;
feedback_id: number;
title: string;
}>;
const details: Array<{ strategyId: number; title: string; action: string }> = [];
let scored = 0;
for (const fb of staleFeedbacks) {
// 检查是否有其他 agent 给了非 neutral 反馈
const otherFeedback = db.prepare(`
SELECT feedback FROM strategy_feedback
WHERE strategy_id = ? AND agent_id != ? AND feedback != 'neutral'
`).all(fb.strategy_id, fb.agent_id);
if (otherFeedback.length === 0) {
// 无人提供有效反馈 → 降分为 negative
db.prepare(
`UPDATE strategy_feedback SET feedback = 'negative', comment = ? WHERE id = ?`
).run("自动降分:采纳后 7 天内无实际效果反馈", fb.feedback_id);
// 同步减少 positive_count(如果之前因 neutral 未增加则无需操作)
details.push({
strategyId: fb.strategy_id,
title: fb.title,
action: "neutral→negative (7天无反馈)",
});
scored++;
}
}
if (scored > 0) {
logError("evolution_auto_score", new Error(`Auto-scored ${scored} stale feedbacks`));
}
return { scored, details };
}
+117
View File
@@ -0,0 +1,117 @@
export declare const HEARTBEAT_CONFIG: {
TRUST_SCORE_INCREMENT_INTERVAL: number;
TRUST_SCORE_MAX: number;
};
export interface AgentInfo {
agent_id: string;
name: string;
role: "admin" | "member" | "group_admin";
status: "online" | "offline";
trust_score: number;
last_heartbeat: number | null;
created_at: number;
capabilities?: string[];
}
/**
* 注册新 Agent
* @returns { agentId, apiToken } 或错误信息
*/
export declare function registerAgent(inviteCode: string, name: string, capabilities?: string[]): {
success: boolean;
agentId?: string;
apiToken?: string;
role?: string;
error?: string;
};
/**
* 处理 Agent 心跳
* 连续在线心跳每 TRUST_SCORE_INCREMENT_INTERVAL 次自动增加 1 点 trust_score(上限 TRUST_SCORE_MAX
* @returns 更新后的状态
*/
export declare function heartbeat(agentId: string): {
success: boolean;
status: "online" | "offline";
last_heartbeat: number;
trust_score?: number;
error?: string;
};
/**
* 查询已注册的 Agent 列表
*/
export declare function queryAgents(filters?: {
status?: "online" | "offline" | "all";
role?: "admin" | "member" | "group_admin";
capability?: string;
}): AgentInfo[];
/**
* 查询单个 Agent 信息
*/
export declare function getAgent(agentId: string): AgentInfo | null;
/**
* 启动心跳超时检测定时器
* - 90s 无心跳 → 标记 offline
* - 5min 无心跳 → 通知其他在线 Agent
*/
export declare function startHeartbeatMonitor(onAgentOffline?: (agentId: string) => void): void;
/**
* 停止心跳超时检测
*/
export declare function stopHeartbeatMonitor(): void;
/**
* 清除离线通知标记(Agent 重新上线时调用)
*/
export declare function clearOfflineNotification(agentId: string): void;
/**
* 获取 Agent 信任分
*/
export declare function getAgentTrustScore(agentId: string): number;
/**
* 更新 Agent 信任分(admin only
* @returns 更新后的信任分,或 nullAgent 不存在)
*/
export declare function updateAgentTrustScore(agentId: string, delta: number, operatorId?: string): {
ok: true;
new_score: number;
} | {
ok: false;
error: string;
};
/**
* 设置 Agent 角色(admin only
* 支持 admin / member / group_admin 三种角色
* group_admin 需要同时设置 managed_group_id
*/
export declare function setAgentRole(agentId: string, newRole: "admin" | "member" | "group_admin", operatorId: string, managedGroupId?: string): {
ok: true;
old_role: string;
new_role: string;
managed_group_id: string | null;
} | {
ok: false;
error: string;
};
/**
* 获取 Agent 角色
*/
export declare function getAgentRole(agentId: string): string | null;
/**
* 获取 Agent 的 managed_group_id
*/
export declare function getAgentManagedGroup(agentId: string): string | null;
/**
* 获取心跳超时配置(用于测试)
*/
export declare function getHeartbeatConfig(): {
onlineThreshold: number;
notifyThreshold: number;
checkInterval: number;
};
/**
* 解析 Agent 标识符为完整 agent_id。
* 支持:
* 1. 完整 agent_id(已注册则返回)
* 2. 已知别名(workbuddy / hermes / qclaw,大小写不敏感)
* 3. agent_id 子串匹配(大小写不敏感)
* 返回完整 agent_id 或 null(未找到)
*/
export declare function resolveAgentId(input: string): string | null;
+379
View File
@@ -0,0 +1,379 @@
/**
* identity.ts — Identity Service
* Agent 注册(邀请码)、心跳检测、在线状态查询
*
* Day 3 核心:心跳超时检测定时器
* - 90s 无心跳 → 自动标记 offline
* - 5min 无心跳 → 通知其他在线 Agent
*
* 踩坑经验:
* - better-sqlite3 boolean 绑定必须用 1/0
* - better-sqlite3 undefined 必须用 null
*/
import { randomUUID, randomBytes } from "crypto";
import { db } from "./db.js";
import { generateToken, sha256, verifyInviteCode, markInviteCodeUsed, auditLog, } from "./security.js";
import { pushToAgent, onlineAgents } from "./sse.js";
import { logger } from "./logger.js";
// ─── 常量 ────────────────────────────────────────────────
const HEARTBEAT_ONLINE_THRESHOLD = parseInt(process.env.HEARTBEAT_ONLINE_THRESHOLD ?? "90000", 10); // 90s → offline
const HEARTBEAT_NOTIFY_THRESHOLD = parseInt(process.env.HEARTBEAT_NOTIFY_THRESHOLD ?? "300000", 10); // 5min → 通知
const HEARTBEAT_CHECK_INTERVAL = parseInt(process.env.HEARTBEAT_CHECK_INTERVAL ?? "30000", 10); // 30s 检查一次
// Phase 1.2: 连续心跳信任分增长配置
export const HEARTBEAT_CONFIG = {
TRUST_SCORE_INCREMENT_INTERVAL: 3, // 每 3 次连续心跳 +1 分
TRUST_SCORE_MAX: 100, // trust_score 上限
};
// 连续心跳计数器:agentId → 连续在线心跳次数
const heartbeatCounters = new Map();
// ─── Agent 注册 ──────────────────────────────────────────
/**
* 注册新 Agent
* @returns { agentId, apiToken } 或错误信息
*/
export function registerAgent(inviteCode, name, capabilities = []) {
// 1. 验证邀请码
const role = verifyInviteCode(inviteCode);
if (!role) {
return { success: false, error: "Invalid or expired invite code" };
}
// 2. 标记邀请码已使用
markInviteCodeUsed(inviteCode);
// 3. 创建 Agent 记录
const agentId = `agent_${randomBytes(4).toString("hex")}_${Date.now()}`;
const now = Date.now();
try {
db.prepare(`INSERT INTO agents (agent_id, name, role, status, last_heartbeat, created_at)
VALUES (?, ?, ?, 'offline', NULL, ?)`).run(agentId, name, role, now);
}
catch (err) {
// 可能 agent_id 冲突(极低概率),重试一次
const retryId = `agent_${randomBytes(4).toString("hex")}_${Date.now()}`;
db.prepare(`INSERT INTO agents (agent_id, name, role, status, last_heartbeat, created_at)
VALUES (?, ?, ?, 'offline', NULL, ?)`).run(retryId, name, role, now);
return registerAgentWithId(retryId, name, role, capabilities, now);
}
return registerAgentWithId(agentId, name, role, capabilities, now);
}
function registerAgentWithId(agentId, name, role, capabilities, now) {
// 4. 生成 API Token
const plainToken = generateToken();
const tokenHash = sha256(plainToken);
const tokenId = `token_${agentId}_${randomBytes(4).toString("hex")}`;
db.prepare(`INSERT INTO auth_tokens (token_id, token_type, token_value, agent_id, role, used, created_at)
VALUES (?, 'api_token', ?, ?, ?, 1, ?)`).run(tokenId, tokenHash, agentId, role, now);
// 4.5 同步 token hash 到 agents 表
db.prepare(`UPDATE agents SET api_token=? WHERE agent_id=?`).run(tokenHash, agentId);
// 5. 存储能力(如果有)
for (const cap of capabilities) {
db.prepare(`INSERT INTO agent_capabilities (id, agent_id, capability, verified, created_at)
VALUES (?, ?, ?, 0, ?)`).run(randomUUID(), agentId, cap, now);
}
auditLog("agent_registered", agentId, name, `role=${role}, capabilities=${capabilities.length}`);
return { success: true, agentId, apiToken: plainToken, role };
}
// ─── 心跳 ────────────────────────────────────────────────
/**
* 处理 Agent 心跳
* 连续在线心跳每 TRUST_SCORE_INCREMENT_INTERVAL 次自动增加 1 点 trust_score(上限 TRUST_SCORE_MAX
* @returns 更新后的状态
*/
export function heartbeat(agentId) {
// 检查 Agent 是否存在
const agent = db
.prepare(`SELECT agent_id, trust_score FROM agents WHERE agent_id=?`)
.get(agentId);
if (!agent) {
return { success: false, status: "offline", last_heartbeat: 0, error: "Agent not found" };
}
const now = Date.now();
db.prepare(`UPDATE agents SET status='online', last_heartbeat=? WHERE agent_id=?`).run(now, agentId);
// Phase 1.2: 连续心跳信任分增长
const counter = (heartbeatCounters.get(agentId) ?? 0) + 1;
heartbeatCounters.set(agentId, counter);
if (counter % HEARTBEAT_CONFIG.TRUST_SCORE_INCREMENT_INTERVAL === 0) {
// 每 3 次连续心跳 +1 trust_score
db.prepare(`UPDATE agents SET trust_score = MIN(trust_score + 1, ?) WHERE agent_id = ?`).run(HEARTBEAT_CONFIG.TRUST_SCORE_MAX, agentId);
}
const newTrustScore = db.prepare(`SELECT trust_score FROM agents WHERE agent_id=?`).get(agentId)?.trust_score ?? 50;
return { success: true, status: "online", last_heartbeat: now, trust_score: newTrustScore };
}
// ─── 查询 Agent ──────────────────────────────────────────
/**
* 查询已注册的 Agent 列表
*/
export function queryAgents(filters) {
let sql = `SELECT DISTINCT a.agent_id, a.name, a.role, a.status, a.trust_score, a.last_heartbeat, a.created_at
FROM agents a`;
const params = [];
if (filters?.capability) {
sql += ` LEFT JOIN agent_capabilities c ON a.agent_id = c.agent_id`;
}
const conditions = [];
if (filters?.status && filters.status !== "all") {
conditions.push("a.status = ?");
params.push(filters.status);
}
if (filters?.role) {
conditions.push("a.role = ?");
params.push(filters.role);
}
if (filters?.capability) {
conditions.push("c.capability = ?");
params.push(filters.capability);
}
if (conditions.length > 0) {
sql += " WHERE " + conditions.join(" AND ");
}
sql += " ORDER BY a.created_at ASC";
const rows = db.prepare(sql).all(...params);
return rows.map(row => ({
agent_id: row.agent_id,
name: row.name,
role: row.role,
status: row.status,
trust_score: row.trust_score ?? 50,
last_heartbeat: row.last_heartbeat,
created_at: row.created_at,
}));
}
/**
* 查询单个 Agent 信息
*/
export function getAgent(agentId) {
const row = db
.prepare(`SELECT * FROM agents WHERE agent_id=?`)
.get(agentId);
if (!row)
return null;
// 获取能力列表
const caps = db
.prepare(`SELECT capability FROM agent_capabilities WHERE agent_id=?`)
.all(agentId);
return {
agent_id: row.agent_id,
name: row.name,
role: row.role,
status: row.status,
trust_score: row.trust_score ?? 50,
last_heartbeat: row.last_heartbeat,
created_at: row.created_at,
capabilities: caps.map((c) => c.capability),
};
}
// ─── 心跳超时检测定时器(Day 3 核心) ────────────────────
let heartbeatTimer = null;
let offlineNotifiedSet = new Set(); // 已发送离线通知的 Agent
/**
* 启动心跳超时检测定时器
* - 90s 无心跳 → 标记 offline
* - 5min 无心跳 → 通知其他在线 Agent
*/
export function startHeartbeatMonitor(onAgentOffline) {
if (heartbeatTimer) {
clearInterval(heartbeatTimer);
}
logger.info("HeartbeatMonitor started", { module: "heartbeat", interval_ms: HEARTBEAT_CHECK_INTERVAL });
heartbeatTimer = setInterval(() => {
const now = Date.now();
// 查找所有 status='online' 但心跳超时的 Agent
const staleAgents = db
.prepare(`SELECT agent_id, name, last_heartbeat FROM agents
WHERE status='online' AND last_heartbeat IS NOT NULL AND last_heartbeat < ?
ORDER BY last_heartbeat ASC`)
.all(now - HEARTBEAT_ONLINE_THRESHOLD);
for (const agent of staleAgents) {
const elapsed = now - agent.last_heartbeat;
// 90s → 标记 offline
if (elapsed >= HEARTBEAT_ONLINE_THRESHOLD) {
db.prepare(`UPDATE agents SET status='offline' WHERE agent_id=?`).run(agent.agent_id);
// Phase 1.2: 重置连续心跳计数器
heartbeatCounters.delete(agent.agent_id);
logger.info("agent_offline_marked", {
module: "heartbeat",
agent_id: agent.agent_id,
name: agent.name,
elapsed_s: Math.round(elapsed / 1000),
});
auditLog("agent_offline", agent.agent_id, agent.name, `heartbeat_timeout: ${Math.round(elapsed / 1000)}s`);
// 回调
if (onAgentOffline) {
onAgentOffline(agent.agent_id);
}
}
// 5min → 通知其他在线 Agent
if (elapsed >= HEARTBEAT_NOTIFY_THRESHOLD && !offlineNotifiedSet.has(agent.agent_id)) {
offlineNotifiedSet.add(agent.agent_id);
const onlineList = onlineAgents().filter(id => id !== agent.agent_id);
for (const onlineId of onlineList) {
pushToAgent(onlineId, {
event: "agent_offline",
agent_id: agent.agent_id,
name: agent.name,
last_heartbeat: agent.last_heartbeat,
offline_duration: Math.round(elapsed / 1000),
message: `${agent.name} (${agent.agent_id}) 已离线超过 5 分钟`,
});
}
logger.info("agent_offline_notified", {
module: "heartbeat",
agent_id: agent.agent_id,
notified_count: onlineList.length,
});
}
}
}, HEARTBEAT_CHECK_INTERVAL);
}
/**
* 停止心跳超时检测
*/
export function stopHeartbeatMonitor() {
if (heartbeatTimer) {
clearInterval(heartbeatTimer);
heartbeatTimer = null;
offlineNotifiedSet.clear();
logger.info("HeartbeatMonitor stopped", { module: "heartbeat" });
}
}
/**
* 清除离线通知标记(Agent 重新上线时调用)
*/
export function clearOfflineNotification(agentId) {
offlineNotifiedSet.delete(agentId);
}
// ─── Trust Score 管理(Phase 2 Day 4) ───────────────────
const TRUST_SCORE_MIN = 0;
const TRUST_SCORE_MAX = 100;
const TRUST_SCORE_DEFAULT = 50;
/**
* 获取 Agent 信任分
*/
export function getAgentTrustScore(agentId) {
const row = db
.prepare(`SELECT trust_score FROM agents WHERE agent_id=?`)
.get(agentId);
return row ? row.trust_score : TRUST_SCORE_DEFAULT;
}
/**
* 更新 Agent 信任分(admin only
* @returns 更新后的信任分,或 nullAgent 不存在)
*/
export function updateAgentTrustScore(agentId, delta, operatorId) {
const row = db
.prepare(`SELECT trust_score FROM agents WHERE agent_id=?`)
.get(agentId);
if (!row) {
return { ok: false, error: `Agent ${agentId} not found` };
}
const current = row.trust_score;
const newScore = Math.max(TRUST_SCORE_MIN, Math.min(TRUST_SCORE_MAX, current + delta));
db.prepare(`UPDATE agents SET trust_score=? WHERE agent_id=?`).run(newScore, agentId);
auditLog("trust_score_updated", operatorId ?? null, agentId, `${current}${newScore} (delta=${delta})`);
return { ok: true, new_score: newScore };
}
// ─── Phase 5a: 角色管理 ──────────────────────────────────
/**
* 设置 Agent 角色(admin only
* 支持 admin / member / group_admin 三种角色
* group_admin 需要同时设置 managed_group_id
*/
export function setAgentRole(agentId, newRole, operatorId, managedGroupId) {
// 验证目标 Agent 存在
const agent = db
.prepare(`SELECT role, managed_group_id FROM agents WHERE agent_id=?`)
.get(agentId);
if (!agent) {
return { ok: false, error: `Agent ${agentId} not found` };
}
const oldRole = agent.role;
// 不能修改自己
if (agentId === operatorId) {
return { ok: false, error: "Cannot modify own role" };
}
// 非 admin 不能被设为 admin(防止 member 提权)
if (newRole === "admin" && oldRole !== "admin") {
return { ok: false, error: "Only existing admin can be promoted to admin" };
}
// group_admin 必须有 managed_group_id
const groupId = newRole === "group_admin" ? (managedGroupId ?? null) : null;
// 更新 agents 表
db.prepare(`UPDATE agents SET role=?, managed_group_id=? WHERE agent_id=?`)
.run(newRole, groupId, agentId);
// 同步更新 auth_tokens 表中的 role(保持一致性)
db.prepare(`UPDATE auth_tokens SET role=? WHERE agent_id=? AND token_type='api_token' AND revoked_at IS NULL`)
.run(newRole, agentId);
auditLog("role_changed", operatorId, agentId, `${oldRole}${newRole}${groupId ? `, group=${groupId}` : ""}`);
return { ok: true, old_role: oldRole, new_role: newRole, managed_group_id: groupId };
}
/**
* 获取 Agent 角色
*/
export function getAgentRole(agentId) {
const row = db
.prepare(`SELECT role FROM agents WHERE agent_id=?`)
.get(agentId);
return row?.role ?? null;
}
/**
* 获取 Agent 的 managed_group_id
*/
export function getAgentManagedGroup(agentId) {
const row = db
.prepare(`SELECT managed_group_id FROM agents WHERE agent_id=?`)
.get(agentId);
return row?.managed_group_id ?? null;
}
/**
* 获取心跳超时配置(用于测试)
*/
export function getHeartbeatConfig() {
return {
onlineThreshold: HEARTBEAT_ONLINE_THRESHOLD,
notifyThreshold: HEARTBEAT_NOTIFY_THRESHOLD,
checkInterval: HEARTBEAT_CHECK_INTERVAL,
};
}
// ─────────────────────────────────────────────────────────
// from_agent 格式规范化(Phase 2.1
// ─────────────────────────────────────────────────────────
/**
* 已知的 Agent 别名映射(兼容历史消息格式)
* 规范化后不再需要,但用于迁移阶段
*/
const AGENT_ALIAS_MAP = {
'workbuddy': 'agent_workbuddy_a3f7c2e1_1777300825754',
'hermes': 'agent_hermes_54cfe58b_1777132066111',
'qclaw': 'agent_1c11a7bd_1777129814251',
};
/**
* 解析 Agent 标识符为完整 agent_id。
* 支持:
* 1. 完整 agent_id(已注册则返回)
* 2. 已知别名(workbuddy / hermes / qclaw,大小写不敏感)
* 3. agent_id 子串匹配(大小写不敏感)
* 返回完整 agent_id 或 null(未找到)
*/
export function resolveAgentId(input) {
const trimmed = input.trim();
if (!trimmed)
return null;
// 1. 直接以 agent_ 开头 → 验证是否存在于数据库
if (trimmed.startsWith('agent_')) {
return getAgent(trimmed) ? trimmed : null;
}
// 2. 已知别名映射
const aliasKey = trimmed.toLowerCase();
if (AGENT_ALIAS_MAP[aliasKey]) {
return getAgent(AGENT_ALIAS_MAP[aliasKey]) ? AGENT_ALIAS_MAP[aliasKey] : null;
}
// 3. 子串匹配(agent_id 包含输入,大小写不敏感)
const agents = queryAgents({});
const lower = trimmed.toLowerCase();
for (const agent of agents) {
if (agent.agent_id.toLowerCase().includes(lower)) {
return agent.agent_id;
}
}
return null;
}
//# sourceMappingURL=identity.js.map
+528
View File
@@ -0,0 +1,528 @@
/**
* identity.ts — Identity Service
* Agent 注册(邀请码)、心跳检测、在线状态查询
*
* Day 3 核心:心跳超时检测定时器
* - 90s 无心跳 → 自动标记 offline
* - 5min 无心跳 → 通知其他在线 Agent
*
* 踩坑经验:
* - better-sqlite3 boolean 绑定必须用 1/0
* - better-sqlite3 undefined 必须用 null
*/
import { randomUUID, randomBytes } from "crypto";
import { db } from "./db.js";
import {
generateToken,
sha256,
createInviteCode,
verifyInviteCode,
markInviteCodeUsed,
auditLog,
type AuthContext,
} from "./security.js";
import { pushToAgent, onlineAgents } from "./sse.js";
import { logger } from "./logger.js";
import type { AgentRow, AgentCapabilityRow } from "./types.js";
import { getErrorMessage } from "./types.js";
// ─── 常量 ────────────────────────────────────────────────
const HEARTBEAT_ONLINE_THRESHOLD = parseInt(process.env.HEARTBEAT_ONLINE_THRESHOLD ?? "90000", 10); // 90s → offline
const HEARTBEAT_NOTIFY_THRESHOLD = parseInt(process.env.HEARTBEAT_NOTIFY_THRESHOLD ?? "300000", 10); // 5min → 通知
const HEARTBEAT_CHECK_INTERVAL = parseInt(process.env.HEARTBEAT_CHECK_INTERVAL ?? "30000", 10); // 30s 检查一次
// Phase 1.2: 连续心跳信任分增长配置
export const HEARTBEAT_CONFIG = {
TRUST_SCORE_INCREMENT_INTERVAL: 3, // 每 3 次连续心跳 +1 分
TRUST_SCORE_MAX: 100, // trust_score 上限
};
// 连续心跳计数器:agentId → 连续在线心跳次数
const heartbeatCounters = new Map<string, number>();
// ─── 类型 ────────────────────────────────────────────────
export interface AgentInfo {
agent_id: string;
name: string;
role: "admin" | "member" | "group_admin" | "superadmin";
status: "online" | "offline";
trust_score: number;
last_heartbeat: number | null;
created_at: number;
capabilities?: string[];
}
// ─── Agent 注册 ──────────────────────────────────────────
/**
* 注册新 Agent
* @returns { agentId, apiToken } 或错误信息
*/
export function registerAgent(
inviteCode: string,
name: string,
capabilities: string[] = []
): { success: boolean; agentId?: string; apiToken?: string; role?: string; error?: string } {
// 1. 验证邀请码
const role = verifyInviteCode(inviteCode);
if (!role) {
return { success: false, error: "Invalid or expired invite code" };
}
// 2. 标记邀请码已使用
markInviteCodeUsed(inviteCode);
// 3. 创建 Agent 记录
const agentId = `agent_${randomBytes(4).toString("hex")}_${Date.now()}`;
const now = Date.now();
try {
db.prepare(
`INSERT INTO agents (agent_id, name, role, status, last_heartbeat, created_at)
VALUES (?, ?, ?, 'offline', NULL, ?)`
).run(agentId, name, role, now);
} catch (err: unknown) {
// 可能 agent_id 冲突(极低概率),重试一次
const retryId = `agent_${randomBytes(4).toString("hex")}_${Date.now()}`;
db.prepare(
`INSERT INTO agents (agent_id, name, role, status, last_heartbeat, created_at)
VALUES (?, ?, ?, 'offline', NULL, ?)`
).run(retryId, name, role, now);
return registerAgentWithId(retryId, name, role, capabilities, now);
}
return registerAgentWithId(agentId, name, role, capabilities, now);
}
function registerAgentWithId(
agentId: string,
name: string,
role: "admin" | "member",
capabilities: string[],
now: number
): { success: boolean; agentId: string; apiToken: string; role: string } {
// 4. 生成 API Token
const plainToken = generateToken();
const tokenHash = sha256(plainToken);
const tokenId = `token_${agentId}_${randomBytes(4).toString("hex")}`;
db.prepare(
`INSERT INTO auth_tokens (token_id, token_type, token_value, agent_id, role, used, created_at)
VALUES (?, 'api_token', ?, ?, ?, 1, ?)`
).run(tokenId, tokenHash, agentId, role, now);
// 4.5 同步 token hash 到 agents 表
db.prepare(`UPDATE agents SET api_token=? WHERE agent_id=?`).run(tokenHash, agentId);
// 5. 存储能力(如果有)
for (const cap of capabilities) {
db.prepare(
`INSERT INTO agent_capabilities (id, agent_id, capability, verified, created_at)
VALUES (?, ?, ?, 0, ?)`
).run(randomUUID(), agentId, cap, now);
}
auditLog("agent_registered", agentId, name, `role=${role}, capabilities=${capabilities.length}`);
return { success: true, agentId, apiToken: plainToken, role };
}
// ─── 心跳 ────────────────────────────────────────────────
/**
* 处理 Agent 心跳
* 连续在线心跳每 TRUST_SCORE_INCREMENT_INTERVAL 次自动增加 1 点 trust_score(上限 TRUST_SCORE_MAX
* @returns 更新后的状态
*/
export function heartbeat(agentId: string): {
success: boolean;
status: "online" | "offline";
last_heartbeat: number;
trust_score?: number;
error?: string;
} {
// 检查 Agent 是否存在
const agent = db
.prepare(`SELECT agent_id, trust_score FROM agents WHERE agent_id=?`)
.get(agentId) as Pick<AgentRow, "agent_id" | "trust_score"> | undefined;
if (!agent) {
return { success: false, status: "offline", last_heartbeat: 0, error: "Agent not found" };
}
const now = Date.now();
db.prepare(
`UPDATE agents SET status='online', last_heartbeat=? WHERE agent_id=?`
).run(now, agentId);
// Phase 1.2: 连续心跳信任分增长
const counter = (heartbeatCounters.get(agentId) ?? 0) + 1;
heartbeatCounters.set(agentId, counter);
if (counter % HEARTBEAT_CONFIG.TRUST_SCORE_INCREMENT_INTERVAL === 0) {
// 每 3 次连续心跳 +1 trust_score
db.prepare(
`UPDATE agents SET trust_score = MIN(trust_score + 1, ?) WHERE agent_id = ?`
).run(HEARTBEAT_CONFIG.TRUST_SCORE_MAX, agentId);
}
const newTrustScore = (db.prepare(`SELECT trust_score FROM agents WHERE agent_id=?`).get(agentId) as any)?.trust_score ?? 50;
return { success: true, status: "online", last_heartbeat: now, trust_score: newTrustScore };
}
// ─── 查询 Agent ──────────────────────────────────────────
/**
* 查询已注册的 Agent 列表
*/
export function queryAgents(filters?: {
status?: "online" | "offline" | "all";
role?: "admin" | "member" | "group_admin";
capability?: string;
}): AgentInfo[] {
let sql = `SELECT DISTINCT a.agent_id, a.name, a.role, a.status, a.trust_score, a.last_heartbeat, a.created_at
FROM agents a`;
const params: (string | number)[] = [];
if (filters?.capability) {
sql += ` LEFT JOIN agent_capabilities c ON a.agent_id = c.agent_id`;
}
const conditions: string[] = [];
if (filters?.status && filters.status !== "all") {
conditions.push("a.status = ?");
params.push(filters.status);
}
if (filters?.role) {
conditions.push("a.role = ?");
params.push(filters.role);
}
if (filters?.capability) {
conditions.push("c.capability = ?");
params.push(filters.capability);
}
if (conditions.length > 0) {
sql += " WHERE " + conditions.join(" AND ");
}
sql += " ORDER BY a.created_at ASC";
const rows = db.prepare(sql).all(...params) as AgentRow[];
return rows.map(row => ({
agent_id: row.agent_id,
name: row.name,
role: row.role,
status: row.status,
trust_score: row.trust_score ?? 50,
last_heartbeat: row.last_heartbeat ?? null,
created_at: row.created_at,
}));
}
/**
* 查询单个 Agent 信息
*/
export function getAgent(agentId: string): AgentInfo | null {
const row = db
.prepare(`SELECT * FROM agents WHERE agent_id=?`)
.get(agentId) as AgentRow | undefined;
if (!row) return null;
// 获取能力列表
const caps = db
.prepare(`SELECT capability FROM agent_capabilities WHERE agent_id=?`)
.all(agentId) as AgentCapabilityRow[];
return {
agent_id: row.agent_id,
name: row.name,
role: row.role,
status: row.status,
trust_score: row.trust_score ?? 50,
last_heartbeat: row.last_heartbeat ?? null,
created_at: row.created_at,
capabilities: caps.map((c) => c.capability),
};
}
// ─── 心跳超时检测定时器(Day 3 核心) ────────────────────
let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
let offlineNotifiedSet = new Set<string>(); // 已发送离线通知的 Agent
/**
* 启动心跳超时检测定时器
* - 90s 无心跳 → 标记 offline
* - 5min 无心跳 → 通知其他在线 Agent
*/
export function startHeartbeatMonitor(onAgentOffline?: (agentId: string) => void): void {
if (heartbeatTimer) {
clearInterval(heartbeatTimer);
}
logger.info("HeartbeatMonitor started", { module: "heartbeat", interval_ms: HEARTBEAT_CHECK_INTERVAL });
heartbeatTimer = setInterval(() => {
const now = Date.now();
// 查找所有 status='online' 但心跳超时的 Agent
const staleAgents = db
.prepare(
`SELECT agent_id, name, last_heartbeat FROM agents
WHERE status='online' AND last_heartbeat IS NOT NULL AND last_heartbeat < ?
ORDER BY last_heartbeat ASC`
)
.all(now - HEARTBEAT_ONLINE_THRESHOLD) as any[];
for (const agent of staleAgents) {
const elapsed = now - agent.last_heartbeat;
// 90s → 标记 offline
if (elapsed >= HEARTBEAT_ONLINE_THRESHOLD) {
db.prepare(
`UPDATE agents SET status='offline' WHERE agent_id=?`
).run(agent.agent_id);
// Phase 1.2: 重置连续心跳计数器
heartbeatCounters.delete(agent.agent_id);
logger.info("agent_offline_marked", {
module: "heartbeat",
agent_id: agent.agent_id,
name: agent.name,
elapsed_s: Math.round(elapsed / 1000),
});
auditLog("agent_offline", agent.agent_id, agent.name,
`heartbeat_timeout: ${Math.round(elapsed / 1000)}s`);
// 回调
if (onAgentOffline) {
onAgentOffline(agent.agent_id);
}
}
// 5min → 通知其他在线 Agent
if (elapsed >= HEARTBEAT_NOTIFY_THRESHOLD && !offlineNotifiedSet.has(agent.agent_id)) {
offlineNotifiedSet.add(agent.agent_id);
const onlineList = onlineAgents().filter(id => id !== agent.agent_id);
for (const onlineId of onlineList) {
pushToAgent(onlineId, {
event: "agent_offline",
agent_id: agent.agent_id,
name: agent.name,
last_heartbeat: agent.last_heartbeat,
offline_duration: Math.round(elapsed / 1000),
message: `${agent.name} (${agent.agent_id}) 已离线超过 5 分钟`,
});
}
logger.info("agent_offline_notified", {
module: "heartbeat",
agent_id: agent.agent_id,
notified_count: onlineList.length,
});
}
}
}, HEARTBEAT_CHECK_INTERVAL);
}
/**
* 停止心跳超时检测
*/
export function stopHeartbeatMonitor(): void {
if (heartbeatTimer) {
clearInterval(heartbeatTimer);
heartbeatTimer = null;
offlineNotifiedSet.clear();
logger.info("HeartbeatMonitor stopped", { module: "heartbeat" });
}
}
/**
* 清除离线通知标记(Agent 重新上线时调用)
*/
export function clearOfflineNotification(agentId: string): void {
offlineNotifiedSet.delete(agentId);
}
// ─── Trust Score 管理(Phase 2 Day 4) ───────────────────
const TRUST_SCORE_MIN = 0;
const TRUST_SCORE_MAX = 100;
const TRUST_SCORE_DEFAULT = 50;
/**
* 获取 Agent 信任分
*/
export function getAgentTrustScore(agentId: string): number {
const row = db
.prepare(`SELECT trust_score FROM agents WHERE agent_id=?`)
.get(agentId) as any;
return row ? row.trust_score : TRUST_SCORE_DEFAULT;
}
/**
* 更新 Agent 信任分(admin only
* @returns 更新后的信任分,或 nullAgent 不存在)
*/
export function updateAgentTrustScore(
agentId: string,
delta: number,
operatorId?: string
): { ok: true; new_score: number } | { ok: false; error: string } {
const row = db
.prepare(`SELECT trust_score FROM agents WHERE agent_id=?`)
.get(agentId) as any;
if (!row) {
return { ok: false, error: `Agent ${agentId} not found` };
}
const current = row.trust_score;
const newScore = Math.max(TRUST_SCORE_MIN, Math.min(TRUST_SCORE_MAX, current + delta));
db.prepare(`UPDATE agents SET trust_score=? WHERE agent_id=?`).run(newScore, agentId);
auditLog("trust_score_updated", operatorId ?? null, agentId,
`${current}${newScore} (delta=${delta})`);
return { ok: true, new_score: newScore };
}
// ─── Phase 5a: 角色管理 ──────────────────────────────────
/**
* 设置 Agent 角色(admin only
* 支持 admin / member / group_admin 三种角色
* group_admin 需要同时设置 managed_group_id
*/
export function setAgentRole(
agentId: string,
newRole: "admin" | "member" | "group_admin",
operatorId: string,
managedGroupId?: string
): { ok: true; old_role: string; new_role: string; managed_group_id: string | null } | { ok: false; error: string } {
// 验证目标 Agent 存在
const agent = db
.prepare(`SELECT role, managed_group_id FROM agents WHERE agent_id=?`)
.get(agentId) as any;
if (!agent) {
return { ok: false, error: `Agent ${agentId} not found` };
}
const oldRole = agent.role;
// 不能修改自己
if (agentId === operatorId) {
return { ok: false, error: "Cannot modify own role" };
}
// 非 admin 不能被设为 admin(防止 member 提权)
if (newRole === "admin" && oldRole !== "admin") {
return { ok: false, error: "Only existing admin can be promoted to admin" };
}
// group_admin 必须有 managed_group_id
const groupId = newRole === "group_admin" ? (managedGroupId ?? null) : null;
// 更新 agents 表
db.prepare(`UPDATE agents SET role=?, managed_group_id=? WHERE agent_id=?`)
.run(newRole, groupId, agentId);
// 同步更新 auth_tokens 表中的 role(保持一致性)
db.prepare(`UPDATE auth_tokens SET role=? WHERE agent_id=? AND token_type='api_token' AND revoked_at IS NULL`)
.run(newRole, agentId);
auditLog("role_changed", operatorId, agentId,
`${oldRole}${newRole}${groupId ? `, group=${groupId}` : ""}`);
return { ok: true, old_role: oldRole, new_role: newRole, managed_group_id: groupId };
}
/**
* 获取 Agent 角色
*/
export function getAgentRole(agentId: string): string | null {
const row = db
.prepare(`SELECT role FROM agents WHERE agent_id=?`)
.get(agentId) as any;
return row?.role ?? null;
}
/**
* 获取 Agent 的 managed_group_id
*/
export function getAgentManagedGroup(agentId: string): string | null {
const row = db
.prepare(`SELECT managed_group_id FROM agents WHERE agent_id=?`)
.get(agentId) as any;
return row?.managed_group_id ?? null;
}
/**
* 获取心跳超时配置(用于测试)
*/
export function getHeartbeatConfig() {
return {
onlineThreshold: HEARTBEAT_ONLINE_THRESHOLD,
notifyThreshold: HEARTBEAT_NOTIFY_THRESHOLD,
checkInterval: HEARTBEAT_CHECK_INTERVAL,
};
}
// ─────────────────────────────────────────────────────────
// from_agent 格式规范化(Phase 2.1
// ─────────────────────────────────────────────────────────
/**
* 已知的 Agent 别名映射(兼容历史消息格式)
* 规范化后不再需要,但用于迁移阶段
*/
const AGENT_ALIAS_MAP: Record<string, string> = {
'workbuddy': 'agent_workbuddy_a3f7c2e1_1777300825754',
'hermes': 'agent_hermes_54cfe58b_1777132066111',
'qclaw': 'agent_1c11a7bd_1777129814251',
};
/**
* 解析 Agent 标识符为完整 agent_id。
* 支持:
* 1. 完整 agent_id(已注册则返回)
* 2. 已知别名(workbuddy / hermes / qclaw,大小写不敏感)
* 3. agent_id 子串匹配(大小写不敏感)
* 返回完整 agent_id 或 null(未找到)
*/
export function resolveAgentId(input: string): string | null {
const trimmed = input.trim();
if (!trimmed) return null;
// 1. 直接以 agent_ 开头 → 验证是否存在于数据库
if (trimmed.startsWith('agent_')) {
return getAgent(trimmed) ? trimmed : null;
}
// 2. 已知别名映射
const aliasKey = trimmed.toLowerCase();
if (AGENT_ALIAS_MAP[aliasKey]) {
return getAgent(AGENT_ALIAS_MAP[aliasKey]) ? AGENT_ALIAS_MAP[aliasKey] : null;
}
// 3. 子串匹配(agent_id 包含输入,大小写不敏感)
const agents = queryAgents({});
const lower = trimmed.toLowerCase();
for (const agent of agents) {
if (agent.agent_id.toLowerCase().includes(lower)) {
return agent.agent_id;
}
}
return null;
}
+27
View File
@@ -0,0 +1,27 @@
/**
* logger.ts — 结构化 JSON 日志(Phase 5b
* 替换 console.log/error/warn 为 JSON 格式输出
*
* 输出目标:stdoutinfo/debug+ stderrwarn/error
* 格式:{"timestamp":"ISO8601","level":"info","traceId":"xxx","module":"server","msg":"xxx",...meta}
*
* 环境变量:LOG_LEVEL(默认 info
*/
export interface ChildLogger {
info(msg: string, meta?: Record<string, unknown>): void;
warn(msg: string, meta?: Record<string, unknown>): void;
error(msg: string, meta?: Record<string, unknown>): void;
debug(msg: string, meta?: Record<string, unknown>): void;
}
export declare const logger: {
info(msg: string, meta?: Record<string, unknown>): void;
warn(msg: string, meta?: Record<string, unknown>): void;
error(msg: string, meta?: Record<string, unknown>): void;
debug(msg: string, meta?: Record<string, unknown>): void;
child(bindings: {
traceId?: string;
module?: string;
}): ChildLogger;
};
/** 记录带 Error.stack 的错误(仅写入 stderr,不暴露给客户端) */
export declare function logError(label: string, err: unknown, meta?: Record<string, unknown>): void;
+76
View File
@@ -0,0 +1,76 @@
/**
* logger.ts — 结构化 JSON 日志(Phase 5b
* 替换 console.log/error/warn 为 JSON 格式输出
*
* 输出目标:stdoutinfo/debug+ stderrwarn/error
* 格式:{"timestamp":"ISO8601","level":"info","traceId":"xxx","module":"server","msg":"xxx",...meta}
*
* 环境变量:LOG_LEVEL(默认 info
*/
const LOG_LEVEL_PRIORITY = {
debug: 0,
info: 1,
warn: 2,
error: 3,
};
const MIN_LEVEL = process.env.LOG_LEVEL || "info";
function shouldLog(level) {
return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[MIN_LEVEL];
}
function write(entry) {
if (!shouldLog(entry.level))
return;
const line = JSON.stringify(entry);
if (entry.level === "warn" || entry.level === "error") {
process.stderr.write(line + "\n");
}
else {
process.stdout.write(line + "\n");
}
}
function formatMsg(args) {
return args.map(a => {
if (a instanceof Error)
return a.message;
if (typeof a === "object" && a !== null)
return JSON.stringify(a);
return String(a);
}).join(" ");
}
export const logger = {
info(msg, meta) {
write({ timestamp: new Date().toISOString(), level: "info", msg, ...meta });
},
warn(msg, meta) {
write({ timestamp: new Date().toISOString(), level: "warn", msg, ...meta });
},
error(msg, meta) {
write({ timestamp: new Date().toISOString(), level: "error", msg, ...meta });
},
debug(msg, meta) {
write({ timestamp: new Date().toISOString(), level: "debug", msg, ...meta });
},
child(bindings) {
return {
info(msg, meta) {
write({ timestamp: new Date().toISOString(), level: "info", traceId: bindings.traceId, module: bindings.module, msg, ...meta });
},
warn(msg, meta) {
write({ timestamp: new Date().toISOString(), level: "warn", traceId: bindings.traceId, module: bindings.module, msg, ...meta });
},
error(msg, meta) {
write({ timestamp: new Date().toISOString(), level: "error", traceId: bindings.traceId, module: bindings.module, msg, ...meta });
},
debug(msg, meta) {
write({ timestamp: new Date().toISOString(), level: "debug", traceId: bindings.traceId, module: bindings.module, msg, ...meta });
},
};
},
};
/** 记录带 Error.stack 的错误(仅写入 stderr,不暴露给客户端) */
export function logError(label, err, meta) {
const message = err instanceof Error ? err.message : String(err);
const stack = err instanceof Error ? err.stack : undefined;
write({ timestamp: new Date().toISOString(), level: "error", msg: label, error: message, stack, ...meta });
}
//# sourceMappingURL=logger.js.map
+100
View File
@@ -0,0 +1,100 @@
/**
* logger.ts — 结构化 JSON 日志(Phase 5b
* 替换 console.log/error/warn 为 JSON 格式输出
*
* 输出目标:stdoutinfo/debug+ stderrwarn/error
* 格式:{"timestamp":"ISO8601","level":"info","traceId":"xxx","module":"server","msg":"xxx",...meta}
*
* 环境变量:LOG_LEVEL(默认 info
*/
type LogLevel = "debug" | "info" | "warn" | "error";
interface LogEntry {
timestamp: string;
level: LogLevel;
traceId?: string;
module?: string;
msg: string;
[key: string]: unknown;
}
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
debug: 0,
info: 1,
warn: 2,
error: 3,
};
const MIN_LEVEL: LogLevel = (process.env.LOG_LEVEL as LogLevel) || "info";
function shouldLog(level: LogLevel): boolean {
return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[MIN_LEVEL];
}
function write(entry: LogEntry): void {
if (!shouldLog(entry.level)) return;
const line = JSON.stringify(entry);
if (entry.level === "warn" || entry.level === "error") {
process.stderr.write(line + "\n");
} else {
process.stdout.write(line + "\n");
}
}
function formatMsg(args: unknown[]): string {
return args.map(a => {
if (a instanceof Error) return a.message;
if (typeof a === "object" && a !== null) return JSON.stringify(a);
return String(a);
}).join(" ");
}
export interface ChildLogger {
info(msg: string, meta?: Record<string, unknown>): void;
warn(msg: string, meta?: Record<string, unknown>): void;
error(msg: string, meta?: Record<string, unknown>): void;
debug(msg: string, meta?: Record<string, unknown>): void;
}
export const logger = {
info(msg: string, meta?: Record<string, unknown>): void {
write({ timestamp: new Date().toISOString(), level: "info", msg, ...meta });
},
warn(msg: string, meta?: Record<string, unknown>): void {
write({ timestamp: new Date().toISOString(), level: "warn", msg, ...meta });
},
error(msg: string, meta?: Record<string, unknown>): void {
write({ timestamp: new Date().toISOString(), level: "error", msg, ...meta });
},
debug(msg: string, meta?: Record<string, unknown>): void {
write({ timestamp: new Date().toISOString(), level: "debug", msg, ...meta });
},
child(bindings: { traceId?: string; module?: string }): ChildLogger {
return {
info(msg: string, meta?: Record<string, unknown>): void {
write({ timestamp: new Date().toISOString(), level: "info", traceId: bindings.traceId, module: bindings.module, msg, ...meta });
},
warn(msg: string, meta?: Record<string, unknown>): void {
write({ timestamp: new Date().toISOString(), level: "warn", traceId: bindings.traceId, module: bindings.module, msg, ...meta });
},
error(msg: string, meta?: Record<string, unknown>): void {
write({ timestamp: new Date().toISOString(), level: "error", traceId: bindings.traceId, module: bindings.module, msg, ...meta });
},
debug(msg: string, meta?: Record<string, unknown>): void {
write({ timestamp: new Date().toISOString(), level: "debug", traceId: bindings.traceId, module: bindings.module, msg, ...meta });
},
};
},
};
/** 记录带 Error.stack 的错误(仅写入 stderr,不暴露给客户端) */
export function logError(label: string, err: unknown, meta?: Record<string, unknown>): void {
const message = err instanceof Error ? err.message : String(err);
const stack = err instanceof Error ? err.stack : undefined;
write({ timestamp: new Date().toISOString(), level: "error", msg: label, error: message, stack, ...meta });
}
+83
View File
@@ -0,0 +1,83 @@
export interface MemoryEntry {
id: string;
agent_id: string;
title: string | null;
content: string;
scope: "private" | "group" | "collective";
tags: string | null;
source_agent_id: string | null;
source_task_id: string | null;
created_at: number;
updated_at: number | null;
}
export interface MemoryStats {
total: number;
by_agent: Record<string, number>;
by_scope: Record<string, number>;
fts_entries: number;
}
/**
* 存储新记忆
*
* @returns
* - { ok: true, memory } — 成功
* - { ok: false, error } — 失败
*/
export declare function storeMemory(agentId: string, content: string, options?: {
title?: string;
scope?: "private" | "group" | "collective";
tags?: string[];
source_agent_id?: string;
source_task_id?: string;
}): {
ok: true;
memory: MemoryEntry;
} | {
ok: false;
error: string;
};
/**
* 通过全文搜索召回记忆
*
* 搜索范围:
* - private: 仅本人的记忆
* - group: scope=group 或 scope=collective 的记忆
* - collective: scope=collective 的记忆
*
* @param query 搜索关键词(FTS5 query syntax
* @param agentId 查询者 ID(用于 scope 过滤)
* @param options 可选参数
* @returns 匹配的记忆列表
*/
export declare function recallMemory(query: string, agentId: string, options?: {
limit?: number;
scope?: "private" | "group" | "collective" | "all";
}): MemoryEntry[];
/**
* 列出 Agent 的记忆
*/
export declare function listMemories(agentId: string, options?: {
scope?: "private" | "group" | "collective" | "all";
limit?: number;
offset?: number;
}): MemoryEntry[];
/**
* 删除记忆
* 仅允许删除自己的记忆,或 admin 删除任何记忆
*/
export declare function deleteMemory(memoryId: string, agentId: string, role: string): {
ok: true;
deleted: boolean;
} | {
ok: false;
error: string;
};
/**
* 获取记忆统计信息
*/
export declare function getMemoryStats(): MemoryStats;
/**
* 为所有已有 memories 重建 FTS 索引(Phase 2 Migration
* 在 server.ts 启动时调用一次
*/
export declare function rebuildFtsIndex(): void;
+336
View File
@@ -0,0 +1,336 @@
/**
* memory.ts — Memory Service (Phase 1 Week 2)
*
* 功能:
* - storeMemory: 存储记忆(private/group/collective 三种 scope
* - recallMemory: 通过 FTS5 全文搜索召回记忆
* - listMemories: 列出 Agent 的记忆(支持 scope 筛选)
* - deleteMemory: 删除记忆
* - getMemoryStats: 获取记忆统计
*
* 设计要点:
* - FTS5 全文索引自动同步(通过 triggers)
* - scope 控制:private 仅本人可见,group 组内可见,collective 全局可见
* - 内容长度限制 10KB
* - 标签支持:JSON array 字符串存储
*/
import { randomUUID } from "crypto";
import { db } from "./db.js";
import { buildFtsTokens, buildSearchQuery } from "./tokenizer.js";
import { auditLog } from "./security.js";
import { logError, logger } from "./logger.js";
// ─── 常量 ────────────────────────────────────────────────
const MAX_CONTENT_LENGTH = 10000;
const MAX_TITLE_LENGTH = 500;
const MAX_RECALL_RESULTS = 20;
const MAX_LIST_RESULTS = 50;
// ─── 存储记忆 ────────────────────────────────────────────
/**
* 存储新记忆
*
* @returns
* - { ok: true, memory } — 成功
* - { ok: false, error } — 失败
*/
export function storeMemory(agentId, content, options) {
// 参数校验
if (!content || content.trim().length === 0) {
return { ok: false, error: "Memory content cannot be empty" };
}
if (content.length > MAX_CONTENT_LENGTH) {
return {
ok: false,
error: `Memory content too long (${content.length} > ${MAX_CONTENT_LENGTH} chars)`,
};
}
const title = options?.title?.trim() ?? null;
if (title && title.length > MAX_TITLE_LENGTH) {
return {
ok: false,
error: `Memory title too long (${title.length} > ${MAX_TITLE_LENGTH} chars)`,
};
}
const scope = options?.scope ?? "private";
if (!["private", "group", "collective"].includes(scope)) {
return { ok: false, error: `Invalid scope: ${scope}` };
}
const tags = options?.tags ?? null;
const tagsJson = tags ? JSON.stringify(tags) : null;
const sourceAgentId = options?.source_agent_id ?? null;
const sourceTaskId = options?.source_task_id ?? null;
const now = Date.now();
const id = randomUUID();
try {
const ftsTokens = buildFtsTokens(title, content);
db.prepare(`INSERT INTO memories (id, agent_id, title, content, fts_tokens, scope, tags, source_agent_id, source_task_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, agentId, title, content, ftsTokens, scope, tagsJson, sourceAgentId, sourceTaskId, now, now);
// 同步写入 FTS5 索引
db.prepare(`INSERT INTO memories_fts (title, content, tags, fts_tokens) VALUES (?, ?, ?, ?)`).run(title, content, tagsJson, ftsTokens);
const memory = {
id,
agent_id: agentId,
title,
content,
scope,
tags: tagsJson,
source_agent_id: sourceAgentId,
source_task_id: sourceTaskId,
created_at: now,
updated_at: now,
};
return { ok: true, memory };
}
catch (err) {
return { ok: false, error: `Failed to store memory: ${err.message}` };
}
}
// ─── 召回记忆(FTS5 全文搜索) ──────────────────────────
/**
* 通过全文搜索召回记忆
*
* 搜索范围:
* - private: 仅本人的记忆
* - group: scope=group 或 scope=collective 的记忆
* - collective: scope=collective 的记忆
*
* @param query 搜索关键词(FTS5 query syntax
* @param agentId 查询者 ID(用于 scope 过滤)
* @param options 可选参数
* @returns 匹配的记忆列表
*/
export function recallMemory(query, agentId, options) {
if (!query || query.trim().length === 0) {
return [];
}
const limit = Math.min(options?.limit ?? MAX_RECALL_RESULTS, MAX_RECALL_RESULTS);
const scope = options?.scope ?? "all";
// 构建 FTS5 查询(N-gram 中文分词)
const safeQuery = buildSearchQuery(query);
if (!safeQuery) {
return [];
}
try {
let sql;
let params;
if (scope === "all") {
// 搜索所有可见的记忆(private 仅限本人 + group + collective
// Phase 2 Day 4: 按 agent trust_score 加权排序(高信任排名靠前)
sql = `
SELECT m.*, COALESCE(a.trust_score, 50) AS source_trust_score
FROM memories m
JOIN memories_fts fts ON m.title = fts.title AND m.content = fts.content
LEFT JOIN agents a ON m.agent_id = a.agent_id
WHERE memories_fts MATCH ?
AND (m.agent_id = ? OR m.scope IN ('group', 'collective'))
ORDER BY source_trust_score DESC, rank
LIMIT ?
`;
params = [safeQuery, agentId, limit];
}
else if (scope === "private") {
sql = `
SELECT m.*, COALESCE(a.trust_score, 50) AS source_trust_score
FROM memories m
JOIN memories_fts fts ON m.title = fts.title AND m.content = fts.content
LEFT JOIN agents a ON m.agent_id = a.agent_id
WHERE memories_fts MATCH ?
AND m.agent_id = ? AND m.scope = 'private'
ORDER BY source_trust_score DESC, rank
LIMIT ?
`;
params = [safeQuery, agentId, limit];
}
else if (scope === "group") {
sql = `
SELECT m.*, COALESCE(a.trust_score, 50) AS source_trust_score
FROM memories m
JOIN memories_fts fts ON m.title = fts.title AND m.content = fts.content
LEFT JOIN agents a ON m.agent_id = a.agent_id
WHERE memories_fts MATCH ?
AND (m.agent_id = ? OR m.scope IN ('group', 'collective'))
AND m.scope != 'private'
ORDER BY source_trust_score DESC, rank
LIMIT ?
`;
params = [safeQuery, agentId, limit];
}
else {
// collective
sql = `
SELECT m.*, COALESCE(a.trust_score, 50) AS source_trust_score
FROM memories m
JOIN memories_fts fts ON m.title = fts.title AND m.content = fts.content
LEFT JOIN agents a ON m.agent_id = a.agent_id
WHERE memories_fts MATCH ?
AND m.scope = 'collective'
ORDER BY source_trust_score DESC, rank
LIMIT ?
`;
params = [safeQuery, limit];
}
return db.prepare(sql).all(...params);
}
catch (err) {
logError("memory_recallMemory_error", err);
return [];
}
}
// ─── 列出记忆 ────────────────────────────────────────────
/**
* 列出 Agent 的记忆
*/
export function listMemories(agentId, options) {
const limit = Math.min(options?.limit ?? MAX_LIST_RESULTS, MAX_LIST_RESULTS);
const offset = options?.offset ?? 0;
const scope = options?.scope ?? "all";
try {
let sql;
let params;
if (scope === "all") {
sql = `
SELECT m.*, COALESCE(a.trust_score, 50) AS source_trust_score
FROM memories m
LEFT JOIN agents a ON m.agent_id = a.agent_id
WHERE m.agent_id = ? OR m.scope IN ('group', 'collective')
ORDER BY source_trust_score DESC, m.created_at DESC
LIMIT ? OFFSET ?
`;
params = [agentId, limit, offset];
}
else if (scope === "private") {
sql = `
SELECT m.*, COALESCE(a.trust_score, 50) AS source_trust_score
FROM memories m
LEFT JOIN agents a ON m.agent_id = a.agent_id
WHERE m.agent_id = ? AND m.scope = 'private'
ORDER BY m.created_at DESC
LIMIT ? OFFSET ?
`;
params = [agentId, limit, offset];
}
else {
sql = `
SELECT m.*, COALESCE(a.trust_score, 50) AS source_trust_score
FROM memories m
LEFT JOIN agents a ON m.agent_id = a.agent_id
WHERE (m.agent_id = ? OR m.scope IN ('group', 'collective'))
AND m.scope = ?
ORDER BY source_trust_score DESC, m.created_at DESC
LIMIT ? OFFSET ?
`;
params = [agentId, scope, limit, offset];
}
return db.prepare(sql).all(...params);
}
catch (err) {
logError("memory_listMemories_error", err);
return [];
}
}
// ─── 删除记忆 ────────────────────────────────────────────
/**
* 删除记忆
* 仅允许删除自己的记忆,或 admin 删除任何记忆
*/
export function deleteMemory(memoryId, agentId, role) {
try {
// 查找记忆
const memory = db.prepare(`SELECT * FROM memories WHERE id = ?`).get(memoryId);
if (!memory) {
return { ok: false, error: `Memory ${memoryId} not found` };
}
// 权限检查:只能删除自己的记忆(admin 可以删除任何)
if (memory.agent_id !== agentId && role !== "admin") {
return { ok: false, error: "Permission denied: can only delete own memories" };
}
// 删除 FTS 索引(通过 title + content 匹配)
try {
db.prepare(`DELETE FROM memories_fts WHERE title = ? AND content = ?`).run(memory.title, memory.content);
// Phase 5a Day 2: 审计 FTS 索引删除
auditLog("delete_memory_fts", agentId, memoryId, `title=${memory.title?.slice(0, 50) ?? "null"}`);
}
catch {
// FTS 删除失败不影响主表删除
}
db.prepare(`DELETE FROM memories WHERE id = ?`).run(memoryId);
// Phase 5a Day 2: 审计记忆主表删除
auditLog("delete_memory_db", agentId, memoryId, `scope=${memory.scope}, agent=${memory.agent_id}`);
return { ok: true, deleted: true };
}
catch (err) {
return { ok: false, error: `Failed to delete memory: ${err.message}` };
}
}
// ─── 记忆统计 ────────────────────────────────────────────
/**
* 获取记忆统计信息
*/
export function getMemoryStats() {
try {
const totalRow = db.prepare(`SELECT COUNT(*) as cnt FROM memories`).get();
let ftsEntries = 0;
try {
const ftsRow = db.prepare(`SELECT COUNT(*) as cnt FROM memories_fts`).get();
ftsEntries = ftsRow?.cnt ?? 0;
}
catch {
// FTS 表可能不存在
}
const byAgentRows = db.prepare(`SELECT agent_id, COUNT(*) as cnt FROM memories GROUP BY agent_id`).all();
const byScopeRows = db.prepare(`SELECT scope, COUNT(*) as cnt FROM memories GROUP BY scope`).all();
const byAgent = {};
for (const row of byAgentRows) {
byAgent[row.agent_id] = row.cnt;
}
const byScope = {};
for (const row of byScopeRows) {
byScope[row.scope] = row.cnt;
}
return {
total: totalRow?.cnt ?? 0,
by_agent: byAgent,
by_scope: byScope,
fts_entries: ftsEntries,
};
}
catch (err) {
logError("memory_getMemoryStats_error", err);
return { total: 0, by_agent: {}, by_scope: {}, fts_entries: 0 };
}
}
// ─── FTS 索引重建 ────────────────────────────────────────
/**
* 为所有已有 memories 重建 FTS 索引(Phase 2 Migration
* 在 server.ts 启动时调用一次
*/
export function rebuildFtsIndex() {
try {
const memCount = db.prepare(`SELECT COUNT(*) as cnt FROM memories`).get()?.cnt ?? 0;
let ftsCount = 0;
try {
ftsCount = db.prepare(`SELECT COUNT(*) as cnt FROM memories_fts`).get()?.cnt ?? 0;
}
catch {
// FTS 表不存在,跳过
return;
}
if (memCount === 0 || ftsCount >= memCount) {
return; // 不需要重建
}
logger.info("memory_fts_rebuild_start", { module: "memory", mem_count: memCount, fts_count: ftsCount });
const memories = db.prepare(`SELECT id, title, content, tags, source_agent_id, source_task_id FROM memories`).all();
const insertFts = db.prepare(`INSERT INTO memories_fts (title, content, tags, fts_tokens) VALUES (?, ?, ?, ?)`);
const rebuildBatch = db.transaction((mems) => {
for (const m of mems) {
const tokens = buildFtsTokens(m.title, m.content);
insertFts.run(m.title, m.content, m.tags, tokens);
}
});
rebuildBatch(memories);
logger.info("memory_fts_rebuild_done", { module: "memory", entries: memories.length });
}
catch (err) {
logError("memory_rebuildFtsIndex_error", err);
}
}
//# sourceMappingURL=memory.js.map
+435
View File
@@ -0,0 +1,435 @@
/**
* memory.ts — Memory Service (Phase 1 Week 2)
*
* 功能:
* - storeMemory: 存储记忆(private/group/collective 三种 scope
* - recallMemory: 通过 FTS5 全文搜索召回记忆
* - listMemories: 列出 Agent 的记忆(支持 scope 筛选)
* - deleteMemory: 删除记忆
* - getMemoryStats: 获取记忆统计
*
* 设计要点:
* - FTS5 全文索引自动同步(通过 triggers)
* - scope 控制:private 仅本人可见,group 组内可见,collective 全局可见
* - 内容长度限制 10KB
* - 标签支持:JSON array 字符串存储
*/
import { randomUUID } from "crypto";
import { db } from "./db.js";
import { buildFtsTokens, buildSearchQuery } from "./tokenizer.js";
import { auditLog } from "./security.js";
import type { MemoryRow } from "./types.js";
import { getErrorMessage } from "./types.js";
import { logError, logger } from "./logger.js";
// ─── 常量 ────────────────────────────────────────────────
const MAX_CONTENT_LENGTH = 10000;
const MAX_TITLE_LENGTH = 500;
const MAX_RECALL_RESULTS = 20;
const MAX_LIST_RESULTS = 50;
// ─── 类型定义 ────────────────────────────────────────────
export interface MemoryEntry {
id: string;
agent_id: string;
title: string | null;
content: string;
scope: "private" | "group" | "collective";
tags: string | null; // JSON array
source_agent_id: string | null; // Phase 2 Day 4: 溯源
source_task_id: string | null; // Phase 2 Day 4: 溯源
created_at: number;
updated_at: number | null;
}
export interface MemoryStats {
total: number;
by_agent: Record<string, number>;
by_scope: Record<string, number>;
fts_entries: number;
}
// ─── 存储记忆 ────────────────────────────────────────────
/**
* 存储新记忆
*
* @returns
* - { ok: true, memory } — 成功
* - { ok: false, error } — 失败
*/
export function storeMemory(
agentId: string,
content: string,
options?: {
title?: string;
scope?: "private" | "group" | "collective";
tags?: string[];
source_agent_id?: string; // Phase 2 Day 4: 溯源(collective 写入时自动设置)
source_task_id?: string; // Phase 2 Day 4: 溯源(关联任务)
}
): { ok: true; memory: MemoryEntry } | { ok: false; error: string } {
// 参数校验
if (!content || content.trim().length === 0) {
return { ok: false, error: "Memory content cannot be empty" };
}
if (content.length > MAX_CONTENT_LENGTH) {
return {
ok: false,
error: `Memory content too long (${content.length} > ${MAX_CONTENT_LENGTH} chars)`,
};
}
const title = options?.title?.trim() ?? null;
if (title && title.length > MAX_TITLE_LENGTH) {
return {
ok: false,
error: `Memory title too long (${title.length} > ${MAX_TITLE_LENGTH} chars)`,
};
}
const scope = options?.scope ?? "private";
if (!["private", "group", "collective"].includes(scope)) {
return { ok: false, error: `Invalid scope: ${scope}` };
}
const tags = options?.tags ?? null;
const tagsJson = tags ? JSON.stringify(tags) : null;
const sourceAgentId = options?.source_agent_id ?? null;
const sourceTaskId = options?.source_task_id ?? null;
const now = Date.now();
const id = randomUUID();
try {
const ftsTokens = buildFtsTokens(title, content);
db.prepare(
`INSERT INTO memories (id, agent_id, title, content, fts_tokens, scope, tags, source_agent_id, source_task_id, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
).run(id, agentId, title, content, ftsTokens, scope, tagsJson, sourceAgentId, sourceTaskId, now, now);
// 同步写入 FTS5 索引
db.prepare(
`INSERT INTO memories_fts (title, content, tags, fts_tokens) VALUES (?, ?, ?, ?)`
).run(title, content, tagsJson, ftsTokens);
const memory: MemoryEntry = {
id,
agent_id: agentId,
title,
content,
scope,
tags: tagsJson,
source_agent_id: sourceAgentId,
source_task_id: sourceTaskId,
created_at: now,
updated_at: now,
};
return { ok: true, memory };
} catch (err: unknown) {
return { ok: false, error: `Failed to store memory: ${getErrorMessage(err)}` };
}
}
// ─── 召回记忆(FTS5 全文搜索) ──────────────────────────
/**
* 通过全文搜索召回记忆
*
* 搜索范围:
* - private: 仅本人的记忆
* - group: scope=group 或 scope=collective 的记忆
* - collective: scope=collective 的记忆
*
* @param query 搜索关键词(FTS5 query syntax
* @param agentId 查询者 ID(用于 scope 过滤)
* @param options 可选参数
* @returns 匹配的记忆列表
*/
export function recallMemory(
query: string,
agentId: string,
options?: {
limit?: number;
scope?: "private" | "group" | "collective" | "all";
}
): MemoryEntry[] {
if (!query || query.trim().length === 0) {
return [];
}
const limit = Math.min(options?.limit ?? MAX_RECALL_RESULTS, MAX_RECALL_RESULTS);
const scope = options?.scope ?? "all";
// 构建 FTS5 查询(N-gram 中文分词)
const safeQuery = buildSearchQuery(query);
if (!safeQuery) {
return [];
}
try {
let sql: string;
let params: (string | number)[];
if (scope === "all") {
// 搜索所有可见的记忆(private 仅限本人 + group + collective
// Phase 2 Day 4: 按 agent trust_score 加权排序(高信任排名靠前)
sql = `
SELECT m.*, COALESCE(a.trust_score, 50) AS source_trust_score
FROM memories m
JOIN memories_fts fts ON m.title = fts.title AND m.content = fts.content
LEFT JOIN agents a ON m.agent_id = a.agent_id
WHERE memories_fts MATCH ?
AND (m.agent_id = ? OR m.scope IN ('group', 'collective'))
ORDER BY source_trust_score DESC, rank
LIMIT ?
`;
params = [safeQuery, agentId, limit];
} else if (scope === "private") {
sql = `
SELECT m.*, COALESCE(a.trust_score, 50) AS source_trust_score
FROM memories m
JOIN memories_fts fts ON m.title = fts.title AND m.content = fts.content
LEFT JOIN agents a ON m.agent_id = a.agent_id
WHERE memories_fts MATCH ?
AND m.agent_id = ? AND m.scope = 'private'
ORDER BY source_trust_score DESC, rank
LIMIT ?
`;
params = [safeQuery, agentId, limit];
} else if (scope === "group") {
sql = `
SELECT m.*, COALESCE(a.trust_score, 50) AS source_trust_score
FROM memories m
JOIN memories_fts fts ON m.title = fts.title AND m.content = fts.content
LEFT JOIN agents a ON m.agent_id = a.agent_id
WHERE memories_fts MATCH ?
AND (m.agent_id = ? OR m.scope IN ('group', 'collective'))
AND m.scope != 'private'
ORDER BY source_trust_score DESC, rank
LIMIT ?
`;
params = [safeQuery, agentId, limit];
} else {
// collective
sql = `
SELECT m.*, COALESCE(a.trust_score, 50) AS source_trust_score
FROM memories m
JOIN memories_fts fts ON m.title = fts.title AND m.content = fts.content
LEFT JOIN agents a ON m.agent_id = a.agent_id
WHERE memories_fts MATCH ?
AND m.scope = 'collective'
ORDER BY source_trust_score DESC, rank
LIMIT ?
`;
params = [safeQuery, limit];
}
return db.prepare(sql).all(...params) as MemoryEntry[];
} catch (err: unknown) {
logError("memory_recallMemory_error", err);
return [];
}
}
// ─── 列出记忆 ────────────────────────────────────────────
/**
* 列出 Agent 的记忆
*/
export function listMemories(
agentId: string,
options?: {
scope?: "private" | "group" | "collective" | "all";
limit?: number;
offset?: number;
}
): MemoryEntry[] {
const limit = Math.min(options?.limit ?? MAX_LIST_RESULTS, MAX_LIST_RESULTS);
const offset = options?.offset ?? 0;
const scope = options?.scope ?? "all";
try {
let sql: string;
let params: (string | number)[];
if (scope === "all") {
sql = `
SELECT m.*, COALESCE(a.trust_score, 50) AS source_trust_score
FROM memories m
LEFT JOIN agents a ON m.agent_id = a.agent_id
WHERE m.agent_id = ? OR m.scope IN ('group', 'collective')
ORDER BY source_trust_score DESC, m.created_at DESC
LIMIT ? OFFSET ?
`;
params = [agentId, limit, offset];
} else if (scope === "private") {
sql = `
SELECT m.*, COALESCE(a.trust_score, 50) AS source_trust_score
FROM memories m
LEFT JOIN agents a ON m.agent_id = a.agent_id
WHERE m.agent_id = ? AND m.scope = 'private'
ORDER BY m.created_at DESC
LIMIT ? OFFSET ?
`;
params = [agentId, limit, offset];
} else {
sql = `
SELECT m.*, COALESCE(a.trust_score, 50) AS source_trust_score
FROM memories m
LEFT JOIN agents a ON m.agent_id = a.agent_id
WHERE (m.agent_id = ? OR m.scope IN ('group', 'collective'))
AND m.scope = ?
ORDER BY source_trust_score DESC, m.created_at DESC
LIMIT ? OFFSET ?
`;
params = [agentId, scope, limit, offset];
}
return db.prepare(sql).all(...params) as MemoryEntry[];
} catch (err: unknown) {
logError("memory_listMemories_error", err);
return [];
}
}
// ─── 删除记忆 ────────────────────────────────────────────
/**
* 删除记忆
* 仅允许删除自己的记忆,或 admin 删除任何记忆
*/
export function deleteMemory(
memoryId: string,
agentId: string,
role: string
): { ok: true; deleted: boolean } | { ok: false; error: string } {
try {
// 查找记忆
const memory = db.prepare(`SELECT * FROM memories WHERE id = ?`).get(memoryId) as MemoryEntry | undefined;
if (!memory) {
return { ok: false, error: `Memory ${memoryId} not found` };
}
// 权限检查:只能删除自己的记忆(admin 可以删除任何)
if (memory.agent_id !== agentId && role !== "admin") {
return { ok: false, error: "Permission denied: can only delete own memories" };
}
// 删除 FTS 索引(通过 title + content 匹配)
try {
db.prepare(
`DELETE FROM memories_fts WHERE title = ? AND content = ?`
).run(memory.title, memory.content);
// Phase 5a Day 2: 审计 FTS 索引删除
auditLog("delete_memory_fts", agentId, memoryId, `title=${memory.title?.slice(0, 50) ?? "null"}`);
} catch {
// FTS 删除失败不影响主表删除
}
db.prepare(`DELETE FROM memories WHERE id = ?`).run(memoryId);
// Phase 5a Day 2: 审计记忆主表删除
auditLog("delete_memory_db", agentId, memoryId, `scope=${memory.scope}, agent=${memory.agent_id}`);
return { ok: true, deleted: true };
} catch (err: unknown) {
return { ok: false, error: `Failed to delete memory: ${getErrorMessage(err)}` };
}
}
// ─── 记忆统计 ────────────────────────────────────────────
/**
* 获取记忆统计信息
*/
export function getMemoryStats(): MemoryStats {
try {
const totalRow = db.prepare(`SELECT COUNT(*) as cnt FROM memories`).get() as any;
let ftsEntries = 0;
try {
const ftsRow = db.prepare(`SELECT COUNT(*) as cnt FROM memories_fts`).get() as any;
ftsEntries = ftsRow?.cnt ?? 0;
} catch {
// FTS 表可能不存在
}
const byAgentRows = db.prepare(
`SELECT agent_id, COUNT(*) as cnt FROM memories GROUP BY agent_id`
).all() as { agent_id: string; cnt: number }[];
const byScopeRows = db.prepare(
`SELECT scope, COUNT(*) as cnt FROM memories GROUP BY scope`
).all() as { scope: string; cnt: number }[];
const byAgent: Record<string, number> = {};
for (const row of byAgentRows) {
byAgent[row.agent_id] = row.cnt;
}
const byScope: Record<string, number> = {};
for (const row of byScopeRows) {
byScope[row.scope] = row.cnt;
}
return {
total: totalRow?.cnt ?? 0,
by_agent: byAgent,
by_scope: byScope,
fts_entries: ftsEntries,
};
} catch (err: unknown) {
logError("memory_getMemoryStats_error", err);
return { total: 0, by_agent: {}, by_scope: {}, fts_entries: 0 };
}
}
// ─── FTS 索引重建 ────────────────────────────────────────
/**
* 为所有已有 memories 重建 FTS 索引(Phase 2 Migration
* 在 server.ts 启动时调用一次
*/
export function rebuildFtsIndex(): void {
try {
const memCount = (db.prepare(`SELECT COUNT(*) as cnt FROM memories`).get() as any)?.cnt ?? 0;
let ftsCount = 0;
try {
ftsCount = (db.prepare(`SELECT COUNT(*) as cnt FROM memories_fts`).get() as any)?.cnt ?? 0;
} catch {
// FTS 表不存在,跳过
return;
}
if (memCount === 0 || ftsCount >= memCount) {
return; // 不需要重建
}
logger.info("memory_fts_rebuild_start", { module: "memory", mem_count: memCount, fts_count: ftsCount });
const memories = db.prepare(
`SELECT id, title, content, tags, source_agent_id, source_task_id FROM memories`
).all() as Pick<MemoryRow, "id" | "title" | "content" | "tags" | "source_agent_id" | "source_task_id">[];
const insertFts = db.prepare(
`INSERT INTO memories_fts (title, content, tags, fts_tokens) VALUES (?, ?, ?, ?)`
);
const rebuildBatch = db.transaction((mems: Pick<MemoryRow, "id" | "title" | "content" | "tags" | "source_agent_id" | "source_task_id">[]) => {
for (const m of mems) {
const tokens = buildFtsTokens(m.title ?? null, m.content);
insertFts.run(m.title, m.content, m.tags ?? null, tokens);
}
});
rebuildBatch(memories);
logger.info("memory_fts_rebuild_done", { module: "memory", entries: memories.length });
} catch (err: unknown) {
logError("memory_rebuildFtsIndex_error", err);
}
}
+31
View File
@@ -0,0 +1,31 @@
/**
* metrics.ts — Prometheus 兼容指标(Phase 5b / Phase 3.1
* 零依赖实现,内存存储
*
* 指标:
* - mcp_calls_total{tool_name, status, role} : Counter
* - active_sse_connections : Gauge
* - message_delivery_total{status} : Counter
* - http_requests_total{method, path, status} : Counter
* - http_request_duration_ms{method, path} : Histogram (简易)
* - db_query_duration_ms{operation} : Histogram (简易)
* ──────────────────────────────── Phase 3.1 新增 ────────────────────────────────
* - hub_agents_online : Gauge
* - hub_messages_total{status} : Gauge
* - hub_trust_scores{agent_id} : Gauge
*/
import type { Database as DatabaseType } from "better-sqlite3";
export declare function incrementCounter(name: string, labels?: Record<string, string>, value?: number): void;
export declare function setGauge(name: string, value: number): void;
export declare function incrementGauge(name: string, value?: number): void;
export declare function decrementGauge(name: string, value?: number): void;
export declare function observeHistogram(name: string, valueMs: number, labels?: Record<string, string>): void;
export declare function getMetricsOutput(): string;
export declare function incrementMcpCall(toolName: string, status: "success" | "error" | "denied", role: string): void;
export declare function trackHttpRequest(method: string, path: string, statusCode: number, durationMs: number): void;
export declare function trackDbQuery(operation: string, durationMs: number): void;
/**
* 从 SQLite 数据库采集 Hub 层指标,返回 Prometheus 文本格式字符串。
* 由 server.ts 在 /metrics 路由中调用。
*/
export declare function collectHubMetrics(db: DatabaseType): string;
+220
View File
@@ -0,0 +1,220 @@
/**
* metrics.ts — Prometheus 兼容指标(Phase 5b / Phase 3.1
* 零依赖实现,内存存储
*
* 指标:
* - mcp_calls_total{tool_name, status, role} : Counter
* - active_sse_connections : Gauge
* - message_delivery_total{status} : Counter
* - http_requests_total{method, path, status} : Counter
* - http_request_duration_ms{method, path} : Histogram (简易)
* - db_query_duration_ms{operation} : Histogram (简易)
* ──────────────────────────────── Phase 3.1 新增 ────────────────────────────────
* - hub_agents_online : Gauge
* - hub_messages_total{status} : Gauge
* - hub_trust_scores{agent_id} : Gauge
*/
const counters = [];
function getOrCreateCounter(name, labels) {
for (const c of counters) {
if (c.labels._name !== name)
continue;
let match = true;
for (const k of Object.keys(labels)) {
if (c.labels[k] !== labels[k]) {
match = false;
break;
}
}
if (match)
return c;
}
const c = { _type: "counter", value: 0, labels: { _name: name, ...labels } };
counters.push(c);
return c;
}
export function incrementCounter(name, labels = {}, value = 1) {
const c = getOrCreateCounter(name, labels);
c.value += value;
}
// ─── Gauge ───────────────────────────────────────────────
const gauges = {};
export function setGauge(name, value) {
gauges[name] = value;
}
export function incrementGauge(name, value = 1) {
gauges[name] = (gauges[name] ?? 0) + value;
}
export function decrementGauge(name, value = 1) {
gauges[name] = (gauges[name] ?? 0) - value;
}
const histograms = [];
const HISTOGRAM_BUCKETS = [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000];
function getOrCreateHistogram(name, labels) {
for (const h of histograms) {
if (h.labels._name !== name)
continue;
let match = true;
for (const k of Object.keys(labels)) {
if (h.labels[k] !== labels[k]) {
match = false;
break;
}
}
if (match)
return h;
}
const h = {
_type: "histogram",
_sum: 0, _count: 0, _min: Infinity, _max: 0,
buckets: {},
labels: { _name: name, ...labels },
};
for (const b of HISTOGRAM_BUCKETS)
h.buckets[String(b)] = 0;
h.buckets["+Inf"] = 0;
histograms.push(h);
return h;
}
export function observeHistogram(name, valueMs, labels = {}) {
const h = getOrCreateHistogram(name, labels);
h._sum += valueMs;
h._count += 1;
if (valueMs < h._min)
h._min = valueMs;
if (valueMs > h._max)
h._max = valueMs;
for (const b of HISTOGRAM_BUCKETS) {
if (valueMs <= b)
h.buckets[String(b)]++;
}
h.buckets["+Inf"]++;
}
// ─── Prometheus 文本输出 ─────────────────────────────────
function escapeLabelValue(s) {
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
}
function formatLabels(labels) {
const entries = Object.entries(labels)
.filter(([k]) => k !== "_name")
.map(([k, v]) => `${k}="${escapeLabelValue(v)}"`);
return entries.length > 0 ? `{${entries.join(",")}}` : "";
}
export function getMetricsOutput() {
const lines = [];
// 声明所有已知指标类型(即使无数据也输出 # TYPE,便于 Prometheus 发现)
const declaredTypes = {
mcp_calls_total: "counter",
active_sse_connections: "gauge",
message_delivery_total: "counter",
http_requests_total: "counter",
http_request_duration_ms: "histogram",
db_query_duration_ms: "histogram",
// Phase 3.1: Hub 数据库指标
hub_agents_online: "gauge",
hub_messages_total: "gauge",
hub_trust_scores: "gauge",
};
const seenTypes = new Set();
// Counters
for (const c of counters) {
const name = c.labels._name;
seenTypes.add(name);
const lbl = formatLabels(c.labels);
lines.push(`# TYPE ${name} counter`);
lines.push(`${name}${lbl} ${c.value}`);
}
// Gauges
for (const [name, value] of Object.entries(gauges)) {
seenTypes.add(name);
lines.push(`# TYPE ${name} gauge`);
lines.push(`${name} ${value}`);
}
// Histograms
for (const h of histograms) {
const name = h.labels._name;
seenTypes.add(name);
const lbl = formatLabels(h.labels);
lines.push(`# TYPE ${name} histogram`);
// _sum, _count
lines.push(`${name}_sum${lbl} ${Math.round(h._sum * 100) / 100}`);
lines.push(`${name}_count${lbl} ${h._count}`);
// _bucket
for (const b of HISTOGRAM_BUCKETS) {
lines.push(`${name}_bucket{le="${b}"${lbl ? ", " + lbl.slice(1, -1) : ""}} ${h.buckets[String(b)] ?? 0}`);
}
lines.push(`${name}_bucket{le="+Inf"${lbl ? ", " + lbl.slice(1, -1) : ""}} ${h.buckets["+Inf"] ?? 0}`);
}
// 声明尚未有数据的指标类型
for (const [name, type] of Object.entries(declaredTypes)) {
if (!seenTypes.has(name)) {
lines.push(`# TYPE ${name} ${type}`);
if (type === "counter" || type === "gauge") {
lines.push(`${name} 0`);
}
}
}
return lines.join("\n") + "\n";
}
// ─── 便捷函数(供 tools.ts / server.ts 埋点用) ───────────
export function incrementMcpCall(toolName, status, role) {
incrementCounter("mcp_calls_total", { tool_name: toolName, status, role });
}
export function trackHttpRequest(method, path, statusCode, durationMs) {
incrementCounter("http_requests_total", { method, path: simplifyPath(path), status: String(statusCode) });
observeHistogram("http_request_duration_ms", durationMs, { method, path: simplifyPath(path) });
}
export function trackDbQuery(operation, durationMs) {
observeHistogram("db_query_duration_ms", durationMs, { operation });
}
function simplifyPath(path) {
// /api/tasks/abc123 → /api/tasks/:id
return path.replace(/\/[a-f0-9-]{8,}/g, "/:id").replace(/\/\d+/g, "/:id");
}
// ─── Phase 3.1: Hub 数据库指标采集 ────────────────────────────────────────
/**
* 从 SQLite 数据库采集 Hub 层指标,返回 Prometheus 文本格式字符串。
* 由 server.ts 在 /metrics 路由中调用。
*/
export function collectHubMetrics(db) {
const lines = [];
// 1. hub_agents_online — 在线 Agent 数量
try {
const row = db.prepare(`SELECT COUNT(*) as cnt FROM agents WHERE status = 'online'`).get();
lines.push(`# TYPE hub_agents_online gauge`);
lines.push(`# HELP hub_agents_online Number of agents currently online`);
lines.push(`hub_agents_online ${row.cnt}`);
}
catch (e) {
lines.push(`# TYPE hub_agents_online gauge`);
lines.push(`hub_agents_online 0`);
}
// 2. hub_messages_total — 消息总数(按 status 分类)
try {
const rows = db.prepare(`SELECT status, COUNT(*) as cnt FROM messages GROUP BY status`).all();
lines.push(`# TYPE hub_messages_total gauge`);
lines.push(`# HELP hub_messages_total Total messages by delivery status`);
for (const r of rows) {
lines.push(`hub_messages_total{status="${escapeLabelValue(r.status)}"} ${r.cnt}`);
}
}
catch (e) {
lines.push(`# TYPE hub_messages_total gauge`);
lines.push(`hub_messages_total{status="unknown"} 0`);
}
// 3. hub_trust_scores — 各 Agent 的 trust_score
try {
const rows = db.prepare(`SELECT agent_id, trust_score FROM agents`).all();
lines.push(`# TYPE hub_trust_scores gauge`);
lines.push(`# HELP hub_trust_scores Trust score per agent (0-100)`);
for (const r of rows) {
lines.push(`hub_trust_scores{agent_id="${escapeLabelValue(r.agent_id)}"} ${r.trust_score}`);
}
}
catch (e) {
lines.push(`# TYPE hub_trust_scores gauge`);
lines.push(`hub_trust_scores{agent_id="unknown"} 0`);
}
return lines.join("\n") + "\n";
}
//# sourceMappingURL=metrics.js.map
+263
View File
@@ -0,0 +1,263 @@
/**
* metrics.ts — Prometheus 兼容指标(Phase 5b / Phase 3.1
* 零依赖实现,内存存储
*
* 指标:
* - mcp_calls_total{tool_name, status, role} : Counter
* - active_sse_connections : Gauge
* - message_delivery_total{status} : Counter
* - http_requests_total{method, path, status} : Counter
* - http_request_duration_ms{method, path} : Histogram (简易)
* - db_query_duration_ms{operation} : Histogram (简易)
* ──────────────────────────────── Phase 3.1 新增 ────────────────────────────────
* - hub_agents_online : Gauge
* - hub_messages_total{status} : Gauge
* - hub_trust_scores{agent_id} : Gauge
*/
import type { Database as DatabaseType } from "better-sqlite3";
// ─── Counter ─────────────────────────────────────────────
interface CounterMetric {
_type: "counter";
value: number;
labels: Record<string, string>;
}
const counters: CounterMetric[] = [];
function getOrCreateCounter(name: string, labels: Record<string, string>): CounterMetric {
for (const c of counters) {
if (c.labels._name !== name) continue;
let match = true;
for (const k of Object.keys(labels)) {
if (c.labels[k] !== labels[k]) { match = false; break; }
}
if (match) return c;
}
const c: CounterMetric = { _type: "counter", value: 0, labels: { _name: name, ...labels } };
counters.push(c);
return c;
}
export function incrementCounter(name: string, labels: Record<string, string> = {}, value = 1): void {
const c = getOrCreateCounter(name, labels);
c.value += value;
}
// ─── Gauge ───────────────────────────────────────────────
const gauges: Record<string, number> = {};
export function setGauge(name: string, value: number): void {
gauges[name] = value;
}
export function incrementGauge(name: string, value = 1): void {
gauges[name] = (gauges[name] ?? 0) + value;
}
export function decrementGauge(name: string, value = 1): void {
gauges[name] = (gauges[name] ?? 0) - value;
}
// ─── Histogram(简易分桶)─────────────────────────────────
interface HistogramMetric {
_type: "histogram";
_sum: number;
_count: number;
_min: number;
_max: number;
buckets: Record<string, number>;
labels: Record<string, string>;
}
const histograms: HistogramMetric[] = [];
const HISTOGRAM_BUCKETS = [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000];
function getOrCreateHistogram(name: string, labels: Record<string, string>): HistogramMetric {
for (const h of histograms) {
if (h.labels._name !== name) continue;
let match = true;
for (const k of Object.keys(labels)) {
if (h.labels[k] !== labels[k]) { match = false; break; }
}
if (match) return h;
}
const h: HistogramMetric = {
_type: "histogram",
_sum: 0, _count: 0, _min: Infinity, _max: 0,
buckets: {},
labels: { _name: name, ...labels },
};
for (const b of HISTOGRAM_BUCKETS) h.buckets[String(b)] = 0;
h.buckets["+Inf"] = 0;
histograms.push(h);
return h;
}
export function observeHistogram(name: string, valueMs: number, labels: Record<string, string> = {}): void {
const h = getOrCreateHistogram(name, labels);
h._sum += valueMs;
h._count += 1;
if (valueMs < h._min) h._min = valueMs;
if (valueMs > h._max) h._max = valueMs;
for (const b of HISTOGRAM_BUCKETS) {
if (valueMs <= b) h.buckets[String(b)]++;
}
h.buckets["+Inf"]++;
}
// ─── Prometheus 文本输出 ─────────────────────────────────
function escapeLabelValue(s: string): string {
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
}
function formatLabels(labels: Record<string, string>): string {
const entries = Object.entries(labels)
.filter(([k]) => k !== "_name")
.map(([k, v]) => `${k}="${escapeLabelValue(v)}"`);
return entries.length > 0 ? `{${entries.join(",")}}` : "";
}
export function getMetricsOutput(): string {
const lines: string[] = [];
// 声明所有已知指标类型(即使无数据也输出 # TYPE,便于 Prometheus 发现)
const declaredTypes: Record<string, string> = {
mcp_calls_total: "counter",
active_sse_connections: "gauge",
message_delivery_total: "counter",
http_requests_total: "counter",
http_request_duration_ms: "histogram",
db_query_duration_ms: "histogram",
// Phase 3.1: Hub 数据库指标
hub_agents_online: "gauge",
hub_messages_total: "gauge",
hub_trust_scores: "gauge",
};
const seenTypes = new Set<string>();
// Counters
for (const c of counters) {
const name = c.labels._name;
seenTypes.add(name);
const lbl = formatLabels(c.labels);
lines.push(`# TYPE ${name} counter`);
lines.push(`${name}${lbl} ${c.value}`);
}
// Gauges
for (const [name, value] of Object.entries(gauges)) {
seenTypes.add(name);
lines.push(`# TYPE ${name} gauge`);
lines.push(`${name} ${value}`);
}
// Histograms
for (const h of histograms) {
const name = h.labels._name;
seenTypes.add(name);
const lbl = formatLabels(h.labels);
lines.push(`# TYPE ${name} histogram`);
// _sum, _count
lines.push(`${name}_sum${lbl} ${Math.round(h._sum * 100) / 100}`);
lines.push(`${name}_count${lbl} ${h._count}`);
// _bucket
for (const b of HISTOGRAM_BUCKETS) {
lines.push(`${name}_bucket{le="${b}"${lbl ? ", " + lbl.slice(1, -1) : ""}} ${h.buckets[String(b)] ?? 0}`);
}
lines.push(`${name}_bucket{le="+Inf"${lbl ? ", " + lbl.slice(1, -1) : ""}} ${h.buckets["+Inf"] ?? 0}`);
}
// 声明尚未有数据的指标类型
for (const [name, type] of Object.entries(declaredTypes)) {
if (!seenTypes.has(name)) {
lines.push(`# TYPE ${name} ${type}`);
if (type === "counter" || type === "gauge") {
lines.push(`${name} 0`);
}
}
}
return lines.join("\n") + "\n";
}
// ─── 便捷函数(供 tools.ts / server.ts 埋点用) ───────────
export function incrementMcpCall(toolName: string, status: "success" | "error" | "denied", role: string): void {
incrementCounter("mcp_calls_total", { tool_name: toolName, status, role });
}
export function trackHttpRequest(method: string, path: string, statusCode: number, durationMs: number): void {
incrementCounter("http_requests_total", { method, path: simplifyPath(path), status: String(statusCode) });
observeHistogram("http_request_duration_ms", durationMs, { method, path: simplifyPath(path) });
}
export function trackDbQuery(operation: string, durationMs: number): void {
observeHistogram("db_query_duration_ms", durationMs, { operation });
}
function simplifyPath(path: string): string {
// /api/tasks/abc123 → /api/tasks/:id
return path.replace(/\/[a-f0-9-]{8,}/g, "/:id").replace(/\/\d+/g, "/:id");
}
// ─── Phase 3.1: Hub 数据库指标采集 ────────────────────────────────────────
/**
* 从 SQLite 数据库采集 Hub 层指标,返回 Prometheus 文本格式字符串。
* 由 server.ts 在 /metrics 路由中调用。
*/
export function collectHubMetrics(db: DatabaseType): string {
const lines: string[] = [];
// 1. hub_agents_online — 在线 Agent 数量
try {
const row = db.prepare(
`SELECT COUNT(*) as cnt FROM agents WHERE status = 'online'`
).get() as { cnt: number };
lines.push(`# TYPE hub_agents_online gauge`);
lines.push(`# HELP hub_agents_online Number of agents currently online`);
lines.push(`hub_agents_online ${row.cnt}`);
} catch (e) {
lines.push(`# TYPE hub_agents_online gauge`);
lines.push(`hub_agents_online 0`);
}
// 2. hub_messages_total — 消息总数(按 status 分类)
try {
const rows = db.prepare(
`SELECT status, COUNT(*) as cnt FROM messages GROUP BY status`
).all() as { status: string; cnt: number }[];
lines.push(`# TYPE hub_messages_total gauge`);
lines.push(`# HELP hub_messages_total Total messages by delivery status`);
for (const r of rows) {
lines.push(`hub_messages_total{status="${escapeLabelValue(r.status)}"} ${r.cnt}`);
}
} catch (e) {
lines.push(`# TYPE hub_messages_total gauge`);
lines.push(`hub_messages_total{status="unknown"} 0`);
}
// 3. hub_trust_scores — 各 Agent 的 trust_score
try {
const rows = db.prepare(
`SELECT agent_id, trust_score FROM agents`
).all() as { agent_id: string; trust_score: number }[];
lines.push(`# TYPE hub_trust_scores gauge`);
lines.push(`# HELP hub_trust_scores Trust score per agent (0-100)`);
for (const r of rows) {
lines.push(`hub_trust_scores{agent_id="${escapeLabelValue(r.agent_id)}"} ${r.trust_score}`);
}
} catch (e) {
lines.push(`# TYPE hub_trust_scores gauge`);
lines.push(`hub_trust_scores{agent_id="unknown"} 0`);
}
return lines.join("\n") + "\n";
}
+223
View File
@@ -0,0 +1,223 @@
import { type Task, type Pipeline, type PipelineTask } from "./db.js";
import type { DepType } from "./repo/types.js";
import { taskRepo } from "./repo/sqlite-impl.js";
export type TaskCreateInput = {
description: string;
context?: string;
priority?: "low" | "normal" | "high" | "urgent";
assigned_to?: string;
assigned_by: string;
pipeline_id?: string;
required_capability?: string;
tags?: string[];
due_at?: number;
};
/**
* 创建任务
*/
export declare function createTask(input: TaskCreateInput): Task;
/**
* 分配任务(inbox → assigned 或重新分配)
*/
export declare function assignTask(taskId: string, toAgent: string, operatorId: string): Task;
/**
* 认领任务(inbox → assigned
*/
export declare function claimTask(taskId: string, agentId: string): Task;
/**
* 取消任务
*/
export declare function cancelTask(taskId: string, operatorId: string, reason?: string): Task;
/**
* 更新任务状态(带状态机校验)
*/
export declare function updateTaskStatus(taskId: string, status: string, operatorId: string, result?: string | null, progress?: number): Task;
/**
* 多维查询任务
*/
export declare function listTasks(filters: {
assigned_to?: string;
assigned_by?: string;
status?: string;
pipeline_id?: string;
required_capability?: string;
limit?: number;
}): Task[];
export type PipelineCreateInput = {
name: string;
description?: string;
creator: string;
config?: {
auto_assign?: boolean;
capability_match?: boolean;
};
};
/**
* 创建 Pipeline
*/
export declare function createPipeline(input: PipelineCreateInput): Pipeline;
/**
* 激活 Pipeline
*/
export declare function activatePipeline(pipelineId: string, operatorId: string): Pipeline;
/**
* 完成 Pipeline
*/
export declare function completePipeline(pipelineId: string, operatorId: string): Pipeline;
/**
* 取消 Pipeline
*/
export declare function cancelPipeline(pipelineId: string, operatorId: string): Pipeline;
/**
* 添加任务到 Pipeline
*/
export declare function addTaskToPipeline(pipelineId: string, taskId: string, orderIndex?: number, operatorId?: string): PipelineTask;
/**
* 获取 Pipeline 进度
*/
export declare function getPipelineStatus(pipelineId: string): {
pipeline: Pipeline;
tasks: Task[];
stats: {
total: number;
inbox: number;
assigned: number;
in_progress: number;
completed: number;
failed: number;
cancelled: number;
};
};
export type CapabilityInput = {
agent_id: string;
capability: string;
params?: Record<string, unknown>;
verified?: boolean;
};
/**
* 注册 Agent 能力
*/
export declare function registerCapability(input: CapabilityInput): {
id: string;
};
/**
* 智能推荐任务执行方
*/
export declare function suggestAssignee(taskId: string): Array<{
agent_id: string;
name: string;
capability_match: boolean;
online: boolean;
current_tasks: number;
}>;
/**
* 添加任务依赖关系(含环检测)
*/
export declare function addDependency(upstreamId: string, downstreamId: string, depType?: DepType, operatorId?: string): {
dependency: ReturnType<typeof taskRepo.addDependency>;
downstream_updated: boolean;
};
/**
* 删除依赖关系
*/
export declare function removeDependency(upstreamId: string, downstreamId: string, operatorId?: string): {
removed: boolean;
downstream_ready: boolean;
};
/**
* 获取任务的上下游依赖
*/
export declare function getDependencies(taskId: string): {
upstreams: Array<{
task_id: string;
status: string;
dep_type: string;
dep_status: string;
}>;
downstreams: Array<{
task_id: string;
status: string;
dep_type: string;
dep_status: string;
}>;
};
/**
* 检查任务依赖是否满足
*/
export declare function checkDependenciesSatisfied(taskId: string): {
satisfied: boolean;
pending_deps: Array<{
task_id: string;
dep_type: string;
}>;
};
/**
* 创建并行组
* 将多个任务标记为同一 parallel_group,表示它们可以并行执行。
* 同一 parallel_group 内的任务在 Pipeline 中逻辑上是并行的。
*/
export declare function createParallelGroup(taskIds: string[], operatorId?: string): {
group_id: string;
task_count: number;
tasks: Array<{
id: string;
parallel_group: string;
}>;
};
/**
* 获取并行组信息
*/
export declare function getParallelGroup(groupId: string): {
group_id: string;
tasks: Array<{
id: string;
status: string;
description: string;
}>;
};
/**
* 请求交接
* 当前负责人将任务交接给目标 Agent。目标 Agent 必须 accept/reject。
* 交接期间任务状态保持不变,handoff_status 设为 'requested'。
*/
export declare function requestHandoff(taskId: string, targetAgentId: string, operatorId: string): {
task_id: string;
handoff_status: string;
from: string;
to: string;
};
/**
* 接受交接
* 目标 Agent 接受交接,任务 assigned_to 转移。
*/
export declare function acceptHandoff(taskId: string, operatorId: string): {
task_id: string;
new_assignee: string;
};
/**
* 拒绝交接
* 目标 Agent 拒绝交接,handoff_status 回退为 null。
*/
export declare function rejectHandoff(taskId: string, operatorId: string, reason?: string): {
task_id: string;
rejected_by: string;
reason: string;
};
/**
* 添加质量门
* 在 Pipeline 中设置质量门。质量门在指定 order_index 之后阻塞后续任务。
*/
export declare function addQualityGate(pipelineId: string, gateName: string, criteria: string, afterOrder: number, operatorId: string): {
gate: ReturnType<typeof taskRepo.addQualityGate>;
pipeline_id: string;
};
/**
* 评估质量门
* 评估者对质量门进行通过/失败判定。
* 质量门失败时,检查 Pipeline 中是否有 after_order 之后的任务需要阻止。
*/
export declare function evaluateQualityGate(gateId: string, status: "passed" | "failed", evaluatorId: string, result?: string): {
gate_id: string;
status: string;
blocked_tasks: string[];
};
+730
View File
@@ -0,0 +1,730 @@
/**
* orchestrator.ts — Task Orchestrator Service (Phase 4a + Phase 4b)
*
* 任务状态机 + Pipeline 管理 + Agent 能力匹配 + 依赖链 + 质量门
*/
import { randomUUID } from "crypto";
import { db } from "./db.js";
import { pushToAgent, onlineAgents } from "./sse.js";
import { auditLog, recalculateTrustScore } from "./security.js";
import { taskRepo } from "./repo/sqlite-impl.js";
function getOne(sql, ...params) {
return db.prepare(sql).get(...params);
}
function getAll(sql, ...params) {
return db.prepare(sql).all(...params);
}
// ─── 常量 ──────────────────────────────────────────────
/** 合法的状态转换映射 */
const VALID_TRANSITIONS = {
inbox: ["assigned", "cancelled"],
assigned: ["waiting", "in_progress", "cancelled"],
waiting: ["in_progress", "cancelled"], // Phase 4b: 依赖满足后可开始
pending: ["in_progress", "cancelled"], // 兼容旧数据
in_progress: ["completed", "failed", "cancelled"],
completed: [], // 终态
failed: [], // 终态
cancelled: [], // 终态
};
/** 终态集合 */
const TERMINAL_STATES = new Set(["completed", "failed", "cancelled"]);
// ─── 状态机校验 ──────────────────────────────────────────
function validateTransition(from, to) {
const allowed = VALID_TRANSITIONS[from];
if (!allowed) {
throw new Error(`Unknown source status: ${from}`);
}
if (!allowed.includes(to)) {
throw new Error(`Invalid transition: ${from}${to}. Allowed: [${allowed.join(", ")}]`);
}
}
/**
* 创建任务
*/
export function createTask(input) {
const now = Date.now();
const status = input.assigned_to ? "assigned" : "inbox";
const task = {
id: `task_${now}_${randomUUID().slice(0, 6)}`,
assigned_by: input.assigned_by,
assigned_to: input.assigned_to ?? "",
description: input.description,
context: input.context ?? null,
priority: input.priority ?? "normal",
status,
result: null,
progress: 0,
pipeline_id: input.pipeline_id ?? null,
order_index: 0,
required_capability: input.required_capability ?? null,
due_at: input.due_at ?? null,
assigned_at: input.assigned_to ? now : null,
completed_at: null,
tags: input.tags ? JSON.stringify(input.tags) : "[]",
created_at: now,
updated_at: now,
}; // Phase 4b: parallel_group/handoff_status 由 DB DEFAULT 填充
db.prepare(`INSERT INTO tasks (id, assigned_by, assigned_to, description, context, priority, status, result, progress, pipeline_id, order_index, required_capability, due_at, assigned_at, completed_at, tags, created_at, updated_at)
VALUES (@id, @assigned_by, @assigned_to, @description, @context, @priority, @status, @result, @progress, @pipeline_id, @order_index, @required_capability, @due_at, @assigned_at, @completed_at, @tags, @created_at, @updated_at)`).run(task);
auditLog("create_task", input.assigned_by, task.id, `status=${status}`);
return task;
}
/**
* 分配任务(inbox → assigned 或重新分配)
*/
export function assignTask(taskId, toAgent, operatorId) {
const task = getOne(`SELECT * FROM tasks WHERE id=?`, taskId);
if (!task)
throw new Error(`Task not found: ${taskId}`);
if (TERMINAL_STATES.has(task.status))
throw new Error(`Cannot assign task in terminal state: ${task.status}`);
const now = Date.now();
db.prepare(`UPDATE tasks SET assigned_to=?, assigned_at=?, status='assigned', updated_at=? WHERE id=?`).run(toAgent, now, now, taskId);
auditLog("assign_task", operatorId, taskId, `to=${toAgent}`);
pushToAgent(toAgent, {
type: "task_assigned",
content: JSON.stringify({
task_id: taskId,
description: task.description,
priority: task.priority,
context: task.context,
from: operatorId,
hint: "调用 update_task_status(in_progress) 开始执行,完成后调用 update_task_status(completed) 并携带结果。",
}),
});
return getOne(`SELECT * FROM tasks WHERE id=?`, taskId);
}
/**
* 认领任务(inbox → assigned
*/
export function claimTask(taskId, agentId) {
const task = getOne(`SELECT * FROM tasks WHERE id=?`, taskId);
if (!task)
throw new Error(`Task not found: ${taskId}`);
if (task.status !== "inbox")
throw new Error(`Cannot claim task in status: ${task.status}. Only inbox tasks can be claimed.`);
return assignTask(taskId, agentId, agentId);
}
/**
* 取消任务
*/
export function cancelTask(taskId, operatorId, reason) {
const task = getOne(`SELECT * FROM tasks WHERE id=?`, taskId);
if (!task)
throw new Error(`Task not found: ${taskId}`);
if (TERMINAL_STATES.has(task.status))
throw new Error(`Cannot cancel task in terminal state: ${task.status}`);
const now = Date.now();
db.prepare(`UPDATE tasks SET status='cancelled', result=?, updated_at=? WHERE id=?`).run(reason ?? "Cancelled by " + operatorId, now, taskId);
auditLog("cancel_task", operatorId, taskId, reason ?? "cancelled");
if (task.assigned_to) {
pushToAgent(task.assigned_to, {
type: "task_cancelled",
content: JSON.stringify({ task_id: taskId, reason, cancelled_by: operatorId }),
});
}
pushToAgent(task.assigned_by, {
type: "task_cancelled",
content: JSON.stringify({ task_id: taskId, reason, cancelled_by: operatorId }),
});
return getOne(`SELECT * FROM tasks WHERE id=?`, taskId);
}
/**
* 更新任务状态(带状态机校验)
*/
export function updateTaskStatus(taskId, status, operatorId, result, progress) {
const task = getOne(`SELECT * FROM tasks WHERE id=?`, taskId);
if (!task)
throw new Error(`Task not found: ${taskId}`);
validateTransition(task.status, status);
const now = Date.now();
const completedAt = (status === "completed" || status === "failed") ? now : null;
db.prepare(`UPDATE tasks SET status=?, result=?, progress=?, completed_at=?, updated_at=? WHERE id=?`).run(status, result ?? task.result, progress ?? task.progress, completedAt ?? task.completed_at, now, taskId);
auditLog("update_task_status", operatorId, taskId, `${task.status}${status}`);
pushToAgent(task.assigned_by, {
type: "task_update",
content: JSON.stringify({
task_id: taskId,
status,
result: result ?? null,
progress: progress ?? task.progress,
from: operatorId,
}),
});
// Phase 4b: 任务完成时级联满足下游依赖
if (status === "completed") {
cascadeDependencySatisfaction(taskId);
}
return getOne(`SELECT * FROM tasks WHERE id=?`, taskId);
}
/**
* 多维查询任务
*/
export function listTasks(filters) {
const conditions = [];
const params = [];
if (filters.assigned_to) {
conditions.push("assigned_to = ?");
params.push(filters.assigned_to);
}
if (filters.assigned_by) {
conditions.push("assigned_by = ?");
params.push(filters.assigned_by);
}
if (filters.status && filters.status !== "all") {
conditions.push("status = ?");
params.push(filters.status);
}
if (filters.pipeline_id) {
conditions.push("pipeline_id = ?");
params.push(filters.pipeline_id);
}
if (filters.required_capability) {
conditions.push("required_capability = ?");
params.push(filters.required_capability);
}
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
const limit = filters.limit ?? 50;
return getAll(`SELECT * FROM tasks ${where} ORDER BY created_at DESC LIMIT ?`, ...params, limit);
}
/**
* 创建 Pipeline
*/
export function createPipeline(input) {
const now = Date.now();
const pipeline = {
id: `pipe_${now}_${randomUUID().slice(0, 6)}`,
name: input.name,
description: input.description ?? null,
status: "draft",
creator: input.creator,
config: input.config ? JSON.stringify(input.config) : null,
created_at: now,
updated_at: now,
};
db.prepare(`INSERT INTO pipelines VALUES (@id,@name,@description,@status,@creator,@config,@created_at,@updated_at)`).run(pipeline);
auditLog("create_pipeline", input.creator, pipeline.id, `name=${input.name}`);
return pipeline;
}
/**
* 激活 Pipeline
*/
export function activatePipeline(pipelineId, operatorId) {
return updatePipelineStatus(pipelineId, "active", operatorId);
}
/**
* 完成 Pipeline
*/
export function completePipeline(pipelineId, operatorId) {
return updatePipelineStatus(pipelineId, "completed", operatorId);
}
/**
* 取消 Pipeline
*/
export function cancelPipeline(pipelineId, operatorId) {
return updatePipelineStatus(pipelineId, "cancelled", operatorId);
}
function updatePipelineStatus(pipelineId, status, operatorId) {
const pipeline = getOne(`SELECT * FROM pipelines WHERE id=?`, pipelineId);
if (!pipeline)
throw new Error(`Pipeline not found: ${pipelineId}`);
const now = Date.now();
db.prepare(`UPDATE pipelines SET status=?, updated_at=? WHERE id=?`).run(status, now, pipelineId);
auditLog("update_pipeline_status", operatorId, pipelineId, `${pipeline.status}${status}`);
return getOne(`SELECT * FROM pipelines WHERE id=?`, pipelineId);
}
/**
* 添加任务到 Pipeline
*/
export function addTaskToPipeline(pipelineId, taskId, orderIndex, operatorId) {
const pipeline = getOne(`SELECT * FROM pipelines WHERE id=?`, pipelineId);
if (!pipeline)
throw new Error(`Pipeline not found: ${pipelineId}`);
const task = getOne(`SELECT * FROM tasks WHERE id=?`, taskId);
if (!task)
throw new Error(`Task not found: ${taskId}`);
let order = orderIndex ?? 0;
if (orderIndex === undefined) {
const maxRow = getOne(`SELECT MAX(order_index) as max_order FROM pipeline_tasks WHERE pipeline_id=?`, pipelineId);
order = (maxRow?.max_order ?? -1) + 1;
}
const now = Date.now();
const pt = {
id: `pt_${now}_${randomUUID().slice(0, 6)}`,
pipeline_id: pipelineId,
task_id: taskId,
order_index: order,
created_at: now,
};
db.prepare(`INSERT OR REPLACE INTO pipeline_tasks VALUES (@id,@pipeline_id,@task_id,@order_index,@created_at)`).run(pt);
db.prepare(`UPDATE tasks SET pipeline_id=?, updated_at=? WHERE id=?`).run(pipelineId, now, taskId);
if (operatorId) {
auditLog("add_task_to_pipeline", operatorId, pipelineId, `task=${taskId},order=${order}`);
}
return pt;
}
/**
* 获取 Pipeline 进度
*/
export function getPipelineStatus(pipelineId) {
const pipeline = getOne(`SELECT * FROM pipelines WHERE id=?`, pipelineId);
if (!pipeline)
throw new Error(`Pipeline not found: ${pipelineId}`);
const tasks = getAll(`SELECT t.* FROM tasks t JOIN pipeline_tasks pt ON t.id=pt.task_id WHERE pt.pipeline_id=? ORDER BY pt.order_index ASC`, pipelineId);
const stats = {
total: tasks.length,
inbox: 0, assigned: 0, in_progress: 0,
completed: 0, failed: 0, cancelled: 0,
};
for (const t of tasks) {
const s = t.status;
if (s in stats)
stats[s]++;
}
return { pipeline, tasks, stats };
}
/**
* 注册 Agent 能力
*/
export function registerCapability(input) {
const now = Date.now();
const id = `cap_${now}_${randomUUID().slice(0, 6)}`;
db.prepare(`INSERT INTO agent_capabilities VALUES (?,?,?,?,?,?)`).run(id, input.agent_id, input.capability, input.params ? JSON.stringify(input.params) : null, input.verified ? 1 : 0, input.verified ? now : null, now);
auditLog("register_capability", input.agent_id, id, `capability=${input.capability}`);
// Phase 5a Day 2: 验证的能力影响信任评分
if (input.verified) {
try {
recalculateTrustScore(input.agent_id);
}
catch { }
}
return { id };
}
/**
* 智能推荐任务执行方
*/
export function suggestAssignee(taskId) {
const task = getOne(`SELECT * FROM tasks WHERE id=?`, taskId);
if (!task)
throw new Error(`Task not found: ${taskId}`);
const agents = getAll(`SELECT agent_id, name, status FROM agents WHERE role != 'admin' OR role IS NULL`);
const onlineSet = new Set(onlineAgents());
const results = [];
for (const agent of agents) {
let capability_match = false;
if (task.required_capability) {
const cap = getOne(`SELECT COUNT(*) as count FROM agent_capabilities WHERE agent_id=? AND capability=?`, agent.agent_id, task.required_capability);
capability_match = (cap?.count ?? 0) > 0;
}
else {
capability_match = true;
}
const taskCount = getOne(`SELECT COUNT(*) as count FROM tasks WHERE assigned_to=? AND status IN ('assigned', 'in_progress')`, agent.agent_id);
results.push({
agent_id: agent.agent_id,
name: agent.name,
capability_match,
online: onlineSet.has(agent.agent_id),
current_tasks: taskCount?.count ?? 0,
});
}
results.sort((a, b) => {
if (a.capability_match !== b.capability_match)
return b.capability_match ? 1 : -1;
if (a.online !== b.online)
return b.online ? 1 : -1;
return a.current_tasks - b.current_tasks;
});
return results;
}
// ═══════════════════════════════════════════════════════════════
// Phase 4b — 依赖链核心
// ═══════════════════════════════════════════════════════════════
/**
* 级联满足下游依赖
* 当上游任务完成时,将所有 finish_to_start 类型的下游依赖标记为 satisfied,
* 并检查下游任务是否所有依赖都满足——如果满足则从 waiting 变为 assigned。
*/
function cascadeDependencySatisfaction(completedTaskId) {
// 标记依赖为 satisfied
const satisfiedCount = taskRepo.satisfyDownstream(completedTaskId);
if (satisfiedCount === 0)
return;
// 找到所有被影响的下游任务(这些任务有 upstream = completedTaskId
const downstreams = getAll(`SELECT DISTINCT downstream_id FROM task_dependencies WHERE upstream_id=? AND status='satisfied'`, completedTaskId);
for (const d of downstreams) {
const task = getOne(`SELECT * FROM tasks WHERE id=?`, d.downstream_id);
if (!task || task.status !== "waiting")
continue;
// 检查该任务是否所有上游依赖都满足
if (taskRepo.checkDependenciesSatisfied(d.downstream_id)) {
taskRepo.setTaskReady(d.downstream_id);
auditLog("cascade_ready", "system", d.downstream_id, `upstream=${completedTaskId}`);
// 通知下游任务负责人
if (task.assigned_to) {
pushToAgent(task.assigned_to, {
type: "dependency_satisfied",
content: JSON.stringify({
task_id: d.downstream_id,
satisfied_by: completedTaskId,
hint: "所有上游依赖已满足,任务可以开始执行。调用 update_task_status(in_progress) 开始。",
}),
});
}
}
}
}
/**
* 添加任务依赖关系(含环检测)
*/
export function addDependency(upstreamId, downstreamId, depType = "finish_to_start", operatorId) {
// 验证任务存在
const upstream = getOne(`SELECT * FROM tasks WHERE id=?`, upstreamId);
if (!upstream)
throw new Error(`Upstream task not found: ${upstreamId}`);
const downstream = getOne(`SELECT * FROM tasks WHERE id=?`, downstreamId);
if (!downstream)
throw new Error(`Downstream task not found: ${downstreamId}`);
// 环检测
if (taskRepo.wouldCreateCycle(upstreamId, downstreamId)) {
throw new Error(`Adding dependency ${upstreamId}${downstreamId} would create a cycle`);
}
const dependency = taskRepo.addDependency(upstreamId, downstreamId, depType);
// 如果上游已完成,立即标记为 satisfied
let downstream_updated = false;
if (upstream.status === "completed" && depType === "finish_to_start") {
taskRepo.satisfyDownstream(upstreamId);
// 检查下游是否所有依赖都满足
if (downstream.status === "waiting" && taskRepo.checkDependenciesSatisfied(downstreamId)) {
taskRepo.setTaskReady(downstreamId);
downstream_updated = true;
}
}
else if (downstream.status === "assigned" || downstream.status === "inbox") {
// 下游任务有未满足依赖,设为 waiting
taskRepo.setTaskWaiting(downstreamId);
downstream_updated = true;
}
if (operatorId) {
auditLog("add_dependency", operatorId, upstreamId, `${downstreamId}(${depType})`);
}
return { dependency, downstream_updated };
}
/**
* 删除依赖关系
*/
export function removeDependency(upstreamId, downstreamId, operatorId) {
taskRepo.removeDependency(upstreamId, downstreamId);
// 检查下游任务是否因依赖减少而可以执行
let downstream_ready = false;
const downstream = getOne(`SELECT * FROM tasks WHERE id=?`, downstreamId);
if (downstream && downstream.status === "waiting") {
if (taskRepo.checkDependenciesSatisfied(downstreamId)) {
taskRepo.setTaskReady(downstreamId);
downstream_ready = true;
}
}
if (operatorId) {
auditLog("remove_dependency", operatorId, upstreamId, `${downstreamId}`);
}
return { removed: true, downstream_ready };
}
/**
* 获取任务的上下游依赖
*/
export function getDependencies(taskId) {
const { upstreams, downstreams } = taskRepo.getDependencies(taskId);
const mapDep = (dep) => {
const task = getOne(`SELECT id, status FROM tasks WHERE id=?`, dep.downstream_id);
return {
task_id: dep.downstream_id,
status: task?.status ?? "unknown",
dep_type: dep.dep_type,
dep_status: dep.status,
};
};
const mapUp = (dep) => {
const task = getOne(`SELECT id, status FROM tasks WHERE id=?`, dep.upstream_id);
return {
task_id: dep.upstream_id,
status: task?.status ?? "unknown",
dep_type: dep.dep_type,
dep_status: dep.status,
};
};
return {
upstreams: upstreams.map(mapUp),
downstreams: downstreams.map(mapDep),
};
}
/**
* 检查任务依赖是否满足
*/
export function checkDependenciesSatisfied(taskId) {
const { upstreams } = taskRepo.getDependencies(taskId);
const pendingDeps = upstreams
.filter(d => d.status === "pending")
.map(d => ({ task_id: d.upstream_id, dep_type: d.dep_type }));
return {
satisfied: pendingDeps.length === 0,
pending_deps: pendingDeps,
};
}
// ═══════════════════════════════════════════════════════════════
// Phase 4b Day 2 — 并行组
// ═══════════════════════════════════════════════════════════════
/**
* 创建并行组
* 将多个任务标记为同一 parallel_group,表示它们可以并行执行。
* 同一 parallel_group 内的任务在 Pipeline 中逻辑上是并行的。
*/
export function createParallelGroup(taskIds, operatorId) {
if (taskIds.length < 2) {
throw new Error("Parallel group requires at least 2 tasks");
}
if (taskIds.length > 10) {
throw new Error("Parallel group cannot exceed 10 tasks");
}
const now = Date.now();
const groupId = `pg_${now}_${randomUUID().slice(0, 6)}`;
// 验证所有任务存在
const tasks = [];
for (const taskId of taskIds) {
const task = getOne(`SELECT id FROM tasks WHERE id=?`, taskId);
if (!task)
throw new Error(`Task not found: ${taskId}`);
tasks.push({ id: taskId, parallel_group: groupId });
}
// 批量更新 parallel_group
const updateStmt = db.prepare(`UPDATE tasks SET parallel_group=?, updated_at=? WHERE id=?`);
const updateMany = db.transaction((ids, group, ts) => {
for (const id of ids) {
updateStmt.run(group, ts, id);
}
});
updateMany(taskIds, groupId, now);
if (operatorId) {
auditLog("create_parallel_group", operatorId, groupId, `tasks=${taskIds.join(",")}`);
}
return { group_id: groupId, task_count: taskIds.length, tasks };
}
/**
* 获取并行组信息
*/
export function getParallelGroup(groupId) {
const tasks = getAll(`SELECT id, status, description FROM tasks WHERE parallel_group=?`, groupId);
if (tasks.length === 0) {
throw new Error(`Parallel group not found: ${groupId}`);
}
return {
group_id: groupId,
tasks: tasks.map(t => ({ id: t.id, status: t.status, description: t.description })),
};
}
// ═══════════════════════════════════════════════════════════════
// Phase 4b Day 3 — 交接协议(Handoff Protocol
// ═══════════════════════════════════════════════════════════════
/**
* 请求交接
* 当前负责人将任务交接给目标 Agent。目标 Agent 必须 accept/reject。
* 交接期间任务状态保持不变,handoff_status 设为 'requested'。
*/
export function requestHandoff(taskId, targetAgentId, operatorId) {
const task = getOne(`SELECT * FROM tasks WHERE id=?`, taskId);
if (!task)
throw new Error(`Task not found: ${taskId}`);
if (TERMINAL_STATES.has(task.status))
throw new Error(`Cannot handoff task in terminal state: ${task.status}`);
if (task.handoff_status === "requested")
throw new Error(`Handoff already requested for task: ${taskId}`);
if (!task.assigned_to)
throw new Error(`Task has no assignee: ${taskId}`);
// 只有负责人或创建者可以请求交接
if (operatorId !== task.assigned_to && operatorId !== task.assigned_by) {
throw new Error(`Only assignee or creator can request handoff. Current: ${operatorId}, assignee: ${task.assigned_to}`);
}
const now = Date.now();
db.prepare(`UPDATE tasks SET handoff_status='requested', handoff_to=?, updated_at=? WHERE id=?`).run(targetAgentId, now, taskId);
auditLog("request_handoff", operatorId, taskId, `${targetAgentId}`);
// SSE 通知目标 Agent
pushToAgent(targetAgentId, {
type: "handoff_requested",
content: JSON.stringify({
task_id: taskId,
description: task.description,
from: task.assigned_to,
priority: task.priority,
hint: "调用 accept_handoff 接管任务,或 reject_handoff 拒绝交接。",
}),
});
// SSE 通知原负责人
if (task.assigned_to !== operatorId) {
pushToAgent(task.assigned_to, {
type: "handoff_requested",
content: JSON.stringify({
task_id: taskId,
from: operatorId,
to: targetAgentId,
}),
});
}
return { task_id: taskId, handoff_status: "requested", from: task.assigned_to, to: targetAgentId };
}
/**
* 接受交接
* 目标 Agent 接受交接,任务 assigned_to 转移。
*/
export function acceptHandoff(taskId, operatorId) {
const task = getOne(`SELECT * FROM tasks WHERE id=?`, taskId);
if (!task)
throw new Error(`Task not found: ${taskId}`);
if (task.handoff_status !== "requested")
throw new Error(`No pending handoff for task: ${taskId}`);
if (!task.handoff_to)
throw new Error(`Task handoff_to is null: ${taskId}`);
// 只有目标 Agent 可以接受
if (operatorId !== task.handoff_to) {
throw new Error(`Only the target agent can accept handoff. Target: ${task.handoff_to}, caller: ${operatorId}`);
}
const now = Date.now();
const oldAssignee = task.assigned_to;
db.prepare(`UPDATE tasks SET assigned_to=?, handoff_status='accepted', handoff_to=null, assigned_at=?, updated_at=? WHERE id=?`).run(operatorId, now, now, taskId);
auditLog("accept_handoff", operatorId, taskId, `from=${oldAssignee}`);
// SSE 通知原负责人
if (oldAssignee) {
pushToAgent(oldAssignee, {
type: "handoff_accepted",
content: JSON.stringify({
task_id: taskId,
accepted_by: operatorId,
}),
});
}
// SSE 通知创建者
if (task.assigned_by && task.assigned_by !== oldAssignee && task.assigned_by !== operatorId) {
pushToAgent(task.assigned_by, {
type: "handoff_accepted",
content: JSON.stringify({
task_id: taskId,
from: oldAssignee,
to: operatorId,
}),
});
}
// SSE 通知新负责人(任务已分配给你)
pushToAgent(operatorId, {
type: "task_assigned",
content: JSON.stringify({
task_id: taskId,
description: task.description,
priority: task.priority,
context: task.context,
from: oldAssignee,
hint: "你已接管此任务。调用 update_task_status(in_progress) 开始执行。",
}),
});
return { task_id: taskId, new_assignee: operatorId };
}
/**
* 拒绝交接
* 目标 Agent 拒绝交接,handoff_status 回退为 null。
*/
export function rejectHandoff(taskId, operatorId, reason) {
const task = getOne(`SELECT * FROM tasks WHERE id=?`, taskId);
if (!task)
throw new Error(`Task not found: ${taskId}`);
if (task.handoff_status !== "requested")
throw new Error(`No pending handoff for task: ${taskId}`);
// 只有目标 Agent 可以拒绝
if (operatorId !== task.handoff_to) {
throw new Error(`Only the target agent can reject handoff. Target: ${task.handoff_to}, caller: ${operatorId}`);
}
const now = Date.now();
const rejectReason = reason ?? "No reason provided";
db.prepare(`UPDATE tasks SET handoff_status=null, handoff_to=null, updated_at=? WHERE id=?`).run(now, taskId);
auditLog("reject_handoff", operatorId, taskId, `reason=${rejectReason}`);
// SSE 通知原负责人
if (task.assigned_to) {
pushToAgent(task.assigned_to, {
type: "handoff_rejected",
content: JSON.stringify({
task_id: taskId,
rejected_by: operatorId,
reason: rejectReason,
}),
});
}
return { task_id: taskId, rejected_by: operatorId, reason: rejectReason };
}
// ═══════════════════════════════════════════════════════════════
// Phase 4b Day 3 — 质量门(Quality Gate)业务逻辑
// ═══════════════════════════════════════════════════════════════
/**
* 添加质量门
* 在 Pipeline 中设置质量门。质量门在指定 order_index 之后阻塞后续任务。
*/
export function addQualityGate(pipelineId, gateName, criteria, afterOrder, operatorId) {
// 验证 Pipeline 存在
const pipeline = getOne(`SELECT * FROM pipelines WHERE id=?`, pipelineId);
if (!pipeline)
throw new Error(`Pipeline not found: ${pipelineId}`);
const now = Date.now();
const gate = taskRepo.addQualityGate({
pipeline_id: pipelineId,
gate_name: gateName,
criteria,
after_order: afterOrder,
status: "pending",
evaluator_id: null,
result: null,
evaluated_at: null,
created_at: now,
});
auditLog("add_quality_gate", operatorId, gate.id, `pipeline=${pipelineId}, name=${gateName}, after=${afterOrder}`);
return { gate, pipeline_id: pipelineId };
}
/**
* 评估质量门
* 评估者对质量门进行通过/失败判定。
* 质量门失败时,检查 Pipeline 中是否有 after_order 之后的任务需要阻止。
*/
export function evaluateQualityGate(gateId, status, evaluatorId, result) {
// 验证质量门存在
const gate = getOne(`SELECT id, pipeline_id, status, after_order FROM quality_gates WHERE id=?`, gateId);
if (!gate)
throw new Error(`Quality gate not found: ${gateId}`);
if (gate.status !== "pending")
throw new Error(`Quality gate already evaluated: ${gate.status}`);
// 更新质量门状态
taskRepo.updateQualityGateStatus(gateId, status, evaluatorId, result);
auditLog("evaluate_quality_gate", evaluatorId, gateId, `status=${status}`);
// 如果质量门失败,找出 Pipeline 中 after_order 之后的任务并设为 waiting
const blockedTasks = [];
if (status === "failed") {
const laterTasks = getAll(`SELECT pt.task_id, t.status
FROM pipeline_tasks pt
JOIN tasks t ON pt.task_id = t.id
WHERE pt.pipeline_id=? AND pt.order_index > ?
AND t.status NOT IN ('completed', 'failed', 'cancelled')`, gate.pipeline_id, gate.after_order);
for (const t of laterTasks) {
if (t.status !== "waiting") {
taskRepo.setTaskWaiting(t.task_id);
blockedTasks.push(t.task_id);
const task = getOne(`SELECT assigned_to FROM tasks WHERE id=?`, t.task_id);
if (task?.assigned_to) {
pushToAgent(task.assigned_to, {
type: "quality_gate_failed",
content: JSON.stringify({
task_id: t.task_id,
gate_id: gateId,
gate_name: gateId,
hint: "前置质量门未通过,任务已暂停。等待质量门重新评估。",
}),
});
}
}
}
}
return { gate_id: gateId, status, blocked_tasks: blockedTasks };
}
//# sourceMappingURL=orchestrator.js.map
File diff suppressed because it is too large Load Diff
+79
View File
@@ -0,0 +1,79 @@
/**
* repo/interfaces.ts — 数据访问层接口定义 (Phase 2 Day 2, Phase 4b 扩展)
*
* 将 tools.ts/server.ts 中散落的 msgStmt/taskStmt/consumedStmt 直接 SQL 调用
* 统一到接口层,便于测试 mock 和未来数据源替换。
*/
import type { Message, Task, ConsumedEntry } from "../db.js";
import type { TaskDependency, QualityGate, DepType, GateStatus } from "./types.js";
export interface IMessageRepo {
/** 插入新消息 */
insert(msg: Message): void;
/** 标记消息为已投递 */
markDelivered(id: string): void;
/** 标记消息为已读 */
markRead(id: string): void;
/** 标记消息为已确认 */
markAcknowledged(id: string): void;
/** 批量标记指定接收方所有未读消息为已投递 */
markAllDelivered(toAgent: string): void;
/** 查询指定接收方的待处理消息(status=unread */
pendingFor(toAgent: string): Message[];
/** 按 ID 查询消息 */
getById(id: string): Message | undefined;
/** 按接收方 + 状态查询消息 */
listByStatus(toAgent: string, status: string): Message[];
/** 更新消息状态(REST PATCH 用) */
updateStatus(id: string, status: string): void;
/** 查询指定接收方在指定时间戳之后的消息(用于 SSE 断线重连回放) */
listSince(toAgent: string, since: number): Message[];
}
export interface ITaskRepo {
/** 插入新任务 */
insert(task: Task): void;
/** 按 ID 查询任务 */
getById(id: string): Task | undefined;
/** 更新任务状态/结果/进度 */
update(id: string, status: string, result: string | null, progress: number): void;
/** 分配任务(设置 assigned_to + status=assigned */
assignTo(id: string, assignedTo: string): void;
/** 按执行者 + 状态列出任务 */
listFor(assignedTo: string, status: string): Task[];
/** 按 Pipeline 列出任务 */
listByPipeline(pipelineId: string): Task[];
/** 添加依赖关系(返回依赖记录,失败抛异常) */
addDependency(upstreamId: string, downstreamId: string, depType?: DepType): TaskDependency;
/** 删除依赖关系 */
removeDependency(upstreamId: string, downstreamId: string): void;
/** 获取任务的上下游依赖 */
getDependencies(taskId: string): {
upstreams: TaskDependency[];
downstreams: TaskDependency[];
};
/** 检查任务所有上游依赖是否满足 */
checkDependenciesSatisfied(taskId: string): boolean;
/** 设置任务为 waiting 状态 */
setTaskWaiting(taskId: string): void;
/** 将 waiting 任务转为 assigned(依赖满足后) */
setTaskReady(taskId: string): void;
/** 检测添加依赖是否会形成环 */
wouldCreateCycle(upstreamId: string, downstreamId: string): boolean;
/** 将指定 upstream 的所有 downstream 依赖标记为 satisfied */
satisfyDownstream(upstreamId: string): number;
/** 添加质量门 */
addQualityGate(gate: Omit<QualityGate, "id"> & {
id?: string;
}): QualityGate;
/** 更新质量门状态 */
updateQualityGateStatus(gateId: string, status: GateStatus, evaluatorId: string, result?: string): void;
/** 列出 Pipeline 的质量门 */
listGatesByPipeline(pipelineId: string): QualityGate[];
}
export interface IConsumedLogRepo {
/** 插入消费记录(OR REPLACE */
insert(entry: ConsumedEntry): void;
/** 查询某 agent 对某资源的消费记录 */
check(agentId: string, resource: string): ConsumedEntry | undefined;
/** 列出某 agent 的消费记录 */
listByAgent(agentId: string, limit?: number): ConsumedEntry[];
}
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=interfaces.js.map
@@ -0,0 +1,114 @@
/**
* repo/interfaces.ts — 数据访问层接口定义 (Phase 2 Day 2, Phase 4b 扩展)
*
* 将 tools.ts/server.ts 中散落的 msgStmt/taskStmt/consumedStmt 直接 SQL 调用
* 统一到接口层,便于测试 mock 和未来数据源替换。
*/
import type { Message, Task, ConsumedEntry } from "../db.js";
import type { TaskDependency, QualityGate, DepType, DepStatus, GateStatus } from "./types.js";
// ─── Message Repo ──────────────────────────────────────────────
export interface IMessageRepo {
/** 插入新消息 */
insert(msg: Message): void;
/** 标记消息为已投递 */
markDelivered(id: string): void;
/** 标记消息为已读 */
markRead(id: string): void;
/** 标记消息为已确认 */
markAcknowledged(id: string): void;
/** 批量标记指定接收方所有未读消息为已投递 */
markAllDelivered(toAgent: string): void;
/** 查询指定接收方的待处理消息(status=unread */
pendingFor(toAgent: string): Message[];
/** 按 ID 查询消息 */
getById(id: string): Message | undefined;
/** 按接收方 + 状态查询消息 */
listByStatus(toAgent: string, status: string): Message[];
/** 更新消息状态(REST PATCH 用) */
updateStatus(id: string, status: string): void;
/** 查询指定接收方在指定时间戳之后的消息(用于 SSE 断线重连回放) */
listSince(toAgent: string, since: number): Message[];
}
// ─── Task Repo ────────────────────────────────────────────────
export interface ITaskRepo {
/** 插入新任务 */
insert(task: Task): void;
/** 按 ID 查询任务 */
getById(id: string): Task | undefined;
/** 更新任务状态/结果/进度 */
update(id: string, status: string, result: string | null, progress: number): void;
/** 分配任务(设置 assigned_to + status=assigned */
assignTo(id: string, assignedTo: string): void;
/** 按执行者 + 状态列出任务 */
listFor(assignedTo: string, status: string): Task[];
/** 按 Pipeline 列出任务 */
listByPipeline(pipelineId: string): Task[];
// ─── Phase 4b: 依赖链方法 ────────────────────────────────
/** 添加依赖关系(返回依赖记录,失败抛异常) */
addDependency(upstreamId: string, downstreamId: string, depType?: DepType): TaskDependency;
/** 删除依赖关系 */
removeDependency(upstreamId: string, downstreamId: string): void;
/** 获取任务的上下游依赖 */
getDependencies(taskId: string): { upstreams: TaskDependency[]; downstreams: TaskDependency[] };
/** 检查任务所有上游依赖是否满足 */
checkDependenciesSatisfied(taskId: string): boolean;
/** 设置任务为 waiting 状态 */
setTaskWaiting(taskId: string): void;
/** 将 waiting 任务转为 assigned(依赖满足后) */
setTaskReady(taskId: string): void;
/** 检测添加依赖是否会形成环 */
wouldCreateCycle(upstreamId: string, downstreamId: string): boolean;
/** 将指定 upstream 的所有 downstream 依赖标记为 satisfied */
satisfyDownstream(upstreamId: string): number;
// ─── Phase 4b: 质量门方法 ────────────────────────────────
/** 添加质量门 */
addQualityGate(gate: Omit<QualityGate, "id"> & { id?: string }): QualityGate;
/** 更新质量门状态 */
updateQualityGateStatus(gateId: string, status: GateStatus, evaluatorId: string, result?: string): void;
/** 列出 Pipeline 的质量门 */
listGatesByPipeline(pipelineId: string): QualityGate[];
}
// ─── ConsumedLog Repo ──────────────────────────────────────────
export interface IConsumedLogRepo {
/** 插入消费记录(OR REPLACE */
insert(entry: ConsumedEntry): void;
/** 查询某 agent 对某资源的消费记录 */
check(agentId: string, resource: string): ConsumedEntry | undefined;
/** 列出某 agent 的消费记录 */
listByAgent(agentId: string, limit?: number): ConsumedEntry[];
}
+4
View File
@@ -0,0 +1,4 @@
import type { IMessageRepo, ITaskRepo, IConsumedLogRepo } from "./interfaces.js";
export declare const messageRepo: IMessageRepo;
export declare const taskRepo: ITaskRepo;
export declare const consumedRepo: IConsumedLogRepo;
@@ -0,0 +1,168 @@
/**
* repo/sqlite-impl.ts — IMessageRepo / ITaskRepo / IConsumedLogRepo 的 SQLite 实现
* Phase 2 Day 2 基础 + Phase 4b 依赖链 + 质量门扩展
*/
import { db, msgStmt, taskStmt, consumedStmt, } from "../db.js";
// ─── MessageRepo ──────────────────────────────────────────────
class SqliteMessageRepo {
insert(msg) {
msgStmt.insert.run(msg);
}
markDelivered(id) {
msgStmt.markDelivered.run(id);
}
markRead(id) {
msgStmt.markRead.run(id);
}
markAcknowledged(id) {
msgStmt.markAcknowledged.run(id);
}
markAllDelivered(toAgent) {
msgStmt.markAllDelivered.run(toAgent);
}
pendingFor(toAgent) {
return msgStmt.pendingFor.all(toAgent);
}
getById(id) {
return msgStmt.getById.get(id);
}
listByStatus(toAgent, status) {
const stmt = db.prepare(`SELECT * FROM messages WHERE to_agent=? AND status=? ORDER BY created_at ASC`);
return stmt.all(toAgent, status);
}
updateStatus(id, status) {
const stmt = db.prepare(`UPDATE messages SET status=? WHERE id=?`);
stmt.run(status, id);
}
listSince(toAgent, since) {
const stmt = db.prepare(`SELECT * FROM messages WHERE to_agent=? AND created_at > ? ORDER BY created_at ASC`);
return stmt.all(toAgent, since);
}
}
// ─── TaskRepo ─────────────────────────────────────────────────
class SqliteTaskRepo {
insert(task) {
taskStmt.insert.run(task);
}
getById(id) {
return taskStmt.getById.get(id);
}
update(id, status, result, progress) {
taskStmt.update.run(status, result, progress, Date.now(), id);
}
assignTo(id, assignedTo) {
taskStmt.updateAssignee.run(assignedTo, Date.now(), Date.now(), id);
}
listFor(assignedTo, status) {
return taskStmt.listFor.all(assignedTo, status);
}
listByPipeline(pipelineId) {
return taskStmt.listByPipeline.all(pipelineId);
}
// ─── Phase 4b: 依赖链实现 ────────────────────────────────
addDependency(upstreamId, downstreamId, depType = "finish_to_start") {
if (upstreamId === downstreamId) {
throw new Error("Cannot create self-dependency");
}
// 检查是否已存在
const existing = db.prepare(`SELECT * FROM task_dependencies WHERE upstream_id=? AND downstream_id=?`).get(upstreamId, downstreamId);
if (existing) {
throw new Error(`Dependency already exists: ${upstreamId}${downstreamId}`);
}
const id = `dep_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
const now = Date.now();
const dep = {
id, upstream_id: upstreamId, downstream_id: downstreamId,
dep_type: depType, status: "pending", created_at: now,
};
db.prepare(`INSERT INTO task_dependencies (id, upstream_id, downstream_id, dep_type, status, created_at)
VALUES (@id, @upstream_id, @downstream_id, @dep_type, @status, @created_at)`).run(dep);
return dep;
}
removeDependency(upstreamId, downstreamId) {
const result = db.prepare(`DELETE FROM task_dependencies WHERE upstream_id=? AND downstream_id=?`).run(upstreamId, downstreamId);
if (result.changes === 0) {
throw new Error(`Dependency not found: ${upstreamId}${downstreamId}`);
}
}
getDependencies(taskId) {
const upstreams = db.prepare(`SELECT * FROM task_dependencies WHERE downstream_id=? ORDER BY created_at ASC`).all(taskId);
const downstreams = db.prepare(`SELECT * FROM task_dependencies WHERE upstream_id=? ORDER BY created_at ASC`).all(taskId);
return { upstreams, downstreams };
}
checkDependenciesSatisfied(taskId) {
// 查找所有 pending 状态的上游依赖
const pending = db.prepare(`SELECT COUNT(*) as cnt FROM task_dependencies
WHERE downstream_id=? AND status='pending'`).get(taskId);
return pending.cnt === 0;
}
setTaskWaiting(taskId) {
db.prepare(`UPDATE tasks SET status='waiting', updated_at=? WHERE id=?`).run(Date.now(), taskId);
}
setTaskReady(taskId) {
const task = db.prepare(`SELECT * FROM tasks WHERE id=?`).get(taskId);
if (!task)
throw new Error(`Task not found: ${taskId}`);
if (task.status !== "waiting") {
throw new Error(`Task is not in waiting state: ${task.status}`);
}
db.prepare(`UPDATE tasks SET status='assigned', updated_at=? WHERE id=?`).run(Date.now(), taskId);
}
wouldCreateCycle(upstreamId, downstreamId) {
// DFS: 从 downstreamId 出发,沿现有依赖的 downstream 方向搜索,
// 看是否能到达 upstreamId。
// 如果能到达,说明添加 upstreamId→downstreamId 后会形成环。
const visited = new Set();
const stack = [downstreamId];
while (stack.length > 0) {
const current = stack.pop();
if (current === upstreamId)
return true;
if (visited.has(current))
continue;
visited.add(current);
const deps = db.prepare(`SELECT downstream_id FROM task_dependencies WHERE upstream_id=?`).all(current);
for (const d of deps) {
stack.push(d.downstream_id);
}
}
return false;
}
satisfyDownstream(upstreamId) {
const result = db.prepare(`UPDATE task_dependencies SET status='satisfied'
WHERE upstream_id=? AND status='pending' AND dep_type='finish_to_start'`).run(upstreamId);
return result.changes;
}
// ─── Phase 4b: 质量门实现 ────────────────────────────────
addQualityGate(gate) {
const id = gate.id ?? `qg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
const fullGate = { ...gate, id };
db.prepare(`INSERT INTO quality_gates (id, pipeline_id, gate_name, criteria, after_order, status, evaluator_id, result, evaluated_at, created_at)
VALUES (@id, @pipeline_id, @gate_name, @criteria, @after_order, @status, @evaluator_id, @result, @evaluated_at, @created_at)`).run(fullGate);
return fullGate;
}
updateQualityGateStatus(gateId, status, evaluatorId, result) {
const now = Date.now();
db.prepare(`UPDATE quality_gates SET status=?, evaluator_id=?, result=?, evaluated_at=? WHERE id=?`).run(status, evaluatorId, result ?? null, now, gateId);
}
listGatesByPipeline(pipelineId) {
return db.prepare(`SELECT * FROM quality_gates WHERE pipeline_id=? ORDER BY after_order ASC`).all(pipelineId);
}
}
// ─── ConsumedLogRepo ──────────────────────────────────────────
class SqliteConsumedLogRepo {
insert(entry) {
consumedStmt.insert.run(entry);
}
check(agentId, resource) {
return consumedStmt.check.get(agentId, resource);
}
listByAgent(agentId, limit = 50) {
return consumedStmt.listByAgent.all(agentId, limit);
}
}
// ─── 单例导出 ──────────────────────────────────────────────────
export const messageRepo = new SqliteMessageRepo();
export const taskRepo = new SqliteTaskRepo();
export const consumedRepo = new SqliteConsumedLogRepo();
//# sourceMappingURL=sqlite-impl.js.map
@@ -0,0 +1,253 @@
/**
* repo/sqlite-impl.ts — IMessageRepo / ITaskRepo / IConsumedLogRepo 的 SQLite 实现
* Phase 2 Day 2 基础 + Phase 4b 依赖链 + 质量门扩展
*/
import {
db,
msgStmt,
taskStmt,
consumedStmt,
type Message,
type Task,
type ConsumedEntry,
} from "../db.js";
import type { IMessageRepo, ITaskRepo, IConsumedLogRepo } from "./interfaces.js";
import type { TaskDependency, QualityGate, DepType, DepStatus, GateStatus } from "./types.js";
// ─── MessageRepo ──────────────────────────────────────────────
class SqliteMessageRepo implements IMessageRepo {
insert(msg: Message): void {
msgStmt.insert.run(msg);
}
markDelivered(id: string): void {
msgStmt.markDelivered.run(id);
}
markRead(id: string): void {
msgStmt.markRead.run(id);
}
markAcknowledged(id: string): void {
msgStmt.markAcknowledged.run(id);
}
markAllDelivered(toAgent: string): void {
msgStmt.markAllDelivered.run(toAgent);
}
pendingFor(toAgent: string): Message[] {
return msgStmt.pendingFor.all(toAgent) as Message[];
}
getById(id: string): Message | undefined {
return msgStmt.getById.get(id) as Message | undefined;
}
listByStatus(toAgent: string, status: string): Message[] {
const stmt = db.prepare(
`SELECT * FROM messages WHERE to_agent=? AND status=? ORDER BY created_at ASC`
);
return stmt.all(toAgent, status) as Message[];
}
updateStatus(id: string, status: string): void {
const stmt = db.prepare(`UPDATE messages SET status=? WHERE id=?`);
stmt.run(status, id);
}
listSince(toAgent: string, since: number): Message[] {
const stmt = db.prepare(
`SELECT * FROM messages WHERE to_agent=? AND created_at > ? ORDER BY created_at ASC`
);
return stmt.all(toAgent, since) as Message[];
}
}
// ─── TaskRepo ─────────────────────────────────────────────────
class SqliteTaskRepo implements ITaskRepo {
insert(task: Task): void {
taskStmt.insert.run(task);
}
getById(id: string): Task | undefined {
return taskStmt.getById.get(id) as Task | undefined;
}
update(id: string, status: string, result: string | null, progress: number): void {
taskStmt.update.run(status, result, progress, Date.now(), id);
}
assignTo(id: string, assignedTo: string): void {
taskStmt.updateAssignee.run(assignedTo, Date.now(), Date.now(), id);
}
listFor(assignedTo: string, status: string): Task[] {
return taskStmt.listFor.all(assignedTo, status) as Task[];
}
listByPipeline(pipelineId: string): Task[] {
return taskStmt.listByPipeline.all(pipelineId) as Task[];
}
// ─── Phase 4b: 依赖链实现 ────────────────────────────────
addDependency(upstreamId: string, downstreamId: string, depType: DepType = "finish_to_start"): TaskDependency {
if (upstreamId === downstreamId) {
throw new Error("Cannot create self-dependency");
}
// 检查是否已存在
const existing = db.prepare(
`SELECT * FROM task_dependencies WHERE upstream_id=? AND downstream_id=?`
).get(upstreamId, downstreamId);
if (existing) {
throw new Error(`Dependency already exists: ${upstreamId}${downstreamId}`);
}
const id = `dep_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
const now = Date.now();
const dep: TaskDependency = {
id, upstream_id: upstreamId, downstream_id: downstreamId,
dep_type: depType, status: "pending", created_at: now,
};
db.prepare(
`INSERT INTO task_dependencies (id, upstream_id, downstream_id, dep_type, status, created_at)
VALUES (@id, @upstream_id, @downstream_id, @dep_type, @status, @created_at)`
).run(dep);
return dep;
}
removeDependency(upstreamId: string, downstreamId: string): void {
const result = db.prepare(
`DELETE FROM task_dependencies WHERE upstream_id=? AND downstream_id=?`
).run(upstreamId, downstreamId);
if (result.changes === 0) {
throw new Error(`Dependency not found: ${upstreamId}${downstreamId}`);
}
}
getDependencies(taskId: string): { upstreams: TaskDependency[]; downstreams: TaskDependency[] } {
const upstreams = db.prepare(
`SELECT * FROM task_dependencies WHERE downstream_id=? ORDER BY created_at ASC`
).all(taskId) as TaskDependency[];
const downstreams = db.prepare(
`SELECT * FROM task_dependencies WHERE upstream_id=? ORDER BY created_at ASC`
).all(taskId) as TaskDependency[];
return { upstreams, downstreams };
}
checkDependenciesSatisfied(taskId: string): boolean {
// 查找所有 pending 状态的上游依赖
const pending = db.prepare(
`SELECT COUNT(*) as cnt FROM task_dependencies
WHERE downstream_id=? AND status='pending'`
).get(taskId) as { cnt: number };
return pending.cnt === 0;
}
setTaskWaiting(taskId: string): void {
db.prepare(
`UPDATE tasks SET status='waiting', updated_at=? WHERE id=?`
).run(Date.now(), taskId);
}
setTaskReady(taskId: string): void {
const task = db.prepare(`SELECT * FROM tasks WHERE id=?`).get(taskId) as Task | undefined;
if (!task) throw new Error(`Task not found: ${taskId}`);
if (task.status !== "waiting") {
throw new Error(`Task is not in waiting state: ${task.status}`);
}
db.prepare(
`UPDATE tasks SET status='assigned', updated_at=? WHERE id=?`
).run(Date.now(), taskId);
}
wouldCreateCycle(upstreamId: string, downstreamId: string): boolean {
// DFS: 从 downstreamId 出发,沿现有依赖的 downstream 方向搜索,
// 看是否能到达 upstreamId。
// 如果能到达,说明添加 upstreamId→downstreamId 后会形成环。
const visited = new Set<string>();
const stack = [downstreamId];
while (stack.length > 0) {
const current = stack.pop()!;
if (current === upstreamId) return true;
if (visited.has(current)) continue;
visited.add(current);
const deps = db.prepare(
`SELECT downstream_id FROM task_dependencies WHERE upstream_id=?`
).all(current) as Array<{ downstream_id: string }>;
for (const d of deps) {
stack.push(d.downstream_id);
}
}
return false;
}
satisfyDownstream(upstreamId: string): number {
const result = db.prepare(
`UPDATE task_dependencies SET status='satisfied'
WHERE upstream_id=? AND status='pending' AND dep_type='finish_to_start'`
).run(upstreamId);
return result.changes;
}
// ─── Phase 4b: 质量门实现 ────────────────────────────────
addQualityGate(gate: Omit<QualityGate, "id"> & { id?: string }): QualityGate {
const id = gate.id ?? `qg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
const fullGate: QualityGate = { ...gate, id };
db.prepare(
`INSERT INTO quality_gates (id, pipeline_id, gate_name, criteria, after_order, status, evaluator_id, result, evaluated_at, created_at)
VALUES (@id, @pipeline_id, @gate_name, @criteria, @after_order, @status, @evaluator_id, @result, @evaluated_at, @created_at)`
).run(fullGate);
return fullGate;
}
updateQualityGateStatus(gateId: string, status: GateStatus, evaluatorId: string, result?: string): void {
const now = Date.now();
db.prepare(
`UPDATE quality_gates SET status=?, evaluator_id=?, result=?, evaluated_at=? WHERE id=?`
).run(status, evaluatorId, result ?? null, now, gateId);
}
listGatesByPipeline(pipelineId: string): QualityGate[] {
return db.prepare(
`SELECT * FROM quality_gates WHERE pipeline_id=? ORDER BY after_order ASC`
).all(pipelineId) as QualityGate[];
}
}
// ─── ConsumedLogRepo ──────────────────────────────────────────
class SqliteConsumedLogRepo implements IConsumedLogRepo {
insert(entry: ConsumedEntry): void {
consumedStmt.insert.run(entry);
}
check(agentId: string, resource: string): ConsumedEntry | undefined {
return consumedStmt.check.get(agentId, resource) as ConsumedEntry | undefined;
}
listByAgent(agentId: string, limit = 50): ConsumedEntry[] {
return consumedStmt.listByAgent.all(agentId, limit) as ConsumedEntry[];
}
}
// ─── 单例导出 ──────────────────────────────────────────────────
export const messageRepo: IMessageRepo = new SqliteMessageRepo();
export const taskRepo: ITaskRepo = new SqliteTaskRepo();
export const consumedRepo: IConsumedLogRepo = new SqliteConsumedLogRepo();
+33
View File
@@ -0,0 +1,33 @@
/**
* repo/types.ts — Phase 4b 依赖链 + 质量门类型定义
*/
/** 依赖类型 */
export type DepType = "finish_to_start" | "start_to_start" | "finish_to_finish";
/** 依赖状态 */
export type DepStatus = "pending" | "satisfied" | "failed";
/** 任务依赖关系 */
export interface TaskDependency {
id: string;
upstream_id: string;
downstream_id: string;
dep_type: DepType;
status: DepStatus;
created_at: number;
}
/** 质量门状态 */
export type GateStatus = "pending" | "passed" | "failed";
/** 质量门 */
export interface QualityGate {
id: string;
pipeline_id: string;
gate_name: string;
criteria: string;
after_order: number;
status: GateStatus;
evaluator_id?: string | null;
result?: string | null;
evaluated_at?: number | null;
created_at: number;
}
/** Phase 4b: 完整任务状态(含 waiting */
export type TaskStatusAll = "inbox" | "assigned" | "waiting" | "pending" | "in_progress" | "completed" | "failed" | "cancelled";
+5
View File
@@ -0,0 +1,5 @@
/**
* repo/types.ts — Phase 4b 依赖链 + 质量门类型定义
*/
export {};
//# sourceMappingURL=types.js.map
+47
View File
@@ -0,0 +1,47 @@
/**
* repo/types.ts — Phase 4b 依赖链 + 质量门类型定义
*/
// ─── 依赖类型 ──────────────────────────────────────────────
/** 依赖类型 */
export type DepType = "finish_to_start" | "start_to_start" | "finish_to_finish";
/** 依赖状态 */
export type DepStatus = "pending" | "satisfied" | "failed";
/** 任务依赖关系 */
export interface TaskDependency {
id: string;
upstream_id: string;
downstream_id: string;
dep_type: DepType;
status: DepStatus;
created_at: number;
}
// ─── 质量门类型 ─────────────────────────────────────────────
/** 质量门状态 */
export type GateStatus = "pending" | "passed" | "failed";
/** 质量门 */
export interface QualityGate {
id: string;
pipeline_id: string;
gate_name: string;
criteria: string; // JSON: { type, threshold?, check_expr? }
after_order: number;
status: GateStatus;
evaluator_id?: string | null;
result?: string | null;
evaluated_at?: number | null;
created_at: number;
}
// ─── 状态机扩展 ─────────────────────────────────────────────
/** Phase 4b: 完整任务状态(含 waiting */
export type TaskStatusAll =
| "inbox" | "assigned" | "waiting" | "pending" | "in_progress"
| "completed" | "failed" | "cancelled";
+118
View File
@@ -0,0 +1,118 @@
import type { Request, Response, NextFunction } from "express";
declare global {
namespace Express {
interface Request {
auth?: {
agent?: AuthContext | undefined;
};
}
}
}
export type AgentRole = "admin" | "member" | "group_admin";
export interface AuthContext {
agentId: string;
role: AgentRole;
}
/** SHA-256 哈希 */
export declare function sha256(input: string): string;
/** 生成明文 Token(一次性返回) */
export declare function generateToken(): string;
/** 验证 Token 并返回 AuthContext,失败返回 null */
export declare function verifyToken(plainToken: string): AuthContext | null;
export declare function rateLimiter(agentId: string): boolean;
/** 工具访问级别 */
type PermissionLevel = "public" | "member" | "admin";
export declare const TOOL_PERMISSIONS: Record<string, PermissionLevel>;
/**
* 检查工具调用权限
* group_admin 权限等同于 member(仅任务相关工具),其余 admin 工具不可用
* @returns true=允许, false=拒绝
*/
export declare function checkPermission(toolName: string, role: AgentRole): boolean;
/**
* 获取权限级别(用于返回错误信息)
*/
export declare function getRequiredPermission(toolName: string): PermissionLevel | undefined;
/**
* 强制认证中间件 — 所有 API/MCP 端点使用
* 无有效 Token → 401
*/
export declare function authMiddleware(req: Request, res: Response, next: NextFunction): void;
/**
* 可选认证中间件 — SSE 端点使用
* 有 Token 则验证,无 Token 则 auth = undefined
* ⚠️ 关键:未认证时 auth 必须为 undefined,不能创建默认 authContext
*/
export declare function optionalAuthMiddleware(req: Request, res: Response, next: NextFunction): void;
/**
* 生成邀请码(明文)
* @returns 明文邀请码
*/
export declare function generateInviteCode(): string;
/**
* 创建邀请码记录
* @returns 明文邀请码(唯一一次可见)
*/
export declare function createInviteCode(role?: "admin" | "member"): string;
/**
* 验证邀请码并标记已使用
* @returns 有效邀请码的角色,或 null
*/
export declare function verifyInviteCode(plainCode: string): "admin" | "member" | null;
/**
* 标记邀请码已使用
*/
export declare function markInviteCodeUsed(plainCode: string): void;
/**
* 吊销 API Token
*/
export declare function revokeToken(tokenId: string): boolean;
/**
* 记录审计日志(带哈希链)
* 每条记录包含 prev_hash 和 record_hash,形成不可篡改链
*/
export declare function auditLog(action: string, agentId: string | null, target?: string, details?: string): void;
/**
* 验证审计日志哈希链完整性
* @returns { valid, total, checked, firstBreak } — valid=true 表示链完整
*/
export declare function verifyAuditChain(): {
valid: boolean;
total: number;
checked: number;
firstBreak?: {
id: string;
action: string;
expected: string;
actual: string;
};
};
/**
* 重新计算 Agent 信任评分
*
* 公式:
* base = 50
* + verified_capabilities × 3
* + auto_approved_strategies × 2
* + positive_feedback × 1
* - negative_feedback × 2
* - rejected_applications × 3
* - revoked_token_count × 10
* → clamp(0, 100)
*
* @returns 计算后的信任分数
*/
export declare function recalculateTrustScore(agentId: string): number;
/**
* 重新计算所有 Agent 的信任评分
* @returns { agent_id, score } 数组
*/
export declare function recalculateAllTrustScores(): Array<{
agent_id: string;
score: number;
}>;
/**
* 检查路径是否安全(防止路径遍历)
*/
export declare function sanitizePath(inputPath: string): boolean;
export {};
+398
View File
@@ -0,0 +1,398 @@
/**
* security.ts — Security Guard
* Token 认证 + 速率限制 + MCP 工具权限矩阵 + 邀请码 + 审计日志
*
* 踩坑经验:
* - better-sqlite3 不接受 JS boolean,必须用 1/0
* - better-sqlite3 不接受 undefined,必须用 null
* - optionalAuth 未认证时不要默认创建 authContext
*/
import { createHash, randomBytes } from "crypto";
import { db } from "./db.js";
import { logError } from "./logger.js";
// ─── Token 工具函数 ──────────────────────────────────────
/** SHA-256 哈希 */
export function sha256(input) {
return createHash("sha256").update(input).digest("hex");
}
/** 生成明文 Token(一次性返回) */
export function generateToken() {
return randomBytes(32).toString("hex"); // 64 字符明文 Token
}
/** 验证 Token 并返回 AuthContext,失败返回 null */
export function verifyToken(plainToken) {
const hash = sha256(plainToken);
const row = db
.prepare(`SELECT agent_id, role FROM auth_tokens
WHERE token_type='api_token' AND token_value=? AND used=1 AND revoked_at IS NULL`)
.get(hash);
if (!row)
return null;
// 检查是否过期
const expiresRow = db
.prepare(`SELECT expires_at FROM auth_tokens WHERE token_value=?`)
.get(hash);
if (expiresRow?.expires_at && Date.now() > expiresRow.expires_at) {
return null;
}
return { agentId: row.agent_id, role: row.role };
}
// ─── 速率限制 ────────────────────────────────────────────
const rateLimitMap = new Map();
const RATE_LIMIT_WINDOW = parseInt(process.env.RATE_LIMIT_WINDOW ?? "1000", 10); // 默认 1 秒窗口
const RATE_LIMIT_MAX = parseInt(process.env.RATE_LIMIT_MAX ?? "10", 10); // 默认每秒 10 请求
export function rateLimiter(agentId) {
const now = Date.now();
const entry = rateLimitMap.get(agentId);
if (!entry || now - entry.windowStart > RATE_LIMIT_WINDOW) {
rateLimitMap.set(agentId, { count: 1, windowStart: now });
return true;
}
entry.count++;
return entry.count <= RATE_LIMIT_MAX;
}
export const TOOL_PERMISSIONS = {
// 注册免认证
register_agent: "public",
// 心跳与查询 — member 及以上
heartbeat: "member",
query_agents: "member",
get_online_agents: "member",
// 消息与任务 — member 及以上
send_message: "member",
assign_task: "member",
update_task_status: "member",
get_task_status: "member",
broadcast_message: "member",
acknowledge_message: "member",
mark_consumed: "member",
check_consumed: "member",
// 记忆 — member 及以上
store_memory: "member",
recall_memory: "member",
list_memories: "member",
delete_memory: "member",
// 管理 — 仅 admin
revoke_token: "admin",
set_trust_score: "admin",
set_agent_role: "admin", // Phase 5a: 任命/撤销 group_admin
recalculate_trust_scores: "admin", // Phase 5a: 手动重算信任分
// Phase 3: Evolution Engine
share_experience: "member",
propose_strategy: "member",
list_strategies: "member",
search_strategies: "member",
apply_strategy: "member",
feedback_strategy: "member",
approve_strategy: "admin", // 审批仅 admin
get_evolution_status: "member",
// Phase 4b Day 2: 依赖链 + 并行组
add_dependency: "member",
remove_dependency: "member",
get_task_dependencies: "member",
create_parallel_group: "member",
// Phase 4b Day 3: 交接协议 + 质量门
request_handoff: "member",
accept_handoff: "member",
reject_handoff: "member",
add_quality_gate: "member",
evaluate_quality_gate: "member",
// Phase 4b Day 4: 分级审批
propose_strategy_tiered: "member",
check_veto_window: "member",
veto_strategy: "admin",
// Phase 2.2: 策略采纳闭环
score_applied_strategies: "admin",
// v2.3 Phase 1.1: 文件传输
upload_file: "member",
download_file: "member",
list_attachments: "member",
// v2.3 Phase 3.2: 数据库维护
get_db_stats: "admin",
archive_data: "admin",
};
/**
* 检查工具调用权限
* group_admin 权限等同于 member(仅任务相关工具),其余 admin 工具不可用
* @returns true=允许, false=拒绝
*/
export function checkPermission(toolName, role) {
const level = TOOL_PERMISSIONS[toolName];
if (!level) {
// 未注册的工具默认 member 可访问
return true;
}
if (level === "public")
return true;
if (level === "member")
return true;
if (level === "admin")
return role === "admin";
return false;
}
/**
* 获取权限级别(用于返回错误信息)
*/
export function getRequiredPermission(toolName) {
return TOOL_PERMISSIONS[toolName];
}
// ─── Express 中间件 ──────────────────────────────────────
/**
* 强制认证中间件 — 所有 API/MCP 端点使用
* 无有效 Token → 401
*/
export function authMiddleware(req, res, next) {
const token = extractToken(req);
if (!token) {
res.status(401).json({ error: "Missing authentication token" });
return;
}
const ctx = verifyToken(token);
if (!ctx) {
res.status(401).json({ error: "Invalid or expired token" });
return;
}
// 速率限制
if (!rateLimiter(ctx.agentId)) {
res.status(429).json({ error: "Rate limit exceeded (10 req/s)" });
return;
}
// 将认证信息挂载到 req 上
req.auth = { agent: ctx };
next();
}
/**
* 可选认证中间件 — SSE 端点使用
* 有 Token 则验证,无 Token 则 auth = undefined
* ⚠️ 关键:未认证时 auth 必须为 undefined,不能创建默认 authContext
*/
export function optionalAuthMiddleware(req, res, next) {
const token = extractToken(req);
if (!token) {
req.auth = { agent: undefined };
next();
return;
}
const ctx = verifyToken(token);
req.auth = { agent: ctx ?? undefined }; // undefined 不是 null
next();
}
/** 从 Header 或 Query 提取 Token */
function extractToken(req) {
// Header: Authorization: Bearer <token>
const authHeader = req.headers.authorization;
if (authHeader?.startsWith("Bearer ")) {
return authHeader.slice(7);
}
// Query: ?token=<token>
const queryToken = req.query.token;
if (queryToken) {
return queryToken;
}
// x-api-key header
const apiKey = req.headers["x-api-key"];
if (apiKey) {
return apiKey;
}
return null;
}
// ─── 邀请码管理 ──────────────────────────────────────────
/**
* 生成邀请码(明文)
* @returns 明文邀请码
*/
export function generateInviteCode() {
return randomBytes(4).toString("hex"); // 8 字符
}
/**
* 创建邀请码记录
* @returns 明文邀请码(唯一一次可见)
*/
export function createInviteCode(role = "member") {
const plain = generateInviteCode();
const hash = sha256(plain);
const now = Date.now();
const expiresAt = now + 24 * 60 * 60 * 1000; // 24 小时有效
db.prepare(`INSERT INTO auth_tokens (token_id, token_type, token_value, role, used, created_at, expires_at)
VALUES (?, 'invite_code', ?, ?, 0, ?, ?)`).run(`invite_${now}_${randomBytes(4).toString("hex")}`, hash, role, now, expiresAt);
return plain;
}
/**
* 验证邀请码并标记已使用
* @returns 有效邀请码的角色,或 null
*/
export function verifyInviteCode(plainCode) {
const hash = sha256(plainCode);
const row = db
.prepare(`SELECT role, expires_at FROM auth_tokens
WHERE token_type='invite_code' AND token_value=? AND used=0 AND revoked_at IS NULL`)
.get(hash);
if (!row)
return null;
// 检查过期
if (row.expires_at && Date.now() > row.expires_at) {
return null;
}
return row.role;
}
/**
* 标记邀请码已使用
*/
export function markInviteCodeUsed(plainCode) {
const hash = sha256(plainCode);
db.prepare(`UPDATE auth_tokens SET used=1 WHERE token_type='invite_code' AND token_value=?`).run(hash);
}
// ─── Token 吊销 ──────────────────────────────────────────
/**
* 吊销 API Token
*/
export function revokeToken(tokenId) {
const now = Date.now();
const result = db
.prepare(`UPDATE auth_tokens SET revoked_at=? WHERE token_id=? AND token_type='api_token'`)
.run(now, tokenId);
return result.changes > 0;
}
// ─── 审计日志(Phase 5a: 哈希链防篡改) ─────────────────
/**
* 记录审计日志(带哈希链)
* 每条记录包含 prev_hash 和 record_hash,形成不可篡改链
*/
export function auditLog(action, agentId, target, details) {
const id = `audit_${Date.now()}_${randomBytes(4).toString("hex")}`;
const now = Date.now();
try {
// 获取上一条记录的 record_hash
const lastRow = db.prepare(`SELECT record_hash FROM audit_log ORDER BY created_at DESC, id DESC LIMIT 1`).get();
const prevHash = lastRow?.record_hash ?? "GENESIS";
// 计算当前记录的 hash
const hashInput = `${prevHash}|${action}|${agentId ?? ""}|${target ?? ""}|${details ?? ""}|${now}`;
const recordHash = createHash("sha256").update(hashInput).digest("hex");
db.prepare(`INSERT INTO audit_log (id, action, agent_id, target, details, prev_hash, record_hash, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(id, action, agentId, target || null, details || null, prevHash, recordHash, now);
}
catch (err) {
logError("audit_log_failed", err);
}
}
/**
* 验证审计日志哈希链完整性
* @returns { valid, total, checked, firstBreak } — valid=true 表示链完整
*/
export function verifyAuditChain() {
const rows = db.prepare(`SELECT id, action, agent_id, target, details, prev_hash, record_hash, created_at
FROM audit_log ORDER BY created_at ASC, id ASC`).all();
if (rows.length === 0) {
return { valid: true, total: 0, checked: 0 };
}
let expectedPrev = "GENESIS";
for (const row of rows) {
// 旧数据(哈希链实现前写入的)prev_hash/record_hash 为 null,跳过验证
if (row.prev_hash === null || row.record_hash === null) {
if (row.record_hash)
expectedPrev = row.record_hash;
// 继续用上一条的 record_hash 作为 expectedPrev
continue;
}
// 检查 prev_hash 连续性
if (row.prev_hash !== expectedPrev) {
return {
valid: false,
total: rows.length,
checked: rows.indexOf(row),
firstBreak: {
id: row.id,
action: row.action,
expected: expectedPrev,
actual: row.prev_hash,
},
};
}
// 重新计算 hash 验证
const hashInput = `${row.prev_hash}|${row.action}|${row.agent_id ?? ""}|${row.target ?? ""}|${row.details ?? ""}|${row.created_at}`;
const computedHash = createHash("sha256").update(hashInput).digest("hex");
if (computedHash !== row.record_hash) {
return {
valid: false,
total: rows.length,
checked: rows.indexOf(row) + 1,
firstBreak: {
id: row.id,
action: row.action,
expected: computedHash,
actual: row.record_hash,
},
};
}
expectedPrev = row.record_hash;
}
return { valid: true, total: rows.length, checked: rows.length };
}
// ─── 信任评分自动化(Phase 5a Day 2) ───────────────────
/**
* 重新计算 Agent 信任评分
*
* 公式:
* base = 50
* + verified_capabilities × 3
* + auto_approved_strategies × 2
* + positive_feedback × 1
* - negative_feedback × 2
* - rejected_applications × 3
* - revoked_token_count × 10
* → clamp(0, 100)
*
* @returns 计算后的信任分数
*/
export function recalculateTrustScore(agentId) {
const verifiedCaps = db.prepare(`SELECT COUNT(*) as cnt FROM agent_capabilities WHERE agent_id=? AND verified=1`).get(agentId)?.cnt ?? 0;
const autoStrategies = db.prepare(`SELECT COUNT(*) as cnt FROM strategies WHERE proposer_id=? AND status='approved'`).get(agentId)?.cnt ?? 0;
// 注意:strategy_applications 没有 status/rejected 列,无法直接统计拒绝数
// 退而查 apply_strategy_fail 审计记录
const rejectedApps = db.prepare(`SELECT COUNT(*) as cnt FROM audit_log WHERE action='apply_strategy' AND agent_id=? AND details LIKE '%fail%'`).get(agentId)?.cnt ?? 0;
const revokedTokens = db.prepare(`SELECT COUNT(*) as cnt FROM audit_log WHERE action='revoke_token' AND agent_id=?`).get(agentId)?.cnt ?? 0;
// 注意:strategy_feedback.agent_id 是反馈者,不是提案者
// 要查"别人给该 agent 策略的反馈"需要 JOIN strategies.proposer_id
const positiveFb = db.prepare(`SELECT COUNT(*) as cnt FROM strategy_feedback sf
JOIN strategies s ON sf.strategy_id = s.id
WHERE s.proposer_id = ? AND sf.feedback = 'positive' AND sf.agent_id != ?`).get(agentId, agentId)?.cnt ?? 0;
const negativeFb = db.prepare(`SELECT COUNT(*) as cnt FROM strategy_feedback sf
JOIN strategies s ON sf.strategy_id = s.id
WHERE s.proposer_id = ? AND sf.feedback = 'negative' AND sf.agent_id != ?`).get(agentId, agentId)?.cnt ?? 0;
let score = 50;
score += verifiedCaps * 3;
score += autoStrategies * 2;
score += positiveFb * 1;
score -= negativeFb * 2;
score -= rejectedApps * 3;
score -= revokedTokens * 10;
// clamp(0, 100)
score = Math.max(0, Math.min(100, score));
// 写回 agents.trust_score
db.prepare(`UPDATE agents SET trust_score=? WHERE agent_id=?`).run(score, agentId);
return score;
}
/**
* 重新计算所有 Agent 的信任评分
* @returns { agent_id, score } 数组
*/
export function recalculateAllTrustScores() {
const agents = db.prepare(`SELECT agent_id FROM agents`).all();
const results = [];
for (const agent of agents) {
const score = recalculateTrustScore(agent.agent_id);
results.push({ agent_id: agent.agent_id, score });
}
return results;
}
// ─── 路径安全 ────────────────────────────────────────────
/**
* 检查路径是否安全(防止路径遍历)
*/
export function sanitizePath(inputPath) {
const normalized = inputPath.replace(/\\/g, "/");
return (!normalized.includes("..") &&
!normalized.startsWith("/") &&
!normalized.includes("\0"));
}
//# sourceMappingURL=security.js.map
+524
View File
@@ -0,0 +1,524 @@
/**
* security.ts — Security Guard
* Token 认证 + 速率限制 + MCP 工具权限矩阵 + 邀请码 + 审计日志
*
* 踩坑经验:
* - better-sqlite3 不接受 JS boolean,必须用 1/0
* - better-sqlite3 不接受 undefined,必须用 null
* - optionalAuth 未认证时不要默认创建 authContext
*/
import { createHash, randomBytes } from "crypto";
import type { Request, Response, NextFunction } from "express";
import { db } from "./db.js";
import { logError } from "./logger.js";
import { getErrorMessage } from "./types.js";
// ─── Express 类型扩展 ──────────────────────────────────
declare global {
namespace Express {
interface Request {
auth?: {
agent?: AuthContext | undefined;
};
}
}
}
// ─── 类型定义 ────────────────────────────────────────────
export type AgentRole = "admin" | "member" | "group_admin";
export interface AuthContext {
agentId: string;
role: AgentRole;
}
// ─── Token 工具函数 ──────────────────────────────────────
/** SHA-256 哈希 */
export function sha256(input: string): string {
return createHash("sha256").update(input).digest("hex");
}
/** 生成明文 Token(一次性返回) */
export function generateToken(): string {
return randomBytes(32).toString("hex"); // 64 字符明文 Token
}
/** 验证 Token 并返回 AuthContext,失败返回 null */
export function verifyToken(plainToken: string): AuthContext | null {
const hash = sha256(plainToken);
const row = db
.prepare(
`SELECT agent_id, role FROM auth_tokens
WHERE token_type='api_token' AND token_value=? AND used=1 AND revoked_at IS NULL`
)
.get(hash) as any;
if (!row) return null;
// 检查是否过期
const expiresRow = db
.prepare(`SELECT expires_at FROM auth_tokens WHERE token_value=?`)
.get(hash) as any;
if (expiresRow?.expires_at && Date.now() > expiresRow.expires_at) {
return null;
}
return { agentId: row.agent_id, role: row.role };
}
// ─── 速率限制 ────────────────────────────────────────────
const rateLimitMap = new Map<string, { count: number; windowStart: number }>();
const RATE_LIMIT_WINDOW = parseInt(process.env.RATE_LIMIT_WINDOW ?? "1000", 10); // 默认 1 秒窗口
const RATE_LIMIT_MAX = parseInt(process.env.RATE_LIMIT_MAX ?? "10", 10); // 默认每秒 10 请求
export function rateLimiter(agentId: string): boolean {
const now = Date.now();
const entry = rateLimitMap.get(agentId);
if (!entry || now - entry.windowStart > RATE_LIMIT_WINDOW) {
rateLimitMap.set(agentId, { count: 1, windowStart: now });
return true;
}
entry.count++;
return entry.count <= RATE_LIMIT_MAX;
}
// ─── MCP 工具权限矩阵 ────────────────────────────────────
/** 工具访问级别 */
type PermissionLevel = "public" | "member" | "admin";
export const TOOL_PERMISSIONS: Record<string, PermissionLevel> = {
// 注册免认证
register_agent: "public",
// 心跳与查询 — member 及以上
heartbeat: "member",
query_agents: "member",
get_online_agents: "member",
// 消息与任务 — member 及以上
send_message: "member",
assign_task: "member",
update_task_status: "member",
get_task_status: "member",
broadcast_message: "member",
acknowledge_message: "member",
mark_consumed: "member",
check_consumed: "member",
// 记忆 — member 及以上
store_memory: "member",
recall_memory: "member",
list_memories: "member",
delete_memory: "member",
// 管理 — 仅 admin
revoke_token: "admin",
set_trust_score: "admin",
set_agent_role: "admin", // Phase 5a: 任命/撤销 group_admin
recalculate_trust_scores: "admin", // Phase 5a: 手动重算信任分
// Phase 3: Evolution Engine
share_experience: "member",
propose_strategy: "member",
list_strategies: "member",
search_strategies: "member",
apply_strategy: "member",
feedback_strategy: "member",
approve_strategy: "admin", // 审批仅 admin
get_evolution_status: "member",
// Phase 4b Day 2: 依赖链 + 并行组
add_dependency: "member",
remove_dependency: "member",
get_task_dependencies: "member",
create_parallel_group: "member",
// Phase 4b Day 3: 交接协议 + 质量门
request_handoff: "member",
accept_handoff: "member",
reject_handoff: "member",
add_quality_gate: "member",
evaluate_quality_gate: "member",
// Phase 4b Day 4: 分级审批
propose_strategy_tiered: "member",
check_veto_window: "member",
veto_strategy: "admin",
// Phase 2.2: 策略采纳闭环
score_applied_strategies: "admin",
// v2.3 Phase 1.1: 文件传输
upload_file: "member",
download_file: "member",
list_attachments: "member",
// v2.3 Phase 3.2: 数据库维护
get_db_stats: "admin",
archive_data: "admin",
};
/**
* 检查工具调用权限
* group_admin 权限等同于 member(仅任务相关工具),其余 admin 工具不可用
* @returns true=允许, false=拒绝
*/
export function checkPermission(
toolName: string,
role: AgentRole
): boolean {
const level = TOOL_PERMISSIONS[toolName];
if (!level) {
// 未注册的工具默认 member 可访问
return true;
}
if (level === "public") return true;
if (level === "member") return true;
if (level === "admin") return role === "admin";
return false;
}
/**
* 获取权限级别(用于返回错误信息)
*/
export function getRequiredPermission(toolName: string): PermissionLevel | undefined {
return TOOL_PERMISSIONS[toolName];
}
// ─── Express 中间件 ──────────────────────────────────────
/**
* 强制认证中间件 — 所有 API/MCP 端点使用
* 无有效 Token → 401
*/
export function authMiddleware(req: Request, res: Response, next: NextFunction): void {
const token = extractToken(req);
if (!token) {
res.status(401).json({ error: "Missing authentication token" });
return;
}
const ctx = verifyToken(token);
if (!ctx) {
res.status(401).json({ error: "Invalid or expired token" });
return;
}
// 速率限制
if (!rateLimiter(ctx.agentId)) {
res.status(429).json({ error: "Rate limit exceeded (10 req/s)" });
return;
}
// 将认证信息挂载到 req 上
(req as any).auth = { agent: ctx };
next();
}
/**
* 可选认证中间件 — SSE 端点使用
* 有 Token 则验证,无 Token 则 auth = undefined
* ⚠️ 关键:未认证时 auth 必须为 undefined,不能创建默认 authContext
*/
export function optionalAuthMiddleware(req: Request, res: Response, next: NextFunction): void {
const token = extractToken(req);
if (!token) {
(req as any).auth = { agent: undefined };
next();
return;
}
const ctx = verifyToken(token);
(req as any).auth = { agent: ctx ?? undefined }; // undefined 不是 null
next();
}
/** 从 Header 或 Query 提取 Token */
function extractToken(req: Request): string | null {
// Header: Authorization: Bearer <token>
const authHeader = req.headers.authorization;
if (authHeader?.startsWith("Bearer ")) {
return authHeader.slice(7);
}
// Query: ?token=<token>
const queryToken = req.query.token as string | undefined;
if (queryToken) {
return queryToken;
}
// x-api-key header
const apiKey = req.headers["x-api-key"] as string | undefined;
if (apiKey) {
return apiKey;
}
return null;
}
// ─── 邀请码管理 ──────────────────────────────────────────
/**
* 生成邀请码(明文)
* @returns 明文邀请码
*/
export function generateInviteCode(): string {
return randomBytes(4).toString("hex"); // 8 字符
}
/**
* 创建邀请码记录
* @returns 明文邀请码(唯一一次可见)
*/
export function createInviteCode(role: "admin" | "member" = "member"): string {
const plain = generateInviteCode();
const hash = sha256(plain);
const now = Date.now();
const expiresAt = now + 24 * 60 * 60 * 1000; // 24 小时有效
db.prepare(
`INSERT INTO auth_tokens (token_id, token_type, token_value, role, used, created_at, expires_at)
VALUES (?, 'invite_code', ?, ?, 0, ?, ?)`
).run(
`invite_${now}_${randomBytes(4).toString("hex")}`,
hash,
role,
now,
expiresAt
);
return plain;
}
/**
* 验证邀请码并标记已使用
* @returns 有效邀请码的角色,或 null
*/
export function verifyInviteCode(plainCode: string): "admin" | "member" | null {
const hash = sha256(plainCode);
const row = db
.prepare(
`SELECT role, expires_at FROM auth_tokens
WHERE token_type='invite_code' AND token_value=? AND used=0 AND revoked_at IS NULL`
)
.get(hash) as any;
if (!row) return null;
// 检查过期
if (row.expires_at && Date.now() > row.expires_at) {
return null;
}
return row.role;
}
/**
* 标记邀请码已使用
*/
export function markInviteCodeUsed(plainCode: string): void {
const hash = sha256(plainCode);
db.prepare(
`UPDATE auth_tokens SET used=1 WHERE token_type='invite_code' AND token_value=?`
).run(hash);
}
// ─── Token 吊销 ──────────────────────────────────────────
/**
* 吊销 API Token
*/
export function revokeToken(tokenId: string): boolean {
const now = Date.now();
const result = db
.prepare(
`UPDATE auth_tokens SET revoked_at=? WHERE token_id=? AND token_type='api_token'`
)
.run(now, tokenId);
return result.changes > 0;
}
// ─── 审计日志(Phase 5a: 哈希链防篡改) ─────────────────
/**
* 记录审计日志(带哈希链)
* 每条记录包含 prev_hash 和 record_hash,形成不可篡改链
*/
export function auditLog(action: string, agentId: string | null, target?: string, details?: string): void {
const id = `audit_${Date.now()}_${randomBytes(4).toString("hex")}`;
const now = Date.now();
try {
// 获取上一条记录的 record_hash
const lastRow = db.prepare(
`SELECT record_hash FROM audit_log ORDER BY created_at DESC, id DESC LIMIT 1`
).get() as any;
const prevHash = lastRow?.record_hash ?? "GENESIS";
// 计算当前记录的 hash
const hashInput = `${prevHash}|${action}|${agentId ?? ""}|${target ?? ""}|${details ?? ""}|${now}`;
const recordHash = createHash("sha256").update(hashInput).digest("hex");
db.prepare(
`INSERT INTO audit_log (id, action, agent_id, target, details, prev_hash, record_hash, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
).run(id, action, agentId, target || null, details || null, prevHash, recordHash, now);
} catch (err: unknown) {
logError("audit_log_failed", err);
}
}
/**
* 验证审计日志哈希链完整性
* @returns { valid, total, checked, firstBreak } — valid=true 表示链完整
*/
export function verifyAuditChain(): {
valid: boolean;
total: number;
checked: number;
firstBreak?: { id: string; action: string; expected: string; actual: string };
} {
const rows = db.prepare(
`SELECT id, action, agent_id, target, details, prev_hash, record_hash, created_at
FROM audit_log ORDER BY created_at ASC, id ASC`
).all() as any[];
if (rows.length === 0) {
return { valid: true, total: 0, checked: 0 };
}
let expectedPrev = "GENESIS";
for (const row of rows) {
// 旧数据(哈希链实现前写入的)prev_hash/record_hash 为 null,跳过验证
if (row.prev_hash === null || row.record_hash === null) {
if (row.record_hash) expectedPrev = row.record_hash;
// 继续用上一条的 record_hash 作为 expectedPrev
continue;
}
// 检查 prev_hash 连续性
if (row.prev_hash !== expectedPrev) {
return {
valid: false,
total: rows.length,
checked: rows.indexOf(row),
firstBreak: {
id: row.id,
action: row.action,
expected: expectedPrev,
actual: row.prev_hash,
},
};
}
// 重新计算 hash 验证
const hashInput = `${row.prev_hash}|${row.action}|${row.agent_id ?? ""}|${row.target ?? ""}|${row.details ?? ""}|${row.created_at}`;
const computedHash = createHash("sha256").update(hashInput).digest("hex");
if (computedHash !== row.record_hash) {
return {
valid: false,
total: rows.length,
checked: rows.indexOf(row) + 1,
firstBreak: {
id: row.id,
action: row.action,
expected: computedHash,
actual: row.record_hash,
},
};
}
expectedPrev = row.record_hash;
}
return { valid: true, total: rows.length, checked: rows.length };
}
// ─── 信任评分自动化(Phase 5a Day 2) ───────────────────
/**
* 重新计算 Agent 信任评分
*
* 公式:
* base = 50
* + verified_capabilities × 3
* + auto_approved_strategies × 2
* + positive_feedback × 1
* - negative_feedback × 2
* - rejected_applications × 3
* - revoked_token_count × 10
* → clamp(0, 100)
*
* @returns 计算后的信任分数
*/
export function recalculateTrustScore(agentId: string): number {
const verifiedCaps = (db.prepare(
`SELECT COUNT(*) as cnt FROM agent_capabilities WHERE agent_id=? AND verified=1`
).get(agentId) as any)?.cnt ?? 0;
const autoStrategies = (db.prepare(
`SELECT COUNT(*) as cnt FROM strategies WHERE proposer_id=? AND status='approved'`
).get(agentId) as any)?.cnt ?? 0;
// 注意:strategy_applications 没有 status/rejected 列,无法直接统计拒绝数
// 退而查 apply_strategy_fail 审计记录
const rejectedApps = (db.prepare(
`SELECT COUNT(*) as cnt FROM audit_log WHERE action='apply_strategy' AND agent_id=? AND details LIKE '%fail%'`
).get(agentId) as any)?.cnt ?? 0;
const revokedTokens = (db.prepare(
`SELECT COUNT(*) as cnt FROM audit_log WHERE action='revoke_token' AND agent_id=?`
).get(agentId) as any)?.cnt ?? 0;
// 注意:strategy_feedback.agent_id 是反馈者,不是提案者
// 要查"别人给该 agent 策略的反馈"需要 JOIN strategies.proposer_id
const positiveFb = (db.prepare(
`SELECT COUNT(*) as cnt FROM strategy_feedback sf
JOIN strategies s ON sf.strategy_id = s.id
WHERE s.proposer_id = ? AND sf.feedback = 'positive' AND sf.agent_id != ?`
).get(agentId, agentId) as any)?.cnt ?? 0;
const negativeFb = (db.prepare(
`SELECT COUNT(*) as cnt FROM strategy_feedback sf
JOIN strategies s ON sf.strategy_id = s.id
WHERE s.proposer_id = ? AND sf.feedback = 'negative' AND sf.agent_id != ?`
).get(agentId, agentId) as any)?.cnt ?? 0;
let score = 50;
score += verifiedCaps * 3;
score += autoStrategies * 2;
score += positiveFb * 1;
score -= negativeFb * 2;
score -= rejectedApps * 3;
score -= revokedTokens * 10;
// clamp(0, 100)
score = Math.max(0, Math.min(100, score));
// 写回 agents.trust_score
db.prepare(`UPDATE agents SET trust_score=? WHERE agent_id=?`).run(score, agentId);
return score;
}
/**
* 重新计算所有 Agent 的信任评分
* @returns { agent_id, score } 数组
*/
export function recalculateAllTrustScores(): Array<{ agent_id: string; score: number }> {
const agents = db.prepare(`SELECT agent_id FROM agents`).all() as Array<{ agent_id: string }>;
const results: Array<{ agent_id: string; score: number }> = [];
for (const agent of agents) {
const score = recalculateTrustScore(agent.agent_id);
results.push({ agent_id: agent.agent_id, score });
}
return results;
}
// ─── 路径安全 ────────────────────────────────────────────
/**
* 检查路径是否安全(防止路径遍历)
*/
export function sanitizePath(inputPath: string): boolean {
const normalized = inputPath.replace(/\\/g, "/");
return (
!normalized.includes("..") &&
!normalized.startsWith("/") &&
!normalized.includes("\0")
);
}
+1
View File
@@ -0,0 +1 @@
export {};
+547
View File
@@ -0,0 +1,547 @@
/**
* server.ts — 主入口
* Express HTTP 服务器 + MCP Server + SSE 推送 + Security 中间件
*
* Phase 5b 变更:
* - 结构化 JSON 日志(logger.ts
* - 全局错误处理中间件
* - 增强健康检查(/health
* - 优雅关闭(SIGTERM/SIGINT
* - Prometheus metrics 端点(/metrics
* - CORS + 安全头中间件
* - 请求追踪(traceId
*/
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { registerTools } from "./tools.js";
import { registerClient, removeClient, pushToAgent, onlineAgents, drainAllClients } from "./sse.js";
import { getDbStats, db, scheduleCleanup, stopCleanup } from "./db.js";
import { messageRepo, taskRepo, consumedRepo } from "./repo/sqlite-impl.js";
import { authMiddleware, optionalAuthMiddleware, createInviteCode, auditLog, rateLimiter, } from "./security.js";
import { startHeartbeatMonitor, stopHeartbeatMonitor } from "./identity.js";
import { startDedupCleanup, stopDedupCleanup } from "./dedup.js";
import { rebuildFtsIndex } from "./memory.js";
import { logger, logError } from "./logger.js";
import { join } from "path";
import { getMetricsOutput, trackHttpRequest, incrementGauge, decrementGauge, collectHubMetrics, } from "./metrics.js";
// ═══════════════════════════════════════════════════════════════
// Phase 6: 配置外部化(零依赖,所有配置有默认值)
// ═══════════════════════════════════════════════════════════════
const config = {
port: parseInt(process.env.PORT ?? "3100", 10),
logLevel: process.env.LOG_LEVEL || "info",
corsOrigins: (process.env.CORS_ORIGINS ?? "").split(",").map(s => s.trim()).filter(Boolean),
dbPath: process.env.DB_PATH || "./comm_hub.db",
sseHeartbeatInterval: parseInt(process.env.SSE_HEARTBEAT_INTERVAL ?? "10000", 10),
sseReplayWindow: parseInt(process.env.SSE_REPLAY_WINDOW ?? "3600", 10) * 1000,
rateLimitWindow: parseInt(process.env.RATE_LIMIT_WINDOW ?? "1000", 10),
rateLimitMax: parseInt(process.env.RATE_LIMIT_MAX ?? "10", 10),
heartbeatOnlineThreshold: parseInt(process.env.HEARTBEAT_ONLINE_THRESHOLD ?? "90000", 10),
heartbeatNotifyThreshold: parseInt(process.env.HEARTBEAT_NOTIFY_THRESHOLD ?? "300000", 10),
heartbeatCheckInterval: parseInt(process.env.HEARTBEAT_CHECK_INTERVAL ?? "30000", 10),
dedupTTL: parseInt(process.env.DEDUP_TTL ?? "900", 10) * 1000,
dedupCleanupInterval: parseInt(process.env.DEDUP_CLEANUP_INTERVAL ?? "60000", 10),
tokenExpireDays: parseInt(process.env.TOKEN_EXPIRE_DAYS ?? "90", 10),
uploadDir: process.env.UPLOAD_DIR || join(process.cwd(), "uploads"),
maxFileSize: parseInt(process.env.MAX_FILE_SIZE ?? "10485760", 10), // 10MB
};
const app = express();
app.use(express.json());
// ═══════════════════════════════════════════════════════════════
// Phase 5b: CORS 中间件(零依赖)
// ═══════════════════════════════════════════════════════════════
const CORS_ORIGINS = config.corsOrigins;
app.use((req, res, next) => {
const origin = req.headers.origin;
if (origin && CORS_ORIGINS.includes(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PATCH, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Trace-Id, X-Api-Key");
res.setHeader("Access-Control-Max-Age", "86400");
}
if (req.method === "OPTIONS") {
return res.sendStatus(204);
}
next();
});
// ═══════════════════════════════════════════════════════════════
// Phase 5b: 安全头中间件(零依赖 Helmet 替代)
// ═══════════════════════════════════════════════════════════════
app.use((_req, res, next) => {
res.setHeader("X-Frame-Options", "DENY");
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("X-XSS-Protection", "1; mode=block");
res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
res.setHeader("Content-Security-Policy", "default-src 'self'");
next();
});
// ═══════════════════════════════════════════════════════════════
// Phase 5b: 请求追踪(traceId
// ═══════════════════════════════════════════════════════════════
app.use((req, res, next) => {
const traceId = req.headers["x-trace-id"] || crypto.randomUUID().slice(0, 8);
req.traceId = traceId;
res.setHeader("X-Trace-Id", traceId);
next();
});
// ═══════════════════════════════════════════════════════════════
// Phase 5b: HTTP 请求日志 + metrics 中间件
// ═══════════════════════════════════════════════════════════════
app.use((req, res, next) => {
const start = Date.now();
res.on("finish", () => {
const duration = Date.now() - start;
const traceId = req.traceId;
logger.info("http_request", {
traceId,
module: "server",
method: req.method,
path: req.path,
status: res.statusCode,
duration_ms: duration,
});
trackHttpRequest(req.method, req.path, res.statusCode, duration);
});
next();
});
// ═══════════════════════════════════════════════════════════════
// SSE 端点:Agent 启动时订阅一次,保持长连接
// GET /events/:agent_id?token=<api_token>
// ═══════════════════════════════════════════════════════════════
// SSE 重连回放窗口(秒),默认 1 小时
const SSE_REPLAY_WINDOW = config.sseReplayWindow;
app.get("/events/:agent_id", optionalAuthMiddleware, (req, res) => {
const { agent_id } = req.params;
const authContext = req.auth?.agent;
// SSE 必要响应头
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.setHeader("X-Accel-Buffering", "no");
res.flushHeaders();
// 注册连接
registerClient(agent_id, res);
incrementGauge("active_sse_connections");
// 检查 Last-Event-ID(断线重连场景)
const lastEventId = req.headers["last-event-id"];
if (lastEventId) {
// 解析 lastEventId 为时间戳(毫秒)
const since = parseInt(lastEventId, 10);
if (!isNaN(since)) {
// 检查是否在回放窗口内
const now = Date.now();
const windowStart = now - SSE_REPLAY_WINDOW;
const effectiveSince = Math.max(since, windowStart);
// 查询并回放该时间戳之后的消息
const missedMessages = messageRepo.listSince(agent_id, effectiveSince);
if (missedMessages.length > 0) {
for (const msg of missedMessages) {
pushToAgent(agent_id, {
event: "new_message",
message: msg,
});
}
logger.info("SSE replay", { module: "sse", agent_id, replay_count: missedMessages.length, since: effectiveSince });
}
}
}
else {
// 首次连接:补发离线期间积压的未读消息
const pending = messageRepo.pendingFor(agent_id);
if (pending.length > 0) {
for (const msg of pending) {
pushToAgent(agent_id, {
event: "new_message",
message: msg,
});
}
messageRepo.markAllDelivered(agent_id);
logger.info("SSE backfill", { module: "sse", agent_id, pending_count: pending.length });
}
}
// 补发积压的未执行任务
const pendingTasks = taskRepo.listFor(agent_id, "pending");
for (const task of pendingTasks) {
pushToAgent(agent_id, {
event: "task_assigned",
task: {
...task,
instruction: "你有一项待执行的任务,请立即处理。",
},
});
}
if (pendingTasks.length > 0) {
logger.info("SSE tasks push", { module: "sse", agent_id, pending_tasks: pendingTasks.length });
}
// 心跳(10 秒间隔)
const heartbeat = setInterval(() => {
try {
res.write(": ping\n\n");
}
catch (_) {
clearInterval(heartbeat);
}
}, config.sseHeartbeatInterval);
// 断线清理
req.on("close", () => {
clearInterval(heartbeat);
removeClient(agent_id);
decrementGauge("active_sse_connections");
});
});
// ═══════════════════════════════════════════════════════════════
// Phase 5b: 增强健康检查端点(免认证)
// ═══════════════════════════════════════════════════════════════
app.get("/health", (_req, res) => {
const stats = getDbStats();
const mem = process.memoryUsage();
let dbSize = 0;
try {
const row = db.prepare(`SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size()`).get();
dbSize = row?.size ?? 0;
}
catch { }
res.json({
status: "ok",
version: "2.3.1",
uptime: process.uptime(),
timestamp: Date.now(),
memory: {
rss: Math.round(mem.rss / 1024 / 1024),
heap_used: Math.round(mem.heapUsed / 1024 / 1024),
heap_total: Math.round(mem.heapTotal / 1024 / 1024),
},
db: {
size: dbSize,
size_mb: Math.round(dbSize / 1024 / 1024 * 100) / 100,
tables: stats,
},
sse: {
active_connections: onlineAgents().length,
},
});
});
// ═══════════════════════════════════════════════════════════════
// Phase 5b: Prometheus Metrics 端点(免认证)
// ═══════════════════════════════════════════════════════════════
app.get("/metrics", (_req, res) => {
res.setHeader("Content-Type", "text/plain; version=0.0.4; charset=utf-8");
// Phase 3.1: 拼接 Hub 数据库指标(agents / messages / trust_scores
const hubMetrics = collectHubMetrics(db);
const output = getMetricsOutput() + hubMetrics;
res.send(output);
});
// ═══════════════════════════════════════════════════════════════
// 管理端点:/admin/invite/generate — 生成邀请码
// ═══════════════════════════════════════════════════════════════
app.post("/admin/invite/generate", authMiddleware, (req, res) => {
const role = req.auth?.agent?.role;
if (role !== "admin") {
res.status(403).json({ error: "Admin access required" });
return;
}
const targetRole = req.body.role === "admin" ? "admin" : "member";
const code = createInviteCode(targetRole);
auditLog("invite_generated", req.auth?.agent?.agentId ?? null, undefined, `role=${targetRole}`);
res.json({
success: true,
invite_code: code,
role: targetRole,
expires_in: "24h",
});
});
// ═══════════════════════════════════════════════════════════════
// REST API:供自动化脚本通过 curl 轮询任务和消息(需认证)
// ═══════════════════════════════════════════════════════════════
// GET /api/tasks?agent_id=workbuddy&status=pending
app.get("/api/tasks", authMiddleware, (req, res) => {
const { agent_id, status } = req.query;
if (!agent_id) {
res.status(400).json({ error: "agent_id is required" });
return;
}
if (status && !["pending", "in_progress", "completed", "failed"].includes(status)) {
res.status(400).json({ error: `Invalid status: ${status}` });
return;
}
const tasks = status
? taskRepo.listFor(agent_id, status)
: taskRepo.listFor(agent_id, "pending");
res.json({ tasks, count: tasks.length });
});
// GET /api/messages?agent_id=workbuddy&status=unread
app.get("/api/messages", authMiddleware, (req, res) => {
const { agent_id, status } = req.query;
if (!agent_id) {
res.status(400).json({ error: "agent_id is required" });
return;
}
const validStatuses = ["unread", "delivered", "read", "acknowledged"];
if (status && !validStatuses.includes(status)) {
res.status(400).json({ error: `Invalid status: ${status}. Valid: ${validStatuses.join(", ")}` });
return;
}
const queryStatus = status || "unread";
const messages = messageRepo.listByStatus(agent_id, queryStatus);
res.json({ messages, count: messages.length });
});
// PATCH /api/tasks/:id/status
app.patch("/api/tasks/:id/status", authMiddleware, (req, res) => {
const { status, result, progress } = req.body;
if (!["in_progress", "completed", "failed"].includes(status)) {
res.status(400).json({ error: `Invalid status: ${status}` });
return;
}
const task = taskRepo.getById(req.params.id);
if (!task) {
res.status(404).json({ error: "Task not found" });
return;
}
taskRepo.update(req.params.id, status, result || null, progress || 0);
pushToAgent(task.assigned_by, {
event: "task_updated",
update: {
task_id: task.id,
status,
result: result || null,
progress: progress || 0,
updated_by: "workbuddy-automation",
timestamp: Date.now(),
},
});
res.json({ success: true, task_id: task.id, status });
});
// PATCH /api/messages/:id/status
app.patch("/api/messages/:id/status", authMiddleware, (req, res) => {
const { status } = req.body;
const validStatuses = ["read", "delivered", "acknowledged"];
if (!validStatuses.includes(status)) {
res.status(400).json({ error: `Invalid status: ${status}. Valid: ${validStatuses.join(", ")}` });
return;
}
try {
messageRepo.updateStatus(req.params.id, status);
res.json({ success: true, message_id: req.params.id, status });
}
catch (err) {
res.status(500).json({ error: err.message });
}
});
// GET /api/consumed?agent_id=hermes&resource=feedback/xxx.json
app.get("/api/consumed", authMiddleware, (req, res) => {
const { agent_id, resource } = req.query;
if (!agent_id) {
res.status(400).json({ error: "agent_id is required" });
return;
}
if (resource) {
const record = consumedRepo.check(agent_id, resource);
res.json({
consumed: !!record,
resource,
record: record || null,
});
}
else {
const records = consumedRepo.listByAgent(agent_id, 50);
res.json({ records, count: records.length });
}
});
// ═══════════════════════════════════════════════════════════════
// MCP 端点:Stateless 模式
// ═══════════════════════════════════════════════════════════════
function createMcpServer(authContext) {
const server = new McpServer({
name: "agent-comm-hub",
version: "2.3.1",
});
registerTools(server, authContext);
return server;
}
function extractToolName(req) {
try {
const body = req.body;
if (body?.method === "tools/call" && body?.params?.name) {
return body.params.name;
}
}
catch { }
return null;
}
// POST /mcp
app.post("/mcp", optionalAuthMiddleware, async (req, res) => {
const authContext = req.auth?.agent
? { agentId: req.auth.agent.agentId, role: req.auth.agent.role }
: undefined;
if (authContext) {
if (!rateLimiter(authContext.agentId)) {
res.status(429).json({
jsonrpc: "2.0",
error: { code: -32001, message: "Rate limit exceeded (10 req/s)" },
id: null,
});
return;
}
}
const server = createMcpServer(authContext);
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
try {
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
res.on("close", () => {
transport.close();
server.close();
});
}
catch (error) {
logError("[MCP] handleRequest error", error, { module: "mcp", traceId: req.traceId });
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
error: { code: -32603, message: "Internal server error" },
id: null,
});
}
}
});
// GET /mcp
app.get("/mcp", optionalAuthMiddleware, async (req, res) => {
const authContext = req.auth?.agent
? { agentId: req.auth.agent.agentId, role: req.auth.agent.role }
: undefined;
const server = createMcpServer(authContext);
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
try {
await server.connect(transport);
await transport.handleRequest(req, res, undefined);
res.on("close", () => {
transport.close();
server.close();
});
}
catch (error) {
logError("[MCP] GET /mcp error", error, { module: "mcp", traceId: req.traceId });
if (!res.headersSent) {
res.status(500).end();
}
}
});
// DELETE /mcp
app.delete("/mcp", optionalAuthMiddleware, async (req, res) => {
const authContext = req.auth?.agent
? { agentId: req.auth.agent.agentId, role: req.auth.agent.role }
: undefined;
const server = createMcpServer(authContext);
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
try {
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
res.on("close", () => {
transport.close();
server.close();
});
}
catch (error) {
logError("[MCP] DELETE /mcp error", error, { module: "mcp", traceId: req.traceId });
if (!res.headersSent) {
res.status(500).end();
}
}
});
// ═══════════════════════════════════════════════════════════════
// Phase 5b: 404 处理(在 error handler 之前)
// ═══════════════════════════════════════════════════════════════
app.use((req, res) => {
const traceId = req.traceId;
res.status(404).json({
error: true,
message: "Not Found",
traceId,
});
});
// ═══════════════════════════════════════════════════════════════
// Phase 5b: 全局错误处理中间件(放在所有路由之后)
// ═══════════════════════════════════════════════════════════════
app.use((err, req, res, _next) => {
const traceId = req.traceId;
logError("unhandled_error", err, { traceId, path: req.path, method: req.method });
if (res.headersSent)
return;
res.status(err.status || 500).json({
error: true,
message: process.env.NODE_ENV === "development" ? err.message : "Internal Server Error",
traceId,
});
});
// ═══════════════════════════════════════════════════════════════
// Phase 5b: 优雅关闭
// ═══════════════════════════════════════════════════════════════
let httpServer = null;
async function gracefulShutdown(signal) {
logger.info("shutdown_initiated", { signal, module: "server" });
// 1. 停止接受新连接
if (httpServer) {
httpServer.close(() => {
logger.info("http_server_closed", { module: "server" });
});
}
// 2. drain SSE 连接
drainAllClients();
logger.info("sse_drained", { module: "server" });
// 3. 停止定时器
stopHeartbeatMonitor();
stopDedupCleanup();
stopCleanup();
// 4. 关闭数据库
try {
db.close();
logger.info("database_closed", { module: "server" });
}
catch (err) {
logError("database_close_error", err, { module: "server" });
}
logger.info("shutdown_complete", { module: "server" });
process.exit(0);
}
// ═══════════════════════════════════════════════════════════════
// 未捕获异常兜底
// ═══════════════════════════════════════════════════════════════
process.on("uncaughtException", (err) => {
logError("uncaught_exception", err, { module: "process" });
process.exit(1);
});
process.on("unhandledRejection", (reason) => {
logError("unhandled_rejection", reason, { module: "process" });
});
// ═══════════════════════════════════════════════════════════════
// 启动
// ═══════════════════════════════════════════════════════════════
httpServer = app.listen(config.port, () => {
logger.info("server_started", {
module: "server",
version: "2.3.1",
port: config.port,
phase: "5b",
});
// 启动心跳超时监控
startHeartbeatMonitor((agentId) => {
logger.info("agent_offline_timeout", { module: "monitor", agent_id: agentId });
});
// 启动去重缓存 TTL 清理(15min)
startDedupCleanup();
// Phase 6: 启动定时清理(过期 Token / Dedup / Consumed
scheduleCleanup(config.dedupTTL);
// 重建 FTS 索引
rebuildFtsIndex();
});
// 优雅关闭信号监听
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
//# sourceMappingURL=server.js.map
+613
View File
@@ -0,0 +1,613 @@
/**
* server.ts — 主入口
* Express HTTP 服务器 + MCP Server + SSE 推送 + Security 中间件
*
* Phase 5b 变更:
* - 结构化 JSON 日志(logger.ts
* - 全局错误处理中间件
* - 增强健康检查(/health
* - 优雅关闭(SIGTERM/SIGINT
* - Prometheus metrics 端点(/metrics
* - CORS + 安全头中间件
* - 请求追踪(traceId
*/
import express, { type Request, type Response, type NextFunction } from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { registerTools } from "./tools.js";
import { registerClient, removeClient, pushToAgent, onlineAgents, drainAllClients } from "./sse.js";
import { getDbStats, db, scheduleCleanup, stopCleanup } from "./db.js";
import { messageRepo, taskRepo, consumedRepo } from "./repo/sqlite-impl.js";
import {
authMiddleware,
optionalAuthMiddleware,
checkPermission,
getRequiredPermission,
createInviteCode,
auditLog,
rateLimiter,
type AuthContext,
} from "./security.js";
import { startHeartbeatMonitor, clearOfflineNotification, stopHeartbeatMonitor } from "./identity.js";
import { startDedupCleanup, stopDedupCleanup } from "./dedup.js";
import { getErrorMessage } from "./types.js";
import { rebuildFtsIndex } from "./memory.js";
import { logger, logError } from "./logger.js";
import { join } from "path";
import {
getMetricsOutput,
trackHttpRequest,
setGauge,
incrementGauge,
decrementGauge,
collectHubMetrics,
} from "./metrics.js";
// ═══════════════════════════════════════════════════════════════
// Phase 6: 配置外部化(零依赖,所有配置有默认值)
// ═══════════════════════════════════════════════════════════════
const config = {
port: parseInt(process.env.PORT ?? "3100", 10),
logLevel: process.env.LOG_LEVEL || "info",
corsOrigins: (process.env.CORS_ORIGINS ?? "").split(",").map(s => s.trim()).filter(Boolean),
dbPath: process.env.DB_PATH || "./comm_hub.db",
sseHeartbeatInterval: parseInt(process.env.SSE_HEARTBEAT_INTERVAL ?? "10000", 10),
sseReplayWindow: parseInt(process.env.SSE_REPLAY_WINDOW ?? "3600", 10) * 1000,
rateLimitWindow: parseInt(process.env.RATE_LIMIT_WINDOW ?? "1000", 10),
rateLimitMax: parseInt(process.env.RATE_LIMIT_MAX ?? "10", 10),
heartbeatOnlineThreshold: parseInt(process.env.HEARTBEAT_ONLINE_THRESHOLD ?? "90000", 10),
heartbeatNotifyThreshold: parseInt(process.env.HEARTBEAT_NOTIFY_THRESHOLD ?? "300000", 10),
heartbeatCheckInterval: parseInt(process.env.HEARTBEAT_CHECK_INTERVAL ?? "30000", 10),
dedupTTL: parseInt(process.env.DEDUP_TTL ?? "900", 10) * 1000,
dedupCleanupInterval: parseInt(process.env.DEDUP_CLEANUP_INTERVAL ?? "60000", 10),
tokenExpireDays: parseInt(process.env.TOKEN_EXPIRE_DAYS ?? "90", 10),
uploadDir: process.env.UPLOAD_DIR || join(process.cwd(), "uploads"),
maxFileSize: parseInt(process.env.MAX_FILE_SIZE ?? "10485760", 10), // 10MB
};
const app = express();
app.use(express.json());
// ═══════════════════════════════════════════════════════════════
// Phase 5b: CORS 中间件(零依赖)
// ═══════════════════════════════════════════════════════════════
const CORS_ORIGINS = config.corsOrigins;
app.use((req: Request, res: Response, next: NextFunction) => {
const origin = req.headers.origin as string | undefined;
if (origin && CORS_ORIGINS.includes(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PATCH, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Trace-Id, X-Api-Key");
res.setHeader("Access-Control-Max-Age", "86400");
}
if (req.method === "OPTIONS") {
return res.sendStatus(204);
}
next();
});
// ═══════════════════════════════════════════════════════════════
// Phase 5b: 安全头中间件(零依赖 Helmet 替代)
// ═══════════════════════════════════════════════════════════════
app.use((_req: Request, res: Response, next: NextFunction) => {
res.setHeader("X-Frame-Options", "DENY");
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("X-XSS-Protection", "1; mode=block");
res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
res.setHeader("Content-Security-Policy", "default-src 'self'");
next();
});
// ═══════════════════════════════════════════════════════════════
// Phase 5b: 请求追踪(traceId
// ═══════════════════════════════════════════════════════════════
app.use((req: Request, res: Response, next: NextFunction) => {
const traceId = (req.headers["x-trace-id"] as string) || crypto.randomUUID().slice(0, 8);
(req as any).traceId = traceId;
res.setHeader("X-Trace-Id", traceId);
next();
});
// ═══════════════════════════════════════════════════════════════
// Phase 5b: HTTP 请求日志 + metrics 中间件
// ═══════════════════════════════════════════════════════════════
app.use((req: Request, res: Response, next: NextFunction) => {
const start = Date.now();
res.on("finish", () => {
const duration = Date.now() - start;
const traceId = (req as any).traceId;
logger.info("http_request", {
traceId,
module: "server",
method: req.method,
path: req.path,
status: res.statusCode,
duration_ms: duration,
});
trackHttpRequest(req.method, req.path, res.statusCode, duration);
});
next();
});
// ═══════════════════════════════════════════════════════════════
// SSE 端点:Agent 启动时订阅一次,保持长连接
// GET /events/:agent_id?token=<api_token>
// ═══════════════════════════════════════════════════════════════
// SSE 重连回放窗口(秒),默认 1 小时
const SSE_REPLAY_WINDOW = config.sseReplayWindow;
app.get("/events/:agent_id", optionalAuthMiddleware, (req: Request, res: Response) => {
const { agent_id } = req.params;
const authContext: AuthContext | undefined = req.auth?.agent;
// SSE 必要响应头
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.setHeader("X-Accel-Buffering", "no");
res.flushHeaders();
// 注册连接
registerClient(agent_id, res);
incrementGauge("active_sse_connections");
// 检查 Last-Event-ID(断线重连场景)
const lastEventId = req.headers["last-event-id"] as string | undefined;
if (lastEventId) {
// 解析 lastEventId 为时间戳(毫秒)
const since = parseInt(lastEventId, 10);
if (!isNaN(since)) {
// 检查是否在回放窗口内
const now = Date.now();
const windowStart = now - SSE_REPLAY_WINDOW;
const effectiveSince = Math.max(since, windowStart);
// 查询并回放该时间戳之后的消息
const missedMessages = messageRepo.listSince(agent_id, effectiveSince);
if (missedMessages.length > 0) {
for (const msg of missedMessages) {
pushToAgent(agent_id, {
event: "new_message",
message: msg,
});
}
logger.info("SSE replay", { module: "sse", agent_id, replay_count: missedMessages.length, since: effectiveSince });
}
}
} else {
// 首次连接:补发离线期间积压的未读消息
const pending = messageRepo.pendingFor(agent_id);
if (pending.length > 0) {
for (const msg of pending) {
pushToAgent(agent_id, {
event: "new_message",
message: msg,
});
}
messageRepo.markAllDelivered(agent_id);
logger.info("SSE backfill", { module: "sse", agent_id, pending_count: pending.length });
}
}
// 补发积压的未执行任务
const pendingTasks = taskRepo.listFor(agent_id, "pending");
for (const task of pendingTasks) {
pushToAgent(agent_id, {
event: "task_assigned",
task: {
...task,
instruction: "你有一项待执行的任务,请立即处理。",
},
});
}
if (pendingTasks.length > 0) {
logger.info("SSE tasks push", { module: "sse", agent_id, pending_tasks: pendingTasks.length });
}
// 心跳(10 秒间隔)
const heartbeat = setInterval(() => {
try { res.write(": ping\n\n"); } catch (_) { clearInterval(heartbeat); }
}, config.sseHeartbeatInterval);
// 断线清理
req.on("close", () => {
clearInterval(heartbeat);
removeClient(agent_id);
decrementGauge("active_sse_connections");
});
});
// ═══════════════════════════════════════════════════════════════
// Phase 5b: 增强健康检查端点(免认证)
// ═══════════════════════════════════════════════════════════════
app.get("/health", (_req: Request, res: Response) => {
const stats = getDbStats();
const mem = process.memoryUsage();
let dbSize = 0;
try {
const row = db.prepare(`SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size()`).get() as any;
dbSize = row?.size ?? 0;
} catch {}
res.json({
status: "ok",
version: "2.4.0",
uptime: process.uptime(),
timestamp: Date.now(),
memory: {
rss: Math.round(mem.rss / 1024 / 1024),
heap_used: Math.round(mem.heapUsed / 1024 / 1024),
heap_total: Math.round(mem.heapTotal / 1024 / 1024),
},
db: {
size: dbSize,
size_mb: Math.round(dbSize / 1024 / 1024 * 100) / 100,
tables: stats,
},
sse: {
active_connections: onlineAgents().length,
},
});
});
// ═══════════════════════════════════════════════════════════════
// Phase 5b: Prometheus Metrics 端点(免认证)
// ═══════════════════════════════════════════════════════════════
app.get("/metrics", (_req: Request, res: Response) => {
res.setHeader("Content-Type", "text/plain; version=0.0.4; charset=utf-8");
// Phase 3.1: 拼接 Hub 数据库指标(agents / messages / trust_scores
const hubMetrics = collectHubMetrics(db);
const output = getMetricsOutput() + hubMetrics;
res.send(output);
});
// ═══════════════════════════════════════════════════════════════
// 管理端点:/admin/invite/generate — 生成邀请码
// ═══════════════════════════════════════════════════════════════
app.post("/admin/invite/generate", authMiddleware, (req: Request, res: Response) => {
const role = req.auth?.agent?.role;
if (role !== "admin") {
res.status(403).json({ error: "Admin access required" });
return;
}
const targetRole = req.body.role === "admin" ? "admin" as const : "member" as const;
const code = createInviteCode(targetRole);
auditLog("invite_generated", req.auth?.agent?.agentId ?? null, undefined, `role=${targetRole}`);
res.json({
success: true,
invite_code: code,
role: targetRole,
expires_in: "24h",
});
});
// ═══════════════════════════════════════════════════════════════
// REST API:供自动化脚本通过 curl 轮询任务和消息(需认证)
// ═══════════════════════════════════════════════════════════════
// GET /api/tasks?agent_id=workbuddy&status=pending
app.get("/api/tasks", authMiddleware, (req: Request, res: Response) => {
const { agent_id, status } = req.query;
if (!agent_id) {
res.status(400).json({ error: "agent_id is required" });
return;
}
if (status && !["pending", "in_progress", "completed", "failed"].includes(status as string)) {
res.status(400).json({ error: `Invalid status: ${status}` });
return;
}
const tasks = status
? taskRepo.listFor(agent_id as string, status as string)
: taskRepo.listFor(agent_id as string, "pending");
res.json({ tasks, count: tasks.length });
});
// GET /api/messages?agent_id=workbuddy&status=unread
app.get("/api/messages", authMiddleware, (req: Request, res: Response) => {
const { agent_id, status } = req.query;
if (!agent_id) {
res.status(400).json({ error: "agent_id is required" });
return;
}
const validStatuses = ["unread", "delivered", "read", "acknowledged"];
if (status && !validStatuses.includes(status as string)) {
res.status(400).json({ error: `Invalid status: ${status}. Valid: ${validStatuses.join(", ")}` });
return;
}
const queryStatus = (status as string) || "unread";
const messages = messageRepo.listByStatus(agent_id as string, queryStatus);
res.json({ messages, count: messages.length });
});
// PATCH /api/tasks/:id/status
app.patch("/api/tasks/:id/status", authMiddleware, (req: Request, res: Response) => {
const { status, result, progress } = req.body;
if (!["in_progress", "completed", "failed"].includes(status)) {
res.status(400).json({ error: `Invalid status: ${status}` });
return;
}
const task = taskRepo.getById(req.params.id);
if (!task) {
res.status(404).json({ error: "Task not found" });
return;
}
taskRepo.update(req.params.id, status, result || null, progress || 0);
pushToAgent(task.assigned_by, {
event: "task_updated",
update: {
task_id: task.id,
status,
result: result || null,
progress: progress || 0,
updated_by: "workbuddy-automation",
timestamp: Date.now(),
},
});
res.json({ success: true, task_id: task.id, status });
});
// PATCH /api/messages/:id/status
app.patch("/api/messages/:id/status", authMiddleware, (req: Request, res: Response) => {
const { status } = req.body;
const validStatuses = ["read", "delivered", "acknowledged"];
if (!validStatuses.includes(status)) {
res.status(400).json({ error: `Invalid status: ${status}. Valid: ${validStatuses.join(", ")}` });
return;
}
try {
messageRepo.updateStatus(req.params.id, status);
res.json({ success: true, message_id: req.params.id, status });
} catch (err: unknown) {
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
}
});
// GET /api/consumed?agent_id=hermes&resource=feedback/xxx.json
app.get("/api/consumed", authMiddleware, (req: Request, res: Response) => {
const { agent_id, resource } = req.query;
if (!agent_id) {
res.status(400).json({ error: "agent_id is required" });
return;
}
if (resource) {
const record = consumedRepo.check(agent_id as string, resource as string);
res.json({
consumed: !!record,
resource,
record: record || null,
});
} else {
const records = consumedRepo.listByAgent(agent_id as string, 50);
res.json({ records, count: records.length });
}
});
// ═══════════════════════════════════════════════════════════════
// MCP 端点:Stateless 模式
// ═══════════════════════════════════════════════════════════════
function createMcpServer(authContext: AuthContext | undefined): McpServer {
const server = new McpServer({
name: "agent-comm-hub",
version: "2.4.0",
});
registerTools(server, authContext);
return server;
}
function extractToolName(req: express.Request): string | null {
try {
const body = req.body;
if (body?.method === "tools/call" && body?.params?.name) {
return body.params.name as string;
}
} catch {}
return null;
}
// POST /mcp
app.post("/mcp", optionalAuthMiddleware, async (req: Request, res: Response) => {
const authContext: AuthContext | undefined = req.auth?.agent
? { agentId: req.auth.agent.agentId, role: req.auth.agent.role }
: undefined;
if (authContext) {
if (!rateLimiter(authContext.agentId)) {
res.status(429).json({
jsonrpc: "2.0",
error: { code: -32001, message: "Rate limit exceeded (10 req/s)" },
id: null,
});
return;
}
}
const server = createMcpServer(authContext);
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
try {
await server.connect(transport);
await transport.handleRequest(req as any, res as any, req.body);
res.on("close", () => {
transport.close();
server.close();
});
} catch (error) {
logError("[MCP] handleRequest error", error, { module: "mcp", traceId: (req as any).traceId });
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
error: { code: -32603, message: "Internal server error" },
id: null,
});
}
}
});
// GET /mcp
app.get("/mcp", optionalAuthMiddleware, async (req: Request, res: Response) => {
const authContext: AuthContext | undefined = req.auth?.agent
? { agentId: req.auth.agent.agentId, role: req.auth.agent.role }
: undefined;
const server = createMcpServer(authContext);
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
try {
await server.connect(transport);
await transport.handleRequest(req as any, res as any, undefined);
res.on("close", () => {
transport.close();
server.close();
});
} catch (error) {
logError("[MCP] GET /mcp error", error, { module: "mcp", traceId: (req as any).traceId });
if (!res.headersSent) {
res.status(500).end();
}
}
});
// DELETE /mcp
app.delete("/mcp", optionalAuthMiddleware, async (req: Request, res: Response) => {
const authContext: AuthContext | undefined = req.auth?.agent
? { agentId: req.auth.agent.agentId, role: req.auth.agent.role }
: undefined;
const server = createMcpServer(authContext);
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
try {
await server.connect(transport);
await transport.handleRequest(req as any, res as any, req.body);
res.on("close", () => {
transport.close();
server.close();
});
} catch (error) {
logError("[MCP] DELETE /mcp error", error, { module: "mcp", traceId: (req as any).traceId });
if (!res.headersSent) {
res.status(500).end();
}
}
});
// ═══════════════════════════════════════════════════════════════
// Phase 5b: 404 处理(在 error handler 之前)
// ═══════════════════════════════════════════════════════════════
app.use((req: Request, res: Response) => {
const traceId = (req as any).traceId;
res.status(404).json({
error: true,
message: "Not Found",
traceId,
});
});
// ═══════════════════════════════════════════════════════════════
// Phase 5b: 全局错误处理中间件(放在所有路由之后)
// ═══════════════════════════════════════════════════════════════
app.use((err: Error & { status?: number }, req: Request, res: Response, _next: NextFunction) => {
const traceId = (req as unknown as Record<string, unknown>).traceId as string | undefined;
logError("unhandled_error", err, { traceId, path: req.path, method: req.method });
if (res.headersSent) return;
res.status(err.status || 500).json({
error: true,
message: process.env.NODE_ENV === "development" ? err.message : "Internal Server Error",
traceId,
});
});
// ═══════════════════════════════════════════════════════════════
// Phase 5b: 优雅关闭
// ═══════════════════════════════════════════════════════════════
let httpServer: ReturnType<typeof app.listen> | null = null;
async function gracefulShutdown(signal: string): Promise<void> {
logger.info("shutdown_initiated", { signal, module: "server" });
// 1. 停止接受新连接
if (httpServer) {
httpServer.close(() => {
logger.info("http_server_closed", { module: "server" });
});
}
// 2. drain SSE 连接
drainAllClients();
logger.info("sse_drained", { module: "server" });
// 3. 停止定时器
stopHeartbeatMonitor();
stopDedupCleanup();
stopCleanup();
// 4. 关闭数据库
try {
db.close();
logger.info("database_closed", { module: "server" });
} catch (err) {
logError("database_close_error", err, { module: "server" });
}
logger.info("shutdown_complete", { module: "server" });
process.exit(0);
}
// ═══════════════════════════════════════════════════════════════
// 未捕获异常兜底
// ═══════════════════════════════════════════════════════════════
process.on("uncaughtException", (err: Error) => {
logError("uncaught_exception", err, { module: "process" });
process.exit(1);
});
process.on("unhandledRejection", (reason: unknown) => {
logError("unhandled_rejection", reason, { module: "process" });
});
// ═══════════════════════════════════════════════════════════════
// 启动
// ═══════════════════════════════════════════════════════════════
httpServer = app.listen(config.port, () => {
logger.info("server_started", {
module: "server",
version: "2.4.0",
port: config.port,
phase: "5b",
});
// 启动心跳超时监控
startHeartbeatMonitor((agentId) => {
logger.info("agent_offline_timeout", { module: "monitor", agent_id: agentId });
});
// 启动去重缓存 TTL 清理(15min)
startDedupCleanup();
// Phase 6: 启动定时清理(过期 Token / Dedup / Consumed
scheduleCleanup(config.dedupTTL);
// 重建 FTS 索引
rebuildFtsIndex();
});
// 优雅关闭信号监听
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
+47
View File
@@ -0,0 +1,47 @@
/**
* sse.ts — SSE 连接管理 (Phase 1 Week 2 增强)
* 维护 AgentID → Response 映射,实现零轮询实时推送
*
* Week 2 增强:
* - pushToAgent 支持可选的 dedup_id,用于客户端去重
* - 每个 SSE 事件附加 event_id(递增),客户端可据此去重
*/
import type { Response } from "express";
/**
* 获取下一个 event_id(不递增,预览用)
*/
export declare function peekNextEventId(agentId: string): number;
/**
* 注册 Agent 的 SSE 连接
*/
export declare function registerClient(agentId: string, res: Response): void;
/**
* 移除 Agent 连接(断线时调用)
*/
export declare function removeClient(agentId: string): void;
/**
* 向指定 Agent 推送事件
*
* 每个推送附加递增的 event_id,客户端可据此实现去重:
* - event_id 是严格递增的
* - 客户端保存 last_seen_event_id,忽略 ≤ last_seen 的消息
*
* @param agentId 目标 Agent
* @param event 事件数据(会被序列化为 JSON)
* @param dedupId 可选的去重标识(如 msg_hash),附加到事件中供客户端验证
* @returns true = 在线已推送;false = 离线,消息已持久化等待补发
*/
export declare function pushToAgent(agentId: string, event: object, dedupId?: string): boolean;
/**
* 广播给多个 Agent
*/
export declare function broadcast(agentIds: string[], event: object): Record<string, boolean>;
/**
* 查询哪些 Agent 在线
*/
export declare function onlineAgents(): string[];
/**
* Phase 5b: 优雅关闭时 drain 所有 SSE 连接
* 向每个客户端发送 close 事件后关闭连接
*/
export declare function drainAllClients(): void;
+112
View File
@@ -0,0 +1,112 @@
import { logger } from "./logger.js";
// 在线 Agent 连接池
const clients = new Map();
// ─── 客户端去重:per-connection 递增 event_id ─────────────
const clientEventCounters = new Map();
function nextEventId(agentId) {
const current = clientEventCounters.get(agentId) ?? 0;
const next = current + 1;
clientEventCounters.set(agentId, next);
return next;
}
/**
* 获取下一个 event_id(不递增,预览用)
*/
export function peekNextEventId(agentId) {
return (clientEventCounters.get(agentId) ?? 0) + 1;
}
/**
* 注册 Agent 的 SSE 连接
*/
export function registerClient(agentId, res) {
// 如果已有旧连接,先关掉(Agent 重启场景)
const existing = clients.get(agentId);
if (existing) {
try {
existing.end();
}
catch (_) { }
}
clients.set(agentId, res);
// 重置 event counter
clientEventCounters.set(agentId, 0);
logger.info("sse_client_connected", { module: "sse", agent_id: agentId, total: clients.size });
}
/**
* 移除 Agent 连接(断线时调用)
*/
export function removeClient(agentId) {
clients.delete(agentId);
clientEventCounters.delete(agentId);
logger.info("sse_client_disconnected", { module: "sse", agent_id: agentId, total: clients.size });
}
/**
* 向指定 Agent 推送事件
*
* 每个推送附加递增的 event_id,客户端可据此实现去重:
* - event_id 是严格递增的
* - 客户端保存 last_seen_event_id,忽略 ≤ last_seen 的消息
*
* @param agentId 目标 Agent
* @param event 事件数据(会被序列化为 JSON)
* @param dedupId 可选的去重标识(如 msg_hash),附加到事件中供客户端验证
* @returns true = 在线已推送;false = 离线,消息已持久化等待补发
*/
export function pushToAgent(agentId, event, dedupId) {
const res = clients.get(agentId);
if (!res)
return false;
try {
const eventId = nextEventId(agentId);
const payload = {
...event,
_hub_event_id: eventId,
...(dedupId ? { _hub_dedup_id: dedupId } : {}),
};
// SSE 事件格式:id + event + data
res.write(`id: ${eventId}\n`);
res.write(`event: message\n`);
res.write(`data: ${JSON.stringify(payload)}\n\n`);
return true;
}
catch (err) {
// 连接异常,移除
removeClient(agentId);
return false;
}
}
/**
* 广播给多个 Agent
*/
export function broadcast(agentIds, event) {
const results = {};
for (const id of agentIds) {
results[id] = pushToAgent(id, event);
}
return results;
}
/**
* 查询哪些 Agent 在线
*/
export function onlineAgents() {
return [...clients.keys()];
}
/**
* Phase 5b: 优雅关闭时 drain 所有 SSE 连接
* 向每个客户端发送 close 事件后关闭连接
*/
export function drainAllClients() {
for (const [agentId, res] of clients.entries()) {
try {
const eventId = nextEventId(agentId);
res.write(`id: ${eventId}\n`);
res.write(`event: hub_shutdown\n`);
res.write(`data: {"message":"Server shutting down"}\n\n`);
res.end();
}
catch { }
}
clients.clear();
clientEventCounters.clear();
}
//# sourceMappingURL=sse.js.map
+124
View File
@@ -0,0 +1,124 @@
/**
* sse.ts — SSE 连接管理 (Phase 1 Week 2 增强)
* 维护 AgentID → Response 映射,实现零轮询实时推送
*
* Week 2 增强:
* - pushToAgent 支持可选的 dedup_id,用于客户端去重
* - 每个 SSE 事件附加 event_id(递增),客户端可据此去重
*/
import type { Response } from "express";
import { logger } from "./logger.js";
// 在线 Agent 连接池
const clients = new Map<string, Response>();
// ─── 客户端去重:per-connection 递增 event_id ─────────────
const clientEventCounters = new Map<string, number>();
function nextEventId(agentId: string): number {
const current = clientEventCounters.get(agentId) ?? 0;
const next = current + 1;
clientEventCounters.set(agentId, next);
return next;
}
/**
* 获取下一个 event_id(不递增,预览用)
*/
export function peekNextEventId(agentId: string): number {
return (clientEventCounters.get(agentId) ?? 0) + 1;
}
/**
* 注册 Agent 的 SSE 连接
*/
export function registerClient(agentId: string, res: Response): void {
// 如果已有旧连接,先关掉(Agent 重启场景)
const existing = clients.get(agentId);
if (existing) {
try { existing.end(); } catch (_) {}
}
clients.set(agentId, res);
// 重置 event counter
clientEventCounters.set(agentId, 0);
logger.info("sse_client_connected", { module: "sse", agent_id: agentId, total: clients.size });
}
/**
* 移除 Agent 连接(断线时调用)
*/
export function removeClient(agentId: string): void {
clients.delete(agentId);
clientEventCounters.delete(agentId);
logger.info("sse_client_disconnected", { module: "sse", agent_id: agentId, total: clients.size });
}
/**
* 向指定 Agent 推送事件
*
* 每个推送附加递增的 event_id,客户端可据此实现去重:
* - event_id 是严格递增的
* - 客户端保存 last_seen_event_id,忽略 ≤ last_seen 的消息
*
* @param agentId 目标 Agent
* @param event 事件数据(会被序列化为 JSON)
* @param dedupId 可选的去重标识(如 msg_hash),附加到事件中供客户端验证
* @returns true = 在线已推送;false = 离线,消息已持久化等待补发
*/
export function pushToAgent(agentId: string, event: object, dedupId?: string): boolean {
const res = clients.get(agentId);
if (!res) return false;
try {
const eventId = nextEventId(agentId);
const payload = {
...event,
_hub_event_id: eventId,
...(dedupId ? { _hub_dedup_id: dedupId } : {}),
};
// SSE 事件格式:id + event + data
res.write(`id: ${eventId}\n`);
res.write(`event: message\n`);
res.write(`data: ${JSON.stringify(payload)}\n\n`);
return true;
} catch (err) {
// 连接异常,移除
removeClient(agentId);
return false;
}
}
/**
* 广播给多个 Agent
*/
export function broadcast(agentIds: string[], event: object): Record<string, boolean> {
const results: Record<string, boolean> = {};
for (const id of agentIds) {
results[id] = pushToAgent(id, event);
}
return results;
}
/**
* 查询哪些 Agent 在线
*/
export function onlineAgents(): string[] {
return [...clients.keys()];
}
/**
* Phase 5b: 优雅关闭时 drain 所有 SSE 连接
* 向每个客户端发送 close 事件后关闭连接
*/
export function drainAllClients(): void {
for (const [agentId, res] of clients.entries()) {
try {
const eventId = nextEventId(agentId);
res.write(`id: ${eventId}\n`);
res.write(`event: hub_shutdown\n`);
res.write(`data: {"message":"Server shutting down"}\n\n`);
res.end();
} catch {}
}
clients.clear();
clientEventCounters.clear();
}
+1
View File
@@ -0,0 +1 @@
export {};

Some files were not shown because too many files have changed in this diff Show More