diff --git a/.changeset/memos-connection-setup.md b/.changeset/memos-connection-setup.md new file mode 100644 index 00000000..f9adda47 --- /dev/null +++ b/.changeset/memos-connection-setup.md @@ -0,0 +1,7 @@ +--- +'@inkcre/ext-memos': minor +--- + +简化 Memos 连接准备:自动生成或复用 PAT、按需启用 Core,通过 Memos 自有只读能力获取服务地址,并链接对应版本的作者帮助。 + +本次 Python 与 MF 需共同发行 0.3.0,不能复用公开 0.2.0。当前包基线为 0.1.0,单次 minor 只会生成 0.2.0;合并前必须协调 Version PR 的精确版本,不能将此 fragment 视为已完成 0.3.0 对齐。 diff --git a/.changeset/python-sdk03-companion-mf.md b/.changeset/python-sdk03-companion-mf.md new file mode 100644 index 00000000..67050534 --- /dev/null +++ b/.changeset/python-sdk03-companion-mf.md @@ -0,0 +1,6 @@ +--- +'@inkcre/ext-twitter': patch +'@inkcre/ext-mail': patch +--- + +配套支持 Python Host SDK 0.3 的 Python 发行,将 Twitter 和 Mail 的 Module Federation 分发推进到相同的精确 Extension Release。功能、界面、导出和 Web Host SDK 兼容范围保持不变。 diff --git a/apps/client-web/package.json b/apps/client-web/package.json index 8a2a5ad8..3f8d2d0c 100644 --- a/apps/client-web/package.json +++ b/apps/client-web/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@inkcre/core": "workspace:*", - "@inkcre/extension-runtime-client-web": "https://github.com/InKCre/ext-reg/releases/download/runtime-client-web-v0.1.0/inkcre-extension-runtime-client-web-0.1.0.tgz", + "@inkcre/extension-runtime-client-web": "https://github.com/InKCre/ext-reg/releases/download/runtime-client-web-v0.2.0/inkcre-extension-runtime-client-web-0.2.0.tgz", "@inkcre/ui-web": "2.0.1", "@module-federation/runtime": "^0.21.4", "@supabase/postgrest-js": "^2.84.0", diff --git a/apps/client-web/src/components/extension/extensionCard/extensionCard.md b/apps/client-web/src/components/extension/extensionCard/extensionCard.md index b33984f9..9b1aafab 100644 --- a/apps/client-web/src/components/extension/extensionCard/extensionCard.md +++ b/apps/client-web/src/components/extension/extensionCard/extensionCard.md @@ -22,6 +22,7 @@ A component that displays extension information and provides controls for toggli - Toggle enable/disable status with a switch - Mount an Extension-owned setup contribution from this browser's running Web Distribution - Keep setup availability independent of which Client is selected for enablement control +- Link the exact installed release's available global, Core, and Web documentation even when the extension is disabled or has no browser distribution; distinguish missing documentation from failed discovery - Edit extension configuration via JSON editor in a dialog - Change the exact shared version through the selected Host only while every Peer is disabled - Auto-formats configuration as JSON for easier editing diff --git a/apps/client-web/src/components/extension/extensionCard/extensionCard.vue b/apps/client-web/src/components/extension/extensionCard/extensionCard.vue index e1ef4386..bccd3bea 100644 --- a/apps/client-web/src/components/extension/extensionCard/extensionCard.vue +++ b/apps/client-web/src/components/extension/extensionCard/extensionCard.vue @@ -10,6 +10,11 @@ import { type JsonEditorValidation, } from '@inkcre/ui-web' import { getExtensionHost, getExtensionSetupContribution } from '@/core' +import { configStore, ExtensionRegistryOriginResolver } from '@inkcre/core' +import { + getExtensionDocumentation, + type ExtensionDocumentationLink, +} from '@inkcre/extension-runtime-client-web' import { extensionCardProps, extensionCardEmits } from './extensionCard' const props = defineProps(extensionCardProps) @@ -29,12 +34,38 @@ const isUninstalling = ref(false) const operationError = ref(null) const configModel = ref(JSON.stringify(props.extension.config, null, 2)) const versionModel = ref(props.extension.version) +const documentation = shallowRef([]) +const documentationStatus = ref<'loading' | 'available' | 'missing' | 'unavailable'>('loading') const canSaveConfig = computed( () => configValidation.value?.status === 'valid' && configValidation.value.text === configModel.value ) // --- computed --- +watch( + () => [props.extension.name, props.extension.version] as const, + async ([name, version], _previous, onCleanup) => { + let current = true + onCleanup(() => { + current = false + }) + documentation.value = [] + documentationStatus.value = 'loading' + try { + const origin = await new ExtensionRegistryOriginResolver( + () => configStore.peerConfig.extension_registry_url + ).resolve() + const links = await getExtensionDocumentation(origin, name, version) + if (!current) return + documentation.value = links ?? [] + documentationStatus.value = links?.length ? 'available' : 'missing' + } catch { + if (current) documentationStatus.value = 'unavailable' + } + }, + { immediate: true } +) + watch( () => props.extension.config, (config) => { @@ -184,6 +215,24 @@ const onUninstall = async () => { {{ t('extension.uninstallDisabled') }}

