Merge pull request #7 from Windyskr/agent/support-memos-0.29-attachments

fix: 适配 Memos 0.29.x 附件 API
This commit is contained in:
jonny
2026-08-05 11:24:14 +08:00
committed by GitHub
35 changed files with 305 additions and 425 deletions
+3 -1
View File
@@ -1,5 +1,7 @@
一款通过浏览器插件发布 [Memos](https://usememos.com/) 的插件。基于 iSpeak-bber 修改,原作者为 [DreamyTZK](https://www.antmoe.com/)。 一款通过浏览器插件发布 [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) [![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,其他版本不支持
+2 -1
View File
@@ -1,4 +1,5 @@
## 更新日志 ## 更新日志
- 20260805 适配 Memos 0.29.x Attachment API,修复附件上传、绑定和预览地址
- 20260425 新增德语/法语/西班牙语支持 - 20260425 新增德语/法语/西班牙语支持
- 20260423 优化 firefox 抖动问题和支持手机版,支持 edge 浏览器扩展 - 20260423 优化 firefox 抖动问题和支持手机版,支持 edge 浏览器扩展
- 20260422 调整发送设置,支持仅发送附件 - 20260422 调整发送设置,支持仅发送附件
@@ -69,4 +70,4 @@
2022.10.24 添加 visiblity 发送设置。 2022.10.24 添加 visiblity 发送设置。
</details> </details>
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "Speichern" "message": "Speichern"
}, },
"supportedMemosVersion": { "supportedMemosVersion": {
"message": "Kompatibel mit Memos v0.15.0 - 0.27.x" "message": "Kompatibel mit Memos v0.15.0 - 0.29.x"
}, },
"settingsConnectionTitle": { "settingsConnectionTitle": {
"message": "Verbindung" "message": "Verbindung"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "Save" "message": "Save"
}, },
"supportedMemosVersion": { "supportedMemosVersion": {
"message": "Compatible with Memos v0.15.0 - 0.27.x" "message": "Compatible with Memos v0.15.0 - 0.29.x"
}, },
"settingsConnectionTitle": { "settingsConnectionTitle": {
"message": "Connection" "message": "Connection"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "Guardar" "message": "Guardar"
}, },
"supportedMemosVersion": { "supportedMemosVersion": {
"message": "Compatible con Memos v0.15.0 - 0.27.x" "message": "Compatible con Memos v0.15.0 - 0.29.x"
}, },
"settingsConnectionTitle": { "settingsConnectionTitle": {
"message": "Conexión" "message": "Conexión"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "Enregistrer" "message": "Enregistrer"
}, },
"supportedMemosVersion": { "supportedMemosVersion": {
"message": "Compatible avec Memos v0.15.0 - 0.27.x" "message": "Compatible avec Memos v0.15.0 - 0.29.x"
}, },
"settingsConnectionTitle": { "settingsConnectionTitle": {
"message": "Connexion" "message": "Connexion"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "保存" "message": "保存"
}, },
"supportedMemosVersion": { "supportedMemosVersion": {
"message": "Memos v0.15.0 - 0.27.x に対応" "message": "Memos v0.15.0 - 0.29.x に対応"
}, },
"settingsConnectionTitle": { "settingsConnectionTitle": {
"message": "接続設定" "message": "接続設定"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "저장" "message": "저장"
}, },
"supportedMemosVersion": { "supportedMemosVersion": {
"message": "Memos v0.15.0 - 0.27.x 호환" "message": "Memos v0.15.0 - 0.29.x 호환"
}, },
"settingsConnectionTitle": { "settingsConnectionTitle": {
"message": "연결 설정" "message": "연결 설정"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "保存" "message": "保存"
}, },
"supportedMemosVersion": { "supportedMemosVersion": {
"message": "兼容 Memos v0.15.0 - 0.27.x" "message": "兼容 Memos v0.15.0 - 0.29.x"
}, },
"settingsConnectionTitle": { "settingsConnectionTitle": {
"message": "连接设置" "message": "连接设置"
+34 -21
View File
@@ -187,6 +187,7 @@
function normalizeUploadedItem(entity, fallbackFilename) { function normalizeUploadedItem(entity, fallbackFilename) {
if (!entity) return null if (!entity) return null
entity = entity.attachment || entity.resource || entity
const inferredId = (function () { const inferredId = (function () {
const value = entity.id != null ? entity.id : entity.ID != null ? entity.ID : entity.Id 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) if (typeof value === 'number' && Number.isFinite(value)) return Math.floor(value)
@@ -204,7 +205,11 @@
name: name, name: name,
filename: entity.filename || fallbackFilename || name, filename: entity.filename || fallbackFilename || name,
createTime: entity.createTime || entity.createdTs || entity.createdAt, 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 return
} }
if (global.MemosApiModern) { if (global.MemosApiV023) {
global.MemosApiModern.fetchMemosWithFallback( global.MemosApiV023.listMemos(info, { pageSize: 1000 }, function (data) {
info, if (success) success(collectTags(info, extractMemos(data)))
'?pageSize=1000', }, fail)
function (data) {
if (success) success(collectTags(info, extractMemos(data)))
},
fail
)
} }
} }
@@ -364,10 +364,12 @@
} }
function uploadFile(file, options, success, fail) { function uploadFile(file, options, success, fail) {
const oldName = String(file && file.name ? file.name : 'upload').split('.') const sourceName = String(file && file.name ? file.name : 'upload')
const fileExt = String(file && file.name ? file.name : '').split('.').pop() 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 now = global.dayjs().format('YYYYMMDDHHmmss')
const nextName = oldName[0] + '_' + now + (fileExt ? '.' + fileExt : '') const nextName = filenameBase + '_' + now + fileExt
if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021) { if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021) {
global.MemosApiV020V021.uploadResourceBlob( global.MemosApiV020V021.uploadResourceBlob(
@@ -396,7 +398,7 @@
info, info,
payload, payload,
function (resp) { function (resp) {
const entity = (resp && resp.resource) || resp const entity = (resp && (resp.attachment || resp.resource)) || resp
if (success) success(normalizeUploadedItem(entity, nextName)) if (success) success(normalizeUploadedItem(entity, nextName))
}, },
fail fail
@@ -416,9 +418,9 @@
} }
requestJson({ requestJson({
url: info.apiUrl + 'api/v1/' + memoName, url: info.apiUrl + 'api/v1/' + memoName + '?updateMask=state',
type: 'PATCH', type: 'PATCH',
data: JSON.stringify({ state: 'ARCHIVED' }), data: JSON.stringify({ name: memoName, state: 'ARCHIVED' }),
contentType: 'application/json', contentType: 'application/json',
dataType: 'json', dataType: 'json',
headers: { Authorization: 'Bearer ' + info.apiTokens } headers: { Authorization: 'Bearer ' + info.apiTokens }
@@ -458,24 +460,35 @@
return 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({ requestJson({
url: info.apiUrl + 'api/v1/memos', url: info.apiUrl + 'api/v1/memos',
type: 'POST', type: 'POST',
data: JSON.stringify({ data: JSON.stringify({
content: payload.content, content: payload.content,
visibility: payload.visibility visibility: payload.visibility,
...(canCreateWithAttachments ? { attachments: attachmentReferences } : {})
}), }),
contentType: 'application/json', contentType: 'application/json',
dataType: 'json', dataType: 'json',
headers: { Authorization: 'Bearer ' + info.apiTokens } headers: { Authorization: 'Bearer ' + info.apiTokens }
}, function (data) { }, function (data) {
const createdName = data && data.name ? data.name : data && data.memo && data.memo.name ? data.memo.name : '' 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 (!createdName) {
if (success) success(data) if (success) success(data)
return return
} }
if (resources.length === 0) { if (resources.length === 0 || canCreateWithAttachments) {
getMemo(createdName, success, fail) getMemo(createdName, success, fail)
return return
} }
@@ -497,7 +510,7 @@
return { return {
flavor: flavor, flavor: flavor,
needsAuthenticatedImagePreview: function () { needsAuthenticatedImagePreview: function () {
return flavor === FLAVOR_V020_V021 return true
}, },
listTags: listTags, listTags: listTags,
searchMemos: searchMemos, searchMemos: searchMemos,
@@ -518,4 +531,4 @@
probeFlavor: probeFlavor, probeFlavor: probeFlavor,
resolve: resolve resolve: resolve
} }
})(window) })(window)
+47 -108
View File
@@ -151,10 +151,53 @@
return copy 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.$ global.$
.ajax({ .ajax({
url: urlAttachments, url: urlAttachments,
data: JSON.stringify(payload), data: JSON.stringify(stripVisibility(payload)),
type: 'POST', type: 'POST',
cache: false, cache: false,
processData: false, processData: false,
@@ -166,115 +209,11 @@
onSuccess(data, 'attachments') onSuccess(data, 'attachments')
}) })
.fail(function (xhr) { .fail(function (xhr) {
if (xhr && xhr.status === 400) { if (isNotFoundLikeXhr(xhr)) {
global.$ uploadResource()
.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)
})
})
return return
} }
if (onFail) onFail(xhr)
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)
})
}) })
} }
+11 -4
View File
@@ -480,6 +480,15 @@ function buildV1ResourceStreamUrl(info, resource) {
} }
if (root && !root.endsWith('/')) root += '/' 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) { function isImageResource(r) {
if (!r) return false if (!r) return false
const t = typeof r.type === 'string' ? r.type.toLowerCase() : '' const t = typeof r.type === 'string' ? r.type.toLowerCase() : ''
@@ -517,13 +526,11 @@ function buildV1ResourceStreamUrl(info, resource) {
if (id) return buildStreamUrl(id) if (id) return buildStreamUrl(id)
// Fallback for older resource shapes. // Fallback for older resource shapes.
const name = typeof resource.name === 'string' ? resource.name : ''
// In some memo payloads, the uid may appear as `name` directly. // In some memo payloads, the uid may appear as `name` directly.
// Example: name="ETU6hjuR..." should map to /o/r/:uid, not /file/:name/:filename. // Example: name="ETU6hjuR..." should map to /o/r/:uid, not /file/:name/:filename.
if (isProbablyUid(name)) return buildStreamUrl(name.trim()) 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 if (name && fileId) return root + 'file/' + name + '/' + fileId
return '' return ''
} }
@@ -1283,4 +1290,4 @@ function sendText(preparedContent) {
}) })
} }
}) })
} }
+1 -1
View File
@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "__MSG_extName__", "name": "__MSG_extName__",
"default_locale": "en", "default_locale": "en",
"version": "2026.04.25", "version": "2026.08.05",
"action": { "action": {
"default_popup": "popup.html", "default_popup": "popup.html",
"default_icon": "assets/logo_24x24.png", "default_icon": "assets/logo_24x24.png",
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "Speichern" "message": "Speichern"
}, },
"supportedMemosVersion": { "supportedMemosVersion": {
"message": "Kompatibel mit Memos v0.15.0 - 0.27.x" "message": "Kompatibel mit Memos v0.15.0 - 0.29.x"
}, },
"settingsConnectionTitle": { "settingsConnectionTitle": {
"message": "Verbindung" "message": "Verbindung"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "Save" "message": "Save"
}, },
"supportedMemosVersion": { "supportedMemosVersion": {
"message": "Compatible with Memos v0.15.0 - 0.27.x" "message": "Compatible with Memos v0.15.0 - 0.29.x"
}, },
"settingsConnectionTitle": { "settingsConnectionTitle": {
"message": "Connection" "message": "Connection"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "Guardar" "message": "Guardar"
}, },
"supportedMemosVersion": { "supportedMemosVersion": {
"message": "Compatible con Memos v0.15.0 - 0.27.x" "message": "Compatible con Memos v0.15.0 - 0.29.x"
}, },
"settingsConnectionTitle": { "settingsConnectionTitle": {
"message": "Conexión" "message": "Conexión"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "Enregistrer" "message": "Enregistrer"
}, },
"supportedMemosVersion": { "supportedMemosVersion": {
"message": "Compatible avec Memos v0.15.0 - 0.27.x" "message": "Compatible avec Memos v0.15.0 - 0.29.x"
}, },
"settingsConnectionTitle": { "settingsConnectionTitle": {
"message": "Connexion" "message": "Connexion"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "保存" "message": "保存"
}, },
"supportedMemosVersion": { "supportedMemosVersion": {
"message": "Memos v0.15.0 - 0.27.x に対応" "message": "Memos v0.15.0 - 0.29.x に対応"
}, },
"settingsConnectionTitle": { "settingsConnectionTitle": {
"message": "接続設定" "message": "接続設定"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "저장" "message": "저장"
}, },
"supportedMemosVersion": { "supportedMemosVersion": {
"message": "Memos v0.15.0 - 0.27.x 호환" "message": "Memos v0.15.0 - 0.29.x 호환"
}, },
"settingsConnectionTitle": { "settingsConnectionTitle": {
"message": "연결 설정" "message": "연결 설정"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "保存" "message": "保存"
}, },
"supportedMemosVersion": { "supportedMemosVersion": {
"message": "兼容 Memos v0.15.0 - 0.27.x" "message": "兼容 Memos v0.15.0 - 0.29.x"
}, },
"settingsConnectionTitle": { "settingsConnectionTitle": {
"message": "连接设置" "message": "连接设置"
+34 -21
View File
@@ -187,6 +187,7 @@
function normalizeUploadedItem(entity, fallbackFilename) { function normalizeUploadedItem(entity, fallbackFilename) {
if (!entity) return null if (!entity) return null
entity = entity.attachment || entity.resource || entity
const inferredId = (function () { const inferredId = (function () {
const value = entity.id != null ? entity.id : entity.ID != null ? entity.ID : entity.Id 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) if (typeof value === 'number' && Number.isFinite(value)) return Math.floor(value)
@@ -204,7 +205,11 @@
name: name, name: name,
filename: entity.filename || fallbackFilename || name, filename: entity.filename || fallbackFilename || name,
createTime: entity.createTime || entity.createdTs || entity.createdAt, 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 return
} }
if (global.MemosApiModern) { if (global.MemosApiV023) {
global.MemosApiModern.fetchMemosWithFallback( global.MemosApiV023.listMemos(info, { pageSize: 1000 }, function (data) {
info, if (success) success(collectTags(info, extractMemos(data)))
'?pageSize=1000', }, fail)
function (data) {
if (success) success(collectTags(info, extractMemos(data)))
},
fail
)
} }
} }
@@ -364,10 +364,12 @@
} }
function uploadFile(file, options, success, fail) { function uploadFile(file, options, success, fail) {
const oldName = String(file && file.name ? file.name : 'upload').split('.') const sourceName = String(file && file.name ? file.name : 'upload')
const fileExt = String(file && file.name ? file.name : '').split('.').pop() 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 now = global.dayjs().format('YYYYMMDDHHmmss')
const nextName = oldName[0] + '_' + now + (fileExt ? '.' + fileExt : '') const nextName = filenameBase + '_' + now + fileExt
if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021) { if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021) {
global.MemosApiV020V021.uploadResourceBlob( global.MemosApiV020V021.uploadResourceBlob(
@@ -396,7 +398,7 @@
info, info,
payload, payload,
function (resp) { function (resp) {
const entity = (resp && resp.resource) || resp const entity = (resp && (resp.attachment || resp.resource)) || resp
if (success) success(normalizeUploadedItem(entity, nextName)) if (success) success(normalizeUploadedItem(entity, nextName))
}, },
fail fail
@@ -416,9 +418,9 @@
} }
requestJson({ requestJson({
url: info.apiUrl + 'api/v1/' + memoName, url: info.apiUrl + 'api/v1/' + memoName + '?updateMask=state',
type: 'PATCH', type: 'PATCH',
data: JSON.stringify({ state: 'ARCHIVED' }), data: JSON.stringify({ name: memoName, state: 'ARCHIVED' }),
contentType: 'application/json', contentType: 'application/json',
dataType: 'json', dataType: 'json',
headers: { Authorization: 'Bearer ' + info.apiTokens } headers: { Authorization: 'Bearer ' + info.apiTokens }
@@ -458,24 +460,35 @@
return 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({ requestJson({
url: info.apiUrl + 'api/v1/memos', url: info.apiUrl + 'api/v1/memos',
type: 'POST', type: 'POST',
data: JSON.stringify({ data: JSON.stringify({
content: payload.content, content: payload.content,
visibility: payload.visibility visibility: payload.visibility,
...(canCreateWithAttachments ? { attachments: attachmentReferences } : {})
}), }),
contentType: 'application/json', contentType: 'application/json',
dataType: 'json', dataType: 'json',
headers: { Authorization: 'Bearer ' + info.apiTokens } headers: { Authorization: 'Bearer ' + info.apiTokens }
}, function (data) { }, function (data) {
const createdName = data && data.name ? data.name : data && data.memo && data.memo.name ? data.memo.name : '' 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 (!createdName) {
if (success) success(data) if (success) success(data)
return return
} }
if (resources.length === 0) { if (resources.length === 0 || canCreateWithAttachments) {
getMemo(createdName, success, fail) getMemo(createdName, success, fail)
return return
} }
@@ -497,7 +510,7 @@
return { return {
flavor: flavor, flavor: flavor,
needsAuthenticatedImagePreview: function () { needsAuthenticatedImagePreview: function () {
return flavor === FLAVOR_V020_V021 return true
}, },
listTags: listTags, listTags: listTags,
searchMemos: searchMemos, searchMemos: searchMemos,
@@ -518,4 +531,4 @@
probeFlavor: probeFlavor, probeFlavor: probeFlavor,
resolve: resolve resolve: resolve
} }
})(window) })(window)
+47 -108
View File
@@ -151,10 +151,53 @@
return copy 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.$ global.$
.ajax({ .ajax({
url: urlAttachments, url: urlAttachments,
data: JSON.stringify(payload), data: JSON.stringify(stripVisibility(payload)),
type: 'POST', type: 'POST',
cache: false, cache: false,
processData: false, processData: false,
@@ -166,115 +209,11 @@
onSuccess(data, 'attachments') onSuccess(data, 'attachments')
}) })
.fail(function (xhr) { .fail(function (xhr) {
if (xhr && xhr.status === 400) { if (isNotFoundLikeXhr(xhr)) {
global.$ uploadResource()
.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)
})
})
return return
} }
if (onFail) onFail(xhr)
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)
})
}) })
} }
+11 -4
View File
@@ -480,6 +480,15 @@ function buildV1ResourceStreamUrl(info, resource) {
} }
if (root && !root.endsWith('/')) root += '/' 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) { function isImageResource(r) {
if (!r) return false if (!r) return false
const t = typeof r.type === 'string' ? r.type.toLowerCase() : '' const t = typeof r.type === 'string' ? r.type.toLowerCase() : ''
@@ -517,13 +526,11 @@ function buildV1ResourceStreamUrl(info, resource) {
if (id) return buildStreamUrl(id) if (id) return buildStreamUrl(id)
// Fallback for older resource shapes. // Fallback for older resource shapes.
const name = typeof resource.name === 'string' ? resource.name : ''
// In some memo payloads, the uid may appear as `name` directly. // In some memo payloads, the uid may appear as `name` directly.
// Example: name="ETU6hjuR..." should map to /o/r/:uid, not /file/:name/:filename. // Example: name="ETU6hjuR..." should map to /o/r/:uid, not /file/:name/:filename.
if (isProbablyUid(name)) return buildStreamUrl(name.trim()) 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 if (name && fileId) return root + 'file/' + name + '/' + fileId
return '' return ''
} }
@@ -1283,4 +1290,4 @@ function sendText(preparedContent) {
}) })
} }
}) })
} }
+1 -1
View File
@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "__MSG_extName__", "name": "__MSG_extName__",
"default_locale": "en", "default_locale": "en",
"version": "2026.04.25", "version": "2026.08.05",
"action": { "action": {
"default_popup": "popup.html", "default_popup": "popup.html",
"default_icon": "assets/logo_24x24.png", "default_icon": "assets/logo_24x24.png",
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "Speichern" "message": "Speichern"
}, },
"supportedMemosVersion": { "supportedMemosVersion": {
"message": "Kompatibel mit Memos v0.15.0 - 0.27.x" "message": "Kompatibel mit Memos v0.15.0 - 0.29.x"
}, },
"settingsConnectionTitle": { "settingsConnectionTitle": {
"message": "Verbindung" "message": "Verbindung"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "Save" "message": "Save"
}, },
"supportedMemosVersion": { "supportedMemosVersion": {
"message": "Compatible with Memos v0.15.0 - 0.27.x" "message": "Compatible with Memos v0.15.0 - 0.29.x"
}, },
"settingsConnectionTitle": { "settingsConnectionTitle": {
"message": "Connection" "message": "Connection"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "Guardar" "message": "Guardar"
}, },
"supportedMemosVersion": { "supportedMemosVersion": {
"message": "Compatible con Memos v0.15.0 - 0.27.x" "message": "Compatible con Memos v0.15.0 - 0.29.x"
}, },
"settingsConnectionTitle": { "settingsConnectionTitle": {
"message": "Conexión" "message": "Conexión"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "Enregistrer" "message": "Enregistrer"
}, },
"supportedMemosVersion": { "supportedMemosVersion": {
"message": "Compatible avec Memos v0.15.0 - 0.27.x" "message": "Compatible avec Memos v0.15.0 - 0.29.x"
}, },
"settingsConnectionTitle": { "settingsConnectionTitle": {
"message": "Connexion" "message": "Connexion"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "保存" "message": "保存"
}, },
"supportedMemosVersion": { "supportedMemosVersion": {
"message": "Memos v0.15.0 - 0.27.x に対応" "message": "Memos v0.15.0 - 0.29.x に対応"
}, },
"settingsConnectionTitle": { "settingsConnectionTitle": {
"message": "接続設定" "message": "接続設定"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "저장" "message": "저장"
}, },
"supportedMemosVersion": { "supportedMemosVersion": {
"message": "Memos v0.15.0 - 0.27.x 호환" "message": "Memos v0.15.0 - 0.29.x 호환"
}, },
"settingsConnectionTitle": { "settingsConnectionTitle": {
"message": "연결 설정" "message": "연결 설정"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "保存" "message": "保存"
}, },
"supportedMemosVersion": { "supportedMemosVersion": {
"message": "兼容 Memos v0.15.0 - 0.27.x" "message": "兼容 Memos v0.15.0 - 0.29.x"
}, },
"settingsConnectionTitle": { "settingsConnectionTitle": {
"message": "连接设置" "message": "连接设置"
+34 -21
View File
@@ -187,6 +187,7 @@
function normalizeUploadedItem(entity, fallbackFilename) { function normalizeUploadedItem(entity, fallbackFilename) {
if (!entity) return null if (!entity) return null
entity = entity.attachment || entity.resource || entity
const inferredId = (function () { const inferredId = (function () {
const value = entity.id != null ? entity.id : entity.ID != null ? entity.ID : entity.Id 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) if (typeof value === 'number' && Number.isFinite(value)) return Math.floor(value)
@@ -204,7 +205,11 @@
name: name, name: name,
filename: entity.filename || fallbackFilename || name, filename: entity.filename || fallbackFilename || name,
createTime: entity.createTime || entity.createdTs || entity.createdAt, 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 return
} }
if (global.MemosApiModern) { if (global.MemosApiV023) {
global.MemosApiModern.fetchMemosWithFallback( global.MemosApiV023.listMemos(info, { pageSize: 1000 }, function (data) {
info, if (success) success(collectTags(info, extractMemos(data)))
'?pageSize=1000', }, fail)
function (data) {
if (success) success(collectTags(info, extractMemos(data)))
},
fail
)
} }
} }
@@ -364,10 +364,12 @@
} }
function uploadFile(file, options, success, fail) { function uploadFile(file, options, success, fail) {
const oldName = String(file && file.name ? file.name : 'upload').split('.') const sourceName = String(file && file.name ? file.name : 'upload')
const fileExt = String(file && file.name ? file.name : '').split('.').pop() 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 now = global.dayjs().format('YYYYMMDDHHmmss')
const nextName = oldName[0] + '_' + now + (fileExt ? '.' + fileExt : '') const nextName = filenameBase + '_' + now + fileExt
if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021) { if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021) {
global.MemosApiV020V021.uploadResourceBlob( global.MemosApiV020V021.uploadResourceBlob(
@@ -396,7 +398,7 @@
info, info,
payload, payload,
function (resp) { function (resp) {
const entity = (resp && resp.resource) || resp const entity = (resp && (resp.attachment || resp.resource)) || resp
if (success) success(normalizeUploadedItem(entity, nextName)) if (success) success(normalizeUploadedItem(entity, nextName))
}, },
fail fail
@@ -416,9 +418,9 @@
} }
requestJson({ requestJson({
url: info.apiUrl + 'api/v1/' + memoName, url: info.apiUrl + 'api/v1/' + memoName + '?updateMask=state',
type: 'PATCH', type: 'PATCH',
data: JSON.stringify({ state: 'ARCHIVED' }), data: JSON.stringify({ name: memoName, state: 'ARCHIVED' }),
contentType: 'application/json', contentType: 'application/json',
dataType: 'json', dataType: 'json',
headers: { Authorization: 'Bearer ' + info.apiTokens } headers: { Authorization: 'Bearer ' + info.apiTokens }
@@ -458,24 +460,35 @@
return 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({ requestJson({
url: info.apiUrl + 'api/v1/memos', url: info.apiUrl + 'api/v1/memos',
type: 'POST', type: 'POST',
data: JSON.stringify({ data: JSON.stringify({
content: payload.content, content: payload.content,
visibility: payload.visibility visibility: payload.visibility,
...(canCreateWithAttachments ? { attachments: attachmentReferences } : {})
}), }),
contentType: 'application/json', contentType: 'application/json',
dataType: 'json', dataType: 'json',
headers: { Authorization: 'Bearer ' + info.apiTokens } headers: { Authorization: 'Bearer ' + info.apiTokens }
}, function (data) { }, function (data) {
const createdName = data && data.name ? data.name : data && data.memo && data.memo.name ? data.memo.name : '' 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 (!createdName) {
if (success) success(data) if (success) success(data)
return return
} }
if (resources.length === 0) { if (resources.length === 0 || canCreateWithAttachments) {
getMemo(createdName, success, fail) getMemo(createdName, success, fail)
return return
} }
@@ -497,7 +510,7 @@
return { return {
flavor: flavor, flavor: flavor,
needsAuthenticatedImagePreview: function () { needsAuthenticatedImagePreview: function () {
return flavor === FLAVOR_V020_V021 return true
}, },
listTags: listTags, listTags: listTags,
searchMemos: searchMemos, searchMemos: searchMemos,
@@ -518,4 +531,4 @@
probeFlavor: probeFlavor, probeFlavor: probeFlavor,
resolve: resolve resolve: resolve
} }
})(window) })(window)
+47 -108
View File
@@ -151,10 +151,53 @@
return copy 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.$ global.$
.ajax({ .ajax({
url: urlAttachments, url: urlAttachments,
data: JSON.stringify(payload), data: JSON.stringify(stripVisibility(payload)),
type: 'POST', type: 'POST',
cache: false, cache: false,
processData: false, processData: false,
@@ -166,115 +209,11 @@
onSuccess(data, 'attachments') onSuccess(data, 'attachments')
}) })
.fail(function (xhr) { .fail(function (xhr) {
if (xhr && xhr.status === 400) { if (isNotFoundLikeXhr(xhr)) {
global.$ uploadResource()
.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)
})
})
return return
} }
if (onFail) onFail(xhr)
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)
})
}) })
} }
+11 -4
View File
@@ -505,6 +505,15 @@ function buildV1ResourceStreamUrl(info, resource) {
} }
if (root && !root.endsWith('/')) root += '/' 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) { function isImageResource(r) {
if (!r) return false if (!r) return false
const t = typeof r.type === 'string' ? r.type.toLowerCase() : '' const t = typeof r.type === 'string' ? r.type.toLowerCase() : ''
@@ -542,13 +551,11 @@ function buildV1ResourceStreamUrl(info, resource) {
if (id) return buildStreamUrl(id) if (id) return buildStreamUrl(id)
// Fallback for older resource shapes. // Fallback for older resource shapes.
const name = typeof resource.name === 'string' ? resource.name : ''
// In some memo payloads, the uid may appear as `name` directly. // In some memo payloads, the uid may appear as `name` directly.
// Example: name="ETU6hjuR..." should map to /o/r/:uid, not /file/:name/:filename. // Example: name="ETU6hjuR..." should map to /o/r/:uid, not /file/:name/:filename.
if (isProbablyUid(name)) return buildStreamUrl(name.trim()) 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 if (name && fileId) return root + 'file/' + name + '/' + fileId
return '' return ''
} }
@@ -1308,4 +1315,4 @@ function sendText(preparedContent) {
}) })
} }
}) })
} }
+1 -1
View File
@@ -2,7 +2,7 @@
"manifest_version": 2, "manifest_version": 2,
"name": "__MSG_extName__", "name": "__MSG_extName__",
"default_locale": "en", "default_locale": "en",
"version": "2026.4.25", "version": "2026.8.5",
"browser_action": { "browser_action": {
"default_popup": "popup.html", "default_popup": "popup.html",
"default_icon": "assets/logo_24x24.png", "default_icon": "assets/logo_24x24.png",