From 83abc0033902348d2bf7562ab6e09f1ac35dbc04 Mon Sep 17 00:00:00 2001 From: Lan_zhijiang Date: Mon, 21 Sep 2026 08:32:06 +0800 Subject: [PATCH 1/5] =?UTF-8?q?chore(docs):=20=E6=9B=B4=E6=96=B0=E5=85=B1?= =?UTF-8?q?=E4=BA=AB=E6=8F=92=E4=BB=B6=E6=96=87=E6=A1=A3=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 固定到支持静态文档构建与发布协议的 Hub 提交 --- docs/_shared | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_shared b/docs/_shared index 5d0d8d7..5aa97e6 160000 --- a/docs/_shared +++ b/docs/_shared @@ -1 +1 @@ -Subproject commit 5d0d8d7facfe68227c5bbc93361ba77f4573e7fa +Subproject commit 5aa97e6243dce02dd27c5f58eda345be2ba47297 From f29a407ae02980b221e9419bb38421aa95805771 Mon Sep 17 00:00:00 2001 From: Lan_zhijiang Date: Mon, 21 Sep 2026 08:33:22 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat(memos):=20=E6=8F=90=E4=BE=9B=E5=8F=AF?= =?UTF-8?q?=E8=A7=86=E5=8C=96=E8=BF=9E=E6=8E=A5=E5=87=86=E5=A4=87=E5=90=91?= =?UTF-8?q?=E5=AF=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 Memos Module Federation 扩展及中英文安装文档 - 通过可视化流程生成并复用连接 PAT,支持中断恢复 - 将包结构和工作区约束纳入现有检查 --- .changeset/memos-connection-setup.md | 5 + .../module-federation/.vitepress/config.mts | 9 + .../.vitepress/theme/index.ts | 1 + .../memos/docs/module-federation/index.md | 32 ++ extensions/memos/index.html | 12 + extensions/memos/package.json | 45 +++ extensions/memos/src/DevApp.vue | 7 + extensions/memos/src/MemosSetup.vue | 339 ++++++++++++++++++ extensions/memos/src/index.ts | 5 + extensions/memos/src/main.ts | 10 + extensions/memos/src/setup.ts | 71 ++++ extensions/memos/tsconfig.json | 14 + extensions/memos/vite.config.ts | 21 ++ pnpm-lock.yaml | 46 +++ scripts/check-package-contract.mjs | 1 + scripts/check-workspace-contract.mjs | 6 + 16 files changed, 624 insertions(+) create mode 100644 .changeset/memos-connection-setup.md create mode 100644 extensions/memos/docs/module-federation/.vitepress/config.mts create mode 100644 extensions/memos/docs/module-federation/.vitepress/theme/index.ts create mode 100644 extensions/memos/docs/module-federation/index.md create mode 100644 extensions/memos/index.html create mode 100644 extensions/memos/package.json create mode 100644 extensions/memos/src/DevApp.vue create mode 100644 extensions/memos/src/MemosSetup.vue create mode 100644 extensions/memos/src/index.ts create mode 100644 extensions/memos/src/main.ts create mode 100644 extensions/memos/src/setup.ts create mode 100644 extensions/memos/tsconfig.json create mode 100644 extensions/memos/vite.config.ts diff --git a/.changeset/memos-connection-setup.md b/.changeset/memos-connection-setup.md new file mode 100644 index 0000000..4f23174 --- /dev/null +++ b/.changeset/memos-connection-setup.md @@ -0,0 +1,5 @@ +--- +'@inkcre/ext-memos': minor +--- + +新增 Memos 可视化连接准备向导,生成或复用 PAT、启用 Core 并提供客户端连接信息;首次 MF 发行对齐已有 Python 0.2.0。 diff --git a/extensions/memos/docs/module-federation/.vitepress/config.mts b/extensions/memos/docs/module-federation/.vitepress/config.mts new file mode 100644 index 0000000..da0a092 --- /dev/null +++ b/extensions/memos/docs/module-federation/.vitepress/config.mts @@ -0,0 +1,9 @@ +import { extensionDocs } from '../../../../../docs/_shared/website/extension-docs/preset.mts' + +export default extensionDocs({ + name: 'inkcre/memos', + title: 'Memos Web Setup', + scope: 'module-federation', + description: 'Prepare a Memos client connection from the Web interface.', + sidebar: [{ text: 'Connection Setup', link: '/' }], +}) diff --git a/extensions/memos/docs/module-federation/.vitepress/theme/index.ts b/extensions/memos/docs/module-federation/.vitepress/theme/index.ts new file mode 100644 index 0000000..e23b068 --- /dev/null +++ b/extensions/memos/docs/module-federation/.vitepress/theme/index.ts @@ -0,0 +1 @@ +export { default } from '../../../../../../docs/_shared/website/.vitepress/theme/index' diff --git a/extensions/memos/docs/module-federation/index.md b/extensions/memos/docs/module-federation/index.md new file mode 100644 index 0000000..3e7e75c --- /dev/null +++ b/extensions/memos/docs/module-federation/index.md @@ -0,0 +1,32 @@ +--- +description: Use the Memos Web Extension to prepare connection details for a compatible app. +--- + +# Prepare your client connection + +The Memos Web Extension provides a connection-preparation interface. It does not create a Source, +schedule collection, or write a test note. The Core and browser must use matching Distributions of +the same installed Extension Release. + +1. In **Extensions**, enable Memos in this browser, then open **Setup**. +2. Choose the online Core providing the service. A single eligible Core is selected automatically. +3. If its address is missing, set the Core's Public HTTP Base URL in **Clients → Config** and + select **Refresh status**. The address must be reachable from your client device. +4. If you have no saved PAT, select **Generate PAT** or enter one you already prepared, then select + **Save PAT and enable Memos**. It must have the `memos_pat_` prefix and 32 ASCII letters or digits. +5. If a PAT already exists, the page reuses it. Select **Enable Memos on this Core** if necessary. +6. Copy **Server URL** and **Personal Access Token** into your compatible client's sign-in screen. + Keep `/memos` in the URL; do not add `/api/v1` or use the PostgREST address. + +An already configured connection opens directly on these connection details. Opening or refreshing +the page never generates or rotates a PAT. The result says that the information is ready; verify +the actual sign-in in the external app. The established compatibility baseline is MoeMemos Android +2.0.4, not every Memos version. + +Saving and enabling are separate operations. If saving fails, the draft remains available. If +enablement fails after saving, retry with that saved PAT. An uncertain response first requires +reading the actual saved state; do not generate another credential just to retry. If copying is +unavailable, reveal the token and manually select it. Hide it again before sharing a screenshot. + +PAT replacement and revocation remain explicit Extension Config operations. Replacing a token +requires updating every app that used the old token. This setup interface never does it silently. diff --git a/extensions/memos/index.html b/extensions/memos/index.html new file mode 100644 index 0000000..f9c1874 --- /dev/null +++ b/extensions/memos/index.html @@ -0,0 +1,12 @@ + + + + + + Memos Extension + + +
+ + + diff --git a/extensions/memos/package.json b/extensions/memos/package.json new file mode 100644 index 0000000..699ed1a --- /dev/null +++ b/extensions/memos/package.json @@ -0,0 +1,45 @@ +{ + "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:*" + }, + "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 0000000..24f0141 --- /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 0000000..f74fe2b --- /dev/null +++ b/extensions/memos/src/MemosSetup.vue @@ -0,0 +1,339 @@ + + + + + diff --git a/extensions/memos/src/index.ts b/extensions/memos/src/index.ts new file mode 100644 index 0000000..7e83654 --- /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 0000000..d7aa7ee --- /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 0000000..1cf8ad7 --- /dev/null +++ b/extensions/memos/src/setup.ts @@ -0,0 +1,71 @@ +import { + ExtensionModel, + InstalledExtensionSchema, + PeerManager, + PeerProtocolResponseSchema, + type InstalledExtension, + type JsonValue, + type Peer, +} from '@inkcre/core' + +export const extensionName = 'inkcre/memos' +const management = 'core.extension.management.v1' +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 function serverUrl(peer: Peer): string | null { + const value = peer.config.http_public_base_url + if (typeof value !== 'string' || !value.trim()) return null + try { + const url = new URL(value) + if ( + !['http:', 'https:'].includes(url.protocol) || + url.username || + url.password || + url.search || + url.hash + ) + return null + url.pathname = `${url.pathname.replace(/\/+$/, '')}/memos` + return url.href + } catch { + return null + } +} + +export async function readSetup(): Promise<{ extension: ExtensionModel; peers: Peer[] }> { + const [extension, peers] = await Promise.all([ + ExtensionModel.get(extensionName), + PeerManager.listLive(), + ]) + if (!extension) throw new Error('Memos is not installed.') + return { + extension, + peers: peers.filter((peer) => { + try { + return peer.capabilitySnapshot().some((capability) => capability.id === management) + } catch { + return false + } + }), + } +} + +export async function manageMemos(peer: Peer, body: JsonValue): Promise { + const result = await PeerManager.delegate(management, { body }, peer.id) + const response = PeerProtocolResponseSchema.parse(result) + if (response.status < 200 || response.status >= 300) { + // Validation payloads can echo the PAT. The UI gives action-specific recovery text instead. + throw new Error(`Core returned HTTP ${response.status}.`) + } + return InstalledExtensionSchema.parse(response.body) +} diff --git a/extensions/memos/tsconfig.json b/extensions/memos/tsconfig.json new file mode 100644 index 0000000..dc91eb8 --- /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 0000000..92a8b6b --- /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 e3a9e63..3bc5a08 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -348,6 +348,52 @@ importers: specifier: ^4.1.12 version: 4.4.3 + extensions/memos: + dependencies: + '@inkcre/core': + specifier: workspace:* + version: link:../../packages/core + '@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': diff --git a/scripts/check-package-contract.mjs b/scripts/check-package-contract.mjs index 7d5691c..aae6092 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 4fc6d9b..af23f9f 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 = [ From 2f0692352de5a166de5caeaa21696fcc992f3209 Mon Sep 17 00:00:00 2001 From: Lan_zhijiang Date: Mon, 21 Sep 2026 18:07:09 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat(memos):=20=E7=B2=BE=E7=AE=80=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E5=87=86=E5=A4=87=E5=B9=B6=E9=93=BE=E6=8E=A5=E7=B2=BE?= =?UTF-8?q?=E7=A1=AE=E5=8F=91=E8=A1=8C=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/memos-connection-setup.md | 4 +- .../extension/extensionCard/extensionCard.md | 1 + .../extension/extensionCard/extensionCard.vue | 49 +++ apps/client-web/src/extension-peer-control.ts | 54 +-- apps/client-web/src/locales/messages/en.json | 7 + .../src/locales/messages/zh-CN.json | 7 + docs/30-unit-tdd/native-extension-runtime.md | 46 ++- .../memos/docs/module-federation/index.md | 29 +- extensions/memos/package.json | 3 +- extensions/memos/src/MemosSetup.vue | 327 ++++++++---------- extensions/memos/src/setup.ts | 86 ++--- pnpm-lock.yaml | 10 + 12 files changed, 341 insertions(+), 282 deletions(-) diff --git a/.changeset/memos-connection-setup.md b/.changeset/memos-connection-setup.md index 4f23174..f9adda4 100644 --- a/.changeset/memos-connection-setup.md +++ b/.changeset/memos-connection-setup.md @@ -2,4 +2,6 @@ '@inkcre/ext-memos': minor --- -新增 Memos 可视化连接准备向导,生成或复用 PAT、启用 Core 并提供客户端连接信息;首次 MF 发行对齐已有 Python 0.2.0。 +简化 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/apps/client-web/src/components/extension/extensionCard/extensionCard.md b/apps/client-web/src/components/extension/extensionCard/extensionCard.md index b33984f..9b1aafa 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 e1ef438..bccd3be 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') }} +