{{ operationError }}

+ +

+ {{ t('extension.documentationUnavailable') }} +

+ + + + + Memos Extension + + +
+ + + diff --git a/extensions/memos/package.json b/extensions/memos/package.json new file mode 100644 index 00000000..62f485a7 --- /dev/null +++ b/extensions/memos/package.json @@ -0,0 +1,46 @@ +{ + "name": "@inkcre/ext-memos", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "type-check": "vue-tsc --noEmit" + }, + "dependencies": { + "@inkcre/core": "workspace:*", + "@inkcre/extension-runtime-client-web": "https://github.com/InKCre/ext-reg/releases/download/runtime-client-web-v0.2.0/inkcre-extension-runtime-client-web-0.2.0.tgz" + }, + "devDependencies": { + "@inkcre/ext-dev-utils": "workspace:*", + "@inkcre/ui-web": "2.0.1", + "@module-federation/vite": "^1.1.0", + "@vitejs/plugin-vue": "^6.0.1", + "@vue/tsconfig": "^0.7.0", + "typescript": "catalog:", + "vite": "^7.0.6", + "vue-tsc": "catalog:" + }, + "peerDependencies": { + "@vueuse/core": "^14.0.0", + "pinia": "^3.0.0", + "vue": "^3.5.25", + "vue-router": "^4.5.0", + "zod": "^4.1.0" + }, + "inkcre": { + "name": "inkcre/memos", + "nickname": "Memos", + "module_federation": { + "host_sdk": "@inkcre/core", + "host_sdk_version": ">=0.3.0 <0.4.0" + } + }, + "intent": { + "skills": [ + "@inkcre/ui-web" + ] + } +} diff --git a/extensions/memos/src/DevApp.vue b/extensions/memos/src/DevApp.vue new file mode 100644 index 00000000..24f0141c --- /dev/null +++ b/extensions/memos/src/DevApp.vue @@ -0,0 +1,7 @@ + + + diff --git a/extensions/memos/src/MemosSetup.vue b/extensions/memos/src/MemosSetup.vue new file mode 100644 index 00000000..f7ce31a8 --- /dev/null +++ b/extensions/memos/src/MemosSetup.vue @@ -0,0 +1,306 @@ + + + + + diff --git a/extensions/memos/src/index.ts b/extensions/memos/src/index.ts new file mode 100644 index 00000000..7e83654e --- /dev/null +++ b/extensions/memos/src/index.ts @@ -0,0 +1,5 @@ +import type { ExtensionModule } from '@inkcre/core' +import MemosSetup from './MemosSetup.vue' + +const extension: ExtensionModule = { setup: { component: MemosSetup } } +export default extension diff --git a/extensions/memos/src/main.ts b/extensions/memos/src/main.ts new file mode 100644 index 00000000..d7aa7eed --- /dev/null +++ b/extensions/memos/src/main.ts @@ -0,0 +1,10 @@ +import { bootstrapExtension } from '@inkcre/ext-dev-utils' +import '@inkcre/ui-web/styles' +import App from './DevApp.vue' +import extensionModule from './index' + +void bootstrapExtension({ + rootComponent: App, + extensionModule, + routes: [{ path: '/', component: () => import('./MemosSetup.vue') }], +}).catch(() => console.error('Unable to start the Memos extension playground.')) diff --git a/extensions/memos/src/setup.ts b/extensions/memos/src/setup.ts new file mode 100644 index 00000000..b3227ff1 --- /dev/null +++ b/extensions/memos/src/setup.ts @@ -0,0 +1,77 @@ +import { + ExtensionModel, + configStore, + ExtensionRegistryOriginResolver, + PeerManager, + PeerProtocolResponseSchema, + type InstalledExtension, + type Peer, +} from '@inkcre/core' +import { + getExtensionDocumentation, + listAdvertisedExtensionManagementPeers, +} from '@inkcre/extension-runtime-client-web' + +export const extensionName = 'inkcre/memos' +export const tokenPattern = /^memos_pat_[0-9A-Za-z]{32}$/ + +export function personalAccessToken(extension: InstalledExtension): string | null { + const token = extension.config.personal_access_token + return typeof token === 'string' && tokenPattern.test(token) ? token : null +} + +export function generateToken(): string { + const bytes = crypto.getRandomValues(new Uint8Array(16)) + return `memos_pat_${Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('')}` +} + +export async function readSetup(): Promise<{ extension: ExtensionModel; peers: Peer[] }> { + const [extension, peers] = await Promise.all([ + ExtensionModel.get(extensionName), + listAdvertisedExtensionManagementPeers(), + ]) + if (!extension) throw new Error('Memos is not installed.') + return { extension, peers } +} + +export class MemosConnectionError extends Error { + constructor(readonly status: number) { + super(`Memos connection returned HTTP ${status}.`) + } +} + +export async function readServerUrl(peerId: string): Promise { + const result = await PeerManager.delegate('memos.connection.v1', {}, peerId) + const response = PeerProtocolResponseSchema.parse(result) + if (response.status !== 200) throw new MemosConnectionError(response.status) + const body = response.body + if ( + !body || + typeof body !== 'object' || + !('server_url' in body) || + typeof body.server_url !== 'string' + ) + throw new Error('Memos connection did not return a server URL.') + const url = new URL(body.server_url) + if ( + !['http:', 'https:'].includes(url.protocol) || + url.username || + url.password || + url.search || + url.hash + ) + throw new Error('Memos connection returned an invalid server URL.') + return url.href +} + +export async function readHelpUrl(version: string): Promise { + const origin = await new ExtensionRegistryOriginResolver( + () => configStore.peerConfig.extension_registry_url + ).resolve() + const documentation = await getExtensionDocumentation(origin, extensionName, version) + const entry = documentation?.find((link) => link.scope === 'module-federation') + if (!entry) return null + const url = new URL(entry.entry_url) + url.hash = 'prepare-your-client-connection' + return url.href +} diff --git a/extensions/memos/tsconfig.json b/extensions/memos/tsconfig.json new file mode 100644 index 00000000..dc91eb8d --- /dev/null +++ b/extensions/memos/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "outDir": "./dist/types", + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo", + "experimentalDecorators": true, + "skipLibCheck": true, + "paths": { + "@inkcre/core": ["../../packages/core/src/index.ts"], + "@inkcre/core/*": ["../../packages/core/src/*"] + } + }, + "include": ["src/**/*", "src/**/*.vue"] +} diff --git a/extensions/memos/vite.config.ts b/extensions/memos/vite.config.ts new file mode 100644 index 00000000..92a8b6b6 --- /dev/null +++ b/extensions/memos/vite.config.ts @@ -0,0 +1,21 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { federation } from '@module-federation/vite' +import mfShared from '../mf-shared' +import extensionPackage from './package.json' + +export default defineConfig({ + plugins: [ + vue(), + federation({ + name: 'extension.memos', + filename: 'remoteEntry.js', + manifest: true, + exposes: { '.': fileURLToPath(new URL('./src/index.ts', import.meta.url)) }, + shared: mfShared(extensionPackage.inkcre.module_federation.host_sdk_version), + }), + ], + base: './', + build: { target: 'es2022', outDir: 'dist/client-web', sourcemap: true }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e3a9e63a..e5ae8751 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,8 +96,8 @@ importers: specifier: workspace:* version: link:../../packages/core '@inkcre/extension-runtime-client-web': - specifier: https://github.com/InKCre/ext-reg/releases/download/runtime-client-web-v0.1.0/inkcre-extension-runtime-client-web-0.1.0.tgz - version: https://github.com/InKCre/ext-reg/releases/download/runtime-client-web-v0.1.0/inkcre-extension-runtime-client-web-0.1.0.tgz(@inkcre/core@packages+core)(@module-federation/runtime@0.21.6) + specifier: https://github.com/InKCre/ext-reg/releases/download/runtime-client-web-v0.2.0/inkcre-extension-runtime-client-web-0.2.0.tgz + version: https://github.com/InKCre/ext-reg/releases/download/runtime-client-web-v0.2.0/inkcre-extension-runtime-client-web-0.2.0.tgz(@inkcre/core@packages+core)(@module-federation/runtime@0.21.6) '@inkcre/ui-web': specifier: 2.0.1 version: 2.0.1(unocss@66.7.5(vite@7.3.6(@types/node@22.17.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass-embedded@1.90.0)(sass@1.101.6)(yaml@2.9.0)))(vue@3.5.40(typescript@5.9.3)) @@ -348,6 +348,55 @@ importers: specifier: ^4.1.12 version: 4.4.3 + extensions/memos: + dependencies: + '@inkcre/core': + specifier: workspace:* + version: link:../../packages/core + '@inkcre/extension-runtime-client-web': + specifier: https://github.com/InKCre/ext-reg/releases/download/runtime-client-web-v0.2.0/inkcre-extension-runtime-client-web-0.2.0.tgz + version: https://github.com/InKCre/ext-reg/releases/download/runtime-client-web-v0.2.0/inkcre-extension-runtime-client-web-0.2.0.tgz(@inkcre/core@packages+core)(@module-federation/runtime@0.22.1) + '@vueuse/core': + specifier: ^14.0.0 + version: 14.3.0(vue@3.5.40(typescript@5.9.3)) + pinia: + specifier: ^3.0.0 + version: 3.0.3(typescript@5.9.3)(vue@3.5.40(typescript@5.9.3)) + vue: + specifier: ^3.5.25 + version: 3.5.40(typescript@5.9.3) + vue-router: + specifier: ^4.5.0 + version: 4.5.1(vue@3.5.40(typescript@5.9.3)) + zod: + specifier: ^4.1.0 + version: 4.4.3 + devDependencies: + '@inkcre/ext-dev-utils': + specifier: workspace:* + version: link:../../packages/ext-dev-utils + '@inkcre/ui-web': + specifier: 2.0.1 + version: 2.0.1(unocss@66.7.5(vite@7.3.6(@types/node@22.17.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass-embedded@1.90.0)(sass@1.101.6)(yaml@2.9.0)))(vue@3.5.40(typescript@5.9.3)) + '@module-federation/vite': + specifier: ^1.1.0 + version: 1.7.1(rollup@4.59.0) + '@vitejs/plugin-vue': + specifier: ^6.0.1 + version: 6.0.3(vite@7.3.6(@types/node@22.17.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass-embedded@1.90.0)(sass@1.101.6)(yaml@2.9.0))(vue@3.5.40(typescript@5.9.3)) + '@vue/tsconfig': + specifier: ^0.7.0 + version: 0.7.0(typescript@5.9.3)(vue@3.5.40(typescript@5.9.3)) + typescript: + specifier: 'catalog:' + version: 5.9.3 + vite: + specifier: ^7.0.6 + version: 7.3.6(@types/node@22.17.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass-embedded@1.90.0)(sass@1.101.6)(yaml@2.9.0) + vue-tsc: + specifier: 'catalog:' + version: 3.3.8(typescript@5.9.3) + extensions/twitter: dependencies: '@inkcre/core': @@ -1323,11 +1372,11 @@ packages: cpu: [x64] os: [win32] - '@inkcre/extension-runtime-client-web@https://github.com/InKCre/ext-reg/releases/download/runtime-client-web-v0.1.0/inkcre-extension-runtime-client-web-0.1.0.tgz': - resolution: {integrity: sha512-UGDxtoN6Pcv+NCbr8InejfGg6UdvtCrZU7dZsx+Yev/aXmyA1YChfY7VicJTJRr5UIs9JbWe1smNwH1UB/bYQw==, tarball: https://github.com/InKCre/ext-reg/releases/download/runtime-client-web-v0.1.0/inkcre-extension-runtime-client-web-0.1.0.tgz} - version: 0.1.0 + '@inkcre/extension-runtime-client-web@https://github.com/InKCre/ext-reg/releases/download/runtime-client-web-v0.2.0/inkcre-extension-runtime-client-web-0.2.0.tgz': + resolution: {integrity: sha512-Rj39VKGY32qMAiYjoxZbmGR1dnEdQsNpVWjPBKor7beqE5WDpU9BAw7YMOCkouf6WxeX9jt8Ap70RbDkkKEyuQ==, tarball: https://github.com/InKCre/ext-reg/releases/download/runtime-client-web-v0.2.0/inkcre-extension-runtime-client-web-0.2.0.tgz} + version: 0.2.0 peerDependencies: - '@inkcre/core': '>=0.1.2 <0.2.0' + '@inkcre/core': '>=0.3.0 <0.4.0' '@module-federation/runtime': '>=0.21.4 <0.23.0' '@inkcre/ui-web@2.0.1': @@ -7211,13 +7260,20 @@ snapshots: '@img/sharp-win32-x64@0.35.2': optional: true - '@inkcre/extension-runtime-client-web@https://github.com/InKCre/ext-reg/releases/download/runtime-client-web-v0.1.0/inkcre-extension-runtime-client-web-0.1.0.tgz(@inkcre/core@packages+core)(@module-federation/runtime@0.21.6)': + '@inkcre/extension-runtime-client-web@https://github.com/InKCre/ext-reg/releases/download/runtime-client-web-v0.2.0/inkcre-extension-runtime-client-web-0.2.0.tgz(@inkcre/core@packages+core)(@module-federation/runtime@0.21.6)': dependencies: '@inkcre/core': link:packages/core '@module-federation/runtime': 0.21.6 semver: 7.8.5 zod: 4.1.12 + '@inkcre/extension-runtime-client-web@https://github.com/InKCre/ext-reg/releases/download/runtime-client-web-v0.2.0/inkcre-extension-runtime-client-web-0.2.0.tgz(@inkcre/core@packages+core)(@module-federation/runtime@0.22.1)': + dependencies: + '@inkcre/core': link:packages/core + '@module-federation/runtime': 0.22.1 + semver: 7.8.5 + zod: 4.1.12 + '@inkcre/ui-web@2.0.1(unocss@66.7.5(vite@7.3.6(@types/node@22.17.2)(jiti@2.7.0)(lightningcss@1.33.0)(sass-embedded@1.90.0)(sass@1.101.6)(yaml@2.9.0)))(vue@3.5.40(typescript@5.9.3))': dependencies: '@codemirror/autocomplete': 6.20.3 diff --git a/scripts/check-package-contract.mjs b/scripts/check-package-contract.mjs index 7d5691c8..aae6092a 100644 --- a/scripts/check-package-contract.mjs +++ b/scripts/check-package-contract.mjs @@ -26,6 +26,7 @@ await Promise.all( 'apps/client-webext/.output/chrome-mv3/content-scripts/content.js', 'apps/client-webext/.output/chrome-mv3/content-scripts/content.css', 'extensions/mail/dist/client-web/remoteEntry.js', + 'extensions/memos/dist/client-web/remoteEntry.js', 'extensions/twitter/dist/client-web/remoteEntry.js', ].map((path) => requireFile(`${repoRoot}/${path}`)) ) diff --git a/scripts/check-workspace-contract.mjs b/scripts/check-workspace-contract.mjs index 4fc6d9b3..af23f9f3 100644 --- a/scripts/check-workspace-contract.mjs +++ b/scripts/check-workspace-contract.mjs @@ -42,6 +42,12 @@ const workspaces = [ requiredScripts: ['build', 'dev', 'type-check'], builder: 'vite', }, + { + path: 'extensions/memos', + name: '@inkcre/ext-memos', + requiredScripts: ['build', 'dev', 'type-check'], + builder: 'vite', + }, ] const bannedDirectTooling = [