跳到正文

12 - 控制面设计:开源实现与模块拆解 ​

一句话:控制面只做编排与记账、不含任何 Agent 逻辑,API 层收管理请求,引擎层跑生命周期、分发、配额与闲置检测,状态落在 PostgreSQL 的 desired/observed 与乐观锁 revision 上;这让实例侧保持无感,代价是它成为唯一有权写实例配置的角色,自己宕机时用户对话仍能继续但暂存、分发、审批回传全部停摆,且每个接口都与 QwenPaw 原生 API 的演进强耦合。

一、设计目标 ​

产品控制面是整个系统的"大脑",负责:

  • 实例全生命周期管理(创建、暂存、恢复、回收)
  • 密钥治理(RT 生成/轮换/吊销)
  • Skill/MCP 分发编排
  • 配额管理与计量
  • 产物取回与交付
  • 审计日志

控制面代码随本方案一起开源,采用 Python FastAPI 框架,与 QwenPaw 同生态。

二、模块架构 ​

三、核心数据模型 ​

3.1 实例注册表 ​

sql
CREATE TABLE instance_registry (
    id              BIGSERIAL PRIMARY KEY,
    instance_id     VARCHAR(64) UNIQUE NOT NULL,
    user_id         VARCHAR(64) UNIQUE NOT NULL,
    tenant_id       VARCHAR(64) NOT NULL,
    
    -- 状态机
    desired_state   VARCHAR(20) NOT NULL DEFAULT 'active',
    observed_state  VARCHAR(20) NOT NULL DEFAULT 'pending',
    revision        INTEGER NOT NULL DEFAULT 1,
    
    -- 资源引用
    keys_ref        VARCHAR(256),
    snapshot_ref    VARCHAR(512),
    image_ref       VARCHAR(256) NOT NULL,
    
    -- 时间戳
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    last_active_at  TIMESTAMPTZ,
    retired_at      TIMESTAMPTZ,
    
    -- 元数据
    metadata        JSONB DEFAULT '{}',
    last_error      TEXT,
    
    CONSTRAINT chk_state CHECK (desired_state IN ('active', 'staged', 'retired')),
    CONSTRAINT chk_observed CHECK (observed_state IN ('pending', 'active', 'starting', 'staged', 'staging', 'restoring', 'retired', 'failed'))
);

CREATE INDEX idx_registry_user ON instance_registry(user_id);
CREATE INDEX idx_registry_state ON instance_registry(observed_state);
CREATE INDEX idx_registry_tenant ON instance_registry(tenant_id, observed_state);

3.2 凭据 Vault ​

sql
CREATE TABLE credentials (
    id              BIGSERIAL PRIMARY KEY,
    instance_id     VARCHAR(64) NOT NULL REFERENCES instance_registry(instance_id),
    credential_type VARCHAR(50) NOT NULL,  -- runtime_token / model_key / mcp_credential
    encrypted_value BYTEA NOT NULL,         -- Fernet 加密
    revision        INTEGER NOT NULL DEFAULT 1,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    expires_at      TIMESTAMPTZ,
    
    UNIQUE(instance_id, credential_type)
);

CREATE TABLE credential_grace_period (
    id              BIGSERIAL PRIMARY KEY,
    instance_id     VARCHAR(64) NOT NULL,
    credential_type VARCHAR(50) NOT NULL,
    old_value_hash  VARCHAR(128) NOT NULL,  -- 旧值哈希(不存明文)
    expires_at      TIMESTAMPTZ NOT NULL    -- 24h 后过期
);

CREATE INDEX idx_cred_instance ON credentials(instance_id);

3.3 任务注册表 ​

sql
CREATE TABLE task_registry (
    task_id         VARCHAR(64) PRIMARY KEY,
    user_id         VARCHAR(64) NOT NULL,
    instance_id     VARCHAR(64) NOT NULL,
    session_id      VARCHAR(128) NOT NULL,
    
    input           JSONB NOT NULL,
    request_context JSONB DEFAULT '{}',
    
    status          VARCHAR(20) NOT NULL DEFAULT 'submitted',
    artifacts       JSONB DEFAULT '[]',
    artifact_count  INTEGER DEFAULT 0,
    total_tokens    INTEGER DEFAULT 0,
    
    submitted_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    accepted_at     TIMESTAMPTZ,
    completed_at    TIMESTAMPTZ,
    
    trace_id        VARCHAR(64),
    source          VARCHAR(20),
    error_code      VARCHAR(50),
    error_message   TEXT
);

