4 Commits

Author SHA1 Message Date
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
35 changed files with 320 additions and 440 deletions
+3 -1
View File
@@ -1,5 +1,7 @@
一款通过浏览器插件发布 [Memos](https://usememos.com/) 的插件。基于 iSpeak-bber 修改,原作者为 [DreamyTZK](https://www.antmoe.com/)。
兼容 Memos v0.15.0 - v0.29.x,支持新版 Attachment API。
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/jonnyan404)
## 在线商店安装
@@ -18,4 +20,4 @@ Android
### 使用教程
- 在文本框输入想搜索的关键字,点击搜索按钮
- 随机和搜索功能在`0.24`以上版本支持私有权限的 memo,其它版本不支持
- 随机和搜索功能在 `0.24` 以上版本支持私有权限的 memo,其他版本不支持
+1
View File
@@ -1,4 +1,5 @@
## 更新日志
- 20260805 适配 Memos 0.29.x Attachment API,修复附件上传、绑定和预览地址;修复 0.20.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.29.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.29.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.29.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.29.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.29.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.29.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.29.x"
},
"settingsConnectionTitle": {
"message": "连接设置"
+33 -20
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
}
}
@@ -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)
}
}
@@ -364,10 +364,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 +398,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 +418,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 +460,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 +510,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)
})
}
+15 -8
View File
@@ -480,6 +480,15 @@ function buildV1ResourceStreamUrl(info, resource) {
}
if (root && !root.endsWith('/')) root += '/'
if (resource.externalLink) return resource.externalLink
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
const t = typeof r.type === 'string' ? r.type.toLowerCase() : ''
@@ -507,6 +516,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 +530,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 ''
}
+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.29.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.29.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.29.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.29.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.29.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.29.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.29.x"
},
"settingsConnectionTitle": {
"message": "连接设置"
+33 -20
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
}
}
@@ -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)
}
}
@@ -364,10 +364,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 +398,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 +418,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 +460,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 +510,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)
})
}
+15 -8
View File
@@ -480,6 +480,15 @@ function buildV1ResourceStreamUrl(info, resource) {
}
if (root && !root.endsWith('/')) root += '/'
if (resource.externalLink) return resource.externalLink
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
const t = typeof r.type === 'string' ? r.type.toLowerCase() : ''
@@ -507,6 +516,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 +530,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 ''
}
+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.29.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.29.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.29.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.29.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.29.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.29.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.29.x"
},
"settingsConnectionTitle": {
"message": "连接设置"
+33 -20
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
}
}
@@ -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)
}
}
@@ -364,10 +364,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 +398,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 +418,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 +460,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 +510,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)
})
}
+15 -8
View File
@@ -505,6 +505,15 @@ function buildV1ResourceStreamUrl(info, resource) {
}
if (root && !root.endsWith('/')) root += '/'
if (resource.externalLink) return resource.externalLink
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
const t = typeof r.type === 'string' ? r.type.toLowerCase() : ''
@@ -532,6 +541,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 +555,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 ''
}
+1 -1
View File
@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "__MSG_extName__",
"default_locale": "en",
"version": "2026.4.25",
"version": "2026.8.5",
"browser_action": {
"default_popup": "popup.html",
"default_icon": "assets/logo_24x24.png",