From 1999f9677c8310f67f4532d0b1a691c69ee01216 Mon Sep 17 00:00:00 2001 From: QAyong Date: Thu, 8 Oct 2026 00:45:58 +0800 Subject: [PATCH] =?UTF-8?q?fix(buddy):=20=E7=BB=9F=E4=B8=80=E9=9D=A2?= =?UTF-8?q?=E5=90=91=E7=94=A8=E6=88=B7=E7=9A=84=E6=8F=92=E4=BB=B6=E6=9C=AF?= =?UTF-8?q?=E8=AF=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../electron/main/extensions/runtime/view.js | 2 +- apps/buddy/extensions/README.md | 14 ++++- apps/buddy/src/i18n/locales/en-US/settings.ts | 4 +- apps/buddy/src/i18n/locales/zh-CN/settings.ts | 4 +- .../__tests__/extensionLabels.spec.ts | 36 +++++++++++ .../src/modules/extensions/extensionLabels.ts | 28 ++++----- .../__tests__/pluginTerminology.spec.ts | 59 +++++++++++++++++++ 7 files changed, 126 insertions(+), 21 deletions(-) create mode 100644 apps/buddy/src/modules/extensions/__tests__/extensionLabels.spec.ts create mode 100644 apps/buddy/src/modules/extensions/widgets/__tests__/pluginTerminology.spec.ts diff --git a/apps/buddy/electron/main/extensions/runtime/view.js b/apps/buddy/electron/main/extensions/runtime/view.js index 708ce7b5..128009ab 100644 --- a/apps/buddy/electron/main/extensions/runtime/view.js +++ b/apps/buddy/electron/main/extensions/runtime/view.js @@ -165,7 +165,7 @@ async function initialize() { await request('view.ready') } catch (error) { - document.querySelector('main').textContent = overlay ? '' : 'This extension view could not be loaded.' + document.querySelector('main').textContent = overlay ? '' : 'This plugin view could not be loaded.' void request('view.failed', { code: /^EXTENSION_[A-Z_]+$/.test(error?.message) ? error.message : 'EXTENSION_VIEW_FAILED' }).catch(() => {}) } } diff --git a/apps/buddy/extensions/README.md b/apps/buddy/extensions/README.md index 9094ef6e..4bf6fdd2 100644 --- a/apps/buddy/extensions/README.md +++ b/apps/buddy/extensions/README.md @@ -1,5 +1,15 @@ # Lexora 插件开发 +## 术语与范围 + +**插件**是用户可以安装、启用、禁用、更新和卸载的功能包。代码和协议中的 `Extension`(工作台扩展)是这套插件机制的技术名称,不代表另一类可安装产品;用户界面统一使用“插件”。`extension.json`(插件清单)和 `.lexora-extension`(安装包后缀)保持不变。 + +**插件视图**是插件提供的界面,可以在资源面板里作为标签页打开,也可以显示为独立页面、工作台控件或装饰。资源面板是展示容器,不是插件的所属模块;一个插件可以提供多个视图,也可以只提供命令、AI 工具或后台动作。关闭标签页不等于禁用插件。 + +**Agent 内部扩展**是应用向底层 AI 运行时注入工具、策略和事件处理逻辑的机制,与用户安装的工作台插件不同;插件只能通过宿主开放的接口贡献 AI 能力,不能直接注入进程内扩展。 + +## 开发与安装 + 插件可以添加命令、上下文视图与独立配置页面。[Lexora Plugins](https://github.com/useLexora/plugins) 保存平铺的插件源码与市场目录;开发工具及创作 Skill 位于独立的 Lexora Plugin SDK。`sdk/index.d.ts` 提供公共类型,无需发布或安装单独的 npm SDK。 也可以直接在 Lexora 对话中输入 `$plugin-creator` 和需求,例如 `$plugin-creator 打字时窗口出现礼花`。内置技能生成源码并使用 Lexora 编译器打包,随后显示安装确认;源码保留在任务工作区,便于继续修改。 @@ -30,8 +40,8 @@ API 3 还支持原生设置模块、分组和单项,以及带权限的 Agent API 1 不开放 Node、shell 或任意 Desktop API。文件只读,仅能访问用户选中的文件;HTTPS GET 仅访问已确认的来源,不共享浏览器登录态。`context.storage` 保存私有 JSON,`dataVersion` 改变时入口提供 `migrate(previous, from, to)`,失败保留旧数据。上下文视图可用 `setState` 保存标签状态,独立配置页通过宿主命令保存设置。 -更新安装后,当前版本继续运行,点击“重启扩展”应用更新。禁用停止插件,重新启用保留配置;普通卸载保留插件设置、私有 JSON 与上下文视图位置,但移除代码和定时任务。“卸载并清理”还会删除设置、私有数据和视图状态,不删除任务内容或用户文件。 +更新安装后,当前版本继续运行,点击“重启插件”应用更新。禁用停止插件,重新启用保留配置;普通卸载保留插件设置、私有 JSON 与上下文视图位置,但移除代码和定时任务。“卸载并清理”还会删除设置、私有数据和视图状态,不删除任务内容或用户文件。 -需要独立开发窗口时,先构建 Electron,再运行 `node extensions/tools.mjs dev /path/to/plugin`。开发窗口使用单独的临时配置;修改后重新加载构建目录并重启扩展。SDK 快照可以通过 `node extensions/tools.mjs export-sdk /path/to/plugin-sdk/sdk` 导出。 +需要独立开发窗口时,先构建 Electron,再运行 `node extensions/tools.mjs dev /path/to/plugin`。开发窗口使用单独的临时配置;修改后重新加载构建目录并重启插件。SDK 快照可以通过 `node extensions/tools.mjs export-sdk /path/to/plugin-sdk/sdk` 导出。 API 2 通过 `contributes.placements` 提供装饰、工作台面板与思考等级控件。API 3 增加可替换的内容区、操作菜单、独立分屏挂载、带插件命名空间的输入命令与可取消交互。创作时用 `lexora_plugin_capabilities` 按需查询当前宿主支持的位置。内容仍在隔离视图运行;清单和调用示例见 [插件协议](../service/resources/skills/plugin-creator/references/protocol.md)。插件自行提供配置交互,内容区及控件可由插件启停并交还宿主默认内容;具体用法见 [内容插槽](../service/resources/skills/plugin-creator/references/slots.md)。旧版插件继续可用。 diff --git a/apps/buddy/src/i18n/locales/en-US/settings.ts b/apps/buddy/src/i18n/locales/en-US/settings.ts index a749b5b3..8bac5b24 100644 --- a/apps/buddy/src/i18n/locales/en-US/settings.ts +++ b/apps/buddy/src/i18n/locales/en-US/settings.ts @@ -55,8 +55,8 @@ export default { 'desktop.shortcuts.scope.main': 'Task pane', 'desktop.shortcuts.scope.context': 'Context', - 'desktop.settings.category.extensions': 'Workbench extensions', - 'desktop.settings.categoryDescription.extensions': 'Install and manage commands and views for the workbench.', + 'desktop.settings.category.extensions': 'Plugins', + 'desktop.settings.categoryDescription.extensions': 'Install and manage plugins to add commands and views to the workbench.', 'desktop.settings.category.browser': 'Browser', 'desktop.settings.categoryDescription.browser': 'Configure screenshots, zoom, Energy Saver, and browsing data for the built-in browser.', 'desktop.browser.preferences': 'Browsing preferences', diff --git a/apps/buddy/src/i18n/locales/zh-CN/settings.ts b/apps/buddy/src/i18n/locales/zh-CN/settings.ts index 7763e61e..e6b70def 100644 --- a/apps/buddy/src/i18n/locales/zh-CN/settings.ts +++ b/apps/buddy/src/i18n/locales/zh-CN/settings.ts @@ -53,8 +53,8 @@ export default { 'desktop.shortcuts.scope.main': '任务面板', 'desktop.shortcuts.scope.context': '上下文', - 'desktop.settings.category.extensions': '工作台扩展', - 'desktop.settings.categoryDescription.extensions': '安装和管理工作台的命令与视图扩展。', + 'desktop.settings.category.extensions': '插件', + 'desktop.settings.categoryDescription.extensions': '安装和管理插件,为工作台添加命令与视图。', 'desktop.settings.category.browser': '浏览器', 'desktop.settings.categoryDescription.browser': '配置内置浏览器的截图、缩放、节能模式与浏览数据。', 'desktop.browser.preferences': '浏览偏好', diff --git a/apps/buddy/src/modules/extensions/__tests__/extensionLabels.spec.ts b/apps/buddy/src/modules/extensions/__tests__/extensionLabels.spec.ts new file mode 100644 index 00000000..5ad2d74e --- /dev/null +++ b/apps/buddy/src/modules/extensions/__tests__/extensionLabels.spec.ts @@ -0,0 +1,36 @@ +import { readFileSync } from 'node:fs' +import { describe, expect, it } from 'vitest' +import enSettings from '@/i18n/locales/en-US/settings' +import zhSettings from '@/i18n/locales/zh-CN/settings' +import { extensionLabels } from '../extensionLabels' +import { extensionsPage } from '../routes' + +const cases = [ + { language: 'zh-CN', settings: zhSettings, labels: { title: '插件', empty: '尚未安装插件', restart: '重启插件', removeTitle: '卸载插件?', loading: '正在加载插件…', failed: '插件视图暂时不可用', retry: '重启插件' } }, + { language: 'en-US', settings: enSettings, labels: { title: 'Plugins', empty: 'No plugins installed', restart: 'Restart plugin', removeTitle: 'Uninstall plugin?', loading: 'Loading plugin…', failed: 'This plugin view is unavailable', retry: 'Restart plugin' } }, +] as const + +describe('user-facing plugin terminology', () => { + it.each(cases)('uses consistent labels across navigation, settings and management in $language', ({ language, settings, labels }) => { + const actual = extensionLabels(language) + expect(actual).toMatchObject(labels) + expect(extensionsPage.title(language)).toBe(actual.title) + expect(settings['desktop.settings.category.extensions']).toBe(actual.title) + expect(settings['desktop.settings.categoryDescription.extensions']).toMatch(/插件|plugins/) + for (const text of Object.values(actual)) + expect(text).not.toMatch(/扩展|\bextensions?\b/i) + }) + + it('preserves language fallback and bilingual label coverage', () => { + expect(extensionLabels('unknown')).toEqual(extensionLabels('zh-CN')) + expect(Object.keys(extensionLabels('en-US'))).toEqual(Object.keys(extensionLabels('zh-CN'))) + }) + + it('uses plugin terminology in the isolated view failure fallback', () => { + const source = readFileSync(new URL('../../../../electron/main/extensions/runtime/view.js', import.meta.url), 'utf8') + expect(source).toContain('\'This plugin view could not be loaded.\'') + expect(source).not.toContain('This extension view') + expect(source).toContain('channel: \'lexora-extension\'') + expect(source).toContain('\'EXTENSION_VIEW_FAILED\'') + }) +}) diff --git a/apps/buddy/src/modules/extensions/extensionLabels.ts b/apps/buddy/src/modules/extensions/extensionLabels.ts index 955a0e94..72c2531a 100644 --- a/apps/buddy/src/modules/extensions/extensionLabels.ts +++ b/apps/buddy/src/modules/extensions/extensionLabels.ts @@ -1,5 +1,5 @@ const zh = { - title: '工作台扩展', + title: '插件', marketplace: '插件市场', installed: '已安装', marketplaceEmpty: '插件市场尚未开放', @@ -13,7 +13,7 @@ const zh = { creatorUnavailable: '插件创作技能不可用,请在技能设置中启用 plugin-creator。', acquisitionOptions: '更多插件获取方式', development: '加载开发目录', - empty: '还没有安装工作台扩展', + empty: '尚未安装插件', permissions: '申请的权限', selected: '读取通过命令选中的文件', network: '访问网站', @@ -24,18 +24,18 @@ const zh = { confirm: '确认安装', version: '版本', pending: '待应用版本', - restart: '重启扩展', + restart: '重启插件', enable: '启用', disable: '禁用', remove: '卸载', removeAndClear: '卸载并清理', cleanupHint: '“卸载并清理”还会永久删除插件设置、私有数据和视图状态,无法撤销。任务内容和用户文件不受影响。', - removeTitle: '卸载扩展?', + removeTitle: '卸载插件?', retained: '普通卸载会保留插件设置、私有数据和视图位置,重新安装后可继续使用。', diagnostics: '运行诊断', - loading: '正在加载扩展…', - failed: '扩展视图暂时不可用', - retry: '重新启动', + loading: '正在加载插件…', + failed: '插件视图暂时不可用', + retry: '重启插件', dependencies: '依赖', active: '运行中', inactive: '按需启动', @@ -51,7 +51,7 @@ const zh = { error: '错误代码', } const en: typeof zh = { - title: 'Workbench extensions', + title: 'Plugins', marketplace: 'Marketplace', installed: 'Installed', marketplaceEmpty: 'The marketplace is not available yet', @@ -65,7 +65,7 @@ const en: typeof zh = { creatorUnavailable: 'The plugin creation skill is unavailable. Enable plugin-creator in Skills settings.', acquisitionOptions: 'More ways to add plugins', development: 'Load development folder', - empty: 'No workbench extensions installed', + empty: 'No plugins installed', permissions: 'Requested permissions', selected: 'Read files selected through commands', network: 'Access website', @@ -76,18 +76,18 @@ const en: typeof zh = { confirm: 'Confirm installation', version: 'Version', pending: 'Pending version', - restart: 'Restart extension', + restart: 'Restart plugin', enable: 'Enable', disable: 'Disable', remove: 'Uninstall', removeAndClear: 'Uninstall and clear data', cleanupHint: '“Uninstall and clear data” also permanently deletes plugin settings, private data and view state. This cannot be undone. Tasks and user files are not affected.', - removeTitle: 'Uninstall extension?', + removeTitle: 'Uninstall plugin?', retained: 'Regular uninstall retains plugin settings, private data and view positions for reinstallation.', diagnostics: 'Runtime diagnostics', - loading: 'Loading extension…', - failed: 'This extension view is unavailable', - retry: 'Restart', + loading: 'Loading plugin…', + failed: 'This plugin view is unavailable', + retry: 'Restart plugin', dependencies: 'Dependencies', active: 'Running', inactive: 'On demand', diff --git a/apps/buddy/src/modules/extensions/widgets/__tests__/pluginTerminology.spec.ts b/apps/buddy/src/modules/extensions/widgets/__tests__/pluginTerminology.spec.ts new file mode 100644 index 00000000..c92e71a1 --- /dev/null +++ b/apps/buddy/src/modules/extensions/widgets/__tests__/pluginTerminology.spec.ts @@ -0,0 +1,59 @@ +// @vitest-environment jsdom +import type { ExtensionViewInput } from '@buddy-shared/extensions/extensionApi' +import { afterEach, expect, it, vi } from 'vitest' +import { createApp, h, nextTick, shallowReactive, shallowRef } from 'vue' +import { extensionLabels } from '../../extensionLabels' +import DesktopExtensionWorkbench from '../../layouts/DesktopExtensionWorkbench.vue' +import DesktopExtensionSurface from '../DesktopExtensionSurface.vue' + +const context = { + language: shallowRef('zh-CN'), + state: { api: { restart: vi.fn(async () => {}) } }, + views: { surfaces: shallowReactive(new Map()), show: vi.fn(), hide: vi.fn() }, +} +vi.mock('../../extensionContext', () => ({ useExtensionContext: () => context })) +vi.mock('@/shared/ui/navigation/DesktopBackToTasksButton.vue', () => ({ default: { render: () => null } })) + +const disposables: (() => void)[] = [] +afterEach(() => { + disposables.splice(0).forEach(dispose => dispose()) + context.views.surfaces.clear() + vi.clearAllMocks() +}) + +function mount(render: () => ReturnType) { + const element = document.createElement('div') + document.body.append(element) + const app = createApp({ render }) + app.mount(element) + disposables.push(() => { + app.unmount() + element.remove() + }) + return element +} + +it.each(['zh-CN', 'en-US'])('labels the plugin management selector consistently in %s', (language) => { + const element = mount(() => h(DesktopExtensionWorkbench, { language, section: 'installed' })) + const labels = extensionLabels(language) + expect(element.querySelector('[aria-label]')?.getAttribute('aria-label')).toBe(labels.title) + expect(element.textContent).toContain(labels.installed) +}) + +it.each(['zh-CN', 'en-US'])('shows plugin loading and failure copy and restarts the plugin in %s', async (language) => { + context.language.value = language + const input: ExtensionViewInput = { extensionId: 'tests.reader', viewId: 'reader-view', viewType: 'tests.reader.view', resource: null, state: {}, stateVersion: 1 } + const labels = extensionLabels(language) + const element = mount(() => h(DesktopExtensionSurface, { input, visible: true })) + await nextTick() + expect(element.querySelector('[role="status"]')?.textContent).toContain(labels.loading) + + context.views.surfaces.set(input.viewId, { error: 'EXTENSION_VIEW_FAILED', session: null, eligible: true }) + await nextTick() + expect(element.querySelector('[role="status"]')?.textContent).toContain(labels.failed) + expect(element.textContent).not.toContain(labels.loading) + const button = element.querySelector('button')! + expect(button.textContent).toContain(labels.restart) + button.click() + expect(context.state.api.restart).toHaveBeenCalledExactlyOnceWith(input.extensionId) +})