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,213 @@
"""K1.3 端点测试Kimi Code OAuth start/disconnect/status内存替身无 DB/无网络)。
覆盖:
- POST .../start → 202 + user_code/verification_uri + 创建 job后台轮询 poll_token 成功
→ 存加密 oauth_enc + job done**token 不进 job 结果/响应**
- 后台轮询 authorization_pending → 继续轮询直到成功;
- POST .../disconnect → 清除凭据行;
- GET .../status → connected 真值(无 token 本体)。
后台 work 用 `run_job` 自建独立 session请求 session 已关闭)——测试经依赖 override
注 fake http + fake store共享实例断后台落库+ FakeSessionFactorymonkeypatch
`kimi_oauth.asyncio.sleep` 为 no-op避免真等 interval 秒)。
"""
from __future__ import annotations
import os
from typing import Any
import pytest
from cryptography.fernet import Fernet
from fakes_providers import FakeCredentialStore
from fastapi.testclient import TestClient
from ww_api.services.credentials import STUB_OWNER_ID
from ww_api.services.kimi_oauth import decrypt_oauth_bundle
from ww_db import get_session
class _FakeResponse:
def __init__(self, status_code: int, body: dict[str, Any]) -> None:
self.status_code = status_code
self._body = body
def json(self) -> Any:
return self._body
class _ScriptedHttp:
"""按调用序号返回预设响应device auth → poll attempts。aclose 无操作。"""
def __init__(self, responses: list[_FakeResponse]) -> None:
self._responses = responses
self.calls: list[tuple[str, dict[str, str]]] = []
async def post(self, url: str, *, data: dict[str, str]) -> _FakeResponse:
idx = len(self.calls)
self.calls.append((url, data))
return self._responses[idx]
async def aclose(self) -> None:
return None
def _make_client(
*,
store: FakeCredentialStore,
http: _ScriptedHttp,
enc_key: str,
monkeypatch: pytest.MonkeyPatch,
) -> TestClient:
os.environ["CREDENTIAL_ENC_KEY"] = enc_key
from ww_config import get_settings
get_settings.cache_clear()
from fakes_projects import FakeJobRepo, FakeSession, FakeSessionFactory
from ww_api import routers
from ww_api.main import create_app
from ww_api.services import job_runner
from ww_api.services.project_deps import (
get_job_repo,
get_session_factory,
)
from ww_api.services.provider_deps import get_credential_store
# 后台轮询不真等 interval 秒。
async def _no_sleep(_seconds: float) -> None:
return None
monkeypatch.setattr("ww_api.routers.kimi_oauth.asyncio.sleep", _no_sleep)
# 后台 work 自建 SqlCredentialStore(session)——指到共享 fake store断后台落库
monkeypatch.setattr(routers.kimi_oauth, "SqlCredentialStore", lambda _session: store)
# 后台 work 内调模块级 `_default_http_client()` 自建 http非 dep——monkeypatch 它指到
# scripted fake否则后台轮询会真联网见 device-flow 注入纪律)。
monkeypatch.setattr(routers.kimi_oauth, "_default_http_client", lambda: http)
# `run_job` 默认 repo_factory 构 `SqlJobRepo(session)`——FakeSession 无 .execute
# 故 monkeypatch 模块级 `job_runner.SqlJobRepo` 指到共享 fake见 memory/gotchas
job_repo = FakeJobRepo()
monkeypatch.setattr(job_runner, "SqlJobRepo", lambda _session: job_repo)
app = create_app()
app.dependency_overrides[get_credential_store] = lambda: store
app.dependency_overrides[get_job_repo] = lambda: job_repo
app.dependency_overrides[get_session] = lambda: FakeSession()
app.dependency_overrides[get_session_factory] = lambda: FakeSessionFactory()
app.dependency_overrides[routers.kimi_oauth._default_http_client] = lambda: http
return TestClient(app)
def _device_auth_response() -> _FakeResponse:
return _FakeResponse(
200,
{
"device_code": "dev-1",
"user_code": "WXYZ-9999",
"verification_uri": "https://kimi.com/device",
"verification_uri_complete": "https://kimi.com/device?code=WXYZ-9999",
"expires_in": 600,
"interval": 5,
},
)
def _token_response() -> _FakeResponse:
return _FakeResponse(
200, {"access_token": "secret-acc", "refresh_token": "secret-ref", "expires_in": 900}
)
def test_start_returns_202_and_polls_to_success(monkeypatch: pytest.MonkeyPatch) -> None:
enc_key = Fernet.generate_key().decode()
store = FakeCredentialStore()
# device auth → poll #1 successTestClient 同步跑完 background task
http = _ScriptedHttp([_device_auth_response(), _token_response()])
client = _make_client(store=store, http=http, enc_key=enc_key, monkeypatch=monkeypatch)
resp = client.post("/settings/providers/kimi-code/oauth/start")
assert resp.status_code == 202
body = resp.json()
assert body["user_code"] == "WXYZ-9999"
assert body["verification_uri"] == "https://kimi.com/device"
assert body["interval"] == 5
assert "job_id" in body
# 响应绝不含 token。
assert "secret-acc" not in resp.text
assert "secret-ref" not in resp.text
# 后台轮询成功 → 加密 oauth 凭据落库(共享 store
cred = store.oauth.get((STUB_OWNER_ID, "kimi-code"))
assert cred is not None
token = decrypt_oauth_bundle(cred, key=enc_key)
assert token.access_token == "secret-acc"
assert token.refresh_token == "secret-ref"
def test_start_polls_through_authorization_pending(monkeypatch: pytest.MonkeyPatch) -> None:
enc_key = Fernet.generate_key().decode()
store = FakeCredentialStore()
http = _ScriptedHttp(
[
_device_auth_response(),
_FakeResponse(400, {"error": "authorization_pending"}),
_token_response(),
]
)
client = _make_client(store=store, http=http, enc_key=enc_key, monkeypatch=monkeypatch)
resp = client.post("/settings/providers/kimi-code/oauth/start")
assert resp.status_code == 202
# pending 后继续轮询 → 第二次成功落库。
assert (STUB_OWNER_ID, "kimi-code") in store.oauth
def test_disconnect_clears_credential(monkeypatch: pytest.MonkeyPatch) -> None:
enc_key = Fernet.generate_key().decode()
store = FakeCredentialStore()
store.oauth[(STUB_OWNER_ID, "kimi-code")] = b"blob"
http = _ScriptedHttp([])
client = _make_client(store=store, http=http, enc_key=enc_key, monkeypatch=monkeypatch)
resp = client.post("/settings/providers/kimi-code/oauth/disconnect")
assert resp.status_code == 200
assert resp.json()["disconnected"] is True
assert (STUB_OWNER_ID, "kimi-code") not in store.oauth
def test_status_reflects_connection(monkeypatch: pytest.MonkeyPatch) -> None:
from datetime import UTC, datetime, timedelta
from ww_api.services.kimi_oauth import TokenSet, encrypt_oauth_bundle
enc_key = Fernet.generate_key().decode()
store = FakeCredentialStore()
expires = datetime.now(UTC) + timedelta(seconds=900)
token = TokenSet(access_token="a", refresh_token="r", expires_at=expires)
store.oauth[(STUB_OWNER_ID, "kimi-code")] = encrypt_oauth_bundle(token, key=enc_key)
http = _ScriptedHttp([])
client = _make_client(store=store, http=http, enc_key=enc_key, monkeypatch=monkeypatch)
resp = client.get("/settings/providers/kimi-code/oauth/status")
assert resp.status_code == 200
body = resp.json()
assert body["connected"] is True
assert body["expires_at"] is not None
# 状态绝不回 token 本体。
assert "access_token" not in body
assert "refresh_token" not in body
def test_status_not_connected_when_no_credential(monkeypatch: pytest.MonkeyPatch) -> None:
enc_key = Fernet.generate_key().decode()
store = FakeCredentialStore()
http = _ScriptedHttp([])
client = _make_client(store=store, http=http, enc_key=enc_key, monkeypatch=monkeypatch)
resp = client.get("/settings/providers/kimi-code/oauth/status")
assert resp.status_code == 200
assert resp.json()["connected"] is False