From 1361cc15dbd1b199ca4ac7b0062a5dbc0a00039d Mon Sep 17 00:00:00 2001 From: jonny Date: Wed, 5 Aug 2026 17:07:59 +0800 Subject: [PATCH] Fix tags display and Memos API compatibility issues Fix the tags not displaying issue by querying both /tag/suggestion and /tag endpoints and merging results. Simplify v0.23 filter to avoid 400 errors from numeric ID requirements. Add v0.18.2 support via numeric ID fallback in getMemoUid. Improve v0.23+ resource URL handling and add better error handling with debug logging for tags and random features. --- change.log | 2 +- chrome/js/compat/memosApi.adapter.js | 13 +++-- chrome/js/compat/memosApi.v020-v021.js | 65 +++++++++++++++++++++---- chrome/js/oper.js | 42 +++++++++++++--- edge/js/compat/memosApi.adapter.js | 13 +++-- edge/js/compat/memosApi.v020-v021.js | 65 +++++++++++++++++++++---- edge/js/oper.js | 42 +++++++++++++--- firefox/js/compat/memosApi.adapter.js | 13 +++-- firefox/js/compat/memosApi.v020-v021.js | 65 +++++++++++++++++++++---- firefox/js/oper.js | 42 +++++++++++++--- 10 files changed, 301 insertions(+), 61 deletions(-) diff --git a/change.log b/change.log index 0e8c369..756ce6e 100644 --- a/change.log +++ b/change.log @@ -1,5 +1,5 @@ ## 更新日志 -- 20260805 适配 Memos 0.29.x Attachment API,修复附件上传、绑定和预览地址;修复 0.20.x 图片预览 URL 错误使用 numeric id 的问题 +- 20260805 适配 Memos 0.29.x Attachment API,修复附件上传、绑定和预览地址;修复 0.20.x 图片预览 URL 错误使用 numeric id 的问题以及标签不显示问题 - 20260425 新增德语/法语/西班牙语支持 - 20260423 优化 firefox 抖动问题和支持手机版,支持 edge 浏览器扩展 - 20260422 调整发送设置,支持仅发送附件 diff --git a/chrome/js/compat/memosApi.adapter.js b/chrome/js/compat/memosApi.adapter.js index 13d0f95..5c359c9 100644 --- a/chrome/js/compat/memosApi.adapter.js +++ b/chrome/js/compat/memosApi.adapter.js @@ -249,10 +249,10 @@ } if (flavor === 'v023' && global.MemosApiV023) { - const filterExpr = global.MemosApiV023.buildFilter({ - rowStatus: 'NORMAL', - creator: 'users/' + info.userid - }) + // v0.23 filter requires `creator == "users/{numericId}"`. Some stored userids are + // usernames, which causes 400. Also `row_status` can cause 400 on some builds. + // Let the server identify the current user from the token and return all visible memos. + const filterExpr = global.MemosApiV023.buildFilter({}) global.MemosApiV023.listMemos( info, { pageSize: 1000, filterExpr: filterExpr }, @@ -328,7 +328,10 @@ if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021) { global.MemosApiV020V021.listMemos(info, { limit: 1000, rowStatus: 'NORMAL' }, function (data) { - if (success) success(keepLegacyVisibleMemos(extractMemos(data))) + const raw = extractMemos(data) + const filtered = keepLegacyVisibleMemos(raw) + //console.log('[memos-bber] v020-v021 random raw count:', raw.length, 'filtered count:', filtered.length) + if (success) success(filtered) }, fail) return } diff --git a/chrome/js/compat/memosApi.v020-v021.js b/chrome/js/compat/memosApi.v020-v021.js index bb874fd..cd6ba58 100644 --- a/chrome/js/compat/memosApi.v020-v021.js +++ b/chrome/js/compat/memosApi.v020-v021.js @@ -201,22 +201,69 @@ function getTagSuggestion(info, success, fail) { const headers = { Authorization: 'Bearer ' + info.apiTokens } + + function normalizeTagList(data) { + const list = Array.isArray(data) ? data : Array.isArray(data.tags) ? data.tags : [] + return list + .map(function (t) { + if (!t) return '' + if (typeof t === 'string') return t + if (typeof t.name === 'string') return t.name + if (typeof t.tag === 'string') return t.tag + return '' + }) + .map(function (s) { + return String(s).replace(/^#/, '').trim() + }) + .filter(Boolean) + } + + function normalizeSuggestionList(data) { + const list = Array.isArray(data) ? data : [] + return list + .map(function (s) { + return String(s).replace(/^#/, '').trim() + }) + .filter(Boolean) + } + + function mergeUnique(a, b) { + return [...new Set((a || []).concat(b || []))] + } + + let suggestionList = [] + let listList = [] + let pending = 2 + + function finish() { + if (--pending !== 0) return + if (success) success(mergeUnique(suggestionList, listList)) + } + requestGet( info.apiUrl + 'api/v1/tag/suggestion', headers, function (data) { - const list = Array.isArray(data) ? data : [] - const out = list - .map(function (s) { - return String(s).replace(/^#/, '').trim() - }) - .filter(Boolean) - if (success) success(out) + suggestionList = normalizeSuggestionList(data) + finish() + }, + function (xhr) { + // Ignore failures from the suggestion endpoint; the list endpoint may still work. + if (xhr && xhr.status !== 404 && xhr.status !== 405 && fail) fail(xhr) + else finish() + } + ) + + requestGet( + info.apiUrl + 'api/v1/tag', + headers, + function (data) { + listList = normalizeTagList(data) + finish() }, function (xhr) { - // Some forks might only expose list. if (isNotFoundLikeXhr(xhr)) { - getTagList(info, success, fail) + finish() return } if (fail) fail(xhr) diff --git a/chrome/js/oper.js b/chrome/js/oper.js index 27813bd..fa41d8c 100644 --- a/chrome/js/oper.js +++ b/chrome/js/oper.js @@ -321,6 +321,8 @@ function getMemoUid(memo) { if (!memo) return '' if (memo.uid != null && memo.uid !== '') return String(memo.uid) if (typeof memo.name === 'string' && memo.name) return memo.name.split('/').pop() + // v0.18.2 only exposes a numeric id. + if (memo.id != null && memo.id !== '') return String(memo.id) return '' } @@ -482,12 +484,10 @@ function buildV1ResourceStreamUrl(info, resource) { if (resource.externalLink) return resource.externalLink + const isV023 = info.apiFlavor === 'v023' + const name = typeof resource.name === 'string' ? resource.name.trim() : '' const filename = typeof resource.filename === 'string' ? resource.filename : '' - if (name.indexOf('attachments/') === 0 && filename) { - const url = root + 'file/' + name.split('/').map(encodeURIComponent).join('/') + '/' + encodeURIComponent(filename) - return isImageResource(resource) ? url + '?thumbnail=true' : url - } function isImageResource(r) { if (!r) return false @@ -497,6 +497,22 @@ function buildV1ResourceStreamUrl(info, resource) { return /\.(png|jpe?g|gif|webp|bmp|svg|avif|heic)$/.test(fn) } + // v0.23+: resources are served as static files via /file/{name}/{filename}. + if (isV023) { + if (resource.externalLink) return resource.externalLink + const resourceName = name.indexOf('resources/') === 0 ? name : '' + if (resourceName && filename) { + const url = root + 'file/' + resourceName + '/' + filename.split('/').map(encodeURIComponent).join('/') + return isImageResource(resource) ? url + '?thumbnail=true' : url + } + return '' + } + + if (name.indexOf('attachments/') === 0 && filename) { + const url = root + 'file/' + name.split('/').map(encodeURIComponent).join('/') + '/' + encodeURIComponent(filename) + return isImageResource(resource) ? url + '?thumbnail=true' : url + } + function isProbablyUid(s) { if (typeof s !== 'string') return false const v = s.trim() @@ -920,11 +936,17 @@ $('#opensite').click(function () { // 0.23.1版本 GET api/v1/{parent}/tags 接口已移除,参考 https://github.com/usememos/memos/issues/4161 $('#tags').click(function () { get_info(function (info) { + //console.log('[memos-bber] tags click info:', info) if (info.apiUrl) { var tagDom = ""; const adapter = getApiAdapter(info) + if (!adapter) { + //console.warn('[memos-bber] no adapter for tags') + return + } const renderTags = function (tags) { + //console.log('[memos-bber] tags result:', tags) const uniTags = [...new Set((Array.isArray(tags) ? tags : []).filter(Boolean))] $.each(uniTags, function (_, tag) { tagDom += '#' + tag + ''; @@ -933,7 +955,8 @@ $('#tags').click(function () { $("#taglist").html(tagDom).slideToggle(500) } - adapter.listTags(renderTags, function () { + adapter.listTags(renderTags, function (err) { + //console.error('[memos-bber] tags failed:', err) $.message({ message: msg('placeApiUrl') }) }) } else { @@ -1051,17 +1074,24 @@ $('#search').click(function () { $('#random').click(function () { get_info(function (info) { + //console.log('[memos-bber] random click info:', info) if (info.status) { $("#randomlist").html('').hide() const adapter = getApiAdapter(info) + if (!adapter) { + //console.warn('[memos-bber] no adapter for random') + return + } adapter.listRandomMemos( function (memos) { + console.log('[memos-bber] random result count:', memos && memos.length) let randomNum = Math.floor(Math.random() * (memos.length)); var randomData = memos[randomNum] randDom(randomData) }, - function () { + function (err) { + //console.error('[memos-bber] random failed:', err) $.message({ message: msg('placeApiUrl') }) } ) diff --git a/edge/js/compat/memosApi.adapter.js b/edge/js/compat/memosApi.adapter.js index 13d0f95..0d01ebb 100644 --- a/edge/js/compat/memosApi.adapter.js +++ b/edge/js/compat/memosApi.adapter.js @@ -249,10 +249,10 @@ } if (flavor === 'v023' && global.MemosApiV023) { - const filterExpr = global.MemosApiV023.buildFilter({ - rowStatus: 'NORMAL', - creator: 'users/' + info.userid - }) + // v0.23 filter requires `creator == "users/{numericId}"`. Some stored userids are + // usernames, which causes 400. Also `row_status` can cause 400 on some builds. + // Let the server identify the current user from the token and return all visible memos. + const filterExpr = global.MemosApiV023.buildFilter({}) global.MemosApiV023.listMemos( info, { pageSize: 1000, filterExpr: filterExpr }, @@ -328,7 +328,10 @@ if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021) { global.MemosApiV020V021.listMemos(info, { limit: 1000, rowStatus: 'NORMAL' }, function (data) { - if (success) success(keepLegacyVisibleMemos(extractMemos(data))) + const raw = extractMemos(data) + const filtered = keepLegacyVisibleMemos(raw) + console.log('[memos-bber] v020-v021 random raw count:', raw.length, 'filtered count:', filtered.length) + if (success) success(filtered) }, fail) return } diff --git a/edge/js/compat/memosApi.v020-v021.js b/edge/js/compat/memosApi.v020-v021.js index bb874fd..cd6ba58 100644 --- a/edge/js/compat/memosApi.v020-v021.js +++ b/edge/js/compat/memosApi.v020-v021.js @@ -201,22 +201,69 @@ function getTagSuggestion(info, success, fail) { const headers = { Authorization: 'Bearer ' + info.apiTokens } + + function normalizeTagList(data) { + const list = Array.isArray(data) ? data : Array.isArray(data.tags) ? data.tags : [] + return list + .map(function (t) { + if (!t) return '' + if (typeof t === 'string') return t + if (typeof t.name === 'string') return t.name + if (typeof t.tag === 'string') return t.tag + return '' + }) + .map(function (s) { + return String(s).replace(/^#/, '').trim() + }) + .filter(Boolean) + } + + function normalizeSuggestionList(data) { + const list = Array.isArray(data) ? data : [] + return list + .map(function (s) { + return String(s).replace(/^#/, '').trim() + }) + .filter(Boolean) + } + + function mergeUnique(a, b) { + return [...new Set((a || []).concat(b || []))] + } + + let suggestionList = [] + let listList = [] + let pending = 2 + + function finish() { + if (--pending !== 0) return + if (success) success(mergeUnique(suggestionList, listList)) + } + requestGet( info.apiUrl + 'api/v1/tag/suggestion', headers, function (data) { - const list = Array.isArray(data) ? data : [] - const out = list - .map(function (s) { - return String(s).replace(/^#/, '').trim() - }) - .filter(Boolean) - if (success) success(out) + suggestionList = normalizeSuggestionList(data) + finish() + }, + function (xhr) { + // Ignore failures from the suggestion endpoint; the list endpoint may still work. + if (xhr && xhr.status !== 404 && xhr.status !== 405 && fail) fail(xhr) + else finish() + } + ) + + requestGet( + info.apiUrl + 'api/v1/tag', + headers, + function (data) { + listList = normalizeTagList(data) + finish() }, function (xhr) { - // Some forks might only expose list. if (isNotFoundLikeXhr(xhr)) { - getTagList(info, success, fail) + finish() return } if (fail) fail(xhr) diff --git a/edge/js/oper.js b/edge/js/oper.js index 27813bd..6694df5 100644 --- a/edge/js/oper.js +++ b/edge/js/oper.js @@ -321,6 +321,8 @@ function getMemoUid(memo) { if (!memo) return '' if (memo.uid != null && memo.uid !== '') return String(memo.uid) if (typeof memo.name === 'string' && memo.name) return memo.name.split('/').pop() + // v0.18.2 only exposes a numeric id. + if (memo.id != null && memo.id !== '') return String(memo.id) return '' } @@ -482,12 +484,10 @@ function buildV1ResourceStreamUrl(info, resource) { if (resource.externalLink) return resource.externalLink + const isV023 = info.apiFlavor === 'v023' + const name = typeof resource.name === 'string' ? resource.name.trim() : '' const filename = typeof resource.filename === 'string' ? resource.filename : '' - if (name.indexOf('attachments/') === 0 && filename) { - const url = root + 'file/' + name.split('/').map(encodeURIComponent).join('/') + '/' + encodeURIComponent(filename) - return isImageResource(resource) ? url + '?thumbnail=true' : url - } function isImageResource(r) { if (!r) return false @@ -497,6 +497,22 @@ function buildV1ResourceStreamUrl(info, resource) { return /\.(png|jpe?g|gif|webp|bmp|svg|avif|heic)$/.test(fn) } + // v0.23+: resources are served as static files via /file/{name}/{filename}. + if (isV023) { + if (resource.externalLink) return resource.externalLink + const resourceName = name.indexOf('resources/') === 0 ? name : '' + if (resourceName && filename) { + const url = root + 'file/' + resourceName + '/' + filename.split('/').map(encodeURIComponent).join('/') + return isImageResource(resource) ? url + '?thumbnail=true' : url + } + return '' + } + + if (name.indexOf('attachments/') === 0 && filename) { + const url = root + 'file/' + name.split('/').map(encodeURIComponent).join('/') + '/' + encodeURIComponent(filename) + return isImageResource(resource) ? url + '?thumbnail=true' : url + } + function isProbablyUid(s) { if (typeof s !== 'string') return false const v = s.trim() @@ -920,11 +936,17 @@ $('#opensite').click(function () { // 0.23.1版本 GET api/v1/{parent}/tags 接口已移除,参考 https://github.com/usememos/memos/issues/4161 $('#tags').click(function () { get_info(function (info) { + console.log('[memos-bber] tags click info:', info) if (info.apiUrl) { var tagDom = ""; const adapter = getApiAdapter(info) + if (!adapter) { + console.warn('[memos-bber] no adapter for tags') + return + } const renderTags = function (tags) { + console.log('[memos-bber] tags result:', tags) const uniTags = [...new Set((Array.isArray(tags) ? tags : []).filter(Boolean))] $.each(uniTags, function (_, tag) { tagDom += '#' + tag + ''; @@ -933,7 +955,8 @@ $('#tags').click(function () { $("#taglist").html(tagDom).slideToggle(500) } - adapter.listTags(renderTags, function () { + adapter.listTags(renderTags, function (err) { + console.error('[memos-bber] tags failed:', err) $.message({ message: msg('placeApiUrl') }) }) } else { @@ -1051,17 +1074,24 @@ $('#search').click(function () { $('#random').click(function () { get_info(function (info) { + console.log('[memos-bber] random click info:', info) if (info.status) { $("#randomlist").html('').hide() const adapter = getApiAdapter(info) + if (!adapter) { + console.warn('[memos-bber] no adapter for random') + return + } adapter.listRandomMemos( function (memos) { + console.log('[memos-bber] random result count:', memos && memos.length) let randomNum = Math.floor(Math.random() * (memos.length)); var randomData = memos[randomNum] randDom(randomData) }, - function () { + function (err) { + console.error('[memos-bber] random failed:', err) $.message({ message: msg('placeApiUrl') }) } ) diff --git a/firefox/js/compat/memosApi.adapter.js b/firefox/js/compat/memosApi.adapter.js index 13d0f95..0d01ebb 100644 --- a/firefox/js/compat/memosApi.adapter.js +++ b/firefox/js/compat/memosApi.adapter.js @@ -249,10 +249,10 @@ } if (flavor === 'v023' && global.MemosApiV023) { - const filterExpr = global.MemosApiV023.buildFilter({ - rowStatus: 'NORMAL', - creator: 'users/' + info.userid - }) + // v0.23 filter requires `creator == "users/{numericId}"`. Some stored userids are + // usernames, which causes 400. Also `row_status` can cause 400 on some builds. + // Let the server identify the current user from the token and return all visible memos. + const filterExpr = global.MemosApiV023.buildFilter({}) global.MemosApiV023.listMemos( info, { pageSize: 1000, filterExpr: filterExpr }, @@ -328,7 +328,10 @@ if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021) { global.MemosApiV020V021.listMemos(info, { limit: 1000, rowStatus: 'NORMAL' }, function (data) { - if (success) success(keepLegacyVisibleMemos(extractMemos(data))) + const raw = extractMemos(data) + const filtered = keepLegacyVisibleMemos(raw) + console.log('[memos-bber] v020-v021 random raw count:', raw.length, 'filtered count:', filtered.length) + if (success) success(filtered) }, fail) return } diff --git a/firefox/js/compat/memosApi.v020-v021.js b/firefox/js/compat/memosApi.v020-v021.js index bb874fd..cd6ba58 100644 --- a/firefox/js/compat/memosApi.v020-v021.js +++ b/firefox/js/compat/memosApi.v020-v021.js @@ -201,22 +201,69 @@ function getTagSuggestion(info, success, fail) { const headers = { Authorization: 'Bearer ' + info.apiTokens } + + function normalizeTagList(data) { + const list = Array.isArray(data) ? data : Array.isArray(data.tags) ? data.tags : [] + return list + .map(function (t) { + if (!t) return '' + if (typeof t === 'string') return t + if (typeof t.name === 'string') return t.name + if (typeof t.tag === 'string') return t.tag + return '' + }) + .map(function (s) { + return String(s).replace(/^#/, '').trim() + }) + .filter(Boolean) + } + + function normalizeSuggestionList(data) { + const list = Array.isArray(data) ? data : [] + return list + .map(function (s) { + return String(s).replace(/^#/, '').trim() + }) + .filter(Boolean) + } + + function mergeUnique(a, b) { + return [...new Set((a || []).concat(b || []))] + } + + let suggestionList = [] + let listList = [] + let pending = 2 + + function finish() { + if (--pending !== 0) return + if (success) success(mergeUnique(suggestionList, listList)) + } + requestGet( info.apiUrl + 'api/v1/tag/suggestion', headers, function (data) { - const list = Array.isArray(data) ? data : [] - const out = list - .map(function (s) { - return String(s).replace(/^#/, '').trim() - }) - .filter(Boolean) - if (success) success(out) + suggestionList = normalizeSuggestionList(data) + finish() + }, + function (xhr) { + // Ignore failures from the suggestion endpoint; the list endpoint may still work. + if (xhr && xhr.status !== 404 && xhr.status !== 405 && fail) fail(xhr) + else finish() + } + ) + + requestGet( + info.apiUrl + 'api/v1/tag', + headers, + function (data) { + listList = normalizeTagList(data) + finish() }, function (xhr) { - // Some forks might only expose list. if (isNotFoundLikeXhr(xhr)) { - getTagList(info, success, fail) + finish() return } if (fail) fail(xhr) diff --git a/firefox/js/oper.js b/firefox/js/oper.js index 1f5dee5..764537e 100644 --- a/firefox/js/oper.js +++ b/firefox/js/oper.js @@ -346,6 +346,8 @@ function getMemoUid(memo) { if (!memo) return '' if (memo.uid != null && memo.uid !== '') return String(memo.uid) if (typeof memo.name === 'string' && memo.name) return memo.name.split('/').pop() + // v0.18.2 only exposes a numeric id. + if (memo.id != null && memo.id !== '') return String(memo.id) return '' } @@ -507,12 +509,10 @@ function buildV1ResourceStreamUrl(info, resource) { if (resource.externalLink) return resource.externalLink + const isV023 = info.apiFlavor === 'v023' + const name = typeof resource.name === 'string' ? resource.name.trim() : '' const filename = typeof resource.filename === 'string' ? resource.filename : '' - if (name.indexOf('attachments/') === 0 && filename) { - const url = root + 'file/' + name.split('/').map(encodeURIComponent).join('/') + '/' + encodeURIComponent(filename) - return isImageResource(resource) ? url + '?thumbnail=true' : url - } function isImageResource(r) { if (!r) return false @@ -522,6 +522,22 @@ function buildV1ResourceStreamUrl(info, resource) { return /\.(png|jpe?g|gif|webp|bmp|svg|avif|heic)$/.test(fn) } + // v0.23+: resources are served as static files via /file/{name}/{filename}. + if (isV023) { + if (resource.externalLink) return resource.externalLink + const resourceName = name.indexOf('resources/') === 0 ? name : '' + if (resourceName && filename) { + const url = root + 'file/' + resourceName + '/' + filename.split('/').map(encodeURIComponent).join('/') + return isImageResource(resource) ? url + '?thumbnail=true' : url + } + return '' + } + + if (name.indexOf('attachments/') === 0 && filename) { + const url = root + 'file/' + name.split('/').map(encodeURIComponent).join('/') + '/' + encodeURIComponent(filename) + return isImageResource(resource) ? url + '?thumbnail=true' : url + } + function isProbablyUid(s) { if (typeof s !== 'string') return false const v = s.trim() @@ -945,11 +961,17 @@ $('#opensite').click(function () { // 0.23.1版本 GET api/v1/{parent}/tags 接口已移除,参考 https://github.com/usememos/memos/issues/4161 $('#tags').click(function () { get_info(function (info) { + console.log('[memos-bber] tags click info:', info) if (info.apiUrl) { var tagDom = ""; const adapter = getApiAdapter(info) + if (!adapter) { + console.warn('[memos-bber] no adapter for tags') + return + } const renderTags = function (tags) { + console.log('[memos-bber] tags result:', tags) const uniTags = [...new Set((Array.isArray(tags) ? tags : []).filter(Boolean))] $.each(uniTags, function (_, tag) { tagDom += '#' + tag + ''; @@ -958,7 +980,8 @@ $('#tags').click(function () { $("#taglist").html(tagDom).slideToggle(500) } - adapter.listTags(renderTags, function () { + adapter.listTags(renderTags, function (err) { + console.error('[memos-bber] tags failed:', err) $.message({ message: msg('placeApiUrl') }) }) } else { @@ -1076,17 +1099,24 @@ $('#search').click(function () { $('#random').click(function () { get_info(function (info) { + console.log('[memos-bber] random click info:', info) if (info.status) { $("#randomlist").html('').hide() const adapter = getApiAdapter(info) + if (!adapter) { + console.warn('[memos-bber] no adapter for random') + return + } adapter.listRandomMemos( function (memos) { + console.log('[memos-bber] random result count:', memos && memos.length) let randomNum = Math.floor(Math.random() * (memos.length)); var randomData = memos[randomNum] randDom(randomData) }, - function () { + function (err) { + console.error('[memos-bber] random failed:', err) $.message({ message: msg('placeApiUrl') }) } )