Skip to content

fix: plugin pin preference sync across devices#8848

Open
Sisyphbaous-DT-Project wants to merge 1 commit into
AstrBotDevs:masterfrom
Sisyphbaous-DT-Project:fix/plugin-pinned-preferences-sync
Open

fix: plugin pin preference sync across devices#8848
Sisyphbaous-DT-Project wants to merge 1 commit into
AstrBotDevs:masterfrom
Sisyphbaous-DT-Project:fix/plugin-pinned-preferences-sync

Conversation

@Sisyphbaous-DT-Project

@Sisyphbaous-DT-Project Sisyphbaous-DT-Project commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

本 PR 将 Dashboard 插件置顶状态保存到后端,使不同浏览器和不同设备打开 WebUI 时可以看到一致的置顶顺序。

此前插件置顶状态只保存在浏览器 localStorage 中,因此在电脑浏览器置顶插件后,换手机或其他浏览器打开 WebUI 时不会同步。

Modifications / 改动点

  • 新增 v1 Dashboard API,用于读取和更新插件置顶偏好。

  • 使用现有后端 preferences 表保存置顶插件名称列表。

  • 区分“后端尚无偏好记录”和“后端已有空置顶列表”,避免旧 localStorage 数据把用户已经清空的置顶重新恢复。

  • 更新已安装插件页面:优先从后端加载置顶状态,仅在后端没有偏好记录时迁移旧本地置顶数据。

  • 保留 localStorage 作为旧数据兼容和本机缓存,不再作为跨设备同步的唯一来源。

  • 更新 OpenAPI 定义和生成的 Dashboard API 客户端文件。

  • 增加后端和前端回归测试,覆盖偏好持久化、空列表语义、权限校验、数据库异常和迁移决策。

  • This is NOT a breaking change. / 这不是一个破坏性变更。

Screenshots or Test Results / 运行截图或测试结果

git diff --check
node --test dashboard/tests/pluginPinnedPreferenceSync.test.mjs

结果:9 passed。

.venv/bin/python -m pytest tests/test_fastapi_v1_dashboard.py -k 'plugin_preferences_pinned' -q -s --maxfail=1

结果:8 passed。

ruff check astrbot/dashboard/services/plugin_preference_service.py astrbot/dashboard/api/plugin_preferences.py astrbot/dashboard/schemas.py tests/test_fastapi_v1_dashboard.py

结果:all checks passed。

cd dashboard && pnpm --ignore-workspace typecheck

结果:passed。


Checklist / 检查清单

  • 😊 If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
    / 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。

  • 👀 My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
    / 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”

  • 🤓 I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in requirements.txt and pyproject.toml.
    / 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到 requirements.txtpyproject.toml 文件相应位置。

  • 😮 My changes do not introduce malicious code.
    / 我的更改没有引入恶意代码。

Summary by Sourcery

Persist and sync dashboard plugin pinned preferences via a new backend API and service, ensuring consistent ordering across devices while falling back to local storage when needed.

New Features:

  • Add a v1 Plugin Preferences API to read and update globally pinned extensions.
  • Persist dashboard plugin pinned state in the backend preferences store instead of only localStorage.

Bug Fixes:

  • Ensure empty pinned preference lists are distinguished from missing preferences to avoid unintentionally restoring cleared pins.
  • Handle database errors for plugin pinned preference reads/writes with proper API error responses.

Enhancements:

  • Debounce and coalesce front-end saves of pinned plugin preferences to the backend while maintaining local caching.
  • Introduce a backend PluginPreferenceService to normalize and manage pinned extension lists globally.
  • Resolve conflicts between local and remote pinned states during initial load, with one-time migration from legacy localStorage when appropriate.

Tests:

  • Add backend tests covering plugin pinned preference persistence, empty-list semantics, permission checks, and database failure handling.
  • Add front-end unit tests for the pinned preference resolution and migration logic.

@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. area:webui The bug / feature is about webui(dashboard) of astrbot. feature:plugin The bug / feature is about AstrBot plugin system. labels Jun 17, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a global plugin preference service to persist pinned dashboard extensions. It adds backend API endpoints, database integration, OpenAPI specifications, and corresponding frontend SDK updates. On the frontend, the installed plugins tab is updated to synchronize pinned plugins with the backend, including debouncing and migration logic from local storage. Comprehensive tests are added for both the frontend synchronization logic and backend endpoints. Feedback on the frontend implementation suggests introducing an 'isMounted' flag in the Vue component to prevent potential memory leaks and state updates after the component is unmounted.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +247 to +305
const schedulePersistPinnedExtensions = (delay = SAVE_DEBOUNCE_MS) => {
if (saveTimer) {
clearTimeout(saveTimer);
}
saveTimer = setTimeout(async () => {
saveTimer = null;
await persistPinnedExtensions();
}, delay);
};

const hydratePinnedPreferences = async () => {
const hydrateStartVersion = pinnedPreferenceVersion;
try {
const response = await pluginPreferencesApi.getPinnedExtensions();
const remoteData = response?.data?.data ?? {};
const remoteNames = normalizePinnedExtensions(
remoteData?.pinned_extensions,
);
const localNames = pinnedExtensionNames.value;
const resolution = resolvePinnedExtensionNames({
localNames,
remoteNames,
preferenceExists: remoteData?.preference_exists,
});

if (pinnedPreferenceVersion !== hydrateStartVersion) {
return;
}

applyPinnedExtensionNames(resolution.names, {
markSaved: !resolution.shouldMigrate,
});

if (resolution.shouldMigrate && resolution.migrateNames) {
const migrationVersion = pinnedPreferenceVersion;
const migrated = await persistPinnedExtensions();
if (
!migrated &&
pinnedPreferenceVersion === migrationVersion &&
migrationVersion > savedPinnedPreferenceVersion
) {
schedulePersistPinnedExtensions(1000);
}
}
} catch (error) {
console.warn("加载插件置顶偏好失败,继续使用本地缓存", error);
}
};

