11 Commits

Author SHA1 Message Date
jonny ae9b37d87a Add changelog section to README
Reorganized README by removing the version compatibility info and adding a new Changelog section with a link to the change log file. Moved the Ko-fi donation button to be more prominent.
2026-08-05 17:26:29 +08:00
jonny c4da5f790a Update Memos version support text
Refresh the changelog and localized compatibility strings to reflect support for Memos 0.28.x-0.30.x, and correct the older image preview note to 0.19.x-0.23.x.
2026-08-05 17:20:16 +08:00
jonny 1361cc15db 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.
2026-08-05 17:07:59 +08:00
jonny afbb7ac5cc Fix 0.20.x image preview URLs by prioritizing uid check
Reorder fallback checks in buildV1ResourceStreamUrl to prioritize uid-like identifiers before numeric IDs. This fixes image preview URLs in Memos 0.20.x that incorrectly used numeric IDs instead of proper UIDs. Applied to Chrome, Edge, and Firefox extensions. Updated changelog accordingly.
2026-08-05 12:19:16 +08:00
jonny e01ea6b0df Merge pull request #7 from Windyskr/agent/support-memos-0.29-attachments
fix: 适配 Memos 0.29.x 附件 API
2026-08-05 11:24:14 +08:00
Windyskr 5b0483007a fix: 对齐 Memos 0.29 更新接口 2026-08-05 00:30:09 +08:00
Windyskr e4529814a4 fix: 适配 Memos 0.29.x 附件 API 2026-08-05 00:06:19 +08:00
jonny 776ee343f4 Update README.md 2026-06-06 21:08:47 +08:00
jonny 4ea7d19e28 Create FUNDING.yml 2026-06-06 20:54:50 +08:00
jonny 6221eebcc5 Firefox version has passed the app store review 2026-04-27 19:07:32 +08:00
jonny e65a2a1878 Update manifest.json 2026-04-24 18:56:03 +08:00
39 changed files with 614 additions and 489 deletions
+1
View File
@@ -0,0 +1 @@
ko_fi: jonnyan404
+8 -2
View File
@@ -1,9 +1,15 @@
一款通过浏览器插件发布 [Memos](https://usememos.com/) 的插件。基于 iSpeak-bber 修改,原作者为 [DreamyTZK](https://www.antmoe.com/)。
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/jonnyan404)
## 更新日志
- 当前支持memos `v0.15.0 - v0.30.x` [点击查看详细更新日志](https://raw.githubusercontent.com/Jonnyan404/memos-bber/refs/heads/main/change.log)
## 在线商店安装
- Chrome 应用商店:https://chrome.google.com/webstore/detail/memos-bber/cbhjebjfccgchgbmfbobjmebjjckgofe/
- (审核中)FireFox 应用商店: https://addons.mozilla.org/zh-CN/firefox/addon/memos-bber/
- FireFox 应用商店: https://addons.mozilla.org/zh-CN/firefox/addon/memos-bber/
- (等待上架)Edge: 使用离线安装包或者直接使用 chrome 扩展
## 移动端
@@ -16,4 +22,4 @@ Android
### 使用教程
- 在文本框输入想搜索的关键字,点击搜索按钮
- 随机和搜索功能在`0.24`以上版本支持私有权限的 memo,其它版本不支持
- 随机和搜索功能在 `0.24` 以上版本支持私有权限的 memo,其他版本不支持
+1
View File
@@ -1,4 +1,5 @@
## 更新日志
- 20260805 适配 Memos 0.28.x-0.30.x Attachment API,修复附件上传、绑定和预览地址;修复 0.19.x-0.23.x 图片预览 URL 错误使用 numeric id 的问题以及标签不显示问题
- 20260425 新增德语/法语/西班牙语支持
- 20260423 优化 firefox 抖动问题和支持手机版,支持 edge 浏览器扩展
- 20260422 调整发送设置,支持仅发送附件
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "Speichern"
},
"supportedMemosVersion": {
"message": "Kompatibel mit Memos v0.15.0 - 0.27.x"
"message": "Kompatibel mit Memos v0.15.0 - 0.30.x"
},
"settingsConnectionTitle": {
"message": "Verbindung"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "Save"
},
"supportedMemosVersion": {
"message": "Compatible with Memos v0.15.0 - 0.27.x"
"message": "Compatible with Memos v0.15.0 - 0.30.x"
},
"settingsConnectionTitle": {
"message": "Connection"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "Guardar"
},
"supportedMemosVersion": {
"message": "Compatible con Memos v0.15.0 - 0.27.x"
"message": "Compatible con Memos v0.15.0 - 0.30.x"
},
"settingsConnectionTitle": {
"message": "Conexión"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "Enregistrer"
},
"supportedMemosVersion": {
"message": "Compatible avec Memos v0.15.0 - 0.27.x"
"message": "Compatible avec Memos v0.15.0 - 0.30.x"
},
"settingsConnectionTitle": {
"message": "Connexion"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "保存"
},
"supportedMemosVersion": {
"message": "Memos v0.15.0 - 0.27.x に対応"
"message": "Memos v0.15.0 - 0.30.x に対応"
},
"settingsConnectionTitle": {
"message": "接続設定"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "저장"
},
"supportedMemosVersion": {
"message": "Memos v0.15.0 - 0.27.x 호환"
"message": "Memos v0.15.0 - 0.30.x 호환"
},
"settingsConnectionTitle": {
"message": "연결 설정"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "保存"
},
"supportedMemosVersion": {
"message": "兼容 Memos v0.15.0 - 0.27.x"
"message": "兼容 Memos v0.15.0 - 0.30.x"
},
"settingsConnectionTitle": {
"message": "连接设置"
+41 -25
View File
@@ -187,6 +187,7 @@
function normalizeUploadedItem(entity, fallbackFilename) {
if (!entity) return null
entity = entity.attachment || entity.resource || entity
const inferredId = (function () {
const value = entity.id != null ? entity.id : entity.ID != null ? entity.ID : entity.Id
if (typeof value === 'number' && Number.isFinite(value)) return Math.floor(value)
@@ -204,7 +205,11 @@
name: name,
filename: entity.filename || fallbackFilename || name,
createTime: entity.createTime || entity.createdTs || entity.createdAt,
type: entity.type
type: entity.type,
uid: entity.uid,
size: entity.size,
memo: entity.memo,
externalLink: entity.externalLink
}
}
@@ -244,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 },
@@ -259,15 +264,10 @@
return
}
if (global.MemosApiModern) {
global.MemosApiModern.fetchMemosWithFallback(
info,
'?pageSize=1000',
function (data) {
if (success) success(collectTags(info, extractMemos(data)))
},
fail
)
if (global.MemosApiV023) {
global.MemosApiV023.listMemos(info, { pageSize: 1000 }, function (data) {
if (success) success(collectTags(info, extractMemos(data)))
}, fail)
}
}
@@ -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
}
@@ -364,10 +367,12 @@
}
function uploadFile(file, options, success, fail) {
const oldName = String(file && file.name ? file.name : 'upload').split('.')
const fileExt = String(file && file.name ? file.name : '').split('.').pop()
const sourceName = String(file && file.name ? file.name : 'upload')
const lastDot = sourceName.lastIndexOf('.')
const filenameBase = lastDot > 0 ? sourceName.slice(0, lastDot) : sourceName
const fileExt = lastDot > 0 ? sourceName.slice(lastDot) : ''
const now = global.dayjs().format('YYYYMMDDHHmmss')
const nextName = oldName[0] + '_' + now + (fileExt ? '.' + fileExt : '')
const nextName = filenameBase + '_' + now + fileExt
if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021) {
global.MemosApiV020V021.uploadResourceBlob(
@@ -396,7 +401,7 @@
info,
payload,
function (resp) {
const entity = (resp && resp.resource) || resp
const entity = (resp && (resp.attachment || resp.resource)) || resp
if (success) success(normalizeUploadedItem(entity, nextName))
},
fail
@@ -416,9 +421,9 @@
}
requestJson({
url: info.apiUrl + 'api/v1/' + memoName,
url: info.apiUrl + 'api/v1/' + memoName + '?updateMask=state',
type: 'PATCH',
data: JSON.stringify({ state: 'ARCHIVED' }),
data: JSON.stringify({ name: memoName, state: 'ARCHIVED' }),
contentType: 'application/json',
dataType: 'json',
headers: { Authorization: 'Bearer ' + info.apiTokens }
@@ -458,24 +463,35 @@
return
}
const uploadedItems = Array.isArray(payload.resourceIdList) ? payload.resourceIdList : []
const attachmentReferences = uploadedItems
.filter(function (item) {
return item && typeof item.name === 'string' && item.name.indexOf('attachments/') === 0
})
.map(function (item) {
return { name: item.name }
})
const canCreateWithAttachments = flavor === 'modern' && attachmentReferences.length === uploadedItems.length
requestJson({
url: info.apiUrl + 'api/v1/memos',
type: 'POST',
data: JSON.stringify({
content: payload.content,
visibility: payload.visibility
visibility: payload.visibility,
...(canCreateWithAttachments ? { attachments: attachmentReferences } : {})
}),
contentType: 'application/json',
dataType: 'json',
headers: { Authorization: 'Bearer ' + info.apiTokens }
}, function (data) {
const createdName = data && data.name ? data.name : data && data.memo && data.memo.name ? data.memo.name : ''
const resources = Array.isArray(payload.resourceIdList) ? payload.resourceIdList : []
const resources = uploadedItems
if (!createdName) {
if (success) success(data)
return
}
if (resources.length === 0) {
if (resources.length === 0 || canCreateWithAttachments) {
getMemo(createdName, success, fail)
return
}
@@ -497,7 +513,7 @@
return {
flavor: flavor,
needsAuthenticatedImagePreview: function () {
return flavor === FLAVOR_V020_V021
return true
},
listTags: listTags,
searchMemos: searchMemos,
+47 -108
View File
@@ -151,10 +151,53 @@
return copy
}
function uploadResource() {
global.$
.ajax({
url: urlResources,
data: JSON.stringify(payload),
type: 'POST',
cache: false,
processData: false,
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'resources')
})
.fail(function (xhr) {
if (!xhr || xhr.status !== 400) {
if (onFail) onFail(xhr)
return
}
global.$
.ajax({
url: urlResources,
data: JSON.stringify(stripVisibility(payload)),
type: 'POST',
cache: false,
processData: false,
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'resources')
})
.fail(function (xhr2) {
if (onFail) onFail(xhr2)
})
})
}
// Attachment visibility was removed in Memos v0.26. Send the current
// attachment shape first and retain the resource endpoint as a legacy fallback.
global.$
.ajax({
url: urlAttachments,
data: JSON.stringify(payload),
data: JSON.stringify(stripVisibility(payload)),
type: 'POST',
cache: false,
processData: false,
@@ -166,115 +209,11 @@
onSuccess(data, 'attachments')
})
.fail(function (xhr) {
if (xhr && xhr.status === 400) {
global.$
.ajax({
url: urlAttachments,
data: JSON.stringify(stripVisibility(payload)),
type: 'POST',
cache: false,
processData: false,
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'attachments')
})
.fail(function (xhrRetry) {
if (!isNotFoundLikeXhr(xhrRetry)) {
if (onFail) onFail(xhrRetry)
return
}
// fall through to resources below
xhr = xhrRetry
if (!isNotFoundLikeXhr(xhr)) {
if (onFail) onFail(xhr)
return
}
global.$
.ajax({
url: urlResources,
data: JSON.stringify(payload),
type: 'POST',
cache: false,
processData: false,
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'resources')
})
.fail(function (xhr2) {
if (xhr2 && xhr2.status === 400) {
global.$
.ajax({
url: urlResources,
data: JSON.stringify(stripVisibility(payload)),
type: 'POST',
cache: false,
processData: false,
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'resources')
})
.fail(function (xhr3) {
if (onFail) onFail(xhr3)
})
return
}
if (onFail) onFail(xhr2)
})
})
if (isNotFoundLikeXhr(xhr)) {
uploadResource()
return
}
if (!isNotFoundLikeXhr(xhr)) {
if (onFail) onFail(xhr)
return
}
global.$
.ajax({
url: urlResources,
data: JSON.stringify(payload),
type: 'POST',
cache: false,
processData: false,
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'resources')
})
.fail(function (xhr2) {
if (xhr2 && xhr2.status === 400) {
global.$
.ajax({
url: urlResources,
data: JSON.stringify(stripVisibility(payload)),
type: 'POST',
cache: false,
processData: false,
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'resources')
})
.fail(function (xhr3) {
if (onFail) onFail(xhr3)
})
return
}
if (onFail) onFail(xhr2)
})
if (onFail) onFail(xhr)
})
}
+56 -9
View File
@@ -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)
+47 -10
View File
@@ -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 ''
}
@@ -480,6 +482,13 @@ function buildV1ResourceStreamUrl(info, resource) {
}
if (root && !root.endsWith('/')) root += '/'
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 : ''
function isImageResource(r) {
if (!r) return false
const t = typeof r.type === 'string' ? r.type.toLowerCase() : ''
@@ -488,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()
@@ -507,6 +532,11 @@ function buildV1ResourceStreamUrl(info, resource) {
const uid = typeof uidRaw === 'string' ? uidRaw : uidRaw != null ? String(uidRaw) : ''
if (uid.trim() !== '') return buildStreamUrl(uid.trim())
// Fallback for older resource shapes.
// In some memo payloads, the uid may appear as `name` directly.
// Example: name="ETU6hjuR..." should map to /o/r/:uid, not /file/:name/:filename.
if (isProbablyUid(name)) return buildStreamUrl(name.trim())
// Legacy versions (e.g. v0.18) may only expose numeric `id` without `uid/name`.
const idRaw = resource.id != null ? resource.id : resource.ID != null ? resource.ID : resource.Id
const id = typeof idRaw === 'number' && Number.isFinite(idRaw)
@@ -516,14 +546,7 @@ function buildV1ResourceStreamUrl(info, resource) {
: ''
if (id) return buildStreamUrl(id)
// Fallback for older resource shapes.
const name = typeof resource.name === 'string' ? resource.name : ''
// In some memo payloads, the uid may appear as `name` directly.
// Example: name="ETU6hjuR..." should map to /o/r/:uid, not /file/:name/:filename.
if (isProbablyUid(name)) return buildStreamUrl(name.trim())
const fileId = resource.publicId || resource.filename
const fileId = resource.publicId || filename
if (name && fileId) return root + 'file/' + name + '/' + fileId
return ''
}
@@ -913,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 += '<span class="item-container">#' + tag + '</span>';
@@ -926,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 {
@@ -1044,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') })
}
)
+1 -1
View File
@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "__MSG_extName__",
"default_locale": "en",
"version": "2026.04.25",
"version": "2026.08.05",
"action": {
"default_popup": "popup.html",
"default_icon": "assets/logo_24x24.png",
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "Speichern"
},
"supportedMemosVersion": {
"message": "Kompatibel mit Memos v0.15.0 - 0.27.x"
"message": "Kompatibel mit Memos v0.15.0 - 0.30.x"
},
"settingsConnectionTitle": {
"message": "Verbindung"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "Save"
},
"supportedMemosVersion": {
"message": "Compatible with Memos v0.15.0 - 0.27.x"
"message": "Compatible with Memos v0.15.0 - 0.30.x"
},
"settingsConnectionTitle": {
"message": "Connection"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "Guardar"
},
"supportedMemosVersion": {
"message": "Compatible con Memos v0.15.0 - 0.27.x"
"message": "Compatible con Memos v0.15.0 - 0.30.x"
},
"settingsConnectionTitle": {
"message": "Conexión"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "Enregistrer"
},
"supportedMemosVersion": {
"message": "Compatible avec Memos v0.15.0 - 0.27.x"
"message": "Compatible avec Memos v0.15.0 - 0.30.x"
},
"settingsConnectionTitle": {
"message": "Connexion"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "保存"
},
"supportedMemosVersion": {
"message": "Memos v0.15.0 - 0.27.x に対応"
"message": "Memos v0.15.0 - 0.30.x に対応"
},
"settingsConnectionTitle": {
"message": "接続設定"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "저장"
},
"supportedMemosVersion": {
"message": "Memos v0.15.0 - 0.27.x 호환"
"message": "Memos v0.15.0 - 0.30.x 호환"
},
"settingsConnectionTitle": {
"message": "연결 설정"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "保存"
},
"supportedMemosVersion": {
"message": "兼容 Memos v0.15.0 - 0.27.x"
"message": "兼容 Memos v0.15.0 - 0.30.x"
},
"settingsConnectionTitle": {
"message": "连接设置"
+41 -25
View File
@@ -187,6 +187,7 @@
function normalizeUploadedItem(entity, fallbackFilename) {
if (!entity) return null
entity = entity.attachment || entity.resource || entity
const inferredId = (function () {
const value = entity.id != null ? entity.id : entity.ID != null ? entity.ID : entity.Id
if (typeof value === 'number' && Number.isFinite(value)) return Math.floor(value)
@@ -204,7 +205,11 @@
name: name,
filename: entity.filename || fallbackFilename || name,
createTime: entity.createTime || entity.createdTs || entity.createdAt,
type: entity.type
type: entity.type,
uid: entity.uid,
size: entity.size,
memo: entity.memo,
externalLink: entity.externalLink
}
}
@@ -244,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 },
@@ -259,15 +264,10 @@
return
}
if (global.MemosApiModern) {
global.MemosApiModern.fetchMemosWithFallback(
info,
'?pageSize=1000',
function (data) {
if (success) success(collectTags(info, extractMemos(data)))
},
fail
)
if (global.MemosApiV023) {
global.MemosApiV023.listMemos(info, { pageSize: 1000 }, function (data) {
if (success) success(collectTags(info, extractMemos(data)))
}, fail)
}
}
@@ -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
}
@@ -364,10 +367,12 @@
}
function uploadFile(file, options, success, fail) {
const oldName = String(file && file.name ? file.name : 'upload').split('.')
const fileExt = String(file && file.name ? file.name : '').split('.').pop()
const sourceName = String(file && file.name ? file.name : 'upload')
const lastDot = sourceName.lastIndexOf('.')
const filenameBase = lastDot > 0 ? sourceName.slice(0, lastDot) : sourceName
const fileExt = lastDot > 0 ? sourceName.slice(lastDot) : ''
const now = global.dayjs().format('YYYYMMDDHHmmss')
const nextName = oldName[0] + '_' + now + (fileExt ? '.' + fileExt : '')
const nextName = filenameBase + '_' + now + fileExt
if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021) {
global.MemosApiV020V021.uploadResourceBlob(
@@ -396,7 +401,7 @@
info,
payload,
function (resp) {
const entity = (resp && resp.resource) || resp
const entity = (resp && (resp.attachment || resp.resource)) || resp
if (success) success(normalizeUploadedItem(entity, nextName))
},
fail
@@ -416,9 +421,9 @@
}
requestJson({
url: info.apiUrl + 'api/v1/' + memoName,
url: info.apiUrl + 'api/v1/' + memoName + '?updateMask=state',
type: 'PATCH',
data: JSON.stringify({ state: 'ARCHIVED' }),
data: JSON.stringify({ name: memoName, state: 'ARCHIVED' }),
contentType: 'application/json',
dataType: 'json',
headers: { Authorization: 'Bearer ' + info.apiTokens }
@@ -458,24 +463,35 @@
return
}
const uploadedItems = Array.isArray(payload.resourceIdList) ? payload.resourceIdList : []
const attachmentReferences = uploadedItems
.filter(function (item) {
return item && typeof item.name === 'string' && item.name.indexOf('attachments/') === 0
})
.map(function (item) {
return { name: item.name }
})
const canCreateWithAttachments = flavor === 'modern' && attachmentReferences.length === uploadedItems.length
requestJson({
url: info.apiUrl + 'api/v1/memos',
type: 'POST',
data: JSON.stringify({
content: payload.content,
visibility: payload.visibility
visibility: payload.visibility,
...(canCreateWithAttachments ? { attachments: attachmentReferences } : {})
}),
contentType: 'application/json',
dataType: 'json',
headers: { Authorization: 'Bearer ' + info.apiTokens }
}, function (data) {
const createdName = data && data.name ? data.name : data && data.memo && data.memo.name ? data.memo.name : ''
const resources = Array.isArray(payload.resourceIdList) ? payload.resourceIdList : []
const resources = uploadedItems
if (!createdName) {
if (success) success(data)
return
}
if (resources.length === 0) {
if (resources.length === 0 || canCreateWithAttachments) {
getMemo(createdName, success, fail)
return
}
@@ -497,7 +513,7 @@
return {
flavor: flavor,
needsAuthenticatedImagePreview: function () {
return flavor === FLAVOR_V020_V021
return true
},
listTags: listTags,
searchMemos: searchMemos,
+47 -108
View File
@@ -151,10 +151,53 @@
return copy
}
function uploadResource() {
global.$
.ajax({
url: urlResources,
data: JSON.stringify(payload),
type: 'POST',
cache: false,
processData: false,
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'resources')
})
.fail(function (xhr) {
if (!xhr || xhr.status !== 400) {
if (onFail) onFail(xhr)
return
}
global.$
.ajax({
url: urlResources,
data: JSON.stringify(stripVisibility(payload)),
type: 'POST',
cache: false,
processData: false,
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'resources')
})
.fail(function (xhr2) {
if (onFail) onFail(xhr2)
})
})
}
// Attachment visibility was removed in Memos v0.26. Send the current
// attachment shape first and retain the resource endpoint as a legacy fallback.
global.$
.ajax({
url: urlAttachments,
data: JSON.stringify(payload),
data: JSON.stringify(stripVisibility(payload)),
type: 'POST',
cache: false,
processData: false,
@@ -166,115 +209,11 @@
onSuccess(data, 'attachments')
})
.fail(function (xhr) {
if (xhr && xhr.status === 400) {
global.$
.ajax({
url: urlAttachments,
data: JSON.stringify(stripVisibility(payload)),
type: 'POST',
cache: false,
processData: false,
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'attachments')
})
.fail(function (xhrRetry) {
if (!isNotFoundLikeXhr(xhrRetry)) {
if (onFail) onFail(xhrRetry)
return
}
// fall through to resources below
xhr = xhrRetry
if (!isNotFoundLikeXhr(xhr)) {
if (onFail) onFail(xhr)
return
}
global.$
.ajax({
url: urlResources,
data: JSON.stringify(payload),
type: 'POST',
cache: false,
processData: false,
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'resources')
})
.fail(function (xhr2) {
if (xhr2 && xhr2.status === 400) {
global.$
.ajax({
url: urlResources,
data: JSON.stringify(stripVisibility(payload)),
type: 'POST',
cache: false,
processData: false,
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'resources')
})
.fail(function (xhr3) {
if (onFail) onFail(xhr3)
})
return
}
if (onFail) onFail(xhr2)
})
})
if (isNotFoundLikeXhr(xhr)) {
uploadResource()
return
}
if (!isNotFoundLikeXhr(xhr)) {
if (onFail) onFail(xhr)
return
}
global.$
.ajax({
url: urlResources,
data: JSON.stringify(payload),
type: 'POST',
cache: false,
processData: false,
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'resources')
})
.fail(function (xhr2) {
if (xhr2 && xhr2.status === 400) {
global.$
.ajax({
url: urlResources,
data: JSON.stringify(stripVisibility(payload)),
type: 'POST',
cache: false,
processData: false,
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'resources')
})
.fail(function (xhr3) {
if (onFail) onFail(xhr3)
})
return
}
if (onFail) onFail(xhr2)
})
if (onFail) onFail(xhr)
})
}
+56 -9
View File
@@ -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)
+47 -10
View File
@@ -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 ''
}
@@ -480,6 +482,13 @@ function buildV1ResourceStreamUrl(info, resource) {
}
if (root && !root.endsWith('/')) root += '/'
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 : ''
function isImageResource(r) {
if (!r) return false
const t = typeof r.type === 'string' ? r.type.toLowerCase() : ''
@@ -488,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()
@@ -507,6 +532,11 @@ function buildV1ResourceStreamUrl(info, resource) {
const uid = typeof uidRaw === 'string' ? uidRaw : uidRaw != null ? String(uidRaw) : ''
if (uid.trim() !== '') return buildStreamUrl(uid.trim())
// Fallback for older resource shapes.
// In some memo payloads, the uid may appear as `name` directly.
// Example: name="ETU6hjuR..." should map to /o/r/:uid, not /file/:name/:filename.
if (isProbablyUid(name)) return buildStreamUrl(name.trim())
// Legacy versions (e.g. v0.18) may only expose numeric `id` without `uid/name`.
const idRaw = resource.id != null ? resource.id : resource.ID != null ? resource.ID : resource.Id
const id = typeof idRaw === 'number' && Number.isFinite(idRaw)
@@ -516,14 +546,7 @@ function buildV1ResourceStreamUrl(info, resource) {
: ''
if (id) return buildStreamUrl(id)
// Fallback for older resource shapes.
const name = typeof resource.name === 'string' ? resource.name : ''
// In some memo payloads, the uid may appear as `name` directly.
// Example: name="ETU6hjuR..." should map to /o/r/:uid, not /file/:name/:filename.
if (isProbablyUid(name)) return buildStreamUrl(name.trim())
const fileId = resource.publicId || resource.filename
const fileId = resource.publicId || filename
if (name && fileId) return root + 'file/' + name + '/' + fileId
return ''
}
@@ -913,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 += '<span class="item-container">#' + tag + '</span>';
@@ -926,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 {
@@ -1044,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') })
}
)
+1 -1
View File
@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "__MSG_extName__",
"default_locale": "en",
"version": "2026.04.25",
"version": "2026.08.05",
"action": {
"default_popup": "popup.html",
"default_icon": "assets/logo_24x24.png",
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "Speichern"
},
"supportedMemosVersion": {
"message": "Kompatibel mit Memos v0.15.0 - 0.27.x"
"message": "Kompatibel mit Memos v0.15.0 - 0.30.x"
},
"settingsConnectionTitle": {
"message": "Verbindung"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "Save"
},
"supportedMemosVersion": {
"message": "Compatible with Memos v0.15.0 - 0.27.x"
"message": "Compatible with Memos v0.15.0 - 0.30.x"
},
"settingsConnectionTitle": {
"message": "Connection"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "Guardar"
},
"supportedMemosVersion": {
"message": "Compatible con Memos v0.15.0 - 0.27.x"
"message": "Compatible con Memos v0.15.0 - 0.30.x"
},
"settingsConnectionTitle": {
"message": "Conexión"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "Enregistrer"
},
"supportedMemosVersion": {
"message": "Compatible avec Memos v0.15.0 - 0.27.x"
"message": "Compatible avec Memos v0.15.0 - 0.30.x"
},
"settingsConnectionTitle": {
"message": "Connexion"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "保存"
},
"supportedMemosVersion": {
"message": "Memos v0.15.0 - 0.27.x に対応"
"message": "Memos v0.15.0 - 0.30.x に対応"
},
"settingsConnectionTitle": {
"message": "接続設定"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "저장"
},
"supportedMemosVersion": {
"message": "Memos v0.15.0 - 0.27.x 호환"
"message": "Memos v0.15.0 - 0.30.x 호환"
},
"settingsConnectionTitle": {
"message": "연결 설정"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "保存"
},
"supportedMemosVersion": {
"message": "兼容 Memos v0.15.0 - 0.27.x"
"message": "兼容 Memos v0.15.0 - 0.30.x"
},
"settingsConnectionTitle": {
"message": "连接设置"
+41 -25
View File
@@ -187,6 +187,7 @@
function normalizeUploadedItem(entity, fallbackFilename) {
if (!entity) return null
entity = entity.attachment || entity.resource || entity
const inferredId = (function () {
const value = entity.id != null ? entity.id : entity.ID != null ? entity.ID : entity.Id
if (typeof value === 'number' && Number.isFinite(value)) return Math.floor(value)
@@ -204,7 +205,11 @@
name: name,
filename: entity.filename || fallbackFilename || name,
createTime: entity.createTime || entity.createdTs || entity.createdAt,
type: entity.type
type: entity.type,
uid: entity.uid,
size: entity.size,
memo: entity.memo,
externalLink: entity.externalLink
}
}
@@ -244,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 },
@@ -259,15 +264,10 @@
return
}
if (global.MemosApiModern) {
global.MemosApiModern.fetchMemosWithFallback(
info,
'?pageSize=1000',
function (data) {
if (success) success(collectTags(info, extractMemos(data)))
},
fail
)
if (global.MemosApiV023) {
global.MemosApiV023.listMemos(info, { pageSize: 1000 }, function (data) {
if (success) success(collectTags(info, extractMemos(data)))
}, fail)
}
}
@@ -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
}
@@ -364,10 +367,12 @@
}
function uploadFile(file, options, success, fail) {
const oldName = String(file && file.name ? file.name : 'upload').split('.')
const fileExt = String(file && file.name ? file.name : '').split('.').pop()
const sourceName = String(file && file.name ? file.name : 'upload')
const lastDot = sourceName.lastIndexOf('.')
const filenameBase = lastDot > 0 ? sourceName.slice(0, lastDot) : sourceName
const fileExt = lastDot > 0 ? sourceName.slice(lastDot) : ''
const now = global.dayjs().format('YYYYMMDDHHmmss')
const nextName = oldName[0] + '_' + now + (fileExt ? '.' + fileExt : '')
const nextName = filenameBase + '_' + now + fileExt
if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021) {
global.MemosApiV020V021.uploadResourceBlob(
@@ -396,7 +401,7 @@
info,
payload,
function (resp) {
const entity = (resp && resp.resource) || resp
const entity = (resp && (resp.attachment || resp.resource)) || resp
if (success) success(normalizeUploadedItem(entity, nextName))
},
fail
@@ -416,9 +421,9 @@
}
requestJson({
url: info.apiUrl + 'api/v1/' + memoName,
url: info.apiUrl + 'api/v1/' + memoName + '?updateMask=state',
type: 'PATCH',
data: JSON.stringify({ state: 'ARCHIVED' }),
data: JSON.stringify({ name: memoName, state: 'ARCHIVED' }),
contentType: 'application/json',
dataType: 'json',
headers: { Authorization: 'Bearer ' + info.apiTokens }
@@ -458,24 +463,35 @@
return
}
const uploadedItems = Array.isArray(payload.resourceIdList) ? payload.resourceIdList : []
const attachmentReferences = uploadedItems
.filter(function (item) {
return item && typeof item.name === 'string' && item.name.indexOf('attachments/') === 0
})
.map(function (item) {
return { name: item.name }
})
const canCreateWithAttachments = flavor === 'modern' && attachmentReferences.length === uploadedItems.length
requestJson({
url: info.apiUrl + 'api/v1/memos',
type: 'POST',
data: JSON.stringify({
content: payload.content,
visibility: payload.visibility
visibility: payload.visibility,
...(canCreateWithAttachments ? { attachments: attachmentReferences } : {})
}),
contentType: 'application/json',
dataType: 'json',
headers: { Authorization: 'Bearer ' + info.apiTokens }
}, function (data) {
const createdName = data && data.name ? data.name : data && data.memo && data.memo.name ? data.memo.name : ''
const resources = Array.isArray(payload.resourceIdList) ? payload.resourceIdList : []
const resources = uploadedItems
if (!createdName) {
if (success) success(data)
return
}
if (resources.length === 0) {
if (resources.length === 0 || canCreateWithAttachments) {
getMemo(createdName, success, fail)
return
}
@@ -497,7 +513,7 @@
return {
flavor: flavor,
needsAuthenticatedImagePreview: function () {
return flavor === FLAVOR_V020_V021
return true
},
listTags: listTags,
searchMemos: searchMemos,
+47 -108
View File
@@ -151,10 +151,53 @@
return copy
}
function uploadResource() {
global.$
.ajax({
url: urlResources,
data: JSON.stringify(payload),
type: 'POST',
cache: false,
processData: false,
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'resources')
})
.fail(function (xhr) {
if (!xhr || xhr.status !== 400) {
if (onFail) onFail(xhr)
return
}
global.$
.ajax({
url: urlResources,
data: JSON.stringify(stripVisibility(payload)),
type: 'POST',
cache: false,
processData: false,
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'resources')
})
.fail(function (xhr2) {
if (onFail) onFail(xhr2)
})
})
}
// Attachment visibility was removed in Memos v0.26. Send the current
// attachment shape first and retain the resource endpoint as a legacy fallback.
global.$
.ajax({
url: urlAttachments,
data: JSON.stringify(payload),
data: JSON.stringify(stripVisibility(payload)),
type: 'POST',
cache: false,
processData: false,
@@ -166,115 +209,11 @@
onSuccess(data, 'attachments')
})
.fail(function (xhr) {
if (xhr && xhr.status === 400) {
global.$
.ajax({
url: urlAttachments,
data: JSON.stringify(stripVisibility(payload)),
type: 'POST',
cache: false,
processData: false,
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'attachments')
})
.fail(function (xhrRetry) {
if (!isNotFoundLikeXhr(xhrRetry)) {
if (onFail) onFail(xhrRetry)
return
}
// fall through to resources below
xhr = xhrRetry
if (!isNotFoundLikeXhr(xhr)) {
if (onFail) onFail(xhr)
return
}
global.$
.ajax({
url: urlResources,
data: JSON.stringify(payload),
type: 'POST',
cache: false,
processData: false,
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'resources')
})
.fail(function (xhr2) {
if (xhr2 && xhr2.status === 400) {
global.$
.ajax({
url: urlResources,
data: JSON.stringify(stripVisibility(payload)),
type: 'POST',
cache: false,
processData: false,
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'resources')
})
.fail(function (xhr3) {
if (onFail) onFail(xhr3)
})
return
}
if (onFail) onFail(xhr2)
})
})
if (isNotFoundLikeXhr(xhr)) {
uploadResource()
return
}
if (!isNotFoundLikeXhr(xhr)) {
if (onFail) onFail(xhr)
return
}
global.$
.ajax({
url: urlResources,
data: JSON.stringify(payload),
type: 'POST',
cache: false,
processData: false,
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'resources')
})
.fail(function (xhr2) {
if (xhr2 && xhr2.status === 400) {
global.$
.ajax({
url: urlResources,
data: JSON.stringify(stripVisibility(payload)),
type: 'POST',
cache: false,
processData: false,
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'resources')
})
.fail(function (xhr3) {
if (onFail) onFail(xhr3)
})
return
}
if (onFail) onFail(xhr2)
})
if (onFail) onFail(xhr)
})
}
+56 -9
View File
@@ -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)
+47 -10
View File
@@ -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 ''
}
@@ -505,6 +507,13 @@ function buildV1ResourceStreamUrl(info, resource) {
}
if (root && !root.endsWith('/')) root += '/'
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 : ''
function isImageResource(r) {
if (!r) return false
const t = typeof r.type === 'string' ? r.type.toLowerCase() : ''
@@ -513,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()
@@ -532,6 +557,11 @@ function buildV1ResourceStreamUrl(info, resource) {
const uid = typeof uidRaw === 'string' ? uidRaw : uidRaw != null ? String(uidRaw) : ''
if (uid.trim() !== '') return buildStreamUrl(uid.trim())
// Fallback for older resource shapes.
// In some memo payloads, the uid may appear as `name` directly.
// Example: name="ETU6hjuR..." should map to /o/r/:uid, not /file/:name/:filename.
if (isProbablyUid(name)) return buildStreamUrl(name.trim())
// Legacy versions (e.g. v0.18) may only expose numeric `id` without `uid/name`.
const idRaw = resource.id != null ? resource.id : resource.ID != null ? resource.ID : resource.Id
const id = typeof idRaw === 'number' && Number.isFinite(idRaw)
@@ -541,14 +571,7 @@ function buildV1ResourceStreamUrl(info, resource) {
: ''
if (id) return buildStreamUrl(id)
// Fallback for older resource shapes.
const name = typeof resource.name === 'string' ? resource.name : ''
// In some memo payloads, the uid may appear as `name` directly.
// Example: name="ETU6hjuR..." should map to /o/r/:uid, not /file/:name/:filename.
if (isProbablyUid(name)) return buildStreamUrl(name.trim())
const fileId = resource.publicId || resource.filename
const fileId = resource.publicId || filename
if (name && fileId) return root + 'file/' + name + '/' + fileId
return ''
}
@@ -938,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 += '<span class="item-container">#' + tag + '</span>';
@@ -951,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 {
@@ -1069,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') })
}
)
+1 -1
View File
@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "__MSG_extName__",
"default_locale": "en",
"version": "2026.04.25",
"version": "2026.8.5",
"browser_action": {
"default_popup": "popup.html",
"default_icon": "assets/logo_24x24.png",