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>
155 lines
4.2 KiB
TypeScript
155 lines
4.2 KiB
TypeScript
// 命令面板(⌘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);
|
||
}
|