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..6eb615a13d 100644 --- a/apps/web/pnpm-lock.yaml +++ b/apps/web/pnpm-lock.yaml @@ -842,6 +842,9 @@ importers: ../../packages/plugins/library: dependencies: + exceljs: + specifier: 4.4.0 + version: 4.4.0 jszip: specifier: 3.10.1 version: 3.10.1 @@ -930,6 +933,9 @@ importers: workdsh-contracts: specifier: workspace:* version: link:../../contracts + workdsh-plugin-office: + specifier: workspace:* + version: link:../office workdsh-ui: specifier: workspace:* version: link:../../ui diff --git a/apps/web/scripts/build-library.mjs b/apps/web/scripts/build-library.mjs index 3d180723fa..d9fe4aa243 100644 --- a/apps/web/scripts/build-library.mjs +++ b/apps/web/scripts/build-library.mjs @@ -1,6 +1,19 @@ import { build } from 'esbuild'; import { writeFile } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; +import { createRequire } from 'node:module'; const entry = new URL('../../../packages/plugins/library/', import.meta.url); const result = await build({ entryPoints: [fileURLToPath(new URL('src/client.tsx', entry))], bundle: true, write: false, format: 'cjs', platform: 'browser', target: 'es2022', external: ['@deepseek-ai/dsh-client-ui-primitives', 'react', 'react/jsx-runtime'] }); await writeFile(new URL('dist/client.browser.js', entry), `window.__ModuleLoader__.load({id: "workdsh-plugin-library", factory: function(require) { const module = {exports:{}};\n${result.outputFiles[0].text}\nreturn module.exports; }});\n`); + +// Reuse Office's public extraction module at build time. Library must not require +// an unpublished Office plugin at runtime or activate Office in a personal Profile. +const requireOffice = createRequire(new URL('package.json', entry)); +await build({ + entryPoints: [fileURLToPath(new URL('src/index.ts', entry))], + bundle: true, format: 'esm', platform: 'node', target: 'node22', packages: 'external', + plugins: [{ name: 'office-tabular-only', setup(builder) { + builder.onResolve({ filter: /^workdsh-plugin-office\/tabular$/ }, () => ({ path: requireOffice.resolve('workdsh-plugin-office/tabular') })); + } }], + outfile: fileURLToPath(new URL('dist/index.js', entry)), +}); 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..1ab50ab733 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 解码/解析;表格提取模块随 Library 构建,运行时复用 ExcelJS,无需安装或激活 Office 插件即可导入和检索。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..ee0ed92063 100644 --- a/packages/plugins/library/package.json +++ b/packages/plugins/library/package.json @@ -76,12 +76,14 @@ "react": "19.2.4", "workdsh-contracts": "workspace:*", "workdsh-ui": "workspace:*", - "@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.2" + "@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.2", + "workdsh-plugin-office": "workspace:*" }, "dependencies": { "jszip": "3.10.1", "pdfjs-dist": "5.4.624", - "zod": "4.4.3" + "zod": "4.4.3", + "exceljs": "4.4.0" }, "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}