This commit is contained in:
2026-04-18 20:29:33 +08:00
parent 8c3fbd9bcf
commit c39dbc59ee
10 changed files with 233 additions and 49 deletions

View File

@@ -356,6 +356,12 @@ function buildNpcDialoguePromptBase(
payload: NpcChatDialogueRequest | NpcChatTurnRequest | NpcRecruitDialogueRequest,
) {
const encounter = describeEncounter(payload.encounter);
const character =
(payload as NpcChatTurnRequest).character ??
(payload as NpcChatTurnRequest).player;
if (!(payload as NpcChatTurnRequest).character && character) {
(payload as NpcChatTurnRequest).character = character;
}
return [
`世界:${describeWorld(payload.worldType)}`,
@@ -422,12 +428,16 @@ export function buildNpcChatTurnReplyPrompt(
) {
const encounter = describeEncounter(payload.encounter);
const npcState = asRecord(payload.npcState);
const conversationHistory =
Array.isArray(payload.conversationHistory) && payload.conversationHistory.length > 0
? payload.conversationHistory
: payload.dialogue ?? payload.conversationHistory ?? [];
const affinity = readNumber(npcState?.affinity, 0);
const chattedCount = readNumber(npcState?.chattedCount, 0);
return [
buildNpcDialoguePromptBase(payload),
describeNpcConversationHistory(payload.conversationHistory, encounter.npcName),
describeNpcConversationHistory(conversationHistory, encounter.npcName),
`当前关系值:${affinity}`,
`已聊天轮次:${chattedCount}`,
`玩家刚刚说:${payload.playerMessage}`,
@@ -442,10 +452,14 @@ export function buildNpcChatTurnSuggestionPrompt(
npcReply: string,
) {
const encounter = describeEncounter(payload.encounter);
const conversationHistory =
Array.isArray(payload.conversationHistory) && payload.conversationHistory.length > 0
? payload.conversationHistory
: payload.dialogue ?? payload.conversationHistory ?? [];
return [
buildNpcDialoguePromptBase(payload),
describeNpcConversationHistory(payload.conversationHistory, encounter.npcName),
describeNpcConversationHistory(conversationHistory, encounter.npcName),
`玩家刚刚说:${payload.playerMessage}`,
`NPC 刚刚回复:${npcReply}`,
`请围绕刚刚这轮对话,为玩家生成 3 条可以继续和 ${encounter.npcName} 聊下去的中文短句候选。`,

View File

@@ -0,0 +1,43 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { npcChatTurnRequestSchema } from './chatService.js';
test('npc chat turn schema normalizes player and dialogue aliases', () => {
const payload = npcChatTurnRequestSchema.parse({
worldType: 'WUXIA',
player: {
id: 'hero',
name: '沈行',
},
encounter: {
id: 'npc-liu',
npcName: '柳无声',
},
monsters: [],
history: [],
context: {
sceneName: '客栈内室',
},
dialogue: [
{
speaker: 'player',
text: '你刚才那句话是什么意思?',
},
],
playerMessage: '你能说得再明白一点吗?',
npcState: {
affinity: 4,
chattedCount: 1,
recruited: false,
},
});
assert.equal(payload.character.name, '沈行');
assert.deepEqual(payload.conversationHistory, [
{
speaker: 'player',
text: '你刚才那句话是什么意思?',
},
]);
});

View File

@@ -23,7 +23,8 @@ const baseCharacterChatSchema = z.object({
const baseNpcChatSchema = z.object({
worldType: z.string().trim().min(1),
character: jsonObjectSchema,
character: jsonObjectSchema.optional(),
player: jsonObjectSchema.optional(),
encounter: jsonObjectSchema,
monsters: z.array(jsonObjectSchema).default([]),
history: z.array(jsonObjectSchema).default([]),
@@ -47,17 +48,35 @@ export const characterChatSummaryRequestSchema = baseCharacterChatSchema.extend(
) satisfies z.ZodType<CharacterChatSummaryRequest>;
export const npcChatDialogueRequestSchema = baseNpcChatSchema.extend({
character: jsonObjectSchema,
topic: z.string().trim().min(1),
resultSummary: z.string().optional().default(''),
}) satisfies z.ZodType<NpcChatDialogueRequest>;
export const npcChatTurnRequestSchema = baseNpcChatSchema.extend({
conversationHistory: z.array(jsonObjectSchema).default([]),
playerMessage: z.string().trim().min(1),
npcState: jsonObjectSchema,
}) satisfies z.ZodType<NpcChatTurnRequest>;
export const npcChatTurnRequestSchema = baseNpcChatSchema
.extend({
conversationHistory: z.array(jsonObjectSchema).optional(),
dialogue: z.array(jsonObjectSchema).optional(),
playerMessage: z.string().trim().min(1),
npcState: jsonObjectSchema,
})
.superRefine((value, ctx) => {
if (!value.character && !value.player) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'npc chat turn request requires character or player',
path: ['character'],
});
}
})
.transform((value) => ({
...value,
character: value.character ?? value.player ?? {},
conversationHistory: value.conversationHistory ?? value.dialogue ?? [],
})) satisfies z.ZodType<NpcChatTurnRequest>;
export const npcRecruitDialogueRequestSchema = baseNpcChatSchema.extend({
character: jsonObjectSchema,
invitationText: z.string().trim().min(1),
recruitSummary: z.string().optional().default(''),
}) satisfies z.ZodType<NpcRecruitDialogueRequest>;