From ae5c5c8374ada3ddaf8834275e3903901847073d Mon Sep 17 00:00:00 2001 From: techflag <562635045@qq.com> Date: Sun, 4 Oct 2026 20:06:33 +0800 Subject: [PATCH 1/2] feat(library): import and reference CSV and XLSX through Office --- .github/workflows/ci.yml | 3 ++ apps/web/pnpm-lock.yaml | 3 ++ apps/web/scripts/build-office.mjs | 2 + apps/web/scripts/pack-library-release.mjs | 2 +- apps/web/scripts/pack-office-release.mjs | 2 +- packages/contracts/src/library.ts | 4 +- packages/plugins/library/README.md | 8 +++- packages/plugins/library/package.json | 3 +- .../library/src/client/LibraryPanel.tsx | 10 ++--- .../src/client/LibraryReferencePage.tsx | 2 +- .../src/client/LibrarySelectionChips.tsx | 2 +- .../library/src/remote/connection-api.ts | 2 +- .../library/src/services/converters.ts | 6 +++ .../library/src/services/library-manager.ts | 4 +- .../plugins/library/src/storage/domain.ts | 2 +- .../library/src/tools/library-tools.ts | 2 +- .../plugins/library/tests/library.test.mjs | 42 +++++++++++++++++ packages/plugins/office/package.json | 6 ++- .../plugins/office/src/OfficeDocument.tsx | 2 + packages/plugins/office/src/client.tsx | 25 +++++++++-- .../plugins/office/src/csv/CsvDocument.tsx | 2 +- packages/plugins/office/src/tabular.d.ts | 1 + packages/plugins/office/src/tabular.js | 45 +++++++++++++++++++ 23 files changed, 157 insertions(+), 23 deletions(-) create mode 100644 packages/plugins/office/src/tabular.d.ts create mode 100644 packages/plugins/office/src/tabular.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d27e5c57a..5834832942 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -144,6 +144,9 @@ jobs: run: pnpm exec playwright install --with-deps chromium - working-directory: apps/web run: pnpm test:integration + - name: Library import, search and task reference tests + working-directory: apps/web + run: node --test ../../packages/plugins/library/tests/*.test.mjs - name: Enterprise desktop identity and collaboration tests working-directory: apps/web run: node --test ../../packages/providers/identity-enterprise/tests/*.test.mjs ../../packages/plugins/enterprise-collaboration/tests/*.test.mjs diff --git a/apps/web/pnpm-lock.yaml b/apps/web/pnpm-lock.yaml index 0d3fe0f1a2..46d3a01a1f 100644 --- a/apps/web/pnpm-lock.yaml +++ b/apps/web/pnpm-lock.yaml @@ -848,6 +848,9 @@ importers: pdfjs-dist: specifier: 5.4.624 version: 5.4.624 + workdsh-plugin-office: + specifier: workspace:* + version: link:../office zod: specifier: 4.4.3 version: 4.4.3 diff --git a/apps/web/scripts/build-office.mjs b/apps/web/scripts/build-office.mjs index 3277b93d10..6f31184c64 100644 --- a/apps/web/scripts/build-office.mjs +++ b/apps/web/scripts/build-office.mjs @@ -8,6 +8,8 @@ import { fileURLToPath } from "node:url"; const root = new URL("../../../packages/plugins/office/", import.meta.url); const wordOnly = process.argv.includes("--word-only"); await mkdir(new URL("dist/", root), { recursive: true }); +await build({ entryPoints: [fileURLToPath(new URL("src/tabular.js", root))], bundle: true, platform: "node", format: "esm", target: "node22", external: ["exceljs"], outfile: fileURLToPath(new URL("dist/tabular.js", root)) }); +await writeFile(new URL("dist/tabular.d.ts", root), await readFile(new URL("src/tabular.d.ts", root))); const font=await readFile(new URL("src/pdf/fonts/NotoSansSC.ttf",root)); const fontSource=JSON.parse(await readFile(new URL("src/pdf/fonts/source.json",root),"utf8")); if(font.length!==fontSource.bytes||createHash("sha256").update(font).digest("hex")!==fontSource.sha256)throw Error("Bundled PDF font integrity mismatch"); diff --git a/apps/web/scripts/pack-library-release.mjs b/apps/web/scripts/pack-library-release.mjs index 30b9080cfe..726a35a7ce 100644 --- a/apps/web/scripts/pack-library-release.mjs +++ b/apps/web/scripts/pack-library-release.mjs @@ -17,7 +17,7 @@ delete manifest.devDependencies; delete manifest.scripts; manifest.workdshRelease = { scope: 'local-library-alpha', storage: '$DSH_HOME/library', - formats: ['md', 'markdown', 'txt', 'html', 'htm', 'pdf', 'docx', 'pptx'], + formats: ['md', 'markdown', 'txt', 'html', 'htm', 'pdf', 'docx', 'pptx', 'csv', 'xlsx'], preservesOriginals: true, }; await writeFile(join(stage, 'package.json'), `${JSON.stringify(manifest, null, 2)}\n`); diff --git a/apps/web/scripts/pack-office-release.mjs b/apps/web/scripts/pack-office-release.mjs index 9c8076d6ef..b874e7a40e 100644 --- a/apps/web/scripts/pack-office-release.mjs +++ b/apps/web/scripts/pack-office-release.mjs @@ -21,7 +21,7 @@ const manifest = JSON.parse(await readFile(join(plugin, "package.json"), "utf8") delete manifest.devDependencies; delete manifest.scripts; manifest.description = "Independent Harness plugin: browser Office working copies and native PPTX templates (alpha preview)"; -manifest.workdshRelease = {scope: "office-browser-preview", wordOnly: false, formats: ["docx", "pptx", "xlsx", "pdf", "html"]}; +manifest.workdshRelease = {scope: "office-browser-preview", wordOnly: false, formats: ["docx", "pptx", "xlsx", "csv", "pdf", "html"]}; await writeFile(join(stage, "package.json"), JSON.stringify(manifest, null, 2) + "\n"); for (const file of manifest.files) await cp(join(plugin, file), join(stage, file), {recursive: true}); execFileSync("corepack", ["pnpm", "pack", "--pack-destination", destination], {cwd: stage, stdio: "inherit"}); diff --git a/packages/contracts/src/library.ts b/packages/contracts/src/library.ts index e417585164..b6b41d3f39 100644 --- a/packages/contracts/src/library.ts +++ b/packages/contracts/src/library.ts @@ -1,6 +1,6 @@ import type { ActorContext, ResourceOwner } from './governance.js'; -export type LibraryAssetKind = 'markdown' | 'text' | 'pdf' | 'docx' | 'pptx' | 'html'; +export type LibraryAssetKind = 'markdown' | 'text' | 'pdf' | 'docx' | 'pptx' | 'html' | 'csv' | 'xlsx'; export type LibraryConversionStatus = 'ready' | 'pending' | 'failed'; export type LibraryAssetStatus = 'active' | 'disabled'; @@ -114,7 +114,7 @@ export interface LibraryDraft { export interface LibraryOriginalPreviewInput { readonly name: string; - readonly kind: Extract; + readonly kind: Extract; readonly bytes: Uint8Array; } diff --git a/packages/plugins/library/README.md b/packages/plugins/library/README.md index 6cbd2eee73..468a5deb0b 100644 --- a/packages/plugins/library/README.md +++ b/packages/plugins/library/README.md @@ -17,7 +17,7 @@ ## 验收与下一步 -完成对应 PLAN 任务及 [验收矩阵](../../../apps/web/docs/ACCEPTANCE.md) 场景,记录真实测试证据后才更新为完成。当前 Host 与 Client 入口可加载,数据保存到 `$DSH_HOME/library`。支持 MD、TXT、HTML、PDF、DOCX、PPTX;所有格式保留原件并生成独立检索文本。HTML 在无 Host 权限、无网络访问的沙箱中按原始样式和内联交互展示;PDF 可直接预览;安装 Office 插件时,Word 使用 `docx-preview`、PowerPoint 使用 `pptx-react-viewer` 打开原件,未安装时回退为派生检索文本和原件下载。Library 与 Office 通过公开可选预览注册表协作,不互相导入运行时内部实现。 +完成对应 PLAN 任务及 [验收矩阵](../../../apps/web/docs/ACCEPTANCE.md) 场景,记录真实测试证据后才更新为完成。当前 Host 与 Client 入口可加载,数据保存到 `$DSH_HOME/library`。支持 MD、TXT、HTML、PDF、DOCX、PPTX、CSV、XLSX;所有格式保留原件并生成独立检索文本。HTML 在无 Host 权限、无网络访问的沙箱中按原始样式和内联交互展示;PDF 可直接预览;安装 Office 插件时,Word 使用 `docx-preview`、PowerPoint 使用 `pptx-react-viewer` 打开原件,未安装时回退为派生检索文本和原件下载。Library 与 Office 通过公开可选预览注册表协作,不互相导入运行时内部实现。 发布包可通过 `corepack pnpm probe:library` 验证独立安装、两次冷启动、卸载保留 `$DSH_HOME/library`、重新安装和资料恢复。页面支持搜索、最近、本地产物、完整目录树、目录内新建与导入、移动、添加到当前任务、停用与重新启用;停用会立即阻断历史任务引用的正文读取。搜索结果显示目录、类型、来源、修订、转换状态和页/段/幻灯片位置。 @@ -36,3 +36,9 @@ ### 可选 Host 成员服务选择 公开 registerLibraryConnection、registerLibraryTools、registerLibraryContextInjection 接受可选可信选择器,返回服务端 actor、LibraryService 和 verify。工具与上下文使用实际 Agent 选择,不从模型参数读取主体;操作前后核验,拒绝不匹配的会话上下文。默认 apply 不传选择器,保持个人行为。企业装配必须提供隔离存储和可撤权身份;这些入口本身不是文件执行沙箱。 + +## 表格资料 + +CSV/XLSX 可上传、按类型检索并引用到任务;原始文件按字节保留。Host 转换通过 Office 的公开 `workdsh-plugin-office/tabular` 入口复用 CSV 解码/解析及 ExcelJS。CSV 支持 UTF-8、GB18030、UTF-16,以及逗号、分号和制表符;XLSX 提取工作表名称和单元格已有值,公式不重新计算,图表、图片和格式不进入检索文本。检索文本最多 1500 行/120 列/24000 单元格,XLSX 最多 20 个工作表;截取会显示警告,完整原件仍可下载。 + +原件预览通过公开可选注册表复用 Office:CSV 为只读表格,XLSX 使用现有隔离编辑器,导出为副本,不覆盖资料库修订。未启用 Office 预览时显示检索文本。个人与企业 Desktop、Web 复用相同导入和预览实现,实际资料授权保持不变。 diff --git a/packages/plugins/library/package.json b/packages/plugins/library/package.json index 25405a1034..75f8675635 100644 --- a/packages/plugins/library/package.json +++ b/packages/plugins/library/package.json @@ -81,7 +81,8 @@ "dependencies": { "jszip": "3.10.1", "pdfjs-dist": "5.4.624", - "zod": "4.4.3" + "zod": "4.4.3", + "workdsh-plugin-office": "workspace:*" }, "dsh": { "bundle": { diff --git a/packages/plugins/library/src/client/LibraryPanel.tsx b/packages/plugins/library/src/client/LibraryPanel.tsx index b59979d68c..6d5cbd3428 100644 --- a/packages/plugins/library/src/client/LibraryPanel.tsx +++ b/packages/plugins/library/src/client/LibraryPanel.tsx @@ -68,7 +68,7 @@ export function LibraryPanel({ management, previewRegistry, toggleNavigation, st if (entry.revision?.conversionStatus === 'ready') setContent(await management.readText(entry.asset!.id, revisionId)); if (entry.asset?.kind === 'pdf') { const bytes = await management.readOriginal(entry.asset.id, revisionId); setOriginalUrl(URL.createObjectURL(new Blob([bytes as BlobPart], { type: 'application/pdf' }))); } if (entry.asset?.kind === 'html') { const bytes = await management.readOriginal(entry.asset.id, revisionId); setHtmlPreview(new TextDecoder().decode(bytes)); } - if ((entry.asset?.kind === 'docx' || entry.asset?.kind === 'pptx') && previewRegistry.canOpen(entry.asset.kind)) { const bytes = await management.readOriginal(entry.asset.id, revisionId); setOfficePreview({ name: entry.name, kind: entry.asset.kind, bytes }); } + if ((entry.asset?.kind === 'docx' || entry.asset?.kind === 'pptx' || entry.asset?.kind === 'csv' || entry.asset?.kind === 'xlsx') && previewRegistry.canOpen(entry.asset.kind)) { const bytes = await management.readOriginal(entry.asset.id, revisionId); setOfficePreview({ name: entry.name, kind: entry.asset.kind, bytes }); } } catch (cause) { setError(cause instanceof Error ? cause.message : '读取资料失败。'); } }; const runSearch = async (nextView: View = 'search', nextQuery = query) => { @@ -109,10 +109,10 @@ export function LibraryPanel({ management, previewRegistry, toggleNavigation, st const resultEntries = hits.map(hit => findById(hit.nodeId)).filter((row): row is LibraryTreeEntry => Boolean(row)); return
- +
{notice ?
{notice}
: null}{error ?
{error}
: null} - {selected ? <>
{entryPath(selected).replace(` / ${selected.name}`, '')}/{selected.name}{selected.asset?.status === 'disabled' ? 已停用 : selected.revision?.conversionStatus === 'failed' ? 转换失败 : selected.revision?.conversionStatus === 'pending' ? 转换中 : null}
{isEditable(selected) ? : null}
{selected.revision?.conversionStatus === 'failed' ?
原件已安全保存,但检索文本转换失败,因此暂不可搜索。仍可预览或下载原件。
: null}{draft ?
编辑草稿发布后生成新的只读修订
当前修订
{draft.content}
草稿预览
{draftContent}