11// ==UserScript==
22// @name flomo2Anki
33// @namespace http://tampermonkey.net/
4- // @version 0.9
4+ // @version 1.0
55// @description 将 flomo 笔记发送到 Anki,支持单张和批量发送,并处理标签和时间链接
66// @author springrain
77// @match https://v.flomoapp.com/mine*
7777 const content = extractContent ( memo ) ;
7878 const tags = extractTags ( memo ) ;
7979 const modelName = getModelName ( tags ) ;
80- const fields = getFields ( content , modelName ) ;
81-
82-
80+ // 获取包含元数据的字段信息
81+ const { fields, primaryField } = getFields ( content , modelName ) ;
8382 if ( ! content ) throw new Error ( '卡片内容为空' ) ;
8483
85-
8684 return {
8785 deckName : config . defaultDeck ,
8886 modelName : modelName ,
8987 fields : fields ,
9088 tags : tags ,
89+ primaryField : primaryField // 添加主字段信息
9190 } ;
9291 } ) ;
9392
94-
9593 console . log ( '正在发送到 Anki:' , notes ) ; // 调试信息
9694
97-
9895 // 检查重复卡片并发送
9996 const { successNotes, updatedNotes } = await checkAndSendNotes ( notes ) ;
10097
107104 }
108105 }
109106
107+ // 修改后的checkAndSendNotes函数
108+ async function checkAndSendNotes ( notes ) {
109+ const successNotes = [ ] ;
110+ const updatedNotes = [ ] ;
111+
112+ for ( const note of notes ) {
113+ try {
114+ // 获取主字段信息
115+ const primaryFieldName = note . primaryField . name ;
116+ let primaryFieldValue = note . primaryField . value ;
117+
118+ // 转义特殊字符(双引号)
119+ primaryFieldValue = primaryFieldValue . replace ( / " / g, '\\"' ) ;
120+
121+ // 构建精确查询语句
122+ const query = `"deck:${ note . deckName } " "note:${ note . modelName } " "${ primaryFieldName } :${ primaryFieldValue } "` ;
123+
124+ const findResult = await ankiconnectRequest ( 'findNotes' , { query } ) ;
125+
126+ if ( findResult . result . length > 0 ) {
127+ // 更新现有卡片
128+ const noteId = findResult . result [ 0 ] ;
129+ await updateNote ( noteId , note ) ;
130+ updatedNotes . push ( note ) ;
131+ } else {
132+ // 添加新卡片
133+ await ankiconnectRequest ( 'addNote' , {
134+ note : {
135+ deckName : note . deckName ,
136+ modelName : note . modelName ,
137+ fields : note . fields ,
138+ tags : note . tags
139+ }
140+ } ) ;
141+ successNotes . push ( note ) ;
142+ }
143+ } catch ( error ) {
144+ console . error ( 'Error:' , error ) ;
145+ throw error ;
146+ }
147+ }
148+ return { successNotes, updatedNotes } ;
149+ }
110150
111- // 检查重复卡片并发送
112- async function checkAndSendNotes ( notes ) {
113- const successNotes = [ ] ; // 成功添加的卡片
114- const updatedNotes = [ ] ; // 成功更新的卡片
115-
116-
117- for ( const note of notes ) {
118- try {
119- // 查找是否存在相同卡片
120- const findResult = await ankiconnectRequest ( 'findNotes' , {
121- query : `deck:"${ note . deckName } " note:"${ note . modelName } " front:"${ note . fields . Front } "` ,
122- } ) ;
123-
124-
125- if ( findResult . result . length > 0 ) {
126- // 如果找到相同卡片,则更新
127- const noteId = findResult . result [ 0 ] ;
128- await ankiconnectRequest ( 'updateNoteFields' , {
129- note : {
130- id : noteId ,
131- fields : note . fields ,
132- } ,
133- } ) ;
134- updatedNotes . push ( note ) ;
135- } else {
136- // 如果没有找到,则添加新卡片
137- await ankiconnectRequest ( 'addNote' , { note } ) ;
138- successNotes . push ( note ) ;
139- }
140- } catch ( error ) {
141- console . error ( 'Error:' , error ) ;
142- throw error ;
143- }
144- }
145-
151+ // 新增的updateNote函数
152+ async function updateNote ( noteId , newNote ) {
153+ // 更新字段
154+ await ankiconnectRequest ( 'updateNoteFields' , {
155+ note : {
156+ id : noteId ,
157+ fields : newNote . fields
158+ }
159+ } ) ;
146160
147- return { successNotes, updatedNotes } ;
148- }
161+ // 合并标签
162+ const noteInfo = await ankiconnectRequest ( 'notesInfo' , { notes : [ noteId ] } ) ;
163+ const existingTags = noteInfo . result [ 0 ] . tags || [ ] ;
164+ const mergedTags = [ ...new Set ( [ ...existingTags , ...newNote . tags ] ) ] ;
149165
166+ // 直接传递 noteId 和 tags
167+ await ankiconnectRequest ( 'updateNoteTags' , {
168+ note : noteId ,
169+ tags : mergedTags
170+ } ) ;
171+ }
150172
151173 // 提取内容并处理
152174 function extractContent ( memo ) {
153175 const contentElement = memo . querySelector ( '.mainContent' ) ;
154176 if ( ! contentElement ) return '' ;
155177
156-
157178 // 克隆节点以避免修改原始 DOM
158179 const clonedContent = contentElement . cloneNode ( true ) ;
159180
160-
161181 // 剔除不需要的部分
162182 const relatedElements = clonedContent . querySelectorAll ( '.related' ) ;
163183 relatedElements . forEach ( ( el ) => el . remove ( ) ) ;
164184
165-
166185 // 清理无效标签
167186 let cleanedContent = cleanInvalidTags ( clonedContent . innerHTML ) ;
168187
169-
170188 // 删除内容中的 # 标签
171189 cleanedContent = cleanedContent . replace ( / < s p a n c l a s s = " t a g " > # ( .* ?) < \/ s p a n > / g, '' ) ;
172190
173-
174191 // 添加时间链接
175192 const timeLink = memo . querySelector ( '.time' ) ;
176193 if ( timeLink ) {
180197 cleanedContent += `Source:<a href="${ fullUrl } ">${ timeText } </a>` ;
181198 }
182199
183-
184200 return cleanedContent . trim ( ) ;
185201 }
186202
210226 }
211227
212228
213- // 根据模板名称处理内容并生成字段
214- function getFields ( content , modelName ) {
215- const fields = { } ;
216- if ( modelName === '问答卡片' ) {
217- // 创建一个临时 DOM 元素来解析 HTML
218- const tempDiv = document . createElement ( 'div' ) ;
219- tempDiv . innerHTML = content ;
229+ // 根据模板名称处理内容并生成字段
230+ function getFields ( content , modelName ) {
231+ const fields = { } ;
232+ if ( modelName === '问答卡片' ) {
233+ // 创建一个临时 DOM 元素来解析 HTML
234+ const tempDiv = document . createElement ( 'div' ) ;
235+ tempDiv . innerHTML = content ;
220236
221- // 获取所有的 <p> 标签
222- const paragraphs = tempDiv . querySelectorAll ( 'p' ) ;
237+ // 获取所有的 <p> 标签
238+ const paragraphs = tempDiv . querySelectorAll ( 'p' ) ;
223239
224- let question = '' ;
240+ let question = '' ;
225241
226- // 处理第一个 <p> 标签
227- const firstParagraph = paragraphs [ 0 ] ;
228- const firstParagraphText = firstParagraph . textContent . trim ( ) ;
242+ // 处理第一个 <p> 标签
243+ const firstParagraph = paragraphs [ 0 ] ;
244+ const firstParagraphText = firstParagraph . textContent . trim ( ) ;
229245
230- // 判断第一个 <p> 标签是否包含问题
231- if ( firstParagraphText && ! firstParagraphText . endsWith ( '#' ) ) {
232- // 情况1:标签与问题在同一行
233- question = firstParagraphText . replace ( / # [ ^ ] + / g, '' ) . trim ( ) ; // 去掉标签
234- } else {
235- // 情况2:标签单独一行,问题在随后一行
236- if ( paragraphs . length > 1 ) {
237- question = paragraphs [ 1 ] . textContent . trim ( ) ;
238- }
246+ // 判断第一个 <p> 标签是否包含问题
247+ if ( firstParagraphText && ! firstParagraphText . endsWith ( '#' ) ) {
248+ // 情况1:标签与问题在同一行
249+ question = firstParagraphText . replace ( / # [ ^ ] + / g, '' ) . trim ( ) ; // 去掉标签
250+ } else {
251+ // 情况2:标签单独一行,问题在随后一行
252+ if ( paragraphs . length > 1 ) {
253+ question = paragraphs [ 1 ] . textContent . trim ( ) ;
254+ }
255+ }
256+ // 剩余内容作为答案字段
257+ const answer = content
258+ . replace ( question , '' ) // 剔除问题部分
259+ . replace ( / < p > \s * < \/ p > / g, '' ) // 匹配 <p></p> 及其内部的空白字符
260+ . replace ( / < p \s * \/ > / g, '' ) // 匹配自闭合的 <p />
261+ . trim ( ) ;
262+ return {
263+ fields : {
264+ [ config . fieldMapping [ modelName ] . Front ] : question ,
265+ [ config . fieldMapping [ modelName ] . Back ] : answer
266+ } ,
267+ primaryField : {
268+ name : config . fieldMapping [ modelName ] . Front ,
269+ value : question
270+ }
271+ } ;
272+ } else {
273+ return {
274+ fields : {
275+ [ config . fieldMapping [ modelName ] . Front ] : content ,
276+ [ config . fieldMapping [ modelName ] . Back ] : content
277+ } ,
278+ primaryField : {
279+ name : config . fieldMapping [ modelName ] . Front ,
280+ value : content
281+ }
282+ } ;
239283 }
240- // 剩余内容作为答案字段
241- const answer = content
242- . replace ( question , '' ) // 剔除问题部分
243- . replace ( / < p > \s * < \/ p > / g, '' ) // 匹配 <p></p> 及其内部的空白字符
244- . replace ( / < p \s * \/ > / g, '' ) // 匹配自闭合的 <p />
245- . trim ( ) ;
246- fields [ config . fieldMapping [ modelName ] . Front ] = question ;
247- fields [ config . fieldMapping [ modelName ] . Back ] = answer ;
248- } else {
249- fields [ config . fieldMapping [ modelName ] . Front ] = content ;
250- fields [ config . fieldMapping [ modelName ] . Back ] = content ;
251- }
252- return fields ;
253- }
254-
255-
284+ }
256285
257286 // 清理无效标签
258287 function cleanInvalidTags ( html ) {
@@ -262,15 +291,12 @@ function getFields(content, modelName) {
262291 //将flomo卡片中的缩略图链接改为原图链接
263292 html = html . replace ( / < i m g [ ^ > ] * s r c = " ( [ ^ " ] * \/ t h u m b n a i l [ ^ " ] * ) " [ ^ > ] * d a t a - s o u r c e = " ( [ ^ " ] * ) " [ ^ > ] * > / g, '<img src="$2" class="el-image__inner" style="object-fit: cover;">' ) ;
264293
265-
266294 // 剔除空的 <div class="placeholder"></div>
267295 html = html . replace ( / < d i v [ ^ > ] * c l a s s = " p l a c e h o l d e r " [ ^ > ] * > < \/ d i v > / g, '' ) ;
268296
269-
270297 // 剔除其他空的 div 标签
271298 html = html . replace ( / < d i v [ ^ > ] * > < \/ d i v > / g, '' ) ;
272299
273-
274300 return html . trim ( ) ;
275301 }
276302
@@ -292,6 +318,7 @@ function getFields(content, modelName) {
292318 } ) ;
293319 }
294320
321+
295322 // 初始化
296323 function init ( ) {
297324 // 查找所有卡片
@@ -304,6 +331,7 @@ function getFields(content, modelName) {
304331 addBatchAndSelectAllButtons ( ) ;
305332 }
306333
334+
307335 // 为单张卡片添加复选框和按钮
308336 function addCheckboxAndButton ( memo ) {
309337 // 检查是否已添加按钮和复选框
@@ -323,6 +351,7 @@ function getFields(content, modelName) {
323351 }
324352 }
325353
354+
326355 // 全选按钮点击事件
327356 function handleSelectAll ( ) {
328357 // 获取所有复选框(包括后续动态加载的)
@@ -418,7 +447,7 @@ function getFields(content, modelName) {
418447 } ) ;
419448
420449 // 开始观察页面变化
421- observer . observe ( document . body , { childList : true , subtree : true } ) ;
450+ observer . observe ( document . body , { childList : true , subtree : true } ) ;
422451
423452 // 页面加载完成后初始化
424453 window . addEventListener ( 'load' , init ) ;
0 commit comments