完善创作节点循环与等待态交互,并大幅打磨 Web 壳与会话运行时。

- 节点循环:配方开局直坐 DAG 第一步;验收后先问下一步意向,再确认开干并可钉编排参数;「按意见修改」只重跑当前节点。
- 询问分流:能力 opening 走说话面引导,可跳过追问按题干去重;追问与产物同轮挂载,避免拆成两段历史。
- 运行时:补强 revision 重跑、创作流程合并/进度指针、工具循环与 worker 执行;新增运行日志与 web:watch。
- 前端:统一等待态文案与底栏主按钮,完善询问卡/产物验收/呈现壳样式与交互。
- 同步世界模拟器模块提示、编排文档与相关测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-17 01:28:12 +08:00
co-authored by Cursor
parent 20938b04d7
commit ec474cb946
56 changed files with 7459 additions and 1371 deletions
+147 -106
View File
@@ -5,6 +5,7 @@ import type {
CompleteWithToolsResult,
LlmProvider,
StreamCallbacks,
TokenUsage,
} from "./client.js";
import {
recordTokenUsage,
@@ -15,6 +16,7 @@ import {
buildContextTrace,
type LlmContextTrace,
} from "../types/context-trace.js";
import { debugLog, labelCaller, labelLlmMode, labelTool } from "../log.js";
export type LlmTrackingContext = {
sessionId?: string;
@@ -41,6 +43,77 @@ function capturePendingTrace(
});
}
function recordUsage(
ctx: LlmTrackingContext,
caller: string | undefined,
model: string | undefined,
usage: TokenUsage | undefined,
): void {
if (!usage) return;
const record = recordTokenUsage({
sessionId: ctx.sessionId,
bookId: ctx.bookId,
bookTitle: ctx.bookTitle,
orchestratorId: ctx.orchestratorId,
caller: caller ?? "unknown",
model: model ?? "unknown",
promptTokens: usage.promptTokens,
completionTokens: usage.completionTokens,
totalTokens: usage.totalTokens,
cachedTokens: usage.cachedTokens,
cacheMissTokens: usage.cacheMissTokens,
});
ctx.pendingUsage = toMessageTokenUsage(record);
}
function summarizeComplete(result: CompleteResult): string {
const bits = [
result.model ? `模型=${result.model}` : "",
`思维链=${result.reasoning?.trim().length ?? 0}字`,
`正文=${result.content?.length ?? 0}字`,
];
if (result.usage) {
bits.push(`用量=${result.usage.promptTokens}+${result.usage.completionTokens}`);
}
return bits.filter(Boolean).join(" ");
}
function summarizeTools(result: CompleteWithToolsResult): string {
const tools =
result.toolCalls.map((t) => labelTool(t.name)).join("、") || "无";
const bits = [
result.model ? `模型=${result.model}` : "",
`思维链=${result.reasoning?.trim().length ?? 0}字`,
`正文=${result.content?.length ?? 0}字`,
`工具=${tools}`,
];
if (result.usage) {
bits.push(`用量=${result.usage.promptTokens}+${result.usage.completionTokens}`);
}
return bits.filter(Boolean).join(" ");
}
async function loggedLlm<T>(
kind: string,
caller: string | undefined,
run: () => Promise<T>,
summarize: (result: T) => string,
): Promise<T> {
const who = labelCaller(caller);
const mode = labelLlmMode(kind);
const t0 = Date.now();
debugLog("llm", `开始 ${who} · ${mode}`);
try {
const result = await run();
debugLog("llm", `结束 ${who} · ${mode} ${Date.now() - t0}毫秒 ${summarize(result)}`);
return result;
} catch (err) {
const detail = err instanceof Error ? err.message : String(err);
debugLog("llm", `失败 ${who} · ${mode} ${Date.now() - t0}毫秒 ${detail.slice(0, 160)}`);
throw err;
}
}
export class TokenTrackingProvider implements LlmProvider {
constructor(
private readonly inner: LlmProvider,
@@ -51,29 +124,21 @@ export class TokenTrackingProvider implements LlmProvider {
messages: Parameters<LlmProvider["complete"]>[0],
options?: CompleteOptions,
): Promise<CompleteResult> {
const result = await this.inner.complete(messages, options);
const ctx = this.getContext();
capturePendingTrace(ctx, messages, options?.caller, result.model);
if (result.usage) {
const record = recordTokenUsage({
sessionId: ctx.sessionId,
bookId: ctx.bookId,
bookTitle: ctx.bookTitle,
orchestratorId: ctx.orchestratorId,
caller: options?.caller ?? "unknown",
model: result.model ?? "unknown",
promptTokens: result.usage.promptTokens,
completionTokens: result.usage.completionTokens,
totalTokens: result.usage.totalTokens,
cachedTokens: result.usage.cachedTokens,
cacheMissTokens: result.usage.cacheMissTokens,
});
ctx.pendingUsage = toMessageTokenUsage(record);
}
if (result.reasoning?.trim()) {
ctx.pendingReasoning = result.reasoning.trim();
}
return result;
return loggedLlm(
"complete",
options?.caller,
async () => {
const result = await this.inner.complete(messages, options);
const ctx = this.getContext();
capturePendingTrace(ctx, messages, options?.caller, result.model);
recordUsage(ctx, options?.caller, result.model, result.usage);
if (result.reasoning?.trim()) {
ctx.pendingReasoning = result.reasoning.trim();
}
return result;
},
summarizeComplete,
);
}
async completeStream(
@@ -88,67 +153,51 @@ export class TokenTrackingProvider implements LlmProvider {
if (result.content) callbacks.onContentDelta?.(result.content);
return result;
}
let reasoningBuf = "";
const result = await inner.completeStream(messages, options, {
onReasoningDelta: (delta) => {
reasoningBuf += delta;
return loggedLlm(
"stream",
options?.caller,
async () => {
let reasoningBuf = "";
const result = await inner.completeStream!(messages, options, {
onReasoningDelta: (delta) => {
reasoningBuf += delta;
const ctx = this.getContext();
ctx.pendingReasoning = reasoningBuf;
callbacks.onReasoningDelta?.(delta);
},
onContentDelta: callbacks.onContentDelta,
});
const ctx = this.getContext();
ctx.pendingReasoning = reasoningBuf;
callbacks.onReasoningDelta?.(delta);
capturePendingTrace(ctx, messages, options?.caller, result.model);
recordUsage(ctx, options?.caller, result.model, result.usage);
if (result.reasoning?.trim()) {
ctx.pendingReasoning = result.reasoning.trim();
}
return result;
},
onContentDelta: callbacks.onContentDelta,
});
const ctx = this.getContext();
capturePendingTrace(ctx, messages, options?.caller, result.model);
if (result.usage) {
const record = recordTokenUsage({
sessionId: ctx.sessionId,
bookId: ctx.bookId,
bookTitle: ctx.bookTitle,
orchestratorId: ctx.orchestratorId,
caller: options?.caller ?? "unknown",
model: result.model ?? "unknown",
promptTokens: result.usage.promptTokens,
completionTokens: result.usage.completionTokens,
totalTokens: result.usage.totalTokens,
cachedTokens: result.usage.cachedTokens,
cacheMissTokens: result.usage.cacheMissTokens,
});
ctx.pendingUsage = toMessageTokenUsage(record);
}
if (result.reasoning?.trim()) {
ctx.pendingReasoning = result.reasoning.trim();
}
return result;
summarizeComplete,
);
}
async completeWithTools(
messages: Parameters<LlmProvider["completeWithTools"]>[0],
options: CompleteWithToolsOptions,
): Promise<CompleteWithToolsResult> {
const result = await this.inner.completeWithTools(messages, options);
const ctx = this.getContext();
capturePendingTrace(ctx, messages, options.caller, result.model);
if (result.usage) {
const record = recordTokenUsage({
sessionId: ctx.sessionId,
bookId: ctx.bookId,
bookTitle: ctx.bookTitle,
orchestratorId: ctx.orchestratorId,
caller: options.caller ?? "unknown",
model: result.model ?? "unknown",
promptTokens: result.usage.promptTokens,
completionTokens: result.usage.completionTokens,
totalTokens: result.usage.totalTokens,
cachedTokens: result.usage.cachedTokens,
cacheMissTokens: result.usage.cacheMissTokens,
});
ctx.pendingUsage = toMessageTokenUsage(record);
}
if (result.reasoning?.trim()) {
ctx.pendingReasoning = result.reasoning.trim();
}
return result;
return loggedLlm(
"tools",
options.caller,
async () => {
const result = await this.inner.completeWithTools(messages, options);
const ctx = this.getContext();
capturePendingTrace(ctx, messages, options.caller, result.model);
recordUsage(ctx, options.caller, result.model, result.usage);
if (result.reasoning?.trim()) {
ctx.pendingReasoning = result.reasoning.trim();
}
return result;
},
summarizeTools,
);
}
async completeWithToolsStream(
@@ -160,37 +209,29 @@ export class TokenTrackingProvider implements LlmProvider {
if (!inner.completeWithToolsStream) {
return this.completeWithTools(messages, options);
}
let reasoningBuf = "";
const result = await inner.completeWithToolsStream(messages, options, {
onReasoningDelta: (delta) => {
reasoningBuf += delta;
return loggedLlm(
"tools-stream",
options.caller,
async () => {
let reasoningBuf = "";
const result = await inner.completeWithToolsStream!(messages, options, {
onReasoningDelta: (delta) => {
reasoningBuf += delta;
const ctx = this.getContext();
ctx.pendingReasoning = reasoningBuf;
callbacks.onReasoningDelta?.(delta);
},
onContentDelta: callbacks.onContentDelta,
});
const ctx = this.getContext();
ctx.pendingReasoning = reasoningBuf;
callbacks.onReasoningDelta?.(delta);
capturePendingTrace(ctx, messages, options.caller, result.model);
recordUsage(ctx, options.caller, result.model, result.usage);
if (result.reasoning?.trim()) {
ctx.pendingReasoning = result.reasoning.trim();
}
return result;
},
onContentDelta: callbacks.onContentDelta,
});
const ctx = this.getContext();
capturePendingTrace(ctx, messages, options.caller, result.model);
if (result.usage) {
const record = recordTokenUsage({
sessionId: ctx.sessionId,
bookId: ctx.bookId,
bookTitle: ctx.bookTitle,
orchestratorId: ctx.orchestratorId,
caller: options.caller ?? "unknown",
model: result.model ?? "unknown",
promptTokens: result.usage.promptTokens,
completionTokens: result.usage.completionTokens,
totalTokens: result.usage.totalTokens,
cachedTokens: result.usage.cachedTokens,
cacheMissTokens: result.usage.cacheMissTokens,
});
ctx.pendingUsage = toMessageTokenUsage(record);
}
if (result.reasoning?.trim()) {
ctx.pendingReasoning = result.reasoning.trim();
}
return result;
summarizeTools,
);
}
}