Files
writer-work-flow/packages/skills/ww_skills/skill_registry.py
Yaojia Wang 765dbdfbd4 feat: M4 文风 + M5 生成/多provider/Skill + Kimi Code 订阅接入 + 本地联调修复
M4(文风): style-auditor 双轨(提取指纹/漂移第四审)+ jobs 长任务框架(zombie reaper) + 回炉 refine + GET /style read-back。
M5(生成+扩展): worldbuilder/character-gen(入库 continuity 409 gate + partition_writes 白名单 + schema→JSONB 形变);
  网关多 provider 回退链/熔断/能力降级(Anthropic/Gemini 适配器);Skill registry + 表权限沙箱 + 规则;
  前端 角色生成器/世界观/Codex/规则页/技能库/⌘K 命令面板。
K1(Kimi Code 订阅接入): OAuth device-flow(kimi-code)+ 静态 Console key(kimi-code-key)两路径;
  coding 端点 KimiCLI 伪造头(实测 UA allow-list 门禁,缺则 403)+ JSON 模式结构化(thinking ⊥ tool_choice)。
本地联调修复: CORS 中间件;assemble 注入 premise+「写第N章」指令(修空 prompt 400);
  GET /outline·/draft read-back + 大纲/工作台/审稿页重载;写页 client/server 常量边界 + notFound 健壮化;
  字数 toLocaleString locale 水合;审稿页终稿从已存草稿 seed(修 accept 422)。
门禁: backend ruff/mypy(157)/alembic 无漂移/pytest 451 · frontend lint/tsc/vitest/build。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 10:39:58 +02:00

132 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Skill registry loaderARCH §5.6 加载段 / PRODUCT_SPEC §5.5)。
从 `SkillRepo``skills` 表的读侧抽象)加载声明式 skill 行 → 转 frozen `AgentSpec`
(内置 builtin / 用户 custom / 社区 community 同构)。加载后按 `tier` 经 LLM 网关执行
(复用 §5.1 的 agent 机制,本模块**不**重实现网关)。
加载时对每条声明跑 `validate_declaration`越权reads/writes 指向未知表)的 skill
直接拒绝(不入册),守 §5.6 表权限契约(不可信用户输入的第一道闸)。
不变量 #2行里只带 `tier`writer/analyst/lightregistry 不解析具体 model
model 解析在网关。registry 加载后不可变specs 字典只读)。
"""
from __future__ import annotations
from typing import Protocol, cast, get_args
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from ww_agents import AgentSpec
from ww_db.models import Skill
from ww_llm_gateway.types import Tier
from ww_shared import AppError, ErrorCode
from ww_skills.skill_permissions import validate_declaration
_VALID_TIERS: frozenset[str] = frozenset(get_args(Tier))
class SkillRecord(BaseModel):
"""`skills` 表一行的声明式快照frozensnake_case
`input_schema`/`output_schema` 在 DB 里是 JSON Schema dict用户自定义无 Python 类型),
registry 暂以纯声明prompt + 表权限 + tier执行结构化 schema 的运行期绑定属后续,
故这里不携带 type[BaseModel](内置 8 agent 仍用各自硬编码的 spec见 ww_agents
"""
model_config = {"frozen": True}
name: str
scope: str # builtin / custom / community
tier: Tier
system_prompt: str
reads: list[str] = []
writes: list[str] = []
genre: str | None = None
def _to_spec(record: SkillRecord) -> AgentSpec:
return AgentSpec(
name=record.name,
tier=record.tier,
system_prompt=record.system_prompt,
input_schema=None,
output_schema=None,
reads=list(record.reads),
writes=list(record.writes),
genre=record.genre,
scope=record.scope,
)
class SkillRepo(Protocol):
"""`skills` 表读侧接口registry 加载源;测试注内存 fake"""
async def list_all(self) -> list[SkillRecord]: ...
class SkillRegistry:
"""加载后的只读 skill 注册表name → AgentSpec"""
def __init__(self, specs: dict[str, AgentSpec]) -> None:
self._specs = dict(specs)
@classmethod
async def load(cls, repo: SkillRepo) -> SkillRegistry:
"""从 repo 拉全部声明 → 校验表权限 → 建 frozen AgentSpec 字典。
越权声明reads/writes 指向未知表)→ AppError(VALIDATION),加载中断(不入册)。
"""
specs: dict[str, AgentSpec] = {}
for record in await repo.list_all():
spec = _to_spec(record)
validate_declaration(spec) # 越权 → 抛 VALIDATION
specs[spec.name] = spec
return cls(specs)
def get(self, name: str) -> AgentSpec:
"""按名取 spec不存在 → AppError(NOT_FOUND)。"""
spec = self._specs.get(name)
if spec is None:
raise AppError(ErrorCode.NOT_FOUND, f"skill not found: {name}")
return spec
def names(self) -> list[str]:
return sorted(self._specs)
def list_scope(self, scope: str) -> list[AgentSpec]:
"""按 scopebuiltin/custom/community过滤按 name 排序。"""
return [s for s in sorted(self._specs.values(), key=lambda s: s.name) if s.scope == scope]
def _row_to_record(row: Skill) -> SkillRecord:
# DB `tier` 是裸 str非法档位 → VALIDATION不可信用户输入第一道闸
if row.tier not in _VALID_TIERS:
raise AppError(
ErrorCode.VALIDATION,
f"Skill「{row.name}」声明了非法档位:{row.tier}",
{"skill": row.name, "tier": row.tier},
)
return SkillRecord(
name=row.name,
scope=row.scope,
tier=cast(Tier, row.tier),
system_prompt=row.system_prompt,
reads=[str(t) for t in (row.reads or [])],
writes=[str(t) for t in (row.writes or [])],
genre=row.genre,
)
class SqlSkillRepo:
"""`skills` 表读侧 SQLAlchemy 实现registry 加载源;只读)。"""
def __init__(self, session: AsyncSession) -> None:
self._s = session
async def list_all(self) -> list[SkillRecord]:
rows = (await self._s.execute(select(Skill))).scalars().all()
return [_row_to_record(r) for r in rows]