Skip to content

Commit d15354a

Browse files
许君山许君山
authored andcommitted
fix: resolve conjugation edge cases and API race conditions
- Fix Kuru/Godan/Ichidan verb conjugation edge cases (e.g., 来る, 行く, ある, いらっしゃる, くれる) - Add 5s timeout to Jisho API requests in backend to prevent hanging - Use absolute path for Kuromoji dictionary to prevent startup crash - Implement AbortController in frontend to cancel pending AI explanations and prevent race conditions
1 parent b8f165c commit d15354a

3 files changed

Lines changed: 64 additions & 14 deletions

File tree

backend/conjugationEngine.js

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,27 @@ class GodanVerb extends Verb {
2222
}
2323

2424
getDictionaryForm() { return this.dictionaryForm; }
25-
getNegative() { return this.stem + this.mapLastChar('a') + 'ない'; }
26-
getPolite() { return this.stem + this.mapLastChar('i') + 'ます'; }
25+
getNegative() {
26+
if (this.dictionaryForm === 'ある') return 'ない';
27+
return this.stem + this.mapLastChar('a') + 'ない';
28+
}
29+
getPolite() {
30+
if (['いらっしゃる', 'おっしゃる', 'なさる', 'くださる', 'ござる'].includes(this.dictionaryForm)) {
31+
return this.stem + 'い' + 'ます';
32+
}
33+
return this.stem + this.mapLastChar('i') + 'ます';
34+
}
2735
getTeForm() { return this.stem + this.getTeTaSuffix(true); }
2836
getTaForm() { return this.stem + this.getTeTaSuffix(false); }
2937
getPotential() { return this.stem + this.mapLastChar('e') + 'る'; }
3038
getPassive() { return this.stem + this.mapLastChar('a') + 'れる'; }
3139
getCausative() { return this.stem + this.mapLastChar('a') + 'せる'; }
32-
getImperative() { return this.stem + this.mapLastChar('e'); }
40+
getImperative() {
41+
if (['いらっしゃる', 'おっしゃる', 'なさる', 'くださる'].includes(this.dictionaryForm)) {
42+
return this.stem + 'い';
43+
}
44+
return this.stem + this.mapLastChar('e');
45+
}
3346
getVolitional() { return this.stem + this.mapLastChar('o') + 'う'; }
3447

