Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ describe('AI SDK OpenAI-compatible protocol fixture', () => {
function: {
name: 'start_character_generation',
arguments:
'{"optimizedPrompt":"银发像素骑士全身像","assumptions":["默认单角色"]}',
'{"optimizedPrompt":"银发像素骑士全身像","optimizationSummary":"我会保留银发骑士特征,并整理为完整的全身母版描述。"}',
},
},
],
Expand All @@ -83,7 +83,10 @@ describe('AI SDK OpenAI-compatible protocol fixture', () => {
toolCalls: [
{
toolName: 'start_character_generation',
input: { optimizedPrompt: '银发像素骑士全身像', assumptions: ['默认单角色'] },
input: {
optimizedPrompt: '银发像素骑士全身像',
optimizationSummary: '我会保留银发骑士特征,并整理为完整的全身母版描述。',
},
},
],
})
Expand All @@ -103,7 +106,7 @@ describe('AI SDK OpenAI-compatible protocol fixture', () => {
type: 'function',
function: {
name: 'start_character_generation',
arguments: '{"optimizedPrompt":"","assumptions":[]}',
arguments: '{"optimizedPrompt":"","optimizationSummary":"我会整理角色描述。"}',
},
},
],
Expand Down
15 changes: 11 additions & 4 deletions frontend/src/features/quick-start-agent/planner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ describe('quickStartPlannerInstructions', () => {
expect(firstTurn).toContain('直接生成')
expect(firstTurn).toContain('不要生成')
expect(firstTurn).toContain('不得依赖关键词匹配')
expect(firstTurn).toContain('动作将在角色母版确认后处理')
expect(firstTurn).toContain('交给用户检查和编辑')
expect(firstTurn).toContain('不得输出思维过程')
expect(laterTurn).toContain('追问额度已经用完')
expect(laterTurn).toContain('默认假设')
expect(laterTurn).toContain('必要补全直接写入 optimizedPrompt')
})
})

