fix: 优化命令

This commit is contained in:
eondrcode 2026-07-10 13:53:55 +08:00
parent 1e20ae6519
commit 76875b7352
11 changed files with 901 additions and 155 deletions

View file

@ -264,7 +264,7 @@ BPM settings are split into focused pages:
| Page | What you can configure |
|------|------------------------|
| **Basic** | Language, persistent filters, delayed startup, auto takeover, startup update checks, source update checks, source auto-update, BPM tag visibility, ribbon order, commands, debug mode, and GitHub token |
| **Basic** | Language, persistent filters, delayed startup, auto takeover, startup update checks, source update checks, source auto-update, BPM tag visibility, ribbon order, command registration, debug mode, and GitHub token |
| **Main Page Actions** | Choose which plugin actions appear directly on plugin cards and which stay in the right-click menu |
| **Style** | Plugin list layout, item display style, group/tag styles, and disabled-plugin fading |
| **Groups** | Create, rename, recolor, and delete plugin groups |
@ -278,9 +278,14 @@ BPM settings are split into focused pages:
| Command | Availability | Description |
|---------|--------------|-------------|
| **Open the plugin manager** | Always available | Opens the BPM main interface |
| **Control a plugin** | Always available | Search a plugin, then enable, disable, single-start, restart, open settings, open folder, open repository, or copy ID |
| **Save current plugin state as profile** | Always available | Saves the current enabled/disabled state as a reusable command profile |
| **Restore previous plugin state** | Available after a BPM command changes plugin state | Restores the snapshot captured before the last command-driven state change |
| **Troubleshoot plugin conflicts** | Always available | Starts the conflict diagnosis workflow |
| **Enable/Disable selected plugin** | Optional setting | Registers one command per plugin for direct toggling |
| **One-click Enable/Disable selected group** | Optional setting | Registers group-level commands for batch toggling |
| **One-click Enable/Disable selected tag** | Optional setting | Registers tag-level commands for batch toggling |
| **Apply selected profile** | Optional setting | Registers one command per saved plugin profile |
---

View file

@ -262,7 +262,7 @@ BPM 设置按功能拆分为多个页面:
| 页面 | 可配置内容 |
|------|------------|
| **Basic** | 语言、筛选持久化、延迟启动、自动接管、启动检查更新、来源检查更新、来源自动更新、BPM 标签显示、Ribbon 编排、命令、调试模式和 GitHub Token |
| **Basic** | 语言、筛选持久化、延迟启动、自动接管、启动检查更新、来源检查更新、来源自动更新、BPM 标签显示、Ribbon 编排、命令注册、调试模式和 GitHub Token |
| **Main Page Actions** | 选择哪些插件操作直接显示在插件卡片上,哪些收纳到右键菜单 |
| **Style** | 插件列表布局、项目显示样式、分组/标签样式和禁用插件淡化 |
| **Groups** | 创建、重命名、重新着色和删除插件分组 |
@ -276,9 +276,14 @@ BPM 设置按功能拆分为多个页面:
| 命令 | 可用性 | 说明 |
|------|--------|------|
| **Open the plugin manager** | 始终可用 | 打开 BPM 主界面 |
| **Control a plugin** | 始终可用 | 搜索插件后执行启用、禁用、单次启动、重启、打开设置、打开目录、打开仓库或复制 ID |
| **Save current plugin state as profile** | 始终可用 | 将当前插件启用/禁用状态保存为可复用 Profile |
| **Restore previous plugin state** | BPM 命令改变插件状态后可用 | 恢复最近一次命令操作前保存的状态快照 |
| **Troubleshoot plugin conflicts** | 始终可用 | 启动冲突诊断流程 |
| **Enable/Disable [Plugin Name]** | 可选设置 | 为每个插件注册独立启用/禁用命令 |
| **One-click Enable/Disable [Group Name]** | 可选设置 | 为分组注册批量切换命令 |
| **One-click Enable/Disable [Tag Name]** | 可选设置 | 为标签注册批量切换命令 |
| **Apply [Profile Name]** | 可选设置 | 为已保存的插件 Profile 注册一键应用命令 |
---

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "better-plugins-manager",
"version": "1.0.12",
"version": "1.0.13",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "better-plugins-manager",
"version": "1.0.12",
"version": "1.0.13",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",

View file

@ -6,7 +6,7 @@
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
"version": "node version-bump.mjs && git add manifest.json versions.json package-lock.json"
},
"keywords": [],
"author": "zero",

View file

