feat: life-script AI 运行时和视图优化

This commit is contained in:
2026-05-26 20:50:05 +08:00
parent c289097ca0
commit a51d225897
4 changed files with 46 additions and 8 deletions
+43 -5
View File
@@ -18,6 +18,33 @@ const parseSseFrame = (frame) => {
} }
}; };
const findOverlapLength = (current, next) => {
const max = Math.min(current.length, next.length);
for (let size = max; size > 0; size -= 1) {
if (current.slice(-size) === next.slice(0, size)) return size;
}
return 0;
};
const mergeStreamOutput = (current, chunk) => {
const next = String(chunk || '');
if (!next) return { output: current, delta: '' };
if (!current) return { output: next, delta: next };
if (next === current) return { output: current, delta: '' };
if (next.length >= 16 && current.endsWith(next)) return { output: current, delta: '' };
if (next.startsWith(current)) {
return { output: next, delta: next.slice(current.length) };
}
const currentIndex = next.length > current.length ? next.indexOf(current) : -1;
if (currentIndex >= 0) {
return { output: next, delta: next.slice(currentIndex + current.length) };
}
const overlap = findOverlapLength(current, next);
if (overlap < 8) return { output: current + next, delta: next };
const delta = next.slice(overlap);
return { output: current + delta, delta };
};
const authHeaders = () => { const authHeaders = () => {
const token = localStorage.getItem('access_token'); const token = localStorage.getItem('access_token');
return token ? { Authorization: `Bearer ${token}` } : {}; return token ? { Authorization: `Bearer ${token}` } : {};
@@ -73,6 +100,7 @@ export const streamAiScene = async ({
let output = ''; let output = '';
let closed = false; let closed = false;
let recovered = false; let recovered = false;
let streamStarted = false;
let recoveryTimer; let recoveryTimer;
let recoveryPromise; let recoveryPromise;
@@ -92,6 +120,7 @@ export const streamAiScene = async ({
const completeFromRecoveredOutput = async () => { const completeFromRecoveredOutput = async () => {
if (closed) return; if (closed) return;
if (streamStarted || output.trim()) return;
try { try {
const recoveredOutput = await recoverOnce(); const recoveredOutput = await recoverOnce();
if (closed) return; if (closed) return;
@@ -107,7 +136,7 @@ export const streamAiScene = async ({
recoveryTimer = setTimeout(() => { recoveryTimer = setTimeout(() => {
completeFromRecoveredOutput(); completeFromRecoveredOutput();
}, 8000); }, 25000);
const finishRecovered = (event, message) => { const finishRecovered = (event, message) => {
if (!output.trim()) return false; if (!output.trim()) return false;
@@ -127,13 +156,13 @@ export const streamAiScene = async ({
}; };
const recoverOrThrow = async (message, event) => { const recoverOrThrow = async (message, event) => {
if (finishRecovered(event, message)) return;
try { try {
output = await recoverOnce(); output = await recoverOnce();
recovered = true; recovered = true;
closed = true; closed = true;
clearRecoveryTimer(); clearRecoveryTimer();
} catch (error) { } catch (error) {
if (finishRecovered(event, message || error?.message)) return;
const finalMessage = message || error?.message || 'AI 生成结果暂时没有返回'; const finalMessage = message || error?.message || 'AI 生成结果暂时没有返回';
onError?.(finalMessage, event); onError?.(finalMessage, event);
throw new Error(finalMessage); throw new Error(finalMessage);
@@ -148,12 +177,19 @@ export const streamAiScene = async ({
const event = parseSseFrame(frame); const event = parseSseFrame(frame);
if (!event) return; if (!event) return;
if (event.type === 'start') { if (event.type === 'start') {
streamStarted = true;
clearRecoveryTimer();
onStart?.(event); onStart?.(event);
} else if (event.type === 'delta') { } else if (event.type === 'delta') {
const delta = event.content || ''; streamStarted = true;
output += delta; clearRecoveryTimer();
onDelta?.(delta, output, event); const merged = mergeStreamOutput(output, event.content);
output = merged.output;
if (merged.delta) {
onDelta?.(merged.delta, output, event);
}
} else if (event.type === 'done') { } else if (event.type === 'done') {
streamStarted = true;
closed = true; closed = true;
clearRecoveryTimer(); clearRecoveryTimer();
onDone?.(event, output); onDone?.(event, output);
@@ -191,6 +227,8 @@ export const streamAiScene = async ({
while (true) { while (true) {
const { value, done } = await reader.read(); const { value, done } = await reader.read();
if (done) break; if (done) break;
streamStarted = true;
clearRecoveryTimer();
consumeText(decoder.decode(value, { stream: true })); consumeText(decoder.decode(value, { stream: true }));
if (closed || recovered) break; if (closed || recovered) break;
} }
+1 -1
View File
@@ -18,7 +18,7 @@ const PathView = ({ onGoToScript }) => {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [streamPath, setStreamPath] = useState(''); const [streamPath, setStreamPath] = useState('');
const pathWriter = useTypewriterStream({ interval: 18, step: 1 }); const pathWriter = useTypewriterStream({ interval: 30, step: 1 });
const selectedScript = getSelectedScript(); const selectedScript = getSelectedScript();
+1 -1
View File
@@ -40,7 +40,7 @@ const ScriptView = ({ onOpenProfile }) => {
const [length, setLength] = useState(scriptLengths[0].value); const [length, setLength] = useState(scriptLengths[0].value);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [streamContent, setStreamContent] = useState(''); const [streamContent, setStreamContent] = useState('');
const scriptWriter = useTypewriterStream({ interval: 18, step: 1 }); const scriptWriter = useTypewriterStream({ interval: 30, step: 1 });
// 编辑模态框状态 // 编辑模态框状态
const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [isEditModalOpen, setIsEditModalOpen] = useState(false);
+1 -1
View File
@@ -94,7 +94,7 @@ const TimelineView = () => {
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [streamFeedback, setStreamFeedback] = useState(''); const [streamFeedback, setStreamFeedback] = useState('');
const feedbackWriter = useTypewriterStream({ interval: 18, step: 1 }); const feedbackWriter = useTypewriterStream({ interval: 30, step: 1 });
// 编辑模式状态:null 表示新增模式,有值表示编辑模式(存储事件 ID) // 编辑模式状态:null 表示新增模式,有值表示编辑模式(存储事件 ID)
const [editingEventId, setEditingEventId] = useState(null); const [editingEventId, setEditingEventId] = useState(null);