预设兼容:按真实抓包对齐装配与包装层,补上声明式工具与预设正则。

- 装配只认 prompt_order.enabled;squash_system_messages 只合并相邻 system(新增 system 档,连接符按酒馆用单换行)
- 包装层:new_chat_prompt、示例对话(new_example_chat_prompt + <START> 切段 + name)、角色卡三块与 personality_format/scenario_format、{{user}}/{{char}} 取值、任务契约并进预设条目
- 工具协议:按 ToolBindings / 伪工具协议数据声明真 tools(presetToolPolicy=preset),工具描述用到的条目不再重复作为消息发出
- 正则:promptOnly 作用于对话历史(含 depth 过滤)、写入时脚本改写回复;{{//}} 注释按配平扫描
- tools/st-request-diff:真假上游抓包对比工具、三份预设的机制结论与实测报告
This commit is contained in:
2026-10-23 00:13:08 +08:00
parent ed09e5c831
commit 9dc927282f
94 changed files with 11558 additions and 413 deletions
+355 -1
View File
@@ -1,20 +1,26 @@
import { randomUUID } from "node:crypto";
import type { IncomingMessage, ServerResponse } from "node:http";
import {
abortReroll,
beginReroll,
createConversation,
createShell,
deleteConversation,
deleteShell,
duplicateConversation,
EMPTY_SHELL_SLOTS,
getConversation,
getShell,
listConversations,
listShells,
mergeTurnReplies,
promoteTemporary,
runDirectChatMultiTurn,
saveConversation,
switchTurnVariant,
updateConversation,
updateShell,
writeRerollReplies,
type DirectChatAssistantReply,
type DirectChatConversation,
type DirectChatMessage,
@@ -40,6 +46,27 @@ function json(res: ServerResponse, status: number, data: unknown): void {
res.end(JSON.stringify(data));
}
function readWindows(raw: unknown): DirectChatWindow[] | undefined {
if (!Array.isArray(raw)) return undefined;
const out: DirectChatWindow[] = [];
for (const item of raw) {
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
const row = item as Record<string, unknown>;
const id = typeof row.id === "string" ? row.id.trim() : "";
const profileId = typeof row.profileId === "string" ? row.profileId.trim() : "";
if (!id || !profileId) continue;
const window: DirectChatWindow = { id, profileId };
if (typeof row.profileName === "string" && row.profileName.trim()) {
window.profileName = row.profileName.trim();
}
if (typeof row.model === "string" && row.model.trim()) {
window.model = row.model.trim();
}
out.push(window);
}
return out;
}
function wantsNdjson(req: IncomingMessage): boolean {
return String(req.headers.accept || "").includes("application/x-ndjson");
}
@@ -68,11 +95,14 @@ function createStreamingSaver(
windows: DirectChatWindow[],
text: string,
turnId: string,
baseTurns?: DirectChatTurn[],
) {
const ts = new Date().toISOString();
const replies: DirectChatAssistantReply[] = [];
let timer: ReturnType<typeof setTimeout> | null = null;
let closed = false;
let produced = false;
let turnsState = baseTurns ? structuredClone(baseTurns) : null;
const saveNow = () => {
if (timer) {
@@ -80,6 +110,19 @@ function createStreamingSaver(
timer = null;
}
if (closed) return;
if (turnsState) {
turnsState = writeRerollReplies(
turnsState,
turnId,
replies.map((r) => ({ ...r })),
);
saveConversation({
...conversation,
windows,
turns: turnsState,
});
return;
}
const turn: DirectChatTurn = {
id: turnId,
user: { content: text, createdAt: ts },
@@ -108,6 +151,7 @@ function createStreamingSaver(
reply = { windowId: event.windowId, content: "" };
replies.push(reply);
}
if (event.delta) produced = true;
if (event.field === "thinking") {
reply.thinking = `${reply.thinking || ""}${event.delta}`;
} else {
@@ -117,11 +161,18 @@ function createStreamingSaver(
},
onWindow(result: DirectChatMultiWindowResult) {
const next = replyFromWindowResult(result);
if (next.content || next.thinking || next.error) produced = true;
const idx = replies.findIndex((r) => r.windowId === result.windowId);
if (idx >= 0) replies[idx] = next;
else replies.push(next);
saveNow();
},
sawOutput() {
return produced;
},
currentTurns() {
return turnsState;
},
close() {
closed = true;
if (timer) {
@@ -136,7 +187,10 @@ async function writeMultiStream(
res: ServerResponse,
run: (hooks: MultiStreamHooks) => Promise<DirectChatMultiTurnResult>,
finish: (multi: DirectChatMultiTurnResult) => Record<string, unknown>,
): Promise<void> {
): Promise<
| { ok: true; multi: DirectChatMultiTurnResult }
| { ok: false; error: string }
> {
res.writeHead(200, {
"Content-Type": "application/x-ndjson; charset=utf-8",
"Cache-Control": "no-cache, no-transform",
@@ -152,6 +206,7 @@ async function writeMultiStream(
});
writeEvent({ type: "done", multi, ...finish(multi) });
res.end();
return { ok: true, multi };
} catch (err) {
const error = err instanceof Error ? err.message : String(err);
try {
@@ -160,6 +215,7 @@ async function writeMultiStream(
} catch {
res.destroy();
}
return { ok: false, error };
}
}
@@ -185,6 +241,14 @@ function slotsFromBody(body: Record<string, unknown>): DirectChatShellSlots {
typeof body.postTurn === "string"
? body.postTurn
: EMPTY_SHELL_SLOTS.postTurn,
charName: typeof body.charName === "string" ? body.charName : undefined,
exampleDialogue:
typeof body.exampleDialogue === "string" ? body.exampleDialogue : undefined,
charDescription:
typeof body.charDescription === "string" ? body.charDescription : undefined,
charPersonality:
typeof body.charPersonality === "string" ? body.charPersonality : undefined,
scenario: typeof body.scenario === "string" ? body.scenario : undefined,
};
}
@@ -300,12 +364,16 @@ export async function handleDirectChatApi(
const body = JSON.parse(await readBody(req)) as {
name?: string;
messages?: DirectChatMessage[];
windows?: DirectChatWindow[];
turns?: DirectChatTurn[];
};
try {
const conversation = createConversation({
shellId,
name: body.name,
messages: body.messages,
windows: readWindows(body.windows),
turns: Array.isArray(body.turns) ? body.turns : undefined,
});
json(res, 201, { conversation });
} catch (err) {
@@ -342,11 +410,13 @@ export async function handleDirectChatApi(
const body = JSON.parse(await readBody(req)) as {
name?: string;
messages?: DirectChatMessage[];
windows?: DirectChatWindow[];
};
try {
const conversation = updateConversation(conversationId, {
name: body.name,
messages: body.messages,
windows: readWindows(body.windows),
});
json(res, 200, { conversation });
} catch (err) {
@@ -369,6 +439,18 @@ export async function handleDirectChatApi(
return true;
}
if (req.method === "POST" && sub === "/duplicate") {
try {
const conversation = duplicateConversation(conversationId);
json(res, 201, { conversation });
} catch (err) {
json(res, 404, {
error: err instanceof Error ? err.message : String(err),
});
}
return true;
}
if (req.method === "POST" && sub === "/messages") {
const body = JSON.parse(await readBody(req)) as {
text?: string;
@@ -463,6 +545,278 @@ export async function handleDirectChatApi(
return true;
}
const rerollMatch = sub.match(/^\/turns\/([^/]+)\/reroll$/);
if (rerollMatch && req.method === "POST") {
const turnId = decodeURIComponent(rerollMatch[1]!);
const body = JSON.parse(await readBody(req)) as {
text?: string;
windows?: DirectChatWindow[];
complete?: boolean;
};
const text = typeof body.text === "string" ? body.text.trim() : "";
if (!text) {
json(res, 400, { error: "text 不能为空" });
return true;
}
const conversation = getConversation(conversationId);
if (!conversation) {
json(res, 404, { error: "会话不存在" });
return true;
}
const shell = getShell(conversation.shellId);
if (!shell) {
json(res, 404, { error: "上下文壳不存在" });
return true;
}
const windows =
Array.isArray(body.windows) && body.windows.length
? body.windows
: conversation.windows;
if (!windows.length) {
json(res, 400, { error: "请先新增至少一个模型窗口" });
return true;
}
const prepared = beginReroll(conversation.turns, turnId, text);
if (!prepared) {
json(res, 404, { error: "回合不存在" });
return true;
}
const preparedConversation = saveConversation({
...conversation,
windows,
turns: prepared.turns,
});
const saver = createStreamingSaver(
preparedConversation,
windows,
text,
turnId,
prepared.turns,
);
const run = (hooks?: MultiStreamHooks) =>
runDirectChatMultiTurn({
message: text,
slots: {
systemPrompt: shell.systemPrompt,
loreBefore: shell.loreBefore,
seedHistory: shell.seedHistory,
loreAfter: shell.loreAfter,
postTurn: shell.postTurn,
},
presetId: shell.presetId,
windows,
turns: prepared.prior,
complete: body.complete,
turnId,
onDelta: (event) => {
saver.onDelta(event);
hooks?.onDelta(event);
},
onWindow: (result) => {
saver.onWindow(result);
hooks?.onWindow(result);
},
});
const restoreIfEmpty = () => {
saver.close();
if (saver.sawOutput()) return;
const restored = abortReroll(prepared.turns, turnId);
if (!restored) return;
saveConversation({
...preparedConversation,
windows,
turns: restored,
});
};
try {
if (wantsNdjson(req)) {
const outcome = await writeMultiStream(res, run, (multi) => {
saver.close();
const turns = writeRerollReplies(
saver.currentTurns() ?? prepared.turns,
turnId,
multi.turn.replies,
);
const updated = saveConversation({
...preparedConversation,
windows,
turns,
});
return { conversation: updated, shell };
});
if (!outcome.ok) restoreIfEmpty();
return true;
}
const multi = await run();
saver.close();
const turns = writeRerollReplies(
saver.currentTurns() ?? prepared.turns,
turnId,
multi.turn.replies,
);
const updated = saveConversation({
...preparedConversation,
windows,
turns,
});
json(res, 200, { conversation: updated, shell, multi });
} catch (err) {
restoreIfEmpty();
json(res, 400, {
error: err instanceof Error ? err.message : String(err),
});
}
return true;
}
const retryMatch = sub.match(/^\/turns\/([^/]+)\/retry$/);
if (retryMatch && req.method === "POST") {
const turnId = decodeURIComponent(retryMatch[1]!);
const body = JSON.parse(await readBody(req)) as {
windowIds?: string[];
complete?: boolean;
};
const requested = Array.isArray(body.windowIds)
? body.windowIds.filter(
(x): x is string => typeof x === "string" && Boolean(x.trim()),
)
: [];
if (!requested.length) {
json(res, 400, { error: "缺少 windowIds" });
return true;
}
const conversation = getConversation(conversationId);
if (!conversation) {
json(res, 404, { error: "会话不存在" });
return true;
}
const shell = getShell(conversation.shellId);
if (!shell) {
json(res, 404, { error: "上下文壳不存在" });
return true;
}
const turnIndex = conversation.turns.findIndex((t) => t.id === turnId);
const turn = turnIndex >= 0 ? conversation.turns[turnIndex] : undefined;
if (!turn) {
json(res, 404, { error: "回合不存在" });
return true;
}
const text = turn.user.content.trim();
if (!text) {
json(res, 400, { error: "本轮缺少用户输入" });
return true;
}
const wanted = new Set(requested);
const windows = conversation.windows.filter((w) => wanted.has(w.id));
if (!windows.length) {
json(res, 400, { error: "没有可重试的窗口" });
return true;
}
try {
// 关键:不调 beginReroll。重试只补选中的窗口,其余回复原地保留。
const runRetry = (hooks?: Partial<MultiStreamHooks>) =>
runDirectChatMultiTurn({
message: text,
slots: {
systemPrompt: shell.systemPrompt,
loreBefore: shell.loreBefore,
seedHistory: shell.seedHistory,
loreAfter: shell.loreAfter,
postTurn: shell.postTurn,
},
presetId: shell.presetId,
windows,
turns: conversation.turns.slice(0, turnIndex),
complete: body.complete,
onDelta: hooks?.onDelta,
onWindow: hooks?.onWindow,
});
// 每完成一个窗口就落盘:多个窗口是并行的,先好的先写入,
// 前端也能逐张刷新,不用等最慢的那个。
const persistOne = (result: DirectChatMultiWindowResult) => {
const latest = getConversation(conversationId);
if (!latest) return;
const reply: DirectChatAssistantReply = {
windowId: result.windowId,
content: result.reply ?? "",
...(result.reasoning ? { thinking: result.reasoning } : {}),
...(result.model ? { model: result.model } : {}),
...(result.error ? { error: result.error } : {}),
...(result.contextTrace
? { contextTrace: result.contextTrace }
: {}),
};
saveConversation({
...latest,
turns: mergeTurnReplies(latest.turns, turnId, [reply]),
});
};
if (wantsNdjson(req)) {
const outcome = await writeMultiStream(
res,
(hooks) =>
runRetry({
onDelta: hooks.onDelta,
onWindow: (result) => {
persistOne(result);
hooks.onWindow(result);
},
}),
() => ({
conversation: getConversation(conversationId) ?? conversation,
shell,
}),
);
if (!outcome.ok) return true;
return true;
}
const multi = await runRetry({
onWindow: (result) => persistOne(result),
});
const latest = getConversation(conversationId) ?? conversation;
const updated = saveConversation({
...latest,
turns: mergeTurnReplies(latest.turns, turnId, multi.turn.replies),
});
json(res, 200, { conversation: updated, shell, multi });
} catch (err) {
json(res, 400, {
error: err instanceof Error ? err.message : String(err),
});
}
return true;
}
const variantMatch = sub.match(/^\/turns\/([^/]+)\/variant$/);
if (variantMatch && req.method === "POST") {
const turnId = decodeURIComponent(variantMatch[1]!);
const body = JSON.parse(await readBody(req)) as { direction?: string };
if (body.direction !== "prev" && body.direction !== "next") {
json(res, 400, { error: "direction 须为 prev 或 next" });
return true;
}
const conversation = getConversation(conversationId);
if (!conversation) {
json(res, 404, { error: "会话不存在" });
return true;
}
const turns = switchTurnVariant(
conversation.turns,
turnId,
body.direction,
);
if (!turns) {
json(res, 400, { error: "没有更多版本" });
return true;
}
const updated = saveConversation({ ...conversation, turns });
json(res, 200, { conversation: updated });
return true;
}
const selectMatch = sub.match(/^\/turns\/([^/]+)\/select$/);
if (selectMatch && req.method === "POST") {
const turnId = decodeURIComponent(selectMatch[1]!);