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,75 @@
import { describe, expect, it } from "vitest";
import {
clampHighlight,
filterCommands,
globalCommands,
moveHighlight,
projectCommands,
} from "./palette";
describe("projectCommands", () => {
it("includes nav + action commands wired to the project id", () => {
const cmds = projectCommands("p1");
const write = cmds.find((c) => c.id === "nav-write");
expect(write?.href).toBe("/projects/p1/write");
const genChar = cmds.find((c) => c.id === "action-gen-character");
expect(genChar?.kind).toBe("action");
expect(genChar?.href).toBe("/projects/p1/codex?gen=character");
});
});
describe("globalCommands", () => {
it("offers home + settings", () => {
expect(globalCommands().map((c) => c.id)).toEqual([
"nav-home",
"nav-settings",
]);
});
});
describe("filterCommands", () => {
const cmds = projectCommands("p1");
it("returns all on empty query", () => {
expect(filterCommands(cmds, " ")).toHaveLength(cmds.length);
});
it("matches on title", () => {
const out = filterCommands(cmds, "审稿");
expect(out.map((c) => c.id)).toContain("nav-review");
});
it("matches on keyword case-insensitively", () => {
const out = filterCommands(cmds, "FORESHADOW");
expect(out.map((c) => c.id)).toContain("nav-foreshadow");
});
it("matches generation actions by 角色/世界观", () => {
expect(filterCommands(cmds, "角色").map((c) => c.id)).toContain(
"action-gen-character",
);
expect(filterCommands(cmds, "世界观").map((c) => c.id)).toContain(
"action-gen-world",
);
});
it("returns empty when nothing matches", () => {
expect(filterCommands(cmds, "zzzz-no-match")).toEqual([]);
});
});
describe("clampHighlight / moveHighlight", () => {
it("wraps within [0, len)", () => {
expect(clampHighlight(0, 3)).toBe(0);
expect(clampHighlight(3, 3)).toBe(0);
expect(clampHighlight(-1, 3)).toBe(2);
expect(clampHighlight(5, 0)).toBe(0);
});
it("moves up/down with wrap", () => {
expect(moveHighlight(0, 1, 3)).toBe(1);
expect(moveHighlight(2, 1, 3)).toBe(0);
expect(moveHighlight(0, -1, 3)).toBe(2);
});
});

View File

@@ -0,0 +1,154 @@
// 命令面板⌘K纯逻辑命令清单组装 + 模糊过滤 + 键盘选择状态机。
// 对齐 UX §7快速导航/动作)。纯逻辑,便于 node 环境单测(不依赖 DOM
export type CommandKind = "navigate" | "action";
export interface Command {
id: string;
// 展示标题(如「写本章」「跳转伏笔」)。
title: string;
// 用于匹配的关键词(拼音/别名),与 title 一起参与过滤。
keywords: string[];
kind: CommandKind;
// navigate跳转路径action交由调用方按 id 分派。
href?: string;
group: string;
}
// 项目内可用命令projectId 已知)。导航项给 href动作项生成角色等给 id。
export function projectCommands(projectId: string): Command[] {
const p = `/projects/${projectId}`;
return [
{
id: "nav-write",
title: "写本章",
keywords: ["write", "写作", "工作台", "起草"],
kind: "navigate",
href: `${p}/write`,
group: "导航",
},
{
id: "nav-review",
title: "审稿",
keywords: ["review", "质检", "冲突", "裁决"],
kind: "navigate",
href: `${p}/review`,
group: "导航",
},
{
id: "nav-outline",
title: "大纲",
keywords: ["outline", "节拍", "beats"],
kind: "navigate",
href: `${p}/outline`,
group: "导航",
},
{
id: "nav-foreshadow",
title: "跳转伏笔",
keywords: ["foreshadow", "伏笔", "看板", "线索"],
kind: "navigate",
href: `${p}/foreshadow`,
group: "导航",
},
{
id: "nav-style",
title: "文风",
keywords: ["style", "文风", "指纹", "漂移"],
kind: "navigate",
href: `${p}/style`,
group: "导航",
},
{
id: "nav-codex",
title: "搜设定",
keywords: ["codex", "设定库", "人物", "世界观", "角色"],
kind: "navigate",
href: `${p}/codex`,
group: "导航",
},
{
id: "nav-rules",
title: "规则",
keywords: ["rules", "规则", "硬规则"],
kind: "navigate",
href: `${p}/rules`,
group: "导航",
},
{
id: "nav-skills",
title: "技能库",
keywords: ["skills", "技能", "注册表"],
kind: "navigate",
href: `${p}/skills`,
group: "导航",
},
{
id: "action-gen-character",
title: "生成角色",
keywords: ["character", "角色", "生成器", "群像"],
kind: "action",
href: `${p}/codex?gen=character`,
group: "动作",
},
{
id: "action-gen-world",
title: "生成世界观",
keywords: ["world", "世界观", "设定", "设计器"],
kind: "action",
href: `${p}/codex?gen=world`,
group: "动作",
},
];
}
// 全局命令(无 projectId 时;如作品库/设置)。
export function globalCommands(): Command[] {
return [
{
id: "nav-home",
title: "作品库",
keywords: ["home", "作品", "library", "projects"],
kind: "navigate",
href: "/",
group: "导航",
},
{
id: "nav-settings",
title: "设置",
keywords: ["settings", "设置", "凭据", "provider", "档位"],
kind: "navigate",
href: "/settings/providers",
group: "导航",
},
];
}
// 模糊过滤:空查询返回全部;否则在 title + keywords 上做大小写无关子串匹配(保持原顺序)。
export function filterCommands(
commands: readonly Command[],
query: string,
): Command[] {
const q = query.trim().toLowerCase();
if (!q) return [...commands];
return commands.filter((cmd) => {
if (cmd.title.toLowerCase().includes(q)) return true;
return cmd.keywords.some((kw) => kw.toLowerCase().includes(q));
});
}
// 键盘高亮索引在 [0, len) 内循环(空列表归 0
export function clampHighlight(index: number, length: number): number {
if (length <= 0) return 0;
const wrapped = index % length;
return wrapped < 0 ? wrapped + length : wrapped;
}
// 上/下方向移动高亮(循环)。
export function moveHighlight(
index: number,
delta: number,
length: number,
): number {
return clampHighlight(index + delta, length);
}