3548
mapLastChar(row) {
@@ -49,7 +62,7 @@ class GodanVerb extends Verb {
4962
}
5063

5164
getTeTaSuffix(isTe) {
52-
if (this.dictionaryForm === '行く' || this.dictionaryForm === 'いく') {
65+
if (this.dictionaryForm.endsWith('行く') || this.dictionaryForm.endsWith('いく')) {
5366
return isTe ? 'って' : 'った';
5467
}
5568
const suffixMap = {
@@ -84,7 +97,10 @@ class IchidanVerb extends Verb {
8497
getPotential() { return this.stem + 'られる'; }
8598
getPassive() { return this.stem + 'られる'; }
8699
getCausative() { return this.stem + 'させる'; }
87-
getImperative() { return this.stem + 'ろ'; }
100+
getImperative() {
101+
if (this.dictionaryForm === 'くれる') return 'くれ';
102+
return this.stem + 'ろ';
103+
}
88104
getVolitional() { return this.stem + 'よう'; }
89105
}
90106

@@ -113,9 +129,12 @@ class KuruVerb extends Verb {
113129
constructor(dictionaryForm) {
114130
super();
115131
this.dictionaryForm = dictionaryForm;
116-
// 判断是否包含汉字「来」:「来る」→ prefix='来', 「くる」→ prefix=''
117-
this.hasKanji = dictionaryForm.includes('来');
118-
this.prefix = this.hasKanji ? dictionaryForm.slice(0, dictionaryForm.indexOf('来') + 1) : '';
132+
this.hasKanji = dictionaryForm.endsWith('来る');
133+
if (this.hasKanji) {
134+
this.prefix = dictionaryForm.slice(0, -2) + '来';
135+
} else {
136+
this.prefix = dictionaryForm.slice(0, -2);
137+
}
119138
}
120139

121140
getDictionaryForm() { return this.dictionaryForm; }

backend/server.js

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const commonVerbs = JSON.parse(fs.readFileSync(path.join(__dirname, 'common-verb
1919
// 调用 Jisho API 获取词汇(支持全词类)
2020
function searchJisho(keyword, verbOnly = true) {
2121
return new Promise((resolve, reject) => {
22-
https.get(`https://jisho.org/api/v1/search/words?keyword=${encodeURIComponent(keyword)}`, (res) => {
22+
const req = https.get(`https://jisho.org/api/v1/search/words?keyword=${encodeURIComponent(keyword)}`, (res) => {
2323
let data = '';
2424
res.on('data', chunk => data += chunk);
2525
res.on('end', () => {
@@ -68,15 +68,20 @@ function searchJisho(keyword, verbOnly = true) {
6868
reject(e);
6969
}
7070
});
71-
}).on('error', reject);
71+
});
72+
req.on('error', reject);
73+
req.setTimeout(5000, () => {
74+
req.destroy();
75+
reject(new Error('Jisho API timeout'));
76+
});
7277
});
7378
}
7479

7580
// 查询单个词的详细信息(用于非动词查词)
7681
function lookupWordJisho(keyword) {
7782
return new Promise((resolve, reject) => {
7883
const url = `https://jisho.org/api/v1/search/words?keyword=${encodeURIComponent(keyword)}`;
79-
https.get(url, (res) => {
84+
const req = https.get(url, (res) => {
8085
let data = '';
8186
res.on('data', chunk => data += chunk);
8287
res.on('end', () => {
@@ -127,7 +132,12 @@ function lookupWordJisho(keyword) {
127132
reject(e);
128133
}
129134
});
130-
}).on('error', reject);
135+
});
136+
req.on('error', reject);
137+
req.setTimeout(5000, () => {
138+
req.destroy();
139+
reject(new Error('Jisho API timeout'));
140+
});
131141
});
132142
}
133143

@@ -146,7 +156,8 @@ app.use(express.json());
146156
let tokenizer = null;
147157

148158
// 初始化 Kuromoji 分词器
149-
kuromoji.builder({ dicPath: 'node_modules/kuromoji/dict' }).build((err, _tokenizer) => {
159+
const dicPath = path.join(__dirname, 'node_modules/kuromoji/dict');
160+
kuromoji.builder({ dicPath }).build((err, _tokenizer) => {
150161
if (err) {
151162
console.error('Failed to build Kuromoji tokenizer:', err);
152163
} else {

frontend/src/App.vue

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,12 @@ onUnmounted(() => {
640640
});
641641
642642
const conjugate = async () => {
643+
if (loading.value) return;
644+
if (aiAbortController) {
645+
aiAbortController.abort();
646+
aiAbortController = null;
647+
}
648+
loadingAi.value = false;
643649
error.value = '';
644650
result.value = null;
645651
aiRawExplanation.value = '';
@@ -690,10 +696,17 @@ const conjugate = async () => {
690696
}
691697
};
692698
699+
let aiAbortController = null;
700+
693701
const fetchAiExplanation = async () => {
694702
const wordName = result.value?.dictionaryForm || result.value?.word;
695703
if (!wordName) return;
696704
705+
if (aiAbortController) {
706+
aiAbortController.abort();
707+
}
708+
aiAbortController = new AbortController();
709+
697710
const currentWordType = result.value?.wordType || 'verb';
698711
const currentIsVerb = currentWordType === 'verb';
699712
@@ -710,6 +723,7 @@ const fetchAiExplanation = async () => {
710723
headers: {
711724
'Content-Type': 'application/json'
712725
},
726+
signal: aiAbortController.signal,
713727
body: JSON.stringify({
714728
verb: wordName,
715729
model: selectedModel.value || 'qwen2.5:7b',
@@ -826,10 +840,16 @@ const fetchAiExplanation = async () => {
826840
}
827841
}
828842
} catch (err) {
843+
if (err.name === 'AbortError') return;
829844
aiError.value = err.message || 'AI 解析请求失败';
830845
completeProgress();
831846
} finally {
832-
loadingAi.value = false;
847+
if (aiAbortController && !aiAbortController.signal.aborted) {
848+
loadingAi.value = false;
849+
aiAbortController = null;
850+
} else if (!aiAbortController) {
851+
loadingAi.value = false;
852+
}
833853
}
834854
};
835855
</script>

0 commit comments

Comments
 (0)