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 @@
+
+
+
+
+
+ {{
+ tr('Connect a Memos-compatible app to this instance.', '将兼容 Memos 的 App 连接到此实例。')
+ }}
+
+ {{ error }}
+
+
+ {{
+ tr(
+ 'No online Core can manage Memos. Start Core, then refresh.',
+ '没有可管理 Memos 的在线 Core。请启动 Core 后刷新。'
+ )
+ }}
+
+
+
+ Core: {{ peer.name }}
+
+
+ {{ tr('Connection details are ready', '连接信息已准备好') }}
+
+
+
+
+
+
+
+
+ {{
+ tr(
+ 'Paste both values into your app. Keep the PAT private.',
+ '将两项信息填入 App,并妥善保管 PAT。'
+ )
+ }}
+
+
+
+
+ {{ notice }}
+ {{
+ tr('Memos connection help', 'Memos 连接帮助')
+ }}
+
+ {{ tr('No connection guide for this version.', '此版本尚无连接指南。') }}
+
+
+ {{ tr('Connection help is temporarily unavailable.', '连接帮助暂时不可用。') }}
+
+
+
+
+
+
+
+
+
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 = [