CREATE INDEX idx_task_user ON task_registry(user_id, submitted_at DESC);
CREATE INDEX idx_task_status ON task_registry(status) WHERE status NOT IN ('completed', 'failed', 'timeout', 'rejected');

3.4 审计日志 ​

sql
CREATE TABLE audit_log (
    id              BIGSERIAL PRIMARY KEY,
    trace_id        VARCHAR(64),
    timestamp       TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    event_type      VARCHAR(50) NOT NULL,
    source          VARCHAR(20) NOT NULL,  -- gateway / control / instance
    
    user_id         VARCHAR(64),
    instance_id     VARCHAR(64),
    session_id      VARCHAR(128),
    task_id         VARCHAR(64),
    
    action          VARCHAR(100),
    target          VARCHAR(256),
    result          VARCHAR(20),
    details         JSONB DEFAULT '{}',
    
    source_ip       INET,
    user_agent      TEXT
);

CREATE INDEX idx_audit_time ON audit_log(timestamp DESC);
CREATE INDEX idx_audit_user ON audit_log(user_id, timestamp DESC);
CREATE INDEX idx_audit_trace ON audit_log(trace_id);
CREATE INDEX idx_audit_event ON audit_log(event_type, timestamp DESC);

-- 分区表(按月分区,便于归档清理)
-- CREATE TABLE audit_log_2026_10 PARTITION OF audit_log
--     FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');

四、核心引擎实现 ​

4.1 生命周期引擎 ​

python
class LifecycleEngine:
    """实例生命周期管理"""
    
    def __init__(self, k8s_client, registry, vault, nacos_client, minio_client):
        self.k8s = k8s_client
        self.registry = registry
        self.vault = vault
        self.nacos = nacos_client
        self.minio = minio_client
    
    async def create_instance(self, user_id: str, tenant_id: str, tier: str) -> InstanceRecord:
        """创建实例"""
        instance_id = f"inst_{uuid4().hex[:12]}"
        
        # 1. 生成密钥
        rt = self.vault.get_or_create_runtime_token(instance_id)
        
        # 2. 创建 K8s 资源
        await self.k8s.create_stateful_set(
            name=f"qwenpaw-{user_id}",
            namespace="qwenpaw-instances",
            image=IMAGE_REF,
            user_id=user_id,
            instance_id=instance_id,
            runtime_token=rt,
            tier=tier,
        )
        
        # 3. 等待就绪
        await self._wait_for_ready(instance_id, timeout=120)
        
        # 4. 注册
        await self.nacos.register(instance_id, user_id)
        await self.registry.create(instance_id, user_id, tenant_id, state="active")
        
        return await self.registry.get(instance_id)
    
    async def stage_instance(self, instance_id: str, reason: str):
        """暂存实例"""
        inst = await self.registry.get(instance_id)
        
        # 1. 备份
        backup_id = await self._create_backup(inst)
        snapshot_ref = f"backups/{inst.user_id}/{backup_id}.tar.gz"
        
        # 2. 停止 Pod
        await self.k8s.scale_stateful_set(inst.instance_id, replicas=0)
        
        # 3. Nacos 注销
        await self.nacos.deregister(instance_id)
        
        # 4. 更新状态
        await self.registry.update(instance_id, {
            "observed_state": "staged",
            "snapshot_ref": snapshot_ref,
        })
    
    async def restore_instance(self, instance_id: str):
        """恢复实例"""
        inst = await self.registry.get(instance_id)
        
        # 1. 重建 Pod
        await self.k8s.scale_stateful_set(inst.instance_id, replicas=1)
        
        # 2. 等待就绪
        await self._wait_for_ready(instance_id, timeout=120)
        
        # 3. 恢复数据
        if inst.snapshot_ref:
            await self._restore_backup(inst, inst.snapshot_ref)
        
        # 4. Nacos 注册
        await self.nacos.register(instance_id, inst.user_id)
        
        # 5. 更新状态
        await self.registry.update(instance_id, {
            "observed_state": "active",
        })
    
    async def retire_instance(self, instance_id: str):
        """回收实例"""
        inst = await self.registry.get(instance_id)
        
        # 1. 交接归档
        handover_ref = await self._export_handover(inst)
        
        # 2. 吊销密钥
        await self.vault.revoke(instance_id)
        
        # 3. 删除 K8s 资源
        await self.k8s.delete_stateful_set(inst.instance_id)
        await self.k8s.delete_pvc(f"qwenpaw-{inst.user_id}-data")
        await self.k8s.delete_pvc(f"qwenpaw-{inst.user_id}-secrets")
        await self.k8s.delete_pvc(f"qwenpaw-{inst.user_id}-backups")
        
        # 4. Nacos 注销
        await self.nacos.deregister(instance_id)
        
        # 5. 更新状态
        await self.registry.update(instance_id, {
            "observed_state": "retired",
            "retired_at": datetime.utcnow(),
            "snapshot_ref": handover_ref,
        })