@ -1,146 +1,704 @@
import { App, PluginManifest } from "obsidian";
import Manager from "./main";
import { ManagerModal } from "./modal/manager-modal";
import { TroubleshootModal } from "./troubleshoot/troubleshoot-modal";
const Commands = (app: App, manager: Manager) => {
manager.addCommand({
id: 'manager-view',
name: manager.translator.t('命令_管理面板_描述'),
callback: () => { new ManagerModal(app, manager).open() }
});
// 排查冲突命令
manager.addCommand({
id: 'troubleshoot-conflicts',
name: manager.translator.t('排查_按钮_描述'),
callback: () => { new TroubleshootModal(app, manager).open() }
});
if (manager.settings.DELAY) {
// 单行命令
if (manager.settings.COMMAND_ITEM) {
const plugins: PluginManifest[] = Object.values(manager.appPlugins.manifests).filter((pm: PluginManifest) => pm.id !== manager.manifest.id);
plugins.forEach(plugin => {
const mp = manager.settings.Plugins.find(mp => mp.id === plugin.id);
if (mp) {
manager.addCommand({
id: `manager-${mp.id}`,
name: `${mp.enabled ? manager.translator.t('通用_关闭_文本') : manager.translator.t('通用_开启_文本')} ${mp.name} `,
callback: async () => {
if (mp.enabled) {
mp.enabled = false;
await manager.savePluginAndExport(mp.id);
await manager.appPlugins.disablePlugin(plugin.id);
Commands(app, manager);
} else {
mp.enabled = true;
await manager.savePluginAndExport(mp.id);
await manager.appPlugins.enablePlugin(plugin.id);
Commands(app, manager);
}
}
});
}
});
}
// 分组命令
if (manager.settings.COMMAND_GROUP) {
manager.settings.GROUPS.forEach((group) => {
manager.addCommand({
id: `manager-${group.id}-enabled`,
name: `${manager.translator.t('命令行_一键启用_文本')} ${group.name}`,
callback: async () => {
const filteredPlugins = manager.settings.Plugins.filter(plugin => plugin.group === group.id);
for (const plugin of filteredPlugins) {
if (plugin && !plugin.enabled) {
await manager.appPlugins.enablePlugin(plugin.id);
plugin.enabled = true;
await manager.savePluginAndExport(plugin.id);
}
}
Commands(app, manager);
}
});
manager.addCommand({
id: `manager-${group.id}-disable`,
name: `${manager.translator.t('命令行_一键禁用_文本')} ${group.name}`,
callback: async () => {
const filteredPlugins = manager.settings.Plugins.filter(plugin => plugin.group === group.id);
for (const plugin of filteredPlugins) {
if (plugin && plugin.enabled) {
await manager.appPlugins.disablePlugin(plugin.id);
plugin.enabled = false;
await manager.savePluginAndExport(plugin.id);
}
}
Commands(app, manager);
}
});
});
}
} else {
// 单行命令
if (manager.settings.COMMAND_ITEM) {
const plugins: PluginManifest[] = Object.values(manager.appPlugins.manifests).filter((pm: PluginManifest) => pm.id !== manager.manifest.id);
plugins.forEach(plugin => {
const enabled = manager.appPlugins.enabledPlugins.has(plugin.id);
manager.addCommand({
id: `manager-${plugin.id}`,
name: `${enabled ? manager.translator.t('命令行_禁用_文本') : manager.translator.t('命令行_启用_文本')} ${plugin.name} `,
callback: async () => {
if (enabled) {
await manager.appPlugins.disablePluginAndSave(plugin.id);
const mp = manager.settings.Plugins.find(p => p.id === plugin.id);
if (mp) mp.enabled = false;
await manager.savePluginAndExport(plugin.id);
Commands(app, manager);
} else {
await manager.appPlugins.enablePluginAndSave(plugin.id);
const mp = manager.settings.Plugins.find(p => p.id === plugin.id);
if (mp) mp.enabled = true;
await manager.savePluginAndExport(plugin.id);
Commands(app, manager);
}
}
});
});
}
// 分组命令
if (manager.settings.COMMAND_GROUP) {
manager.settings.GROUPS.forEach((group) => {
manager.addCommand({
id: `manager-${group.id}-enabled`,
name: `${manager.translator.t('命令行_一键启用_文本')} ${group.name} ${manager.translator.t('命令行_分组_文本')}`,
callback: async () => {
const filteredPlugins = manager.settings.Plugins.filter(plugin => plugin.group === group.id);
for (const plugin of filteredPlugins) {
await manager.appPlugins.enablePluginAndSave(plugin.id);
const mp = manager.settings.Plugins.find(p => p.id === plugin.id);
if (mp) mp.enabled = true;
await manager.savePluginAndExport(plugin.id);
}
Commands(app, manager);
}
});
manager.addCommand({
id: `manager-${group.id}-disable`,
name: `${manager.translator.t('命令行_一键禁用_文本')} ${group.name} ${manager.translator.t('命令行_分组_文本')}`,
callback: async () => {
const filteredPlugins = manager.settings.Plugins.filter(plugin => plugin.group === group.id);
for (const plugin of filteredPlugins) {
await manager.appPlugins.disablePluginAndSave(plugin.id);
const mp = manager.settings.Plugins.find(p => p.id === plugin.id);
if (mp) mp.enabled = false;
await manager.savePluginAndExport(plugin.id);
}
Commands(app, manager);
}
});
});
}
}
}
export default Commands
import {
App,
Command,
Notice,
PluginManifest,
SuggestModal,
normalizePath,
setIcon,
} from "obsidian";
import Manager from "./main";
import { ManagerModal } from "./modal/manager-modal";
import { TroubleshootModal } from "./troubleshoot/troubleshoot-modal";
import { BPM_IGNORE_TAG, ManagerPlugin } from "./data/types";
import { managerOpen } from "./utils";
import { ObsidianAppWithInternals, VaultAdapterWithBasePath } from "./obsidian-internals";
import { PluginCommandProfile, PluginCommandState } from "./settings/data";
type CommandManagerLike = {
commands?: Record<string, Command>;
removeCommand?: (id: string) => void;
};
type PluginActionId =
| "toggle"
| "enable"
| "disable"
| "single-start"
| "restart"
| "open-settings"
| "open-dir"
| "open-repo"
| "copy-id";
type PluginAction = {
id: PluginActionId;
label: string;
icon: string;
disabled?: boolean;
};
const commandServices = new WeakMap<Manager, ManagerCommandService>();
class ManagerCommandService {
private readonly app: App;
private readonly manager: Manager;
private staticCommandIds = new Set<string>();
private staticLanguage = "";
private dynamicCommandIds = new Set<string>();
private running = new Set<string>();
constructor(app: App, manager: Manager) {
this.app = app;
this.manager = manager;
}
refresh() {
this.refreshStaticCommands();
this.refreshDynamicCommands();
}
openPluginControl() {
new PluginControlModal(this.app, this).open();
}
openProfileNameModal() {
new ProfileNameModal(this.app, this).open();
}
getTranslator() {
return this.manager.translator;
}
getPluginManifests(): PluginManifest[] {
return Object.values(this.manager.appPlugins.manifests || {})
.filter((plugin) => plugin.id !== this.manager.manifest.id)
.sort((a, b) => (a.name || a.id).localeCompare(b.name || b.id));
}
getManagerPlugin(pluginId: string): ManagerPlugin | undefined {
return this.manager.settings.Plugins.find((plugin) => plugin.id === pluginId);
}
isPluginEnabled(pluginId: string): boolean {
const managerPlugin = this.getManagerPlugin(pluginId);
if (this.manager.settings.DELAY && managerPlugin && !managerPlugin.tags.includes(BPM_IGNORE_TAG)) {
return managerPlugin.enabled;
}
return this.manager.appPlugins.enabledPlugins.has(pluginId);
}
isActionablePlugin(pluginId: string): boolean {
if (pluginId === this.manager.manifest.id) return false;
if (!this.manager.appPlugins.manifests[pluginId]) return false;
const managerPlugin = this.getManagerPlugin(pluginId);
return !managerPlugin?.tags?.includes(BPM_IGNORE_TAG);
}
getPluginActions(pluginId: string): PluginAction[] {
const enabled = this.isPluginEnabled(pluginId);
const isActionable = this.isActionablePlugin(pluginId);
return [
{
id: "toggle",
label: enabled
? this.t("command_action_disable")
: this.t("command_action_enable"),
icon: enabled ? "power-off" : "power",
disabled: !isActionable,
},
{ id: "enable", label: this.t("command_action_enable"), icon: "power", disabled: !isActionable || enabled },
{ id: "disable", label: this.t("command_action_disable"), icon: "power-off", disabled: !isActionable || !enabled },
{
id: "single-start",
label: this.t("command_action_single_start"),
icon: "repeat-1",
disabled: this.manager.settings.DELAY || !isActionable || enabled,
},
{
id: "restart",
label: this.t("command_action_restart"),
icon: "refresh-ccw",
disabled: this.manager.settings.DELAY || !isActionable || !enabled,
},
{ id: "open-settings", label: this.t("command_action_open_settings"), icon: "settings", disabled: !enabled },
{ id: "open-dir", label: this.t("command_action_open_dir"), icon: "folder-open" },
{ id: "open-repo", label: this.t("command_action_open_repo"), icon: "github" },
{ id: "copy-id", label: this.t("command_action_copy_id"), icon: "copy" },
];
}
async runPluginAction(pluginId: string, actionId: PluginActionId) {
const manifest = this.manager.appPlugins.manifests[pluginId];
if (!manifest) {
new Notice(this.t("command_notice_missing_plugin", { id: pluginId }));
return;
}
switch (actionId) {
case "toggle":
await this.togglePlugin(pluginId);
break;
case "enable":
await this.setPluginEnabled(pluginId, true);
break;
case "disable":
await this.setPluginEnabled(pluginId, false);
break;
case "single-start":
await this.singleStartPlugin(pluginId);
break;
case "restart":
await this.restartPlugin(pluginId);
break;
case "open-settings":
await this.openPluginSettings(pluginId);
break;
case "open-dir":
this.openPluginDir(manifest);
break;
case "open-repo":
await this.openPluginRepo(pluginId);
break;
case "copy-id":
this.copyPluginId(pluginId);
break;
}
}
async togglePlugin(pluginId: string) {
await this.setPluginEnabled(pluginId, !this.isPluginEnabled(pluginId));
}
async setPluginEnabled(pluginId: string, targetEnabled: boolean) {
if (!this.isActionablePlugin(pluginId)) {
new Notice(this.t("command_notice_not_actionable"));
return;
}
await this.runLocked(`plugin:${pluginId}`, async () => {
this.capturePreviousState(this.t("command_snapshot_plugin", { name: this.getPluginName(pluginId) }));
const changed = await this.setPluginEnabledInternal(pluginId, targetEnabled);
await this.manager.saveSettings();
if (changed) {
this.refreshAfterStatusChange([pluginId]);
new Notice(targetEnabled
? this.t("command_notice_plugin_enabled", { name: this.getPluginName(pluginId) })
: this.t("command_notice_plugin_disabled", { name: this.getPluginName(pluginId) }));
}
});
}
async applyGroup(groupId: string, targetEnabled: boolean) {
const group = this.manager.settings.GROUPS.find((item) => item.id === groupId);
const label = group?.name || groupId;
const plugins = this.getActionableManagerPlugins((plugin) => plugin.group === groupId);
await this.applyPluginsEnabled(plugins, targetEnabled, this.t("command_snapshot_group", { name: label }));
}
async applyTag(tagId: string, targetEnabled: boolean) {
const tag = this.manager.settings.TAGS.find((item) => item.id === tagId);
const label = tag?.name || tagId;
const plugins = this.getActionableManagerPlugins((plugin) => plugin.tags.includes(tagId));
await this.applyPluginsEnabled(plugins, targetEnabled, this.t("command_snapshot_tag", { name: label }));
}
async saveCurrentProfile(name: string) {
const profileName = name.trim();
if (!profileName) {
new Notice(this.t("command_notice_profile_name_required"));
return;
}
const now = Date.now();
const existing = this.manager.settings.COMMAND_PROFILES.find((profile) => profile.name === profileName);
const profile: PluginCommandProfile = existing || {
id: `${this.slugify(profileName)}-${now}`,
name: profileName,
pluginStates: {},
createdAt: now,
};
profile.name = profileName;
profile.pluginStates = this.captureCurrentState();
profile.updatedAt = now;
if (!existing) this.manager.settings.COMMAND_PROFILES.push(profile);
await this.manager.saveSettings();
this.refresh();
new Notice(this.t("command_notice_profile_saved", { name: profile.name }));
}
async applyProfile(profileId: string) {
const profile = this.manager.settings.COMMAND_PROFILES.find((item) => item.id === profileId);
if (!profile) {
new Notice(this.t("command_notice_profile_missing"));
return;
}
await this.applyPluginStateMap(profile.pluginStates, this.t("command_snapshot_profile", { name: profile.name }));
}
async restorePreviousState() {
const snapshot = this.manager.settings.COMMAND_LAST_STATE;
if (!snapshot) {
new Notice(this.t("command_notice_no_snapshot"));
return;
}
await this.applyPluginStateMap(snapshot.pluginStates, this.t("command_snapshot_restore"), true);
}
private refreshStaticCommands() {
const language = this.manager.settings.LANGUAGE || "";
if (this.staticCommandIds.size > 0 && this.staticLanguage === language) return;
this.removeStaticCommands();
this.staticLanguage = language;
this.registerStaticCommands();
}
private registerStaticCommands() {
this.addStaticCommand({
id: "manager-view",
name: this.manager.translator.t("命令_管理面板_描述"),
callback: () => {
this.manager.managerModal = new ManagerModal(this.app, this.manager);
this.manager.managerModal.open();
},
});
this.addStaticCommand({
id: "control-plugin",
name: this.t("command_control_plugin"),
callback: () => this.openPluginControl(),
});
this.addStaticCommand({
id: "save-command-profile",
name: this.t("command_save_profile"),
callback: () => this.openProfileNameModal(),
});
this.addStaticCommand({
id: "restore-previous-command-state",
name: this.t("command_restore_previous_state"),
checkCallback: (checking) => {
const canRestore = Boolean(this.manager.settings.COMMAND_LAST_STATE);
if (checking) return canRestore;
if (canRestore) void this.restorePreviousState();
return canRestore;
},
});
this.addStaticCommand({
id: "troubleshoot-conflicts",
name: this.manager.translator.t("排查_按钮_描述"),
callback: () => { new TroubleshootModal(this.app, this.manager).open(); },
});
}
private refreshDynamicCommands() {
this.removeDynamicCommands();
if (this.manager.settings.COMMAND_ITEM) this.registerPluginCommands();
if (this.manager.settings.COMMAND_GROUP) this.registerGroupCommands();
if (this.manager.settings.COMMAND_TAG) this.registerTagCommands();
if (this.manager.settings.COMMAND_PROFILE) this.registerProfileCommands();
}
private registerPluginCommands() {
this.getPluginManifests()
.filter((plugin) => this.isActionablePlugin(plugin.id))
.forEach((plugin) => {
const enabled = this.isPluginEnabled(plugin.id);
this.addDynamicCommand({
id: `manager-${plugin.id}`,
name: `${enabled ? this.manager.translator.t("命令行_禁用_文本") : this.manager.translator.t("命令行_启用_文本")} ${plugin.name || plugin.id}`,
callback: () => { void this.togglePlugin(plugin.id); },
});
});
}
private registerGroupCommands() {
this.manager.settings.GROUPS.forEach((group) => {
const name = group.name || group.id;
this.addDynamicCommand({
id: `manager-${group.id}-enabled`,
name: `${this.manager.translator.t("命令行_一键启用_文本")} ${name} ${this.manager.translator.t("命令行_分组_文本")}`,
callback: () => { void this.applyGroup(group.id, true); },
});
this.addDynamicCommand({
id: `manager-${group.id}-disable`,
name: `${this.manager.translator.t("命令行_一键禁用_文本")} ${name} ${this.manager.translator.t("命令行_分组_文本")}`,
callback: () => { void this.applyGroup(group.id, false); },
});
});
}
private registerTagCommands() {
this.manager.settings.TAGS
.filter((tag) => tag.id !== BPM_IGNORE_TAG)
.forEach((tag) => {
const name = tag.name || tag.id;
const commandId = this.safeCommandPart(tag.id);
this.addDynamicCommand({
id: `manager-tag-${commandId}-enabled`,
name: `${this.t("command_enable_tag")} ${name}`,
callback: () => { void this.applyTag(tag.id, true); },
});
this.addDynamicCommand({
id: `manager-tag-${commandId}-disable`,
name: `${this.t("command_disable_tag")} ${name}`,
callback: () => { void this.applyTag(tag.id, false); },
});
});
}
private registerProfileCommands() {
this.manager.settings.COMMAND_PROFILES.forEach((profile) => {
this.addDynamicCommand({
id: `manager-profile-${this.safeCommandPart(profile.id)}-apply`,
name: `${this.t("command_apply_profile")} ${profile.name}`,
callback: () => { void this.applyProfile(profile.id); },
});
});
}
private addDynamicCommand(command: Command) {
const registered = this.manager.addCommand(command);
this.dynamicCommandIds.add(registered.id);
this.dynamicCommandIds.add(this.fullCommandId(command.id));
}
private addStaticCommand(command: Command) {
const registered = this.manager.addCommand(command);
this.staticCommandIds.add(registered.id);
this.staticCommandIds.add(this.fullCommandId(command.id));
}
private removeStaticCommands() {
this.removeCommands(this.staticCommandIds);
}
private removeDynamicCommands() {
this.removeCommands(this.dynamicCommandIds);
}
private removeCommands(commandIds: Set<string>) {
const commandManager = (this.app as unknown as { commands?: CommandManagerLike }).commands;
commandIds.forEach((id) => {
try {
commandManager?.removeCommand?.(id);
} catch {
// ignore unsupported internal command manager variants
}
if (commandManager?.commands) delete commandManager.commands[id];
});
commandIds.clear();
}
private async applyPluginsEnabled(plugins: ManagerPlugin[], targetEnabled: boolean, label: string) {
if (plugins.length === 0) {
new Notice(this.t("command_notice_no_actionable_plugins"));
return;
}
const states: PluginCommandState = {};
plugins.forEach((plugin) => { states[plugin.id] = targetEnabled; });
await this.applyPluginStateMap(states, label);
}
private async applyPluginStateMap(states: PluginCommandState, label: string, restoring = false) {
const entries = Object.entries(states).filter(([pluginId]) => this.isActionablePlugin(pluginId));
if (entries.length === 0) {
new Notice(this.t("command_notice_no_actionable_plugins"));
return;
}
await this.runLocked(`bulk:${label}`, async () => {
this.capturePreviousState(restoring ? this.t("command_snapshot_before_restore") : label);
const progress = new Notice(`${this.t("command_notice_applying")} 0/${entries.length}`, 0);
let changedCount = 0;
let processed = 0;
const changedIds: string[] = [];
try {
for (const [pluginId, targetEnabled] of entries) {
const changed = await this.setPluginEnabledInternal(pluginId, targetEnabled);
if (changed) {
changedCount++;
changedIds.push(pluginId);
}
processed++;
progress.setMessage(`${this.t("command_notice_applying")} ${processed}/${entries.length} · ${pluginId}`);
}
} finally {
progress.hide();
}
await this.manager.saveSettings();
this.refreshAfterStatusChange(changedIds);
new Notice(this.t("command_notice_bulk_done", { count: changedCount }));
});
}
private async setPluginEnabledInternal(pluginId: string, targetEnabled: boolean): Promise<boolean> {
const current = this.isPluginEnabled(pluginId);
const managerPlugin = this.getManagerPlugin(pluginId);
if (current === targetEnabled) {
if (managerPlugin) managerPlugin.enabled = targetEnabled;
return false;
}
if (this.manager.settings.DELAY && managerPlugin && !managerPlugin.tags.includes(BPM_IGNORE_TAG)) {
managerPlugin.enabled = targetEnabled;
if (targetEnabled) await this.manager.appPlugins.enablePlugin(pluginId);
else await this.manager.appPlugins.disablePlugin(pluginId);
} else {
if (targetEnabled) await this.manager.appPlugins.enablePluginAndSave(pluginId);
else await this.manager.appPlugins.disablePluginAndSave(pluginId);
if (managerPlugin) managerPlugin.enabled = targetEnabled;
}
return true;
}
private async singleStartPlugin(pluginId: string) {
if (this.manager.settings.DELAY || !this.isActionablePlugin(pluginId) || this.isPluginEnabled(pluginId)) return;
await this.runLocked(`single:${pluginId}`, async () => {
this.capturePreviousState(this.t("command_snapshot_single_start", { name: this.getPluginName(pluginId) }));
new Notice(this.manager.translator.t("管理器_单次启动中_提示"));
await this.manager.appPlugins.enablePlugin(pluginId);
this.refreshAfterStatusChange([pluginId]);
});
}
private async restartPlugin(pluginId: string) {
if (this.manager.settings.DELAY || !this.isActionablePlugin(pluginId) || !this.isPluginEnabled(pluginId)) return;
await this.runLocked(`restart:${pluginId}`, async () => {
this.capturePreviousState(this.t("command_snapshot_restart", { name: this.getPluginName(pluginId) }));
new Notice(this.manager.translator.t("管理器_重启中_提示"));
await this.manager.appPlugins.disablePluginAndSave(pluginId);
await this.manager.appPlugins.enablePluginAndSave(pluginId);
this.refreshAfterStatusChange([pluginId]);
});
}
private async openPluginSettings(pluginId: string) {
if (!this.isPluginEnabled(pluginId)) {
new Notice(this.t("command_notice_enable_before_settings"));
return;
}
const appSetting = (this.app as ObsidianAppWithInternals).setting;
await appSetting.open();
await appSetting.openTabById(pluginId);
}
private openPluginDir(plugin: PluginManifest) {
const getBasePath = (this.app.vault.adapter as VaultAdapterWithBasePath).getBasePath?.();
const basePath = getBasePath ? normalizePath(getBasePath) : "";
const cfgDir = this.app.vault.configDir;
const rawDir = plugin.dir || `plugins/${plugin.id}`;
const isAbsolute = new RegExp("^(?:[a-zA-Z]:[\\\\/]|[\\\\/])").test(rawDir);
let pluginDir: string;
if (isAbsolute) {
pluginDir = normalizePath(rawDir);
} else if (rawDir.startsWith(cfgDir) || rawDir.startsWith(".") || rawDir.startsWith("/")) {
pluginDir = normalizePath(`${basePath}/${rawDir}`);
} else {
pluginDir = normalizePath(`${basePath}/${cfgDir}/${rawDir}`);
}
managerOpen(pluginDir, this.manager);
}
private async openPluginRepo(pluginId: string) {
const repo = this.manager.settings.REPO_MAP?.[pluginId] || await this.manager.repoResolver.resolveRepo(pluginId);
if (repo) {
window.open(`https://github.com/${repo}`);
return;
}
const isBpmInstall = this.manager.settings.BPM_INSTALLED.includes(pluginId);
new Notice(isBpmInstall
? this.manager.translator.t("管理器_仓库未记录_提示")
: this.manager.translator.t("管理器_仓库需手动添加_提示"));
}
private copyPluginId(pluginId: string) {
void navigator.clipboard.writeText(pluginId);
new Notice(this.manager.translator.t("通知_ID已复制"));
}
private captureCurrentState(): PluginCommandState {
const state: PluginCommandState = {};
this.getPluginManifests()
.filter((plugin) => this.isActionablePlugin(plugin.id))
.forEach((plugin) => { state[plugin.id] = this.isPluginEnabled(plugin.id); });
return state;
}
private capturePreviousState(label: string) {
this.manager.settings.COMMAND_LAST_STATE = {
pluginStates: this.captureCurrentState(),
createdAt: Date.now(),
label,
};
}
private getActionableManagerPlugins(predicate: (plugin: ManagerPlugin) => boolean): ManagerPlugin[] {
return this.manager.settings.Plugins
.filter((plugin) => predicate(plugin))
.filter((plugin) => this.isActionablePlugin(plugin.id));
}
private refreshAfterStatusChange(pluginIds: string[]) {
this.refresh();
try {
if (pluginIds.length === 0) {
void this.manager.managerModal?.reloadShowData();
return;
}
pluginIds.forEach((pluginId) => this.manager.managerModal?.refreshPluginCard(pluginId, { allowReload: true }));
} catch {
// UI may not be open
}
}
private async runLocked(key: string, task: () => Promise<void>) {
if (this.running.has(key)) return;
this.running.add(key);
try {
await task();
} catch (error) {
console.error("[BPM] command failed", key, error);
new Notice(this.t("command_notice_failed"));
} finally {
this.running.delete(key);
}
}
private getPluginName(pluginId: string): string {
const manifest = this.manager.appPlugins.manifests[pluginId];
const managerPlugin = this.getManagerPlugin(pluginId);
return managerPlugin?.name || manifest?.name || pluginId;
}
private fullCommandId(id: string): string {
return `${this.manager.manifest.id}:${id}`;
}
private safeCommandPart(id: string): string {
return id.replace(/[^a-zA-Z0-9_-]/g, "-");
}
private slugify(name: string): string {
const slug = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
return slug || "profile";
}
private t(key: string, vars?: Record<string, string | number | boolean | null | undefined>): string {
return this.manager.translator.t(key, vars);
}
}
class PluginControlModal extends SuggestModal<PluginManifest> {
private readonly service: ManagerCommandService;
constructor(app: App, service: ManagerCommandService) {
super(app);
this.service = service;
this.setPlaceholder(service.getTranslator().t("command_control_placeholder"));
this.emptyStateText = service.getTranslator().t("command_control_empty");
}
getSuggestions(query: string): PluginManifest[] {
const lower = query.trim().toLowerCase();
return this.service.getPluginManifests()
.filter((plugin) => {
if (!lower) return true;
return `${plugin.name} ${plugin.id} ${plugin.description || ""}`.toLowerCase().includes(lower);
})
.slice(0, 50);
}
renderSuggestion(plugin: PluginManifest, el: HTMLElement) {
const row = el.createDiv({ cls: "manager-command-suggestion" });
const icon = row.createSpan({ cls: "manager-command-suggestion__icon" });
setIcon(icon, this.service.isPluginEnabled(plugin.id) ? "toggle-right" : "toggle-left");
const text = row.createDiv({ cls: "manager-command-suggestion__text" });
text.createDiv({ cls: "manager-command-suggestion__title", text: plugin.name || plugin.id });
text.createDiv({ cls: "manager-command-suggestion__meta", text: plugin.id });
}
onChooseSuggestion(plugin: PluginManifest) {
new PluginActionModal(this.app, this.service, plugin).open();
}
}
class PluginActionModal extends SuggestModal<PluginAction> {
private readonly service: ManagerCommandService;
private readonly plugin: PluginManifest;
constructor(app: App, service: ManagerCommandService, plugin: PluginManifest) {
super(app);
this.service = service;
this.plugin = plugin;
this.setPlaceholder(service.getTranslator().t("command_action_placeholder", { name: plugin.name || plugin.id }));
this.emptyStateText = service.getTranslator().t("command_control_empty");
}
getSuggestions(query: string): PluginAction[] {
const lower = query.trim().toLowerCase();
return this.service.getPluginActions(this.plugin.id)
.filter((action) => !lower || action.label.toLowerCase().includes(lower));
}
renderSuggestion(action: PluginAction, el: HTMLElement) {
const row = el.createDiv({ cls: "manager-command-suggestion" });
const icon = row.createSpan({ cls: "manager-command-suggestion__icon" });
setIcon(icon, action.icon);
const text = row.createDiv({ cls: "manager-command-suggestion__text" });
text.createDiv({ cls: "manager-command-suggestion__title", text: action.label });
text.createDiv({
cls: "manager-command-suggestion__meta",
text: action.disabled
? this.service.getTranslator().t("command_action_disabled")
: this.plugin.id,
});
if (action.disabled) row.addClass("is-disabled");
}
onChooseSuggestion(action: PluginAction) {
if (action.disabled) {
new Notice(this.service.getTranslator().t("command_action_disabled"));
return;
}
void this.service.runPluginAction(this.plugin.id, action.id);
}
}
class ProfileNameModal extends SuggestModal<string> {
private readonly service: ManagerCommandService;
constructor(app: App, service: ManagerCommandService) {
super(app);
this.service = service;
this.setPlaceholder(service.getTranslator().t("command_profile_name_placeholder"));
this.emptyStateText = service.getTranslator().t("command_profile_name_empty");
}
getSuggestions(query: string): string[] {
const value = query.trim();
if (!value) return [];
return [value];
}
renderSuggestion(value: string, el: HTMLElement) {
el.createDiv({ text: this.service.getTranslator().t("command_profile_save_as", { name: value }) });
}
onChooseSuggestion(value: string) {
void this.service.saveCurrentProfile(value);
}
}
const Commands = (app: App, manager: Manager) => {
let service = commandServices.get(manager);
if (!service) {
service = new ManagerCommandService(app, manager);
commandServices.set(manager, service);
}
service.refresh();
};
export default Commands;

