Skip to content
Draft
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
9 changes: 9 additions & 0 deletions .changeset/capture-reader.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@inkcre/ext-capture': minor
'@inkcre/core': patch
'@inkcre/client-web': patch
---

新增随手收集分发,从共享信息库读取有序原文、原始图片、真实来源与用户附注。展示不依赖原 Mac 或 Python 执行 Peer 在线。

Core 与 Web 可解释随签名应用交付的 swift_bundled 参与身份;版本管理提示转到对应应用,不调用不支持该格式的 Registry 接口。
5 changes: 5 additions & 0 deletions .github/preview/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
"producer": "../../extensions/caldav/package.json",
"artifact": "../../extensions/caldav/dist/client-web"
},
{
"kind": "module_federation",
"producer": "../../extensions/capture/package.json",
"artifact": "../../extensions/capture/dist/client-web"
},
{
"kind": "module_federation",
"producer": "../../extensions/twitter/package.json",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ export function useExtensionInfoDialogs(options: {
docsLoading.value = true
documentation.value = []
docsError.value = ''
if (pin.distribution === 'swift_bundled') {
docsError.value = t('extension.bundledManagedByApp')
docsLoading.value = false
return
}
try {
const origin = await new ExtensionRegistryOriginResolver(
() => configStore.peerConfig.extension_registry_url
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,9 @@ export function useExtensionPeerRuntime(options: {
? 'Python'
: distribution === 'module_federation'
? 'Module Federation'
: t('extension.distributionUnknown')
: distribution === 'swift_bundled'
? 'Swift (bundled)'
: t('extension.distributionUnknown')
}

function supportedFormats(peer: Peer): NativeDistribution[] {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useI18n } from 'vue-i18n'
import { computed, onUnmounted, ref, shallowRef } from 'vue'
import {
configStore,
Expand Down Expand Up @@ -28,6 +29,7 @@ export function useExtensionVersionDialog(options: {
data: ExtensionCardData
runtime: ReturnType<typeof useExtensionPeerRuntime>
}) {
const { t } = useI18n()
const { data, runtime } = options
const selectedPeer = shallowRef<Peer | null>(null)
const versionOpen = ref(false)
Expand Down Expand Up @@ -68,7 +70,14 @@ export function useExtensionVersionDialog(options: {
versionLoading.value = true
versionOptions.value = []
versionError.value = ''
const readPublished = async (format: NativeDistribution): Promise<ReleaseRecord[]> => {
if (distribution === 'swift_bundled') {
versionError.value = t('extension.bundledManagedByApp')
versionLoading.value = false
return
}
const readPublished = async (
format: 'python' | 'module_federation'
): Promise<ReleaseRecord[]> => {
try {
const catalog = await getExtensionRegistry().getExtension(data.extension.name, format)
const releases = sortPublishedReleases(catalog.releases ?? [])
Expand Down Expand Up @@ -98,7 +107,7 @@ export function useExtensionVersionDialog(options: {
: (preferredPublishedRelease(releases)?.version ?? '')
if (!releases.length) {
// On explicit install, show another published native format without implying this Peer supports it.
const other: NativeDistribution = distribution === 'python' ? 'module_federation' : 'python'
const other = distribution === 'python' ? 'module_federation' : 'python'
if (runtime.publishedFormats.value[other] === undefined) await readPublished(other)
}
} catch (error) {
Expand Down
2 changes: 2 additions & 0 deletions apps/client-web/src/extension-peer-control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ export async function selectExtensionPeerVersion(input: {
livePeerIds: ReadonlySet<string>
manager: ExtensionManager
}): Promise<ExtensionParticipation | ExtensionRuntimeStatus> {
if (input.distribution === 'swift_bundled')
throw new Error('此分发随应用交付,请在对应 Peer 管理。')
const mode = extensionPeerControlMode(input.peer, input.currentPeerId, input.livePeerIds)
if (mode === 'current-runtime') {
if (input.distribution !== 'module_federation')
Expand Down
3 changes: 2 additions & 1 deletion apps/client-web/src/locales/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,8 @@
"runtimeUnknown": "Runtime status unavailable",
"unavailableOnPeer": "Unavailable on this Peer",
"publishedDistributions": "Published distributions",
"noPublishedDistribution": "No published {distribution} releases."
"noPublishedDistribution": "No published {distribution} releases.",
"bundledManagedByApp": "This distribution ships with its application. Manage it and consult its documentation in that application, rather than Registry."
},
"job": {
"loadFailed": "Unable to load this job.",
Expand Down
3 changes: 2 additions & 1 deletion apps/client-web/src/locales/messages/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,8 @@
"runtimeUnknown": "运行状态未知",
"unavailableOnPeer": "此 Peer 不可用",
"publishedDistributions": "已发布分发",
"noPublishedDistribution": "尚无已发布的 {distribution} 版本。"
"noPublishedDistribution": "尚无已发布的 {distribution} 版本。",
"bundledManagedByApp": "此分发随应用交付,不通过 Registry 获取版本或文档。请在对应应用中管理。"
},
"job": {
"loadFailed": "无法读取此任务。",
Expand Down
3 changes: 1 addition & 2 deletions apps/client-web/src/views/extensions/extensions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
PeerManager,
ExtensionParticipationModel,
type ExtensionParticipation,
type NativeDistribution,
type InstalledExtension,
} from '@inkcre/core'
import type { ReleaseRecord } from '@inkcre/extension-runtime-client-web'
Expand All @@ -37,7 +36,7 @@ const installPeers = ref<Peer[]>([])
const installPeerOptions = computed(() =>
installPeers.value.map((peer) => ({ label: peer.name, value: peer.id }))
)
const installDistribution = computed<NativeDistribution>(() =>
const installDistribution = computed<'python' | 'module_federation'>(() =>
route.query.distribution === 'python' ? 'python' : 'module_federation'
)
const peersLoading = ref(false)
Expand Down
21 changes: 21 additions & 0 deletions extensions/capture/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 InKCre contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
11 changes: 11 additions & 0 deletions extensions/capture/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Capture 读取分发

该原生 Module Federation 分发解释 `extensions.capture.capture.v1` 与 `extensions.capture.attachment.v1`,复用 Core 的文本、图片、存储和内容导航。它显示有序原材料、真实出处与单独的用户附注,不执行提交或依赖采集端在线。

Host SDK 要求 `@inkcre/core >=0.4.0 <0.5.0`。通过 Registry 选择并启用本浏览器的 `module_federation` 分发;`initialize` 注册 Resolver,`dispose` 只移除本分发拥有的类。Swift、Python 与 Web 分发按各自能力对等参与同一 Extension。

共享图与提交语义由 [Capture Extension 合同](https://github.com/InKCre/docs/blob/main/20-product-tdd/capture-contract.md) 维护;字段验证在 `src/schema.ts`,顺序和引用校验在 `src/resolver.ts`。缺失材料显示错误,不触发采集端重新抓取。来源 URL 只允许 HTTP(S) 作为可点击链接。

运行 `pnpm --filter @inkcre/ext-capture type-check` 与仓库 `pnpm build`。`dist/client-web` 是可重定位的发布制品,`src/main.ts` 只用于开发说明页。真实验收应安装生产构建,停止原采集者后冷启动打开共享根 Block,检查原文、图片字节、顺序和附注。

Mac 自行通过共享数据库原子写入,不依赖 Python Peer。此 Web 分发只负责读取与呈现,未启用它不影响采集写入。
12 changes: 12 additions & 0 deletions extensions/capture/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>随手收集分发</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
46 changes: 46 additions & 0 deletions extensions/capture/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"name": "@inkcre/ext-capture",
"version": "0.1.0",
"private": true,
"license": "MIT",
"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.2.1",
"@module-federation/vite": "^1.1.0",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/tsconfig": "catalog:",
"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/capture",
"nickname": "随手收集",
"module_federation": {
"host_sdk": "@inkcre/core",
"host_sdk_version": ">=0.4.0 <0.5.0"
}
},
"intent": {
"skills": [
"@inkcre/ui-web"
]
}
}
28 changes: 28 additions & 0 deletions extensions/capture/src/AttachmentContent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script setup lang="ts">
import type { SolvedContentRendererProps } from '@inkcre/core'
import type { AttachmentResolver } from './resolver'
import type { SolvedAttachment } from './schema'
import MaterialContent from './MaterialContent.vue'

defineProps<SolvedContentRendererProps<SolvedAttachment, AttachmentResolver>>()
</script>

<template>
<figure>
<MaterialContent :material="solvedContent.content" />
<figcaption>
{{ solvedContent.metadata.filename || '原始图片' }} · {{ solvedContent.metadata.mime_type }}
</figcaption>
</figure>
</template>

<style scoped>
figure {
margin: 0;
min-width: 0;
}
figcaption {
margin-top: var(--sys-space-xs);
color: var(--sys-color-text-subtle);
}
</style>
12 changes: 12 additions & 0 deletions extensions/capture/src/AttachmentPreview.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script setup lang="ts">
import type { SolvedContentRendererProps } from '@inkcre/core'
import type { AttachmentResolver } from './resolver'
import type { SolvedAttachment } from './schema'
defineProps<SolvedContentRendererProps<SolvedAttachment, AttachmentResolver>>()
</script>
<template>
<span
>{{ solvedContent.metadata.filename || '原始图片' }} ·
{{ solvedContent.metadata.mime_type }}</span
>
</template>
69 changes: 69 additions & 0 deletions extensions/capture/src/CaptureContent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { SolvedContentRendererProps } from '@inkcre/core'
import type { CaptureResolver } from './resolver'
import { safeSourceURL, type SolvedCapture } from './schema'
import MaterialContent from './MaterialContent.vue'

const props = defineProps<SolvedContentRendererProps<SolvedCapture, CaptureResolver>>()
const sourceURL = computed(() => safeSourceURL(props.solvedContent.metadata.origin?.url))
const entries = { selection: '选区', paste: '明确粘贴', screenshot: '框选截图', manual: '想法' }
</script>

<template>
<article class="capture-content">
<header>
<strong>{{ solvedContent.metadata.origin?.title || '随手收集' }}</strong>
<p>
{{ entries[solvedContent.metadata.entry] }} ·
<time :datetime="solvedContent.metadata.captured_at">
{{ new Date(solvedContent.metadata.captured_at).toLocaleString() }}
</time>
</p>
<p v-if="solvedContent.metadata.origin?.application_id">
{{ solvedContent.metadata.origin.application_id }}
</p>
<a v-if="sourceURL" :href="sourceURL" target="_blank" rel="noopener noreferrer">{{
sourceURL
}}</a>
<p v-else-if="solvedContent.metadata.origin?.url">{{ solvedContent.metadata.origin.url }}</p>
</header>
<section aria-label="原始材料" class="materials">
<MaterialContent
v-for="material in solvedContent.materials"
:key="material.block.id"
:material="material"
/>
</section>
<section v-if="solvedContent.annotation" class="annotation" aria-label="补充想法">
<h3>补充想法</h3>
<MaterialContent :material="solvedContent.annotation" />
</section>
</article>
</template>

<style scoped>
.capture-content,
.materials {
display: grid;
gap: var(--sys-space-lg);
min-width: 0;
overflow-wrap: anywhere;
}
.capture-content {
color: var(--sys-color-text-base);
}
header p {
margin: var(--sys-space-xs) 0;
color: var(--sys-color-text-subtle);
}
.annotation {
border-top: 1px solid var(--sys-color-border-base);
padding-top: var(--sys-space-md);
}
h3 {
font: inherit;
font-weight: 600;
margin: 0 0 var(--sys-space-sm);
}
</style>
49 changes: 49 additions & 0 deletions extensions/capture/src/CapturePreview.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { SolvedContentRendererProps } from '@inkcre/core'
import type { CaptureResolver } from './resolver'
import type { SolvedCapture } from './schema'

const props = defineProps<SolvedContentRendererProps<SolvedCapture, CaptureResolver>>()
const text = computed(
() =>
props.solvedContent.materials.find((item) => item.block.resolver === 'core.text.v1')
?.solvedContent
)
const images = computed(
() =>
props.solvedContent.materials.filter(
(item) => item.block.resolver === 'extensions.capture.attachment.v1'
).length
)
</script>

<template>
<article class="capture-preview">
<strong>{{ solvedContent.metadata.origin?.title || '随手收集' }}</strong>
<p v-if="typeof text === 'string'">{{ text }}</p>
<span v-if="images">{{ images }} 张原图</span>
<span v-if="solvedContent.annotation">含补充想法</span>
</article>
</template>

<style scoped>
.capture-preview {
display: grid;
gap: var(--sys-space-xs);
min-width: 0;
max-width: 360px;
}
p {
margin: 0;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
span {
color: var(--sys-color-text-subtle);
}
</style>
14 changes: 14 additions & 0 deletions extensions/capture/src/DevApp.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<template>
<main>
<h1>随手收集</h1>
<p>在连接同一信息库的 Web Peer 中启用此分发,即可读取 Mac 收入的原文、图片与附注。</p>
</main>
</template>

<style scoped>
main {
max-width: 42rem;
margin: var(--sys-space-xl);
color: var(--sys-color-text-base);
}
</style>
Loading
Loading