Skip to content

Commit cf5e923

Browse files
committed
修复一些bug
1 parent 223b414 commit cf5e923

12 files changed

Lines changed: 314 additions & 59 deletions

File tree

.github/workflows/tauri-bundles.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ jobs:
395395
396396
tail -n 200 /tmp/lessai-appimage-smoke.log || true
397397
398-
if grep -Eqi "(segmentation fault|core dumped|EGL_BAD_ALLOC|HandshakeFailure)" /tmp/lessai-appimage-smoke.log; then
398+
if grep -Eqi "(segmentation fault|core dumped|EGL_BAD_ALLOC|HandshakeFailure|GStreamer|appsink|gst-plugin-scanner|libgstapp)" /tmp/lessai-appimage-smoke.log; then
399399
echo "[ERROR] AppImage smoke test detected fatal runtime signature." >&2
400400
exit 1
401401
fi

scripts/install-tauri-linux-deps.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ bundle_extra_packages=(
3535
libssl-dev
3636
rpm
3737
xvfb
38+
gstreamer1.0-plugins-base
39+
gstreamer1.0-tools
3840
)
3941

4042
if [[ "${mode}" == "bundle" ]]; then

scripts/packaging-regression.test.mjs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@ import { fileURLToPath } from "node:url";
66
const root = fileURLToPath(new URL("..", import.meta.url));
77
const tauriConfigPath = resolve(root, "src-tauri/tauri.conf.json");
88
const tauriBundlesWorkflowPath = resolve(root, ".github/workflows/tauri-bundles.yml");
9+
const appImagePatchPath = resolve(root, "scripts/patch-linux-appimage.sh");
10+
const linuxDepsPath = resolve(root, "scripts/install-tauri-linux-deps.sh");
911
const tauriConfig = JSON.parse(readFileSync(tauriConfigPath, "utf8"));
1012
const tauriBundlesWorkflow = readFileSync(tauriBundlesWorkflowPath, "utf8");
13+
const appImagePatch = readFileSync(appImagePatchPath, "utf8");
14+
const linuxDeps = readFileSync(linuxDepsPath, "utf8");
1115

1216
assert.ok(Array.isArray(tauriConfig.bundle?.icon), "bundle.icon 必须为数组");
1317
assert.ok(tauriConfig.bundle.icon.length > 0, "bundle.icon 不能为空");
@@ -49,4 +53,24 @@ assert.ok(
4953
"发布流程缺少 DOCX/PDF 安全优先写回口径"
5054
);
5155

56+
for (const expected of [
57+
"libgstapp.so",
58+
"gst-plugin-scanner",
59+
"GST_PLUGIN_SYSTEM_PATH_1_0",
60+
"GST_PLUGIN_SCANNER_1_0"
61+
]) {
62+
assert.ok(appImagePatch.includes(expected), `AppImage 补丁缺少 GStreamer 配置:${expected}`);
63+
}
64+
65+
for (const expected of ["GStreamer", "appsink", "gst-plugin-scanner", "libgstapp"]) {
66+
assert.ok(
67+
tauriBundlesWorkflow.includes(expected),
68+
`AppImage smoke test 缺少 GStreamer 致命特征匹配:${expected}`
69+
);
70+
}
71+
72+
for (const expected of ["gstreamer1.0-plugins-base", "gstreamer1.0-tools"]) {
73+
assert.ok(linuxDeps.includes(expected), `Linux 打包依赖缺少 GStreamer 包:${expected}`);
74+
}
75+
5276
console.log("[packaging-regression] OK");

scripts/patch-linux-appimage.sh

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,60 @@ if [[ -n "$WEBKIT_SRC" ]]; then
7878
cp -a "$WEBKIT_SRC/." "$APPDIR/usr/lib/x86_64-linux-gnu/webkit2gtk-4.1/"
7979
fi
8080