View file

@ -264,9 +264,13 @@ export default {
_基础设置_筛选持久化_描述: 'After enabling, you will see the same plugin list every time you open the manager.',
_基础设置_单独命令_标题: 'Control Plugin Commands Separately',
_基础设置_单独命令_描述: 'Enable this option to control the enabled and disabled state of each plugin separately. (Restart Obsidian to take effect)',
_基础设置_单独命令_描述: 'Enable direct command palette toggles for individual plugins.',
_基础设置_分组命令_标题: 'Control Plugin Commands by Group',
_基础设置_分组命令_描述: 'Enable this option to enable or disable all plugins in a specified group with one click. (Restart Obsidian to take effect)',
_基础设置_分组命令_描述: 'Enable direct command palette actions for enabling or disabling a whole group.',
command_setting_tag_title: 'Control Plugin Commands by Tag',
command_setting_tag_desc: 'Enable direct command palette actions for enabling or disabling plugins by tag.',
command_setting_profile_title: 'Plugin Profile Commands',
command_setting_profile_desc: 'Register direct apply commands for saved plugin state profiles.',
_主页面功能_标题: 'Main Page Actions',
_主页面功能_描述: 'Choose whether each plugin action on the manager page is shown directly on the item or kept in the context menu.',
@ -312,6 +316,48 @@ export default {
_获取版本中文案: 'Fetching remote version info…',
_管理面板_描述: 'Open the plugin manager',
command_control_plugin: 'Control a plugin',
command_control_placeholder: 'Search plugin to control...',
command_control_empty: 'No matching plugin',
command_action_placeholder: 'Choose action for {name}',
command_action_enable: 'Enable',
command_action_disable: 'Disable',
command_action_single_start: 'Single start',
command_action_restart: 'Restart plugin',
command_action_open_settings: 'Open settings',
command_action_open_dir: 'Open plugin folder',
command_action_open_repo: 'Open repository',
command_action_copy_id: 'Copy plugin ID',
command_action_disabled: 'Unavailable for the current plugin state',
command_enable_tag: 'Enable tag',
command_disable_tag: 'Disable tag',
command_save_profile: 'Save current plugin state as profile',
command_restore_previous_state: 'Restore previous plugin state',
command_apply_profile: 'Apply plugin profile',
command_profile_name_placeholder: 'Profile name...',
command_profile_name_empty: 'Type a profile name',
command_profile_save_as: 'Save profile: {name}',
command_notice_profile_saved: 'Saved plugin profile: {name}',
command_notice_profile_missing: 'Plugin profile no longer exists',
command_notice_profile_name_required: 'Profile name is required',
command_notice_no_snapshot: 'No previous plugin state to restore',
command_notice_missing_plugin: 'Plugin not found: {id}',
command_notice_not_actionable: 'This plugin is not controlled by BPM',
command_notice_no_actionable_plugins: 'No actionable plugins found',
command_notice_applying: 'Applying plugin changes',
command_notice_bulk_done: 'Updated {count} plugins',
command_notice_plugin_enabled: 'Enabled {name}',
command_notice_plugin_disabled: 'Disabled {name}',
command_notice_enable_before_settings: 'Enable this plugin before opening its settings',
command_notice_failed: 'Command failed. Check the console for details.',
command_snapshot_plugin: 'Plugin command: {name}',
command_snapshot_group: 'Group command: {name}',
command_snapshot_tag: 'Tag command: {name}',
command_snapshot_profile: 'Profile command: {name}',
command_snapshot_restore: 'Restore previous plugin state',
command_snapshot_before_restore: 'Before restore',
command_snapshot_single_start: 'Single start: {name}',
command_snapshot_restart: 'Restart: {name}',
_BPM安装_名称: 'bpm install',
_Eondr插件_名称: 'Eondr plugin',
_下载更新_描述: 'Download an update (choose version, incl. pre-release)',

View file

@ -266,9 +266,13 @@ export default {
_基础设置_筛选持久化_描述: '启用后,您将在每次打开管理器时看到相同的插件列表。',
_基础设置_单独命令_标题: '单独控制插件命令',
_基础设置_单独命令_描述: '启用此选项可以单独控制每个插件的启用和禁用状态。(重启Obsidian生效)',
_基础设置_单独命令_描述: '在命令面板中为每个插件注册独立启用/禁用命令。',
_基础设置_分组命令_标题: '分组控制插件命令',
_基础设置_分组命令_描述: '启用此选项可以一键启用或禁用指定分组中的所有插件。(重启Obsidian生效)',
_基础设置_分组命令_描述: '在命令面板中为每个分组注册一键启用/禁用命令。',
command_setting_tag_title: '标签控制插件命令',
command_setting_tag_desc: '在命令面板中按标签一键启用或禁用插件。',
command_setting_profile_title: '插件 Profile 命令',
command_setting_profile_desc: '为已保存的插件状态方案注册一键应用命令。',
_BPM安装_名称: 'BPM 安装',
_Eondr插件_名称: 'Eondr 出品',
@ -316,6 +320,48 @@ export default {
_获取版本中文案: '正在获取云端版本信息…',
_管理面板_描述: '开启插件管理器',
command_control_plugin: '控制插件',
command_control_placeholder: '搜索要控制的插件...',
command_control_empty: '没有匹配的插件',
command_action_placeholder: '选择 {name} 的操作',
command_action_enable: '启用',
command_action_disable: '禁用',
command_action_single_start: '单次启动',
command_action_restart: '重启插件',
command_action_open_settings: '打开设置',
command_action_open_dir: '打开插件目录',
command_action_open_repo: '打开仓库',
command_action_copy_id: '复制插件 ID',
command_action_disabled: '当前插件状态不可用',
command_enable_tag: '启用标签',
command_disable_tag: '禁用标签',
command_save_profile: '保存当前插件状态为 Profile',
command_restore_previous_state: '恢复上一次插件状态',
command_apply_profile: '应用插件 Profile',
command_profile_name_placeholder: 'Profile 名称...',
command_profile_name_empty: '输入一个 Profile 名称',
command_profile_save_as: '保存 Profile{name}',
command_notice_profile_saved: '已保存插件 Profile{name}',
command_notice_profile_missing: '插件 Profile 不存在',
command_notice_profile_name_required: '请输入 Profile 名称',
command_notice_no_snapshot: '没有可恢复的上一次插件状态',
command_notice_missing_plugin: '未找到插件:{id}',
command_notice_not_actionable: '此插件不由 BPM 控制',
command_notice_no_actionable_plugins: '没有可操作的插件',
command_notice_applying: '正在应用插件变更',
command_notice_bulk_done: '已更新 {count} 个插件',
command_notice_plugin_enabled: '已启用 {name}',
command_notice_plugin_disabled: '已禁用 {name}',
command_notice_enable_before_settings: '请先启用此插件再打开设置',
command_notice_failed: '命令执行失败,请查看控制台。',
command_snapshot_plugin: '插件命令:{name}',
command_snapshot_group: '分组命令:{name}',
command_snapshot_tag: '标签命令:{name}',
command_snapshot_profile: 'Profile 命令:{name}',
command_snapshot_restore: '恢复上一次插件状态',
command_snapshot_before_restore: '恢复前状态',
command_snapshot_single_start: '单次启动:{name}',
command_snapshot_restart: '重启:{name}',
_下载更新_描述: '下载指定版本更新(支持预发布)',
_成功_提示: '已安装/更新插件:{name}',
_错误_限速: 'GitHub 请求受限403请配置 GitHub Token 后重试。',

View file

@ -26,6 +26,21 @@ export type TagFilterOperator = FilterOperator;
export type PluginOverviewLayout = "list" | "two-column";
export type PluginUpdateCheckMode = NonNullable<BetaSource["updateCheckMode"]>;
export type ReleaseCompatibilityMode = NonNullable<BetaSource["compatibilityMode"]>;
export type PluginCommandState = Record<string, boolean>;
export interface PluginCommandSnapshot {
pluginStates: PluginCommandState;
createdAt: number;
label?: string;
}
export interface PluginCommandProfile {
id: string;
name: string;
pluginStates: PluginCommandState;
createdAt: number;
updatedAt?: number;
}
export const DEFAULT_MAIN_PAGE_ACTION_PLACEMENT: Record<MainPageActionId, MainPageActionPlacement> = {
checkUpdate: "menu",
@ -76,6 +91,14 @@ export interface ManagerSettings {
COMMAND_ITEM: boolean;
/** 是否为每个分组注册批量启用/禁用命令。 */
COMMAND_GROUP: boolean;
/** 是否为每个标签注册批量启用/禁用命令。 */
COMMAND_TAG: boolean;
/** 是否为保存的插件状态方案注册一键应用命令。 */
COMMAND_PROFILE: boolean;
/** 命令面板保存的插件状态方案。 */
COMMAND_PROFILES: PluginCommandProfile[];
/** 命令面板批量操作前的上一份状态快照,用于撤销。 */
COMMAND_LAST_STATE?: PluginCommandSnapshot;
/** 是否在插件启动后自动检查插件更新。 */
STARTUP_CHECK_UPDATES: boolean;
/** 是否在插件启动后自动检查 GitHub 来源订阅的远程版本。 */
@ -187,6 +210,9 @@ export const DEFAULT_SETTINGS: ManagerSettings = {
SELF_CHECK_IGNORED: false,
COMMAND_ITEM: false,
COMMAND_GROUP: false,
COMMAND_TAG: false,
COMMAND_PROFILE: false,
COMMAND_PROFILES: [],
STARTUP_CHECK_UPDATES: false,
SOURCE_STARTUP_CHECK_UPDATES: false,
SOURCE_AUTO_UPDATE: true,

View file

@ -179,6 +179,28 @@ export default class ManagerBasis extends BaseSetting {
Commands(this.app, this.manager);
});
const CommandTagBar = new Setting(this.containerEl)
.setName(this.manager.translator.t("command_setting_tag_title"))
.setDesc(this.manager.translator.t("command_setting_tag_desc"));
const CommandTagToggle = new ToggleComponent(CommandTagBar.controlEl);
CommandTagToggle.setValue(this.settings.COMMAND_TAG);
CommandTagToggle.onChange((value) => {
this.settings.COMMAND_TAG = value;
void this.manager.saveSettings();
Commands(this.app, this.manager);
});
const CommandProfileBar = new Setting(this.containerEl)
.setName(this.manager.translator.t("command_setting_profile_title"))
.setDesc(this.manager.translator.t("command_setting_profile_desc"));
const CommandProfileToggle = new ToggleComponent(CommandProfileBar.controlEl);
CommandProfileToggle.setValue(this.settings.COMMAND_PROFILE);
CommandProfileToggle.onChange((value) => {
this.settings.COMMAND_PROFILE = value;
void this.manager.saveSettings();
Commands(this.app, this.manager);
});
heading("设置_基础设置_分组_开发网络");
const debugBar = new Setting(this.containerEl)

View file

@ -12171,3 +12171,36 @@ body.theme-dark .manager-transfer-segment[data-state="active"]:hover {
.manager-repo-page .manager-install__segment {
padding: 4px 8px !important;
}
.manager-command-suggestion {
align-items: center;
display: flex;
gap: 10px;
min-height: 38px;
}
.manager-command-suggestion.is-disabled {
opacity: 0.55;
}
.manager-command-suggestion__icon {
color: var(--text-muted);
display: inline-flex;
flex: 0 0 auto;
}
.manager-command-suggestion__text {
min-width: 0;
}
.manager-command-suggestion__title,
.manager-command-suggestion__meta {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.manager-command-suggestion__meta {
color: var(--text-muted);
font-size: var(--font-ui-smaller);
}

View file

@ -10,3 +10,8 @@ writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
versions[targetVersion] = minAppVersion;
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
let packageLock = JSON.parse(readFileSync("package-lock.json", "utf8"));
packageLock.version = targetVersion;
if (packageLock.packages?.[""]) packageLock.packages[""].version = targetVersion;
writeFileSync("package-lock.json", JSON.stringify(packageLock, null, 2));