@@ -15,7 +15,35 @@ const __dirname = path.dirname(__filename);
1515// 读取动词库
1616const commonVerbs = JSON . parse ( fs . readFileSync ( path . join ( __dirname , 'common-verbs.json' ) , 'utf8' ) ) ;
1717
18- // 调用 Jisho API 获取更多动词补充
18+ // 调用 Jisho API 获取汉字(如果 kuromoji 没有汉字的话)
19+ function getKanjiFromJisho ( verb ) {
20+ return new Promise ( ( resolve ) => {
21+ https . get ( `https://jisho.org/api/v1/search/words?keyword=${ encodeURIComponent ( verb ) } ` , ( res ) => {
22+ let data = '' ;
23+ res . on ( 'data' , chunk => data += chunk ) ;
24+ res . on ( 'end' , ( ) => {
25+ try {
26+ const parsed = JSON . parse ( data ) ;
27+ if ( parsed . data && parsed . data . length > 0 ) {
28+ for ( const item of parsed . data ) {
29+ if ( item . japanese && item . japanese . length > 0 ) {
30+ // 检查这个词是否能匹配我们输入的读音
31+ const reading = item . japanese [ 0 ] . reading ;
32+ const word = item . japanese [ 0 ] . word ;
33+ if ( reading === verb && word ) {
34+ return resolve ( word ) ;
35+ }
36+ }
37+ }
38+ }
39+ resolve ( verb ) ; // 没找到合适的汉字,返回原词
40+ } catch ( e ) {
41+ resolve ( verb ) ;
42+ }
43+ } ) ;
44+ } ) . on ( 'error' , ( ) => resolve ( verb ) ) ;
45+ } ) ;
46+ }
1947function searchJisho ( keyword ) {
2048 return new Promise ( ( resolve , reject ) => {
2149 https . get ( `https://jisho.org/api/v1/search/words?keyword=${ encodeURIComponent ( keyword ) } ` , ( res ) => {
@@ -90,10 +118,27 @@ function detectVerbType(verb) {
90118
91119 // 特殊情况硬编码:カ变动词(来る / くる)
92120 if ( hiraganaVerb === 'くる' || hiraganaVerb === '来る' ) {
93- return 'KURU' ;
121+ return { type : 'KURU' , basicForm : '来る' } ;
122+ }
123+ // 特殊情况硬编码:サ变动词(する)
124+ if ( hiraganaVerb === 'する' ) {
125+ return { type : 'SURU' , basicForm : 'する' } ;
126+ }
127+
128+ // 尝试分词,首先用转换后的平假名
129+ let tokens = tokenizer . tokenize ( hiraganaVerb ) ;
130+
131+ // 如果输入包含汉字且能被正确分词,则使用原输入以保留汉字
132+ // 但我们需要确保它是一个有效的动词
133+ const originalTokens = tokenizer . tokenize ( verb ) ;
134+ if ( originalTokens . length > 0 ) {
135+ const originalVerbToken = originalTokens . slice ( ) . reverse ( ) . find ( t => t . pos === '動詞' ) ;
136+ if ( originalVerbToken && originalVerbToken . conjugated_form === '基本形' ) {
137+ // 如果原输入(可能包含汉字)能被正确解析为基本形动词,则优先使用它
138+ tokens = originalTokens ;
139+ }
94140 }
95141
96- const tokens = tokenizer . tokenize ( hiraganaVerb ) ;
97142 if ( tokens . length === 0 ) return null ;
98143
99144 // 对于像 勉強する 这样的词,动词部分在最后
@@ -103,23 +148,51 @@ function detectVerbType(verb) {
103148 if ( ! verbToken ) return null ;
104149
105150 // 严格匹配:确保输入的整个词就是一个动词,或者是以动词结尾的复合词(如勉強する)
106- // 避免像 "tebe" 这种无意义的词被拆分成助词,或者被错误地当作动词的一部分
107- // 检查提取出的动词原形(basic_form)是否能和输入的词(或其后缀)对得上
108- // 因为像 `tabe` 提取出来 basic_form 是 `たべる`,如果输入只有 `tabe` 就不完整
109- // 如果是复合动词如 `勉強する`,verbToken.surface_form 会是 `する`
110- if ( ! hiraganaVerb . endsWith ( verbToken . surface_form ) ) {
151+ // 注意:如果是复合动词,basic_form 可能只包含动词部分(如 する),需要特殊处理
152+ const surfaceMatches = verb . endsWith ( verbToken . surface_form ) || hiraganaVerb . endsWith ( verbToken . surface_form ) ;
153+
154+ if ( ! surfaceMatches ) {
111155 return null ;
112156 }
157+
113158 // 还需要检查提取出的动词是否是一个完整的字典形(基本形)
114159 if ( verbToken . conjugated_form !== '基本形' ) {
115160 return null ;
116161 }
117162
118163 const cType = verbToken . conjugated_type ;
119- if ( cType . includes ( '一段' ) ) return 'ICHIDAN' ;
120- if ( cType . includes ( '五段' ) ) return 'GODAN' ;
121- if ( cType . includes ( 'サ変' ) ) return 'SURU' ;
122- if ( cType . includes ( 'カ変' ) ) return 'KURU' ;
164+
165+ // 构建包含汉字的完整基本形
166+ // 如果是复合动词(如 勉強する),需要把前面的名词部分拼起来
167+ let fullBasicForm = verbToken . basic_form ;
168+ if ( tokens . length > 1 ) {
169+ // 找到动词前的名词部分
170+ const nounTokens = tokens . slice ( 0 , tokens . indexOf ( verbToken ) ) ;
171+ const prefix = nounTokens . map ( t => t . surface_form ) . join ( '' ) ;
172+ // 只有当输入的原始字符串包含这个前缀时,才拼起来
173+ if ( verb . startsWith ( prefix ) || hiraganaVerb . startsWith ( wanakana . toHiragana ( prefix ) ) ) {
174+ // 如果原输入是以汉字开头的(如 勉強),就用原输入的汉字部分
175+ const originalPrefix = verb . substring ( 0 , prefix . length ) ;
176+ fullBasicForm = originalPrefix + verbToken . basic_form ;
177+ }
178+ } else if ( verbToken . surface_form === verbToken . basic_form ) {
179+ // 如果 surface_form 和 basic_form 一样,尽量使用输入的表面形式(如果输入是汉字的话)
180+ // 比如输入 食べる,verbToken.basic_form 可能是 食べる,也可能是 たべる
181+ // 我们倾向于保留用户输入的汉字
182+ if ( wanakana . toHiragana ( verb ) === wanakana . toHiragana ( verbToken . basic_form ) ) {
183+ fullBasicForm = verb ;
184+ }
185+ }
186+
187+ let type = null ;
188+ if ( cType . includes ( '一段' ) ) type = 'ICHIDAN' ;
189+ else if ( cType . includes ( '五段' ) ) type = 'GODAN' ;
190+ else if ( cType . includes ( 'サ変' ) ) type = 'SURU' ;
191+ else if ( cType . includes ( 'カ変' ) ) type = 'KURU' ;
192+
193+ if ( type ) {
194+ return { type, basicForm : fullBasicForm } ;
195+ }
123196
124197 return null ;
125198}
@@ -182,7 +255,8 @@ ${JSON.stringify(conjugationResult, null, 2)}
182255}
183256\`\`\`
184257
185- 第二步:在 JSON 代码块之后,用中文简明扼要地解释该动词的含义,并提供2个实用的日常例句(必须包含日文原文、平假名注音和精准的中文翻译)。支持使用 Markdown 格式加粗、高亮。` ;
258+ 第二步:在 JSON 代码块之后,用中文简明扼要地解释该动词的含义,并提供2个实用的日常例句(必须包含日文原文、平假名注音和精准的中文翻译)。支持使用 Markdown 格式加粗、高亮。
259+ 注意:解释动词类型时,请使用中文习惯的称呼(如“五段动词”、“一段动词”、“サ变动词”、“カ变动词”),不要使用英文(如 Godan、Ichidan、Group 1、Group 2)。` ;
186260
187261 res . setHeader ( 'Content-Type' , 'text/event-stream' ) ;
188262 res . setHeader ( 'Cache-Control' , 'no-cache' ) ;
@@ -255,7 +329,7 @@ app.get('/api/suggest', async (req, res) => {
255329} ) ;
256330
257331// 动词活用 API
258- app . get ( '/api/conjugate' , ( req , res ) => {
332+ app . get ( '/api/conjugate' , async ( req , res ) => {
259333 try {
260334 let { verb, type } = req . query ;
261335
@@ -271,22 +345,39 @@ app.get('/api/conjugate', (req, res) => {
271345 const processedVerb = wanakana . toHiragana ( verb ) ;
272346
273347 // 如果前端没有传 type,就用 kuromoji 自动推断
348+ let finalVerb = processedVerb ;
349+
274350 if ( ! type ) {
275351 if ( ! tokenizer ) {
276352 return res . status ( 503 ) . json ( { error : 'Dictionary is initializing, please try again later.' } ) ;
277353 }
278- type = detectVerbType ( processedVerb ) ;
279- if ( ! type ) {
354+ const detectResult = detectVerbType ( verb ) ;
355+ if ( ! detectResult ) {
280356 return res . status ( 400 ) . json ( {
281357 error : `无法自动识别 "${ verb } " (解析为 "${ processedVerb } ") 的动词类型。请确保输入的是正确的日语动词原形(如:食べる、飲む、勉強する)。`
282358 } ) ;
283359 }
360+ type = detectResult . type ;
361+ finalVerb = detectResult . basicForm ;
362+ }
363+
364+ // 检查字符串是否完全没有汉字(wanakana.isKanji 检查是否只包含汉字,所以要手写正则)
365+ const hasKanji = ( str ) => / [ \u4e00 - \u9faf ] / . test ( str ) ;
366+
367+ // 如果推断出来的还是全平假名,尝试用 Jisho 转换成带汉字的常用形式
368+ // 只有当输入不包含任何汉字(即只有平假名或罗马音)时才尝试转换
369+ if ( ! hasKanji ( verb ) && ! hasKanji ( finalVerb ) ) {
370+ const kanjiVerb = await getKanjiFromJisho ( finalVerb ) ;
371+ if ( kanjiVerb ) {
372+ finalVerb = kanjiVerb ;
373+ }
284374 }
285375
286- const result = conjugate ( processedVerb , type ) ;
376+ const result = conjugate ( finalVerb , type ) ;
287377 // 如果转换后有变化,可以在返回结果里告诉前端这是基于罗马音解析的
288378 res . json ( {
289379 ...result ,
380+ dictionaryForm : finalVerb , // 覆盖为带汉字的原形
290381 originalInput : verb ,
291382 parsedAs : processedVerb
292383 } ) ;
0 commit comments