From 08b19257729442148ad9a85a575fc650c4885fb6 Mon Sep 17 00:00:00 2001 From: Lan_zhijiang Date: Fri, 23 Oct 2026 18:11:24 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(info-base):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=8F=90=E5=8F=96=E7=BB=93=E6=9E=9C=E7=9A=84=E8=B7=A8=E7=AB=AF?= =?UTF-8?q?=E8=AF=BB=E5=8F=96=E4=B8=8E=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/organization-extraction.md | 6 ++ .../ContentExtraction/ContentExtraction.md | 8 ++ .../ContentExtraction/ContentExtraction.scss | 41 ++++++++ .../ContentExtraction/ContentExtraction.ts | 10 ++ .../ContentExtraction/ContentExtraction.vue | 76 +++++++++++++++ .../ContentExtractionPreview.vue | 22 +++++ apps/client-web/src/core.ts | 7 ++ apps/client-web/src/locales/messages/en.json | 7 ++ .../src/locales/messages/zh-CN.json | 7 ++ docs/30-unit-tdd/info-base.md | 9 ++ packages/core/src/index.ts | 3 + packages/core/src/info-base/index.ts | 3 + .../src/info-base/resolvers/extraction.ts | 94 +++++++++++++++++++ .../core/src/info-base/resolvers/index.ts | 5 + 14 files changed, 298 insertions(+) create mode 100644 .changeset/organization-extraction.md create mode 100644 apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.md create mode 100644 apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.scss create mode 100644 apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.ts create mode 100644 apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.vue create mode 100644 apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtractionPreview.vue create mode 100644 packages/core/src/info-base/resolvers/extraction.ts diff --git a/.changeset/organization-extraction.md b/.changeset/organization-extraction.md new file mode 100644 index 00000000..5647223c --- /dev/null +++ b/.changeset/organization-extraction.md @@ -0,0 +1,6 @@ +--- +'@inkcre/core': minor +'@inkcre/client-web': minor +--- + +新增留存依据的提取结果 Resolver 和 Web 阅读视图。正文与必要引文分开呈现,来源存在时可打开;来源删除或解除关联后仍能阅读保留内容,错误映射不影响正文。 diff --git a/apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.md b/apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.md new file mode 100644 index 00000000..5798890e --- /dev/null +++ b/apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.md @@ -0,0 +1,8 @@ +# 提取结果阅读 + +此组件呈现 `core.organization.extraction-result.v1` 的整理正文和分组原文。 +内容模型与来源映射校验属于 core Resolver;组件只安排阅读与导航。 + +正文和内嵌引文不等待来源关系请求。来源缺失或关系读取失败仅影响相应入口, +不能隐藏保留内容或猜测原材料的删除原因。历史出处仅展示实际保存的来源名称。 +预览显示整理正文;完整阅读保留原文空白和代码,不解析为 HTML。 diff --git a/apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.scss b/apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.scss new file mode 100644 index 00000000..27a3204f --- /dev/null +++ b/apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.scss @@ -0,0 +1,41 @@ +.content-extraction { + display: grid; + gap: sys-var(space, lg); + padding: sys-var(space, md); + color: sys-var(color, text, base); + overflow-wrap: anywhere; + @include apply-font(body-md); + + &__text, + blockquote { + white-space: pre-wrap; + } + + &__basis, + &__source { + display: grid; + justify-items: start; + gap: sys-var(space, sm); + } + + &__source + &__source { + margin-top: sys-var(space, md); + } + + h3, + h4 { + margin: 0; + @include apply-font(label-md); + } + + blockquote { + margin: 0; + padding-inline-start: sys-var(space, md); + border-inline-start: 2px solid sys-var(color, border, base); + } + + &__source-state { + color: sys-var(color, text, subtle); + @include apply-font(body-sm); + } +} diff --git a/apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.ts b/apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.ts new file mode 100644 index 00000000..d437296c --- /dev/null +++ b/apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.ts @@ -0,0 +1,10 @@ +import type { + ExtractionContent, + ExtractionResultResolver, + SolvedContentRendererProps, +} from '@inkcre/core' + +export type ContentExtractionProps = SolvedContentRendererProps< + ExtractionContent, + ExtractionResultResolver +> diff --git a/apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.vue b/apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.vue new file mode 100644 index 00000000..d1f6cdfa --- /dev/null +++ b/apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/apps/client-web/src/core.ts b/apps/client-web/src/core.ts index 57e02662..348d5c37 100644 --- a/apps/client-web/src/core.ts +++ b/apps/client-web/src/core.ts @@ -17,6 +17,8 @@ import { localStorageAdapter, registerCoreResolvers, TextResolver, + Resolver, + ExtractionResultResolver, AudioResolver, EpubResolver, FileResolver, @@ -48,6 +50,8 @@ import * as VueUse from '@vueuse/core' import packageJson from '../package.json' import corePackageJson from '../../../packages/core/package.json' import ContentText from '@/components/info-base/resolvers/ContentText.vue' +import ContentExtraction from '@/components/info-base/resolvers/ContentExtraction/ContentExtraction.vue' +import ContentExtractionPreview from '@/components/info-base/resolvers/ContentExtraction/ContentExtractionPreview.vue' import ContentImage from '@/components/info-base/resolvers/ContentImage.vue' import ContentVideo from '@/components/info-base/resolvers/ContentVideo.vue' import ContentHtml from '@/components/info-base/resolvers/ContentHtml.vue' @@ -136,6 +140,9 @@ export function setupResolvers(): void { HtmlResolver.solvedContentRenderer = ContentHtml ZipResolver.solvedContentRenderer = ContentFile registerCoreResolvers() + ExtractionResultResolver.previewRenderer = ContentExtractionPreview + ExtractionResultResolver.solvedContentRenderer = ContentExtraction + Resolver.register(ExtractionResultResolver.type, ExtractionResultResolver) console.log('[Core] Resolver components registered') } diff --git a/apps/client-web/src/locales/messages/en.json b/apps/client-web/src/locales/messages/en.json index 5bf893d4..ddc51173 100644 --- a/apps/client-web/src/locales/messages/en.json +++ b/apps/client-web/src/locales/messages/en.json @@ -445,6 +445,13 @@ "previous": "Previous group", "next": "Next group" } + }, + "extraction": { + "excerpts": "Source excerpts", + "openSource": "Open source", + "sourceUnavailable": "Source association unavailable", + "sourceError": "Unable to read source association", + "sourceLoading": "Reading source association" } } } diff --git a/apps/client-web/src/locales/messages/zh-CN.json b/apps/client-web/src/locales/messages/zh-CN.json index ed6d5508..88f3b1a5 100644 --- a/apps/client-web/src/locales/messages/zh-CN.json +++ b/apps/client-web/src/locales/messages/zh-CN.json @@ -445,6 +445,13 @@ "previous": "上一组", "next": "下一组" } + }, + "extraction": { + "excerpts": "原文依据", + "openSource": "打开原材料", + "sourceUnavailable": "原材料关联不可用", + "sourceError": "无法读取原材料关联", + "sourceLoading": "正在读取原材料关联" } } } diff --git a/docs/30-unit-tdd/info-base.md b/docs/30-unit-tdd/info-base.md index 2b2f9ff3..f77ed7d0 100644 --- a/docs/30-unit-tdd/info-base.md +++ b/docs/30-unit-tdd/info-base.md @@ -55,6 +55,15 @@ the same class and rejects collisions; there is no default decoder. `Resolver.un registers the versioned text, HTML, image, audio, video, PDF, EPUB, ZIP, and file contracts. Extensions register their own namespaced, versioned contracts rather than aliases for retired IDs. +`ExtractionResultResolver` 解码 `core.organization.extraction-result.v1`,由 Client Web 在通用九类 Resolver +之后显式注册。它只依赖 info-base 数据合同,不要求 Python 或提取 Agent 在线。结果正文、历史出处与必要引文 +来自结果自身的 content;来源 Relation 通过 `kind`、`basis_key` 提供当前内部入口。删除来源或解除关系后, +正文和引文仍可读。错误或冲突的来源映射只影响 `getSource()`,不能隐藏合法正文。损坏的结果 payload 明确拒绝。 + +Client Web 的 `ContentExtraction` 分开呈现正文与原文依据,按组显示来源入口状态;链接缺失不推断删除原因。 +历史出处仅展示保存的来源名称,阅读不会重新抓取来源。预览仅展示有限行正文。其他 TypeScript 消费者 +可选择注册这个导出的 Resolver,并按既有合同提供两种 renderer;通用 `registerCoreResolvers()` 的参数不变。 + Every concrete Resolver exposes `getText({ context })`, where `default` and `lexical` are named, resolver-owned projections. Lexical projection is Block-local and non-recursive. Callers preserve four distinct results: diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index fbc01c4e..51ef8e7b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -183,6 +183,9 @@ export { type ProjectionOptions, type TextProjectionContext, TextResolver, + ExtractionResultResolver, + ExtractionContentSchema, + type ExtractionContent, AudioResolver, EpubResolver, FileResolver, diff --git a/packages/core/src/info-base/index.ts b/packages/core/src/info-base/index.ts index 59a57cb6..49d1bfe6 100644 --- a/packages/core/src/info-base/index.ts +++ b/packages/core/src/info-base/index.ts @@ -9,6 +9,9 @@ export { Resolver, ResolverCache, TextResolver, + ExtractionResultResolver, + ExtractionContentSchema, + type ExtractionContent, AudioResolver, EpubResolver, FileResolver, diff --git a/packages/core/src/info-base/resolvers/extraction.ts b/packages/core/src/info-base/resolvers/extraction.ts new file mode 100644 index 00000000..8fc5e1bc --- /dev/null +++ b/packages/core/src/info-base/resolvers/extraction.ts @@ -0,0 +1,94 @@ +import { z } from 'zod' + +import { Resolver } from './base' +import { ResolverContentError, type ProjectionOptions } from './contracts' +import { decodeUnicode } from './decode' + +const nonBlankText = z.string().refine((value) => value.trim().length > 0) + +export const ExtractionContentSchema = z.strictObject({ + text: nonBlankText, + basis: z + .array( + z.strictObject({ + key: nonBlankText, + observed_origin: z.strictObject({ label: nonBlankText }), + excerpts: z.array(nonBlankText).min(1), + }) + ) + .min(1) + .refine((groups) => new Set(groups.map((group) => group.key)).size === groups.length), +}) + +export type ExtractionContent = z.infer + +const sourceRelationSchema = z.strictObject({ + kind: z.literal('core.organization.extraction.source.v1'), + basis_key: nonBlankText, +}) + +export class ExtractionResultResolver extends Resolver { + static readonly type = 'core.organization.extraction-result.v1' + + protected async _getSolvedContent(options: ProjectionOptions): Promise { + const text = decodeUnicode(await this.getRawContent(options), ExtractionResultResolver.type) + let value: unknown + try { + value = JSON.parse(text) + } catch (error) { + if (!(error instanceof SyntaxError)) throw error + throw new ResolverContentError(ExtractionResultResolver.type, 'invalid extraction JSON') + } + const result = ExtractionContentSchema.safeParse(value) + if (!result.success) { + throw new ResolverContentError(ExtractionResultResolver.type, 'invalid extraction content') + } + return result.data + } + + async getText(options: ProjectionOptions = {}): Promise { + const result = await this.getSolvedContent(options) + const parts = [`整理正文\n${result.text}`, '原文依据'] + for (const group of result.basis) { + parts.push(group.observed_origin.label) + parts.push( + ...group.excerpts.map((excerpt) => + excerpt + .split('\n') + .map((line) => `> ${line}`) + .join('\n') + ) + ) + } + return parts.join('\n\n') + } + + async getSource(basisKey: string, options: ProjectionOptions = {}): Promise { + const result = await this.getSolvedContent(options) + if (!result.basis.some((group) => group.key === basisKey)) { + throw new ResolverContentError(ExtractionResultResolver.type, 'unknown basis key') + } + const relations = await this.getRelations({ refresh: options.refresh, includeOut: false }) + const sources: number[] = [] + for (const relation of relations) { + let value: unknown + try { + value = JSON.parse(relation.content) + } catch (error) { + if (!(error instanceof SyntaxError)) throw error + continue + } + if (typeof value !== 'object' || value === null || !('kind' in value)) continue + if (value.kind !== 'core.organization.extraction.source.v1') continue + const mapping = sourceRelationSchema.safeParse(value) + if (!mapping.success) { + throw new ResolverContentError(ExtractionResultResolver.type, 'invalid source mapping') + } + if (mapping.data.basis_key === basisKey) sources.push(relation.from_) + } + if (sources.length > 1) { + throw new ResolverContentError(ExtractionResultResolver.type, 'ambiguous source mapping') + } + return sources[0] ?? null + } +} diff --git a/packages/core/src/info-base/resolvers/index.ts b/packages/core/src/info-base/resolvers/index.ts index 79e9a7b7..9b5fc301 100644 --- a/packages/core/src/info-base/resolvers/index.ts +++ b/packages/core/src/info-base/resolvers/index.ts @@ -25,6 +25,11 @@ export { ResolverCache } from './cache' // Concrete resolver implementations export { TextResolver } from './text' +export { + ExtractionResultResolver, + ExtractionContentSchema, + type ExtractionContent, +} from './extraction' export { HtmlResolver, type HtmlRawContent } from './html' export { ImageResolver, type ImageSolvedContent } from './image' export { AudioResolver, type AudioSolvedContent } from './audio' From 2c467ae831823478efea0b86165eba642acb65c4 Mon Sep 17 00:00:00 2001 From: Lan_zhijiang Date: Fri, 23 Oct 2026 18:13:38 +0800 Subject: [PATCH 2/4] =?UTF-8?q?chore(shared):=20=E5=BC=95=E5=85=A5?= =?UTF-8?q?=E6=8F=90=E5=8F=96=E7=BB=93=E6=9E=9C=E4=B8=8E=E4=BE=9D=E6=8D=AE?= =?UTF-8?q?=E4=BF=9D=E7=95=99=E5=90=88=E5=90=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/_shared | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_shared b/docs/_shared index 7449135f..4b677be8 160000 --- a/docs/_shared +++ b/docs/_shared @@ -1 +1 @@ -Subproject commit 7449135fefdd6056fecba387e59e3005aed5eafd +Subproject commit 4b677be8ab8c8c55c801b51484828a51a172e9b3 From b010838975a7e7a22f3e7f66f20cec483cea739f Mon Sep 17 00:00:00 2001 From: Lan_zhijiang Date: Mon, 26 Oct 2026 14:44:56 +0800 Subject: [PATCH 3/4] =?UTF-8?q?ref(info-base):=20=E6=92=A4=E9=94=80?= =?UTF-8?q?=E6=8F=90=E5=8F=96=E7=BB=93=E6=9E=9C=E4=B8=93=E7=94=A8=E8=AF=BB?= =?UTF-8?q?=E5=8F=96=E4=B8=8E=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用既有 TextResolver、文本组件和图导航 - 移除未发布的 decoder、组件、导出与版本发布声明 --- .changeset/organization-extraction.md | 6 -- .../ContentExtraction/ContentExtraction.md | 8 -- .../ContentExtraction/ContentExtraction.scss | 41 -------- .../ContentExtraction/ContentExtraction.ts | 10 -- .../ContentExtraction/ContentExtraction.vue | 76 --------------- .../ContentExtractionPreview.vue | 22 ----- apps/client-web/src/core.ts | 7 -- apps/client-web/src/locales/messages/en.json | 7 -- .../src/locales/messages/zh-CN.json | 7 -- docs/30-unit-tdd/info-base.md | 11 +-- packages/core/src/index.ts | 3 - packages/core/src/info-base/index.ts | 3 - .../src/info-base/resolvers/extraction.ts | 94 ------------------- .../core/src/info-base/resolvers/index.ts | 5 - 14 files changed, 2 insertions(+), 298 deletions(-) delete mode 100644 .changeset/organization-extraction.md delete mode 100644 apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.md delete mode 100644 apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.scss delete mode 100644 apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.ts delete mode 100644 apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.vue delete mode 100644 apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtractionPreview.vue delete mode 100644 packages/core/src/info-base/resolvers/extraction.ts diff --git a/.changeset/organization-extraction.md b/.changeset/organization-extraction.md deleted file mode 100644 index 5647223c..00000000 --- a/.changeset/organization-extraction.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@inkcre/core': minor -'@inkcre/client-web': minor ---- - -新增留存依据的提取结果 Resolver 和 Web 阅读视图。正文与必要引文分开呈现,来源存在时可打开;来源删除或解除关联后仍能阅读保留内容,错误映射不影响正文。 diff --git a/apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.md b/apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.md deleted file mode 100644 index 5798890e..00000000 --- a/apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.md +++ /dev/null @@ -1,8 +0,0 @@ -# 提取结果阅读 - -此组件呈现 `core.organization.extraction-result.v1` 的整理正文和分组原文。 -内容模型与来源映射校验属于 core Resolver;组件只安排阅读与导航。 - -正文和内嵌引文不等待来源关系请求。来源缺失或关系读取失败仅影响相应入口, -不能隐藏保留内容或猜测原材料的删除原因。历史出处仅展示实际保存的来源名称。 -预览显示整理正文;完整阅读保留原文空白和代码,不解析为 HTML。 diff --git a/apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.scss b/apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.scss deleted file mode 100644 index 27a3204f..00000000 --- a/apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.scss +++ /dev/null @@ -1,41 +0,0 @@ -.content-extraction { - display: grid; - gap: sys-var(space, lg); - padding: sys-var(space, md); - color: sys-var(color, text, base); - overflow-wrap: anywhere; - @include apply-font(body-md); - - &__text, - blockquote { - white-space: pre-wrap; - } - - &__basis, - &__source { - display: grid; - justify-items: start; - gap: sys-var(space, sm); - } - - &__source + &__source { - margin-top: sys-var(space, md); - } - - h3, - h4 { - margin: 0; - @include apply-font(label-md); - } - - blockquote { - margin: 0; - padding-inline-start: sys-var(space, md); - border-inline-start: 2px solid sys-var(color, border, base); - } - - &__source-state { - color: sys-var(color, text, subtle); - @include apply-font(body-sm); - } -} diff --git a/apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.ts b/apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.ts deleted file mode 100644 index d437296c..00000000 --- a/apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { - ExtractionContent, - ExtractionResultResolver, - SolvedContentRendererProps, -} from '@inkcre/core' - -export type ContentExtractionProps = SolvedContentRendererProps< - ExtractionContent, - ExtractionResultResolver -> diff --git a/apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.vue b/apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.vue deleted file mode 100644 index d1f6cdfa..00000000 --- a/apps/client-web/src/components/info-base/resolvers/ContentExtraction/ContentExtraction.vue +++ /dev/null @@ -1,76 +0,0 @@ - - - - - diff --git a/apps/client-web/src/core.ts b/apps/client-web/src/core.ts index 348d5c37..57e02662 100644 --- a/apps/client-web/src/core.ts +++ b/apps/client-web/src/core.ts @@ -17,8 +17,6 @@ import { localStorageAdapter, registerCoreResolvers, TextResolver, - Resolver, - ExtractionResultResolver, AudioResolver, EpubResolver, FileResolver, @@ -50,8 +48,6 @@ import * as VueUse from '@vueuse/core' import packageJson from '../package.json' import corePackageJson from '../../../packages/core/package.json' import ContentText from '@/components/info-base/resolvers/ContentText.vue' -import ContentExtraction from '@/components/info-base/resolvers/ContentExtraction/ContentExtraction.vue' -import ContentExtractionPreview from '@/components/info-base/resolvers/ContentExtraction/ContentExtractionPreview.vue' import ContentImage from '@/components/info-base/resolvers/ContentImage.vue' import ContentVideo from '@/components/info-base/resolvers/ContentVideo.vue' import ContentHtml from '@/components/info-base/resolvers/ContentHtml.vue' @@ -140,9 +136,6 @@ export function setupResolvers(): void { HtmlResolver.solvedContentRenderer = ContentHtml ZipResolver.solvedContentRenderer = ContentFile registerCoreResolvers() - ExtractionResultResolver.previewRenderer = ContentExtractionPreview - ExtractionResultResolver.solvedContentRenderer = ContentExtraction - Resolver.register(ExtractionResultResolver.type, ExtractionResultResolver) console.log('[Core] Resolver components registered') } diff --git a/apps/client-web/src/locales/messages/en.json b/apps/client-web/src/locales/messages/en.json index ddc51173..5bf893d4 100644 --- a/apps/client-web/src/locales/messages/en.json +++ b/apps/client-web/src/locales/messages/en.json @@ -445,13 +445,6 @@ "previous": "Previous group", "next": "Next group" } - }, - "extraction": { - "excerpts": "Source excerpts", - "openSource": "Open source", - "sourceUnavailable": "Source association unavailable", - "sourceError": "Unable to read source association", - "sourceLoading": "Reading source association" } } } diff --git a/apps/client-web/src/locales/messages/zh-CN.json b/apps/client-web/src/locales/messages/zh-CN.json index 88f3b1a5..ed6d5508 100644 --- a/apps/client-web/src/locales/messages/zh-CN.json +++ b/apps/client-web/src/locales/messages/zh-CN.json @@ -445,13 +445,6 @@ "previous": "上一组", "next": "下一组" } - }, - "extraction": { - "excerpts": "原文依据", - "openSource": "打开原材料", - "sourceUnavailable": "原材料关联不可用", - "sourceError": "无法读取原材料关联", - "sourceLoading": "正在读取原材料关联" } } } diff --git a/docs/30-unit-tdd/info-base.md b/docs/30-unit-tdd/info-base.md index f77ed7d0..9dadd2ef 100644 --- a/docs/30-unit-tdd/info-base.md +++ b/docs/30-unit-tdd/info-base.md @@ -55,15 +55,6 @@ the same class and rejects collisions; there is no default decoder. `Resolver.un registers the versioned text, HTML, image, audio, video, PDF, EPUB, ZIP, and file contracts. Extensions register their own namespaced, versioned contracts rather than aliases for retired IDs. -`ExtractionResultResolver` 解码 `core.organization.extraction-result.v1`,由 Client Web 在通用九类 Resolver -之后显式注册。它只依赖 info-base 数据合同,不要求 Python 或提取 Agent 在线。结果正文、历史出处与必要引文 -来自结果自身的 content;来源 Relation 通过 `kind`、`basis_key` 提供当前内部入口。删除来源或解除关系后, -正文和引文仍可读。错误或冲突的来源映射只影响 `getSource()`,不能隐藏合法正文。损坏的结果 payload 明确拒绝。 - -Client Web 的 `ContentExtraction` 分开呈现正文与原文依据,按组显示来源入口状态;链接缺失不推断删除原因。 -历史出处仅展示保存的来源名称,阅读不会重新抓取来源。预览仅展示有限行正文。其他 TypeScript 消费者 -可选择注册这个导出的 Resolver,并按既有合同提供两种 renderer;通用 `registerCoreResolvers()` 的参数不变。 - Every concrete Resolver exposes `getText({ context })`, where `default` and `lexical` are named, resolver-owned projections. Lexical projection is Block-local and non-recursive. Callers preserve four distinct results: @@ -173,3 +164,5 @@ Twitter 的详情 renderer 以作者、正文、媒体与引用、发布时间 每个媒体视图拥有独立的运行态 Resolver 和 Object URL,随视图切换、卸载或加载结果被替换而撤销;视图关闭后的异步结果也会被释放,不与同时打开的视图共享待撤销的句柄。旧格式 renderer 使用相同阅读布局,但其 Resolver 原有的附件关系解析仍可能读取字节;本轮没有改写旧格式解析合同。UI 候选验收入口及边界见 [`scripts/tests/twitter-rendering`](../../scripts/tests/twitter-rendering/README.md)。 媒体显示使用运行态 Object URL,随 Resolver 刷新、释放和缓存替换撤销。协议 DTO、provider 响应及共同信息模型的转换属于 Twitter 适配器,不能让客户端协议字段集变成 info-base 或 Resolver 的持久数据权威。 + +Organization 提取结果沿用 `core.text.v1`,正文与必要引文已是可读文本;客户端通过现有 TextResolver 和文本组件读取,通过普通 `extraction` Relation 导航来源。生成行为不新增客户端结果类型或专用展示协议。 diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 51ef8e7b..fbc01c4e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -183,9 +183,6 @@ export { type ProjectionOptions, type TextProjectionContext, TextResolver, - ExtractionResultResolver, - ExtractionContentSchema, - type ExtractionContent, AudioResolver, EpubResolver, FileResolver, diff --git a/packages/core/src/info-base/index.ts b/packages/core/src/info-base/index.ts index 49d1bfe6..59a57cb6 100644 --- a/packages/core/src/info-base/index.ts +++ b/packages/core/src/info-base/index.ts @@ -9,9 +9,6 @@ export { Resolver, ResolverCache, TextResolver, - ExtractionResultResolver, - ExtractionContentSchema, - type ExtractionContent, AudioResolver, EpubResolver, FileResolver, diff --git a/packages/core/src/info-base/resolvers/extraction.ts b/packages/core/src/info-base/resolvers/extraction.ts deleted file mode 100644 index 8fc5e1bc..00000000 --- a/packages/core/src/info-base/resolvers/extraction.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { z } from 'zod' - -import { Resolver } from './base' -import { ResolverContentError, type ProjectionOptions } from './contracts' -import { decodeUnicode } from './decode' - -const nonBlankText = z.string().refine((value) => value.trim().length > 0) - -export const ExtractionContentSchema = z.strictObject({ - text: nonBlankText, - basis: z - .array( - z.strictObject({ - key: nonBlankText, - observed_origin: z.strictObject({ label: nonBlankText }), - excerpts: z.array(nonBlankText).min(1), - }) - ) - .min(1) - .refine((groups) => new Set(groups.map((group) => group.key)).size === groups.length), -}) - -export type ExtractionContent = z.infer - -const sourceRelationSchema = z.strictObject({ - kind: z.literal('core.organization.extraction.source.v1'), - basis_key: nonBlankText, -}) - -export class ExtractionResultResolver extends Resolver { - static readonly type = 'core.organization.extraction-result.v1' - - protected async _getSolvedContent(options: ProjectionOptions): Promise { - const text = decodeUnicode(await this.getRawContent(options), ExtractionResultResolver.type) - let value: unknown - try { - value = JSON.parse(text) - } catch (error) { - if (!(error instanceof SyntaxError)) throw error - throw new ResolverContentError(ExtractionResultResolver.type, 'invalid extraction JSON') - } - const result = ExtractionContentSchema.safeParse(value) - if (!result.success) { - throw new ResolverContentError(ExtractionResultResolver.type, 'invalid extraction content') - } - return result.data - } - - async getText(options: ProjectionOptions = {}): Promise { - const result = await this.getSolvedContent(options) - const parts = [`整理正文\n${result.text}`, '原文依据'] - for (const group of result.basis) { - parts.push(group.observed_origin.label) - parts.push( - ...group.excerpts.map((excerpt) => - excerpt - .split('\n') - .map((line) => `> ${line}`) - .join('\n') - ) - ) - } - return parts.join('\n\n') - } - - async getSource(basisKey: string, options: ProjectionOptions = {}): Promise { - const result = await this.getSolvedContent(options) - if (!result.basis.some((group) => group.key === basisKey)) { - throw new ResolverContentError(ExtractionResultResolver.type, 'unknown basis key') - } - const relations = await this.getRelations({ refresh: options.refresh, includeOut: false }) - const sources: number[] = [] - for (const relation of relations) { - let value: unknown - try { - value = JSON.parse(relation.content) - } catch (error) { - if (!(error instanceof SyntaxError)) throw error - continue - } - if (typeof value !== 'object' || value === null || !('kind' in value)) continue - if (value.kind !== 'core.organization.extraction.source.v1') continue - const mapping = sourceRelationSchema.safeParse(value) - if (!mapping.success) { - throw new ResolverContentError(ExtractionResultResolver.type, 'invalid source mapping') - } - if (mapping.data.basis_key === basisKey) sources.push(relation.from_) - } - if (sources.length > 1) { - throw new ResolverContentError(ExtractionResultResolver.type, 'ambiguous source mapping') - } - return sources[0] ?? null - } -} diff --git a/packages/core/src/info-base/resolvers/index.ts b/packages/core/src/info-base/resolvers/index.ts index 9b5fc301..79e9a7b7 100644 --- a/packages/core/src/info-base/resolvers/index.ts +++ b/packages/core/src/info-base/resolvers/index.ts @@ -25,11 +25,6 @@ export { ResolverCache } from './cache' // Concrete resolver implementations export { TextResolver } from './text' -export { - ExtractionResultResolver, - ExtractionContentSchema, - type ExtractionContent, -} from './extraction' export { HtmlResolver, type HtmlRawContent } from './html' export { ImageResolver, type ImageSolvedContent } from './image' export { AudioResolver, type AudioSolvedContent } from './audio' From fd151ee78e190f4b7c6428ffb310070b725a76de Mon Sep 17 00:00:00 2001 From: Lan_zhijiang Date: Mon, 26 Oct 2026 14:44:56 +0800 Subject: [PATCH 4/4] =?UTF-8?q?chore(shared):=20=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E6=99=AE=E9=80=9A=E6=96=87=E6=9C=AC=E6=8F=90=E5=8F=96=E5=90=88?= =?UTF-8?q?=E5=90=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/_shared | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_shared b/docs/_shared index 4b677be8..ae3bb7f0 160000 --- a/docs/_shared +++ b/docs/_shared @@ -1 +1 @@ -Subproject commit 4b677be8ab8c8c55c801b51484828a51a172e9b3 +Subproject commit ae3bb7f06a7ea6aeacb4f0e0ee7faec27f1cd3fc