From 5152641c4b564857f23f46b1ea81f9319ef3fd4a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=B0=81=E5=9C=A8=E5=90=B5=E7=9D=80=E5=90=83=E7=B3=96?=
Date: Wed, 17 Jun 2026 20:29:58 +0800
Subject: [PATCH 1/2] feat: add config UI update check
---
clawdbot.plugin.json | 2 +-
lib/check-update.js | 63 +++++++++------
lib/config-ui-server.js | 90 ++++++++++++++++-----
lib/config-ui/app.css | 102 ++++++++++++++++++++++++
lib/config-ui/app.js | 168 ++++++++++++++++++++++++++++++++++++++-
lib/config-ui/index.html | 1 +
moltbot.plugin.json | 2 +-
openclaw.plugin.json | 2 +-
package.json | 2 +-
9 files changed, 383 insertions(+), 49 deletions(-)
diff --git a/clawdbot.plugin.json b/clawdbot.plugin.json
index 2df9c68..ea4d4b8 100644
--- a/clawdbot.plugin.json
+++ b/clawdbot.plugin.json
@@ -2,7 +2,7 @@
"id": "memos-cloud-openclaw-plugin",
"name": "MemOS Cloud OpenClaw Plugin",
"description": "MemOS Cloud recall + add memory via lifecycle hooks",
- "version": "0.1.17",
+ "version": "0.1.18-beta.1",
"kind": "lifecycle",
"main": "./index.js",
"activation": {
diff --git a/lib/check-update.js b/lib/check-update.js
index 906fdf3..75cb71e 100644
--- a/lib/check-update.js
+++ b/lib/check-update.js
@@ -19,7 +19,7 @@ const ANSI = {
};
-function getPackageVersion() {
+export function getPackageVersion() {
try {
const pkgPath = path.join(__dirname, "..", "package.json");
const pkgData = fs.readFileSync(pkgPath, "utf-8");
@@ -30,7 +30,7 @@ function getPackageVersion() {
}
}
-function getLatestVersion(log) {
+export function getLatestVersion() {
return new Promise((resolve, reject) => {
const req = https.get(
`https://registry.npmjs.org/${PLUGIN_NAME}/latest`,
@@ -68,7 +68,7 @@ function getLatestVersion(log) {
});
}
-function compareVersions(v1, v2) {
+export function compareVersions(v1, v2) {
// Split pre-release tags (e.g. 0.1.8-beta.1 -> "0.1.8" and "beta.1")
const split1 = v1.split("-");
const split2 = v2.split("-");
@@ -101,6 +101,37 @@ function compareVersions(v1, v2) {
return 0;
}
+function detectCliName() {
+ // Check the full path of the entry script (e.g., .../moltbot/bin/index.js) or the executable
+ const scriptPath = process.argv[1] ? process.argv[1].toLowerCase() : "";
+ const execPath = process.execPath ? process.execPath.toLowerCase() : "";
+
+ if (scriptPath.includes("moltbot") || execPath.includes("moltbot")) return "moltbot";
+ if (scriptPath.includes("clawdbot") || execPath.includes("clawdbot")) return "clawdbot";
+ return "openclaw";
+}
+
+export async function checkForPluginUpdate() {
+ const currentVersion = getPackageVersion();
+ if (!currentVersion) {
+ throw new Error("Could not read current version from package.json");
+ }
+
+ const latestVersion = await getLatestVersion();
+ const updateAvailable = compareVersions(latestVersion, currentVersion) > 0;
+ const cliName = detectCliName();
+
+ return {
+ pluginName: PLUGIN_NAME,
+ currentVersion,
+ latestVersion,
+ updateAvailable,
+ cliName,
+ updateCommand: `${cliName} plugins update memos-cloud-openclaw-plugin`,
+ checkedAt: new Date().toISOString(),
+ };
+}
+
export function startUpdateChecker(log) {
// Only start the interval if we are in the gateway
const isGateway = process.argv.includes("gateway");
@@ -118,39 +149,23 @@ export function startUpdateChecker(log) {
log.warn?.(`${ANSI.RED}[memos-cloud] Failed to write timestamp file: ${e.message}${ANSI.RESET}`);
}
- const currentVersion = getPackageVersion();
- if (!currentVersion) {
- log.warn?.(`${ANSI.RED}[memos-cloud] Could not read current version from package.json${ANSI.RESET}`);
- return;
- }
-
try {
- const latestVersion = await getLatestVersion(log);
+ const updateStatus = await checkForPluginUpdate();
// Normal version check
- if (compareVersions(latestVersion, currentVersion) <= 0) {
+ if (!updateStatus.updateAvailable) {
return;
}
- const cliName = (() => {
- // Check the full path of the entry script (e.g., .../moltbot/bin/index.js) or the executable
- const scriptPath = process.argv[1] ? process.argv[1].toLowerCase() : "";
- const execPath = process.execPath ? process.execPath.toLowerCase() : "";
-
- if (scriptPath.includes("moltbot") || execPath.includes("moltbot")) return "moltbot";
- if (scriptPath.includes("clawdbot") || execPath.includes("clawdbot")) return "clawdbot";
- return "openclaw";
- })();
-
const border = "=".repeat(64);
log.info?.("");
log.info?.(`${ANSI.GREEN}${border}${ANSI.RESET}`);
log.info?.(`${ANSI.YELLOW}🚀 [memos-cloud] NEW VERSION AVAILABLE!${ANSI.RESET}`);
- log.info?.(`${ANSI.CYAN}📦 Current version : ${currentVersion}${ANSI.RESET}`);
- log.info?.(`${ANSI.GREEN}✨ Latest version : ${latestVersion}${ANSI.RESET}`);
+ log.info?.(`${ANSI.CYAN}📦 Current version : ${updateStatus.currentVersion}${ANSI.RESET}`);
+ log.info?.(`${ANSI.GREEN}✨ Latest version : ${updateStatus.latestVersion}${ANSI.RESET}`);
log.info?.(`${ANSI.CYAN}────────────────────────────────────────────────────────────────${ANSI.RESET}`);
log.info?.(`${ANSI.GREEN}Please run the following command to update manually:${ANSI.RESET}`);
- log.info?.(`${ANSI.YELLOW}${cliName} plugins update memos-cloud-openclaw-plugin${ANSI.RESET}`);
+ log.info?.(`${ANSI.YELLOW}${updateStatus.updateCommand}${ANSI.RESET}`);
log.info?.(`${ANSI.GREEN}${border}${ANSI.RESET}`);
log.info?.("");
diff --git a/lib/config-ui-server.js b/lib/config-ui-server.js
index 1564c0d..dba4214 100644
--- a/lib/config-ui-server.js
+++ b/lib/config-ui-server.js
@@ -1,12 +1,13 @@
-import { createHash, randomBytes } from "node:crypto";
-import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
+import { createHash, randomBytes } from "node:crypto";
+import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { createServer } from "node:http";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
-import { Script } from "node:vm";
-import { getConfigResolution } from "./memos-cloud-api.js";
+import { Script } from "node:vm";
+import { checkForPluginUpdate, getPackageVersion } from "./check-update.js";
+import { getConfigResolution } from "./memos-cloud-api.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -21,7 +22,8 @@ const ANSI_CYAN = "\x1b[36m";
const ANSI_GREEN = "\x1b[32m";
const ANSI_YELLOW = "\x1b[33m";
const ANSI_RESET = "\x1b[0m";
-const DEFAULT_GATEWAY_READY_PORT = 18789;
+const DEFAULT_GATEWAY_READY_PORT = 18789;
+const UPDATE_STATUS_CACHE_MS = 30 * 60 * 1000;
// Hook policies that the gateway requires non-bundled plugins to opt into.
// Without these, the gateway blocks `agent_end` (and other conversation-access
@@ -517,9 +519,10 @@ function buildStatePayload(service) {
const state = readGatewayConfig(service.profile);
const resolution = getConfigResolution(state.config);
return {
- runtime: state.profile.id,
- runtimeDisplayName: state.profile.displayName,
- configPath: state.configPath,
+ runtime: state.profile.id,
+ runtimeDisplayName: state.profile.displayName,
+ pluginVersion: getPackageVersion(),
+ configPath: state.configPath,
entryExists: state.entryExists,
fileExists: state.fileExists,
enabled: state.enabled,
@@ -548,7 +551,7 @@ function getCachedStatePayload(service, maxAgeMs = 1200) {
return payload;
}
-function getCachedHeartbeatPayload(service, maxAgeMs = 1200) {
+function getCachedHeartbeatPayload(service, maxAgeMs = 1200) {
const now = Date.now();
if (service.heartbeatCache && now - service.heartbeatCache.createdAt < maxAgeMs) {
return {
@@ -570,9 +573,41 @@ function getCachedHeartbeatPayload(service, maxAgeMs = 1200) {
...payload,
timestamp: now,
};
-}
-
-function loadAssetTemplate(name) {
+}
+
+async function getCachedUpdateStatusPayload(service, maxAgeMs = UPDATE_STATUS_CACHE_MS, options = {}) {
+ const now = Date.now();
+ if (!options.force && service.updateStatusCache && now - service.updateStatusCache.createdAt < maxAgeMs) {
+ return {
+ ...service.updateStatusCache.payload,
+ cached: true,
+ };
+ }
+
+ try {
+ const status = await checkForPluginUpdate();
+ const payload = {
+ ok: true,
+ ...status,
+ cached: false,
+ };
+ service.updateStatusCache = {
+ createdAt: now,
+ payload,
+ };
+ return payload;
+ } catch (error) {
+ return {
+ ok: false,
+ currentVersion: getPackageVersion(),
+ error: String(error?.message || error),
+ checkedAt: new Date().toISOString(),
+ cached: false,
+ };
+ }
+}
+
+function loadAssetTemplate(name) {
return readFileSync(join(ASSET_DIR, name), "utf8");
}
@@ -827,7 +862,17 @@ async function createService(log) {
const token = randomBytes(24).toString("hex");
const bootId = randomBytes(10).toString("hex");
- const service = { profile, token, bootId, port: 0, url: "", server: null, stateCache: null, heartbeatCache: null };
+ const service = {
+ profile,
+ token,
+ bootId,
+ port: 0,
+ url: "",
+ server: null,
+ stateCache: null,
+ heartbeatCache: null,
+ updateStatusCache: null,
+ };
const server = createServer(async (req, res) => {
try {
@@ -872,12 +917,19 @@ async function createService(log) {
return;
}
- if (requestUrl.pathname === "/api/state" && req.method === "GET") {
- sendJson(res, 200, getCachedStatePayload(service));
- return;
- }
-
- if (requestUrl.pathname === "/api/save" && req.method === "POST") {
+ if (requestUrl.pathname === "/api/state" && req.method === "GET") {
+ sendJson(res, 200, getCachedStatePayload(service));
+ return;
+ }
+
+ if (requestUrl.pathname === "/api/update-status" && req.method === "GET") {
+ sendJson(res, 200, await getCachedUpdateStatusPayload(service, UPDATE_STATUS_CACHE_MS, {
+ force: requestUrl.searchParams.get("force") === "1",
+ }));
+ return;
+ }
+
+ if (requestUrl.pathname === "/api/save" && req.method === "POST") {
let parsed = {};
try {
parsed = JSON.parse((await readRequestBody(req)) || "{}");
diff --git a/lib/config-ui/app.css b/lib/config-ui/app.css
index 58ca635..f636d37 100644
--- a/lib/config-ui/app.css
+++ b/lib/config-ui/app.css
@@ -337,6 +337,99 @@ h1 {
color: var(--warn);
}
+.update-notice {
+ display: none;
+ margin-top: 16px;
+ padding: 14px 16px;
+ border: 1px solid rgba(181, 71, 8, 0.2);
+ border-radius: 18px;
+ background: rgba(181, 71, 8, 0.08);
+ color: var(--warn);
+}
+
+.update-notice.show {
+ display: block;
+}
+
+.update-notice.error {
+ border-color: rgba(180, 35, 24, 0.18);
+ background: rgba(180, 35, 24, 0.08);
+ color: var(--danger);
+}
+
+.update-notice.ok,
+.update-notice.info {
+ border-color: rgba(15, 118, 110, 0.18);
+ background: rgba(15, 118, 110, 0.08);
+ color: var(--accent2);
+}
+
+.update-check-button {
+ appearance: none;
+ min-height: 38px;
+ padding: 8px 14px;
+ border: 1px solid rgba(15, 23, 42, 0.08);
+ background: rgba(255, 255, 255, 0.74);
+ color: var(--strong);
+ font-weight: 700;
+ cursor: pointer;
+ transition: border-color 160ms ease, box-shadow 160ms ease, color 160ms ease, transform 160ms ease;
+}
+
+.update-check-button:hover {
+ border-color: rgba(15, 118, 110, 0.2);
+ color: var(--accent2);
+ transform: translateY(-1px);
+}
+
+.update-check-button:focus {
+ outline: none;
+ border-color: rgba(15, 118, 110, 0.34);
+ box-shadow: 0 0 0 4px rgba(15, 118, 110, 0.12);
+}
+
+.update-check-button:disabled {
+ cursor: wait;
+ opacity: 0.7;
+ transform: none;
+}
+
+.update-copy {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 14px;
+}
+
+.update-title {
+ margin-bottom: 4px;
+ color: var(--strong);
+ font-weight: 800;
+}
+
+.update-body {
+ font-size: 13px;
+ line-height: 1.55;
+}
+
+.update-command {
+ display: block;
+ margin-top: 12px;
+ padding: 10px 12px;
+ border-radius: 14px;
+ background: rgba(255, 255, 255, 0.72);
+ color: var(--strong);
+ font-family: "Cascadia Code", "Consolas", monospace;
+ font-size: 12px;
+ line-height: 1.45;
+ overflow-x: auto;
+}
+
+.update-copy-button {
+ flex: 0 0 auto;
+ white-space: nowrap;
+}
+
.hero-actions {
display: grid;
gap: 12px;
@@ -609,6 +702,11 @@ h1 {
cursor: pointer;
}
+.inline-btn:disabled {
+ opacity: 0.58;
+ cursor: not-allowed;
+}
+
.error {
color: var(--danger);
font-size: 12px;
@@ -868,6 +966,10 @@ body.nav-collapsed .floating-nav {
padding: 18px;
}
+ .update-copy {
+ display: grid;
+ }
+
.card,
.panel {
padding: 18px;
diff --git a/lib/config-ui/app.js b/lib/config-ui/app.js
index 576bc64..d5f68fa 100644
--- a/lib/config-ui/app.js
+++ b/lib/config-ui/app.js
@@ -6,6 +6,9 @@ const APP = {
const knownKeys = new Set(APP.fieldDefinitions.map((field) => field.key));
let remoteState = null;
+let updateStatus = null;
+let updateStatusChecked = false;
+let updateStatusLoading = false;
let draft = null;
let baselineSnapshot = "";
@@ -41,6 +44,8 @@ const elements = {
languageOptionZh: document.getElementById("languageOptionZh"),
statusRow: document.getElementById("statusRow"),
metaRow: document.getElementById("metaRow"),
+ updateCheckButton: document.getElementById("updateCheckButton") || document.createElement("button"),
+ updateNotice: document.getElementById("updateNotice"),
banner: document.getElementById("banner"),
floatingTools: document.getElementById("floatingTools"),
floatingNavLabel: document.getElementById("floatingNavLabel"),
@@ -128,6 +133,7 @@ const UI_TEXT = {
bannerAuthWaiting: "Config session expired. Waiting for the gateway to finish restarting before retrying...",
bannerAuthFailed: "Config session expired. Refresh this page to reconnect.",
bannerCopied: "The config file path was copied to your clipboard.",
+ bannerUpdateCommandCopied: "The update command was copied to your clipboard.",
bannerClipboardFailed: "Clipboard access failed. Copy the address from your browser bar instead.",
bannerSaved: "The plugin config was saved.",
bannerRestartLaunched: "Restart requested. Refresh this page in a few seconds if it does not recover automatically.",
@@ -135,12 +141,21 @@ const UI_TEXT = {
bannerWaitingBack: "Restart requested. Refresh this page in a few seconds if it does not recover automatically.",
bannerRestartRefreshHint: "Restart requested. Refresh this page in a few seconds if it does not recover automatically.",
pillPlugin: "Plugin",
+ pillVersion: "Version",
pillRuntime: "Runtime",
pillEntry: "Entry",
pillPageUrl: "Page URL",
- pillRevision: "Revision",
+ pillRevision: "Config revision",
pillConfigFile: "Config file",
pillInclude: "Include",
+ checkUpdates: "Check for updates",
+ checkingUpdates: "Checking...",
+ updateTitle: "Plugin update available",
+ updateBody: "Current version {current}; latest version {latest}. Update and restart with:",
+ updateCopyCommand: "Copy Update And Restart Command",
+ upToDateTitle: "Already up to date",
+ upToDateBody: "Current version {current} is the latest version.",
+ updateCheckFailed: "Version check failed: {error}",
pillEntryPresent: "present",
pillEntryMissing: "missing",
pillConfigFound: "found",
@@ -209,6 +224,7 @@ const UI_TEXT = {
bannerAuthWaiting: "配置页会话已过期,正在等待 Gateway 完成重启后再恢复...",
bannerAuthFailed: "配置页会话已过期,请刷新页面后重新连接。",
bannerCopied: "配置文件路径已复制到剪贴板。",
+ bannerUpdateCommandCopied: "更新命令已复制到剪贴板。",
bannerClipboardFailed: "复制失败,请直接从浏览器地址栏复制。",
bannerSaved: "插件配置已保存。",
bannerRestartLaunched: "已请求重启。如果页面没有自动恢复,请过几秒刷新重试。",
@@ -216,12 +232,21 @@ const UI_TEXT = {
bannerWaitingBack: "已请求重启。如果页面没有自动恢复,请过几秒刷新重试。",
bannerRestartRefreshHint: "已请求重启。如果页面没有自动恢复,请过几秒刷新重试。",
pillPlugin: "插件",
+ pillVersion: "版本",
pillRuntime: "运行时",
pillEntry: "配置项",
pillPageUrl: "页面地址",
- pillRevision: "版本标识",
+ pillRevision: "配置标识",
pillConfigFile: "配置文件",
pillInclude: "包含",
+ checkUpdates: "检查更新",
+ checkingUpdates: "检查中...",
+ updateTitle: "插件有新版本",
+ updateBody: "当前版本 {current};最新版本 {latest}。可使用以下命令更新并重启:",
+ updateCopyCommand: "复制更新并重启命令",
+ upToDateTitle: "已是最新版本",
+ upToDateBody: "当前版本 {current} 已经是最新版本。",
+ updateCheckFailed: "版本检查失败:{error}",
pillEntryPresent: "已存在",
pillEntryMissing: "不存在",
pillConfigFound: "已找到",
@@ -307,6 +332,14 @@ function uiText(key) {
return UI_TEXT[lang][key] ?? UI_TEXT.en[key] ?? key;
}
+function formatUiText(key, values) {
+ let text = uiText(key);
+ for (const [name, value] of Object.entries(values || {})) {
+ text = text.split(`{${name}}`).join(String(value ?? ""));
+ }
+ return text;
+}
+
function localizedGroup(group) {
const lang = getCurrentLanguage();
const translated = GROUP_TRANSLATIONS[lang]?.[group.id];
@@ -340,6 +373,10 @@ function applyLanguageUi() {
elements.copyPathButton.setAttribute("aria-label", uiText("copyPath"));
elements.copyPathButton.setAttribute("title", uiText("copyPath"));
elements.saveButton.textContent = uiText("save");
+ elements.updateCheckButton.id = "updateCheckButton";
+ elements.updateCheckButton.type = "button";
+ elements.updateCheckButton.className = "pill update-check-button";
+ updateCheckButtonUi();
elements.reloadButton.textContent = uiText("reload");
elements.overlayTitle.textContent = uiText("overlayTitle");
@@ -350,6 +387,22 @@ function applyLanguageUi() {
elements.languageOptionEn.textContent = "EN";
elements.languageOptionZh.textContent = "中文";
updateLanguageDropdownUi();
+ renderUpdateNotice();
+}
+
+function updateCheckButtonUi() {
+ if (!elements.updateCheckButton) return;
+ const version = remoteState?.pluginVersion || "unknown";
+ const action = updateStatusLoading ? uiText("checkingUpdates") : uiText("checkUpdates");
+ elements.updateCheckButton.innerHTML =
+ "" +
+ escapeHtml(uiText("pillVersion")) +
+ "" +
+ escapeHtml(version + " · " + action) +
+ "";
+ elements.updateCheckButton.setAttribute("aria-label", uiText("pillVersion") + " " + version + ", " + action);
+ elements.updateCheckButton.setAttribute("title", action);
+ elements.updateCheckButton.disabled = updateStatusLoading;
}
function updateFloatingNavToggleUi() {
@@ -722,11 +775,16 @@ function renderStatus() {
elements.statusRow.innerHTML = "";
elements.metaRow.innerHTML = "";
elements.pathBox.textContent = remoteState ? remoteState.configPath : "";
+ if (elements.updateCheckButton && !elements.updateCheckButton.parentElement) {
+ elements.updateCheckButton.remove();
+ }
if (!remoteState) return;
elements.statusRow.appendChild(
createPill(remoteState.enabled ? "ok" : "warn", uiText("pillPlugin"), remoteState.enabled ? uiText("enabled") : uiText("disabled")),
);
+ updateCheckButtonUi();
+ elements.statusRow.appendChild(elements.updateCheckButton);
elements.statusRow.appendChild(createPill("", uiText("pillRuntime"), remoteState.runtimeDisplayName));
elements.statusRow.appendChild(
createPill(remoteState.entryExists ? "" : "warn", uiText("pillEntry"), remoteState.entryExists ? uiText("pillEntryPresent") : uiText("pillEntryMissing")),
@@ -743,6 +801,70 @@ function renderStatus() {
}
}
+function renderUpdateNotice() {
+ if (!elements.updateNotice) return;
+
+ if (updateStatusLoading) {
+ elements.updateNotice.hidden = false;
+ elements.updateNotice.className = "update-notice info show";
+ elements.updateNotice.innerHTML =
+ '' +
+ escapeHtml(uiText("checkingUpdates")) +
+ "
";
+ return;
+ }
+
+ if (!updateStatusChecked || !updateStatus) {
+ elements.updateNotice.hidden = true;
+ elements.updateNotice.className = "update-notice";
+ elements.updateNotice.innerHTML = "";
+ return;
+ }
+
+ elements.updateNotice.hidden = false;
+ elements.updateNotice.className = "update-notice " + (updateStatus.ok === false ? "error show" : updateStatus.updateAvailable ? "show" : "ok show");
+
+ if (updateStatus.updateAvailable) {
+ const command = getUpdateAndRestartCommand(updateStatus);
+ elements.updateNotice.innerHTML =
+ '' +
+ '
' +
+ escapeHtml(uiText("updateTitle")) +
+ '
' +
+ escapeHtml(formatUiText("updateBody", { current: updateStatus.currentVersion, latest: updateStatus.latestVersion })) +
+ '
' +
+ '
" +
+ '' +
+ escapeHtml(command) +
+ "";
+
+ elements.updateNotice.querySelector(".update-copy-button")?.addEventListener("click", () => {
+ void copyUpdateCommand(command);
+ });
+ return;
+ }
+
+ if (updateStatus.ok === false) {
+ elements.updateNotice.innerHTML = escapeHtml(formatUiText("updateCheckFailed", { error: updateStatus.error || "Unknown error" }));
+ return;
+ }
+
+ elements.updateNotice.innerHTML =
+ '' +
+ escapeHtml(uiText("upToDateTitle")) +
+ '
' +
+ escapeHtml(formatUiText("upToDateBody", { current: updateStatus.currentVersion || remoteState?.pluginVersion || "unknown" })) +
+ "
";
+}
+
+function getUpdateAndRestartCommand(status) {
+ const cliName = status?.cliName || "openclaw";
+ const updateCommand = status?.updateCommand || `${cliName} plugins update memos-cloud-openclaw-plugin`;
+ return `${updateCommand} && ${cliName} gateway restart`;
+}
+
function renderEnabledField() {
const field = document.createElement("div");
field.className = "field";
@@ -1096,6 +1218,35 @@ async function checkHeartbeat() {
return response.json();
}
+async function loadUpdateStatus(options = {}) {
+ const manual = options.manual === true;
+ if (manual) {
+ updateStatusChecked = true;
+ updateStatusLoading = true;
+ updateCheckButtonUi();
+ renderUpdateNotice();
+ }
+
+ try {
+ updateStatus = await api("/api/update-status" + (options.force ? "?force=1" : ""));
+ updateStatusChecked = true;
+ } catch (error) {
+ if (error?.status === 403) {
+ void recoverFromAuthError();
+ return;
+ }
+ updateStatusChecked = true;
+ updateStatus = {
+ ok: false,
+ error: String(error.message || error),
+ };
+ } finally {
+ updateStatusLoading = false;
+ updateCheckButtonUi();
+ renderUpdateNotice();
+ }
+}
+
function handleHeartbeatState(heartbeat) {
if (!heartbeat || typeof heartbeat !== "object") return;
@@ -1240,6 +1391,16 @@ async function copyConfigPath() {
}
}
+async function copyUpdateCommand(command) {
+ if (!command) return;
+ try {
+ await navigator.clipboard.writeText(command);
+ setBanner("info", uiText("bannerUpdateCommandCopied"));
+ } catch {
+ setBanner("error", uiText("bannerClipboardFailed"));
+ }
+}
+
elements.languageSelectButton.addEventListener("click", () => {
setLanguageMenuOpen(!languageMenuOpen);
});
@@ -1272,6 +1433,9 @@ elements.overlayRefreshButton.addEventListener("click", () => {
window.location.reload();
});
elements.copyPathButton.addEventListener("click", () => void copyConfigPath());
+elements.updateCheckButton?.addEventListener("click", () => {
+ void loadUpdateStatus({ manual: true, force: true });
+});
elements.floatingToggleButton.addEventListener("click", () => {
setNavCollapsed(!navCollapsed);
});
diff --git a/lib/config-ui/index.html b/lib/config-ui/index.html
index f4ce1f7..97f5743 100644
--- a/lib/config-ui/index.html
+++ b/lib/config-ui/index.html
@@ -48,6 +48,7 @@ MemOS Cloud OpenClaw Plugin Config
+
diff --git a/moltbot.plugin.json b/moltbot.plugin.json
index 2df9c68..ea4d4b8 100644
--- a/moltbot.plugin.json
+++ b/moltbot.plugin.json
@@ -2,7 +2,7 @@
"id": "memos-cloud-openclaw-plugin",
"name": "MemOS Cloud OpenClaw Plugin",
"description": "MemOS Cloud recall + add memory via lifecycle hooks",
- "version": "0.1.17",
+ "version": "0.1.18-beta.1",
"kind": "lifecycle",
"main": "./index.js",
"activation": {
diff --git a/openclaw.plugin.json b/openclaw.plugin.json
index 2df9c68..ea4d4b8 100644
--- a/openclaw.plugin.json
+++ b/openclaw.plugin.json
@@ -2,7 +2,7 @@
"id": "memos-cloud-openclaw-plugin",
"name": "MemOS Cloud OpenClaw Plugin",
"description": "MemOS Cloud recall + add memory via lifecycle hooks",
- "version": "0.1.17",
+ "version": "0.1.18-beta.1",
"kind": "lifecycle",
"main": "./index.js",
"activation": {
diff --git a/package.json b/package.json
index e78c5f4..30b7479 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@memtensor/memos-cloud-openclaw-plugin",
- "version": "0.1.17",
+ "version": "0.1.18-beta.1",
"description": "OpenClaw lifecycle plugin for MemOS Cloud (add + recall memory)",
"scripts": {
"sync-version": "node scripts/sync-version.js",
From e1e7fb0f20d549253a762c1bea4ed4d9b4477ac2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=B0=81=E5=9C=A8=E5=90=B5=E7=9D=80=E5=90=83=E7=B3=96?=
Date: Mon, 22 Jun 2026 17:45:56 +0800
Subject: [PATCH 2/2] 0.1.18
---
clawdbot.plugin.json | 2 +-
moltbot.plugin.json | 2 +-
openclaw.plugin.json | 2 +-
package.json | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/clawdbot.plugin.json b/clawdbot.plugin.json
index ea4d4b8..5367322 100644
--- a/clawdbot.plugin.json
+++ b/clawdbot.plugin.json
@@ -2,7 +2,7 @@
"id": "memos-cloud-openclaw-plugin",
"name": "MemOS Cloud OpenClaw Plugin",
"description": "MemOS Cloud recall + add memory via lifecycle hooks",
- "version": "0.1.18-beta.1",
+ "version": "0.1.18",
"kind": "lifecycle",
"main": "./index.js",
"activation": {
diff --git a/moltbot.plugin.json b/moltbot.plugin.json
index ea4d4b8..5367322 100644
--- a/moltbot.plugin.json
+++ b/moltbot.plugin.json
@@ -2,7 +2,7 @@
"id": "memos-cloud-openclaw-plugin",
"name": "MemOS Cloud OpenClaw Plugin",
"description": "MemOS Cloud recall + add memory via lifecycle hooks",
- "version": "0.1.18-beta.1",
+ "version": "0.1.18",
"kind": "lifecycle",
"main": "./index.js",
"activation": {
diff --git a/openclaw.plugin.json b/openclaw.plugin.json
index ea4d4b8..5367322 100644
--- a/openclaw.plugin.json
+++ b/openclaw.plugin.json
@@ -2,7 +2,7 @@
"id": "memos-cloud-openclaw-plugin",
"name": "MemOS Cloud OpenClaw Plugin",
"description": "MemOS Cloud recall + add memory via lifecycle hooks",
- "version": "0.1.18-beta.1",
+ "version": "0.1.18",
"kind": "lifecycle",
"main": "./index.js",
"activation": {
diff --git a/package.json b/package.json
index 30b7479..77d0789 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@memtensor/memos-cloud-openclaw-plugin",
- "version": "0.1.18-beta.1",
+ "version": "0.1.18",
"description": "OpenClaw lifecycle plugin for MemOS Cloud (add + recall memory)",
"scripts": {
"sync-version": "node scripts/sync-version.js",