fix(test): stabilize E2E cockpit flow and add LM Studio novel integration test

Harden cockpit dismiss timing and global settings navigation in E2E helpers, tune short-chapter AI prompts for local models, fix electron-updater CJS loading, and add a 10-chapter auto-writing integration test against LM Studio.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-09 14:03:19 +08:00
parent fe421ffc55
commit 8e5045b882
24 changed files with 425 additions and 70 deletions
@@ -12,11 +12,13 @@ export function buildScenePlanMessages(
outlineSummaries.length > 0
? `关联大纲:\n${outlineSummaries.map((s, i) => `${i + 1}. ${s}`).join('\n')}\n\n`
: ''
const sceneRange = wordMax <= 300 ? '1-2' : '3-6'
const summaryHint = wordMax <= 300 ? '至少40字' : '至少80字'
return [
{
role: 'system',
content:
'你是长篇小说章节规划助手。根据本章目标规划 3-6 个连续场景。仅返回 JSON{"scenes":[{"label":"场景名","summary":"至少80字的场景要点"},...],"estimatedWords":数字}。estimatedWords 须在用户给定字数范围内。'
`你是长篇小说章节规划助手。根据本章目标规划 ${sceneRange} 个连续场景。仅返回 JSON{"scenes":[{"label":"场景名","summary":"${summaryHint}的场景要点"},...],"estimatedWords":数字}。estimatedWords 须在用户给定字数范围内。`
},
{
role: 'user',
@@ -35,15 +37,18 @@ export function buildAutoSceneMessages(
priorSceneSummaries.length > 0
? `已写场景摘要:\n${priorSceneSummaries.map((s, i) => `${i + 1}. ${s}`).join('\n')}\n\n`
: ''
const planLen = Math.max(1, config.scenePlan?.length ?? 1)
const sceneMin = Math.max(60, Math.floor((config.wordMin ?? 2000) / planLen))
const sceneMax = Math.max(sceneMin + 20, Math.ceil((config.wordMax ?? 4000) / planLen))
return [
{
role: 'system',
content:
'你是小说场景写作助手。若需为新元素命名且尚无定论,先仅返回 JSON{"type":"naming_required","elementType":"类型","context":"说明","options":["名1","名2","名3","名4","名5"]}。否则输出 600-1500 字场景 HTML <p> 段落。'
`你是小说场景写作助手。若需为新元素命名且尚无定论,先仅返回 JSON{"type":"naming_required","elementType":"类型","context":"说明","options":["名1","名2","名3","名4","名5"]}。否则输出 ${sceneMin}-${sceneMax} 字场景 HTML <p> 段落,勿超出上限。`
},
{
role: 'user',
content: `${prior}上下文:\n${contextText}\n\n本章目标:${config.chapterGoal}\n\n当前场景:${planItem.label}\n场景要点:${planItem.summary}\n\n请生成本场景。`
content: `${prior}上下文:\n${contextText}\n\n本章目标:${config.chapterGoal}\n\n当前场景:${planItem.label}\n场景要点:${planItem.summary}\n\n请生成本场景,控制在 ${sceneMin}-${sceneMax}`
}
]
}
+16 -5
View File
@@ -1,8 +1,18 @@
import { createRequire } from 'node:module'
import { app, BrowserWindow } from 'electron'
import electronUpdater from 'electron-updater'
import type { AppUpdater } from 'electron-updater'
import { IPC } from '../../shared/ipc-channels'
const { autoUpdater } = electronUpdater
const require = createRequire(import.meta.url)
let autoUpdaterRef: AppUpdater | null = null
function getAutoUpdater(): AppUpdater {
if (!autoUpdaterRef) {
const electronUpdater = require('electron-updater') as { autoUpdater: AppUpdater }
autoUpdaterRef = electronUpdater.autoUpdater
}
return autoUpdaterRef
}
export interface UpdateInfoPayload {
version: string
@@ -31,6 +41,7 @@ export class UpdateService {
!app.isPackaged ||
process.env.NODE_ENV === 'development'
const autoUpdater = getAutoUpdater()
autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = true
@@ -62,7 +73,7 @@ export class UpdateService {
this.broadcast(IPC.UPDATE_AVAILABLE, payload)
return payload
}
const result = await autoUpdater.checkForUpdates()
const result = await getAutoUpdater().checkForUpdates()
const info = result?.updateInfo
if (!info) return null
return {
@@ -78,12 +89,12 @@ export class UpdateService {
this.broadcast(IPC.UPDATE_DOWNLOADED, { version: this.mockVersion })
return
}
await autoUpdater.downloadUpdate()
await getAutoUpdater().downloadUpdate()
}
install(): void {
if (this.disabled || this.mockVersion) return
autoUpdater.quitAndInstall()
getAutoUpdater().quitAndInstall()
}
private broadcast(channel: string, payload: unknown): void {
+10 -1
View File
@@ -107,7 +107,16 @@ export function TopBar(): React.JSX.Element {
>
📊
</button>
<button type="button" className="top-btn" onClick={() => { openSettingsTab(); setView('settings') }}>
<button
type="button"
className="top-btn"
data-testid="topbar-settings"
title={t('settings.title')}
onClick={() => {
openSettingsTab()
setView('settings')
}}
>
</button>
<button
+2
View File
@@ -104,6 +104,8 @@ export function initCytoscape(
if (evt.target === cy) onSelect(null)
})
;(container as HTMLElement & { __bilinCy?: cytoscape.Core }).__bilinCy = cy
return cy
}