import { computed, inject, onMounted, onUnmounted, ref, shallowRef } from 'vue' import { InkButton, InkDropdown, InkForm, InkInput, INK_I18N_KEY } from '@inkcre/ui-web' -import { ExtensionModel, type InstalledExtension, type Peer } from '@inkcre/core' +import { + ExtensionModel, + PeerOutcomeUnknown, + type InstalledExtension, + type Peer, +} from '@inkcre/core' +import { manageExtensionOnPeer } from '@inkcre/extension-runtime-client-web' import { extensionName, generateToken, - manageMemos, + MemosConnectionError, personalAccessToken, + readHelpUrl, readSetup, - serverUrl, - tokenPattern, + readServerUrl, } from './setup' const emit = defineEmits<{ close: [] }>() @@ -20,41 +26,77 @@ const extension = shallowRef(null) const peers = shallowRef([]) const selected = ref('') const draft = ref('') +const address = ref('') const busy = ref(false) const loaded = ref(false) +const requiresRefresh = ref(false) const reveal = ref(false) const error = ref('') const notice = ref('') +const helpUrl = ref(null) +const helpStatus = ref<'loading' | 'available' | 'missing' | 'unavailable'>('loading') let mounted = true onUnmounted(() => { mounted = false }) const peer = computed(() => peers.value.find((candidate) => candidate.id === selected.value)) -const address = computed(() => (peer.value ? serverUrl(peer.value) : null)) const savedToken = computed(() => (extension.value ? personalAccessToken(extension.value) : null)) const enabled = computed(() => extension.value?.enabled.includes(selected.value) ?? false) const ready = computed(() => loaded.value && address.value && savedToken.value && enabled.value) const options = computed(() => peers.value.map((candidate) => ({ label: candidate.name, value: candidate.id })) ) -const validDraft = computed(() => tokenPattern.test(draft.value)) + +async function loadHelp(version: string) { + helpUrl.value = null + helpStatus.value = 'loading' + try { + const url = await readHelpUrl(version) + if (!mounted || extension.value?.version !== version) return + helpUrl.value = url + helpStatus.value = url ? 'available' : 'missing' + } catch { + if (mounted && extension.value?.version === version) helpStatus.value = 'unavailable' + } +} + +async function loadAddress() { + address.value = '' + if (!peer.value || !savedToken.value || !enabled.value) return + try { + address.value = await readServerUrl(peer.value.id) + } catch (cause) { + error.value = + cause instanceof MemosConnectionError && cause.status === 409 + ? tr( + 'PAT saved and Memos enabled. Set this Core’s Public HTTP Base URL in Clients → Config, then refresh.', + 'PAT 已保存,Memos 已启用。请在 Clients → Config 设置此 Core 的公共 HTTP 基址,再刷新。' + ) + : tr( + 'PAT saved and Memos enabled, but its address is unavailable. Check Core and refresh.', + 'PAT 已保存,Memos 已启用,但暂时无法读取服务地址。请检查 Core 后刷新。' + ) + } +} async function refresh() { if (busy.value) return busy.value = true error.value = '' notice.value = '' + address.value = '' reveal.value = false try { const current = await readSetup() if (!mounted) return extension.value = current.extension + void loadHelp(current.extension.version) peers.value = current.peers - if (!current.peers.some((candidate) => candidate.id === selected.value)) { - selected.value = current.peers.length === 1 ? current.peers[0]!.id : '' - } + if (!selected.value && current.peers.length === 1) selected.value = current.peers[0]!.id loaded.value = true + requiresRefresh.value = false + await loadAddress() } catch { loaded.value = false error.value = tr( @@ -66,63 +108,72 @@ async function refresh() { } } +async function selectPeer() { + reveal.value = false + error.value = '' + notice.value = '' + busy.value = true + try { + await loadAddress() + } finally { + busy.value = false + } +} + async function prepare() { - if (busy.value || !loaded.value || !peer.value || !address.value) return - const target = peer.value - const previousToken = savedToken.value - const token = previousToken ?? draft.value - if (!tokenPattern.test(token)) return + if (busy.value || !loaded.value || !peer.value || requiresRefresh.value) return + const target = peer.value.id busy.value = true error.value = '' notice.value = '' - let phase: 'save' | 'enable' = 'save' + address.value = '' + let phase: 'read' | 'save' | 'enable' = 'read' try { - // Recheck after user deliberation. A newly configured PAT must not be silently replaced. const current = await ExtensionModel.get(extensionName) if (!current) throw new Error('Memos is no longer installed.') extension.value = current - if (personalAccessToken(current) !== previousToken) { - error.value = tr( - 'Configuration changed elsewhere. Review the current PAT before continuing.', - '配置已在其他位置变更。请核对当前 PAT 后继续。' - ) - return - } - if (!previousToken) { - extension.value = await manageMemos(target, { + if (!personalAccessToken(current)) { + phase = 'save' + draft.value ||= generateToken() + extension.value = await manageExtensionOnPeer(target, { action: 'patch_config', extension: extensionName, - patch: { personal_access_token: token }, + patch: { personal_access_token: draft.value }, }) - if (personalAccessToken(extension.value) !== token) throw new Error('PAT readback differs.') } phase = 'enable' - if (!extension.value.enabled.includes(target.id)) { - extension.value = await manageMemos(target, { action: 'enable', extension: extensionName }) + if (!extension.value.enabled.includes(target)) { + extension.value = await manageExtensionOnPeer(target, { + action: 'enable', + extension: extensionName, + }) } draft.value = '' - } catch { - // A lost response is not evidence of failure. Read canonical state before allowing a retry. - try { - const current = await ExtensionModel.get(extensionName) - if (!current) throw new Error('Memos is no longer installed.') - extension.value = current + await loadAddress() + } catch (cause) { + if (cause instanceof PeerOutcomeUnknown) { + requiresRefresh.value = true error.value = phase === 'enable' ? tr( - 'The PAT is saved. Check Core availability, then retry enabling with the same PAT.', - 'PAT 已保存。请检查 Core 是否可用,再用同一枚 PAT 重试启用。' + 'PAT saved. Enablement outcome is unknown; refresh before continuing.', + 'PAT 已保存,启用结果未知;请先刷新再继续。' ) : tr( - 'The save was not confirmed. Current saved state has been refreshed; your draft is retained.', - '保存未获确认。已刷新当前保存状态,并保留输入草稿。' + 'Save outcome is unknown; refresh before continuing.', + '保存结果未知;请先刷新再继续。' + ) + } else { + error.value = + phase === 'enable' + ? tr( + 'PAT saved, but Memos could not be enabled. Check Core, then try again.', + 'PAT 已保存,但 Memos 未能启用。请检查 Core 后重试。' + ) + : tr( + 'Connection preparation failed. Check Core and try again.', + '连接准备失败,请检查 Core 后重试。' ) - } catch { - loaded.value = false - error.value = tr( - 'The result is unknown. Refresh to read saved state before retrying; your draft is retained.', - '结果未知。请先刷新已保存状态再重试;输入草稿已保留。' - ) } } finally { busy.value = false @@ -152,14 +203,10 @@ onMounted(refresh)

{{ - tr( - 'Connect a Memos-compatible app to this InKCre instance. No separate Memos server is needed.', - '将兼容 Memos 的 App 连接到此 InKCre 实例,无需另建 Memos 服务器。' - ) + tr('Connect a Memos-compatible app to this instance.', '将兼容 Memos 的 App 连接到此实例。') }}

- @@ -330,10 +303,4 @@ onMounted(refresh) .memos-setup__error { color: var(--sys-color-feedback-error); } -.memos-setup ol { - padding-inline-start: 1.5rem; -} -.memos-setup li + li { - margin-top: 0.5rem; -} diff --git a/extensions/memos/src/setup.ts b/extensions/memos/src/setup.ts index 1cf8ad7..b3227ff 100644 --- a/extensions/memos/src/setup.ts +++ b/extensions/memos/src/setup.ts @@ -1,15 +1,18 @@ import { ExtensionModel, - InstalledExtensionSchema, + configStore, + ExtensionRegistryOriginResolver, PeerManager, PeerProtocolResponseSchema, type InstalledExtension, - type JsonValue, type Peer, } from '@inkcre/core' +import { + getExtensionDocumentation, + listAdvertisedExtensionManagementPeers, +} from '@inkcre/extension-runtime-client-web' export const extensionName = 'inkcre/memos' -const management = 'core.extension.management.v1' export const tokenPattern = /^memos_pat_[0-9A-Za-z]{32}$/ export function personalAccessToken(extension: InstalledExtension): string | null { @@ -22,50 +25,53 @@ export function generateToken(): string { return `memos_pat_${Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('')}` } -export function serverUrl(peer: Peer): string | null { - const value = peer.config.http_public_base_url - if (typeof value !== 'string' || !value.trim()) return null - try { - const url = new URL(value) - if ( - !['http:', 'https:'].includes(url.protocol) || - url.username || - url.password || - url.search || - url.hash - ) - return null - url.pathname = `${url.pathname.replace(/\/+$/, '')}/memos` - return url.href - } catch { - return null - } -} - export async function readSetup(): Promise<{ extension: ExtensionModel; peers: Peer[] }> { const [extension, peers] = await Promise.all([ ExtensionModel.get(extensionName), - PeerManager.listLive(), + listAdvertisedExtensionManagementPeers(), ]) if (!extension) throw new Error('Memos is not installed.') - return { - extension, - peers: peers.filter((peer) => { - try { - return peer.capabilitySnapshot().some((capability) => capability.id === management) - } catch { - return false - } - }), + return { extension, peers } +} + +export class MemosConnectionError extends Error { + constructor(readonly status: number) { + super(`Memos connection returned HTTP ${status}.`) } } -export async function manageMemos(peer: Peer, body: JsonValue): Promise { - const result = await PeerManager.delegate(management, { body }, peer.id) +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 || response.status >= 300) { - // Validation payloads can echo the PAT. The UI gives action-specific recovery text instead. - throw new Error(`Core returned HTTP ${response.status}.`) - } - return InstalledExtensionSchema.parse(response.body) + 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/pnpm-lock.yaml b/pnpm-lock.yaml index 3bc5a08..03000c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -353,6 +353,9 @@ importers: '@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.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.22.1) '@vueuse/core': specifier: ^14.0.0 version: 14.3.0(vue@3.5.40(typescript@5.9.3)) @@ -7264,6 +7267,13 @@ snapshots: 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.1.0/inkcre-extension-runtime-client-web-0.1.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 From d5949f5971688084869cbd4a63f288f9f30494e9 Mon Sep 17 00:00:00 2001 From: Lan_zhijiang Date: Mon, 21 Sep 2026 18:33:44 +0800 Subject: [PATCH 4/5] =?UTF-8?q?chore(release):=20=E9=85=8D=E5=A5=97=20Pyth?= =?UTF-8?q?on=20SDK=200.3=20=E7=9A=84=E5=8F=8C=E6=B8=A0=E9=81=93=E5=8F=91?= =?UTF-8?q?=E8=A1=8C=E6=84=8F=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/python-sdk03-companion-mf.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/python-sdk03-companion-mf.md diff --git a/.changeset/python-sdk03-companion-mf.md b/.changeset/python-sdk03-companion-mf.md new file mode 100644 index 0000000..6705053 --- /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 兼容范围保持不变。 From c66b5bab6d59e570de121666f6ea52985f0fd9f5 Mon Sep 17 00:00:00 2001 From: Lan_zhijiang Date: Mon, 21 Sep 2026 18:58:43 +0800 Subject: [PATCH 5/5] =?UTF-8?q?chore(runtime):=20=E9=87=87=E7=94=A8?= =?UTF-8?q?=E6=AD=A3=E5=BC=8F=E5=8F=91=E5=B8=83=E7=9A=84=20Web=20Runtime?= =?UTF-8?q?=200.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/client-web/package.json | 2 +- extensions/memos/package.json | 2 +- pnpm-lock.yaml | 20 ++++++++++---------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/client-web/package.json b/apps/client-web/package.json index 8a2a5ad..3f8d2d0 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/extensions/memos/package.json b/extensions/memos/package.json index 2f3a657..62f485a 100644 --- a/extensions/memos/package.json +++ b/extensions/memos/package.json @@ -11,7 +11,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" }, "devDependencies": { "@inkcre/ext-dev-utils": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03000c5..e5ae875 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)) @@ -354,8 +354,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.22.1) + 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)) @@ -1372,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': @@ -7260,14 +7260,14 @@ 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.1.0/inkcre-extension-runtime-client-web-0.1.0.tgz(@inkcre/core@packages+core)(@module-federation/runtime@0.22.1)': + '@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