Stabilize pro editing without heavy office runtime

The workspace now carries the pro editing flow, streaming completion path, and lighter Office preview state as one checkpoint so the remote has the current runnable project shape.

Constraint: Preserve the current workspace as a single reviewable project commit while excluding local agent state and verification artifacts. Removed stale Univer runtime dependencies from the lockfile so installs match package.json.

Rejected: Commit runtime screenshots, .omx state, and coverage files | they are local artifacts rather than source state.

Confidence: medium

Scope-risk: broad

Directive: Keep package.json and package-lock.json synchronized when changing frontend dependencies.

Tested: npm run build; C:\Users\ydy\.conda\envs\llmwebsite\python.exe -m pytest backend/tests/test_main_endpoints.py backend/tests/test_main_cancel.py backend/tests/test_llm.py backend/tests/test_llm_extended.py -v -o addopts= (44 passed).

Not-tested: Full pytest with repository coverage addopts currently reports 0% coverage because pytest-cov watches backend.* module names while tests import top-level backend modules.

Co-authored-by: OmX <omx@oh-my-codex.dev>
This commit is contained in:
2026-05-24 23:30:32 +08:00
parent 6dc9933853
commit 59334e4057
41 changed files with 4438 additions and 4875 deletions
+177 -13
View File
@@ -1,4 +1,4 @@
import { API_URL, API_KEY, TTS_URL, TTS_STATUS_URL, TTS_CONFIG_URL } from './config.js'
import { API_URL, API_KEY, PRO_STREAM_URL, TTS_URL, TTS_STATUS_URL, TTS_CONFIG_URL } from './config.js'
import { useSettingsStore } from '../stores/settings'
function generateRequestId() {
@@ -11,6 +11,9 @@ function generateRequestId() {
function getCancelUrl(apiUrl) {
const normalized = String(apiUrl || '').replace(/\/+$/, '')
if (!normalized) return '/v1/completions/cancel'
if (/\/v1\/pro\/completions\/stream$/i.test(normalized)) {
return normalized.replace(/\/v1\/pro\/completions\/stream$/i, '/v1/completions/cancel')
}
if (normalized.endsWith('/v1/completions')) {
return `${normalized}/cancel`
}
@@ -42,6 +45,50 @@ async function sendCancelRequest(cancelUrl, requestId, reason) {
}
}
function createAbortError(message = 'Request aborted') {
const error = new Error(message)
error.name = 'AbortError'
return error
}
function buildCompletionBody(settings, prefix, suffix, languageId, extra = {}) {
return {
prefix,
suffix,
languageId,
model_thinking: settings.modelThinking,
privacy_mode: settings.privacyMode,
user_preferences: {
language: settings.language,
currency: settings.currency,
timezone: settings.detectedTimezone,
},
...extra,
}
}
function parseSseEvent(rawEvent) {
const lines = String(rawEvent || '').replace(/\r/g, '').split('\n')
let event = 'message'
const dataLines = []
for (const line of lines) {
if (!line) continue
if (line.startsWith('event:')) {
event = line.slice(6).trim() || 'message'
continue
}
if (line.startsWith('data:')) {
dataLines.push(line.slice(5).trimStart())
}
}
return {
event,
data: dataLines.join('\n'),
}
}
export async function fetchSuggestion(prefix, suffix, languageId, signal, apiUrl = API_URL) {
let normalizedLanguageId = 'markdown'
if (typeof languageId === 'string' && languageId.trim()) {
@@ -77,18 +124,7 @@ export async function fetchSuggestion(prefix, suffix, languageId, signal, apiUrl
'X-API-Key': API_KEY,
}
const body = {
prefix,
suffix,
languageId: normalizedLanguageId,
model_thinking: settings.modelThinking,
privacy_mode: settings.privacyMode,
user_preferences: {
language: settings.language,
currency: settings.currency,
timezone: settings.detectedTimezone,
},
}
const body = buildCompletionBody(settings, prefix, suffix, normalizedLanguageId)
const res = await fetch(apiUrl, {
method: 'POST',
@@ -117,6 +153,134 @@ export async function fetchSuggestion(prefix, suffix, languageId, signal, apiUrl
}
}
export async function fetchProSuggestionStream(payload, apiUrl = PRO_STREAM_URL) {
const {
prefix = '',
suffix = '',
languageId = 'markdown',
signal,
model = '',
temperature = 0.7,
timeoutMs = 600000,
onChunk,
} = payload || {}
const settings = useSettingsStore()
const requestId = generateRequestId()
const cancelUrl = getCancelUrl(apiUrl)
const requestController = new AbortController()
const timeoutId = setTimeout(() => {
requestController.abort('timeout')
}, timeoutMs)
const relayAbort = () => {
requestController.abort(signal?.reason || 'abort')
}
const onAbort = () => {
const reason = normalizeAbortReason(requestController.signal.reason)
void sendCancelRequest(cancelUrl, requestId, reason)
}
requestController.signal.addEventListener('abort', onAbort, { once: true })
if (signal) {
if (signal.aborted) {
relayAbort()
} else {
signal.addEventListener('abort', relayAbort, { once: true })
}
}
try {
const res = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Request-Id': requestId,
'X-API-Key': API_KEY,
},
body: JSON.stringify(
buildCompletionBody(settings, prefix, suffix, String(languageId || 'markdown').trim() || 'markdown', {
model,
temperature,
})
),
signal: requestController.signal,
})
if (!res.ok) {
const errorText = await res.text()
throw new Error(`HTTP ${res.status}: ${errorText}`)
}
if (!res.body) {
throw new Error('PRO 模式流式响应不可用')
}
const reader = res.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
let finalContent = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
let boundary = buffer.indexOf('\n\n')
while (boundary >= 0) {
const chunk = buffer.slice(0, boundary)
buffer = buffer.slice(boundary + 2)
const parsed = parseSseEvent(chunk)
if (parsed.event === 'chunk' && parsed.data) {
const data = JSON.parse(parsed.data)
const delta = String(data.delta || '')
if (delta) {
finalContent += delta
onChunk?.(delta)
}
}
if (parsed.event === 'done' && parsed.data) {
const data = JSON.parse(parsed.data)
return String(data.content || finalContent || '')
}
if (parsed.event === 'error' && parsed.data) {
const data = JSON.parse(parsed.data)
throw new Error(String(data.error || 'PRO 模式请求失败'))
}
if (parsed.event === 'cancelled') {
throw createAbortError('PRO 模式请求已取消')
}
boundary = buffer.indexOf('\n\n')
}
}
if (requestController.signal.aborted) {
throw createAbortError('PRO 模式请求已中止')
}
return finalContent
} catch (e) {
if (e?.name === 'AbortError') {
throw e
}
throw e
} finally {
clearTimeout(timeoutId)
requestController.signal.removeEventListener('abort', onAbort)
if (signal) {
signal.removeEventListener('abort', relayAbort)
}
}
}
export async function fetchTTS(text, instruct = '', apiUrl = TTS_URL) {
const res = await fetch(apiUrl, {
method: 'POST',