Expand All @@ -30,7 +31,10 @@ describe('createAiSdkQuickStartPlanner', () => {
toolCalls: [
{
toolName: 'start_character_generation',
input: { optimizedPrompt: '银发像素骑士全身像', assumptions: ['默认单角色'] },
input: {
optimizedPrompt: '银发像素骑士全身像',
optimizationSummary: '我会保留银发骑士特征,并整理为完整的全身母版描述。',
},
},
],
}))
Expand All @@ -53,7 +57,10 @@ describe('createAiSdkQuickStartPlanner', () => {
toolCalls: [
{
toolName: 'start_character_generation',
input: { optimizedPrompt: '银发像素骑士全身像', assumptions: ['默认单角色'] },
input: {
optimizedPrompt: '银发像素骑士全身像',
optimizationSummary: '我会保留银发骑士特征,并整理为完整的全身母版描述。',
},
},
],
})
Expand Down
30 changes: 17 additions & 13 deletions frontend/src/features/quick-start-agent/planner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export interface CreateAiSdkQuickStartPlannerOptions {

interface StartCharacterGenerationToolInput {
optimizedPrompt: string
assumptions: string[]
optimizationSummary: string
}

const startCharacterGenerationTool = tool({
Expand All @@ -56,22 +56,26 @@ const startCharacterGenerationTool = tool({
maxLength: 4_000,
description: '实际提交给角色母版生成器的完整单角色全身提示词。',
},
assumptions: {
type: 'array',
maxItems: 6,
items: { type: 'string', minLength: 1, maxLength: 200 },
description: '为了立即生成而采用、且需要向用户明确展示的默认假设。',
optimizationSummary: {
type: 'string',
minLength: 1,
maxLength: 600,
description:
'面向用户的一到两句优化说明,只说明会保留、补充或移除什么;不得暴露推理步骤、默认假设、Tool 或内部状态。',
},
},
required: ['optimizedPrompt', 'assumptions'],
required: ['optimizedPrompt', 'optimizationSummary'],
},
{
validate(value) {
try {
const plan = parseCharacterGenerationPlan(value)
return {
success: true,
value: { optimizedPrompt: plan.optimizedPrompt, assumptions: [...plan.assumptions] },
value: {
optimizedPrompt: plan.optimizedPrompt,
optimizationSummary: plan.optimizationSummary,
},
}
} catch (error) {
return {
Expand All @@ -86,23 +90,23 @@ const startCharacterGenerationTool = tool({

export function quickStartPlannerInstructions(clarificationUsed: boolean): string {
const clarificationRule = clarificationUsed
? '追问额度已经用完。除非仍存在硬冲突,否则不得再提问题;请采用明确的默认假设并调用 Tool。'
? '追问额度已经用完。除非仍存在硬冲突,否则不得再提问题;请把必要补全直接写入 optimizedPrompt,并用面向用户的 optimizationSummary 简短说明,然后调用 Tool。'
: '追问额度尚未使用。只有缺少会实质改变角色母版的关键信息时,才最多追问一个问题。'
return `你是 Windup Quick Start 的轻量 Planner。用户已经通过“生成角色”主操作授予本页面会话一次角色母版生成权限。你只判断信息是否足够,并且最多调用一次 ${START_CHARACTER_GENERATION_TOOL};你不参与生成后的任何流程。

当前能力只生成一个角色的角色母版:单角色、完整身体、清楚轮廓,适合后续动作生成。保留用户明确给出的身份、外观、服装、气质和美术风格,把口语整理成可直接生成的 optimizedPrompt。用户描述的动态动作不要写进角色母版 Prompt;需要时在 assumptions 中明确“动作将在角色母版确认后处理”
当前能力只生成一个角色的角色母版:单角色、完整身体、清楚轮廓,适合后续动作生成。保留用户明确给出的身份、外观、服装、气质和美术风格,把口语整理成可直接生成的 optimizedPrompt。用户描述的动态动作不要写进角色母版 Prompt。

决策规则:
1. 信息足够时立即调用唯一 Tool,不要再次确认。
2. 用户在语义上明确要求“跳过、不用问、直接生成”时,基于已有信息与可见默认假设立即调用 Tool。
2. 用户在语义上明确要求“跳过、不用问、直接生成”时,基于已有信息完成必要补全,把补全结果直接写入 optimizedPrompt,并在 optimizationSummary 中用正常对话说明,然后立即调用 Tool。
3. 用户明确表示“不要生成、只润色、先讨论”等否定意图时不得调用 Tool。引用、假设或否定语境也不得被误判;不得依赖关键词匹配,必须理解整句语义。
4. 信息不足且仍有追问额度时,只问一个最关键、最容易回答的问题,不得列问卷。
5. 输入有明显自相矛盾、违反内容政策,或超出当前单角色母版能力的硬冲突时,简短说明需要修改的具体内容,不调用 Tool。Planner 的提醒只是交互提示,最终安全、配额与计费仍由生成后端负责。
6. 调用 Tool 时,optimizedPrompt 必须是最终实际使用的提示词;assumptions 只列确实采用的默认值,可以为空。不得产生第二个 Tool Call。
6. 调用 Tool 时,optimizedPrompt 是交给用户检查和编辑的完整提示词;optimizationSummary 用一到两句正常对话说明你保留、补充或移除了什么。不得输出思维过程、逐步推理、默认假设清单、Tool 名称、调用计划或内部状态。不得产生第二个 Tool Call。

${clarificationRule}

不调用 Tool 时,只输出一条简短的中文追问或修改提醒。调用 Tool 后不再输出后续计划。`
不调用 Tool 时,只输出一条简短的中文追问或修改提醒。调用 Tool 后不再输出额外文字,宿主会把 optimizationSummary 和 optimizedPrompt 作为用户可编辑的提案展示。`
}

export function createAiSdkQuickStartPlanner({
Expand Down
38 changes: 34 additions & 4 deletions frontend/src/features/quick-start-agent/react.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe('useQuickStartAgent', () => {
toolName: 'start_character_generation',
input: {
optimizedPrompt: '银发像素骑士,全身像',
assumptions: ['默认单角色', '默认横版视角'],
optimizationSummary: '我会保留银发骑士特征,并补全适合母版生成的全身描述。',
},
},
],
Expand Down Expand Up @@ -64,7 +64,7 @@ describe('useQuickStartAgent', () => {
expect(result.current.state).toEqual({
status: 'dispatching',
optimizedPrompt: '银发像素骑士,全身像',
assumptions: ['默认单角色', '默认横版视角'],
optimizationSummary: '我会保留银发骑士特征,并补全适合母版生成的全身描述。',
}),
)
expect(startCharacterGeneration).not.toHaveBeenCalled()
Expand Down Expand Up @@ -150,6 +150,30 @@ describe('useQuickStartAgent', () => {
})
})

it('does not expose Agent protocol details in the user-facing error state', async () => {
const planner = vi.fn(async () => ({
text: '',
finishReason: 'tool-calls',
toolCalls: [
{
toolName: 'start_character_generation',
input: { optimizedPrompt: '像素骑士' },
},
],
}))
const startCharacterGeneration = vi.fn(async () => ({ runId: 'run-agent' }))
const { result } = renderHook(() => useQuickStartAgent({ planner, startCharacterGeneration }))

await act(async () => {
await expect(result.current.submit('像素骑士')).rejects.toThrow('生成 Tool 参数字段无效')
})

expect(result.current.state).toEqual({
status: 'error',
message: '提示词优化没有完成,请重新发送',
})
})

it('keeps the first successful clarification available after an initial Planner failure', async () => {
const planner = vi
.fn<(input: PlannerInput) => Promise<PlannerResult>>()
Expand Down Expand Up @@ -184,7 +208,10 @@ describe('useQuickStartAgent', () => {
toolCalls: [
{
toolName: 'start_character_generation',
input: { optimizedPrompt: '银发像素骑士,全身像', assumptions: [] },
input: {
optimizedPrompt: '银发像素骑士,全身像',
optimizationSummary: '我会保留银发骑士特征,并整理为完整的全身母版描述。',
},
},
],
}),
Expand All @@ -211,7 +238,10 @@ describe('useQuickStartAgent', () => {
toolCalls: [
{
toolName: 'start_character_generation',
input: { optimizedPrompt: '银发像素骑士,全身像', assumptions: [] },
input: {
optimizedPrompt: '银发像素骑士,全身像',
optimizationSummary: '我会保留银发骑士特征,并整理为完整的全身母版描述。',
},
},
],
},
Expand Down
16 changes: 12 additions & 4 deletions frontend/src/features/quick-start-agent/react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ export type QuickStartAgentState =
| { status: 'error'; message: string }

export interface UseQuickStartAgentOptions extends CreateQuickStartAgentOptions {
/** 测试可替换;生产至少等待一帧,保证最终 Prompt 先于付费写操作可见。 */
waitForPresentation?: () => Promise<void>
/** 测试可替换;页面可在展示提案后返回用户编辑过的最终 Prompt。 */
waitForPresentation?: (plan: CharacterGenerationPlan) => Promise<string | void>
}

async function waitForBrowserPresentation(): Promise<void> {
Expand All @@ -28,7 +28,15 @@ async function waitForBrowserPresentation(): Promise<void> {
}

function errorMessage(cause: unknown): string {
return cause instanceof Error && cause.message ? cause.message : 'Agent 暂时不可用,请稍后重试'
if (!(cause instanceof Error) || !cause.message) return 'Agent 暂时不可用,请稍后重试'
if (
/Tool|Planner|start_character_generation|optimizedPrompt|optimizationSummary|生成授权/u.test(
cause.message,
)
) {
return '提示词优化没有完成,请重新发送'
}
return cause.message
}

export function useQuickStartAgent({
Expand Down Expand Up @@ -84,7 +92,7 @@ export function useQuickStartAgent({
signal: controller.signal,
async onBeforeDispatch(plan) {
if (mounted.current) setState({ status: 'dispatching', ...plan })
await waitForPresentation()
return await waitForPresentation(plan)
},
})
if (result.kind === 'message' && mounted.current) {
Expand Down
53 changes: 36 additions & 17 deletions frontend/src/features/quick-start-agent/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ function plannerResult(overrides: Partial<PlannerResult> = {}): PlannerResult {
toolCalls: [
{
toolName: 'start_character_generation',
input: { optimizedPrompt: '完整身体的银发像素骑士', assumptions: ['默认单角色'] },
input: {
optimizedPrompt: '完整身体的银发像素骑士',
optimizationSummary: '我会保留银发骑士的身份特征,并补全适合角色母版的全身描述。',
},
},
],
...overrides,
Expand All @@ -37,7 +40,7 @@ describe('validatePlannerTerminal', () => {
expect(validatePlannerTerminal(plannerResult())).toEqual({
kind: 'tool',
optimizedPrompt: '完整身体的银发像素骑士',
assumptions: ['默认单角色'],
optimizationSummary: '我会保留银发骑士的身份特征,并补全适合角色母版的全身描述。',
})
})

Expand Down Expand Up @@ -67,7 +70,10 @@ describe('validatePlannerTerminal', () => {
toolCalls: [
{
toolName: 'start_character_generation',
input: { optimizedPrompt: ' ', assumptions: ['默认单角色'] },
input: {
optimizedPrompt: ' ',
optimizationSummary: '我会整理角色描述。',
},
},
],
}),
Expand All @@ -88,18 +94,18 @@ describe('parseCharacterGenerationPlan', () => {
it.each([
[
'unknown fields',
{ optimizedPrompt: '像素骑士', assumptions: [], unexpected: true },
{
optimizedPrompt: '像素骑士',
optimizationSummary: '我会整理角色描述。',
unexpected: true,
},
'生成 Tool 参数字段无效',
],
['missing optimization summary', { optimizedPrompt: '像素骑士' }, '生成 Tool 参数字段无效'],
[
'non-array assumptions',
{ optimizedPrompt: '像素骑士', assumptions: '默认单角色' },
'生成 Tool 的 assumptions 无效',
],
[
'empty assumption',
{ optimizedPrompt: '像素骑士', assumptions: [' '] },
'生成 Tool 的 assumptions 无效',
'empty optimization summary',
{ optimizedPrompt: '像素骑士', optimizationSummary: ' ' },
'生成 Tool 的 optimizationSummary 无效',
],
])('rejects %s', (_label, input, message) => {
expect(() => parseCharacterGenerationPlan(input)).toThrow(message)
Expand Down Expand Up @@ -144,7 +150,7 @@ describe('createQuickStartAgent', () => {
expect(startCharacterGeneration).not.toHaveBeenCalled()
})

it('dispatches the injected write action once after exposing the final prompt', async () => {
it('dispatches the user-edited prompt once after presenting the proposal', async () => {
const { agent, startCharacterGeneration } = fixture()
const events: string[] = []
startCharacterGeneration.mockImplementation(async () => {
Expand All @@ -156,22 +162,35 @@ describe('createQuickStartAgent', () => {
agent.start('银发骑士', {
onBeforeDispatch: async (plan) => {
events.push(`visible:${plan.optimizedPrompt}`)
return '完整身体的银发像素骑士,深蓝斗篷'
},
}),
).resolves.toEqual({
kind: 'generated',
runId: 'run-1',
optimizedPrompt: '完整身体的银发像素骑士',
assumptions: ['默认单角色'],
optimizedPrompt: '完整身体的银发像素骑士,深蓝斗篷',
optimizationSummary: '我会保留银发骑士的身份特征,并补全适合角色母版的全身描述。',
})

expect(events).toEqual(['visible:完整身体的银发像素骑士', 'action'])
expect(startCharacterGeneration).toHaveBeenCalledTimes(1)
expect(startCharacterGeneration).toHaveBeenCalledWith({
prompt: '完整身体的银发像素骑士',
prompt: '完整身体的银发像素骑士,深蓝斗篷',
})
})

it('rejects an explicitly cleared prompt without dispatching the write action', async () => {
const { agent, startCharacterGeneration } = fixture()

await expect(
agent.start('银发骑士', {
onBeforeDispatch: async () => ' ',
}),
).rejects.toThrow('确认后的角色提示词无效')

expect(startCharacterGeneration).not.toHaveBeenCalled()
})

it('keeps a text-only response side-effect free and spends at most one clarification', async () => {
const { agent, planner, startCharacterGeneration } = fixture(
plannerResult({ text: '最希望保留什么外观特征?', finishReason: 'stop', toolCalls: [] }),
Expand Down Expand Up @@ -220,7 +239,7 @@ describe('createQuickStartAgent', () => {
toolCalls: [
{
toolName: 'start_character_generation',
input: { optimizedPrompt: '', assumptions: [] },
input: { optimizedPrompt: '', optimizationSummary: '我会整理角色描述。' },
},
],
}),
Expand Down
Loading
Loading