From 53262240207e11e34e8c610918ad993c9b5e6f67 Mon Sep 17 00:00:00 2001 From: Lan_zhijiang Date: Wed, 21 Oct 2026 17:38:25 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(capture):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=85=B1=E4=BA=AB=E9=87=87=E9=9B=86=E5=86=85=E5=AE=B9=E7=9A=84?= =?UTF-8?q?=20Web=20=E8=AF=BB=E5=8F=96=E5=88=86=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 解释有序原文、原始图片、真实来源和独立附注 - 复用 Core Resolver、存储和内容导航,支持采集者停机后冷读 - 接入独立构建、包契约、Changeset 和隔离预览清单 --- .changeset/capture-reader.md | 5 + .github/preview/extensions.json | 5 + extensions/capture/README.md | 9 ++ extensions/capture/index.html | 12 +++ extensions/capture/package.json | 45 ++++++++ extensions/capture/src/AttachmentContent.vue | 28 +++++ extensions/capture/src/AttachmentPreview.vue | 12 +++ extensions/capture/src/CaptureContent.vue | 69 ++++++++++++ extensions/capture/src/CapturePreview.vue | 49 +++++++++ extensions/capture/src/DevApp.vue | 14 +++ extensions/capture/src/MaterialContent.vue | 18 ++++ extensions/capture/src/index.ts | 13 +++ extensions/capture/src/main.ts | 5 + extensions/capture/src/resolver.ts | 107 +++++++++++++++++++ extensions/capture/src/schema.ts | 44 ++++++++ extensions/capture/tsconfig.json | 14 +++ extensions/capture/vite.config.ts | 21 ++++ pnpm-lock.yaml | 68 ++++++++++++ scripts/check-package-contract.mjs | 1 + scripts/check-workspace-contract.mjs | 6 ++ 20 files changed, 545 insertions(+) create mode 100644 .changeset/capture-reader.md create mode 100644 extensions/capture/README.md create mode 100644 extensions/capture/index.html create mode 100644 extensions/capture/package.json create mode 100644 extensions/capture/src/AttachmentContent.vue create mode 100644 extensions/capture/src/AttachmentPreview.vue create mode 100644 extensions/capture/src/CaptureContent.vue create mode 100644 extensions/capture/src/CapturePreview.vue create mode 100644 extensions/capture/src/DevApp.vue create mode 100644 extensions/capture/src/MaterialContent.vue create mode 100644 extensions/capture/src/index.ts create mode 100644 extensions/capture/src/main.ts create mode 100644 extensions/capture/src/resolver.ts create mode 100644 extensions/capture/src/schema.ts create mode 100644 extensions/capture/tsconfig.json create mode 100644 extensions/capture/vite.config.ts diff --git a/.changeset/capture-reader.md b/.changeset/capture-reader.md new file mode 100644 index 00000000..55260a38 --- /dev/null +++ b/.changeset/capture-reader.md @@ -0,0 +1,5 @@ +--- +'@inkcre/ext-capture': minor +--- + +新增随手收集分发,从共享信息库读取有序原文、原始图片、真实来源与用户附注。展示不依赖原 Mac 或 Python 执行 Peer 在线。 diff --git a/.github/preview/extensions.json b/.github/preview/extensions.json index 8ff23808..cadcc92d 100644 --- a/.github/preview/extensions.json +++ b/.github/preview/extensions.json @@ -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", diff --git a/extensions/capture/README.md b/extensions/capture/README.md new file mode 100644 index 00000000..913da96f --- /dev/null +++ b/extensions/capture/README.md @@ -0,0 +1,9 @@ +# 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` 只移除本分发拥有的类。Python 执行分发与本地读取分发分别参与生命周期。 + +业务图及请求合同由 `InKCre/impl-py/docs/30-unit-tdd/capture-extension.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,检查原文、图片字节、顺序和附注。 diff --git a/extensions/capture/index.html b/extensions/capture/index.html new file mode 100644 index 00000000..ec8a1181 --- /dev/null +++ b/extensions/capture/index.html @@ -0,0 +1,12 @@ + + + + + + 随手收集分发 + + +
+ + + diff --git a/extensions/capture/package.json b/extensions/capture/package.json new file mode 100644 index 00000000..c5ca81bc --- /dev/null +++ b/extensions/capture/package.json @@ -0,0 +1,45 @@ +{ + "name": "@inkcre/ext-capture", + "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.2.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/capture", + "nickname": "随手收集", + "module_federation": { + "host_sdk": "@inkcre/core", + "host_sdk_version": ">=0.4.0 <0.5.0" + } + }, + "intent": { + "skills": [ + "@inkcre/ui-web" + ] + } +} diff --git a/extensions/capture/src/AttachmentContent.vue b/extensions/capture/src/AttachmentContent.vue new file mode 100644 index 00000000..ae2e9ed6 --- /dev/null +++ b/extensions/capture/src/AttachmentContent.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/extensions/capture/src/AttachmentPreview.vue b/extensions/capture/src/AttachmentPreview.vue new file mode 100644 index 00000000..b1d18477 --- /dev/null +++ b/extensions/capture/src/AttachmentPreview.vue @@ -0,0 +1,12 @@ + + diff --git a/extensions/capture/src/CaptureContent.vue b/extensions/capture/src/CaptureContent.vue new file mode 100644 index 00000000..02c8a12f --- /dev/null +++ b/extensions/capture/src/CaptureContent.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/extensions/capture/src/CapturePreview.vue b/extensions/capture/src/CapturePreview.vue new file mode 100644 index 00000000..9086796f --- /dev/null +++ b/extensions/capture/src/CapturePreview.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/extensions/capture/src/DevApp.vue b/extensions/capture/src/DevApp.vue new file mode 100644 index 00000000..ca963970 --- /dev/null +++ b/extensions/capture/src/DevApp.vue @@ -0,0 +1,14 @@ + + + diff --git a/extensions/capture/src/MaterialContent.vue b/extensions/capture/src/MaterialContent.vue new file mode 100644 index 00000000..dba8e312 --- /dev/null +++ b/extensions/capture/src/MaterialContent.vue @@ -0,0 +1,18 @@ + + + diff --git a/extensions/capture/src/index.ts b/extensions/capture/src/index.ts new file mode 100644 index 00000000..a7aef51c --- /dev/null +++ b/extensions/capture/src/index.ts @@ -0,0 +1,13 @@ +import { Resolver, type ExtensionModule } from '@inkcre/core' +import { CaptureResolver, AttachmentResolver } from './resolver' + +const resolvers = [CaptureResolver, AttachmentResolver] +const extension: ExtensionModule = { + async initialize() { + for (const resolver of resolvers) Resolver.register(resolver.type, resolver) + }, + async dispose() { + for (const resolver of resolvers) Resolver.unregister(resolver.type, resolver) + }, +} +export default extension diff --git a/extensions/capture/src/main.ts b/extensions/capture/src/main.ts new file mode 100644 index 00000000..c92ced8d --- /dev/null +++ b/extensions/capture/src/main.ts @@ -0,0 +1,5 @@ +import { createApp } from 'vue' +import '@inkcre/ui-web/styles' +import DevApp from './DevApp.vue' + +createApp(DevApp).mount('#app') diff --git a/extensions/capture/src/resolver.ts b/extensions/capture/src/resolver.ts new file mode 100644 index 00000000..8754e01a --- /dev/null +++ b/extensions/capture/src/resolver.ts @@ -0,0 +1,107 @@ +import { markRaw } from 'vue' +import { Block, Resolver, ResolverCache, type ProjectionOptions } from '@inkcre/core' +import CaptureContent from './CaptureContent.vue' +import CapturePreview from './CapturePreview.vue' +import AttachmentContent from './AttachmentContent.vue' +import AttachmentPreview from './AttachmentPreview.vue' +import { + captureMetadata, + attachmentMetadata, + type SolvedCapture, + type SolvedAttachment, + type SolvedMaterial, +} from './schema' + +async function solve( + id: number, + allowed: string[], + options: ProjectionOptions +): Promise { + const block = await Block.find(id) + if (!block) throw new Error(`采集材料 Block ${id} 已不存在。`) + if (!allowed.includes(block.resolver)) throw new Error(`采集材料 Block ${id} 的格式不符合合同。`) + const resolver = await ResolverCache.getResolver(block) + return { + block, + resolver, + solvedContent: await resolver.getSolvedContent({ ...options, materializeMissing: false }), + } +} + +export class CaptureResolver extends Resolver { + static readonly type = 'extensions.capture.capture.v1' + static readonly previewRenderer = markRaw(CapturePreview) + static readonly solvedContentRenderer = markRaw(CaptureContent) + + protected override async _getSolvedContent(options: ProjectionOptions): Promise { + const metadata = captureMetadata.parse(JSON.parse(await this.getRawContent(options))) + const outgoing = await this.getRelations({ + includeIn: false, + includeOut: true, + refresh: options.refresh, + }) + const materials = outgoing.filter((relation) => relation.content.startsWith('material:')) + if (materials.some((relation) => !/^material:(0|[1-9]\d*)$/.test(relation.content))) { + throw new Error('采集材料顺序格式不正确。') + } + materials.sort((left, right) => Number(left.content.slice(9)) - Number(right.content.slice(9))) + if ( + materials.length > 128 || + materials.some((relation, index) => relation.content !== `material:${index}`) + ) { + throw new Error('采集材料顺序不连续或重复。') + } + const annotations = outgoing.filter((relation) => relation.content === 'annotation') + if (annotations.length > 1) throw new Error('采集内容存在重复附注。') + const result: SolvedCapture = { metadata, materials: [], annotation: null } + for (const relation of materials) { + result.materials.push( + await solve(relation.to_, ['core.text.v1', AttachmentResolver.type], options) + ) + } + if (annotations[0]) + result.annotation = await solve(annotations[0].to_, ['core.text.v1'], options) + return result + } + + override async getText(options: ProjectionOptions = {}): Promise { + const content = await this.getSolvedContent(options) + const origin = content.metadata.origin + const parts = [`采集时间:${content.metadata.captured_at}`, `入口:${content.metadata.entry}`] + if (origin?.title) parts.push(`来源标题:${origin.title}`) + if (origin?.url) parts.push(`来源位置:${origin.url}`) + if (origin?.application_id) parts.push(`来源应用:${origin.application_id}`) + for (const material of content.materials) { + const text = await material.resolver.getText(options) + if (text !== null) parts.push(`原材料:\n${text}`) + } + if (content.annotation) + parts.push(`用户附注:\n${await content.annotation.resolver.getText(options)}`) + return parts.join('\n\n') + } +} + +export class AttachmentResolver extends Resolver { + static readonly type = 'extensions.capture.attachment.v1' + static readonly previewRenderer = markRaw(AttachmentPreview) + static readonly solvedContentRenderer = markRaw(AttachmentContent) + + protected override async _getSolvedContent( + options: ProjectionOptions + ): Promise { + const metadata = attachmentMetadata.parse(JSON.parse(await this.getRawContent(options))) + const relations = await this.getRelations({ + includeIn: false, + includeOut: true, + refresh: options.refresh, + }) + const content = relations.filter((relation) => relation.content === 'content') + if (content.length !== 1 || !content[0]) throw new Error('图片附件必须引用一份原始内容。') + return { metadata, content: await solve(content[0].to_, ['core.image.v1'], options) } + } + + override async getText(options: ProjectionOptions = {}): Promise { + const content = await this.getSolvedContent(options) + return content.metadata.filename ?? `图片(${content.metadata.mime_type})` + } +} diff --git a/extensions/capture/src/schema.ts b/extensions/capture/src/schema.ts new file mode 100644 index 00000000..33ceb0f3 --- /dev/null +++ b/extensions/capture/src/schema.ts @@ -0,0 +1,44 @@ +import { z } from 'zod' +import type { Block, Resolver } from '@inkcre/core' + +export const captureMetadata = z.object({ + captured_at: z.iso.datetime({ offset: true }), + entry: z.enum(['selection', 'paste', 'screenshot', 'manual']), + origin: z + .object({ + application_id: z.string().nullable().optional(), + title: z.string().nullable().optional(), + url: z.string().nullable().optional(), + }) + .nullable() + .optional(), +}) +export const attachmentMetadata = z.object({ + mime_type: z.enum(['image/png', 'image/jpeg', 'image/gif', 'image/webp']), + filename: z.string().nullable().optional(), +}) + +export interface SolvedMaterial { + block: Block + resolver: Resolver + solvedContent: unknown +} +export interface SolvedCapture { + metadata: z.infer + materials: SolvedMaterial[] + annotation: SolvedMaterial | null +} +export interface SolvedAttachment { + metadata: z.infer + content: SolvedMaterial +} + +export function safeSourceURL(value: string | null | undefined): string | null { + if (!value) return null + try { + const url = new URL(value) + return ['https:', 'http:'].includes(url.protocol) ? url.href : null + } catch { + return null + } +} diff --git a/extensions/capture/tsconfig.json b/extensions/capture/tsconfig.json new file mode 100644 index 00000000..dc91eb8d --- /dev/null +++ b/extensions/capture/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/capture/vite.config.ts b/extensions/capture/vite.config.ts new file mode 100644 index 00000000..9bdaecd3 --- /dev/null +++ b/extensions/capture/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.capture', + 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 448ac0bf..77cd7598 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -345,6 +345,52 @@ importers: specifier: 'catalog:' version: 3.3.8(typescript@5.9.3) + extensions/capture: + dependencies: + '@inkcre/core': + specifier: workspace:* + version: link:../../packages/core + '@vueuse/core': + specifier: ^14.0.0 + version: 14.4.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.2.1 + version: 2.2.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/github: dependencies: '@inkcre/core': @@ -1510,6 +1556,12 @@ packages: '@inkcre/core': '>=0.4.0 <0.5.0' '@module-federation/runtime': '>=0.21.4 <0.23.0' + '@inkcre/ui-web@2.2.1': + resolution: {integrity: sha512-rsljLVVzpzCRhywF0poVBFl21H48JAQwENwci+efL5/YOLjav9Uv7yh8oMN4eqmOiQw9Hgt4Tl6Epg2Rzb/g+w==, tarball: https://npm.pkg.github.com/download/@inkcre/ui-web/2.2.1/5b22fbb31e4bf02e4f742bb0f9353cacfb4b8fd9} + peerDependencies: + unocss: ^66.5.0 + vue: ^3.5.25 + '@inkcre/ui-web@2.3.0': resolution: {integrity: sha512-fXfAro12+zzyhtjiZqfQBvyMJKVnclrm/qvHst70dnRFZekYIkE+rSfabEJ//avAvvJyVZL7kK6XewXK4PgdJg==, tarball: https://npm.pkg.github.com/download/@inkcre/ui-web/2.3.0/2c81dee78ac6797018f0e62a2a988747cd3c7a3e} peerDependencies: @@ -7538,6 +7590,22 @@ snapshots: semver: 7.8.5 zod: 4.1.12 + '@inkcre/ui-web@2.2.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 + '@codemirror/commands': 6.11.1 + '@codemirror/lang-json': 6.0.2 + '@codemirror/lint': 6.9.7 + '@codemirror/state': 6.7.5 + '@codemirror/view': 6.43.12 + '@vue/shared': 3.5.25 + '@vueuse/core': 14.4.0(vue@3.5.40(typescript@5.9.3)) + dayjs: 1.11.23 + 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)) + vscode-json-languageservice: 5.6.4 + vscode-languageserver-textdocument: 1.0.14 + vue: 3.5.40(typescript@5.9.3) + '@inkcre/ui-web@2.3.0(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 278d46fc..6c6d4288 100644 --- a/scripts/check-package-contract.mjs +++ b/scripts/check-package-contract.mjs @@ -37,6 +37,7 @@ await Promise.all( 'apps/client-webext/.output/chrome-mv3/content-scripts/content.css', 'extensions/github/dist/client-web/remoteEntry.js', 'extensions/caldav/dist/client-web/remoteEntry.js', + 'extensions/capture/dist/client-web/remoteEntry.js', 'extensions/mail/dist/client-web/remoteEntry.js', 'extensions/memos/dist/client-web/remoteEntry.js', 'extensions/twitter/dist/client-web/remoteEntry.js', diff --git a/scripts/check-workspace-contract.mjs b/scripts/check-workspace-contract.mjs index 6812f099..b55ff80d 100644 --- a/scripts/check-workspace-contract.mjs +++ b/scripts/check-workspace-contract.mjs @@ -5,6 +5,12 @@ const repoRoot = fileURLToPath(new URL('..', import.meta.url)) const jsonOutput = process.argv.includes('--json') const workspaces = [ + { + path: 'extensions/capture', + name: '@inkcre/ext-capture', + requiredScripts: ['build', 'dev', 'type-check'], + builder: 'vite', + }, { path: 'extensions/caldav', name: '@inkcre/ext-caldav', From ccbbc97126c8757a07cb86ac0c7e8f9138be67da Mon Sep 17 00:00:00 2001 From: Lan_zhijiang Date: Wed, 21 Oct 2026 20:30:13 +0800 Subject: [PATCH 2/3] =?UTF-8?q?docs(capture):=20=E6=98=8E=E7=A1=AE?= =?UTF-8?q?=E8=AF=BB=E5=8F=96=E5=88=86=E5=8F=91=E4=B8=8D=E6=98=AF=20Mac=20?= =?UTF-8?q?=E5=86=99=E5=85=A5=E5=89=8D=E6=8F=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引用共享图合同,移除 Python 执行者依赖说明。 --- extensions/capture/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/extensions/capture/README.md b/extensions/capture/README.md index 913da96f..41808276 100644 --- a/extensions/capture/README.md +++ b/extensions/capture/README.md @@ -4,6 +4,8 @@ Host SDK 要求 `@inkcre/core >=0.4.0 <0.5.0`。通过 Registry 选择并启用本浏览器的 `module_federation` 分发;`initialize` 注册 Resolver,`dispose` 只移除本分发拥有的类。Python 执行分发与本地读取分发分别参与生命周期。 -业务图及请求合同由 `InKCre/impl-py/docs/30-unit-tdd/capture-extension.md` 维护;字段验证在 `src/schema.ts`,顺序和引用校验在 `src/resolver.ts`。缺失材料显示错误,不触发采集端重新抓取。来源 URL 只允许 HTTP(S) 作为可点击链接。 +共享图与提交语义由 [主动采集持久化契约](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,检查原文、图片字节、顺序和附注。 +运行 `pnpm --filter @inkcre/ext-capture type-check` 与仓库 `pnpm build`。`dist/client-web` 是可重定位的发布制品,`src/main.ts` 只用于开发说明页。真实验收应安装生产构建,停止原采集者后冷启动打开共享根 Block,检查原文、图片字节、顺序和附注。 + +Mac 自行通过共享数据库原子写入,不依赖 Python Peer。此 Web 分发只负责读取与呈现,未启用它不影响采集写入。 From f2f8d3aa7d0cbeb079cabe7347db26221ef07206 Mon Sep 17 00:00:00 2001 From: Lan_zhijiang Date: Wed, 21 Oct 2026 22:55:33 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(extension):=20=E8=AF=86=E5=88=AB?= =?UTF-8?q?=E9=9A=8F=E5=BA=94=E7=94=A8=E4=BA=A4=E4=BB=98=E7=9A=84=20Swift?= =?UTF-8?q?=20=E5=88=86=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 解释原生参与身份并阻止不受支持的 Registry 版本请求 - 随 Capture 读取产物交付 MIT 许可和发布意图 --- .changeset/capture-reader.md | 4 ++++ .../extension/extensionCard/extensionCard.vue | 18 +++++++++++++--- apps/client-web/src/extension-peer-control.ts | 2 ++ apps/client-web/src/locales/messages/en.json | 3 ++- .../src/locales/messages/zh-CN.json | 3 ++- .../src/views/extensions/extensions.vue | 3 +-- extensions/capture/LICENSE | 21 +++++++++++++++++++ extensions/capture/README.md | 4 ++-- extensions/capture/package.json | 1 + extensions/capture/vite.config.ts | 11 ++++++++++ packages/core/src/extension/model.ts | 2 +- 11 files changed, 62 insertions(+), 10 deletions(-) create mode 100644 extensions/capture/LICENSE diff --git a/.changeset/capture-reader.md b/.changeset/capture-reader.md index 55260a38..113115ef 100644 --- a/.changeset/capture-reader.md +++ b/.changeset/capture-reader.md @@ -1,5 +1,9 @@ --- '@inkcre/ext-capture': minor +'@inkcre/core': patch +'@inkcre/client-web': patch --- 新增随手收集分发,从共享信息库读取有序原文、原始图片、真实来源与用户附注。展示不依赖原 Mac 或 Python 执行 Peer 在线。 + +Core 与 Web 可解释随签名应用交付的 swift_bundled 参与身份;版本管理提示转到对应应用,不调用不支持该格式的 Registry 接口。 diff --git a/apps/client-web/src/components/extension/extensionCard/extensionCard.vue b/apps/client-web/src/components/extension/extensionCard/extensionCard.vue index 20c7aa08..c74aecc5 100644 --- a/apps/client-web/src/components/extension/extensionCard/extensionCard.vue +++ b/apps/client-web/src/components/extension/extensionCard/extensionCard.vue @@ -142,7 +142,9 @@ function distributionLabel(distribution: NativeDistribution | null): string { ? 'Python' : distribution === 'module_federation' ? 'Module Federation' - : t('extension.distributionUnknown') + : distribution === 'swift_bundled' + ? 'Swift (bundled)' + : t('extension.distributionUnknown') } function supportedFormats(peer: Peer): NativeDistribution[] { @@ -297,7 +299,12 @@ async function loadVersions(): Promise { versionLoading.value = true versionOptions.value = [] versionError.value = '' - const readPublished = async (format: NativeDistribution) => { + if (distribution === 'swift_bundled') { + versionError.value = t('extension.bundledManagedByApp') + versionLoading.value = false + return + } + const readPublished = async (format: 'python' | 'module_federation') => { try { const catalog = await getExtensionRegistry().getExtension(props.extension.name, format) const releases = sortPublishedReleases(catalog.releases ?? []) @@ -327,7 +334,7 @@ async function loadVersions(): Promise { : (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 (publishedFormats.value[other] === undefined) await readPublished(other) } } catch (error) { @@ -396,6 +403,11 @@ async function openDocumentation(peer: Peer): Promise { 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 diff --git a/apps/client-web/src/extension-peer-control.ts b/apps/client-web/src/extension-peer-control.ts index bb604716..8b4d4cc1 100644 --- a/apps/client-web/src/extension-peer-control.ts +++ b/apps/client-web/src/extension-peer-control.ts @@ -97,6 +97,8 @@ export async function selectExtensionPeerVersion(input: { livePeerIds: ReadonlySet manager: ExtensionManager }): Promise { + 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') diff --git a/apps/client-web/src/locales/messages/en.json b/apps/client-web/src/locales/messages/en.json index bf2b0177..242e1d05 100644 --- a/apps/client-web/src/locales/messages/en.json +++ b/apps/client-web/src/locales/messages/en.json @@ -212,7 +212,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.", diff --git a/apps/client-web/src/locales/messages/zh-CN.json b/apps/client-web/src/locales/messages/zh-CN.json index b12bd5b8..453b3d7a 100644 --- a/apps/client-web/src/locales/messages/zh-CN.json +++ b/apps/client-web/src/locales/messages/zh-CN.json @@ -213,7 +213,8 @@ "runtimeUnknown": "运行状态未知", "unavailableOnPeer": "此 Peer 不可用", "publishedDistributions": "已发布分发", - "noPublishedDistribution": "尚无已发布的 {distribution} 版本。" + "noPublishedDistribution": "尚无已发布的 {distribution} 版本。", + "bundledManagedByApp": "此分发随应用交付,不通过 Registry 获取版本或文档。请在对应应用中管理。" }, "job": { "loadFailed": "无法读取此任务。", diff --git a/apps/client-web/src/views/extensions/extensions.vue b/apps/client-web/src/views/extensions/extensions.vue index c593ea3d..6088d6dc 100644 --- a/apps/client-web/src/views/extensions/extensions.vue +++ b/apps/client-web/src/views/extensions/extensions.vue @@ -10,7 +10,6 @@ import { PeerManager, ExtensionParticipationModel, type ExtensionParticipation, - type NativeDistribution, type InstalledExtension, } from '@inkcre/core' import type { ReleaseRecord } from '@inkcre/extension-runtime-client-web' @@ -36,7 +35,7 @@ const installPeers = ref([]) const installPeerOptions = computed(() => installPeers.value.map((peer) => ({ label: peer.name, value: peer.id })) ) -const installDistribution = computed(() => +const installDistribution = computed<'python' | 'module_federation'>(() => route.query.distribution === 'python' ? 'python' : 'module_federation' ) const peersLoading = ref(false) diff --git a/extensions/capture/LICENSE b/extensions/capture/LICENSE new file mode 100644 index 00000000..94e962af --- /dev/null +++ b/extensions/capture/LICENSE @@ -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. diff --git a/extensions/capture/README.md b/extensions/capture/README.md index 41808276..414ec300 100644 --- a/extensions/capture/README.md +++ b/extensions/capture/README.md @@ -2,9 +2,9 @@ 该原生 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` 只移除本分发拥有的类。Python 执行分发与本地读取分发分别参与生命周期。 +Host SDK 要求 `@inkcre/core >=0.4.0 <0.5.0`。通过 Registry 选择并启用本浏览器的 `module_federation` 分发;`initialize` 注册 Resolver,`dispose` 只移除本分发拥有的类。Swift、Python 与 Web 分发按各自能力对等参与同一 Extension。 -共享图与提交语义由 [主动采集持久化契约](https://github.com/InKCre/docs/blob/main/20-product-tdd/capture-contract.md) 维护;字段验证在 `src/schema.ts`,顺序和引用校验在 `src/resolver.ts`。缺失材料显示错误,不触发采集端重新抓取。来源 URL 只允许 HTTP(S) 作为可点击链接。 +共享图与提交语义由 [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,检查原文、图片字节、顺序和附注。 diff --git a/extensions/capture/package.json b/extensions/capture/package.json index c5ca81bc..8cc4abe5 100644 --- a/extensions/capture/package.json +++ b/extensions/capture/package.json @@ -2,6 +2,7 @@ "name": "@inkcre/ext-capture", "version": "0.1.0", "private": true, + "license": "MIT", "type": "module", "scripts": { "dev": "vite", diff --git a/extensions/capture/vite.config.ts b/extensions/capture/vite.config.ts index 9bdaecd3..b26ef9db 100644 --- a/extensions/capture/vite.config.ts +++ b/extensions/capture/vite.config.ts @@ -1,3 +1,4 @@ +import { readFileSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' @@ -8,6 +9,16 @@ import extensionPackage from './package.json' export default defineConfig({ plugins: [ vue(), + { + name: 'capture-license', + generateBundle() { + this.emitFile({ + type: 'asset', + fileName: 'LICENSE', + source: readFileSync(new URL('./LICENSE', import.meta.url), 'utf8'), + }) + }, + }, federation({ name: 'extension.capture', filename: 'remoteEntry.js', diff --git a/packages/core/src/extension/model.ts b/packages/core/src/extension/model.ts index ac3500c2..98037883 100644 --- a/packages/core/src/extension/model.ts +++ b/packages/core/src/extension/model.ts @@ -22,7 +22,7 @@ export const ExtensionVersionSchema = z.string().refine( { error: 'Extension version must be strict SemVer without build metadata.' } ) -export const NativeDistributionSchema = z.enum(['python', 'module_federation']) +export const NativeDistributionSchema = z.enum(['python', 'module_federation', 'swift_bundled']) export type NativeDistribution = z.infer /** Shared identity and configuration; native versions belong to individual Peers. */