通用执行路径驱动全部生成器("加生成器=加一份声明"):
- @llm: ww_agents +7 输出 schema + 7 spec(book-title/blurb/name/golden-finger/
glossary/opening/fine-outline,只声明 tier)+ build_outline_chapter_context
- @backend: ww_skills GeneratorTool 描述符 + TOOLBOX(11) + get_tool;3 通用端点
GET /skills/toolbox · POST .../skills/{tool_key}/generate(预览不写库,仅记账) ·
POST .../ingest(复用 continuity 409 + partition_writes 白名单);纯 context 派发
- @frontend: 工具箱落地页 RSC + 声明驱动 GeneratorRunner + lib/toolbox 纯函数
+ LeftNav「工具箱」+ ⌘K nav-toolbox/action-gen-*;legacy 3 跳现页
- @qa: tests/test_t6_toolbox_e2e.py 5 用例真 pg + mock 网关零 token,无端点 bug
- P2 收尾: 限流→decisions.md 记延后(单用户原型);noopener/Committable 早已修
守不变量 #2(只声明 tier)/#3(预览不写库,入库经验收 gate)/#9(缓存前缀)。无 DB 迁移。
门禁绿: 后端 ruff/format/mypy 195/alembic 无漂移/pytest 583;前端 lint/tsc/vitest 279/build。
spec 回写 PRODUCT_SPEC §7 + ARCHITECTURE §7.2 端点表。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
185 lines
5.1 KiB
TypeScript
185 lines
5.1 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;
|
||
}
|
||
|
||
// 工具箱生成器的最小描述(命令面板按此生成 action-gen-<key>,无需整份 descriptor)。
|
||
export interface ToolCommandInfo {
|
||
key: string;
|
||
title: string;
|
||
genre?: string | null;
|
||
}
|
||
|
||
// 项目内可用命令(projectId 已知)。导航项给 href;动作项(生成角色等)给 id。
|
||
// tools:工具箱生成器列表(可空)→ 每个生成一条 action-gen-<key> 深链命令(命令面板发现性)。
|
||
export function projectCommands(
|
||
projectId: string,
|
||
tools: readonly ToolCommandInfo[] = [],
|
||
): Command[] {
|
||
const p = `/projects/${projectId}`;
|
||
const toolCommands: Command[] = tools.map((tool) => ({
|
||
id: `action-gen-${tool.key}`,
|
||
title: tool.title,
|
||
keywords: ["生成器", "工具箱", "generator", tool.key, tool.genre ?? ""].filter(
|
||
(k) => k.length > 0,
|
||
),
|
||
kind: "action",
|
||
href: `${p}/toolbox?open=${tool.key}`,
|
||
group: "动作",
|
||
}));
|
||
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-toolbox",
|
||
title: "工具箱",
|
||
keywords: ["toolbox", "工具箱", "生成器", "脑洞", "书名", "金手指"],
|
||
kind: "navigate",
|
||
href: `${p}/toolbox`,
|
||
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: "动作",
|
||
},
|
||
...toolCommands,
|
||
];
|
||
}
|
||
|
||
// 全局命令(无 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);
|
||
}
|