4.2 分发编排引擎 ​

python
class DistributionEngine:
    """Skill/MCP 分发编排"""
    
    async def distribute_skills(self, user_id: str, skill_slugs: list[str]):
        """向实例分发 Skill"""
        inst = await self.registry.get_by_user(user_id)
        rt = await self.vault.get_runtime_token(inst.instance_id)
        
        results = []
        for slug in skill_slugs:
            try:
                # 1. 从市场获取 Skill 包
                skill_info = await self.market.get_skill(slug)
                
                # 2. 安装到实例
                resp = await httpx.post(
                    f"http://{inst.endpoint}/api/skills/pool/import",
                    headers={"x-qwenpaw-runtime-token": rt},
                    json={"bundle_url": skill_info["bundle_url"], "provider_key": "enterprise"},
                    timeout=30.0,
                )
                
                if resp.status_code == 200:
                    # 3. 启用
                    await httpx.post(
                        f"http://{inst.endpoint}/api/skills/{slug}/enable",
                        headers={"x-qwenpaw-runtime-token": rt},
                        timeout=10.0,
                    )
                    results.append({"slug": slug, "status": "installed"})
                else:
                    results.append({"slug": slug, "status": "failed", "error": resp.text})
                    
            except Exception as e:
                results.append({"slug": slug, "status": "error", "error": str(e)})
        
        # 4. 记录审计
        await self.audit.record("skill_distribution", {
            "user_id": user_id,
            "skills": skill_slugs,
            "results": results,
        })
        
        return results
    
    async def distribute_mcp_clients(self, user_id: str, mcp_configs: list[dict]):
        """向实例分发 MCP Client"""
        inst = await self.registry.get_by_user(user_id)
        rt = await self.vault.get_runtime_token(inst.instance_id)
        
        results = []
        for config in mcp_configs:
            try:
                # 1. 从 Vault 获取 MCP 凭证
                credentials = await self.vault.get_mcp_credentials(config["credential_ref"])
                
                # 2. 创建 MCP Client
                headers = config.get("headers", {})
                if credentials:
                    headers["Authorization"] = f"Bearer {credentials['token']}"
                
                resp = await httpx.post(
                    f"http://{inst.endpoint}/api/mcp",
                    headers={"x-qwenpaw-runtime-token": rt},
                    json={
                        "transport": config["transport"],
                        "url": config.get("url"),
                        "headers": headers,
                        "command": config.get("command"),
                    },
                    timeout=30.0,
                )
                
                if resp.status_code == 200:
                    client_id = resp.json()["client_id"]
                    
                    # 3. 设置工具白名单
                    if config.get("tools_whitelist"):
                        await httpx.put(
                            f"http://{inst.endpoint}/api/mcp/tools/{client_id}",
                            headers={"x-qwenpaw-runtime-token": rt},
                            json={"tools": config["tools_whitelist"]},
                            timeout=10.0,
                        )
                    
                    results.append({"name": config["name"], "status": "created", "client_id": client_id})
                else:
                    results.append({"name": config["name"], "status": "failed", "error": resp.text})
                    
            except Exception as e:
                results.append({"name": config["name"], "status": "error", "error": str(e)})
        
        return results

4.3 配额引擎 ​

