Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/memos-connection-setup.md
Original file line number Diff line number Diff line change
@@ -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 对齐。
6 changes: 6 additions & 0 deletions .changeset/python-sdk03-companion-mf.md
Original file line number Diff line number Diff line change
@@ -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 兼容范围保持不变。
2 changes: 1 addition & 1 deletion apps/client-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -29,12 +34,38 @@ const isUninstalling = ref(false)
const operationError = ref<string | null>(null)
const configModel = ref(JSON.stringify(props.extension.config, null, 2))
const versionModel = ref(props.extension.version)
const documentation = shallowRef<ExtensionDocumentationLink[]>([])
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) => {
Expand Down Expand Up @@ -184,6 +215,24 @@ const onUninstall = async () => {
{{ t('extension.uninstallDisabled') }}
</p>
<p v-if="operationError" class="extension-card__error">{{ operationError }}</p>
<nav
v-if="documentation.length"
class="extension-card__actions"
:aria-label="t('extension.documentation')"
>
<a
v-for="link in documentation"
:key="link.scope"
:href="link.entry_url"
target="_blank"
rel="noopener noreferrer"
>
{{ t(`extension.documentationScope.${link.scope}`) }}
</a>
</nav>
<p v-else-if="documentationStatus === 'unavailable'" class="extension-card__hint">
{{ t('extension.documentationUnavailable') }}
</p>

<InkDialog
v-model="setupPopupOpen"
Expand Down
54 changes: 15 additions & 39 deletions apps/client-web/src/extension-peer-control.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import {
InstalledExtensionSchema,
InstallExtensionInputSchema,
ExtensionModel,
Peer,
PeerManager,
PeerProtocolResponseSchema,
type InstalledExtension,
type InstallExtensionInput,
} from '@inkcre/core'
import type { ExtensionManager } from '@inkcre/extension-runtime-client-web'
import {
EXTENSION_MANAGEMENT_CAPABILITY,
manageExtensionOnPeer,
type ExtensionManager,
} from '@inkcre/extension-runtime-client-web'

export const EXTENSION_MANAGEMENT_CAPABILITY = 'core.extension.management.v1'
export { EXTENSION_MANAGEMENT_CAPABILITY }
export type ExtensionPeerControlMode = 'current-runtime' | 'remote-host' | 'desired-state'

export function peerAdvertises(peer: Peer, capability: string): boolean {
Expand Down Expand Up @@ -53,21 +54,10 @@ export async function setExtensionPeerEnabled(input: {
: extension.disablePeer(input.peer.id)
}

const delegated = await PeerManager.delegate(
EXTENSION_MANAGEMENT_CAPABILITY,
{
body: {
action: input.enabled ? 'enable' : 'disable',
extension: input.name,
},
},
input.peer.id
)
const response = PeerProtocolResponseSchema.parse(delegated)
if (response.status !== 200 || response.body === undefined) {
throw new Error(`Extension management Peer returned HTTP ${response.status} for ${input.name}.`)
}
return InstalledExtensionSchema.parse(response.body)
return manageExtensionOnPeer(input.peer.id, {
action: input.enabled ? 'enable' : 'disable',
extension: input.name,
})
}

/** Validate with the selected Host; a Python-only Release need not run in the browser. */
Expand Down Expand Up @@ -98,23 +88,9 @@ export async function installExtensionForPeer(input: {
)
}
}
const delegated = await PeerManager.delegate(
EXTENSION_MANAGEMENT_CAPABILITY,
{ body: { action: 'install', extension: coordinate.name, version: coordinate.version } },
input.peer.id
)
const response = PeerProtocolResponseSchema.parse(delegated)
if (response.status !== 200 || response.body === undefined) {
const detail =
response.body !== null &&
typeof response.body === 'object' &&
'detail' in response.body &&
typeof response.body.detail === 'string'
? response.body.detail
: 'Check that its Core version supports installation through Extension management.'
throw new Error(
`Installation on the selected Client returned HTTP ${response.status}. ${detail}`
)
}
return InstalledExtensionSchema.parse(response.body)
return manageExtensionOnPeer(input.peer.id, {
action: 'install',
extension: coordinate.name,
version: coordinate.version,
})
}
7 changes: 7 additions & 0 deletions apps/client-web/src/locales/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,13 @@
"typeUnavailable": "This source type is unavailable. Check installed extensions or choose an available type."
},
"extension": {
"documentation": "Documentation",
"documentationScope": {
"global": "Extension guide",
"python": "Core guide",
"module-federation": "Web guide"
},
"documentationUnavailable": "Documentation is temporarily unavailable.",
"installNew": "Install Extension",
"installExtensionTitle": "Install New Extension",
"name": "Extension Name",
Expand Down
7 changes: 7 additions & 0 deletions apps/client-web/src/locales/messages/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@
"typeUnavailable": "当前数据源类型不可用,请检查已安装的扩展或选择可用类型。"
},
"extension": {
"documentation": "扩展文档",
"documentationScope": {
"global": "扩展指南",
"python": "Core 指南",
"module-federation": "Web 指南"
},
"documentationUnavailable": "文档暂时不可用。",
"installNew": "安装扩展",
"installExtensionTitle": "安装新扩展",
"name": "扩展名称",
Expand Down
46 changes: 36 additions & 10 deletions docs/30-unit-tdd/native-extension-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Boundary

The native Extension Runtime spans the browser Web Extension Host, its durable state port, and
The native Extension Runtime spans the browser Web Extension Host, its SDK-owned durable models, and
independently versioned Module Federation producers. It owns compatibility preflight, executable
loading, runtime lifecycle, and reconciliation with durable enabled intent. Release publication,
Registry delivery, and deployment workflow remain outside this Unit TDD; cross-unit Extension and
Expand All @@ -13,8 +13,10 @@ artifact delivery belong to
[First-party Extension Delivery](../40-deployment/native-extension-delivery.md).

There is one native format: an Extension Release may associate a Module Federation distribution.
The Host consumes that association directly. There is no shared Extension Runtime/API package,
generic target matcher, or canonical cross-format artifact manifest.
The Host consumes that association directly. `@inkcre/extension-runtime-client-web`, maintained in
ext-reg, owns the Web Host and Registry consumers. `@inkcre/core` owns the shared models, Peer
transport, and browser configuration. There is no generic target matcher or cross-format artifact
manifest.

## Producer and Host Contract

Expand Down Expand Up @@ -48,10 +50,17 @@ passes directly to the current native Module Federation implementation.

## Durable State and Peer Intent

`ExtensionStatePort` is the semantic boundary for listing, reading, installing, changing version,
updating configuration, setting per-Peer enabled intent, and uninstalling. It deliberately hides
SQL, PostgREST routes, and generated relation types. The PostgREST adapter is one implementation,
not the Host contract.
The Runtime uses `ExtensionModel` from the shared `@inkcre/core` instance for installed state,
configuration, and per-Peer enabled intent. There is no application-owned `ExtensionStatePort` or
second PostgREST adapter. The application keeps the selected-Peer policy: current-runtime operations
use its local Host, online remote Hosts receive management commands, and offline enablement changes
update desired state through the SDK model.

`listAdvertisedExtensionManagementPeers` and `manageExtensionOnPeer` in the Runtime own the shared
management capability contract. The latter delegates once to the exact Peer through the SDK's
`PeerManager`, validates the installed-row response, and preserves unknown outcomes. Consumers do
not retry mutations or choose a replacement Peer automatically. Response bodies are not included
in management errors because configuration validation may echo credentials.

Installed state records exact name and version plus the set of enabled Peer IDs. The browser view
selects the Host that validates installation or version changes. The current browser uses its
Expand All @@ -64,8 +73,25 @@ is an exact delegated capability, not a generic Core API call.

Version change and uninstall are refused while any Peer remains enabled or a local runtime is
running. Startup reads canonical installed state and starts only entries enabled for the current
Peer. Shutdown stops volatile runtimes without changing durable enabled intent. Host operations are
serialized so concurrent UI commands cannot interleave lifecycle and persistence transitions.
Peer. Shutdown stops volatile runtimes without changing durable enabled intent.

## Documentation and Memos Connection

Extension cards discover documentation independently of executable preflight or enablement.
They resolve the existing Registry origin and read documentation for the exact installed name and
version through the Runtime's `getExtensionDocumentation`. Only returned global, python, and
module-federation links appear. Missing documents and temporarily unavailable discovery are
different states; neither prevents Extension operations. Links open without credentials or an
opener, and the application does not fetch or embed document bodies.

The Memos setup contribution owns connection preparation. An explicit action reuses a saved PAT or
generates one with browser cryptographic randomness, saves it through `patch_config`, and enables
the selected Core only if it is not already enabled. Online plus enabled is the normal best-effort
runtime assumption. The contribution obtains the complete server URL from `memos.connection.v1`;
it does not read Peer configuration or construct a Memos route. Address-read failures preserve the
saved token and enabled state. Opening, selecting, and refreshing only read state. Unknown mutation
outcomes require refreshing before another attempt. The PAT is masked by default; a normal help
link opens the exact MF release's connection guide.

## Lifecycle and Compensation

Expand Down Expand Up @@ -93,7 +119,7 @@ for inspection, and reports an aggregate after attempting the remaining entries.

## Invariants

- Canonical installed/enabled state is accessed only through `ExtensionStatePort`.
- Canonical installed/enabled state is accessed through the shared SDK's `ExtensionModel`.
- Compatibility is proven before native executable fetch.
- The exact Release and native manifest association remain stable through one start attempt.
- Durable enabled intent and volatile runtime are reconciled with explicit compensation.
Expand Down
2 changes: 1 addition & 1 deletion docs/_shared
Submodule _shared updated 39 files
+37 −0 .github/workflows/pages-preview.yml
+20 −0 00-meta/submodule-profile.md
+8 −0 20-product-tdd/cross-unit-contracts.md
+103 −0 20-product-tdd/extension-documentation-hosting.md
+51 −0 website/.vitepress/config.mts
+54 −0 website/.vitepress/theme/InterfaceGuide.vue
+10 −1 website/.vitepress/theme/index.ts
+26 −6 website/README.md
+4 −2 website/content/en/developer/architecture.md
+4 −0 website/content/en/developer/contributing.md
+45 −0 website/content/en/developer/ecosystem/index.md
+286 −0 website/content/en/developer/ecosystem/source-extension.md
+8 −4 website/content/en/developer/index.md
+84 −0 website/content/en/getting-started.md
+67 −0 website/content/en/guide/collect.md
+63 −0 website/content/en/guide/connect-cli.md
+52 −0 website/content/en/guide/connect.md
+62 −0 website/content/en/guide/daily-use.md
+67 −0 website/content/en/guide/extensions.md
+39 −0 website/content/en/guide/first-source.md
+87 −0 website/content/en/guide/schedules.md
+82 −0 website/content/en/guide/search.md
+196 −0 website/content/en/guide/sinks/chatgpt.md
+47 −0 website/content/en/guide/sources.md
+79 −0 website/content/en/guide/sources/github.md
+118 −0 website/content/en/guide/sources/mail.md
+108 −0 website/content/en/guide/sources/memos.md
+104 −0 website/content/en/guide/sources/rss.md
+101 −0 website/content/en/guide/sources/telegram.md
+221 −0 website/content/en/guide/sources/twitter.md
+25 −0 website/content/en/guide/troubleshooting.md
+7 −3 website/content/en/index.md
+80 −0 website/content/en/self-hosted/advanced.md
+51 −0 website/content/en/self-hosted/getting-started.md
+56 −0 website/content/en/self-hosted/heroku-neon.md
+36 −0 website/content/en/self-hosted/index.md
+58 −0 website/content/en/self-hosted/render-neon.md
+37 −0 website/extension-docs/README.md
+56 −0 website/extension-docs/preset.mts
Original file line number Diff line number Diff line change
@@ -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: '/' }],
})
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '../../../../../../docs/_shared/website/.vitepress/theme/index'
39 changes: 39 additions & 0 deletions extensions/memos/docs/module-federation/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
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. Select **Prepare connection**. The browser generates a PAT if none is saved, otherwise reuses
the saved PAT. It saves through Core and enables Memos only when necessary.
4. Copy **Server URL** and **Personal Access Token** into your compatible client's sign-in screen.
Use the complete URL as returned by Memos; do not append `/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, enabling, and reading the address are separate operations. A failed address read does not
undo a saved PAT or disable Memos. If Core has no public address, set its Public HTTP Base URL in
**Clients → Config**, then select **Refresh status**. The address must be reachable from your
client device and may include a deployment path prefix.

If enablement fails after saving, retry with the saved PAT. An uncertain save or enable response
requires **Refresh status** before continuing; the page never automatically repeats that request
or switches to another Core. An online, enabled Core is assumed to provide the service until a
concrete failure is observed. 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.

The **Memos connection help** link opens this guide for the installed version. Extension cards also
link any global, Core, or Web documentation provided by that exact release, including when the
extension is disabled. Missing or temporarily unavailable documentation does not prevent setup.
12 changes: 12 additions & 0 deletions extensions/memos/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Memos Extension</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
Loading
Loading