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,101 @@
// Kimi Code OAuth device-flow 连接态纯逻辑C3 扩 K1.3K1.4 前端)。
// 把「连接状态查询」「device 启动响应」「job 轮询结果」收窄/归一成一个
// 可渲染的连接阶段connect phase组件只读结果、不含分支逻辑。
// 纯函数 + node-env 单测token 永不出现job 结果只 {connected, provider})。
import type { JobView, PollState } from "@/lib/jobs/job";
import type {
OAuthStartResponse,
OAuthStatusResponse,
} from "@/lib/api/types";
// Kimi Code 是 OAuth 订阅 plan 提供商(与 API-key provider `kimi` 分离)。
export const KIMI_CODE_PROVIDER = "kimi-code";
export const KIMI_CODE_MODEL = "kimi-for-coding";
// 连接流阶段:
// - idle未发起且未连接展示「连接」按钮
// - connected状态查询/job 完成显示已连接(展示「断开」)。
// - awaiting已拿 device code正在等待用户在浏览器授权 + 轮询 job。
// - errordevice 启动失败 / 轮询失败 / 过期 / 拒绝(展示错误 + 允许重试)。
export type ConnectPhase = "idle" | "awaiting" | "connected" | "error";
// device 启动后用户面要展示的信息(无 token
export interface DeviceDisplay {
userCode: string;
verificationUri: string;
verificationUriComplete: string | null;
expiresIn: number;
interval: number;
}
// 把 OAuthStartResponse 收窄成展示用 DeviceDisplay缺字段给安全默认
export function toDeviceDisplay(res: OAuthStartResponse): DeviceDisplay {
return {
userCode: res.user_code,
verificationUri: res.verification_uri,
verificationUriComplete: res.verification_uri_complete ?? null,
expiresIn: typeof res.expires_in === "number" ? res.expires_in : 0,
interval: typeof res.interval === "number" ? res.interval : 5,
};
}
// 优先打开的授权 URL有 complete带 user_code 预填)就用它,否则裸 verification_uri。
export function authOpenUrl(device: DeviceDisplay): string {
return device.verificationUriComplete ?? device.verificationUri;
}
// 连接状态GET .../oauth/status收窄。
export interface ConnectionStatus {
connected: boolean;
expiresAt: string | null;
}
export function toConnectionStatus(
res: OAuthStatusResponse,
): ConnectionStatus {
return {
connected: res.connected === true,
expiresAt: typeof res.expires_at === "string" ? res.expires_at : null,
};
}
// kimi_oauth job 完成态的 result{connected, provider}**绝无 token**)。
export function jobConnected(job: JobView): boolean {
return job.result?.["connected"] === true;
}
// 把「是否已发起连接 + 轮询状态」映射成连接阶段。
// - 未发起started=falseconnected ? connected : idle。
// - 已发起done 且 job.connected → connectederror → error否则 awaiting。
export function connectPhase(args: {
started: boolean;
connected: boolean;
poll: PollState;
}): ConnectPhase {
const { started, connected, poll } = args;
if (!started) {
return connected ? "connected" : "idle";
}
if (poll.status === "done") {
return poll.job !== null && jobConnected(poll.job) ? "connected" : "error";
}
if (poll.status === "error") {
return "error";
}
return "awaiting";
}
// 把 ISO8601 过期时刻格式化成可读文案(无效/缺省→null
export function formatExpiresAt(iso: string | null): string | null {
if (!iso) return null;
const ms = Date.parse(iso);
if (Number.isNaN(ms)) return null;
return new Date(ms).toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
}