81+
# Ensure GStreamer can discover the app plugin that provides appsink.
82+
GSTREAMER_APPDIR="$APPDIR/usr/lib/gstreamer-1.0"
83+
GSTREAMER_APP_PLUGIN=""
84+
for candidate in \
85+
/usr/lib/x86_64-linux-gnu/gstreamer-1.0/libgstapp.so \
86+
/usr/lib/gstreamer-1.0/libgstapp.so \
87+
/usr/lib64/gstreamer-1.0/libgstapp.so; do
88+
if [[ -f "$candidate" ]]; then
89+
GSTREAMER_APP_PLUGIN="$candidate"
90+
break
91+
fi
92+
done
93+
if [[ -z "$GSTREAMER_APP_PLUGIN" ]]; then
94+
echo "[ERROR] GStreamer app plugin not found (libgstapp.so). Install gstreamer1.0-plugins-base." >&2
95+
exit 1
96+
fi
97+
98+
GSTREAMER_PLUGIN_SCANNER=""
99+
for candidate in \
100+
/usr/lib/x86_64-linux-gnu/gstreamer1.0/gstreamer-1.0/gst-plugin-scanner \
101+
/usr/lib/gstreamer1.0/gstreamer-1.0/gst-plugin-scanner \
102+
/usr/lib64/gstreamer1.0/gstreamer-1.0/gst-plugin-scanner \
103+
/usr/libexec/gstreamer-1.0/gst-plugin-scanner \
104+
/usr/lib/gstreamer-1.0/gst-plugin-scanner; do
105+
if [[ -x "$candidate" ]]; then
106+
GSTREAMER_PLUGIN_SCANNER="$candidate"
107+
break
108+
fi
109+
done
110+
if [[ -z "$GSTREAMER_PLUGIN_SCANNER" ]]; then
111+
echo "[ERROR] GStreamer plugin scanner not found. Install libgstreamer1.0-0 or gstreamer1.0-tools." >&2
112+
exit 1
113+
fi
114+
115+
install -d "$GSTREAMER_APPDIR"
116+
cp -a "$GSTREAMER_APP_PLUGIN" "$GSTREAMER_APPDIR/libgstapp.so"
117+
cp -a "$GSTREAMER_PLUGIN_SCANNER" "$GSTREAMER_APPDIR/gst-plugin-scanner"
118+
chmod +x "$GSTREAMER_APPDIR/gst-plugin-scanner"
119+
echo "[INFO] Bundled GStreamer app plugin: $GSTREAMER_APP_PLUGIN"
120+
echo "[INFO] Bundled GStreamer plugin scanner: $GSTREAMER_PLUGIN_SCANNER"
121+
122+
for candidate in \
123+
/usr/lib/x86_64-linux-gnu/gstreamer1.0/gstreamer-1.0/gst-ptp-helper \
124+
/usr/lib/gstreamer1.0/gstreamer-1.0/gst-ptp-helper \
125+
/usr/lib64/gstreamer1.0/gstreamer-1.0/gst-ptp-helper \
126+
/usr/libexec/gstreamer-1.0/gst-ptp-helper; do
127+
if [[ -x "$candidate" ]]; then
128+
cp -a "$candidate" "$GSTREAMER_APPDIR/gst-ptp-helper"
129+
chmod +x "$GSTREAMER_APPDIR/gst-ptp-helper"
130+
echo "[INFO] Bundled GStreamer PTP helper: $candidate"
131+
break
132+
fi
133+
done
134+
81135
# Compatibility links for runtimes that search under AppDir/lib*.
82136
install -d "$APPDIR/lib" "$APPDIR/lib64"
83137
ln -sfn ../usr/lib/x86_64-linux-gnu "$APPDIR/lib/x86_64-linux-gnu"
@@ -119,6 +173,13 @@ fi
119173
# AppImage defaults to safe mode when unset; users can override via LESSAI_LINUX_GRAPHICS_MODE.
120174
export LESSAI_LINUX_GRAPHICS_MODE="${LESSAI_LINUX_GRAPHICS_MODE:-safe}"
121175
176+
GSTREAMER_PLUGIN_DIR="$APPDIR/usr/lib/gstreamer-1.0"
177+
export GST_REGISTRY_REUSE_PLUGIN_SCANNER="no"
178+
export GST_PLUGIN_SYSTEM_PATH_1_0="$GSTREAMER_PLUGIN_DIR"
179+
export GST_PLUGIN_PATH_1_0="$GSTREAMER_PLUGIN_DIR"
180+
export GST_PLUGIN_SCANNER_1_0="$GSTREAMER_PLUGIN_DIR/gst-plugin-scanner"
181+
export GST_PLUGIN_SCANNER="$GSTREAMER_PLUGIN_DIR/gst-plugin-scanner"
182+
122183
WEBKIT_BASE=""
123184
if [[ "${LESSAI_FORCE_BUNDLED_WEBKIT:-0}" == "1" ]]; then
124185
WEBKIT_CANDIDATES=(

scripts/ui-regression.test.mjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,11 @@ const structuredSlotEditorShared = read("src/stages/workbench/document/structure
168168
const documentEditorTypes = read("src/stages/workbench/document/documentEditorTypes.ts");
169169
const selectionDecorationOverlay = read("src/stages/workbench/document/SelectionDecorationOverlay.tsx");
170170
const selectionDecorationHook = read("src/stages/workbench/document/useSelectionDecorationRects.ts");
171+
const busyActionHook = read("src/hooks/useBusyAction.ts");
171172
const workspaceBar = read("src/app/components/WorkspaceBar.tsx");
172173
const helpersSource = read("src/lib/helpers.ts");
174+
const webBridgeAutoJobRuntime = read("src/lib/webBridgeAutoJobRuntime.ts");
175+
const webBridgeModelApi = read("src/lib/webBridgeModelApi.ts");
173176
const textNormalize = read("src/lib/textNormalize.ts");
174177
const slotText = read("src/lib/slotText.ts");
175178
const webBridgeTextCore = read("src/lib/webBridgeTextCore.ts");
@@ -333,6 +336,15 @@ assertIncludes(structuredSlotEditor, "useSelectionDecorationRects({");
333336
assertNotIncludes(documentFlow, 'document.addEventListener("selectionchange"');
334337
assertNotIncludes(plainTextDocumentEditor, 'document.addEventListener("selectionchange"');
335338
assertNotIncludes(structuredSlotEditor, 'document.addEventListener("selectionchange"');
339+
assertIncludes(selectionDecorationHook, "cancelAnimationFrame(frameRef.current)");
340+
assertIncludes(busyActionHook, "Promise.resolve().then(operation)");
341+
assertIncludes(webBridgeAutoJobRuntime, "function isCurrentJob");
342+
assertIncludes(webBridgeAutoJobRuntime, "deps.autoJobs.get(sessionId) === job");
343+
assertIncludes(webBridgeAutoJobRuntime, "stopCurrentJob(sessionId, job)");
344+
assertIncludes(webBridgeAutoJobRuntime, "if (!activeSession || !isCurrentJob(sessionId, job))");
345+
assertIncludes(webBridgeModelApi, "function createModelRequestSignal");
346+
assertIncludes(webBridgeModelApi, "settings.timeoutMs");
347+
assertIncludes(webBridgeModelApi, "if (json == null)");
336348
assertIncludes(startLessaiBat, 'call "scripts\\lessai-windows-common.bat" ensure_deps');
337349
assertIncludes(buildLessaiBat, 'call "scripts\\lessai-windows-common.bat" ensure_deps');
338350
assertIncludes(windowsCommonBat, ":ensure_deps");

src-tauri/src/rewrite/llm/transport.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,12 @@ fn parse_json_chat_response_body(body: &str) -> Result<String, String> {
147147
fn body_preview(body: &str) -> String {
148148
let trimmed = body.trim();
149149
let limit = 300usize;
150-
if trimmed.len() <= limit {
150+
let char_count = trimmed.chars().count();
151+
if char_count <= limit {
151152
trimmed.to_string()
152153
} else {
153-
let preview = &trimmed[..limit];
154-
format!("{preview}…(共 {} 字符)", trimmed.len())
154+
let preview = trimmed.chars().take(limit).collect::<String>();
155+
format!("{preview}…(共 {char_count} 字符)")
155156
}
156157
}
157158

@@ -381,3 +382,17 @@ fn extract_text_field(value: &Value) -> Option<String> {
381382

382383
None
383384
}
385+
386+
#[cfg(test)]
387+
mod tests {
388+
use super::body_preview;
389+
390+
#[test]
391+
fn body_preview_truncates_utf8_without_panicking() {
392+
let body = "中文🙂".repeat(120);
393+
let preview = body_preview(&body);
394+
395+
assert!(preview.ends_with("…(共 360 字符)"));
396+
assert!(preview.starts_with("中文🙂中文🙂"));
397+
}
398+
}

src/app/hooks/useRewriteActions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ export function useRewriteActions(options: {
370370

371371
const handleRetry = useCallback(async () => {
372372
const session = currentSessionRef.current;
373-
const rewriteUnit = findRewriteUnit(session ?? null as never, activeRewriteUnitIdRef.current);
373+
const rewriteUnit = findRewriteUnit(session, activeRewriteUnitIdRef.current);
374374
if (!session || !rewriteUnit) return;
375375
const latestSession = await refreshRewriteableSessionOrNotify({
376376
session,

src/hooks/useBusyAction.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ export function useBusyAction() {
2121
busyActionRef.current = actionKey;
2222
setBusyAction(actionKey);
2323

24-
const promise = operation();
25-
pendingPromiseRef.current = promise as unknown as Promise<unknown>;
24+
const promise = Promise.resolve().then(operation);
25+
pendingPromiseRef.current = promise;
2626

2727
try {
2828
return await promise;

src/lib/helpers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,10 @@ export function rewriteUnitHasEditableSlot(
190190
}
191191

192192
export function findRewriteUnit(
193-
session: DocumentSession,
193+
session: DocumentSession | null | undefined,
194194
rewriteUnitId: string | null | undefined
195195
) {
196-
if (!rewriteUnitId) return null;
196+
if (!session || !rewriteUnitId) return null;
197197
return session.rewriteUnits.find((item) => item.id === rewriteUnitId) ?? null;
198198
}
199199

src/lib/webBridgeAutoJobRuntime.ts

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,28 @@ function sleep(ms: number) {
5454
}
5555

5656
export function createAutoJobRuntime(deps: AutoJobRuntimeDeps) {
57+
function isCurrentJob(sessionId: string, job: WebAutoJob) {
58+
return deps.autoJobs.get(sessionId) === job;
59+
}
60+
61+
function deleteCurrentJob(sessionId: string, job: WebAutoJob) {
62+
if (isCurrentJob(sessionId, job)) {
63+
deps.autoJobs.delete(sessionId);
64+
}
65+
}
66+
67+
function stopCurrentJob(sessionId: string, job: WebAutoJob) {
68+
if (!isCurrentJob(sessionId, job)) {
69+
return false;
70+
}
71+
job.queue = [];
72+
for (const controller of job.controllers.values()) {
73+
controller.abort();
74+
}
75+
deps.autoJobs.delete(sessionId);
76+
return true;
77+
}
78+
5779
async function emitRewriteProgress(
5880
session: DocumentSession,
5981
job: WebAutoJob,
@@ -110,9 +132,13 @@ export function createAutoJobRuntime(deps: AutoJobRuntimeDeps) {
110132
}
111133

112134
while (true) {
135+
if (!isCurrentJob(sessionId, job)) {
136+
return;
137+
}
138+
113139
const session = deps.sessions.get(sessionId);
114140
if (!session) {
115-
deps.autoJobs.delete(sessionId);
141+
deleteCurrentJob(sessionId, job);
116142
return;
117143
}
118144

@@ -122,7 +148,7 @@ export function createAutoJobRuntime(deps: AutoJobRuntimeDeps) {
122148
deps.updateSessionTimestamp(session);
123149
await emitRewriteProgress(session, job, "cancelled");
124150
await emitRewriteFinished(sessionId);
125-
deps.autoJobs.delete(sessionId);
151+
deleteCurrentJob(sessionId, job);
126152
return;
127153
}
128154

@@ -152,13 +178,15 @@ export function createAutoJobRuntime(deps: AutoJobRuntimeDeps) {
152178
job.controllers.set(token, controller);
153179
void emitRewriteProgress(session, job, "running");
154180

155-
const promise = deps
156-
.processRewriteBatch({
157-
session,
158-
rewriteUnitIds,
159-
autoApprove: true,
160-
signal: controller.signal
161-
})
181+
const promise = Promise.resolve()
182+
.then(() =>
183+
deps.processRewriteBatch({
184+
session,
185+
rewriteUnitIds,
186+
autoApprove: true,
187+
signal: controller.signal
188+
})
189+
)
162190
.then(
163191
(completed): WebAutoBatchResult => ({
164192
kind: "success",
@@ -183,7 +211,7 @@ export function createAutoJobRuntime(deps: AutoJobRuntimeDeps) {
183211
void (async () => {
184212
const result = await promise;
185213
const activeSession = deps.sessions.get(sessionId);
186-
if (!activeSession) {
214+
if (!activeSession || !isCurrentJob(sessionId, job)) {
187215
return;
188216
}
189217
if (result.kind === "cancelled") {
@@ -193,23 +221,29 @@ export function createAutoJobRuntime(deps: AutoJobRuntimeDeps) {
193221
}
194222
if (result.kind === "failed") {
195223
deps.markBatchFailure(activeSession, result.rewriteUnitIds, result.error);
224+
stopCurrentJob(sessionId, job);
196225
await emitRewriteFailed(sessionId, result.error);
197-
deps.autoJobs.delete(sessionId);
198226
return;
199227
}
200228
job.completedUnits += result.completed.length;
201229
await emitRewriteUnitCompleted(sessionId, result.completed);
202-
await emitRewriteProgress(activeSession, job, "running");
230+
if (isCurrentJob(sessionId, job)) {
231+
await emitRewriteProgress(activeSession, job, "running");
232+
}
203233
})();
204234
}
205235

236+
if (!isCurrentJob(sessionId, job)) {
237+
return;
238+
}
239+
206240
if (job.controllers.size === 0 && job.queue.length === 0) {
207241
deps.clearRunningUnits(session);
208242
session.status = deps.computeSessionState(session);
209243
deps.updateSessionTimestamp(session);
210244
await emitRewriteProgress(session, job, session.status);
211245
await emitRewriteFinished(sessionId);
212-
deps.autoJobs.delete(sessionId);
246+
deleteCurrentJob(sessionId, job);
213247
return;
214248
}
215249

0 commit comments

Comments
 (0)