onMounted(hydratePinnedPreferences);

onUnmounted(() => {
if (saveTimer) {
clearTimeout(saveTimer);
saveTimer = null;
}

void persistPinnedExtensions();
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

在组件卸载后,异步请求(如 pluginPreferencesApi.getPinnedExtensions())可能仍在进行中。当其解析时,会尝试更新组件状态并可能调度新的定时器,这会导致内存泄漏或在已卸载组件上执行不必要的操作。建议引入一个 isMounted 标志,在组件卸载时将其置为 false,并在异步回调和定时器调度中进行检查。

let isMounted = true;

const schedulePersistPinnedExtensions = (delay = SAVE_DEBOUNCE_MS) => {
  if (!isMounted) return;
  if (saveTimer) {
    clearTimeout(saveTimer);
  }
  saveTimer = setTimeout(async () => {
    saveTimer = null;
    await persistPinnedExtensions();
  }, delay);
};

const hydratePinnedPreferences = async () => {
  const hydrateStartVersion = pinnedPreferenceVersion;
  try {
    const response = await pluginPreferencesApi.getPinnedExtensions();
    if (!isMounted) return;
    const remoteData = response?.data?.data ?? {};
    const remoteNames = normalizePinnedExtensions(
      remoteData?.pinned_extensions,
    );
    const localNames = pinnedExtensionNames.value;
    const resolution = resolvePinnedExtensionNames({
      localNames,
      remoteNames,
      preferenceExists: remoteData?.preference_exists,
    });

    if (pinnedPreferenceVersion !== hydrateStartVersion) {
      return;
    }

    applyPinnedExtensionNames(resolution.names, {
      markSaved: !resolution.shouldMigrate,
    });

    if (resolution.shouldMigrate && resolution.migrateNames) {
      const migrationVersion = pinnedPreferenceVersion;
      const migrated = await persistPinnedExtensions();
      if (
        !migrated &&
        pinnedPreferenceVersion === migrationVersion &&
        migrationVersion > savedPinnedPreferenceVersion
      ) {
        schedulePersistPinnedExtensions(1000);
      }
    }
  } catch (error) {
    console.warn("加载插件置顶偏好失败,继续使用本地缓存", error);
  }
};

onMounted(hydratePinnedPreferences);

onUnmounted(() => {
  isMounted = false;
  if (saveTimer) {
    clearTimeout(saveTimer);
    saveTimer = null;
  }

  void persistPinnedExtensions();
});

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In the OpenAPI spec, PluginPinnedExtensionsRequest.pinned_extensions.items is defined as {}, which makes it effectively untyped; consider constraining it to type: string to reflect the actual usage and align with PluginPinnedExtensionsData.
  • In dashboard/src/api/v1.ts, pluginPreferencesApi.updatePinnedExtensions currently accepts names: unknown[]; tightening this to string[] (matching the schema and normalization logic) would catch misuse at compile time and better document the expected payload shape.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In the OpenAPI spec, `PluginPinnedExtensionsRequest.pinned_extensions.items` is defined as `{}`, which makes it effectively untyped; consider constraining it to `type: string` to reflect the actual usage and align with `PluginPinnedExtensionsData`.
- In `dashboard/src/api/v1.ts`, `pluginPreferencesApi.updatePinnedExtensions` currently accepts `names: unknown[]`; tightening this to `string[]` (matching the schema and normalization logic) would catch misuse at compile time and better document the expected payload shape.

## Individual Comments

### Comment 1
<location path="dashboard/src/api/v1.ts" line_range="1310" />
<code_context>
+      '/plugins/preferences/pinned',
+    );
+  },
+  updatePinnedExtensions(names: unknown[]) {
+    return apiV1Client.put<ApiEnvelope<PluginPinnedExtensionsData>>(
+      '/plugins/preferences/pinned',
+      { pinned_extensions: names },
+    );
+  },
</code_context>
<issue_to_address>
**suggestion:** Tighten the type of `updatePinnedExtensions` parameters to reflect the expected string[] payload.

The backend and OpenAPI schema expect `pinned_extensions` to be a `string[]`, but this method currently uses `names: unknown[]`, weakening type safety. Since the caller already normalizes via `normalizePinnedExtensions`, you can safely change this to `string[]` (or `readonly string[]`) to better align with the server contract and catch invalid shapes at compile time.

```suggestion
  updatePinnedExtensions(names: readonly string[]) {
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread dashboard/src/api/v1.ts
'/plugins/preferences/pinned',
);
},
updatePinnedExtensions(names: unknown[]) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Tighten the type of updatePinnedExtensions parameters to reflect the expected string[] payload.

The backend and OpenAPI schema expect pinned_extensions to be a string[], but this method currently uses names: unknown[], weakening type safety. Since the caller already normalizes via normalizePinnedExtensions, you can safely change this to string[] (or readonly string[]) to better align with the server contract and catch invalid shapes at compile time.

Suggested change
updatePinnedExtensions(names: unknown[]) {
updatePinnedExtensions(names: readonly string[]) {

@Sisyphbaous-DT-Project Sisyphbaous-DT-Project changed the title Fix plugin pin preference sync across devices fix: plugin pin preference sync across devices Jun 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:webui The bug / feature is about webui(dashboard) of astrbot. feature:plugin The bug / feature is about AstrBot plugin system. size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant