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>
This commit is contained in:
Yaojia Wang
2026-06-20 10:39:58 +02:00
parent 5fb7bfb1de
commit 765dbdfbd4
161 changed files with 17330 additions and 208 deletions

View File

@@ -0,0 +1,119 @@
"""T5.4 韧性测试替身(多 provider 适配器 + 失败模拟 + 记账嗅探)——绝不联网。
放独立模块(非 conftest测试走绝对导入 `from fakes_resilience import ...`。
本目录无 __init__.py见 fakes.py 注释)。
"""
from __future__ import annotations
from collections.abc import AsyncIterator, Callable
from ww_llm_gateway.adapters.base import (
Capabilities,
ProviderResult,
ProviderUsage,
StreamChunk,
)
from ww_llm_gateway.errors import TransientProviderError
from ww_llm_gateway.routing import Route
from ww_llm_gateway.types import LlmRequest, Scope, Tier, Usage
class ScriptedAdapter:
"""可编排成功/失败序列的假适配器。
`failures` 为开头要抛的异常列表(每次 complete/stream 消费一个);耗尽后正常返回。
`capabilities_` 控制能力矩阵(测降级)。记录调用次数以断言回退/重试行为。
"""
def __init__(
self,
provider: str,
*,
text: str = "ok",
failures: list[Exception] | None = None,
capabilities_: Capabilities | None = None,
input_tokens: int = 100,
output_tokens: int = 50,
structured_text: str | None = None,
) -> None:
self.provider = provider
self.text = text
self._failures = list(failures or [])
self._caps = capabilities_ or Capabilities(structured_output=True, prefix_cache=True)
self.input_tokens = input_tokens
self.output_tokens = output_tokens
self.structured_text = structured_text
self.complete_calls = 0
self.stream_calls = 0
def capabilities(self) -> Capabilities:
return self._caps
def _maybe_fail(self) -> None:
if self._failures:
raise self._failures.pop(0)
async def complete(self, req: LlmRequest, model: str) -> ProviderResult:
self.complete_calls += 1
self._maybe_fail()
if req.output_schema is not None:
# 模拟原生结构化输出:仅当能力声明支持时才会被网关派到这里。
parsed = req.output_schema.model_validate({} if not _has_fields(req) else _stub(req))
return ProviderResult(
text=parsed.model_dump_json(),
usage=ProviderUsage(
input_tokens=self.input_tokens, output_tokens=self.output_tokens
),
parsed=parsed,
)
return ProviderResult(
text=self.structured_text or self.text,
usage=ProviderUsage(input_tokens=self.input_tokens, output_tokens=self.output_tokens),
)
async def stream(self, req: LlmRequest, model: str) -> AsyncIterator[StreamChunk]:
self.stream_calls += 1
self._maybe_fail()
for ch in self.text:
yield StreamChunk(text=ch)
yield StreamChunk(
usage=ProviderUsage(input_tokens=self.input_tokens, output_tokens=self.output_tokens)
)
def _has_fields(req: LlmRequest) -> bool:
return bool(req.output_schema and req.output_schema.model_fields)
def _stub(req: LlmRequest) -> dict[str, object]:
# 用各字段默认/最小填充——测试 schema 仅需可构造。
assert req.output_schema is not None
out: dict[str, object] = {}
for name, field in req.output_schema.model_fields.items():
if field.is_required():
out[name] = "x"
return out
class FakeLedger:
def __init__(self) -> None:
self.records: list[Usage] = []
async def record(self, scope: Scope, usage: Usage) -> None:
self.records.append(usage)
def transient(msg: str = "boom") -> TransientProviderError:
return TransientProviderError(msg)
def chain(*routes: tuple[str, str]) -> list[Route]:
return [Route(provider=p, model=m) for p, m in routes]
def chain_resolver(routes: list[Route]) -> Callable[[Tier], list[Route]]:
def _resolve(tier: Tier) -> list[Route]:
return routes
return _resolve