diff --git a/docs/development/OpenNexus生产化实施进度-2026-09-08.md b/docs/development/OpenNexus生产化实施进度-2026-09-08.md index 7d58977..ce58dd7 100644 --- a/docs/development/OpenNexus生产化实施进度-2026-09-08.md +++ b/docs/development/OpenNexus生产化实施进度-2026-09-08.md @@ -1000,3 +1000,10 @@ Core 的独立数据目录目前不等于已授权 Vault。Python 旧笔记写 - 安装确认、在线回滚和卸载已接入仅主窗口可调用的 IPC。扩展存储升级到 schema 7,新增可幂等重放的卸载日志;已完成升级可以生成反向变更并通过原有原子事务恢复旧包和配置。卸载只删除活动指针,保留受管对象,不触碰外部路径。 - 定向原生 MCP 验证确认撤销后实例及工具注册均归零;升级→回滚→卸载及 schema 1→7 迁移测试通过,桌面全目标 Clippy `-D warnings` 与编译检查通过。 - 当前社区样例仍以系统 Python 作为入口,无法满足“可执行文件必须包含在签名包内”的原生沙箱门禁;Host 运行/调用 IPC 和两个样例的签名原生包尚未完成。因此 D-04 仍不得登记为通过,`extensions` capability 继续保持关闭。 + +### 2026-09-12 D-04 正式验收 + +- `markdown-workbench` 已改为确定性构建的包内 Rust MCP 可执行文件,发布 ZIP 不再携带或依赖 Python 入口;Python 兼容运行时的真实 ZIP 安装、启用、工具调用、命令调用及 `note-reviewer` Skill 依赖联动测试通过。 +- Host 已接通安装确认、活动包全量复核、原生启用、状态、调用审核、单次确认调用、停用、在线回滚和卸载 IPC。安装事务只在 MCP 达到 ready 后提交;清单、入口、许可或启动失败会恢复原活动指针。社区桌面页已提供明确的摘要确认与“安装并启用”操作。 +- D-04 driver 覆盖两个样例、包内原生入口、版本升级→回滚、幂等卸载、跨重启撤销、五秒内进程/工具归零、外部路径不变以及离线新安装拒绝。正式 Runner 报告为 `H:\OpenNexus-acceptance\d04-9ec0eb0\report`,结果 PASSED,1/1,耗时 38.86 秒且不含凭据。 +- D-04 可记为通过,正式验收累计 **22/30**;A-01、A-04、C-01 与 E-01~E-05 仍未通过。 diff --git a/frontend/src-tauri/src/main.rs b/frontend/src-tauri/src/main.rs index edee4fc..cc10078 100644 --- a/frontend/src-tauri/src/main.rs +++ b/frontend/src-tauri/src/main.rs @@ -114,7 +114,7 @@ fn host_capabilities(host: State<'_, Host>) -> serde_json::Value { .ok() .and_then(|mut core| core.as_mut().map(|c| c.available())) .unwrap_or(false); - serde_json::json!({"protocol":1,"workspace":true,"core":ready,"sync":true,"credentials":true,"extensions":false,"release":"preview","product":"OpenNexus"}) + serde_json::json!({"protocol":1,"workspace":true,"core":ready,"sync":true,"credentials":true,"extensions":cfg!(windows),"release":"preview","product":"OpenNexus"}) } #[derive(serde::Serialize)] diff --git a/frontend/src/features/community/DesktopPackages.spec.ts b/frontend/src/features/community/DesktopPackages.spec.ts index c22342e..493d440 100644 --- a/frontend/src/features/community/DesktopPackages.spec.ts +++ b/frontend/src/features/community/DesktopPackages.spec.ts @@ -24,9 +24,9 @@ it('uses themed surfaces for the section, empty state, and staged package rows', expect(populated.get('.package-list > li').classes()).toContain('item-card') populated.unmount() }) -it('loads durable staged metadata and previews without issuing install commands', async () => { +it('loads durable staged metadata and requires explicit confirmation before installation', async () => { native.invoke.mockImplementation(async command => command === 'extension_staged' ? [item] : { - fingerprint: 'fingerprint', dependencies: { packages: [{ ...item, permissions: ['notes.read'] }] }, changes: [{ target: { configuration: {} }, expected_revision: null }], + fingerprint: 'fingerprint', dependencies: { packages: [{ ...item, kind: 'plugin', permissions: ['notes.read'] }] }, changes: [{ target: { slot: 'slot', package_key: 'package-key', configuration: {} }, expected_revision: null }], }) const wrapper = component(); await flushPromises() expect(native.invoke).toHaveBeenCalledWith('extension_staged', { offset: 0, limit: 20 }) @@ -35,10 +35,36 @@ it('loads durable staged metadata and previews without issuing install commands' await flushPromises() expect(native.invoke).toHaveBeenLastCalledWith('extension_install_preview', { request: { root_key: 'package-key', vault_id: 'vault-one', configurations: { 'package-key': {} } } }) expect(wrapper.text()).toContain('notes.read') - expect(wrapper.text()).toContain('安装执行暂未开放') + expect(wrapper.text()).toContain('确认安装并启用') expect(native.invoke.mock.calls.every(call => ['extension_staged', 'extension_install_preview'].includes(call[0]))).toBe(true) wrapper.unmount() }) +it('confirms the reviewed payload then starts the native runtime', async () => { + vi.stubGlobal('crypto', { randomUUID: () => 'operation-id' }) + native.invoke.mockImplementation(async (command) => { + if (command === 'extension_staged') return [item] + if (command === 'extension_install_preview') return { + fingerprint: 'fingerprint', + dependencies: { packages: [{ ...item, kind: 'plugin', permissions: [] }] }, + changes: [{ target: { slot: 'slot', package_key: 'package-key', configuration: {} }, expected_revision: null }], + } + if (command === 'extension_stage_prepare') return 'request-id' + return { status: 'ready' } + }) + const wrapper = component(); await flushPromises() + await wrapper.findAll('button').find(button => button.text() === '查看安装预览')!.trigger('click') + await wrapper.findAll('button').find(button => button.text() === '检查依赖、权限与配置')!.trigger('click') + await flushPromises() + await wrapper.findAll('button').find(button => button.text() === '确认安装并启用')!.trigger('click') + await flushPromises() + expect(native.invoke).toHaveBeenCalledWith('extension_install_confirm', { request: { + request_id: 'request-id', operation_id: 'operation-id', fingerprint: 'fingerprint', + root_key: 'package-key', vault_id: 'vault-one', configurations: { 'package-key': {} }, + } }) + expect(native.invoke).toHaveBeenCalledWith('extension_enable', { slot: 'slot', vaultId: 'vault-one', installOperationId: 'operation-id' }) + expect(wrapper.text()).toContain('安装和运行健康检查已完成') + wrapper.unmount(); vi.unstubAllGlobals() +}) it('discards delayed preview after switching Vault', async () => { let resolve!: (value: unknown) => void native.invoke.mockImplementation(command => command === 'extension_staged' ? Promise.resolve([item]) : new Promise(done => { resolve = done })) diff --git a/frontend/src/features/community/DesktopPackages.vue b/frontend/src/features/community/DesktopPackages.vue index 99c6e1c..8716868 100644 --- a/frontend/src/features/community/DesktopPackages.vue +++ b/frontend/src/features/community/DesktopPackages.vue @@ -6,9 +6,10 @@ import AppDialog from '@/components/common/AppDialog.vue' const props = defineProps<{ refreshKey: number }>() const workspace = useWorkspaceStore() interface Package { package_key: string; source: string; namespace: string; package_id: string; version: string; state: string } -interface Preview { fingerprint: string; dependencies: { packages: Array<{ package_key: string; namespace: string; package_id: string; version: string; permissions: string[] }> }; changes: Array<{ target: { configuration: unknown }; expected_revision: string | null }> } +interface Preview { fingerprint: string; dependencies: { packages: Array<{ package_key: string; namespace: string; package_id: string; kind: string; version: string; permissions: string[] }> }; changes: Array<{ target: { slot: string; package_key: string; configuration: unknown }; expected_revision: string | null }> } const packages = ref([]), page = ref(0), busy = ref(false), error = ref('') const selected = ref(null), configuration = ref('{}'), preview = ref(null) +const completed = ref('') let generation = 0 const errors: Record = { VAULT_CHANGED: '笔记库已切换,请重新预览。', VAULT_NOT_OPEN: '请先打开笔记库。', @@ -31,7 +32,7 @@ async function refresh() { } catch (reason) { if (generation === current) error.value = message(reason) } finally { if (generation === current) busy.value = false } } -function choose(item: Package) { selected.value = item; configuration.value = '{}'; preview.value = null; error.value = '' } +function choose(item: Package) { selected.value = item; configuration.value = '{}'; preview.value = null; error.value = ''; completed.value = '' } async function inspect() { if (!selected.value || !workspace.vaultId) return const vaultId = workspace.vaultId, rootKey = selected.value.package_key, current = ++generation @@ -44,6 +45,31 @@ async function inspect() { } catch (reason) { if (generation === current) error.value = message(reason) } finally { if (generation === current) busy.value = false } } +async function install() { + if (!selected.value || !preview.value || !workspace.vaultId) return + const vaultId = workspace.vaultId, rootKey = selected.value.package_key, reviewed = preview.value + const current = ++generation; busy.value = true; error.value = ''; completed.value = '' + try { + const parsed = JSON.parse(configuration.value) + const requestId = await hostInvoke('extension_stage_prepare') + const operationId = crypto.randomUUID() + await hostInvoke('extension_install_confirm', { request: { + request_id: requestId, operation_id: operationId, fingerprint: reviewed.fingerprint, + root_key: rootKey, vault_id: vaultId, configurations: { [rootKey]: parsed }, + } }) + const runtimePackage = reviewed.dependencies.packages.find(item => item.kind === 'plugin' || item.kind === 'mcp') + if (!runtimePackage) throw new Error('EXTENSION_RUNTIME_UNSUPPORTED') + const change = reviewed.changes.find(item => item.target.package_key === runtimePackage.package_key) + if (!change) throw new Error('EXTENSION_INSTALL_CONFLICT') + await hostInvoke('extension_enable', { slot: change.target.slot, vaultId, installOperationId: operationId }) + if (generation === current && workspace.vaultId === vaultId) { + completed.value = '安装和运行健康检查已完成。' + preview.value = null + await refresh() + } + } catch (reason) { if (generation === current) error.value = message(reason) } + finally { if (generation === current) busy.value = false } +} function close() { ++generation; selected.value = null; preview.value = null; busy.value = false } watch(() => workspace.vaultId, close) watch(configuration, () => { preview.value = null }) @@ -85,6 +111,7 @@ onBeforeUnmount(() => ++generation)

未声明配置的包请保留空对象,不要填写密码或令牌。

{{ error }}

+

{{ completed }}

按安装顺序排列的包

  • @@ -92,7 +119,8 @@ onBeforeUnmount(() => ++generation)

    请求权限:{{ item.permissions.join('、') || '无' }}

检查配置
{{ JSON.stringify(preview.changes.map(change => change.target.configuration), null, 2) }}
-

依赖和配置检查完成。安装执行暂未开放,此预览不会启用包。

+

确认后将按以上摘要安装,并只在原生沙箱运行健康后提交活动版本。

+