python
class QuotaEngine:
    """配额管理与计量"""
    
    TIER_LIMITS = {
        "basic":    {"qps": 10,  "daily_tokens": 100_000,  "monthly_cost": 100},
        "standard": {"qps": 30,  "daily_tokens": 500_000,  "monthly_cost": 500},
        "enhanced": {"qps": 100, "daily_tokens": 2_000_000, "monthly_cost": 2000},
    }
    
    async def check_quota(self, user_id: str, token_count: int) -> tuple[bool, str]:
        """检查配额"""
        user = await self.user_store.get(user_id)
        tier = user.get("tier", "basic")
        limits = self.TIER_LIMITS[tier]
        
        # 日配额
        daily_key = f"quota:daily:{user_id}:{date.today()}"
        used = await self.redis.get(daily_key)
        if used and int(used) + token_count > limits["daily_tokens"]:
            return False, f"daily_quota_exceeded ({used}/{limits['daily_tokens']})"
        
        # 月配额
        monthly_key = f"quota:monthly:{user_id}:{date.today().month}"
        used = await self.redis.get(monthly_key)
        if used and int(used) + token_count > limits["monthly_cost"]:
            return False, f"monthly_quota_exceeded ({used}/{limits['monthly_cost']})"
        
        return True, "ok"
    
    async def record_usage(self, user_id: str, token_count: int, cost: float):
        """记录用量"""
        today = date.today()
        month = today.month
        
        pipe = self.redis.pipeline()
        pipe.incrby(f"quota:daily:{user_id}:{today}", token_count)
        pipe.incrby(f"quota:monthly:{user_id}:{month}", int(cost * 100))
        pipe.expire(f"quota:daily:{user_id}:{today}", 86400 * 2)
        await pipe.execute()
        
        # 告警检查
        user = await self.user_store.get(user_id)
        tier = user.get("tier", "basic")
        limits = self.TIER_LIMITS[tier]
        
        daily_used = await self.redis.get(f"quota:daily:{user_id}:{today}")
        if daily_used and int(daily_used) > limits["daily_tokens"] * 0.8:
            await self.alert.send("quota_warning", {
                "user_id": user_id,
                "tier": tier,
                "used": int(daily_used),
                "limit": limits["daily_tokens"],
            })

五、API 端点设计 ​

5.1 实例管理 API ​

端点方法说明权限
/api/instancesGET列出实例(支持分页/过滤)admin
/api/instancesPOST创建实例admin
/api/instances/{id}GET获取实例详情admin / owner
/api/instances/{id}/stagePOST暂存实例admin
/api/instances/{id}/restorePOST恢复实例admin
/api/instances/{id}/retirePOST回收实例admin
/api/instances/{id}/restartPOST重启实例admin / owner
/api/instances/{id}/upgradePOST升级镜像admin

5.2 用户管理 API ​

端点方法说明权限
/api/usersGET列出用户admin
/api/users/{id}GET获取用户详情admin / owner
/api/users/{id}/tierPUT更新订阅档位admin
/api/users/{id}/quotaGET查询配额使用情况admin / owner

5.3 分发管理 API ​

端点方法说明权限
/api/distribution/skillsPOST批量分发 Skilladmin
/api/distribution/mcpPOST批量分发 MCP Clientadmin
/api/distribution/statusGET查询分发状态admin

六、开源仓库结构 ​

qwenpaw-enterprise/
├── README.md
├── pyproject.toml
├── Dockerfile
├── docker-compose.yaml          # 本地开发环境
├── k8s/                          # K8s 部署清单
│   ├── namespace.yaml
│   ├── control-plane/
│   │   ├── deployment.yaml
│   │   ├── service.yaml
│   │   └── configmap.yaml
│   ├── gateway/
│   │   ├── higress-config.yaml
│   │   └── ext-authz-deployment.yaml
│   ├── instances/
│   │   ├── statefulset-template.yaml
│   │   ├── pvc-template.yaml
│   │   └── network-policy.yaml
│   └── infra/
│       ├── postgres.yaml
│       ├── redis.yaml
│       ├── nacos.yaml
│       └── minio.yaml
├── src/
│   └── qwenpaw_enterprise/
│       ├── __init__.py
│       ├── main.py               # FastAPI 入口
│       ├── api/                  # API 路由
│       │   ├── instances.py
│       │   ├── users.py
│       │   ├── distribution.py
│       │   ├── quotas.py
│       │   └── audit.py
│       ├── engine/               # 核心引擎
│       │   ├── lifecycle.py
│       │   ├── distribution.py
│       │   ├── quota.py
│       │   ├── image.py
│       │   └── idle_detector.py
│       ├── store/                # 数据存储
│       │   ├── registry.py
│       │   ├── vault.py
│       │   ├── task_store.py
│       │   └── audit_store.py
│       ├── infra/                # 基础设施客户端
│       │   ├── k8s_client.py
│       │   ├── nacos_client.py
│       │   ├── minio_client.py
│       │   └── redis_client.py
│       └── models/               # 数据模型
│           ├── instance.py
│           ├── user.py
│           ├── task.py
│           └── audit.py
├── tests/
│   ├── test_lifecycle.py
│   ├── test_distribution.py
│   └── test_quota.py
└── docs/
    ├── deployment.md
    ├── api-reference.md
    └── architecture.md

把 AI 工程讲到既懂也会用