会话与前端接上听书、呈现投影、请求轮廓和旁观气泡偏好。

新建作品可选听书连载,朗读与消费进度走专用接口。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-07 23:49:44 +08:00
co-authored by Cursor
parent cf6cb68429
commit 66ed1eaca8
21 changed files with 2395 additions and 357 deletions
+83 -33
View File
@@ -1,5 +1,6 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import { listSkills } from "../skills/loader.js";
import type { ModuleCatalogEntry } from "../skills/creation-flow.js";
import { createBook, deleteBook, duplicateBook, getBook, listBooks, updateBook } from "../book/store.js";
import { sessionManager } from "./session-manager.js";
@@ -39,17 +40,64 @@ function moduleStatusFromSections(
return depth >= 2 ? "ready" : "partial";
}
type ModuleBrowseRow = {
id: string;
name: string;
declaration: string;
artifact: string;
hasPrompt: boolean;
sectionsPresent: string[];
status: ModuleStatus;
group: string;
presentMode: "message" | "zero_layer" | null;
layer: "intermediate" | "final" | null;
repeatable: boolean;
closer: boolean;
priorArtifact: boolean;
programSide: boolean;
libraries: string[];
land: string;
};
async function moduleBrowseFields(
entry: ModuleCatalogEntry,
): Promise<
Omit<
ModuleBrowseRow,
| "id"
| "name"
| "declaration"
| "artifact"
| "hasPrompt"
| "sectionsPresent"
| "status"
>
> {
const { dictateModuleLandHint, isDictateProgramModule } = await import(
"../skills/creation-flow.js"
);
const { moduleBrowseGroup, modulePresentationMode } = await import(
"../dictate/creation-plan.js"
);
const { getLibrary } = await import("../libraries/registry.js");
return {
group: moduleBrowseGroup(entry),
presentMode: modulePresentationMode(entry.id),
layer: entry.layer ?? null,
repeatable: Boolean(entry.repeatable),
closer: Boolean(entry.closer),
priorArtifact: entry.kind === "prior-artifact",
programSide: isDictateProgramModule(entry),
libraries: (entry.libraries ?? []).map(
(id) => getLibrary(id)?.label ?? id,
),
land: dictateModuleLandHint(entry),
};
}
async function loadModulesPayload(skillId: string): Promise<{
skillPackId: string;
modules: Array<{
id: string;
name: string;
declaration: string;
artifact: string;
hasPrompt: boolean;
sectionsPresent: string[];
status: ModuleStatus;
}>;
groups: Array<{ id: string; label: string; hint: string }>;
modules: ModuleBrowseRow[];
}> {
const { loadSkill } = await import("../skills/loader.js");
const {
@@ -59,13 +107,14 @@ async function loadModulesPayload(skillId: string): Promise<{
catalogModulesForIntake,
MODULE_SECTION_IDS,
} = await import("../skills/creation-flow.js");
const { MODULE_BROWSE_GROUPS } = await import("../dictate/creation-plan.js");
const skill = await loadSkill(skillId);
if (!skill.skillPackRoot) {
return { skillPackId: skillId, modules: [] };
return { groups: [...MODULE_BROWSE_GROUPS], modules: [] };
}
const catalog = await loadModuleCatalog(skill.skillPackRoot);
const modules = [];
for (const m of catalogModulesForIntake(catalog ?? { modules: [] }, "recipe")) {
const modules: ModuleBrowseRow[] = [];
for (const m of catalogModulesForIntake(catalog ?? { modules: [] }, "dictate")) {
const prompt = await loadModulePrompt(skill.skillPackRoot, m.id);
const sections = prompt
? parseModulePromptSections(prompt)
@@ -83,27 +132,23 @@ async function loadModulesPayload(skillId: string): Promise<{
hasPrompt,
sectionsPresent: [...sectionsPresent],
status: moduleStatusFromSections(hasPrompt, sectionsPresent, taskBody),
...(await moduleBrowseFields(m)),
});
}
return { skillPackId: skillId, modules };
return { groups: [...MODULE_BROWSE_GROUPS], modules };
}
async function loadModuleDetailPayload(
skillId: string,
moduleId: string,
): Promise<{
skillPackId: string;
id: string;
name: string;
declaration: string;
artifact: string;
hasPrompt: boolean;
sectionsPresent: string[];
status: ModuleStatus;
sections: Record<string, string>;
meta: Record<string, unknown> | null;
raw: string;
} | null> {
): Promise<
| (ModuleBrowseRow & {
sections: Record<string, string>;
meta: Record<string, unknown> | null;
raw: string;
})
| null
> {
const { loadSkill } = await import("../skills/loader.js");
const {
loadModuleCatalog,
@@ -141,7 +186,6 @@ async function loadModuleDetailPayload(
}
}
return {
skillPackId: skillId,
id: entry.id,
name: entry.name,
declaration: entry.declaration,
@@ -153,6 +197,7 @@ async function loadModuleDetailPayload(
sectionsPresent,
sections.task,
),
...(await moduleBrowseFields(entry)),
sections,
meta,
raw: prompt ?? "",
@@ -254,7 +299,7 @@ export async function handleBooksApi(
return true;
}
/** 默认配方包的技能池(编排备选) */
/** 对话落盘节点:按角色分组,不含旧配方包 */
if (pathname === "/api/modules" && req.method === "GET") {
const { DEFAULT_ORCHESTRATOR_ID } = await import(
"../config/default-orchestrator.js"
@@ -343,8 +388,8 @@ export async function handleBooksApi(
orchestratorId?: string;
/** 旧客户端可传;新建作品改由创作对话判断路径 */
recipeId?: string;
/** 旧客户端兼容;新建作品统一使用 dictate */
creationMode?: "recipe" | "dictate";
/** 进料方式:对话落盘(默认)| 听书连载 | 旧工序编排 */
creationMode?: "recipe" | "dictate" | "listen_serial";
};
const skills = await listSkills();
const requested = body.orchestratorId?.trim();
@@ -356,14 +401,19 @@ export async function handleBooksApi(
json(res, 400, { error: "没有可用的编排器技能包" });
return true;
}
const book = createBook({ title: body.title });
const rawMode = body.creationMode;
const creationMode =
rawMode === "listen_serial" || rawMode === "recipe" || rawMode === "dictate"
? rawMode
: "dictate";
const book = createBook({ title: body.title, creationMode });
updateBook(book.id, {
activeSkillId: director.name,
activeSkillName: director.description?.split("\n")[0]?.slice(0, 80) || director.name,
orchestratorId: director.name,
orchestratorName: director.name,
creationMode,
});
const creationMode = "dictate";
const session = await sessionManager.createForBook(
book.id,
director.name,