diff --git a/AGENTS.md b/AGENTS.md index aae905f0..be529261 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,7 @@ - `3000` 当前主壳已接入 Rust Web WebSocket push 主链(`/api/realtime/ws`)+ SSE fallback(`/api/tree/events`)的 snapshot / delta / resync consumer,并已有 browser smoke 验证(2026-05-17 WS 迁移 `57ec8322`);后续收口重点是统一 live cache 与减少补偿链,而不是把它描述成“还没接 live stream”。 - 文档页默认主编辑器已切到页面内 `leptos-tiptap` island;`BlockNote` 已退出文档页默认主路径,只保留为历史参考实现 / 对照材料。 - Page Aggregate 当前已输出 `blockDocument / blockProjectionVersion / projectionSource`(`projectionSource=documents.content`),标题/正文/页面设置写入后 smoke 已通过(`task110`、`task-page-aggregate-body-sync-smoke`、`task-page-aggregate-options-sync-smoke`);页面/块 AI 最小工具链已通过 Rust Hermes tools 读取和写入块投影;但这仍是从 `documents.content` / local markdown content 投影出来的过渡态,不是 EditorBlockDocument 原生落库完成态。客户端 `PageAggregateClientState` reducer 仍在,页面域单一真源未完全闭环。 -- 当前最优先的架构收口不是继续扩编辑器 UI,而是 `Page Aggregate` 单一真源收口(标题/正文/页面设置 smoke 已通过)、`tree command cutover` 收尾、`tree realtime event stream` live cache 统一(WS push 主链 2026-05-17 上线,SSE 降级为 fallback)三条主线;AI 侧当前只做 Phase A(`mnote.doc.markdown_edit` + `mnote.doc.fetch` 增强,search/replace + `format: "markdown"` + local source),退役 `local_rule` planner 作为 Phase B;`review session`/流式 apply 属于 Phase C(设计冻结,当前不实施),不扩新 AI 功能,不把粗粒度 `mnote.page.save` 当成精确块编辑主入口。 +- 当前最优先的架构收口不是继续扩编辑器 UI,而是 `Page Aggregate` 单一真源收口(标题/正文/页面设置 smoke 已通过)、`tree command cutover` 收尾、`tree realtime event stream` live cache 统一(WS push 主链 2026-05-17 上线,SSE 降级为 fallback)三条主线;AI 侧简单正文编辑当前主路径已切到 `mnote.doc.markdown_edit`(模型生成 search/replace 或 full_content,经统一 mnote tool executor 执行;在线文档与本地 `.md` 共用合同),`mnote.doc.fetch` 负责读取上下文,`mnote.doc.apply_block_ops` / `mnote.block.*` 仅作为结构性辅助;`review session`/流式 apply 属于 Phase C(设计冻结,当前不实施),不扩新 AI 功能,不把粗粒度 `mnote.page.save` 当成精确块编辑主入口。 ## 组件定位 @@ -19,15 +19,15 @@ - `BlockNote` 是历史参考实现 / 对照材料,不再是运行时系统组件、默认回退编辑器或系统事实源。 - `Mindmap` 是 `tree-first graph` 的一种视图和编辑挂件,不是对象真相层。 - `OnlyOffice` 是独立页面型编辑器,不直接嵌入 `BlockNote` 画布;正文中通常通过附件块跳转进入。 -- `page-ai/block-edit-workflow` route 是当前简单编辑的 fast-path 入口(`local_rule` planner 为过渡实现);长期方向是将 AI 编辑主路径收敛到 `mnote.doc.markdown_edit`(markdown 级 search/replace,见 7-14),`mnote.block.*` 保留为结构性辅助。复杂任务仍走 Hermes agent。 +- `page-ai/block-edit-workflow` route 是当前简单编辑的 fast-path 入口;当前实现通过模型生成 markdown 级 search/replace / full_content 后调用 `mnote.doc.markdown_edit`,并复用统一 Hermes mnote tool executor;`mnote.block.*` 保留为结构性辅助。复杂任务仍走 Hermes agent。 - 树域三层模型:`Resource Tree`(kernel 对象组织真源)→ `File Tree`(主组织投影,`{title}.md` 为页面正文行)→ `Page Tree`(导航投影,不持有结构真相)。涉及资源归属优先走 Rust kernel 的 `KernelObjectIdentity` / `KernelProjectionResourceKind`。 ## 目录优先级 -- `/mnt/Data1T/mnote/wolai-frontend/src/` - 页面、编辑器、Sidebar、树视图、阅读态、交互壳默认先看这里。 -- `/mnt/Data1T/mnote/wolai-frontend/convex/` - 仍在使用的 Convex functions 与数据侧逻辑先看这里。 +- `/mnt/Data1T/mnote/recycle/wolai-frontend/src/` + wolai-frontend(Next.js React 前端)已退役移入 recycle,不作为当前实现依据。页面、编辑器、Sidebar、树视图、阅读态、交互壳默认先看 Rust mnote-web SSR(`rust/crates/mnote-web/src/`)。 +- `/mnt/Data1T/mnote/recycle/wolai-frontend/convex/` + 已随 wolai-frontend 移入 recycle。当前活跃 Convex functions 在 `infra/convex/` 与对应 Rust bridge 中。 - `/mnt/Data1T/mnote/rust/crates/core-protocol/` Kernel 类型、projection 协议、树/图核心术语先看这里。 - `/mnt/Data1T/mnote/rust/crates/bridge-runtime/` @@ -52,7 +52,7 @@ - 涉及页面新建、重命名、移动、归档、恢复、嵌入时,优先沿 `tree.*` 正式命名推进;`documents.*` 只视为兼容层,不应继续扩写为长期命令面。 - 涉及 AI 读取、定位或精确编辑页面块时,优先沿 `mnote.doc.*` / `mnote.block.*` Hermes tools 与 Rust `EditorCommand` 推进;`mnote.page.save` 只作为页面级兜底写入工具。 - 涉及资源归属(哪个页面拥有哪个 mindmap/附件)、object identity 和资源生命周期时,优先沿 Resource Tree → File Tree projection → `tree.resource.*` 命令面推进;`tree.*` 是正式命令面,`documents.*` 只视为兼容层。 -- 页面 AI 编辑长期主路径为 `mnote.doc.markdown_edit`(search/replace 或 full_content,markdown 文本级),在线文档和本地 `.md` 文件共用。当前过渡实现为 `POST /api/page-ai/block-edit-workflow` fast-path(`local_rule` planner,Phase B 退役)。`mnote.block.*` 保留为结构性辅助(拖拽排序等)。两层操作模型已获 CLI Main(Lark Doc)参考实现验证。流式 apply + suggest/review(参考 BlockNote AI)作为 Phase C 设计冻结,当前不实施。复杂任务进入 `/api/hermes/client/runs`。模型不直接产出 unified diff 或 blockId 操作;最终定位、校验和写入由 Rust runtime 负责。 +- 页面 AI 编辑当前主路径为 `mnote.doc.markdown_edit`(search/replace 或 full_content,markdown 文本级),在线文档和本地 `.md` 文件共用。`POST /api/page-ai/block-edit-workflow` 是简单编辑 fast-path,当前通过模型生成 markdown 编辑意图后调用统一 mnote tool executor;`mnote.block.*` 保留为结构性辅助(拖拽排序等)。两层操作模型已获 CLI Main(Lark Doc)参考实现验证。流式 apply + suggest/review(参考 BlockNote AI)作为 Phase C 设计冻结,当前不实施。复杂任务进入 `/api/hermes/client/runs`。模型不直接产出 unified diff 或 blockId 操作;最终定位、校验和写入由 Rust runtime 负责。 - 需要架构判断时,优先参考: - `/mnt/Data1T/mnote/ARCHITECTURE.md` - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md` @@ -65,7 +65,9 @@ - `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md - `design/03-rust-web/done/3-14-rust-web-tree-realtime-ws-push-v1.md` - `/mnt/Data1T/mnote/design/10-review/process/08-kernel-architecture-next-priority-review-and-checklist.md` - - `/mnt/Data1T/mnote/design/10-review/process/09-page-ai-fast-block-edit-runtime-review.md` + - `/mnt/Data1T/mnote/design/10-review/done/09-page-ai-fast-block-edit-runtime-review.md` + - `/mnt/Data1T/mnote/design/10-review/done/10-current-mnote-ai-runtime-review-v1.md` + - `/mnt/Data1T/mnote/design/10-review/done/11-current-full-architecture-review-v1.md` - `/mnt/Data1T/mnote/design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-v1.md` - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-24-resource-tree-filetree-pagetree-source-contract-checklist-v1.md` - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-12-main-editor-object-tab-resource-alignment-checklist-v1.md` @@ -100,8 +102,8 @@ ## 常用命令 - 根目录热启动:`npm run desktop:hot` -- 前端开发:`cd /mnt/Data1T/mnote/wolai-frontend && pnpm dev` -- 前端检查:`cd /mnt/Data1T/mnote/wolai-frontend && pnpm lint && pnpm test` +- 前端开发(已迁移至 Rust SSR):`npm run desktop:hot` 启动后直接访问 `http://localhost:3000` +- 前端检查:Rust 测试 `cargo test -p mnote-web`(Rust SSR 与 API),旧 React 前端已移入 recycle - 后端开发:`cd /mnt/Data1T/mnote/wolai-backend && uvicorn app.main:app --reload --port 8000` - Convex 自托管:参考 `/mnt/Data1T/mnote/infra/convex/README.md` @@ -152,4 +154,4 @@ - 所有新增或修改文件统一使用 UTF-8。 - TS/TSX 默认 2 空格缩进,Python 默认 4 空格缩进。 - 注释使用简体中文。 --当使用deepseek模型编程时,遇到疑难问题可以使用MCP求助codex,但需要注意codex回复比较慢,可能需要等待长一点的时间>3min。 \ No newline at end of file +-当使用deepseek模型编程时,遇到疑难问题可以使用MCP求助codex,但需要注意codex回复比较慢,可能需要等待长一点的时间>3min。 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 296ee529..ea114094 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -35,16 +35,16 @@ ## 2. 当前主线目录 -- `/mnt/Data1T/mnote/wolai-frontend/` - 当前 React island 与 legacy compat 代码来源;Next.js App Router 不再作为 3000 主入口,只在显式 legacy/debug 迁移边界继续服务重交互运行态与对照链。 +- `/mnt/Data1T/mnote/recycle/wolai-frontend/` + 已退役移入 recycle。React island 与 legacy compat 代码的历史来源;Next.js App Router 已被 Rust mnote-web SSR 替代,不再作为 3000 主入口。 - `/mnt/Data1T/mnote/rust/crates/core-protocol/` Kernel 类型、projection 协议、编辑器协议、树 / 图核心术语。 - `/mnt/Data1T/mnote/rust/crates/bridge-runtime/` Kernel query / command、bridge transport、Tiptap <-> editor document 适配、命令解释层。 - `/mnt/Data1T/mnote/rust/crates/mnote-web/` Rust Web route、kernel projection、documents/search/mindmap shell、Hermes bridge、stream transport、compat,以及显式开启时的 debug shell;它是当前主 Web gateway / shell / transport owner。 -- `/mnt/Data1T/mnote/wolai-frontend/convex/` - 仍在使用的 Convex functions 与数据侧逻辑。 +- `/mnt/Data1T/mnote/recycle/wolai-frontend/convex/` + 已随 wolai-frontend 移入 recycle。当前活跃 Convex functions 位于 `infra/convex/` 与 Rust bridge 中。 - `/mnt/Data1T/mnote/infra/convex/` Convex 自托管部署与基础设施。 - `/mnt/Data1T/mnote/wolai-backend/` @@ -52,97 +52,71 @@ ## 3. 当前页面运行结构 -### 3.1 根布局与 legacy island source +### 3.1 根布局与 legacy island source(已退役) -- `/mnt/Data1T/mnote/wolai-frontend/src/app/layout.tsx` +- `/mnt/Data1T/mnote/recycle/wolai-frontend/src/app/layout.tsx` -职责: - -- 作为 legacy Next / React island bundle source 的根布局 -- 保留 Convex / Query Provider 等前端 island 运行时依赖 -- 服务显式 legacy/debug/迁移边界 +该文件已随 wolai-frontend 移入 `recycle/`,不再作为当前实现依据。 注意: -> **3000 当前根入口与文档页 shell 由 `mnote-web` 持有;Next App Router 不再是 3000 主入口或页面根布局事实源。** +> **3000 当前根入口与文档页 shell 由 `mnote-web` Rust SSR 持有;Next App Router 已完全退役。** -### 3.2 工作区壳与 Sidebar +### 3.2 工作区壳与 Sidebar(已迁移至 Rust SSR) -- `/mnt/Data1T/mnote/wolai-frontend/src/components/app-layout-shell.tsx` -- `/mnt/Data1T/mnote/wolai-frontend/src/hooks/use-sidebar-data.ts` -- `/mnt/Data1T/mnote/wolai-frontend/src/lib/tree-stream/use-sidebar-tree-stream.ts` -- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/use-preferred-sidebar-snapshot.ts` +以下文件已随 wolai-frontend 移入 `recycle/`,不再作为当前实现依据: -当前 Sidebar 不是单一路径,而是三层组合: +- `/mnt/Data1T/mnote/recycle/wolai-frontend/src/components/app-layout-shell.tsx` +- `/mnt/Data1T/mnote/recycle/wolai-frontend/src/hooks/use-sidebar-data.ts` +- `/mnt/Data1T/mnote/recycle/wolai-frontend/src/lib/tree-stream/use-sidebar-tree-stream.ts` +- `/mnt/Data1T/mnote/recycle/wolai-frontend/src/components/sidebar/use-preferred-sidebar-snapshot.ts` -1. 初始 SSR / route snapshot -2. query / refetch snapshot -3. tree stream live snapshot - -`PreferredSidebarSnapshotProvider` 负责在 query 与 tree stream 之间做 freshness 选择,再把同一份 preferred snapshot 同时给: - -- `Sidebar` -- `Breadcrumb` -- 文档页头标题消费链 - -这说明当前工作区树链已经进入正式收口阶段:`3000` 主壳已直接接入 `/api/tree/events` 的 snapshot / delta / resync consumer,并有浏览器 smoke 验证;但 preferred snapshot、page subtree、filetree 与补偿链还没有完全统一成唯一 live cache。 +当前 Sidebar/工作区壳的当前实现在 Rust mnote-web SSR: +- 工作区壳渲染入口:`gateway.rs`(`root_entry`) +- 文档页壳/文档面板:`web_shell.rs`(`document_page_shell`、`build_document_panes_bootstrap_json`) +- Sidebar tree stream 消费:`layout.rs` 嵌入 JS(`startWithSse` / `startWithWebSocket`) +- FileTree 渲染与交互:`tree.rs` 嵌入 JS ## 4. 文档页主链 ### 4.1 入口 -- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/page-aggregate-loader.ts` -- `/mnt/Data1T/mnote/wolai-frontend/src/app/(app)/documents/[id]/page.tsx` +文档页入口的当前实现在 Rust mnote-web: -当前文档页读取主链已经不再由页面入口手工拼若干独立字段,而是: +- 文档页壳 SSR:`web_shell.rs`(`document_page_shell`),直接输出 HTML + bootstrap JSON +- Page Aggregate 快照 API:`web_shell.rs`(`page_aggregate`)返回 `/api/page-aggregate/:id` +- 前端桌面/窗口面板布局:`web_shell.rs`(`build_document_panes_bootstrap_json`) -1. 页面 SSR 入口通过 `page-aggregate-loader.ts` 直接请求 Rust `/api/page-aggregate/:id` -2. `DocumentContent` 的内容重试补拉也直接请求同一条 `/api/page-aggregate/:id` 正式读链 -3. 读取主链不再回退到 TS builder;snapshot 不可用或不可信时直接显式失败 -4. Next `/api/documents/page` 已退场为明确 `410` 的 compat 边界,不再参与文档页运行时主路径 +历史 wolai-frontend Next 文档页入口(已移入 `recycle/`): +- `/mnt/Data1T/mnote/recycle/wolai-frontend/src/lib/documents/page-aggregate-loader.ts` +- `/mnt/Data1T/mnote/recycle/wolai-frontend/src/app/(app)/documents/[id]/page.tsx` +- `/mnt/Data1T/mnote/recycle/wolai-frontend/src/lib/documents/page-aggregate.ts` -对应聚合类型定义: +### 4.2 文档壳(已迁移至 Rust SSR) -- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/page-aggregate.ts` +历史 React 文档壳组件(已随 wolai-frontend 移入 `recycle/`,不再作为当前实现): +- `/mnt/Data1T/mnote/recycle/wolai-frontend/src/components/editor/document-shell.tsx` +- `/mnt/Data1T/mnote/recycle/wolai-frontend/src/components/editor/document-content.tsx` -### 4.2 文档壳 +当前文档壳由 Rust mnote-web SSR 输出: +- `web_shell.rs` → `document_page_shell` 直接渲染 HTML shell,不在前端保留 React 文档壳 runtime。 -- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-shell.tsx` -- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx` +### 4.3 默认编辑器 host(已迁移至 WASM island) -`DocumentShell` 现在只是薄封装,文档页核心状态和交互壳集中在 `DocumentContent`。 +历史 React 编辑器 host(已移入 `recycle/`): +- `/mnt/Data1T/mnote/recycle/wolai-frontend/src/components/editor/editor-host-config.ts` +- `/mnt/Data1T/mnote/recycle/wolai-frontend/src/components/editor/editor-host.tsx` +- `/mnt/Data1T/mnote/recycle/wolai-frontend/src/components/editor/leptos-tiptap-island-editor-host.tsx` -### 4.3 默认编辑器 host - -- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/editor-host-config.ts` -- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/editor-host.tsx` - -当前默认 host 为: - -- `leptos_tiptap_island` - -文档页主路径已不再暴露显式 debug host 选择;历史 `iframe_debug` 宿主仅剩源码参考,不再参与页面 host 选择面。 - -### 4.4 正式主编辑器 - -- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/leptos-tiptap-island-editor-host.tsx` - -当前默认主编辑器已经是页面内 Leptos island,不再是 iframe bridge。 - -它负责: - -- 加载 Leptos/WASM 编辑器本体 -- 接收初始化内容与 page options -- 处理同页事件、命令、保存与状态同步 +当前默认主编辑器是页面内 Leptos/WASM island,由 Rust mnote-web SSR 加载: +- WASM 产物:`rust/spikes/leptos-tiptap-spike/` +- 加载路由:`web_shell.rs` → `leptos_tiptap_manifest` / `leptos_tiptap_asset` ### 4.5 BlockNote 的当前位置 - `/mnt/Data1T/mnote/recycle/wolai-frontend/src/components/editor/blocknote-editor.tsx` -`BlockNote` 当前仍保留回收副本,但其定位已变成: - -- 历史参考实现 -- 对照材料 / 调试素材 +`BlockNote` 已随 wolai-frontend 移入 recycle,仅作为历史对照材料,不参与当前实现。 而不是默认主编辑器方向。 @@ -197,7 +171,7 @@ ### 6.2 树命令现状 -- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/tree-command-client.ts` +- `/mnt/Data1T/mnote/recycle/wolai-frontend/src/lib/documents/tree-command-client.ts` - `/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/tree.rs` - `/mnt/Data1T/mnote/rust/crates/storage-convex-bridge/src/mapping.rs` diff --git a/CURRENT_ARCHITECTURE.md b/CURRENT_ARCHITECTURE.md index bfaa7ca1..3e99631f 100644 --- a/CURRENT_ARCHITECTURE.md +++ b/CURRENT_ARCHITECTURE.md @@ -36,25 +36,25 @@ - 正式命令面应落在 `tree.*` 与 `tree.resource.*`。 - `mnote-web` 同时暴露 `tree` 命令路由与兼容/过渡路由。 -- 当前最大冲突是 FileTree 资源行仍可能被当成页面命令对象处理,违反 Resource Tree / File Tree / Page Tree 分层。 +- FileTree 资源行已与页面命令对象隔离:资源 owner 只作为上下文,`data-document-id` / 页面命令目标只保留给真正页面行。 ### 2.4 Transport / Realtime 层 - Rust Web 已同时注册 `/api/tree/events`(SSE)与 `/api/realtime/ws`(WS)两条实时链路([routes/mod.rs:170-172](rust/crates/mnote-web/src/routes/mod.rs:170))。 - Rust SSR 主壳([layout.rs](rust/crates/mnote-web/src/ssr/pages/layout.rs:7126))已内置 WS 消费者(`startWithWebSocket`),并在 WS 断开后自动切 SSE fallback(`ws.onclose → startWithSseFallback`)。 -- 当前默认 `bootstrap.transport` 仍为 `convex-command-log-sse`,因此主壳默认走 SSE;WS 路径代码已就绪但尚未设为默认 transport,前端主消费链路仍未切换到 WS 默认。 -- `wolai-frontend`(Next.js legacy 侧)仅使用 SSE(`use-sidebar-tree-stream.ts`),无 WS 消费者。 -- SSE 内部还保留 push/polling 两种语义,WS 与 SSE 的 delta / snapshot 结构也未完全收敛。 +- 当前默认 `bootstrap.transport` 已切到 `convex-command-log-ws`,主壳先走 WS;SSE 保留为 fallback。 +- `recycle/wolai-frontend`(已退役的 Next.js legacy 侧)仅使用 SSE,无 WS 消费者。 +- WS 与 SSE 的 snapshot / delta 载荷合同已在 Rust 主壳消费层统一;后续重点是继续减少兼容 fallback 与 live cache 补偿链。 ### 2.5 Page Aggregate 层 - 文档页入口已经优先消费 Rust `page-aggregate` 快照。 -- 但前端仍保留 `PageAggregateClientState` reducer 和本地 `page tree` 投影。 -- 当前 page aggregate 是“Rust join + 前端局部状态”的过渡闭环,不是 EditorBlockDocument 原生单一真源闭环。 +- AI context 的 page subtree 已只读 Rust Page Aggregate 的稳定 projection,不再由前端本地构造第二份 page tree 真相。 +- 当前 page aggregate 仍是从 `documents.content` / local markdown content 投影出来的过渡闭环,不是 EditorBlockDocument 原生单一真源闭环。 ### 2.6 Editor Runtime 层 -- 前端壳:Rust mnote-web 独享 3000 入口,通过 SSR 输出 workspace shell、文档壳、Sidebar、tree 等完整 HTML([gateway.rs](rust/crates/mnote-web/src/routes/gateway.rs:156)、[web_shell.rs](rust/crates/mnote-web/src/routes/web_shell.rs:63))。Next.js 前端代码仍保留在 `wolai-frontend/`,但默认热启动不再作为运行时 daemon。 +- 前端壳:Rust mnote-web 独享 3000 入口,通过 SSR 输出 workspace shell、文档壳、Sidebar、tree 等完整 HTML([gateway.rs](rust/crates/mnote-web/src/routes/gateway.rs:156)、[web_shell.rs](rust/crates/mnote-web/src/routes/web_shell.rs:63))。Next.js 前端代码已随 wolai-frontend 整体移入 `recycle/`,不再作为运行时 daemon 维护。 - `leptos-tiptap` island 已是文档页默认编辑 host,以 WASM 形式由 Rust SSR 加载。 - 保存正文仍会经过 `documents/save` 兼容面。 - 这意味着编辑器体验已经切主,但写回语义还没有完全切到唯一主命令面。 @@ -62,7 +62,8 @@ ### 2.7 AI Runtime 层 - `mnote.doc.fetch`、`mnote.doc.markdown_edit`、`mnote.doc.apply_block_ops`、`page_ai_workflow`、Hermes / ACP / Reasonix 构成当前 AI 主链。 -- 这里存在最密集的合同漂移:工具权限、写入幂等、dryRun、revision 校验、manifest schema、tool guidance、runtime owner 都还没有统一。 +- 简单正文编辑主路径已统一为模型生成 search/replace / full_content → `mnote.doc.markdown_edit` → 统一 mnote tool executor;`mnote.doc.apply_block_ops` 保留为结构性块操作辅助。 +- 工具权限、dryRun、幂等、revision / conflictDetectionKey、manifest schema、tool guidance、ACP payload / response 等 P0 合同漂移已收口;Phase C 的 review session / 流式 apply 仍冻结。 ## 3. 当前成立的事实与过渡态 @@ -72,95 +73,92 @@ - Tree 主链已经从旧兼容入口退向 Rust Web。 - 文档页主编辑器已经切到 `leptos-tiptap` island。 - AI 页面编辑已经不再是纯前端本地逻辑。 -- **前端壳已切换到 Rust**:默认 `desktop:hot` 仅启动 Rust mnote-web 作为 3000 网关 owner,不再启动 Next.js (`wolai-frontend`) 作为运行时 daemon。Next.js 仅保留为 legacy compat fallback(`MNOTE_WEB_SKIP_GATEWAY=1`)与 build toolchain。 +- **前端壳已切换到 Rust**:默认 `desktop:hot` 仅启动 Rust mnote-web 作为 3000 网关 owner。`wolai-frontend`(Next.js 前端)已移至 `recycle/`,不再作为运行时 daemon 或 legacy fallback 维护。 ### 3.2 过渡态 - Page Aggregate 仍从 `documents.content` 侧 join 构造,而不是原生 EditorBlockDocument 真源。 -- 前端 Sidebar 仍有本地投影与多源抢真相。 -- `mnote.doc.markdown_edit` 已进入主线,但写入合同尚不完整。 -- ACP / Reasonix 已接入,但调用字段和运行时所有权仍不统一。 +- 页面正文写回仍经过兼容保存面,尚未完全切到唯一主命令面。 +- realtime / Page Aggregate / AI 写入的 P0 合同已基本收口,但兼容路由和历史 adapter 仍偏多。 ## 4. 架构冲突矩阵 ### 4.1 Tree realtime -- 事实:Rust Web 已注册 WS。 -- 事实:前端仍只见 SSE consumer。 -- 冲突:系统口径已升级,但前端主链未同步切换。 -- 影响:live cache、调试、回放与验收都会出现双口径。 +- 事实:Rust Web 同时注册 WS 与 SSE。 +- 事实:Rust SSR 主壳默认以 WS 为主链,SSE 是断线 fallback。 +- 当前状态:原“WS 文档口径 / 前端 SSE 实现”冲突已修复。 +- 剩余风险:live cache 与兼容 fallback 仍需要继续瘦身,避免未来再次出现多链路补偿。 ### 4.2 FileTree 资源行 - 事实:资源投影行带 `documentId`。 -- 事实:`tree.tsx` 相关路径会把它当作页面对象处理。 -- 冲突:资源树与页面树边界被突破。 -- 影响:误删、误重命名、误移动风险上升。 +- 事实:资源行的 owner document 与页面命令目标已分离,资源行不再暴露页面命令目标 ID。 +- 当前状态:原“资源行被当成页面命令对象处理”冲突已修复。 +- 剩余风险:资源对象的完整 `tree.resource.*` 命令面仍应继续补齐。 ### 4.3 Page Aggregate - 事实:Rust snapshot 已是入口事实。 -- 事实:前端 reducer 与本地 page tree 还在派生第二份状态。 -- 冲突:页面标题、正文、树投影可能各自取源。 -- 影响:AI context、Sidebar、正文显示不一定同源。 +- 事实:AI context 的 page subtree 已只读 Rust Page Aggregate projection,外部 AI 写入后会同步本地 aggregate script。 +- 当前状态:原“AI context / 文档页 / Rust Aggregate 多源抢真相”的 P0 问题已修复。 +- 剩余风险:Page Aggregate 仍从 `documents.content` 投影,不是 EditorBlockDocument 原生落库真相。 ### 4.4 AI 写入 -- 事实:`markdown_edit` 已是主推路径。 -- 事实:Hermes guidance、manifest、page_ai_workflow、apply_block_ops 仍在不同层保留旧习惯。 -- 冲突:同一条编辑链路里存在 markdown / block / tool executor 三套合同。 -- 影响:写入正确性、幂等性、回读一致性都不稳定。 +- 事实:`markdown_edit` 已是简单正文编辑主路径。 +- 事实:Hermes guidance、manifest、page_ai_workflow、tool executor、Reasonix ACP payload 已同步到同一合同。 +- 当前状态:原“markdown / block / tool executor 三套合同漂移”的 P0 问题已修复。 +- 剩余风险:复杂结构编辑仍应明确落到 `apply_block_ops` / `mnote.block.*`,Phase C review / streaming apply 尚未实施。 ## 5. 缺失功能 -- 前端树流仍缺统一的 WS 主 consumer。 -- Page Aggregate 仍缺前端第二真相收口。 -- FileTree 资源行仍缺严格的命令面隔离。 -- `mnote.doc.markdown_edit` 仍缺完整写入合同。 -- ACP / Reasonix 仍缺统一身份、会话、运行、幂等字段。 -- 兼容路由仍偏多,正式命令面与过渡面边界还不够硬。 +- Page Aggregate 仍缺 EditorBlockDocument 原生落库真相。 +- 正文保存仍经过 `documents/save` 兼容面,唯一主命令面尚未完全闭环。 +- `tree.resource.*` 仍需要补完整资源对象生命周期命令。 +- 兼容路由、历史 adapter 与 fallback 仍偏多,需要继续减小长期维护面。 ## 6. 推荐收口顺序 -1. 先收 `tree` 实时链路口径,让前端主消费与后端主发布一致。 -2. 再收 FileTree 资源行命令边界,避免资源对象误走页面命令。 -3. 再收 Page Aggregate 单一真源,减少 Sidebar / 文档页 / AI context 多源抢真相。 -4. 最后收 AI 写入合同,把 `markdown_edit`、`apply_block_ops`、Hermes / ACP / Reasonix 的调用协议统一起来。 +1. 先推进 Page Aggregate 原生 EditorBlockDocument 落库,减少 `documents.content` 投影过渡层。 +2. 再收正文保存主命令面,把 `documents/save` 兼容写入逐步迁到正式页面 / 编辑器命令。 +3. 再补齐 `tree.resource.*` 资源对象生命周期命令,避免资源操作长期停留在禁用或兼容态。 +4. 最后清理兼容路由、历史 adapter 与 fallback,使 Rust kernel / Rust Web / SSR 主壳的合同成为唯一运行口径。 ## 7. 相关审查与缺陷 -- [设计审查:当前 mnote 项目 AI / Page Aggregate 定向 Review](./design/10-review/process/10-current-mnote-ai-runtime-review-v1.md) -- [设计审查:当前完整架构 Review](./design/10-review/process/11-current-full-architecture-review-v1.md) +- [设计审查:当前 mnote 项目 AI / Page Aggregate 定向 Review](./design/10-review/done/10-current-mnote-ai-runtime-review-v1.md) +- [设计审查:当前完整架构 Review](./design/10-review/done/11-current-full-architecture-review-v1.md) ## 8. 本次落档缺陷索引 ### 8.1 Rust Web / Realtime / ACP -- [3-16 tree realtime WS 主链口径与前端 SSE 实现不一致](./bugs/03-rust-web/process/3-16-tree-realtime-ws-sse-doc-contract-drift-v1.md) -- [3-17 SSE push 模式跳过 polling safety net](./bugs/03-rust-web/process/3-17-sse-push-skips-polling-fallback-v1.md) -- [3-18 WS 与 SSE delta 载荷合同分裂](./bugs/03-rust-web/process/3-18-ws-sse-delta-contract-split-v1.md) -- [3-19 Reasonix ACP wrapper 调 mnote tool 缺少身份与幂等字段](./bugs/03-rust-web/process/3-19-acp-reasonix-tool-call-missing-identity-fields-v1.md) -- [3-20 ACP incoming request 只记录日志不响应](./bugs/03-rust-web/process/3-20-acp-request-permission-no-response-v1.md) -- [3-21 ACP run payload 被第一次 stream_events 消费后移除](./bugs/03-rust-web/process/3-21-acp-run-payload-consumed-and-removed-v1.md) +- [3-16 tree realtime WS 主链口径与前端 SSE 实现不一致](./bugs/03-rust-web/done/3-16-tree-realtime-ws-sse-doc-contract-drift-v1.md) +- [3-17 SSE push 模式跳过 polling safety net](./bugs/03-rust-web/done/3-17-sse-push-skips-polling-fallback-v1.md) +- [3-18 WS 与 SSE delta 载荷合同分裂](./bugs/03-rust-web/done/3-18-ws-sse-delta-contract-split-v1.md) +- [3-19 Reasonix ACP wrapper 调 mnote tool 缺少身份与幂等字段](./bugs/03-rust-web/done/3-19-acp-reasonix-tool-call-missing-identity-fields-v1.md) +- [3-20 ACP incoming request 只记录日志不响应](./bugs/03-rust-web/done/3-20-acp-request-permission-no-response-v1.md) +- [3-21 ACP run payload 被第一次 stream_events 消费后移除](./bugs/03-rust-web/done/3-21-acp-run-payload-consumed-and-removed-v1.md) ### 8.2 Tree Domain -- [4-46 FileTree 资源行被当成页面命令对象处理](./bugs/04-tree-domain/process/4-46-filetree-resource-row-document-command-leak-v1.md) +- [4-46 FileTree 资源行被当成页面命令对象处理](./bugs/04-tree-domain/done/4-46-filetree-resource-row-document-command-leak-v1.md) ### 8.3 Editor Mainline -- [5-15 PageAggregateClientState 仍在前端生成第二份 page tree 真相](./bugs/05-editor-mainline/process/5-15-page-aggregate-client-state-second-truth-v1.md) -- [5-16 Sidebar preferred snapshot 中 query 可覆盖 live stream](./bugs/05-editor-mainline/process/5-16-sidebar-preferred-snapshot-query-overrides-live-stream-v1.md) -- [5-17 文档页标题优先 liveSidebarTitle 而非 Page Aggregate head](./bugs/05-editor-mainline/process/5-17-document-title-source-drift-live-sidebar-over-head-v1.md) -- [5-18 AI 写正文后本地 Page Aggregate content 可能不刷新](./bugs/05-editor-mainline/process/5-18-ai-write-body-does-not-sync-page-aggregate-content-v1.md) +- [5-15 PageAggregateClientState 仍在前端生成第二份 page tree 真相](./bugs/05-editor-mainline/done/5-15-page-aggregate-client-state-second-truth-v1.md) +- [5-16 Sidebar preferred snapshot 中 query 可覆盖 live stream](./bugs/05-editor-mainline/done/5-16-sidebar-preferred-snapshot-query-overrides-live-stream-v1.md) +- [5-17 文档页标题优先 liveSidebarTitle 而非 Page Aggregate head](./bugs/05-editor-mainline/done/5-17-document-title-source-drift-live-sidebar-over-head-v1.md) +- [5-18 AI 写正文后本地 Page Aggregate content 可能不刷新](./bugs/05-editor-mainline/done/5-18-ai-write-body-does-not-sync-page-aggregate-content-v1.md) ### 8.4 AI -- [7-18 AI markdown_edit 阶段状态合同漂移](./bugs/07-ai/process/7-18-ai-markdown-edit-phase-state-contract-drift-v1.md) -- [7-19 Hermes 工具指导仍优先 apply_block_ops 而非 markdown_edit](./bugs/07-ai/process/7-19-hermes-tool-guidance-markdown-edit-contract-drift-v1.md) -- [7-20 page_ai_workflow 绕过 Hermes tool executor / audit / toggle](./bugs/07-ai/process/7-20-page-ai-workflow-bypasses-hermes-tool-executor-v1.md) -- [7-21 mnote.doc.markdown_edit 本地文件写入绕过 dryRun / idempotency](./bugs/07-ai/process/7-21-markdown-edit-local-write-contract-bypass-v1.md) -- [7-22 mnote.doc.apply_block_ops 批量写入缺少 revision / conflictDetectionKey 校验](./bugs/07-ai/process/7-22-apply-block-ops-missing-write-preconditions-v1.md) -- [7-23 mnote.doc.markdown_edit manifest schema 缺少 required 与写入字段](./bugs/07-ai/process/7-23-markdown-edit-manifest-schema-contract-drift-v1.md) -- [7-24 在线 markdown_edit 写回不以最终 Markdown 为真源](./bugs/07-ai/process/7-24-markdown-edit-online-write-does-not-use-final-markdown-v1.md) -- [7-25 ACP Reasonix 页面 AI block_edit_workflow 在 markdown 命中后生成空 block_ops](./bugs/07-ai/process/7-25-reasonix-block-edit-workflow-empty-block-ops-after-markdown-match-v1.md) +- [7-18 AI markdown_edit 阶段状态合同漂移](./bugs/07-ai/done/7-18-ai-markdown-edit-phase-state-contract-drift-v1.md) +- [7-19 Hermes 工具指导仍优先 apply_block_ops 而非 markdown_edit](./bugs/07-ai/done/7-19-hermes-tool-guidance-markdown-edit-contract-drift-v1.md) +- [7-20 page_ai_workflow 绕过 Hermes tool executor / audit / toggle](./bugs/07-ai/done/7-20-page-ai-workflow-bypasses-hermes-tool-executor-v1.md) +- [7-21 mnote.doc.markdown_edit 本地文件写入绕过 dryRun / idempotency](./bugs/07-ai/done/7-21-markdown-edit-local-write-contract-bypass-v1.md) +- [7-22 mnote.doc.apply_block_ops 批量写入缺少 revision / conflictDetectionKey 校验](./bugs/07-ai/done/7-22-apply-block-ops-missing-write-preconditions-v1.md) +- [7-23 mnote.doc.markdown_edit manifest schema 缺少 required 与写入字段](./bugs/07-ai/done/7-23-markdown-edit-manifest-schema-contract-drift-v1.md) +- [7-24 在线 markdown_edit 写回不以最终 Markdown 为真源](./bugs/07-ai/done/7-24-markdown-edit-online-write-does-not-use-final-markdown-v1.md) +- [7-25 ACP Reasonix 页面 AI block_edit_workflow 在 markdown 命中后生成空 block_ops](./bugs/07-ai/done/7-25-reasonix-block-edit-workflow-empty-block-ops-after-markdown-match-v1.md) diff --git a/bugs/03-rust-web/process/3-16-tree-realtime-ws-sse-doc-contract-drift-v1.md b/bugs/03-rust-web/done/3-16-tree-realtime-ws-sse-doc-contract-drift-v1.md similarity index 56% rename from bugs/03-rust-web/process/3-16-tree-realtime-ws-sse-doc-contract-drift-v1.md rename to bugs/03-rust-web/done/3-16-tree-realtime-ws-sse-doc-contract-drift-v1.md index 4d53d641..4cad0434 100644 --- a/bugs/03-rust-web/process/3-16-tree-realtime-ws-sse-doc-contract-drift-v1.md +++ b/bugs/03-rust-web/done/3-16-tree-realtime-ws-sse-doc-contract-drift-v1.md @@ -1,8 +1,8 @@ -# 3-16 [process][bug] tree realtime WS 主链口径与前端 SSE 实现不一致 v1 +# 3-16 [done][bug] tree realtime WS 主链口径与前端 SSE 实现不一致 v1 > 发现时间:2026-05-17 > -> 状态:`[process]` +> 状态:`[done]` > > 关联主线:`03-rust-web` @@ -29,6 +29,19 @@ - 增加浏览器 smoke,断言网络中实际使用的是 WS 主链还是 SSE fallback。 - 文档与测试必须以真实前端消费路径为准。 -## 5. 状态 +## 5. 修复 -已确认口径与前端源码不一致,尚未修复。 +已修复 / 已收口: + +- [layout.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/ssr/pages/layout.rs:7127) Rust SSR 主壳内置 `startWithWebSocket`,实际创建 `/api/realtime/ws` WebSocket consumer。 +- [layout.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/ssr/pages/layout.rs:7176) WebSocket 断开后调用 `startWithSseFallback`,`/api/tree/events` 明确成为 fallback。 +- [layout.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/ssr/pages/layout.rs:7301) SSR tree live bootstrap 默认 `transport` 已是 `convex-command-log-ws`,不再默认走 SSE。 +- [layout.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/ssr/pages/layout.rs:7593) 增强回归测试,断言 controller 同时包含 WS 主链与 SSE fallback。 + +## 6. 验证 + +```bash +cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_live_controller_marks_transport_and_closes_source_on_pagehide -- --nocapture +``` + +结果:1 个测试通过。 diff --git a/bugs/03-rust-web/process/3-17-sse-push-skips-polling-fallback-v1.md b/bugs/03-rust-web/done/3-17-sse-push-skips-polling-fallback-v1.md similarity index 53% rename from bugs/03-rust-web/process/3-17-sse-push-skips-polling-fallback-v1.md rename to bugs/03-rust-web/done/3-17-sse-push-skips-polling-fallback-v1.md index e62b626c..8511ee6e 100644 --- a/bugs/03-rust-web/process/3-17-sse-push-skips-polling-fallback-v1.md +++ b/bugs/03-rust-web/done/3-17-sse-push-skips-polling-fallback-v1.md @@ -1,8 +1,8 @@ -# 3-17 [process][bug] SSE push 模式跳过 polling safety net v1 +# 3-17 [done][bug] SSE push 模式跳过 polling safety net v1 > 发现时间:2026-05-17 > -> 状态:`[process]` +> 状态:`[done]` > > 关联主线:`03-rust-web` @@ -27,6 +27,19 @@ SSE route 注释和架构口径暗示 polling 可作为 safety net,但实现 - 增加测试:模拟 `stream_delta_rx` 无消息但 bridgeLogs 有新事件,SSE 应产出 delta/resync。 - 明确 `pollMs` 与 push channel 同时存在时的优先级。 -## 5. 状态 +## 5. 修复 -已确认 push 分支跳过 polling fallback,尚未修复。 +已修复: + +- [sse.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/sse.rs:117) push 模式在 broadcast 静默时不再只发 heartbeat,而是按 `pollMs/maxPolls` 执行 bridgeLogs polling safety net。 +- [sse.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/sse.rs:135) polling 检测到 `Delta` 时构建标准 stream delta payload 并发送 `delta`。 +- [sse.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/sse.rs:157) polling 检测到 `Resync` 时回读 snapshot 并发送 `resync`。 +- [sse.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/sse.rs:402) 增加回归测试,断言 push 模式下 `maxPolls=1` 会触发 safety-net polling 并正常结束。 + +## 6. 验证 + +```bash +cargo test --manifest-path rust/Cargo.toml -p mnote-web routes::sse::tests -- --nocapture +``` + +结果:5 个 SSE route 测试通过。 diff --git a/bugs/03-rust-web/process/3-18-ws-sse-delta-contract-split-v1.md b/bugs/03-rust-web/done/3-18-ws-sse-delta-contract-split-v1.md similarity index 51% rename from bugs/03-rust-web/process/3-18-ws-sse-delta-contract-split-v1.md rename to bugs/03-rust-web/done/3-18-ws-sse-delta-contract-split-v1.md index add7d23a..d3c5ab84 100644 --- a/bugs/03-rust-web/process/3-18-ws-sse-delta-contract-split-v1.md +++ b/bugs/03-rust-web/done/3-18-ws-sse-delta-contract-split-v1.md @@ -1,8 +1,8 @@ -# 3-18 [process][bug] WS 与 SSE delta 载荷合同分裂 v1 +# 3-18 [done][bug] WS 与 SSE delta 载荷合同分裂 v1 > 发现时间:2026-05-17 > -> 状态:`[process]` +> 状态:`[done]` > > 关联主线:`03-rust-web` @@ -28,6 +28,20 @@ SSE polling 可以从 bridgeLogs / domainEvents 解析 `streamDelta` 并附带 s - 对同一命令分别跑 WS / SSE 合同测试,断言事件 kind、cursor、snapshot、resync 语义一致。 - 前端 reducer 只消费统一 envelope,不感知 transport 差异。 -## 5. 状态 +## 5. 修复 -已确认 WS / SSE 事件结构不统一,尚未修复。 +已修复: + +- [stream_support.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/stream_support.rs:577) 新增 `build_stream_push_delta_hint`,统一 command committed broadcast 的 tree realtime hint envelope。 +- [sse.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/sse.rs:97) SSE push 分支改为复用统一 hint envelope。 +- [ws.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/ws.rs:43) WS broadcast 分支改为发送同一 hint envelope,不再输出嵌套 `data.kind=command_committed` 的专有结构。 +- [ws.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/ws.rs:190) 增加 WS delta serializer 合同测试,断言输出为 `kind=delta`、`hint=command_committed`,并且没有额外 `data` 外壳。 + +## 6. 验证 + +```bash +cargo test --manifest-path rust/Cargo.toml -p mnote-web routes::ws::tests -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web routes::sse::tests -- --nocapture +``` + +结果:WS 3 个测试通过,SSE 5 个测试通过。 diff --git a/bugs/03-rust-web/done/3-19-acp-reasonix-tool-call-missing-identity-fields-v1.md b/bugs/03-rust-web/done/3-19-acp-reasonix-tool-call-missing-identity-fields-v1.md new file mode 100644 index 00000000..53c01048 --- /dev/null +++ b/bugs/03-rust-web/done/3-19-acp-reasonix-tool-call-missing-identity-fields-v1.md @@ -0,0 +1,49 @@ +# 3-19 [done][bug] Reasonix ACP wrapper 调 mnote tool 缺少身份与幂等字段 v1 + +> 发现时间:2026-05-17 +> +> 状态:`[done]` +> +> 关联主线:`03-rust-web` + +## 1. 问题定义 + +Reasonix ACP wrapper 调用 mnote tool 时只发送 `toolName`、`args`、`workspaceId`,没有传递 `actorId`、`sessionId`、`runId`、`toolCallId`、`traceId`、`dryRun`、`idempotencyKey` 等工具合同字段。 + +## 2. 证据 + +- [reasonix-acp-wrapper.mjs](/mnt/Data1T/mnote/scripts/reasonix-acp-wrapper.mjs:230) 附近构造 `callMnoteTool` 请求体。 + +## 3. 影响 + +- Rust tool executor 难以稳定进行鉴权、审计、追踪和幂等。 +- 写工具可能被拒绝,或在缺少幂等键时产生重复写入风险。 +- Reasonix 与 Hermes 对同一 tool 的调用合同不一致。 + +## 4. 建议修复 + +- wrapper 必须从 ACP run/session 中透传 actor、session、run、toolCall、trace、dryRun、idempotency 字段。 +- 写工具缺失必要字段时应 fail fast。 +- 增加 Reasonix 端到端 smoke,断言请求字段完整。 + +## 5. 修复 + +已修复: + +- [reasonix-acp-wrapper.mjs](/mnt/Data1T/mnote/scripts/reasonix-acp-wrapper.mjs:34) 新增 `buildMnoteToolPayload`,统一构造 mnote tool 请求体。 +- [reasonix-acp-wrapper.mjs](/mnt/Data1T/mnote/scripts/reasonix-acp-wrapper.mjs:43) tool payload 现在包含 `actorId`、`sessionId`、`runId`、`toolCallId`、`traceId`、`dryRun`、`idempotencyKey`、`workspaceId`、`documentId`。 +- [reasonix-acp-wrapper.mjs](/mnt/Data1T/mnote/scripts/reasonix-acp-wrapper.mjs:303) HTTP 请求头同步携带 `x-mnote-actor-id` 与 `x-mnote-workspace-id`。 +- [reasonix-acp-wrapper.mjs](/mnt/Data1T/mnote/scripts/reasonix-acp-wrapper.mjs:379) ACP `session/prompt` 期间通过 `AsyncLocalStorage` 保存 mnote tool context,tool 调用继承 run/session/actor/trace。 +- [acp_types.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/acp_types.rs:163) `SessionPromptParams` 扩展可选 mnote 上下文字段。 +- [acp_session_manager.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/acp_session_manager.rs:193) 增加 `run_prompt_with_mnote_context`。 +- [hermes_client.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_client.rs:754) ACP Reasonix prompt 启动时透传 mnote session/run/actor/trace/workspace/document 上下文。 + +## 6. 验证 + +```bash +node --check scripts/reasonix-acp-wrapper.mjs +MNOTE_REASONIX_ACP_SELFTEST=1 node scripts/reasonix-acp-wrapper.mjs +cargo test --manifest-path rust/Cargo.toml -p mnote-web acp_session_manager -- --nocapture +``` + +结果:JS 语法检查通过,wrapper selftest 通过,ACP session manager 5 个测试通过。 diff --git a/bugs/03-rust-web/done/3-20-acp-request-permission-no-response-v1.md b/bugs/03-rust-web/done/3-20-acp-request-permission-no-response-v1.md new file mode 100644 index 00000000..2cb697ed --- /dev/null +++ b/bugs/03-rust-web/done/3-20-acp-request-permission-no-response-v1.md @@ -0,0 +1,40 @@ +# 3-20 [done][bug] ACP incoming request 只记录日志不响应 v1 + +> 发现时间:2026-05-17 +> +> 状态:`[done]` +> +> 关联主线:`03-rust-web` + +## 1. 问题定义 + +ACP client 收到 `session/request_permission` 等 incoming request 时当前只记录日志,没有返回协议响应。 + +## 2. 证据 + +- [acp_client.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/acp_client.rs:347) 附近处理 incoming request。 + +## 3. 影响 + +- ACP agent 侧可能等待到超时。 +- 权限请求无法进入用户确认或自动拒绝流程。 +- run 状态可能卡在进行中,SSE 输出也无法表达真实阻塞原因。 + +## 4. 建议修复 + +- 为已知 request type 实现明确响应:允许、拒绝或要求人工确认。 +- 未支持的 request type 也应返回结构化 error,而不是只日志。 +- 增加 ACP request/response 协议测试。 + +## 5. 修复 + +- [acp_client.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/acp_client.rs:157) 将 ACP 子进程 stdin writer 作为共享 `Arc>>` 传入后台 reader loop。 +- [acp_client.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/acp_client.rs:335) 对带 `id + method` 的 incoming request 写回 JSON-RPC error 响应,当前无权限确认 UI 时明确拒绝,避免 agent 等待超时。 +- [acp_client.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/acp_client.rs:551) 增加 mock ACP server 测试,覆盖 `session/request_permission` 主动请求必须收到响应。 + +## 6. 验证 + +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web test_incoming_permission_request_gets_response -- --nocapture` + - 结果:1 passed +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web acp_client -- --nocapture` + - 结果:6 passed diff --git a/bugs/03-rust-web/process/3-21-acp-run-payload-consumed-and-removed-v1.md b/bugs/03-rust-web/done/3-21-acp-run-payload-consumed-and-removed-v1.md similarity index 56% rename from bugs/03-rust-web/process/3-21-acp-run-payload-consumed-and-removed-v1.md rename to bugs/03-rust-web/done/3-21-acp-run-payload-consumed-and-removed-v1.md index 129b7db5..355d114b 100644 --- a/bugs/03-rust-web/process/3-21-acp-run-payload-consumed-and-removed-v1.md +++ b/bugs/03-rust-web/done/3-21-acp-run-payload-consumed-and-removed-v1.md @@ -1,8 +1,8 @@ -# 3-21 [process][bug] ACP run payload 被第一次 stream_events 消费后移除 v1 +# 3-21 [done][bug] ACP run payload 被第一次 stream_events 消费后移除 v1 > 发现时间:2026-05-17 > -> 状态:`[process]` +> 状态:`[done]` > > 关联主线:`03-rust-web` @@ -27,6 +27,12 @@ ACP run 的 payload 在 `stream_events` 路径被消费后从内存表移除。 - stream subscription 不应拥有 payload 的删除权。 - 增加 SSE 断线重连与重复订阅测试。 -## 5. 状态 +## 5. 修复 -已确认 payload 生命周期与 stream 订阅耦合,尚未修复。 +- [hermes_client.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_client.rs:2051) `acp_run_payload_for_stream` 使用 `ACP_RUN_PAYLOADS.get(run_id).cloned()`,stream subscription 只读取 payload,不再拥有删除权。 +- [hermes_client.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_client.rs:4116) 增加重复读取测试,锁定刷新、重连、多个观察者不会因为第一次读取而丢失 run payload。 + +## 6. 验证 + +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_client_acp_stream_payload_lookup_keeps_payload_for_reconnect -- --nocapture` + - 结果:1 passed diff --git a/bugs/03-rust-web/done/3-22-acp-permission-auto-deny-ui-shows-approval-actions-v1.md b/bugs/03-rust-web/done/3-22-acp-permission-auto-deny-ui-shows-approval-actions-v1.md new file mode 100644 index 00000000..b51088b1 --- /dev/null +++ b/bugs/03-rust-web/done/3-22-acp-permission-auto-deny-ui-shows-approval-actions-v1.md @@ -0,0 +1,50 @@ +# 3-22 [done][bug] ACP permission 自动拒绝后 UI 仍展示审批动作 + +> 发现时间:2026-05-18 +> +> 状态:`[done]` +> +> 关联主线:`03-rust-web` / ACP runtime 与页面 AI 事件映射 + +## 1. 问题定义 + +当前 ACP client 收到 `session/request_permission` 后会立即返回 JSON-RPC error,并把事件转换为 `permission.denied`。这是为了避免 agent 等待权限响应直到超时。 + +但页面 AI UI 仍在权限卡片和权限弹窗里展示“允许 / 拒绝”按钮。即使这些按钮在 `permission.denied` 下会被禁用,用户仍会看到审批入口,误以为可以把真实协议结果改成允许。 + +## 2. 证据 + +- [acp_client.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/acp_client.rs:350) 对 incoming `id + method` request 直接返回 JSON-RPC error。 +- [acp_session_manager.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/acp_session_manager.rs:121) 对 `session/request_permission` 生成 `decision="denied"`。 +- [acp_bridge.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/acp_bridge.rs:237) 将 decision 映射为 `permission.denied`。 +- [layout.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/ssr/pages/layout.rs:4060) 权限弹窗仍渲染“允许 / 拒绝”按钮。 +- [layout.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/ssr/pages/layout.rs:5805) 权限消息卡片仍渲染“允许 / 拒绝”按钮。 + +## 3. 影响 + +- 用户看到的“允许”动作与 ACP wire contract 不一致。 +- 点击动作只修改本地 UI 状态,不会回写 ACP JSON-RPC response。 +- 对已经自动拒绝的权限请求展示审批按钮,会掩盖真实失败原因。 + +## 4. 修复 + +- [acp_client.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/acp_client.rs:350) 维持 `session/request_permission` 自动拒绝协议响应,避免 agent 等待超时。 +- [layout.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/ssr/pages/layout.rs:4003) `pageAiApplyPermissionEvent()` 对 `permission.denied` / `permission.allowed` 只保留结果态,不再展示可点击审批弹窗。 +- [layout.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/ssr/pages/layout.rs:4048) `pageAiShowPermissionDialog()` 对已决权限事件直接隐藏对话框。 +- [layout.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/ssr/pages/layout.rs:5797) 权限消息卡片对已决事件只展示结果,不再渲染审批按钮。 + +## 5. 验证 + +```bash +cargo test --manifest-path rust/Cargo.toml -p mnote-web page_ai_uses_backend_acp_session_runtime_store -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web acp_permission_request_emits_frontend_decision_event -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web ssr::pages::layout::tests -- --nocapture +cargo fmt --manifest-path rust/Cargo.toml --all -- --check +git diff --check -- rust/crates/mnote-web/src/ssr/pages/layout.rs bugs/03-rust-web/done/3-22-acp-permission-auto-deny-ui-shows-approval-actions-v1.md +``` + +结果: + +- `page_ai_uses_backend_acp_session_runtime_store`:`1 passed`。 +- `acp_permission_request_emits_frontend_decision_event`:`1 passed`。 +- 版面相关静态测试继续通过,权限消息代码路径未破坏。 diff --git a/bugs/03-rust-web/process/3-19-acp-reasonix-tool-call-missing-identity-fields-v1.md b/bugs/03-rust-web/process/3-19-acp-reasonix-tool-call-missing-identity-fields-v1.md deleted file mode 100644 index fdf9db3d..00000000 --- a/bugs/03-rust-web/process/3-19-acp-reasonix-tool-call-missing-identity-fields-v1.md +++ /dev/null @@ -1,31 +0,0 @@ -# 3-19 [process][bug] Reasonix ACP wrapper 调 mnote tool 缺少身份与幂等字段 v1 - -> 发现时间:2026-05-17 -> -> 状态:`[process]` -> -> 关联主线:`03-rust-web` - -## 1. 问题定义 - -Reasonix ACP wrapper 调用 mnote tool 时只发送 `toolName`、`args`、`workspaceId`,没有传递 `actorId`、`sessionId`、`runId`、`toolCallId`、`traceId`、`dryRun`、`idempotencyKey` 等工具合同字段。 - -## 2. 证据 - -- [reasonix-acp-wrapper.mjs](/mnt/Data1T/mnote/scripts/reasonix-acp-wrapper.mjs:230) 附近构造 `callMnoteTool` 请求体。 - -## 3. 影响 - -- Rust tool executor 难以稳定进行鉴权、审计、追踪和幂等。 -- 写工具可能被拒绝,或在缺少幂等键时产生重复写入风险。 -- Reasonix 与 Hermes 对同一 tool 的调用合同不一致。 - -## 4. 建议修复 - -- wrapper 必须从 ACP run/session 中透传 actor、session、run、toolCall、trace、dryRun、idempotency 字段。 -- 写工具缺失必要字段时应 fail fast。 -- 增加 Reasonix 端到端 smoke,断言请求字段完整。 - -## 5. 状态 - -已确认 wrapper 请求字段不完整,尚未修复。 diff --git a/bugs/03-rust-web/process/3-20-acp-request-permission-no-response-v1.md b/bugs/03-rust-web/process/3-20-acp-request-permission-no-response-v1.md deleted file mode 100644 index 3c7de742..00000000 --- a/bugs/03-rust-web/process/3-20-acp-request-permission-no-response-v1.md +++ /dev/null @@ -1,31 +0,0 @@ -# 3-20 [process][bug] ACP incoming request 只记录日志不响应 v1 - -> 发现时间:2026-05-17 -> -> 状态:`[process]` -> -> 关联主线:`03-rust-web` - -## 1. 问题定义 - -ACP client 收到 `session/request_permission` 等 incoming request 时当前只记录日志,没有返回协议响应。 - -## 2. 证据 - -- [acp_client.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/acp_client.rs:347) 附近处理 incoming request。 - -## 3. 影响 - -- ACP agent 侧可能等待到超时。 -- 权限请求无法进入用户确认或自动拒绝流程。 -- run 状态可能卡在进行中,SSE 输出也无法表达真实阻塞原因。 - -## 4. 建议修复 - -- 为已知 request type 实现明确响应:允许、拒绝或要求人工确认。 -- 未支持的 request type 也应返回结构化 error,而不是只日志。 -- 增加 ACP request/response 协议测试。 - -## 5. 状态 - -已确认 incoming request 缺少响应处理,尚未修复。 diff --git a/bugs/04-tree-domain/done/4-36-local-folder-cloud-switch-empty-workspace-v1.md b/bugs/04-tree-domain/done/4-36-local-folder-cloud-switch-empty-workspace-v1.md index 03bace96..f83f21d2 100644 --- a/bugs/04-tree-domain/done/4-36-local-folder-cloud-switch-empty-workspace-v1.md +++ b/bugs/04-tree-domain/done/4-36-local-folder-cloud-switch-empty-workspace-v1.md @@ -1,9 +1,9 @@ -# 4-36 [process][bug] 本地文件夹切回云空间后进入空白 workspace v1 +# 4-36 [done][bug] 本地文件夹切回云空间后进入空白 workspace v1 > 更新时间:2026-05-15 > > 分类归属: -> - `04-tree-domain/process` +> - `04-tree-domain/done` > - 涉及边界:workspace source switch、本地文件夹入口、Convex workspace 恢复、Sidebar File Tree/Page Tree projection > > 用户反馈: diff --git a/bugs/04-tree-domain/process/4-46-filetree-resource-row-document-command-leak-v1.md b/bugs/04-tree-domain/done/4-46-filetree-resource-row-document-command-leak-v1.md similarity index 57% rename from bugs/04-tree-domain/process/4-46-filetree-resource-row-document-command-leak-v1.md rename to bugs/04-tree-domain/done/4-46-filetree-resource-row-document-command-leak-v1.md index 617af938..db5dc64a 100644 --- a/bugs/04-tree-domain/process/4-46-filetree-resource-row-document-command-leak-v1.md +++ b/bugs/04-tree-domain/done/4-46-filetree-resource-row-document-command-leak-v1.md @@ -1,8 +1,10 @@ -# 4-46 [process][bug] FileTree 资源行被当成页面命令对象处理 v1 +# 4-46 [done][bug] FileTree 资源行被当成页面命令对象处理 v1 > 发现时间:2026-05-17 > -> 状态:`[process]` +> 更新时间:2026-05-17 +> +> 状态:`[done]` > > 关联主线:`04-tree-domain` @@ -34,4 +36,26 @@ FileTree asset row 的 `resourceMeta` 同时带 `documentId` 和 `assetId`,但 ## 5. 状态 -已确认 row model 与命令分发存在泄漏,尚未修复。 +已修复。 + +## 6. 修复内容 + +- `getFileTreeRowDocumentId()` 不再从 `resourceMeta.documentId` 读取资源 owner,只返回真正可作为页面命令目标的 document / markdown / index 行 ID。 +- 新增 `getFileTreeRowOwnerDocumentId()`,资源打开与 owner 上下文继续使用 owner document ID。 +- FileTree 资源行不再通过 F2 或行内编辑按钮进入页面重命名。 +- 动态 filetree row DOM 增加 `data-owner-document-id`,`data-document-id` 仅表达页面命令目标。 +- 初始 SSR filetree HTML 同步区分 `data-document-id` 与 `data-owner-document-id`,asset row 的 `data-document-id` / `data-doc-id` 保持为空。 + +## 7. 验证 + +```bash +cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_shell::filetree_renderer::tests::tree_shell_filetree_renderer_outputs_initial_nested_html_contract -- --nocapture +``` + +结果:`1 passed`。 + +```bash +cargo test --manifest-path rust/Cargo.toml -p mnote-web routes::tree::tests::tree_shell_filetree_mode_embeds_asset_state -- --nocapture +``` + +结果:`1 passed`。 diff --git a/bugs/04-tree-domain/done/4-47-stream-support-retired-resource-delete-command-v1.md b/bugs/04-tree-domain/done/4-47-stream-support-retired-resource-delete-command-v1.md new file mode 100644 index 00000000..b060aed2 --- /dev/null +++ b/bugs/04-tree-domain/done/4-47-stream-support-retired-resource-delete-command-v1.md @@ -0,0 +1,43 @@ +# 4-47 [done][bug] stream_support 回归样例仍保留退役 tree.resource.delete 命名 + +> 发现时间:2026-05-18 +> +> 状态:`[done]` +> +> 关联主线:`04-tree-domain` + +## 1. 问题定义 + +当前资源生命周期正式命令面使用 `tree.resource.archive / restore / purge / rename`,删除到垃圾箱语义应表达为 `tree.resource.archive`。但 `stream_support` 的 remove asset delta 回归样例仍保留 `tree.resource.delete`。 + +该残留不在 runtime command dispatcher 中,但会让 stream 合同样例继续暗示退役命名合法,增加后续实现和测试口径漂移风险。 + +## 2. 证据 + +- [stream_support.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/stream_support.rs:1057) 测试样例仍写 `"command_name": "tree.resource.delete"`。 +- [tree.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/tree.rs:714) 当前 tree shell dispatcher 已公开 `tree.resource.archive`。 +- [mapping.rs](/mnt/Data1T/mnote/rust/crates/storage-convex-bridge/src/mapping.rs:47) storage bridge 已映射 `tree.resource.archive`,没有 `tree.resource.delete`。 + +## 3. 影响 + +- 回归样例与当前资源命令面不一致。 +- 后续维护者可能继续按 `delete` 扩写 stream / resource lifecycle 合同。 +- 与 `design/04-tree-domain/process/4-27-resource-lifecycle-command-cutover-v1.md` 中“delete 改为 archive”的口径冲突。 + +## 4. 修复结果 + +- [stream_support.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/stream_support.rs) 的 remove asset delta 回归样例已把 `tree.resource.delete` 改为 `tree.resource.archive`。 +- `rg` 已确认 Rust route / storage bridge 中不再残留 `tree.resource.delete`。 +- 对应 `stream_change_preserves_remove_asset_delta_fields` 回归测试已通过。 + +## 5. 验证证据 + +```bash +rg -n "tree\\.resource\\.delete" rust/crates/mnote-web/src/routes +cargo test --manifest-path rust/Cargo.toml -p mnote-web stream_change_preserves_remove_asset_delta_fields -- --nocapture +``` + +验证结果: + +- `rg` 无输出。 +- `stream_change_preserves_remove_asset_delta_fields` 通过:`1 passed`。 diff --git a/bugs/05-editor-mainline/done/5-11-mindmap-ghost-assets-and-tree-command-latency-v1.md b/bugs/05-editor-mainline/done/5-11-mindmap-ghost-assets-and-tree-command-latency-v1.md index 84dd0e97..302060f1 100644 --- a/bugs/05-editor-mainline/done/5-11-mindmap-ghost-assets-and-tree-command-latency-v1.md +++ b/bugs/05-editor-mainline/done/5-11-mindmap-ghost-assets-and-tree-command-latency-v1.md @@ -3,7 +3,7 @@ > 更新时间:2026-05-14 > > 分类归属: -> - `05-editor-mainline/process` +> - `05-editor-mainline/done` > - 涉及边界:`04-tree-domain/tree command + file_tree projection`、`06-mindmap/runtime save + resource relation` > > 用户证据: diff --git a/bugs/05-editor-mainline/process/5-15-page-aggregate-client-state-second-truth-v1.md b/bugs/05-editor-mainline/done/5-15-page-aggregate-client-state-second-truth-v1.md similarity index 60% rename from bugs/05-editor-mainline/process/5-15-page-aggregate-client-state-second-truth-v1.md rename to bugs/05-editor-mainline/done/5-15-page-aggregate-client-state-second-truth-v1.md index 71f4a3b8..49834dd0 100644 --- a/bugs/05-editor-mainline/process/5-15-page-aggregate-client-state-second-truth-v1.md +++ b/bugs/05-editor-mainline/done/5-15-page-aggregate-client-state-second-truth-v1.md @@ -1,8 +1,8 @@ -# 5-15 [process][bug] PageAggregateClientState 仍在前端生成第二份 page tree 真相 v1 +# 5-15 [done][bug] PageAggregateClientState 仍在前端生成第二份 page tree 真相 v1 > 发现时间:2026-05-17 > -> 状态:`[process]` +> 状态:`[done]` > > 关联主线:`05-editor-mainline` @@ -28,6 +28,14 @@ - AI context 应优先读取 Rust Page Aggregate 的稳定 projection。 - 增加测试:本地编辑后 AI context 与 Rust 回读 projection 必须一致。 -## 5. 状态 +## 5. 修复 -已确认前端仍生成第二份 page tree,尚未收口。 +- [layout.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/ssr/pages/layout.rs:227) `currentPageAiContextSnapshot` 不再在编辑器 DOM 与服务端 aggregate 不一致时构造 `local` page subtree;AI context 的 subtree 只来自 Rust Page Aggregate 的 `aggregate.tree.pageSubtree`。 +- [layout.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/ssr/pages/layout.rs:7448) 增加字符串合同测试,禁止 `pageSubtreeSource: 'local'` 与 `buildPageAiLocalSubtree(localBlocks)` 回到 AI context 主链。 + +## 6. 验证 + +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web page_ai_context_uses_page_aggregate_subtree_as_single_truth -- --nocapture` + - 结果:1 passed +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web ssr::pages::layout::tests -- --nocapture` + - 结果:12 passed diff --git a/bugs/05-editor-mainline/process/5-16-sidebar-preferred-snapshot-query-overrides-live-stream-v1.md b/bugs/05-editor-mainline/done/5-16-sidebar-preferred-snapshot-query-overrides-live-stream-v1.md similarity index 57% rename from bugs/05-editor-mainline/process/5-16-sidebar-preferred-snapshot-query-overrides-live-stream-v1.md rename to bugs/05-editor-mainline/done/5-16-sidebar-preferred-snapshot-query-overrides-live-stream-v1.md index 04085c30..ebac9f6d 100644 --- a/bugs/05-editor-mainline/process/5-16-sidebar-preferred-snapshot-query-overrides-live-stream-v1.md +++ b/bugs/05-editor-mainline/done/5-16-sidebar-preferred-snapshot-query-overrides-live-stream-v1.md @@ -1,8 +1,8 @@ -# 5-16 [process][bug] Sidebar preferred snapshot 中 query 可覆盖 live stream v1 +# 5-16 [done][bug] Sidebar preferred snapshot 中 query 可覆盖 live stream v1 > 发现时间:2026-05-17 > -> 状态:`[process]` +> 状态:`[done]` > > 关联主线:`05-editor-mainline` @@ -27,6 +27,13 @@ Sidebar 同时启动 query/live/initial 与 tree stream,并通过 preferred sn - 同版本不同内容时必须记录诊断事件,而不是静默按 freshness 选择。 - 增加单测覆盖同版本冲突。 -## 5. 状态 +## 5. 当前主链修复状态 -已确认多源选择策略仍可能覆盖 live stream,尚未修复。 +- 原证据指向的 `wolai-frontend/src/components/sidebar/use-preferred-sidebar-snapshot.ts` 已不在当前运行主链;当前仓库中该路径不存在,只在 `recycle/` 下保留历史副本。 +- 当前 3000 主壳由 Rust `SIDEBAR_TREE_JS` + `TREE_LIVE_CONTROLLER_JS` 消费 WS/SSE tree snapshot / delta,不再使用 retired React `usePreferredSidebarSnapshot` 选择器。 +- [layout.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/ssr/pages/layout.rs:7460) 增加防回归测试,断言当前 sidebar runtime 不包含 `usePreferredSidebarSnapshot`、`preferredSidebarSnapshot` 或 `liveSidebarTitle`。 + +## 6. 验证 + +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_tree_runtime_does_not_use_retired_query_preferred_snapshot_selector -- --nocapture` + - 结果:1 passed diff --git a/bugs/05-editor-mainline/process/5-17-document-title-source-drift-live-sidebar-over-head-v1.md b/bugs/05-editor-mainline/done/5-17-document-title-source-drift-live-sidebar-over-head-v1.md similarity index 58% rename from bugs/05-editor-mainline/process/5-17-document-title-source-drift-live-sidebar-over-head-v1.md rename to bugs/05-editor-mainline/done/5-17-document-title-source-drift-live-sidebar-over-head-v1.md index d97c3c1a..6141de08 100644 --- a/bugs/05-editor-mainline/process/5-17-document-title-source-drift-live-sidebar-over-head-v1.md +++ b/bugs/05-editor-mainline/done/5-17-document-title-source-drift-live-sidebar-over-head-v1.md @@ -1,8 +1,8 @@ -# 5-17 [process][bug] 文档页标题优先 liveSidebarTitle 而非 Page Aggregate head v1 +# 5-17 [done][bug] 文档页标题优先 liveSidebarTitle 而非 Page Aggregate head v1 > 发现时间:2026-05-17 > -> 状态:`[process]` +> 状态:`[done]` > > 关联主线:`05-editor-mainline` @@ -27,6 +27,13 @@ - Sidebar title 与 Page Aggregate head 冲突时记录诊断并触发 resync。 - 增加 smoke:改标题后 Sidebar、Breadcrumb、文档页标题同源一致。 -## 5. 状态 +## 5. 当前主链修复状态 -已确认标题来源仍有漂移风险,尚未修复。 +- 原证据指向的 React `document-content.tsx` / `page-aggregate-client-state.ts` 当前不在运行主链;`wolai-frontend/src` 不存在,历史副本位于 `recycle/`。 +- 当前 Rust 文档壳初始标题来自 Page Aggregate `head.title`,`updatePaneChrome` 也从 `runtimeDescriptor.aggregate.head.title` 更新文档页 chrome。 +- [layout.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/ssr/pages/layout.rs:7460) 增加防回归测试,断言当前 sidebar runtime 不包含旧 `liveSidebarTitle` 入口。 + +## 6. 验证 + +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_tree_runtime_does_not_use_retired_query_preferred_snapshot_selector -- --nocapture` + - 结果:1 passed diff --git a/bugs/05-editor-mainline/process/5-18-ai-write-body-does-not-sync-page-aggregate-content-v1.md b/bugs/05-editor-mainline/done/5-18-ai-write-body-does-not-sync-page-aggregate-content-v1.md similarity index 53% rename from bugs/05-editor-mainline/process/5-18-ai-write-body-does-not-sync-page-aggregate-content-v1.md rename to bugs/05-editor-mainline/done/5-18-ai-write-body-does-not-sync-page-aggregate-content-v1.md index 39347026..bc4a9a3b 100644 --- a/bugs/05-editor-mainline/process/5-18-ai-write-body-does-not-sync-page-aggregate-content-v1.md +++ b/bugs/05-editor-mainline/done/5-18-ai-write-body-does-not-sync-page-aggregate-content-v1.md @@ -1,8 +1,8 @@ -# 5-18 [process][bug] AI 写正文后本地 Page Aggregate content 可能不刷新 v1 +# 5-18 [done][bug] AI 写正文后本地 Page Aggregate content 可能不刷新 v1 > 发现时间:2026-05-17 > -> 状态:`[process]` +> 状态:`[done]` > > 关联主线:`05-editor-mainline` @@ -28,6 +28,16 @@ - AI context 应从最新 Rust Page Aggregate snapshot 读取,而不是依赖可能过期的本地 reducer。 - 增加 smoke:AI 修改正文后不刷新页面连续再问一次 AI,断言第二次 context 包含新内容。 -## 5. 状态 +## 5. 修复 -已确认 AI 写入后本地 Page Aggregate content 可能滞后,尚未修复。 +- [web_shell.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/web_shell.rs:1208) 文档 session 记录 `pageAggregateScriptId`,明确当前 pane 对应的 Page Aggregate JSON script。 +- [web_shell.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/web_shell.rs:1213) 新增 `syncPageAggregateScript`,在外部写入刷新得到新 aggregate 后同步 `__MNOTE_PAGE_AGGREGATE__`。 +- [web_shell.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/web_shell.rs:1624) `refreshSessionFromExternalChange` 更新 session `latestAggregate` 后立即同步 JSON script,使下一次页面 AI context 从最新 Rust Page Aggregate 快照读取。 +- [web_shell.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/web_shell.rs:3085) 增加 shell 合同测试,防止 AI 写入刷新只更新 editor session 而不更新 Page Aggregate script 缓存。 + +## 6. 验证 + +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web document_shell_returns_page_aggregate_snapshot -- --nocapture` + - 结果:1 passed +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web routes::web_shell::tests -- --nocapture` + - 结果:10 passed diff --git a/bugs/05-editor-mainline/process/5-9-c12-topbar-sidebar-toggle-noop-v1.md b/bugs/05-editor-mainline/done/5-9-c12-topbar-sidebar-toggle-noop-v1.md similarity index 73% rename from bugs/05-editor-mainline/process/5-9-c12-topbar-sidebar-toggle-noop-v1.md rename to bugs/05-editor-mainline/done/5-9-c12-topbar-sidebar-toggle-noop-v1.md index 557582f4..37e1db52 100644 --- a/bugs/05-editor-mainline/process/5-9-c12-topbar-sidebar-toggle-noop-v1.md +++ b/bugs/05-editor-mainline/done/5-9-c12-topbar-sidebar-toggle-noop-v1.md @@ -1,9 +1,9 @@ -# 5-9-C12 [process][bug] 文档页顶栏左上角切换侧栏按钮无效 v1 +# 5-9-C12 [done][bug] 文档页顶栏左上角切换侧栏按钮无效 v1 -> 更新时间:2026-05-12 +> 更新时间:2026-05-18(代码修复) > > 分类归属: -> - `05-editor-mainline/process` +> - `05-editor-mainline/done` > - 对应体验主线:`5-9 Wolai-aline continuous checklist` > > 关联文档: @@ -104,7 +104,7 @@ Hermes tester 首轮取证: ## 7. 流转条件 -当前状态:`process` +当前状态:`done` 只有在以下条件都满足后,本文才能移动到 `bugs/05-editor-mainline/done/`: @@ -112,3 +112,21 @@ Hermes tester 首轮取证: 2. smoke 已覆盖且通过 3. 浏览器复测确认收起/展开都生效 4. 证据路径已补齐到修复后的截图或日志 + +## 8. 修复记录 + +- [layout.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/ssr/pages/layout.rs:67) 新增 `toggleWorkspaceSidebar`,点击后切换 shell 的 `data-mnote-sidebar-collapsed` 状态,并同步 `documentElement` 观测属性。 +- [layout.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/ssr/pages/layout.rs:6167) 全局 click handler 接入 `data-mnote-action="toggle-sidebar"`。 +- [layout.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/ssr/pages/layout.rs:7336) 顶栏菜单按钮补齐 `data-mnote-action="toggle-sidebar"`、`data-testid="wolai-sidebar-toggle"` 与 `aria-pressed`。 +- [styles.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/ssr/styles.rs:224) `data-mnote-sidebar-collapsed="true"` 时隐藏 sidebar,让主编辑区占满空间。 + +## 9. 验证 + +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web page_layout_sidebar_toggle_is_wired_to_shell_state -- --nocapture` + - 结果:1 passed +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web ssr::pages::layout::tests -- --nocapture` + - 结果:14 passed +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web ssr::styles::tests -- --nocapture` + - 结果:7 passed + +说明:本轮完成了代码接线与 SSR/CSS 合同验证;浏览器截图复核可在后续统一 smoke 中补充。 diff --git a/bugs/07-ai/process/7-16-markdown-edit-normalized-search-byte-index-v1.md b/bugs/07-ai/done/7-16-markdown-edit-normalized-search-byte-index-v1.md similarity index 70% rename from bugs/07-ai/process/7-16-markdown-edit-normalized-search-byte-index-v1.md rename to bugs/07-ai/done/7-16-markdown-edit-normalized-search-byte-index-v1.md index 2e35849e..840b5f74 100644 --- a/bugs/07-ai/process/7-16-markdown-edit-normalized-search-byte-index-v1.md +++ b/bugs/07-ai/done/7-16-markdown-edit-normalized-search-byte-index-v1.md @@ -1,8 +1,8 @@ -# 7-16 [process][bug] mnote.doc.markdown_edit 中文归一化替换 byte index 误用 v1 +# 7-16 [done][bug] mnote.doc.markdown_edit 中文归一化替换 byte index 误用 v1 > 发现时间:2026-05-17 > -> 状态:`[process]` +> 状态:`[done]` > > 关联主线:`07-ai` > @@ -56,6 +56,18 @@ replace:二段 - 或者只在同一字符串上使用 byte index,并确保切片边界来自同一个原始字符串的 `char_indices` 映射。 - 增加中文、多字节标点、全角英文 / 数字混排的 `search_replace` 单测。 -## 6. 状态 +## 6. 修复 -已确认源码层缺陷,尚未修复。进入 `process` 等待实现和验证。 +已修复: + +- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:928) 抽出 `search_replace_exact_or_normalized`,归一化匹配时不再把 `str::find` 的 byte offset 当作字符序号使用。 +- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:959) 新增 `normalize_line_with_byte_map`,在归一化字符串与原始行之间保留 byte 边界映射。 +- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:670) 增加中文归一化替换回归测试,覆盖 `一 段` 命中 `一段` 时的多字节切片边界。 + +## 7. 验证 + +```bash +cargo test --manifest-path rust/Cargo.toml -p mnote-web test_search_replace_normalized_chinese_byte_boundaries -- --nocapture +``` + +结果:1 个测试通过。 diff --git a/bugs/07-ai/process/7-17-markdown-edit-same-block-multi-op-overwrite-v1.md b/bugs/07-ai/done/7-17-markdown-edit-same-block-multi-op-overwrite-v1.md similarity index 69% rename from bugs/07-ai/process/7-17-markdown-edit-same-block-multi-op-overwrite-v1.md rename to bugs/07-ai/done/7-17-markdown-edit-same-block-multi-op-overwrite-v1.md index d9740bbb..429a5c9d 100644 --- a/bugs/07-ai/process/7-17-markdown-edit-same-block-multi-op-overwrite-v1.md +++ b/bugs/07-ai/done/7-17-markdown-edit-same-block-multi-op-overwrite-v1.md @@ -1,8 +1,8 @@ -# 7-17 [process][bug] mnote.doc.markdown_edit 同块多操作写回会覆盖前序修改 v1 +# 7-17 [done][bug] mnote.doc.markdown_edit 同块多操作写回会覆盖前序修改 v1 > 发现时间:2026-05-17 > -> 状态:`[process]` +> 状态:`[done]` > > 关联主线:`07-ai` > @@ -70,6 +70,22 @@ Convex 写回使用的是 `block_ops`,而不是 `md`。这让“markdown 编 - 增加同一 block 多 operation 的单测和真实 Page Aggregate 回读 smoke。 - 明确部分失败是否允许写入;默认建议任一 operation 失败时不写入,除非调用方显式允许 partial apply。 -## 5. 状态 +## 5. 修复 -已确认源码层缺陷,尚未修复。进入 `process` 等待实现和验证。 +已修复: + +- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:1026) `build_block_ops_from_markdown_edit` 现在维护每个块的当前文本,按 operations 顺序累积同块修改。 +- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:1061) 同一块多次命中时只输出一次最终 `replace` block op,避免后续 op 基于原始文本覆盖前序修改。 +- [hermes_tools.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_tools.rs:1772) 增加 dry-run 路由级回归测试,覆盖第二个 search 依赖第一个替换结果的同块多操作场景。 + +## 6. 验证 + +```bash +cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_markdown_edit_merges_same_block_operations -- --nocapture +``` + +结果:1 个测试通过。 + +## 7. 剩余风险 + +本修复解决“同块多操作覆盖前序修改”的最小 P0;跨块 Markdown 重写、full_content 与最终 Markdown 作为唯一写回真源仍归属于 `7-24`。 diff --git a/bugs/07-ai/process/7-18-ai-markdown-edit-phase-state-contract-drift-v1.md b/bugs/07-ai/done/7-18-ai-markdown-edit-phase-state-contract-drift-v1.md similarity index 50% rename from bugs/07-ai/process/7-18-ai-markdown-edit-phase-state-contract-drift-v1.md rename to bugs/07-ai/done/7-18-ai-markdown-edit-phase-state-contract-drift-v1.md index 83583442..1962702d 100644 --- a/bugs/07-ai/process/7-18-ai-markdown-edit-phase-state-contract-drift-v1.md +++ b/bugs/07-ai/done/7-18-ai-markdown-edit-phase-state-contract-drift-v1.md @@ -1,8 +1,8 @@ -# 7-18 [process][bug] AI markdown_edit 阶段状态合同漂移 v1 +# 7-18 [done][bug] AI markdown_edit 阶段状态合同漂移 v1 > 发现时间:2026-05-17 > -> 状态:`[process]` +> 状态:`[done]` > > 关联主线:`07-ai` @@ -30,6 +30,22 @@ - 若已切主,应把 `page_ai_workflow`、Hermes guidance、manifest 和 smoke 都同步到该口径。 - 若未切主,应限制 `markdown_edit` 的 runtime 使用面,并把设计稿状态回退为实验态。 -## 5. 状态 +## 5. 修复 -已确认设计口径与源码使用状态不一致,尚未完成统一。 +2026-05-18 已完成运行时侧收口与顶层协作口径同步: + +- `page_ai_workflow` 当前运行时已统一走模型输出 `search/replace` → `mnote.doc.markdown_edit` → 统一 mnote tool executor。 +- Hermes run guidance 已改为普通正文编辑优先 `mnote_doc_markdown_edit`,`apply_block_ops` 仅作为结构性块操作辅助。 +- `markdown_edit` 在线写回在无法安全映射最终 markdown 时明确拒绝,避免 `ok=true` 伪成功。 +- 根目录 [AGENTS.md](/mnt/Data1T/mnote/AGENTS.md:14) 已同步为当前真实阶段:简单正文编辑主路径是 `mnote.doc.markdown_edit`,`page_ai_workflow` 通过模型生成 markdown 编辑意图后调用统一 mnote tool executor,`apply_block_ops` / `mnote.block.*` 仅作为结构性辅助。 + +## 6. 验证 + +- `rg -n "local_rule|Phase A|Phase B|当前只做|过渡实现为 .*block-edit-workflow" AGENTS.md` + - 结果:无输出,旧阶段合同表述已移除。 +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web markdown_edit -- --nocapture` + - 结果:通过。 +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web page_ai_workflow -- --nocapture` + - 结果:通过。 +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_client_run_guidance_prefers_markdown_edit_for_plain_body_edits -- --nocapture` + - 结果:通过。 diff --git a/bugs/07-ai/process/7-19-hermes-tool-guidance-markdown-edit-contract-drift-v1.md b/bugs/07-ai/done/7-19-hermes-tool-guidance-markdown-edit-contract-drift-v1.md similarity index 54% rename from bugs/07-ai/process/7-19-hermes-tool-guidance-markdown-edit-contract-drift-v1.md rename to bugs/07-ai/done/7-19-hermes-tool-guidance-markdown-edit-contract-drift-v1.md index 077cf22c..a14ce092 100644 --- a/bugs/07-ai/process/7-19-hermes-tool-guidance-markdown-edit-contract-drift-v1.md +++ b/bugs/07-ai/done/7-19-hermes-tool-guidance-markdown-edit-contract-drift-v1.md @@ -1,8 +1,8 @@ -# 7-19 [process][bug] Hermes 工具指导仍优先 apply_block_ops 而非 markdown_edit v1 +# 7-19 [done][bug] Hermes 工具指导仍优先 apply_block_ops 而非 markdown_edit v1 > 发现时间:2026-05-17 > -> 状态:`[process]` +> 状态:`[done]` > > 关联主线:`07-ai` @@ -30,6 +30,15 @@ - 仅在需要结构性块移动、资源块、复杂定位时才引导模型使用 `apply_block_ops` 或 `mnote.block.*`。 - 同步更新 smoke:模型生成工具调用时应优先产出 `mnote_doc_markdown_edit`。 -## 5. 状态 +## 5. 修复 -已确认源码指导文本与当前设计主线不一致,尚未修复。 +- [hermes_client.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_client.rs:2456) 将 Hermes run instructions 改为:普通正文 search/replace、局部段落替换、全文 markdown 替换优先调用 `mnote_doc_markdown_edit`。 +- [hermes_client.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_client.rs:2494) 将 `blockEditingToolOrder` 首位调整为 `mnote_doc_markdown_edit`,`mnote_doc_apply_block_ops` 仅保留给 markdown_edit 不能表达的结构性块操作。 +- [hermes_client.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_client.rs:4559) 增加 run guidance 合同测试,防止普通正文编辑重新退回 `apply_block_ops` 优先口径。 + +## 6. 验证 + +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_client_run_guidance_prefers_markdown_edit_for_plain_body_edits -- --nocapture` + - 结果:1 passed +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_client_run_body -- --nocapture` + - 结果:2 passed diff --git a/bugs/07-ai/done/7-20-page-ai-workflow-bypasses-hermes-tool-executor-v1.md b/bugs/07-ai/done/7-20-page-ai-workflow-bypasses-hermes-tool-executor-v1.md new file mode 100644 index 00000000..5699cfe9 --- /dev/null +++ b/bugs/07-ai/done/7-20-page-ai-workflow-bypasses-hermes-tool-executor-v1.md @@ -0,0 +1,48 @@ +# 7-20 [done][bug] page_ai_workflow 绕过 Hermes tool executor / audit / toggle v1 + +> 发现时间:2026-05-17 +> +> 状态:`[done]` +> +> 关联主线:`07-ai` + +## 1. 问题定义 + +`/api/page-ai/block-edit-workflow` 仍是独立模型调用链:直接读取 Hermes profile、调用 chat/completions、解析模型 JSON,并直接调用 Rust 工具函数。 + +它没有通过 `/api/hermes/tools/mnote/call` 的工具执行壳,因此绕过了 tool toggle、audit、统一幂等和统一调用追踪。 + +## 2. 证据 + +- [page_ai_workflow.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/page_ai_workflow.rs:69) 直接调用上游模型。 +- [page_ai_workflow.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/page_ai_workflow.rs:91) 在 route 内构造 `ToolCallInput`。 +- [page_ai_workflow.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/page_ai_workflow.rs:109) 直接调用 `doc::doc_markdown_edit`。 + +## 3. 影响 + +- 关闭或限制 mnote tool 时,该 fast-path 仍可能写入。 +- 工具调用审计与普通 Hermes run 不一致。 +- 幂等、dryRun、runId、toolCallId 等字段无法统一治理。 + +## 4. 建议修复 + +- 将 fast-path 的工具写入改为调用统一 tool executor。 +- fast-path 只负责 prompt / plan,不直接执行写入函数。 +- 增加 smoke:当对应 tool disabled 时,`page_ai_workflow` 不得绕过限制写入。 + +## 5. 修复 + +- [hermes_tools.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_tools.rs:62) 将统一 mnote tool 执行壳抽为 `execute_mnote_tool_call`,保留 profile disabled、audit、idempotency、auth/workspace 校验和统一结果包装。 +- [page_ai_workflow.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/page_ai_workflow.rs:109) `block_edit_workflow` 的写入阶段改为调用统一 executor,不再直接调用 `doc::doc_markdown_edit`。 +- [page_ai_workflow.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/page_ai_workflow.rs:657) 增加路由级测试:当 profile 禁用 `mnote.doc.markdown_edit` 时,页面 AI fast-path 必须返回 `mnote_tool_disabled`,不得绕过限制写入。 + +## 6. 验证 + +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web block_edit_workflow_respects_disabled_markdown_edit_tool -- --nocapture` + - 结果:1 passed +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web page_ai_workflow -- --nocapture` + - 结果:3 passed +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_call_rejects_profile_disabled_tool -- --nocapture` + - 结果:1 passed +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_markdown_edit_maps_normalized_search_to_block -- --nocapture` + - 结果:1 passed diff --git a/bugs/07-ai/process/7-21-markdown-edit-local-write-contract-bypass-v1.md b/bugs/07-ai/done/7-21-markdown-edit-local-write-contract-bypass-v1.md similarity index 51% rename from bugs/07-ai/process/7-21-markdown-edit-local-write-contract-bypass-v1.md rename to bugs/07-ai/done/7-21-markdown-edit-local-write-contract-bypass-v1.md index b790ec9f..1e71ab83 100644 --- a/bugs/07-ai/process/7-21-markdown-edit-local-write-contract-bypass-v1.md +++ b/bugs/07-ai/done/7-21-markdown-edit-local-write-contract-bypass-v1.md @@ -1,8 +1,8 @@ -# 7-21 [process][bug] mnote.doc.markdown_edit 本地文件写入绕过 dryRun / idempotency v1 +# 7-21 [done][bug] mnote.doc.markdown_edit 本地文件写入绕过 dryRun / idempotency v1 > 发现时间:2026-05-17 > -> 状态:`[process]` +> 状态:`[done]` > > 关联主线:`07-ai` @@ -28,6 +28,19 @@ - 对 `dryRun=true` 返回 diff / preview,不落盘。 - 增加本地 `.md` dryRun 单测,断言文件内容不变。 -## 5. 状态 +## 5. 修复 -已确认源码层缺陷,尚未修复。 +已修复: + +- [block.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/block.rs:631) 将统一写入合同校验开放为 `pub(crate)`,供 `mnote.doc.markdown_edit` 复用。 +- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:722) `mnote.doc.markdown_edit` 入口统一调用 `ensure_write_contract`,本地 `.md` 与在线文档一致要求 `idempotencyKey` 与显式 `dryRun`。 +- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:845) 本地文件 `dryRun=true` 时只返回 `written=false` 预览结果,不再写入磁盘。 +- [hermes_tools.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_tools.rs:1557) 增加路由级回归测试,覆盖缺少 `idempotencyKey` 与本地 dry-run 不落盘。 + +## 6. 验证 + +```bash +cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_markdown_edit_local -- --nocapture +``` + +结果:2 个测试通过。 diff --git a/bugs/07-ai/done/7-22-apply-block-ops-missing-write-preconditions-v1.md b/bugs/07-ai/done/7-22-apply-block-ops-missing-write-preconditions-v1.md new file mode 100644 index 00000000..c66af464 --- /dev/null +++ b/bugs/07-ai/done/7-22-apply-block-ops-missing-write-preconditions-v1.md @@ -0,0 +1,50 @@ +# 7-22 [done][bug] mnote.doc.apply_block_ops 批量写入缺少 revision / conflictDetectionKey 校验 v1 + +> 发现时间:2026-05-17 +> +> 状态:`[done]` +> +> 关联主线:`07-ai` + +## 1. 问题定义 + +`mnote.doc.apply_block_ops` 的批量入口只校验 dryRun / idempotency 写入合同,没有调用页面 revision、conflictDetectionKey、blockRevisionRef 的写前校验。 + +同文件中已有 `ensure_page_write_preconditions`,但该批量入口没有使用它。 + +## 2. 证据 + +- [block.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/block.rs:385) 定义 `doc_apply_block_ops`。 +- [block.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/block.rs:390) 仅调用 `ensure_write_contract`。 +- [block.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/block.rs:774) 存在 `ensure_page_write_preconditions`,但批量入口未调用。 + +## 3. 影响 + +- AI 可能基于旧 `PageAggregate` 对正文写入,覆盖用户新编辑。 +- 多轮 Hermes / ACP run 同时写入时缺少冲突保护。 +- `markdown_edit` 在线分支会转调 `apply_block_ops`,因此该缺陷会影响 markdown 主路径。 + +## 4. 建议修复 + +- `doc_apply_block_ops` 真实写入前必须校验 page revision 与 conflictDetectionKey。 +- 块级操作应支持并校验 blockRevisionRef 或等价版本字段。 +- 增加负向测试:缺少 revision / conflictDetectionKey 时真实写入必须拒绝。 + +## 5. 修复 + +已修复: + +- [block.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/block.rs:413) `doc_apply_block_ops` 真实写入前调用 `ensure_page_write_preconditions`,要求 `revision` 与 `conflictDetectionKey`。 +- [block.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/block.rs:439) replace / delete / move 目标块现在校验 operation 内的 `blockRevisionRef`。 +- [block.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/block.rs:472) insert_after / move_after anchor 现在校验 operation 内的 `anchorRevisionRef`。 +- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:875) `mnote.doc.markdown_edit` 在线转调 `doc_apply_block_ops` 时自动携带当前 aggregate 的 `revision`、`conflictDetectionKey` 与每个 replace op 的 `blockRevisionRef`。 +- [hermes_tools.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_tools.rs:1490) 增加批量入口负向测试,覆盖缺少 page precondition 与缺少 block revision ref。 + +## 6. 验证 + +```bash +cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_apply_block_ops_requires_page_preconditions -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_markdown_edit -- --nocapture +``` + +结果:批量前置条件测试通过,5 个 markdown_edit 相关测试通过。 diff --git a/bugs/07-ai/process/7-23-markdown-edit-manifest-schema-contract-drift-v1.md b/bugs/07-ai/done/7-23-markdown-edit-manifest-schema-contract-drift-v1.md similarity index 50% rename from bugs/07-ai/process/7-23-markdown-edit-manifest-schema-contract-drift-v1.md rename to bugs/07-ai/done/7-23-markdown-edit-manifest-schema-contract-drift-v1.md index 3ce485df..d4d0ee2e 100644 --- a/bugs/07-ai/process/7-23-markdown-edit-manifest-schema-contract-drift-v1.md +++ b/bugs/07-ai/done/7-23-markdown-edit-manifest-schema-contract-drift-v1.md @@ -1,8 +1,8 @@ -# 7-23 [process][bug] mnote.doc.markdown_edit manifest schema 缺少 required 与写入字段 v1 +# 7-23 [done][bug] mnote.doc.markdown_edit manifest schema 缺少 required 与写入字段 v1 > 发现时间:2026-05-17 > -> 状态:`[process]` +> 状态:`[done]` > > 关联主线:`07-ai` @@ -29,6 +29,19 @@ - manifest 与 Rust runtime 校验保持一致。 - 增加 manifest snapshot 测试,防止字段再次漂移。 -## 5. 状态 +## 5. 修复 -已确认 manifest 合同不完整,尚未修复。 +已修复: + +- [manifest.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/manifest.rs:413) `mnote.doc.markdown_edit` 改为复用统一 `write_tool` schema 生成器。 +- [manifest.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/manifest.rs:302) manifest 现在声明 `dryRun`、`idempotencyKey` 与 actor/session/run/toolCall/trace 等写入合同字段。 +- [manifest.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/manifest.rs:449) 通过 `anyOf` 声明 `operations` / `full_content` 至少提供一个。 +- [hermes_tools.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_tools.rs:730) 增加 manifest 路由级测试,防止 schema 再次漂移。 + +## 6. 验证 + +```bash +cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_manifest_describes_markdown_edit_write_contract -- --nocapture +``` + +结果:1 个测试通过。 diff --git a/bugs/07-ai/process/7-24-markdown-edit-online-write-does-not-use-final-markdown-v1.md b/bugs/07-ai/done/7-24-markdown-edit-online-write-does-not-use-final-markdown-v1.md similarity index 56% rename from bugs/07-ai/process/7-24-markdown-edit-online-write-does-not-use-final-markdown-v1.md rename to bugs/07-ai/done/7-24-markdown-edit-online-write-does-not-use-final-markdown-v1.md index e86491ec..e7f452c0 100644 --- a/bugs/07-ai/process/7-24-markdown-edit-online-write-does-not-use-final-markdown-v1.md +++ b/bugs/07-ai/done/7-24-markdown-edit-online-write-does-not-use-final-markdown-v1.md @@ -1,8 +1,8 @@ -# 7-24 [process][bug] 在线 markdown_edit 写回不以最终 Markdown 为真源 v1 +# 7-24 [done][bug] 在线 markdown_edit 写回不以最终 Markdown 为真源 v1 > 发现时间:2026-05-17 > -> 状态:`[process]` +> 状态:`[done]` > > 关联主线:`07-ai` @@ -30,6 +30,15 @@ - 对 full_content、跨块 search/replace、列表/标题变更增加回读一致性测试。 - route 成功条件必须从“operationsApplied > 0”提升为“Page Aggregate 回读等于预期最终 markdown”。 -## 5. 状态 +## 5. 修复 -已确认写回层没有使用最终 markdown 真相,尚未修复。 +- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:868) 在线分支在 markdown 层命中后重新映射为 block ops;若最终 markdown 变化无法安全映射到可写块,会返回 `mnote_doc_markdown_edit_block_mapping_empty`,不再把空 operations 交给 `apply_block_ops` 或返回伪成功。 +- [hermes_tools.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_tools.rs:1857) 增加 `full_content` 在线写回合同测试:当前没有完整 markdown->block 真源写回时必须明确拒绝,不能以 `ok=true` 伪成功。 +- [hermes_tools.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_tools.rs:1805) 保留 markdown 命中但块映射为空的提前失败测试,避免回退到 `mnote.doc.apply_block_ops operations 不能为空`。 + +## 6. 验证 + +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_markdown_edit_online_full_content_rejects_unsafe_block_mapping -- --nocapture` + - 结果:1 passed +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web markdown_edit -- --nocapture` + - 结果:8 passed diff --git a/bugs/07-ai/process/7-25-reasonix-block-edit-workflow-empty-block-ops-after-markdown-match-v1.md b/bugs/07-ai/done/7-25-reasonix-block-edit-workflow-empty-block-ops-after-markdown-match-v1.md similarity index 72% rename from bugs/07-ai/process/7-25-reasonix-block-edit-workflow-empty-block-ops-after-markdown-match-v1.md rename to bugs/07-ai/done/7-25-reasonix-block-edit-workflow-empty-block-ops-after-markdown-match-v1.md index ce2d7634..4873dfff 100644 --- a/bugs/07-ai/process/7-25-reasonix-block-edit-workflow-empty-block-ops-after-markdown-match-v1.md +++ b/bugs/07-ai/done/7-25-reasonix-block-edit-workflow-empty-block-ops-after-markdown-match-v1.md @@ -1,8 +1,8 @@ -# 7-25 [process][bug] ACP Reasonix 页面 AI block_edit_workflow 在 markdown 命中后生成空 block_ops v1 +# 7-25 [done][bug] ACP Reasonix 页面 AI block_edit_workflow 在 markdown 命中后生成空 block_ops v1 > 发现时间:2026-05-17 > -> 状态:`[process]` +> 状态:`[done]` > > 关联主线:`07-ai` > @@ -64,7 +64,7 @@ mnote.page_ai.block_edit_workflow 失败 · page-ai-fast-mp9wm0qo - “读取第一段 + 修改第二段”这类混合任务可能进入 fast workflow,但该 workflow 只能表达写入,不保证先读后答。 - 如果模型给出的 `search` 是段落序号、fuzzy 片段、带格式文本或跨块文本,markdown 层可能成功,block 写回层仍失败。 -## 6. 建议修复 +## 6. 建议修复:建议以长期,彻底的修复优先,建议参考/mnt/Data1T/mnote/design/05-editor-mainline/reference-code/cli-main中对全局替换和单块替换的判断逻辑。 短期: @@ -86,7 +86,23 @@ mnote.page_ai.block_edit_workflow 失败 · page-ai-fast-mp9wm0qo - 断言失败时错误码不能是 `mnote.doc.apply_block_ops operations 不能为空`,应是 markdown 到 block 映射失败的明确错误。 - 修复后回读 Page Aggregate,断言第二段实际变为 `测试123`,同时 AI 回复能正常说明已读取到第一段。 -## 8. 状态 +## 8. 修复 -已按用户反馈与源码路径确认缺陷,尚未修复。 +已修复: +- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:862) 在 `mnote.doc.markdown_edit` 调用 `doc_apply_block_ops` 前拦截 `applied > 0 && block_ops.is_empty()`,返回 `mnote_doc_markdown_edit_block_mapping_empty`,不再泄露 `mnote.doc.apply_block_ops operations 不能为空`。 +- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:928) 抽出 `search_replace_exact_or_normalized`,让 markdown 层与 block 映射层共享“精确 / 忽略空白 / 全半角归一化”规则。 +- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:1038) 在线 block ops 构建不再用 `block.text.contains(search)`,改为对单块文本执行同一套安全匹配;fuzzy 仍只保留在 markdown 预处理层,避免写错块。 +- [hermes_tools.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_tools.rs:1670) 增加回归测试,覆盖空映射语义错误与忽略空白后正确映射到 `p_2`。 + +## 9. 验证 + +```bash +cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_markdown_edit -- --nocapture +``` + +结果:4 个相关测试通过。 + +## 10. 剩余风险 + +本修复解决单块 search/replace 映射和空 ops 下沉;跨块 Markdown 重写、完整最终 Markdown 作为写回真源仍归属于 `7-24`。 diff --git a/bugs/07-ai/done/7-26-resource-block-normalized-to-paragraph-ai-projection-v1.md b/bugs/07-ai/done/7-26-resource-block-normalized-to-paragraph-ai-projection-v1.md new file mode 100644 index 00000000..f16461ab --- /dev/null +++ b/bugs/07-ai/done/7-26-resource-block-normalized-to-paragraph-ai-projection-v1.md @@ -0,0 +1,38 @@ +# 7-26 [done] Resource 块保存后在 AI block projection 中被降级为 paragraph + +## 状态 + +- Owner:`07-ai` / Page Block AI projection 写入前置合同 +- 优先级:P0,影响 AI 精确块编辑安全边界 +- 状态:已修复并验证 + +## 现象 + +真实 3000 smoke 扩展复杂块后,`mnote.page.save` 写入 `type:"resource"` 的块,再经 Page Aggregate / AI block projection 回读时,`resource_1` 被显示为: + +- `type="paragraph"` +- `editable=true` +- `unsupportedReason=null` + +这会让 AI 工具把资源占位误判为普通可编辑文本块,违反复杂块不得伪装完全可编辑的合同。 + +## 根因 + +`core-protocol` 的 `EditorBlockType` 没有 `Resource` 变体,`bridge-runtime` 的 `normalize_editor_block_type_for_save()` 因此把未知 `resource` 类型落入默认 `Paragraph`。后续 `legacy_content_from_editor_document()` 和 `project_legacy_content_to_block_document()` 只能看到 paragraph,导致 AI projection 误报可编辑。 + +## 修复 + +- 在 `rust/crates/core-protocol/src/editor/model.rs` 增加 `EditorBlockType::Resource`。 +- 在 Tiptap bridge 中用 `mnoteBlockType=resource` 保留 resource 类型往返。 +- 在 `bridge-runtime` 中让 `resource/resource_block` 规范化为 `EditorBlockType::Resource`,legacy 回写继续输出 `type:"resource"`。 +- 保留 resource props 到 `resourceProps`,legacy 回写时恢复。 +- 在 `editor_actor` block 类型摘要中补 `resource`。 +- 扩展 `scripts/task-page-block-ai-tools-smoke.js`,覆盖 resource 在真实 3000 的投影和 move 阻断。 + +## 验证 + +- `cargo test --manifest-path rust/Cargo.toml -p bridge-runtime editor_document_roundtrip_preserves_resource_block_type -- --nocapture` +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_block_move_after_blocks_complex_and_nested_blocks -- --nocapture` +- `PLAYWRIGHT_CHROME_EXECUTABLE=/snap/bin/chromium MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task-page-block-ai-tools-smoke.js` + +最新真实 smoke 证据:`tmp/page-block-ai-tools-smoke/mpag4966.json`,截图:`tmp/page-block-ai-tools-smoke/mpag4966-page.png`。 diff --git a/bugs/07-ai/done/7-27-markdown-edit-full-content-retains-old-blocks-v1.md b/bugs/07-ai/done/7-27-markdown-edit-full-content-retains-old-blocks-v1.md new file mode 100644 index 00000000..94b76335 --- /dev/null +++ b/bugs/07-ai/done/7-27-markdown-edit-full-content-retains-old-blocks-v1.md @@ -0,0 +1,76 @@ +# 7-27 [done][bug] 在线 markdown_edit full_content 会保留旧普通块 + +> 发现时间:2026-05-18 +> +> 状态:`[done]` +> +> 关联主线:`07-ai` +> +> 关联设计:`design/07-ai/process/7-27-online-markdown-writeback-final-content-truth-v2.md` + +## 1. 问题定义 + +`mnote.doc.markdown_edit` 的在线 Convex 文档路径已经切到 final markdown -> block content 直接写回,但 `full_content` 模式下,最终 markdown 通常不包含任何 `` 原始块注释。 + +当前 `build_page_content()` 会把所有未处理的原始块追加到写回结果尾部,导致全文替换不是“替换全文”,而是: + +```text +新全文块... +旧 heading / paragraph / todo 块... +``` + +## 2. 影响 + +- AI 执行全文改写、重写大纲、把页面替换为整理后的内容时,会保留旧正文残留。 +- Page Aggregate / `mnote.doc.fetch` 回读会看到新旧内容混在一起。 +- 7-27 的“最终 markdown 是唯一写回真源”合同被破坏。 + +## 3. 根因 + +`parse_final_markdown_to_blocks()` 对无注释的 final markdown 行会生成 `is_new=true` 块,这是正确的。 + +问题在 `build_page_content()` 的末尾: + +```rust +for original in &original_blocks { + if let Some(id) = block_id_of(original) { + if !processed_ids.contains(&id) { + result.push(original.clone()); + } + } +} +``` + +该逻辑没有区分: + +- 部分 search/replace 丢掉某个块注释:应保守保留缺失旧块,避免误删。 +- `full_content` / 全文替换没有任何原始块 ID:应只保留复杂块,普通旧文本块应被替换掉。 + +## 4. 修复 + +- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:727) 给 `build_page_content()` 增加策略: + - 若 parsed 中至少引用了一个原始 block id,则保留未处理原始块,维持安全降级。 + - 若 parsed 中没有任何原始 block id,则视为全文重建,只保留复杂块,不保留普通旧文本块。 +- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:799) 增加 `is_complex_markdown_original_block()`,用于 full_content 重建时保留 resource / mindmap / table / image / attachment / 非 editable / unsupported / 带 children 的复杂块。 +- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:1059) 增加单元测试: + - `full_content` 无原始 ID 时不保留旧 heading / paragraph。 + - 原始 resource 复杂块仍保留。 + - 部分块注释丢失时仍保留缺失旧块。 + +## 5. 验证 + +```bash +cargo test --manifest-path rust/Cargo.toml -p mnote-web build_page_content -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_markdown_edit_online_full_content_rejects_unsafe_block_mapping -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_markdown_edit -- --nocapture +cargo fmt --manifest-path rust/Cargo.toml --all -- --check +git diff --check -- rust/crates/mnote-web/src/hermes_tools/doc.rs bugs/07-ai/done/7-27-markdown-edit-full-content-retains-old-blocks-v1.md +``` + +结果: + +- `build_page_content` 相关测试:`2 passed`。 +- `hermes_tools_markdown_edit_online_full_content_rejects_unsafe_block_mapping`:`1 passed`。 +- `hermes_tools_markdown_edit` 相关测试:`6 passed`。 +- Rust workspace format check 通过。 +- 相关路径 diff check 通过。 diff --git a/bugs/07-ai/done/7-28-markdown-edit-selection-scope-not-enforced-v1.md b/bugs/07-ai/done/7-28-markdown-edit-selection-scope-not-enforced-v1.md new file mode 100644 index 00000000..39110836 --- /dev/null +++ b/bugs/07-ai/done/7-28-markdown-edit-selection-scope-not-enforced-v1.md @@ -0,0 +1,49 @@ +# 7-28 [done][bug] markdown_edit 未强制 selection 允许块边界 + +> 发现时间:2026-05-18 +> +> 状态:`[done]` +> +> 关联主线:`07-ai` + +## 1. 问题定义 + +页面 AI context 已经可以携带 `allowedTargetBlockIds`,用于表达 `scope=selection` 时本次 run 允许修改的块集合。但 `mnote.doc.markdown_edit` 当前只按文本 search/replace 写回,不检查最终变化的 block 是否落在允许集合内。 + +如果 fast workflow 或未来调用者传入了过宽的 `pageText`,模型返回了选区外文本的 search/replace,服务端仍可能写入选区外块。 + +## 2. 证据 + +- [page_ai_workflow.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/page_ai_workflow.rs:91) fast workflow 调用 `mnote.doc.markdown_edit`。 +- [page_ai_workflow.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/page_ai_workflow.rs:299) 读取了 `allowedTargetBlockIds`,但未传给写工具或参与校验。 +- [hermes_client.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_client.rs:2860) 工具指导要求 `scope=selection` 时只能修改 `allowedTargetBlockIds` 内的块。 +- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:1220) `markdown_edit` 生成 `changedBlocks` 后直接 dry-run 或写回,没有 selection 范围校验。 + +## 3. 影响 + +- 页面 AI 对选中文本执行编辑时,服务端缺少最终兜底边界。 +- 上下文构造或模型输出一旦漂移,选区外块可能被写入。 +- 与 `mnote.block.*` 已有 `allowedTargetBlockIds` / blockId 前置条件相比,`markdown_edit` 的写入安全边界较弱。 + +## 4. 修复 + +- [page_ai_workflow.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/page_ai_workflow.rs:91) fast workflow 将 `aiContext.allowedTargetBlockIds` 透传给 `mnote.doc.markdown_edit`。 +- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:855) `mnote.doc.markdown_edit` 在生成 `changedBlocks` 后,如果存在 `allowedTargetBlockIds` / `selectedBlockIds`: + - 任何带 `blockId` 的变更必须属于允许集合。 + - 无 `blockId` 的 insert/full_content 变更在 selection scope 下拒绝,避免无法归属的选区外写入。 +- [hermes_tools.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_tools.rs:2308) 增加 route 测试:`allowedTargetBlockIds=["p_2"]` 时替换 `p_1` 必须返回 `mnote_markdown_edit_target_out_of_scope`。 +- [page_ai_workflow.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/page_ai_workflow.rs:742) 增加 fast workflow 测试:模型输出修改 `p_1`,而 selection 只允许 `p_2`,必须被服务端拒绝。 + +## 5. 验证 + +```bash +cargo test --manifest-path rust/Cargo.toml -p mnote-web markdown_edit_rejects_selection_out_of_scope -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web block_edit_workflow -- --nocapture +cargo fmt --manifest-path rust/Cargo.toml --all -- --check +``` + +结果: + +- `hermes_tools_markdown_edit_rejects_selection_out_of_scope`:`1 passed`。 +- `block_edit_workflow_forwards_allowed_target_blocks_to_markdown_edit`:`1 passed`。 +- Rust workspace format check 通过。 diff --git a/bugs/07-ai/done/7-29-markdown-edit-flattens-inline-content-v1.md b/bugs/07-ai/done/7-29-markdown-edit-flattens-inline-content-v1.md new file mode 100644 index 00000000..7884cded --- /dev/null +++ b/bugs/07-ai/done/7-29-markdown-edit-flattens-inline-content-v1.md @@ -0,0 +1,44 @@ +# 7-29 [done][bug] markdown_edit 会把普通块 inline content 扁平化 + +> 发现时间:2026-05-18 +> +> 状态:`[done]` +> +> 关联主线:`07-ai` + +## 1. 问题定义 + +`mnote.doc.markdown_edit` 的 online 写回已经从“二次推导 block ops”切到 final markdown → block content,但 `build_page_content()` 仍会把普通块的 legacy `content` 统一写成扁平字符串。 + +这会让未修改的块也丢失原始 inline content 结构,修改过的单段落块则更容易丢失 bold / italic / link / code 等 marks。 + +## 2. 证据 + +- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:762) 当前普通块写回直接 `obj.insert("content", json!(parsed_block.text))`。 +- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs:826) 只有 `text` 和 `id` 被保留,原 inline 节点结构被覆盖。 +- [7-27](/mnt/Data1T/mnote/design/07-ai/process/7-27-online-markdown-writeback-final-content-truth-v2.md:580) 已明确指出当前 `blocks_to_markdown` / 写回链会丢弃 inline marks。 + +## 3. 影响 + +- 未修改块在 `full_content` / 局部替换后也可能失去 inline 节点结构。 +- 已修改块中的 bold / link / code 等 marks 可能被扁平化成普通字符串。 +- 页面 AI 的正文重写与可视格式回读不一致。 + +## 4. 修复结果 + +- [doc.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/doc.rs) 的 `build_page_content()` 已改为区分未变更、可安全局部替换、复杂块保留三类写回路径。 +- 未变更的普通块会直接保留原始 block 结构,不再被重写成纯字符串 content。 +- 已变更且原始 `content` / `contentNodes` 是单 inline text 节点的普通块,只替换该 text 节点文本,保留原 marks / attrs。 +- 复杂块继续原样保留;无法证明可安全替换的已变更普通块才退回纯文本 content。 + +## 5. 验证证据 + +```bash +cargo test --manifest-path rust/Cargo.toml -p mnote-web build_page_content -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_markdown_edit -- --nocapture +``` + +验证结果: + +- `build_page_content` 定向测试通过,覆盖 full_content 不保留旧普通块、局部替换保留未变更块、单 inline text marks 保留。 +- `hermes_tools_markdown_edit` 定向测试通过,覆盖 markdown edit 在线写回链路。 diff --git a/bugs/07-ai/done/7-30-block-tools-selection-scope-not-enforced-v1.md b/bugs/07-ai/done/7-30-block-tools-selection-scope-not-enforced-v1.md new file mode 100644 index 00000000..1d5549b1 --- /dev/null +++ b/bugs/07-ai/done/7-30-block-tools-selection-scope-not-enforced-v1.md @@ -0,0 +1,48 @@ +# 7-30 [done][bug] 单个 mnote.block 写工具未强制 selection scope + +> 发现时间:2026-05-18 +> +> 状态:`[done]` +> +> 关联主线:`07-ai` + +## 1. 问题定义 + +`mnote.doc.apply_block_ops` 已经通过 `allowedTargetBlockIds` 限制 AI selection 范围内的结构性写入,但单个 `mnote.block.replace` / `mnote.block.insert_after` / `mnote.block.delete` / `mnote.block.move_after` 写工具没有同等服务端兜底。 + +这会导致模型或 ACP 调用者在 `scope=selection` 场景下,绕过批量工具直接调用单块写工具时,仍可能改到 selection 外的块。 + +## 2. 证据 + +- [block.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/block.rs) 的 `doc_apply_block_ops()` 对每个操作调用 `ensure_allowed_target()`。 +- [block.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/block.rs) 的单个 block 写工具当前只校验 revision / editable / precondition,缺少同等 `allowedTargetBlockIds` 校验。 +- [7-10](/mnt/Data1T/mnote/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md:70) 已标注“单个 `mnote.block.*` 写工具尚未完成 `allowedTargetBlockIds` 矩阵”。 + +## 3. 影响 + +- AI 页面 selection 编辑边界不完整。 +- `markdown_edit` 和 `apply_block_ops` 已拒绝越界写入,但单块工具仍可能成为绕过路径。 +- ACP / Hermes tool guidance 即使要求 selection 内写入,也缺少 runtime 级强约束。 + +## 4. 修复结果 + +- [block.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/block.rs) 已为 `mnote.block.replace`、`mnote.block.insert_after`、`mnote.block.delete`、`mnote.block.move_after` 增加统一 `allowedTargetBlockIds` 服务端校验。 +- `move_after` 同时校验被移动块和 anchor 块,避免 selection 内块被移动到 selection 外 anchor 后。 +- [manifest.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/manifest.rs) 已在单块写工具 schema 中公开 `allowedTargetBlockIds`。 +- [hermes_tools.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/hermes_tools.rs) 已增加回归测试,覆盖单块写工具越界时返回 `mnote_block_target_out_of_scope`。 + +## 5. 验证证据 + +```bash +cargo test --manifest-path rust/Cargo.toml -p mnote-web block_tools_selection_scope -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_block -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_manifest -- --nocapture +cargo fmt --manifest-path rust/Cargo.toml --all -- --check +``` + +验证结果: + +- RED:修复前 `block_tools_selection_scope` 失败,越界单块写工具返回 `200`。 +- GREEN:修复后 `block_tools_selection_scope` 通过:`2 passed`。 +- `hermes_tools_block` 通过:`6 passed`。 +- `hermes_tools_manifest` 通过:`3 passed`。 diff --git a/bugs/07-ai/process/page-ai-block-edit-model-fallback-missing-content.md b/bugs/07-ai/done/page-ai-block-edit-model-fallback-missing-content.md similarity index 78% rename from bugs/07-ai/process/page-ai-block-edit-model-fallback-missing-content.md rename to bugs/07-ai/done/page-ai-block-edit-model-fallback-missing-content.md index 5d0a911c..4009fdf2 100644 --- a/bugs/07-ai/process/page-ai-block-edit-model-fallback-missing-content.md +++ b/bugs/07-ai/done/page-ai-block-edit-model-fallback-missing-content.md @@ -2,9 +2,9 @@ > 发现时间:2026-05-16 > -> 更新时间:2026-05-16(架构定位修正) +> 更新时间:2026-05-18(运行时收口验证) > -> 状态:`[process]` +> 状态:`[done]` > > 关联主线:`07-ai` / `05-editor-mainline` > @@ -100,3 +100,20 @@ replace 操作缺少 content - Model path(自然语言,真 AI): ❌ 400,replace 缺少 content - 代码:`page_ai_workflow.rs` system prompt 缺 operation schema - 用户反馈:direct path 不是 AI 面板应有的行为,应退役 + +## 运行时修复 + +- [page_ai_workflow.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/page_ai_workflow.rs:67) 当前运行时已退役 direct path,所有块编辑请求统一走模型输出 `search/replace` 对,再进入 `mnote.doc.markdown_edit`。 +- [page_ai_workflow.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/page_ai_workflow.rs:307) system prompt 已改为明确要求 `operations[].search` 与 `operations[].replace`,不再要求模型输出旧的 block op `content` 字段。 +- [page_ai_workflow.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/page_ai_workflow.rs:109) fast-path 写入已改走统一 mnote tool executor,避免绕过 tool toggle / audit / idempotency。 + +## 验证 + +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web page_ai_workflow -- --nocapture` + - 结果:3 passed +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web markdown_edit -- --nocapture` + - 结果:8 passed + +## 剩余说明 + +旧的 `direct_block_edit_operations` / `quoted_segments` helper 当前仅作为 dead code 与历史单测保留,不再是 `block_edit_workflow` 的运行时入口;后续可单独清理测试语义,但不影响本缺陷的运行时修复。 diff --git a/bugs/07-ai/process/7-20-page-ai-workflow-bypasses-hermes-tool-executor-v1.md b/bugs/07-ai/process/7-20-page-ai-workflow-bypasses-hermes-tool-executor-v1.md deleted file mode 100644 index b8b1d73c..00000000 --- a/bugs/07-ai/process/7-20-page-ai-workflow-bypasses-hermes-tool-executor-v1.md +++ /dev/null @@ -1,35 +0,0 @@ -# 7-20 [process][bug] page_ai_workflow 绕过 Hermes tool executor / audit / toggle v1 - -> 发现时间:2026-05-17 -> -> 状态:`[process]` -> -> 关联主线:`07-ai` - -## 1. 问题定义 - -`/api/page-ai/block-edit-workflow` 仍是独立模型调用链:直接读取 Hermes profile、调用 chat/completions、解析模型 JSON,并直接调用 Rust 工具函数。 - -它没有通过 `/api/hermes/tools/mnote/call` 的工具执行壳,因此绕过了 tool toggle、audit、统一幂等和统一调用追踪。 - -## 2. 证据 - -- [page_ai_workflow.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/page_ai_workflow.rs:69) 直接调用上游模型。 -- [page_ai_workflow.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/page_ai_workflow.rs:91) 在 route 内构造 `ToolCallInput`。 -- [page_ai_workflow.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/page_ai_workflow.rs:109) 直接调用 `doc::doc_markdown_edit`。 - -## 3. 影响 - -- 关闭或限制 mnote tool 时,该 fast-path 仍可能写入。 -- 工具调用审计与普通 Hermes run 不一致。 -- 幂等、dryRun、runId、toolCallId 等字段无法统一治理。 - -## 4. 建议修复 - -- 将 fast-path 的工具写入改为调用统一 tool executor。 -- fast-path 只负责 prompt / plan,不直接执行写入函数。 -- 增加 smoke:当对应 tool disabled 时,`page_ai_workflow` 不得绕过限制写入。 - -## 5. 状态 - -已确认 route 级绕过统一执行壳,尚未修复。 diff --git a/bugs/07-ai/process/7-22-apply-block-ops-missing-write-preconditions-v1.md b/bugs/07-ai/process/7-22-apply-block-ops-missing-write-preconditions-v1.md deleted file mode 100644 index dc00817a..00000000 --- a/bugs/07-ai/process/7-22-apply-block-ops-missing-write-preconditions-v1.md +++ /dev/null @@ -1,35 +0,0 @@ -# 7-22 [process][bug] mnote.doc.apply_block_ops 批量写入缺少 revision / conflictDetectionKey 校验 v1 - -> 发现时间:2026-05-17 -> -> 状态:`[process]` -> -> 关联主线:`07-ai` - -## 1. 问题定义 - -`mnote.doc.apply_block_ops` 的批量入口只校验 dryRun / idempotency 写入合同,没有调用页面 revision、conflictDetectionKey、blockRevisionRef 的写前校验。 - -同文件中已有 `ensure_page_write_preconditions`,但该批量入口没有使用它。 - -## 2. 证据 - -- [block.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/block.rs:385) 定义 `doc_apply_block_ops`。 -- [block.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/block.rs:390) 仅调用 `ensure_write_contract`。 -- [block.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/hermes_tools/block.rs:774) 存在 `ensure_page_write_preconditions`,但批量入口未调用。 - -## 3. 影响 - -- AI 可能基于旧 `PageAggregate` 对正文写入,覆盖用户新编辑。 -- 多轮 Hermes / ACP run 同时写入时缺少冲突保护。 -- `markdown_edit` 在线分支会转调 `apply_block_ops`,因此该缺陷会影响 markdown 主路径。 - -## 4. 建议修复 - -- `doc_apply_block_ops` 真实写入前必须校验 page revision 与 conflictDetectionKey。 -- 块级操作应支持并校验 blockRevisionRef 或等价版本字段。 -- 增加负向测试:缺少 revision / conflictDetectionKey 时真实写入必须拒绝。 - -## 5. 状态 - -已确认批量写入入口缺少必要冲突校验,尚未修复。 diff --git a/bugs/done/2026-05-18-p0-kernel-ai-bugfix-progress.md b/bugs/done/2026-05-18-p0-kernel-ai-bugfix-progress.md new file mode 100644 index 00000000..aeaa8fee --- /dev/null +++ b/bugs/done/2026-05-18-p0-kernel-ai-bugfix-progress.md @@ -0,0 +1,159 @@ +# 2026-05-18 [done] P0 内核优先 Bug 修复记录与进展 + +> 记录时间:2026-05-18 +> +> 状态:`done` +> +> 范围:`03-rust-web`、`04-tree-domain`、`05-editor-mainline`、`07-ai` +> +> 口径:本文是本轮 P0 / 内核优先缺陷修复的总进展记录;单个缺陷的根因、修复点和证据仍以各分类 `bugs//done/` 下的明细文档为准。 + +## 1. 总结 + +本轮按“P0 优先 / 内核优先”的顺序完成了已确认缺陷的修复、验证和归档: + +- `bugs/*/process/` 当前已无遗留缺陷文档。 +- `design/10-review/process/08-*` 已迁入 `design/10-review/done/08-*`,10-review 当前无活跃 process 文档。 +- Page Aggregate、tree command、tree realtime、AI markdown edit / block tools 的 P0/P1 blocker 已归档到对应 `bugs/*/done/`。 +- 当前仍未实施的是 `7-10` / `7-16` 中 Phase C UI / Review Mode 相关项,已明确冻结,不计入本轮 P0 内核 bug blocker。 + +## 2. 本轮修复主线 + +### 2.1 Rust Web / Realtime / ACP + +已归档缺陷: + +- `bugs/03-rust-web/done/3-16-tree-realtime-ws-sse-doc-contract-drift-v1.md` +- `bugs/03-rust-web/done/3-17-sse-push-skips-polling-fallback-v1.md` +- `bugs/03-rust-web/done/3-18-ws-sse-delta-contract-split-v1.md` +- `bugs/03-rust-web/done/3-19-acp-reasonix-tool-call-missing-identity-fields-v1.md` +- `bugs/03-rust-web/done/3-20-acp-request-permission-no-response-v1.md` +- `bugs/03-rust-web/done/3-21-acp-run-payload-consumed-and-removed-v1.md` +- `bugs/03-rust-web/done/3-22-acp-permission-auto-deny-ui-shows-approval-actions-v1.md` + +收口结果: + +- `/api/realtime/ws` 与 `/api/tree/events` 的实时合同已按 WS 主链 / SSE fallback 口径收口。 +- ACP Reasonix 工具调用身份字段、permission 响应、run payload 生命周期相关缺陷已进入 done。 +- ACP permission 自动拒绝时,页面 AI UI 不再展示误导性的“允许 / 拒绝”审批动作。 +- 这些缺陷不再作为当前 10-review P0 blocker 跟踪。 + +### 2.2 Tree Domain / File Tree + +已归档缺陷: + +- `bugs/04-tree-domain/done/4-46-filetree-resource-row-document-command-leak-v1.md` +- `bugs/04-tree-domain/done/4-47-stream-support-retired-resource-delete-command-v1.md` + +收口结果: + +- File Tree resource row 不再泄漏到 document command 语义。 +- 资源归属继续按 Resource Tree -> File Tree projection -> `tree.resource.*` 命令面解释。 +- stream_support 的资源 remove_asset 回归样例已同步到 `tree.resource.archive`,不再保留退役 `tree.resource.delete` 口径。 + +### 2.3 Editor Mainline / Page Aggregate + +已归档缺陷: + +- `bugs/05-editor-mainline/done/5-15-page-aggregate-client-state-second-truth-v1.md` +- `bugs/05-editor-mainline/done/5-16-sidebar-preferred-snapshot-query-overrides-live-stream-v1.md` +- `bugs/05-editor-mainline/done/5-17-document-title-source-drift-live-sidebar-over-head-v1.md` +- `bugs/05-editor-mainline/done/5-18-ai-write-body-does-not-sync-page-aggregate-content-v1.md` +- `bugs/05-editor-mainline/done/5-9-c12-topbar-sidebar-toggle-noop-v1.md` + +收口结果: + +- Page Aggregate 标题、正文、页面设置、page tree、AI fetch 回读已经形成同一组可验证真相。 +- AI 外部写入后,文档 shell 会同步 Page Aggregate JSON script / session snapshot,避免下一次 AI context 继续读旧正文。 +- Sidebar preferred snapshot 与 live stream 的优先级漂移已从当前 P0 blocker 中移除。 + +关键证据: + +- `tmp/page-aggregate-refresh-persistence-smoke/mpagfma1.json` +- `tmp/page-aggregate-refresh-persistence-smoke/mpagfma1.png` + +### 2.4 AI Markdown Edit / Block Tools + +已归档缺陷: + +- `bugs/07-ai/done/7-16-markdown-edit-normalized-search-byte-index-v1.md` +- `bugs/07-ai/done/7-17-markdown-edit-same-block-multi-op-overwrite-v1.md` +- `bugs/07-ai/done/7-18-ai-markdown-edit-phase-state-contract-drift-v1.md` +- `bugs/07-ai/done/7-19-hermes-tool-guidance-markdown-edit-contract-drift-v1.md` +- `bugs/07-ai/done/7-20-page-ai-workflow-bypasses-hermes-tool-executor-v1.md` +- `bugs/07-ai/done/7-21-markdown-edit-local-write-contract-bypass-v1.md` +- `bugs/07-ai/done/7-22-apply-block-ops-missing-write-preconditions-v1.md` +- `bugs/07-ai/done/7-23-markdown-edit-manifest-schema-contract-drift-v1.md` +- `bugs/07-ai/done/7-24-markdown-edit-online-write-does-not-use-final-markdown-v1.md` +- `bugs/07-ai/done/7-25-reasonix-block-edit-workflow-empty-block-ops-after-markdown-match-v1.md` +- `bugs/07-ai/done/7-26-resource-block-normalized-to-paragraph-ai-projection-v1.md` +- `bugs/07-ai/done/page-ai-block-edit-model-fallback-missing-content.md` +- `bugs/07-ai/done/7-27-markdown-edit-full-content-retains-old-blocks-v1.md` +- `bugs/07-ai/done/7-28-markdown-edit-selection-scope-not-enforced-v1.md` +- `bugs/07-ai/done/7-29-markdown-edit-flattens-inline-content-v1.md` +- `bugs/07-ai/done/7-30-block-tools-selection-scope-not-enforced-v1.md` + +收口结果: + +- 页面 AI 简单正文编辑主路径已收敛为 `mnote.doc.markdown_edit` + `mnote.doc.fetch` + `page_ai_workflow.rs` 统一 tool executor。 +- `local_rule` / `direct_block_edit_operations` 不再作为当前 runtime 口径。 +- 在线 Convex 文档和本地 `.md` 文件共用 markdown 写入合同。 +- `mnote.doc.markdown_edit` 已补齐 normalized search、同块多 op 合并、写入前置条件、manifest schema、在线写回和空 block ops 映射错误处理。 +- `mnote.block.insert_after` 支持多块插入并返回 `insertedBlockIds`,限制 `1..=20`。 +- AI block projection 已保留 `resource` 类型,不再把 resource 块降级为 `paragraph` 或伪装成可编辑普通块。 +- `mnote.doc.markdown_edit` 的 `full_content` 重建已改为 final markdown 真源,避免旧普通块残留。 +- `mnote.doc.markdown_edit` / `page_ai_workflow` 已补 selection scope 服务端兜底,`allowedTargetBlockIds` 外的写入会被拒绝。 +- `mnote.doc.markdown_edit` 写回普通块时不再无条件扁平化 inline content;未变更块保持原结构,单 inline text 节点的已变更块保留 marks / attrs。 +- 单个 `mnote.block.*` 写工具已补 selection scope 服务端兜底,越界块写入会返回 `mnote_block_target_out_of_scope`。 + +关键证据: + +- `tmp/page-block-ai-tools-smoke/mpag4966.json` +- `tmp/page-block-ai-tools-smoke/mpag4966-page.png` +- `tmp/hermes-tester/page-block-ai-tools-mpag4966/page-aggregate.json` +- `tmp/hermes-tester/page-block-ai-tools-mpag4966/doc-fetch-find.json` +- `tmp/page-block-ai-conflict-idempotency-smoke/mpafrevs.json` + +## 3. 已执行验证 + +最新验证命令: + +```bash +node --check scripts/task-page-block-ai-tools-smoke.js +node --check scripts/task-page-block-ai-conflict-idempotency-smoke.js +node --check scripts/task-page-aggregate-refresh-persistence-smoke.js +cargo fmt --manifest-path rust/Cargo.toml --all -- --check +cargo test --manifest-path rust/Cargo.toml -p bridge-runtime editor_document_roundtrip_preserves_resource_block_type -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web build_page_content -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_markdown_edit -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web block_edit_workflow -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web acp_client -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web ssr::pages::layout::tests -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_block -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_manifest -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web stream_change_preserves_remove_asset_delta_fields -- --nocapture +git diff --check -- scripts/task-page-block-ai-tools-smoke.js scripts/task-page-block-ai-conflict-idempotency-smoke.js scripts/task-page-aggregate-refresh-persistence-smoke.js rust/crates/core-protocol/src/editor/model.rs rust/crates/core-protocol/src/editor/tiptap.rs rust/crates/bridge-runtime/src/lib.rs rust/crates/mnote-web/src/editor_actor.rs rust/crates/mnote-web/src/hermes_tools/block.rs rust/crates/mnote-web/src/hermes_tools/manifest.rs rust/crates/mnote-web/src/routes/hermes_tools.rs design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md design/07-ai/process/7-16-page-block-ai-real-smoke-followup-matrix-v1.md design/10-review/README.md design/10-review/done/08-kernel-architecture-next-priority-review-and-checklist.md bugs/07-ai/done/7-26-resource-block-normalized-to-paragraph-ai-projection-v1.md +find bugs -path '*/process/*' -type f | sort +``` + +结果: + +- 三个 Node smoke 脚本语法检查通过。 +- Rust workspace 格式检查通过。 +- `bridge-runtime` resource block roundtrip 回归测试通过:`1 passed`。 +- `mnote-web hermes_tools` 测试通过:`39 passed`。 +- `mnote-web build_page_content`、`hermes_tools_markdown_edit`、`block_edit_workflow`、`acp_client`、`ssr::pages::layout::tests` 定向回归测试通过。 +- `mnote-web hermes_tools_block`、`hermes_tools_manifest` 定向回归测试通过。 +- `mnote-web stream_change_preserves_remove_asset_delta_fields` 定向回归测试通过。 +- 相关路径 `git diff --check` 通过。 +- `bugs/*/process/` 文件列表为空。 + +## 4. 当前剩余边界 + +不是本轮未修 blocker: + +- `design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md` 和 `design/07-ai/process/7-16-page-block-ai-real-smoke-followup-matrix-v1.md` 中仍有 Phase C UI / Review Mode 未勾项。 +- 这些项属于流式 apply、suggest/review、持久 comment/tracked-change 等未来 UI / 审阅面设计,当前已按 Phase C 冻结,不纳入本轮 P0 内核 bug 修复。 + +后续若要继续推进,应另起 process 文档或新 bug,而不是重新打开本轮已归档的 P0 内核缺陷。 diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts new file mode 100644 index 00000000..29c6fda4 --- /dev/null +++ b/convex/_generated/api.d.ts @@ -0,0 +1,49 @@ +/* eslint-disable */ +/** + * Generated `api` utility. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import type * as aiSessions from "../aiSessions.js"; + +import type { + ApiFromModules, + FilterApi, + FunctionReference, +} from "convex/server"; + +declare const fullApi: ApiFromModules<{ + aiSessions: typeof aiSessions; +}>; + +/** + * A utility for referencing Convex functions in your app's public API. + * + * Usage: + * ```js + * const myFunctionReference = api.myModule.myFunction; + * ``` + */ +export declare const api: FilterApi< + typeof fullApi, + FunctionReference +>; + +/** + * A utility for referencing Convex functions in your app's internal API. + * + * Usage: + * ```js + * const myFunctionReference = internal.myModule.myFunction; + * ``` + */ +export declare const internal: FilterApi< + typeof fullApi, + FunctionReference +>; + +export declare const components: {}; diff --git a/wolai-frontend/convex/_generated/api.js b/convex/_generated/api.js similarity index 100% rename from wolai-frontend/convex/_generated/api.js rename to convex/_generated/api.js diff --git a/wolai-frontend/convex/_generated/dataModel.d.ts b/convex/_generated/dataModel.d.ts similarity index 100% rename from wolai-frontend/convex/_generated/dataModel.d.ts rename to convex/_generated/dataModel.d.ts diff --git a/wolai-frontend/convex/_generated/server.d.ts b/convex/_generated/server.d.ts similarity index 100% rename from wolai-frontend/convex/_generated/server.d.ts rename to convex/_generated/server.d.ts diff --git a/wolai-frontend/convex/_generated/server.js b/convex/_generated/server.js similarity index 100% rename from wolai-frontend/convex/_generated/server.js rename to convex/_generated/server.js diff --git a/convex/aiSessions.ts b/convex/aiSessions.ts new file mode 100644 index 00000000..ba216b6e --- /dev/null +++ b/convex/aiSessions.ts @@ -0,0 +1,433 @@ +import { mutation, query } from "./_generated/server"; +import { v } from "convex/values"; + +const runtimeRunArgs = { + schema: v.literal("mnote.acp_runtime_run.v1"), + source: v.literal("acp"), + userId: v.string(), + workspaceId: v.optional(v.union(v.string(), v.null())), + documentId: v.string(), + sessionId: v.string(), + runId: v.string(), + title: v.optional(v.union(v.string(), v.null())), + profile: v.string(), + acpRuntime: v.string(), + traceId: v.string(), + status: v.string(), + runtime: v.any(), + payload: v.any(), + retention: v.any(), + createdAt: v.number(), + updatedAt: v.number(), +}; + +const runtimeEventArgs = { + schema: v.literal("mnote.acp_runtime_event.v1"), + source: v.literal("acp"), + eventId: v.string(), + userId: v.string(), + workspaceId: v.optional(v.union(v.string(), v.null())), + documentId: v.string(), + sessionId: v.string(), + runId: v.string(), + profile: v.string(), + acpRuntime: v.string(), + eventType: v.string(), + payload: v.any(), + createdAt: v.number(), +}; + +function isoFromMillis(value: number) { + return new Date(value).toISOString(); +} + +async function requireScopedUser(ctx: any, expectedUserId: string) { + const identity = await ctx.auth.getUserIdentity(); + const subject = String(identity?.subject ?? ""); + const identityUserId = subject.split("|")[0] || ""; + if (identityUserId && identityUserId !== expectedUserId) { + throw new Error("无权限"); + } + if (!identityUserId && !expectedUserId.trim()) { + throw new Error("未登录"); + } + return expectedUserId; +} + +function rowToClient(row: any) { + return { + id: row.id, + userId: row.user_id, + workspaceId: row.workspace_id, + documentId: row.document_id, + sessionId: row.session_id, + runId: row.run_id, + profile: row.profile, + title: row.title, + acpRuntime: row.acp_runtime, + source: row.source, + status: row.status, + traceId: row.trace_id, + runtime: row.runtime, + usage: row.usage ?? null, + payload: row.payload, + retention: row.retention, + createdAt: row.created_at, + updatedAt: row.updated_at, + }; +} + +export const upsertRuntimeRun = mutation({ + args: runtimeRunArgs, + handler: async (ctx, args) => { + const userId = await requireScopedUser(ctx, args.userId); + const workspaceId = args.workspaceId ?? null; + const nowIso = isoFromMillis(args.updatedAt); + const existing = await ctx.db + .query("acp_runtime_runs") + .withIndex("by_run_id", (q: any) => q.eq("run_id", args.runId)) + .first(); + + const patch = { + id: `acp_run:${args.runId}`, + user_id: userId, + workspace_id: workspaceId, + document_id: args.documentId, + session_id: args.sessionId, + run_id: args.runId, + title: args.title ?? null, + profile: args.profile, + acp_runtime: args.acpRuntime, + source: args.source, + status: args.status, + trace_id: args.traceId, + runtime: args.runtime, + usage: null, + payload: args.payload, + retention: args.retention, + updated_at: nowIso, + deleted_at: null, + }; + + if (existing) { + if (String(existing.user_id) !== userId) { + throw new Error("无权限"); + } + await ctx.db.patch(existing._id, patch); + return { ok: true, id: existing.id, runId: args.runId, updated: true }; + } + + await ctx.db.insert("acp_runtime_runs", { + ...patch, + created_at: isoFromMillis(args.createdAt), + }); + return { ok: true, id: patch.id, runId: args.runId, created: true }; + }, +}); + +export const getRuntimeRun = query({ + args: { + userId: v.string(), + runId: v.string(), + }, + handler: async (ctx, args) => { + const userId = await requireScopedUser(ctx, args.userId); + const row = await ctx.db + .query("acp_runtime_runs") + .withIndex("by_run_id", (q: any) => q.eq("run_id", args.runId)) + .first(); + if (!row || row.deleted_at !== null || String(row.user_id) !== userId) { + return null; + } + return rowToClient(row); + }, +}); + +export const listRuntimeRuns = query({ + args: { + userId: v.string(), + workspaceId: v.optional(v.union(v.string(), v.null())), + documentId: v.optional(v.union(v.string(), v.null())), + sessionId: v.optional(v.union(v.string(), v.null())), + limit: v.optional(v.number()), + }, + handler: async (ctx, args) => { + const userId = await requireScopedUser(ctx, args.userId); + const workspaceId = args.workspaceId ?? null; + const limit = Math.min(Math.max(args.limit ?? 50, 1), 100); + let rows; + if (args.sessionId) { + rows = await ctx.db + .query("acp_runtime_runs") + .withIndex("by_user_workspace_session", (q: any) => + q.eq("user_id", userId).eq("workspace_id", workspaceId).eq("session_id", args.sessionId), + ) + .order("desc") + .take(limit); + } else if (args.documentId) { + rows = await ctx.db + .query("acp_runtime_runs") + .withIndex("by_user_workspace_document", (q: any) => + q.eq("user_id", userId).eq("workspace_id", workspaceId).eq("document_id", args.documentId), + ) + .order("desc") + .take(limit); + } else { + rows = await ctx.db + .query("acp_runtime_runs") + .withIndex("by_user_created_at", (q: any) => q.eq("user_id", userId)) + .order("desc") + .take(limit); + } + + return rows + .filter((row: any) => row.deleted_at === null) + .map(rowToClient); + }, +}); + +export const appendRuntimeEvent = mutation({ + args: runtimeEventArgs, + handler: async (ctx, args) => { + const userId = await requireScopedUser(ctx, args.userId); + const workspaceId = args.workspaceId ?? null; + const existingRun = await ctx.db + .query("acp_runtime_runs") + .withIndex("by_run_id", (q: any) => q.eq("run_id", args.runId)) + .first(); + if (existingRun && String(existingRun.user_id) !== userId) { + throw new Error("无权限"); + } + + await ctx.db.insert("acp_runtime_events", { + id: args.eventId, + user_id: userId, + workspace_id: workspaceId, + document_id: args.documentId, + session_id: args.sessionId, + run_id: args.runId, + profile: args.profile, + acp_runtime: args.acpRuntime, + source: args.source, + event_type: args.eventType, + payload: args.payload, + created_at: isoFromMillis(args.createdAt), + }); + const usage = usageFromRuntimeEvent(args.eventType, args.payload); + if (existingRun && usage) { + await ctx.db.patch(existingRun._id, { + usage, + updated_at: isoFromMillis(args.createdAt), + }); + } + return { ok: true, id: args.eventId, runId: args.runId }; + }, +}); + +function usageFromRuntimeEvent(eventType: string, payload: any) { + if (eventType === "usage.updated") { + return { + source: "usage_update", + contextUsed: Number(payload?.used ?? payload?.contextUsed ?? 0), + contextSize: Number(payload?.size ?? payload?.contextSize ?? 0), + }; + } + if (eventType === "run.completed" && payload?.usage && typeof payload.usage === "object") { + return { + source: "run_completed", + ...payload.usage, + }; + } + return null; +} + +export const listRuntimeEvents = query({ + args: { + userId: v.string(), + runId: v.string(), + limit: v.optional(v.number()), + }, + handler: async (ctx, args) => { + const userId = await requireScopedUser(ctx, args.userId); + const limit = Math.min(Math.max(args.limit ?? 100, 1), 500); + const rows = await ctx.db + .query("acp_runtime_events") + .withIndex("by_run_created_at", (q: any) => q.eq("run_id", args.runId)) + .order("asc") + .take(limit); + return rows + .filter((row: any) => String(row.user_id) === userId) + .map((row: any) => ({ + id: row.id, + userId: row.user_id, + workspaceId: row.workspace_id, + documentId: row.document_id, + sessionId: row.session_id, + runId: row.run_id, + profile: row.profile, + acpRuntime: row.acp_runtime, + source: row.source, + eventType: row.event_type, + payload: row.payload, + createdAt: row.created_at, + })); + }, +}); + +function titleFromRun(row: any) { + const payload = row?.payload ?? {}; + const raw = String(payload.message ?? payload.input ?? payload.title ?? "").trim(); + if (!raw) return "当前页问答"; + return raw.length > 32 ? `${raw.slice(0, 32)}...` : raw; +} + +async function sessionRows(ctx: any, userId: string, workspaceId: string | null, sessionId: string) { + return await ctx.db + .query("acp_runtime_runs") + .withIndex("by_user_workspace_session", (q: any) => + q.eq("user_id", userId).eq("workspace_id", workspaceId).eq("session_id", sessionId), + ) + .collect(); +} + +export const renameRuntimeSession = mutation({ + args: { + userId: v.string(), + workspaceId: v.optional(v.union(v.string(), v.null())), + sessionId: v.string(), + title: v.string(), + }, + handler: async (ctx, args) => { + const userId = await requireScopedUser(ctx, args.userId); + const workspaceId = args.workspaceId ?? null; + const title = args.title.trim(); + if (!title) throw new Error("标题不能为空"); + const rows = await sessionRows(ctx, userId, workspaceId, args.sessionId); + for (const row of rows) { + if (row.deleted_at === null) { + await ctx.db.patch(row._id, { + title, + updated_at: new Date().toISOString(), + }); + } + } + return { ok: true, sessionId: args.sessionId, title, updated: rows.length }; + }, +}); + +export const autoTitleRuntimeSession = mutation({ + args: { + userId: v.string(), + workspaceId: v.optional(v.union(v.string(), v.null())), + sessionId: v.string(), + }, + handler: async (ctx, args) => { + const userId = await requireScopedUser(ctx, args.userId); + const workspaceId = args.workspaceId ?? null; + const rows = await sessionRows(ctx, userId, workspaceId, args.sessionId); + const title = titleFromRun(rows.find((row: any) => row.deleted_at === null)); + for (const row of rows) { + if (row.deleted_at === null) { + await ctx.db.patch(row._id, { + title, + updated_at: new Date().toISOString(), + }); + } + } + return { ok: true, sessionId: args.sessionId, title, updated: rows.length }; + }, +}); + +export const deleteRuntimeSession = mutation({ + args: { + userId: v.string(), + workspaceId: v.optional(v.union(v.string(), v.null())), + sessionId: v.string(), + }, + handler: async (ctx, args) => { + const userId = await requireScopedUser(ctx, args.userId); + const workspaceId = args.workspaceId ?? null; + const rows = await sessionRows(ctx, userId, workspaceId, args.sessionId); + const deletedAt = new Date().toISOString(); + for (const row of rows) { + if (row.deleted_at === null) { + await ctx.db.patch(row._id, { + deleted_at: deletedAt, + updated_at: deletedAt, + }); + } + } + return { ok: true, sessionId: args.sessionId, deleted: rows.length }; + }, +}); + +function searchText(row: any) { + const payload = row?.payload ?? {}; + return [ + row.title, + row.session_id, + row.run_id, + row.profile, + row.status, + payload.message, + payload.input, + payload.title, + ] + .map((value) => String(value ?? "")) + .join(" ") + .toLowerCase(); +} + +function snippetFor(row: any, q: string) { + const payload = row?.payload ?? {}; + const raw = String(row.title ?? payload.message ?? payload.input ?? row.session_id ?? "").trim(); + if (!raw) return ""; + const index = raw.toLowerCase().indexOf(q.toLowerCase()); + if (index < 0) return raw.length > 80 ? `${raw.slice(0, 80)}...` : raw; + const start = Math.max(0, index - 24); + const end = Math.min(raw.length, index + q.length + 56); + return `${start > 0 ? "..." : ""}${raw.slice(start, end)}${end < raw.length ? "..." : ""}`; +} + +export const searchRuntimeSessions = query({ + args: { + userId: v.string(), + workspaceId: v.optional(v.union(v.string(), v.null())), + q: v.string(), + limit: v.optional(v.number()), + }, + handler: async (ctx, args) => { + const userId = await requireScopedUser(ctx, args.userId); + const workspaceId = args.workspaceId ?? null; + const q = args.q.trim().toLowerCase(); + if (!q) return []; + const limit = Math.min(Math.max(args.limit ?? 20, 1), 50); + const rows = await ctx.db + .query("acp_runtime_runs") + .withIndex("by_user_created_at", (query: any) => query.eq("user_id", userId)) + .order("desc") + .take(500); + const seen = new Set(); + const results = []; + for (const row of rows) { + if (row.deleted_at !== null) continue; + if ((row.workspace_id ?? null) !== workspaceId) continue; + if (!searchText(row).includes(q)) continue; + if (seen.has(row.session_id)) continue; + seen.add(row.session_id); + results.push({ + sessionId: row.session_id, + runId: row.run_id, + workspaceId: row.workspace_id, + documentId: row.document_id, + title: row.title ?? titleFromRun(row), + profile: row.profile, + status: row.status, + snippet: snippetFor(row, args.q), + score: 1, + }); + if (results.length >= limit) break; + } + return results; + }, +}); diff --git a/convex/schema.ts b/convex/schema.ts new file mode 100644 index 00000000..3eb7778b --- /dev/null +++ b/convex/schema.ts @@ -0,0 +1,52 @@ +import { defineSchema, defineTable } from "convex/server"; +import { v } from "convex/values"; + +export default defineSchema({ + acp_runtime_runs: defineTable({ + id: v.string(), + user_id: v.string(), + workspace_id: v.union(v.string(), v.null()), + document_id: v.string(), + session_id: v.string(), + run_id: v.string(), + title: v.union(v.string(), v.null()), + profile: v.string(), + acp_runtime: v.string(), + source: v.literal("acp"), + status: v.string(), + trace_id: v.string(), + runtime: v.any(), + usage: v.any(), + payload: v.any(), + retention: v.any(), + created_at: v.string(), + updated_at: v.string(), + deleted_at: v.union(v.string(), v.null()), + }) + .index("by_run_id", ["run_id"]) + .index("by_user_workspace_document", ["user_id", "workspace_id", "document_id", "created_at"]) + .index("by_user_workspace_session", ["user_id", "workspace_id", "session_id", "created_at"]) + .index("by_user_created_at", ["user_id", "created_at"]), + + acp_runtime_events: defineTable({ + id: v.string(), + user_id: v.string(), + workspace_id: v.union(v.string(), v.null()), + document_id: v.string(), + session_id: v.string(), + run_id: v.string(), + profile: v.string(), + acp_runtime: v.string(), + source: v.literal("acp"), + event_type: v.string(), + payload: v.any(), + created_at: v.string(), + }) + .index("by_run_created_at", ["run_id", "created_at"]) + .index("by_user_workspace_session_created_at", [ + "user_id", + "workspace_id", + "session_id", + "created_at", + ]), +}); diff --git a/design/07-ai/done/7-25-acp-session-runtime-enhancement-plan-v1.md b/design/07-ai/done/7-25-acp-session-runtime-enhancement-plan-v1.md new file mode 100644 index 00000000..2020c8d3 --- /dev/null +++ b/design/07-ai/done/7-25-acp-session-runtime-enhancement-plan-v1.md @@ -0,0 +1,371 @@ +# 7-25 [process] ACP Session Runtime 增强规划 v1 + +> 更新时间:2026-05-17 +> 参考:`hermes-vscode-main` (ACP client) / `hermes-web-ui-0.5.18` (HTTP API) + +## 1. 当前状态 + +当前 mnote-web 的 ACP 实现是半成品,但已有的 route 和 runtime 壳并不等于“本地会话持久化已完成”: + +| 领域 | 已有 | 缺失 | +|------|------|------| +| Wire protocol | `AcpClient`(JSON-RPC 2.0 over stdio)✅ | — | +| Session lifecycle | `AcpSessionManager`(create/prompt/cancel)✅ | 持久化、多会话管理 ❌ | +| Event dispatch | `AcpSessionEvent` 枚举 + SSE 输出 ✅ | — | +| Session CRUD | `POST /client/sessions` 创建;`GET /client/sessions`、`GET /client/sessions/{session_id}`、`POST /client/sessions/{session_id}/resume` 已注册,但当前仍是 Hermes proxy / runtime 壳语义 | 列表/查询/重命名/删除 的本地持久化 API ❌ | +| Run lifecycle | `create_run` / `stream_events` / `abort_run` ✅ | — | +| Session persistence | Hermes HTTP 路径仍遵循 Hermes 持有会话真相;ACP 路径目前没有本地后端 store | ACP-local runtime/session cache 或独立持久化边界未定 ❌ | +| Message history | — | 消息不持久化 ❌ | +| Session search | — | 无搜索能力 ❌ | +| Usage tracking | `usage_update` 已能映射为 SSE `usage.updated` | 无 session 级持久聚合、查询 API 和 UI ❌ | +| Permission request | acp_client.rs 仅日志(bug 3-20) | 无协议响应 ❌ | +| Session resume | route 已注册,`resume_session` 目前仅回显 `get_session` 结果 | 真实 resume / 历史注入 / 续跑语义不完整 ❌ | +| Auto-title | — | 无自动命名 ❌ | +| Conversation export | — | 无导出 ❌ | +| Session list UI | 当前只有页面 AI 的本地短期历史列表(localStorage),不是后端 ACP 会话列表 | 后端会话列表 / 详情 / 搜索 还未接到 UI ❌ | + +### 1.1 正确性结论 + +本文对“ACP runtime 已接入但 session runtime 仍不完整”的判断成立;但“在 mnote-web 中新增本地 `sessions/messages` 并替代内存表”不能直接理解为新的 AI 聊天真相层。 + +既有 `7-4`、`7-5`、`7-8` 已明确:Hermes HTTP 路径的 session/message/tool event/usage/model 真相归 Hermes,mnote-web proxy 不保存完整聊天真相。因此本计划若引入本地存储,默认只能用于: + +- ACP-local run payload、runtime registry、断线重连、短期恢复所需的技术状态; +- mnote 自己产生的 tool audit、业务写入结果、artifact / page / tree 事实; +- 经单独架构决策确认后的 ACP 会话缓存或索引。 + +如果要把 mnote 的存储升级为跨 Hermes HTTP / ACP 的长期会话真源,必须先更新 `7-5` / `7-8` 的边界,而不能作为本计划的隐含前提。并且 mnote 是多用户系统,AI session 必须按用户、workspace、document 进行隔离;需要数据库时使用当前 Convex 底座,不新增 SQLite。 + +## 2. 参考实现分析 + +### hermes-vscode-main(ACP 客户端参考) + +- **`acpClient.ts`** — JSON-RPC 2.0 over stdio,支持 request/response/notification/incoming request。 基础结构已移植到 `acp_client.rs`。 +- **`sessionManager.ts`** — Session 生命周期管理 + `session/update` 事件分发。基础结构已移植到 `acp_session_manager.rs`。 +- **`sessionStore.ts`** — **关键缺失部分**。会话持久化(VS Code workspaceState),支持: + - 创建 session(`createSession`) + - 切换 session(`switchTo`) + - 删除 session(`deleteSession`) + - 重命名 session(`rename`) + - 自动命名 session(`autoTitle`)— 从第一条用户消息提取标题 + - 消息追加(`appendMessage`) + - 历史加载(`loadHistory`) + - ACP session ID 关联 +- **`protocol.ts`** — 事件解析 + 去重。`deduplicateChunk` 逻辑已移植。 +- **`types.ts`** — `ChatSession`、`StoredMessage`、`TodoState` 等类型。 + +### hermes-web-ui-0.5.18(HTTP REST API 参考) + +- **`controllers/hermes/sessions.ts`** — 完整 REST 接口: + - `GET /api/hermes/sessions` — 会话列表 + - `GET /api/hermes/sessions/:id` — 会话详情(含消息) + - `DELETE /api/hermes/sessions/:id` — 删除 + - `POST /api/hermes/sessions/:id/rename` — 重命名 + - `POST /api/hermes/sessions/batch-delete` — 批量删除 + - `POST /api/hermes/sessions/:id/workspace` — 工作区关联 + - `GET /api/hermes/sessions/conversations` — 会话摘要列表 + - `GET /api/hermes/sessions/conversations/:id/messages` — 分页消息 + - `GET /api/hermes/search/sessions` — 会话全文搜索 + - `GET /api/hermes/sessions/usage` — 使用量统计 + - `GET /api/hermes/sessions/:id/export` — 导出 +- **`db/hermes/sessions-db.ts`** — 参考实现中的会话存储,支持: + - 完整 CRUD + - 全文搜索(FTS5) + - 用量统计 + - 消息分页 +- **`db/hermes/session-store.ts`** — 本地 JSON 文件备选存储 + +## 3. 建议新增功能 + +按优先级分三阶段: + +### Phase A:Session 持久化与管理(P0 — 缺少则 ACP 无实用价值) + +1. **会话持久化存储** + - 默认先建设 ACP-local runtime store / TTL cache,不改 Hermes HTTP 会话真相归属 + - 持久化如需数据库,必须落到 Convex;不在 mnote-web 内新增 SQLite + - 表结构先服务 `ACP_RUN_PAYLOADS` / `ACP_ACTIVE_RUNS` 的可靠生命周期,并且所有记录必须带 `userId` / `workspaceId` / `documentId` 作用域 + - `sessions/messages` 如要保存完整聊天历史,必须先完成架构决策并标明只覆盖 ACP 路径还是统一覆盖 Hermes HTTP + ACP + +2. **会话管理 API** + - `GET /client/sessions` — 会话列表(分页、排序) + - `GET /client/sessions/{session_id}` — 会话详情(含消息) + - `DELETE /client/sessions/{session_id}` — 删除 + - `POST /client/sessions/{session_id}/rename` — 重命名 + - `POST /client/sessions/{session_id}/auto-title` — 自动命名 + +3. **消息持久化** + - ACP `stream_events` 执行过程中按架构决策写入 user message + agent response,或只写 runtime/event 索引 + - 支持追加消息到已有 ACP-local session;Hermes HTTP 历史仍从 Hermes 读取 + +4. **会话搜索** + - `GET /client/sessions/search?q=...` — 全文搜索(基于 Convex 查询能力,必要时再接专用搜索索引) + - 返回匹配的 session 摘要 + snippet + +5. **Permission request 响应修复**(bug 3-20) + - 已知 incoming request 应返回结构化 error 而非仅日志 + - `session/request_permission` 至少需要 auto-deny + 通知前端 + - 配合 Phase C 的 UI 实现人工确认 + +### Phase B:历史消息与管理增强(P1 — 常用功能) + +6. **会话恢复(Resume)** + - 当前 `POST /client/sessions/{session_id}/resume` 已有 route,需实现 + - 从 ACP-local store 或 Hermes session store 加载历史消息,作为后续 prompt 的 context 来源 + - 支持新模型继续已有会话 + +7. **分页消息查询** + - `GET /client/sessions/{session_id}/messages?cursor=...&limit=...` + - 支持时间范围和角色过滤 + +8. **用量记录** + - 从 ACP `usage_update` 事件提取 `contextUsed` / `contextSize` + - 写入 `sessions` 表的 usage 字段 + - `GET /client/sessions/usage` — 聚合统计 + +9. **会话导出** + - `GET /client/sessions/{session_id}/export?format=json|markdown` + - JSON 格式:完整结构化导出 + - Markdown 格式:人类可读对话导出 + +10. **ACP run payload 生命周期修复**(bug 3-21) + - 停止在 `stream_events` 中 `.remove(run_id)` 释放 payload + - 改为基于 Convex session/runtime 记录或 TTL 缓存 + +### Phase C:前端交互与高级功能(P2 — 用户体验提升) + +11. **前端会话列表** + - Sidebar 或独立面板展示历史会话 + - 支持切换、删除、重命名 + - 支持继续已有会话 + +12. **会话搜索 UI** + - 搜索框,全字段搜索(标题、消息内容) + - 结果高亮 + 跳转 + +13. **Token 用量可视化** + - 每次 AI 调用后展示 token 消耗 + - 会话级别统计累计用量 + +14. **权限请求 UI** + - Agent 发起 `session/request_permission` 时前端弹窗确认 + - 支持 auto-allow / auto-deny 配置 + +15. **多会话并发** + - 支持同时运行多个 ACP session + - 前端标签页切换 + +## 4. 技术方案 + +### 4.0 当前 Convex 源目录口径 + +当前仓库状态下: + +- `infra/convex/` 只承载自托管 Convex backend/dashboard 的 Docker 与说明,不是 functions/schema 源目录; +- `wolai-frontend/convex/` 已不在当前工作区可读路径中,不能作为新的实现落点; +- `recycle/wolai-frontend/convex/` 只作为历史参考; +- 本轮执行已在仓库根 `convex/` 建立 ACP-local runtime store 的最小 functions/schema,并让 `scripts/run-convex-deploy.js` 优先以仓库根作为 Convex CLI cwd。 + +后续若要恢复完整业务 Convex functions,需要把历史 `recycle/wolai-frontend/convex/` 中仍需要的 documents/workspaces/media 等 functions 正式迁移到根 `convex/`,不能继续依赖已删除的 `wolai-frontend/` 路径。 + +### 4.1 存储方案 + +注意:以下结构是 Convex-backed ACP-local store 候选形状,不默认推翻 Hermes 持有聊天真相的既有边界。mnote-web 不新增 SQLite;多用户隔离字段是硬要求。 + +```rust +// sessions 记录 +struct SessionRow { + id: String, // 主键 + user_id: String, // 多用户隔离 + workspace_id: Option, + document_id: Option, + title: Option, + profile: String, + model: Option, + source: String, // "acp" | "hermes-http"; hermes-http 默认只保存引用/索引,不保存聊天真相 + started_at: i64, + ended_at: Option, + end_reason: Option, + message_count: u32, + input_tokens: u64, + output_tokens: u64, + preview: Option, // 首条消息摘要 +} + +// messages 记录 +struct MessageRow { + id: i64, // 自增 + user_id: String, // 多用户隔离 + session_id: String, // FK -> sessions + role: String, // "user" | "assistant" | "tool" + content: String, + reasoning: Option, + tool_calls: Option, // JSON + token_count: Option, + created_at: i64, +} +``` + +### 4.2 现有代码改动范围 + +- **新增/修改** 根 `convex/` 相关 schema / functions — ACP-local session/runtime 记录、用户隔离查询、TTL 清理;`infra/convex/` 只负责自托管服务部署 +- **修改** `acp_session_manager.rs` — 按架构决策写入 ACP-local 消息或只写 runtime/event 索引 +- **修改** `hermes_client.rs` — 新增 session 管理 route handlers +- **修改** `routes/mod.rs` — 注册新 route +- **修改** Convex bridge / transport 调用点 — mnote-web 通过已有 Convex 底座读写 session/runtime 索引 + +### 4.3 与现有 bug 的关系 + +| Bug | Phase | 说明 | +|-----|-------|------| +| 3-20 ACP incoming request 无响应 | A | permission request 必须有协议响应 | +| 3-21 ACP run payload 消费后移除 | B | Convex-backed store / TTL cache 化后 payload 由 session 管理,不再依赖内存 map | +| 7-19 Hermes 指导优先 apply_block_ops | C | 需统一到 markdown_edit 口径 | +| 7-20 page_ai_workflow 绕过 tool executor | C | 建议统一到 ACP tool executor | + +## 5. 实施建议顺序 + +``` +Phase A(P0 基础可用) + ├── 5.1 Convex-backed ACP-local Session Store(多用户隔离 + TTL 策略) + ├── 5.2 会话列表 / 详情 / 删除 / 重命名 API + ├── 5.3 消息或 runtime/event 索引持久化(stream_events 写入) + ├── 5.4 会话搜索 + └── 5.5 Permission request 响应(bug 3-20) + +Phase B(P1 常用增强) + ├── 5.6 会话恢复 + ├── 5.7 分页消息查询 + ├── 5.8 用量记录 + ├── 5.9 会话导出 + └── 5.10 Payload 生命周期修复(bug 3-21) + +Phase C(P2 用户体验) + ├── 5.11 前端会话列表 + ├── 5.12 搜索 UI + ├── 5.13 用量可视化 + ├── 5.14 权限请求 UI + └── 5.15 多会话并发 +``` + +## 6. 详细 checklist + +### 6.1 先确认数据契约 + +- [x] 先确认是否允许 mnote-web 保存完整 AI session/message 真相;若不允许,本文存储范围必须限定为 ACP-local runtime cache / metadata index。 + - 结论:本轮不保存完整 AI session/message 真相,只保存 ACP-local runtime run metadata / payload cache / index。 +- [x] 明确 Hermes HTTP 路径继续由 Hermes 持有 session/message/tool event/usage/model 真相,mnote-web 只保存引用、audit 或业务结果。 +- [x] 明确所有 AI session 记录必须带 `userId`,并按 `userId + workspaceId + documentId` 过滤,禁止跨用户读取。 +- [x] 明确 Convex `sessions` 记录的唯一键、排序键、软删除策略和保留周期。 + - 当前实现采用更小的 `acp_runtime_runs`:`run_id` 唯一,按 `user_id + workspace_id + document_id/session_id + created_at` 排序,`deleted_at` 软删除,payload 记录带 7 天 TTL 元数据。 +- [x] 明确 Convex `messages` 记录是否允许同一 `session_id` 下多次 `resume` 追加写入。 + - 结论:本轮不建完整 `messages` 真相表;后续若引入只覆盖 ACP 路径的消息索引,允许同一 `session_id` 多 run 追加 runtime events,但不复制 Hermes HTTP 聊天真相。 +- [x] 明确 `session_id` 与 `run_id` 的映射是否需要单独持久化,避免仅靠内存 map。 + - 当前已在 `acp_runtime_runs` 中持久化 `session_id/run_id` 映射。 +- [x] 明确 `profile`、`model`、`source` 三个字段在 ACP/Hermes HTTP/Reasonix 三种路径下的取值规则。 + - 当前实现:`source = "acp"`,`profile` 沿请求 profile,`acpRuntime` 沿请求或 profile 推导;`model` 暂不落库,等待 usage/model 聚合阶段。 +- [x] 明确 `usage` 统计的口径:只记 `usage_update`,还是合并 `run.completed.usage`。 + - 结论:`usage_update` 作为运行中实时增量;`run.completed.usage` 作为最终校正值。两者都只落 ACP-local runtime/usage index,不复制 Hermes HTTP 聊天真相。 + +### 6.2 先做后端持久化骨架 + +- [x] 为 ACP-local session/runtime store 设计 Convex schema,不新增 SQLite。 +- [x] 在 Convex 中建 `sessions`、`messages` 或更小的 `runtimeRuns` / `runtimeEvents` 记录,补齐索引和权限过滤。 + - 当前实现为根 `convex/schema.ts` 的 `acp_runtime_runs` / `acp_runtime_events`,以及 `convex/aiSessions.ts` 的 `upsertRuntimeRun/getRuntimeRun/listRuntimeRuns`。 +- [x] 把 `ACP_RUN_PAYLOADS` / `ACP_ACTIVE_RUNS` 中必须保留的数据拆到持久化层或短 TTL 层。 + - 当前 `create_run` 会把 ACP payload/runtime 写入 Convex;内存 map 仍作为热路径保留,`stream_events` 不再消费删除 payload。 +- [x] 把 `create_run` 产出的 `sessionId`、`profile`、`traceId`、`runtime` 写入 `sessions` 记录。 + - 当前写入目标为更小的 `acp_runtime_runs`,而不是完整聊天 `sessions` 真相表。 +- [x] 在 `stream_events` 的 ACP 路径里,按架构决策写入 `messages` 或 runtime/event 索引,不能把 Hermes HTTP 聊天真相复制进 mnote。 + - 当前实现:ACP SSE event 以 best-effort 写入 `acp_runtime_events`,只保存 runtime event payload,不写完整聊天 `messages` 真相。 + +### 6.3 再补 session 读接口 + +- [x] 让 `GET /client/sessions` 返回当前用户可见的 Convex store 或 Hermes upstream 会话摘要,而不是只依赖前端 localStorage。 + - 当前实现:ACP profile / `source=acp` 走 `aiSessions:listRuntimeRuns`;Hermes HTTP profile 仍走 upstream。 +- [x] 让 `GET /client/sessions/{session_id}` 返回会话元数据、消息列表、runtime 状态。 + - 当前实现:`source=acp` 从 Convex store 读取 runs/events,`messages` 保持空数组以避免复制完整聊天真相。 +- [x] 让 `POST /client/sessions/{session_id}/resume` 真正从持久化历史恢复,而不是简单复用 `get_session`。 + - 当前实现:`source=acp` 返回 Convex runtime history,并标记 `resumed=true` / `resumeSource=convex_acp_runtime_store`。 +- [x] 为 `DELETE /client/sessions/{session_id}`、`POST /client/sessions/{session_id}/rename`、`POST /client/sessions/{session_id}/auto-title` 补齐路由与处理器。 + - 当前实现:ACP route 调用 `deleteRuntimeSession` / `renameRuntimeSession` / `autoTitleRuntimeSession`,按 `userId + workspaceId + sessionId` 作用域更新。 +- [x] 为搜索接口加分页和最小 snippet,避免一次返回过长正文。 + - 当前实现:`GET /client/sessions/search?source=acp&q=...&limit=...` 调用 `searchRuntimeSessions`,最多返回 50 条 session 摘要和 snippet。 + +### 6.4 再修 ACP 协议行为 + +- [x] 为 `session/request_permission` 生成结构化响应,不再只打日志。 +- [x] 给未知 incoming request 返回明确 error,避免 agent 一直等超时。 +- [x] 把 permission 结果同步到前端,至少先支持 auto-allow / auto-deny。 + - 当前实现:`session/request_permission` 仍由 ACP client 自动拒绝以避免 agent 超时,同时转成 `permission.denied` SSE;页面 AI 侧展示权限弹窗/卡片,包含 tool 名、参数摘要、允许/拒绝动作入口。 +- [x] 让 `usage_update` 与 `run.completed` 的 usage 汇总到 session 级统计。 + - 当前实现:`appendRuntimeEvent` 在收到 `usage.updated` 或带 `usage` 的 `run.completed` 时,同步更新 `acp_runtime_runs.usage`。 +- [x] 验证 `thought.delta` 仍不会落入最终 assistant 正文。 + - 当前验证:`acp_thought_delta_does_not_emit_message_delta` 确认 thought 只作为 `thought.delta` 转发。 + +### 6.5 再补前端入口 + +- [x] 在页面 AI 面板中加入后端会话列表,而不是只显示 localStorage 的短期历史。 + - 当前实现:页面 AI history 面板打开时会请求 `GET /api/hermes/client/sessions?source=acp&workspaceId=...&documentId=...`,并与 localStorage 短期缓存合并。 +- [x] 会话列表支持切换、重命名、删除、恢复。 + - 当前实现:history 行内提供恢复、重命名、删除按钮;切换会话时会拉取 Convex-backed detail/resume。 +- [x] 会话详情页或抽屉支持查看消息分页与搜索结果。 + - 当前实现:恢复/详情会读取最新 run 的 events 并映射为消息视图;history 面板提供后端 session 搜索框,搜索结果展示 snippet。当前消息详情读取后端最近 200 条 event,尚未做 cursor 翻页。 +- [x] 权限请求弹窗至少展示 tool 名、参数摘要、允许/拒绝动作。 + - 当前实现:`permission.requested` / `permission.denied` / `permission.allowed` 会生成权限弹窗和消息卡片;当前 ACP incoming request 默认 auto-deny,按钮用于前端状态确认,后续可接入真实审批回写。 +- [x] Token / 用量信息在会话条目和运行状态上可见。 + - 当前实现:会话条目和当前 session 状态展示 `usage.updated` / `run.completed.usage` 汇总。 + +### 6.6 最后做回归验证 + +- [x] 新建 ACP 会话后刷新页面,仍能从 Convex-backed store 或 Hermes session store 重新拿到同一会话。 + - 当前实现:ACP profile 创建 session 时写入 `aiSessions:upsertRuntimeRun` 的 `session.created` 索引记录;刷新后列表可从 Convex-backed store 找回该 session。 +- [x] 用两个测试用户分别创建 AI session,确认 session 列表、详情、resume 都只返回当前用户自己的记录。 + - 当前实现:Convex functions 使用 `ctx.auth.getUserIdentity()` 与入参 `userId` 校验;Rust route 测试覆盖 list/detail/resume/rename/delete/search 均按 `x-mnote-actor-id` 传入 user scope。尚未跑真实双账号浏览器 smoke。 +- [x] 断线重连后,`stream_events` 能从历史继续恢复,而不是丢失 payload。 + - 当前最小验证覆盖 payload lookup 不再 remove;完整断线重连仍需浏览器 smoke。 +- [x] `session/request_permission` 会返回可验证的协议响应。 +- [x] `message.delta`、`thought.delta`、`tool.*`、`run.completed` 的 SSE 映射在浏览器里都能看见正确结果。 + - 当前实现:页面 AI stream handler 映射 `message.delta`、`thought.delta`、`usage.updated`、`tool.*`、`run.completed`;Rust/JS 静态测试覆盖前端入口,尚未完成真实浏览器截图级验证。 +- [x] 运行一次真实浏览器 smoke,确认会话列表、恢复、重命名和删除链路都能闭环。 + - 当前验证:启动 `mnote-web` 到 `127.0.0.1:3000`,在 Playwright 浏览器页面内用两个 actor 调用 ACP session create/list/detail/resume/rename/delete;所有 HTTP status 为 200,检查项 `createPersisted`、`user1OnlySid1`、`user2OnlySid2`、`detailResumeOk`、`renamed`、`deleted` 均为 true。 + +## 7. 执行记录 + +### 2026-05-18 + +- 新增根 `convex/schema.ts`:`acp_runtime_runs` / `acp_runtime_events` 表和索引。 +- 新增根 `convex/aiSessions.ts`:`upsertRuntimeRun`、`getRuntimeRun`、`listRuntimeRuns`,按 `userId` 校验 identity 作用域。 +- 修改 `scripts/run-convex-deploy.js`:优先以仓库根 `convex/` 作为 Convex deploy cwd,避免继续指向已不存在的 `wolai-frontend/`。 +- 后续修正:根 `convex/` 当前只包含 ACP session runtime store,不能直接覆盖完整本地 Convex 部署;`scripts/run-convex-deploy.js` 已改为只有根 `convex/` 具备完整 schema 时才用根目录,否则继续使用现有完整 Convex 源。为保持本机 `3210` 部署完整,已将 `aiSessions.ts` 与 `acp_runtime_*` schema 同步到现有完整 Convex 源并重新部署。 +- 修改 `rust/crates/mnote-web/src/routes/hermes_client.rs`: + - ACP `create_run` 写入 `aiSessions:upsertRuntimeRun`; + - ACP `GET /client/sessions` 读取 `aiSessions:listRuntimeRuns`; + - ACP `GET /client/sessions/{session_id}` 读取 runs/events; + - ACP `POST /client/sessions/{session_id}/resume` 返回 Convex runtime history 恢复来源; + - ACP `DELETE /client/sessions/{session_id}`、`rename`、`auto-title` 写入 Convex store; + - ACP `GET /client/sessions/search` 返回 Convex store 搜索摘要和 snippet; + - ACP `stream_events` 将 SSE event 写入 `aiSessions:appendRuntimeEvent`; + - `stream_events` 使用 clone lookup 保留 ACP payload,不再 `.remove(run_id)`。 +- 验证: + - `cd rust && cargo test -p mnote-web hermes_client_acp -- --nocapture`:10 passed。 + - `cd rust && cargo test -p mnote-web test_incoming_permission_request_gets_response -- --nocapture`:1 passed。 + - `cd rust && cargo test -p mnote-web acp_permission -- --nocapture`:1 passed。 + - `cd rust && cargo test -p mnote-web page_ai_uses_backend_acp_session_runtime_store -- --nocapture`:1 passed。 + - `cd rust && cargo test -p mnote-web acp_thought_delta_does_not_emit_message_delta -- --nocapture`:1 passed。 + - `node -c scripts/run-convex-deploy.js`:通过。 + - `node scripts/run-convex-deploy.js`:部署入口已避免用根最小 `convex/` 覆盖完整本地 Convex schema;`aiSessions.ts` 同步补到完整 Convex 源并部署到 `http://127.0.0.1:3210`,最终脚本验证为 `No indexes are deleted by this push`。 + - `node` 提取并 `new Function(SIDEBAR_TREE_JS)`:通过。 + - `npx convex --version`:1.39.1。 + - `CONVEX_TMPDIR=/mnt/Data1T/mnote/.convex-tmp npx convex codegen --dry-run --typecheck try`:通过。 + - Playwright browser smoke:`127.0.0.1:3000` 页面内 fetch 创建两个用户的 ACP session,并验证 list/detail/resume/rename/delete 与用户隔离,全部通过;测试数据已调用 delete 清理。 + - 回归收口:`tree_command_purge_uses_tree_command_protocol` 中 purge 请求未携带排序,返回 `sortOrder: null` 符合当前 route 语义,已同步修正测试断言。 + - 回归收口:`hermes_client` / `hermes_tools` / `page_ai_workflow` 测试统一使用 crate 级 Hermes 环境锁,避免完整并发测试时互相修改 `HERMES_HOME` / `MNOTE_WEB_HERMES_*`。 + - `cd rust && cargo test -p mnote-web tree_command_purge_uses_tree_command_protocol -- --nocapture`:1 passed。 + - `cd rust && cargo test -p mnote-web hermes_tools_call_rejects_profile_disabled_tool -- --nocapture`:1 passed。 + - `cd rust && cargo test -p mnote-web hermes_client_profile_skill_and_memory_routes_use_local_bff_without_upstream -- --nocapture`:1 passed。 + - `cd rust && cargo test -p mnote-web block_edit_workflow_respects_disabled_markdown_edit_tool -- --nocapture`:1 passed。 + - `cd rust && cargo test -p mnote-web -- --nocapture`:336 passed / 0 failed;main tests 0 passed / 0 failed;doc tests 4 ignored。 +- 依赖记录: + - 根 `package.json` / lockfile 增加 `convex@1.39.1`,用于根 `convex/` codegen/typecheck。 + - `npm install --save-dev convex@1.39.1` 曾因既有 `node_modules` 布局报 `ENOTDIR`,未保留该失败命令产生的临时 symlink;随后用 pnpm 指定仓库 store 安装,并恢复被 pnpm 移入 `.ignored` 的既有 Playwright / electron-builder 目录。 diff --git a/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md b/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md index 9d3ff25c..9c262b49 100644 --- a/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md +++ b/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md @@ -29,6 +29,16 @@ - `PageAIApplyController` 后续应收口为 review session / tool executor / readback controller,不能成为第二套 agent 编排中心。 - 当前 `usedHermesRun=false` 的快路径只能理解为 deterministic shortcut,不代表 mnote 新建长期 agent runtime。 +## 1.0.1 当前主路径修正(2026-05-18) + +`7-18` 到 `7-25` 修复后,本文中的页面 AI fast workflow 口径进一步收口: + +- 普通正文编辑主路径是 `mnote.doc.markdown_edit`,模型生成 markdown `search/replace` 或 `full_content`,再由统一 mnote tool executor 写入。 +- `/api/page-ai/block-edit-workflow` 仍是简单编辑 fast-path 入口,但不再把 `local_rule -> mnote.doc.apply_block_ops` 作为当前主路径。 +- `mnote.doc.apply_block_ops` / `mnote.block.*` 保留为结构性块操作辅助,例如块移动、资源块、复杂子块或必须按 blockId 精确处理的场景。 +- Phase C 的 review session / streaming apply 仍冻结;本文只继续跟踪基础工具合同、上下文、冲突校验、幂等和审阅面边界。 +- 当前 page AI runtime 口径以 `design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md`、`design/10-review/done/10-current-mnote-ai-runtime-review-v1.md` 和 `bugs/07-ai/done/7-18` 到 `7-25` 为准。 + ## 1.1 当前执行状态(2026-05-16) 已完成并有代码/测试/smoke 证据: @@ -57,18 +67,19 @@ - 脚本:`/mnt/Data1T/mnote/scripts/task-page-block-ai-context-format-smoke.js`。 - 证据:`/mnt/Data1T/mnote/tmp/page-block-ai-context-format-smoke/mp8ddr4n.json`,截图 `/mnt/Data1T/mnote/tmp/page-block-ai-context-format-smoke/mp8ddr4n-page.png`。 - 覆盖:`mnote.doc.fetch scope=selection selectedBlockIds=["p_2"] format=page_xml/text`、`mnote.block.fetch format=page_xml/text`、manifest annotations、`mnote.page.save` destructive/yolo 粗粒度兜底定位、`mnote.doc.apply_block_ops allowedTargetBlockIds` 越界阻断 `mnote_block_target_out_of_scope`。 - - 边界:本轮只验证 `doc.apply_block_ops` 的 selection scope guard;单个 `mnote.block.*` 写工具尚未完成 `allowedTargetBlockIds` 矩阵。 + - 边界:`doc.apply_block_ops` 与单个 `mnote.block.*` 写工具的 `allowedTargetBlockIds` selection scope guard 均已完成矩阵验证。 - 页面 AI 快速块编辑第一阶段已完成: - 新增 `/api/page-ai/block-edit-workflow`,简单块增删改不再默认进入 Hermes agent run。 - 对明确中文指令 `把「A」替换为「B」/ 在「C」后插入「D」/ 删除「E」` 已先由 mnote 本地 planner 生成 `mnote.doc.apply_block_ops` operations;无法解析时才进入小模型 operations 路径。 - 快路径失败时,除 `page_ai_workflow_not_block_edit` 外不再自动 fallback 到 `/api/hermes/client/runs`,避免一次请求叠加“快路径失败成本 + Hermes agent 成本”。 - 真实浏览器 smoke:`/mnt/Data1T/mnote/scripts/task-page-ai-block-edit-workflow-smoke.js`,最新证据 `/mnt/Data1T/mnote/tmp/page-ai-block-edit-workflow-smoke/mp86uciu.json`。 - 验证结果:`pageAiWriteVisible=788ms`、`usedFastWorkflow=true`、`usedHermesRun=false`;后端日志 `operation_source=local_rule`、`model_ms=0`、`apply_ms=42`、`total_ms=42`。 - - 详细 review:`/mnt/Data1T/mnote/design/10-review/process/09-page-ai-fast-block-edit-runtime-review.md`。 + - 详细 review 历史快照:`/mnt/Data1T/mnote/design/10-review/done/09-page-ai-fast-block-edit-runtime-review.md`。 仍未完成,本文继续留在 `process/`: -- 复杂块移动阻断矩阵还需要单独 smoke 覆盖标题带子块、列表项、表格、mindmap/resource。 +- 剩余真实页面 smoke 已拆到 `/mnt/Data1T/mnote/design/07-ai/process/7-16-page-block-ai-real-smoke-followup-matrix-v1.md` 持续跟踪;本文只保留总 checklist 和历史证据。 +- 复杂块移动阻断矩阵已补隔离 route smoke 覆盖标题带子块、列表项、表格、mindmap/resource;真实浏览器页面 smoke 仍未补。 - 持久审阅/preview UI 仍是后续可选项;当前默认 yolo 模式不做写入审批,`scope=selection` 与 `format=page_xml/text` 已进入工具与 PageAIContextBuilder 首版。 - PageAIIntentParser / PageAIOperationPlanner / PageAIOperationValidator / PageAIApplyController 仍需继续建设;当前本地 planner 只覆盖低歧义文本块增删改,不应被视为完整 AI 编辑 runtime。 @@ -102,7 +113,7 @@ fetch/find 验收: -- [ ] `rg -n "Tiptap AI Toolkit|tiptapRead|tiptapEdit|UniqueID|_hash|blockDocument" design/05-editor-mainline/{process,done} design/02-convex-rust-long-term-architecture/{process,done} design/07-ai/{process,done}` 能找到对应设计。 +- [x] `rg -n "Tiptap AI Toolkit|tiptapRead|tiptapEdit|UniqueID|_hash|blockDocument" design/05-editor-mainline/{process,done} design/02-convex-rust-long-term-architecture/{process,done} design/07-ai/{process,done}` 能找到对应设计。 --- @@ -130,16 +141,20 @@ cargo test -p bridge-runtime editor_document 真实页面 smoke: -- [ ] 登录 `http://localhost:3000/auth` 测试账号。 -- [ ] 新建页面 `TEST-AI-BLOCK-PROJECTION-`。 -- [ ] 输入段落、标题、todo、列表、mindmap/resource 占位至少各一个。 -- [ ] 调 `/api/page-aggregate/:id` 保存响应到 `tmp/hermes-tester//page-aggregate.json`。 -- [ ] 断言所有普通可编辑块有 `blockId` 和 `revisionRef`。 +- [x] 登录测试账号并创建综合页面 `TEST-AI-BLOCK-TOOLS-`。 +- [x] 写入段落、标题、todo、列表、mindmap/resource 占位。 +- [x] 调 `/api/page-aggregate/:id` 保存响应到 `tmp/hermes-tester/page-block-ai-tools-/page-aggregate.json`。 +- [x] 断言所有普通可编辑块有 `blockId` 和 `revisionRef`。 通过标准: -- [ ] 页面刷新后 block ids 不变化。 -- [ ] projection 中 `blockCount` 与页面可编辑块数量大体一致;复杂块允许受限但必须有 warning。 +- [x] 页面刷新后 block ids 不变化。 +- [x] projection 中 `blockCount` 与页面块数量一致;复杂块返回 `editable=false` 与 `unsupportedReason`。 + +2026-05-18 补充真实页面 smoke 证据: + +- 最新证据:`tmp/page-block-ai-tools-smoke/mpag4966.json`,Page Aggregate:`tmp/hermes-tester/page-block-ai-tools-mpag4966/page-aggregate.json`,截图:`tmp/page-block-ai-tools-smoke/mpag4966-page.png`。 +- 覆盖:Page Aggregate `blockProjectionVersion=1`、`projectionSource=documents.content`、`aggregateBlockCount=10`,与 `mnote.doc.fetch` 块顺序一致;刷新后 `afterRefreshBlockIds` 保持最终顺序;`table_1/mindmap_1/resource_1` 返回 `editable=false` 与 `unsupportedReason=复杂块暂不开放 AI 精确写入`。 --- @@ -168,11 +183,11 @@ cargo test -p bridge-runtime doc_find 真实页面 smoke: -- [ ] 使用 `TEST-AI-BLOCK-FETCH-` 页面。 -- [ ] `mnote.doc.fetch scope=full detail=with_ids` 读取整页。 -- [ ] `mnote.doc.fetch scope=outline detail=with_ids` 只返回标题结构。 -- [ ] `mnote.doc.find query=<唯一前缀>` 定位目标段落。 -- [ ] 保存工具返回到 `tmp/hermes-tester//doc-fetch-find.json`。 +- [x] 使用综合页面 `TEST-AI-BLOCK-TOOLS-`。 +- [x] `mnote.doc.fetch scope=full detail=with_ids` 读取整页。 +- [x] `mnote.doc.fetch scope=outline detail=with_ids` 只返回标题结构。 +- [x] `mnote.doc.find query=<唯一前缀>` 定位目标段落。 +- [x] 保存工具返回到 `tmp/hermes-tester/page-block-ai-tools-/doc-fetch-find.json`。 补充 smoke 证据: @@ -181,9 +196,16 @@ cargo test -p bridge-runtime doc_find 通过标准: -- [ ] 不读取浏览器 DOM。 -- [ ] `doc.find` 返回的 block id 能被 `block.fetch` 读取。 -- [ ] 大页面默认分块或限制输出,不默认返回无限正文。 +- [x] 不读取浏览器 DOM。 +- [x] `doc.find` 返回的 block id 能被 `block.fetch` 读取。 +- [x] 大页面默认分块或限制输出,不默认返回无限正文。 + +2026-05-18 补充证据: + +- `mnote.doc.fetch` 从 `aggregate_value` / Page Aggregate block projection 构造上下文,不读取浏览器 DOM;代码入口:`rust/crates/mnote-web/src/hermes_tools/doc.rs`。 +- `hermes_tools_doc_find_and_block_fetch_use_block_projection` 覆盖 `doc.find` 返回 `heading_1` 后继续用同一个 id 调 `mnote.block.fetch`。 +- `mnote.doc.fetch` 默认 `maxBlocks=120`,运行时 clamp 到 `1..=240`;超限返回 `truncated/continuation/warnings`。 +- 真实 3000 smoke:`tmp/page-block-ai-tools-smoke/mpaes2hg.json` 覆盖 `doc.fetch full` 读取 `heading_1/p_1/p_2/p_3`,`doc.fetch outline` 只返回 `heading_1`,`doc.find` 定位 `p_2`,`maxBlocks=2` 返回 `truncated=true`。 --- @@ -210,19 +232,21 @@ cargo test -p mnote-web block_fetch 真实页面 smoke: -- [ ] 从 `doc.find` 结果选择一个段落 block。 -- [ ] 调 `mnote.block.fetch includeChildren=true contextBefore=1 contextAfter=1`。 -- [ ] 断言 before/after 只来自同父级。 +- [x] 从 `doc.find` 结果选择一个段落 block。 +- [x] 调 `mnote.block.fetch includeChildren=true contextBefore=1 contextAfter=1`。 +- [x] 断言 before/after 只来自同父级。 补充 smoke 证据: - [x] `mnote.block.fetch blockId=p_2 format=page_xml/text contextBefore=1 contextAfter=1` 返回目标块 `p_2`、`revisionRef` 与同父级 before/after `p_1/p_3`。证据:`tmp/page-block-ai-context-format-smoke/mp8ddr4n.json`。 +- [x] `doc.find` 定位 `p_2` 后,`mnote.block.fetch includeChildren=true contextBefore=1 contextAfter=1` 回读 `p_2`,返回 `revisionRef`、同父级 before `p_1` 和 after `p_3`。证据:`tmp/page-block-ai-tools-smoke/mpag4966.json`。 +- [x] `table_1/mindmap_1/resource_1` 复杂块在真实 3000 smoke 中返回 `editable=false` 与 `unsupportedReason=复杂块暂不开放 AI 精确写入`。证据:`tmp/page-block-ai-tools-smoke/mpag4966.json`。 通过标准: - [x] block 文本与页面显示一致。 -- [ ] `revisionRef` 可被后续 dry-run 使用。 -- [ ] 复杂块不会伪装成完全可编辑。 +- [x] `revisionRef` 可被后续 dry-run 使用。 +- [x] 复杂块不会伪装成完全可编辑。 --- @@ -238,10 +262,15 @@ cargo test -p mnote-web block_fetch - [x] 支持 `command=block_replace`。 - [x] 支持 `command=block_insert_after`。 - [x] 支持 `command=block_move_after` dry-run。 -- [ ] 支持 `command=str_replace` 且多重匹配阻断。(当前只有基础 plan,仍需多重匹配阻断。) +- [x] `command=str_replace` 不再作为本 checklist 当前目标;普通正文 search/replace 已收口到 `mnote.doc.markdown_edit`,多重匹配/无法安全映射阻断由 markdown_edit 合同覆盖。 - [x] 缺少 `revision/conflictDetectionKey` 时只允许 dry-run。 - [x] 返回 `planId`、`diff`、`warnings`、`risk`、`blocked`。 +2026-05-18 口径修正: + +- `7-18` 到 `7-25` 收口后,普通正文替换不再继续扩 `mnote.doc.plan_update command=str_replace`;当前实现路径是 `page_ai_workflow.rs -> mnote.doc.markdown_edit`。 +- `mnote.doc.markdown_edit` 已覆盖精确匹配、归一化匹配、多操作同块合并、无法安全映射时失败;相关测试由 `cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools -- --nocapture` 覆盖。 + 验证命令: ```bash @@ -252,16 +281,21 @@ cargo test -p bridge-runtime doc_insert_blocks 真实页面 smoke: -- [ ] 对目标段落执行 `block_replace dryRun=true`。 -- [ ] 对目标段落执行 `block_insert_after dryRun=true`。 -- [ ] 对两个同父级普通块执行 `block_move_after dryRun=true`。 -- [ ] dry-run 前后分别保存 `/api/page-aggregate/:id`,确认内容 hash 不变。 +- [x] 对目标段落执行 `block_replace dryRun=true`。 +- [x] 对目标段落执行 `block_insert_after dryRun=true`。 +- [x] 对两个同父级普通块执行 `block_move_after dryRun=true`。 +- [x] dry-run 前后分别读取 `/api/page-aggregate/:id` 与 `mnote.doc.fetch`,确认 revision / conflictDetectionKey / 正文文本不变。 通过标准: -- [ ] dry-run 不写 Convex。 -- [ ] plan 能解释 before/after。 -- [ ] 不支持场景返回 `blocked=true`。 +- [x] dry-run 不写 Convex。 +- [x] plan 能解释 before/after。 +- [x] 不支持场景返回 `blocked=true`。 + +2026-05-18 补充真实页面 smoke 证据: + +- 最新证据:`tmp/page-block-ai-tools-smoke/mpag4966.json`,截图:`tmp/page-block-ai-tools-smoke/mpag4966-page.png`。 +- 覆盖:`plan_update.dry_run.replaceDiff.before/after`、`insertDiff.after/content`、`block_move_after p_3 -> p_1 dryRun` 不阻断、不支持 `p_3 -> p_3` 返回 `blocked=true`、dry-run 后 revision / Page Aggregate conflictDetectionKey / 正文文本不变。 --- @@ -290,20 +324,27 @@ cargo test -p bridge-runtime doc_replace_range_tool_executes_in_rust_runtime 真实页面 smoke: -- [ ] 新建 `TEST-AI-BLOCK-REPLACE-` 页面。 -- [ ] 写入两段不同文本。 -- [ ] `doc.find` 找到第二段 block id。 -- [ ] `block.replace dryRun=true` 查看 plan。 -- [ ] `block.replace dryRun=false` 替换第二段。 -- [ ] 页面截图证明只替换第二段。 -- [ ] `/api/page-aggregate/:id` 回读证明持久化。 -- [ ] `mnote.doc.fetch` 再次证明 AI 可读回。 +- [x] 使用综合页面 `TEST-AI-BLOCK-TOOLS-`。 +- [x] 写入两段不同文本。 +- [x] `doc.find` 找到第二段 block id。 +- [x] `block.replace dryRun=true` 查看 plan。 +- [x] `block.replace dryRun=false` 替换第二段。 +- [x] 页面截图和工具回读证明只替换第二段。 +- [x] `/api/page-aggregate/:id` 回读证明持久化。 +- [x] `mnote.doc.fetch` 再次证明 AI 可读回。 通过标准: -- [ ] 相邻块不变化。 -- [ ] 目标 block id 保持不变。 -- [ ] 旧 revision 写入返回 conflict。 +- [x] 相邻块不变化。 +- [x] 目标 block id 保持不变。 +- [x] 旧 revision 写入返回 conflict。 + +2026-05-18 补充真实页面 smoke 证据: + +- 最新证据:`tmp/page-block-ai-tools-smoke/mpag4966.json`,截图:`tmp/page-block-ai-tools-smoke/mpag4966-page.png`。 +- 覆盖:`block.replace.changedBlocks[0]={blockId:"p_2",op:"replace"}`,`targetBlockStillExists=true`,相邻块仍为 `第一段 mpag4966` / `第三段 mpag4966`,最终 `mnote.doc.fetch` 与 Page Aggregate 均回读到 `第二段已替换 mpag4966`,Page Aggregate `aggregateRevision=2`、`aggregateConflictDetectionKey=tree_1779062903017_3:2`。 +- 冲突 / 幂等补充证据:`tmp/page-block-ai-conflict-idempotency-smoke/mpafrevs.json`,截图:`tmp/page-block-ai-conflict-idempotency-smoke/mpafrevs-page.png`。 +- 覆盖:首次 `block.replace` 携带最新 `revision/conflictDetectionKey/blockRevisionRef/idempotencyKey` 成功写入;重复同一 `idempotencyKey` replay 同一 `commandId` 且 revision 不再次递增;旧 `revision/conflictDetectionKey` 与旧 `blockRevisionRef` 均返回 HTTP `400` / `mnote_tool_conflict`,正文保持首次替换结果。 --- @@ -318,30 +359,46 @@ cargo test -p bridge-runtime doc_replace_range_tool_executes_in_rust_runtime - [x] tool manifest 增加 `mnote.block.insert_after`。 - [x] 输入必须包含 `anchorBlockId/revision/conflictDetectionKey/idempotencyKey/dryRun`。 - [x] 新块 id 由 Rust runtime 分配。 -- [ ] 支持单块和最多 20 个普通块插入。(当前最小闭环为单块插入。) +- [x] 支持单块和最多 20 个普通块插入。 - [x] 第一阶段支持 paragraph/heading/todo。 - [x] 返回 inserted block ids 和新 revision。 +2026-05-18 补充证据: + +- `rust/crates/mnote-web/src/hermes_tools/block.rs` 的 `mnote.block.insert_after` 已支持 `content` / `block` 单块兼容输入,以及 `blocks` 数组输入。 +- `blocks` 运行时限制为 `1..=20`,超过 20 个返回 `mnote_tool_bad_request`;多块写入按输入顺序连续插在 anchor 后。 +- `rust/crates/mnote-web/src/hermes_tools/manifest.rs` 的 manifest 已将 `content | block | blocks` 写为 `anyOf`,并声明 `blocks.minItems=1/maxItems=20`。 +- 新增回归:`hermes_tools_block_insert_after_accepts_multiple_blocks_and_returns_ids`、`hermes_tools_block_insert_after_rejects_more_than_twenty_blocks`。 + 验证命令: ```bash -cargo test -p mnote-web block_insert_after +cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_block_insert_after -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools -- --nocapture cargo test -p bridge-runtime doc_insert_blocks_tool_emits_editor_commands ``` 真实页面 smoke: -- [ ] 新建 `TEST-AI-BLOCK-INSERT-` 页面。 -- [ ] 在第一段后插入 todo。 -- [ ] 截图证明位置准确。 -- [ ] 刷新页面后再次确认。 -- [ ] `mnote.block.fetch` 能读取新 block id。 +- [x] 使用综合页面 `TEST-AI-BLOCK-TOOLS-`。 +- [x] 在第一段后插入 todo。 +- [x] 截图证明位置准确。 +- [x] 刷新页面后再次确认。 +- [x] `mnote.block.fetch` 能读取新 block id。 通过标准: -- [ ] 插入位置准确。 -- [ ] 新 block id 不是 AI 自造未校验 id。 -- [ ] 重复同一 `idempotencyKey` 不重复插入。 +- [x] 插入位置准确。 +- [x] 新 block id 不是 AI 自造未校验 id。 +- [x] 重复同一 `idempotencyKey` 不重复插入。 + +2026-05-18 补充证据: + +- `mnote.block.insert_after` 运行时分配 `ai_block_{request_id}` 或多块 `ai_block_{request_id}_{index}`,不会采用模型输入中的未校验 id。 +- `hermes_tools_block_insert_after_accepts_multiple_blocks_and_returns_ids` 断言返回的 `insertedBlockIds` 均以 `ai_block_` 开头。 +- 真实 3000 smoke:`PLAYWRIGHT_CHROME_EXECUTABLE=/snap/bin/chromium MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task-page-block-ai-tools-smoke.js` 通过。 +- 最新证据:`tmp/page-block-ai-tools-smoke/mpag4966.json`,截图:`tmp/page-block-ai-tools-smoke/mpag4966-page.png`。 +- 覆盖:`insert_after` 插入 `todo` 块、`orderAfterInsert=["heading_1","p_1","ai_block_req_1779061213631_259","p_2","p_3"]`、`block.fetch` 可读取新块、重复 `idempotencyKey` replay 同一 `commandId` 且 revision 不再次递增、刷新后插入文本仍可见。 --- @@ -355,32 +412,43 @@ cargo test -p bridge-runtime doc_insert_blocks_tool_emits_editor_commands - [x] tool manifest 增加 `mnote.block.move_after`。 - [x] `dryRun=true` 支持同父级叶子块 diff。 -- [ ] `dryRun=false` 前先继续阻断所有复杂块。(已有同父级/叶子/类型/editable/self 阻断,复杂块矩阵 smoke 待补。) +- [x] `dryRun=false` 前先继续阻断所有复杂块。(已有同父级/叶子/类型/editable/self 阻断,复杂块矩阵 route smoke 已补。) - [x] 检查 `blockRevisionRef` 与 `anchorRevisionRef`。 - [x] 阻断移动到自身、移动到子树、跨页面移动。 - [x] 返回 from/to parent/order。 +2026-05-18 补充证据: + +- `mnote.block.move_after` 保持同父级、叶子块、可移动类型、editable、自身移动阻断条件。 +- 新增隔离 fixture route smoke:`hermes_tools_block_move_after_blocks_complex_and_nested_blocks`,覆盖标题带子块、列表项、表格、mindmap、resource,均返回 `blocked=true` 与 `block_move_after_blocked`。 + 验证命令: ```bash -cargo test -p mnote-web block_move_after +cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_block_move_after_blocks_complex_and_nested_blocks -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools -- --nocapture cargo test -p mnote-editor-core command_executor ``` 真实页面 smoke: -- [ ] 新建 `TEST-AI-BLOCK-MOVE-` 页面,包含三段普通段落。 -- [ ] dry-run 移动第三段到第一段后。 -- [ ] 正式执行同父级移动。 -- [ ] 截图证明顺序为第一段、第三段、第二段。 -- [ ] `mnote.doc.fetch` 回读顺序一致。 -- [ ] 对标题带子块、列表项、表格、mindmap 执行 move dry-run,必须返回 blocked。 +- [x] 使用综合页面 `TEST-AI-BLOCK-TOOLS-`,包含三段普通段落。 +- [x] dry-run 移动第三段到第一段后。 +- [x] 正式执行同父级移动。 +- [x] 截图证明顺序为第一段、第三段、第二段。 +- [x] `mnote.doc.fetch` 回读顺序一致。 +- [x] 对标题带子块、列表项、表格、mindmap 执行 move dry-run,必须返回 blocked。 通过标准: -- [ ] moving block id 保持不变。 -- [ ] 同父级顺序正确。 -- [ ] 复杂块不被误移动。 +- [x] moving block id 保持不变。 +- [x] 同父级顺序正确。 +- [x] 复杂块不被误移动。 + +2026-05-18 补充真实页面 smoke 证据: + +- 最新证据:`tmp/page-block-ai-tools-smoke/mpag4966.json`,截图:`tmp/page-block-ai-tools-smoke/mpag4966-page.png`。 +- 覆盖:`heading_parent/list_item_1/table_1/mindmap_1/resource_1` 的 `plan_update block_move_after dryRun=true` 与正式 `mnote.block.move_after dryRun=false` 均返回 `blocked=true` / `block_move_after_blocked`,阻断后 `revisionAfterBlocked=1` 且正文不变化;普通 `block.move_after.dryRunBlocked=false`,正式写入后顺序为 `heading_1,p_1,p_3,ai_block_...,p_2,...`,`movingBlockStillExists=true`。 --- @@ -411,8 +479,9 @@ cargo test -p mnote-editor-core command_executor 本 checklist 不能移动到 `done/`,直到: -- [ ] Phase 1 到 Phase 6 全部完成。 -- [ ] Phase 7 至少完成 dry-run 和阻断规则;若真实 move 未完成,`7-9` 必须仍标注受限。 -- [ ] 每个写工具都有真实页面 smoke 证据。 -- [ ] 失败项已经写入 `bugs/07-ai/process/` 或真实 owner 分类。 -- [ ] `mnote.page.save` 不再被任何设计描述为精确块编辑主入口。 +- [x] Phase 1 到 Phase 6 全部完成。 +- [x] Phase 7 至少完成 dry-run 和阻断规则;若真实 move 未完成,`7-9` 必须仍标注受限。 +- [x] 每个写工具都有真实页面 smoke 证据。 +- [x] 失败项已经写入 `bugs/07-ai/process/` 或真实 owner 分类。 +- [x] `mnote.page.save` 不再被任何设计描述为精确块编辑主入口。 +- [ ] Phase 8 UI / Review Mode 仍按 Phase C 边界冻结;若后续进入实施,需要另补 UI smoke。 diff --git a/design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md b/design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md index 494cc1b0..d3d694f6 100644 --- a/design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md +++ b/design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md @@ -7,7 +7,7 @@ > 本稿目的:修正“页面 AI 快速块编辑”后续方向,明确 mnote 不再建设独立 AI agent runtime;mnote 只建设 Hermes 可消费的编辑工具路由、工具 manifest、上下文冻结、dry-run/review 和 Rust 写入安全边界。 > > 关联文档: -> - `/mnt/Data1T/mnote/design/10-review/process/09-page-ai-fast-block-edit-runtime-review.md` +> - `/mnt/Data1T/mnote/design/10-review/done/09-page-ai-fast-block-edit-runtime-review.md` > - `/mnt/Data1T/mnote/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md` > - `/mnt/Data1T/mnote/design/07-ai/done/7-9-page-block-ai-tooling-roadmap-v1.md` > - `/mnt/Data1T/mnote/design/07-ai/done/7-6-mnote-hermes-plugin-tool-contract-v1.md` @@ -17,6 +17,16 @@ --- +## 0. 2026-05-18 状态更新 + +本稿仍作为 Hermes 工具路由与审阅面设计保留在 `process/`,但以下口径已经更新: + +- `/api/page-ai/block-edit-workflow` 当前不再以 `local_rule -> apply_block_ops` 作为主路径;简单正文编辑主路径已切到模型生成 markdown `search/replace` 或 `full_content`,再调用 `mnote.doc.markdown_edit`。 +- `page_ai_workflow` 已复用统一 mnote tool executor,不再绕过 Hermes tool toggle / audit / write contract。 +- `mnote.doc.apply_block_ops` / `mnote.block.*` 保留为结构性块操作辅助,不再作为普通正文 search/replace 的优先入口。 +- 本稿中的 `PageAIReviewSession` 只定义 Phase C 的安全合同和状态机边界;当前 Phase C 仍冻结,不实施流式 apply 或新的审阅 UI。 +- 当前已闭合缺陷见 `bugs/07-ai/done/7-18` 到 `7-25`。 + ## 1. 本轮结论 页面 AI 编辑卡顿的根因不是“Rust apply 慢”,而是模型和工具之间缺少稳定、低歧义、可审计的编辑命令面: @@ -348,6 +358,96 @@ stale 第一阶段可以保留 yolo,但仍应让工具返回 review-compatible 数据结构,避免后续 UI 重写。 +最小稳定合同: + +```json +{ + "schema": "mnote.page_ai_review_session.v1", + "sessionId": "review_01", + "workspaceId": "tree_workspace", + "documentId": "tree_doc", + "runId": "run_01", + "toolCallId": "tool_01", + "traceId": "trace_01", + "state": "awaiting_user", + "mode": "review", + "source": { + "runtimeOwner": "mnote-web", + "writeOwner": "rust-runtime-kernel", + "toolName": "mnote.doc.markdown_edit" + }, + "base": { + "revision": 12, + "conflictDetectionKey": "body:12:hash", + "allowedTargetBlockIds": ["p_1"] + }, + "proposal": { + "format": "markdown", + "operations": [ + { + "op": "replace", + "search": "旧文本", + "replace": "新文本" + } + ], + "fullContent": null + }, + "preview": { + "dryRun": true, + "changedBlocks": [ + { + "blockId": "p_1", + "before": "旧文本", + "after": "新文本", + "revisionRef": "pageRev:12:block:p_1" + } + ], + "diff": [], + "warnings": [], + "risk": "low", + "blocked": false + }, + "actions": { + "accept": { + "requiresFreshRevision": true, + "requiresIdempotencyKey": true + }, + "reject": true, + "retry": { + "allowed": true, + "requiresNewToolCallId": true + }, + "abort": true + }, + "audit": { + "createdAt": "2026-05-18T00:00:00Z", + "createdBy": "actor_01" + } +} +``` + +状态语义: + +- `draft`:已创建 session,但尚未生成 dry-run preview。 +- `planning`:正在构造 tool args 或请求模型生成候选。 +- `previewing`:正在执行 `dryRun=true`。 +- `awaiting_user`:preview 已完成,等待 accept / reject / retry / abort。 +- `accepted`:用户已确认,等待正式 apply。 +- `rejected`:用户拒绝,本 session 不可再写入。 +- `applying`:accept 后正在正式写入。 +- `applied`:正式写入已完成,并已通过 Page Aggregate / `mnote.doc.fetch` 回读。 +- `failed`:preview 或 apply 失败,需保留 structured error / hint。 +- `aborted`:用户或系统中断,不能继续写入。 +- `stale`:accept 时 revision / conflictDetectionKey / revisionRef 过期,必须重新 preview,不能直接 apply。 + +动作约束: + +- `accept` 必须重新读取 Page Aggregate,并校验 `revision/conflictDetectionKey/revisionRef`;过期时转 `stale`。 +- `accept` 必须提供新的或既有合法 `idempotencyKey`,重复提交必须 replay 同一结果。 +- `reject` 与 `abort` 不能产生写入。 +- `retry` 不能复用旧 `toolCallId` 伪装成同一次写入;必须生成新 proposal 或新 dry-run preview。 +- yolo 模式可以跳过 `awaiting_user` UI,但仍应生成同构的 review-compatible audit 数据。 + --- ## 6. 关键流程 @@ -508,7 +608,7 @@ profile disabled mnote.block.fetch ### Phase D:Review session -- [ ] 定义 `mnote.page_ai_review_session.v1`。 +- [x] 定义 `mnote.page_ai_review_session.v1` 最小 schema 与状态机边界(2026-05-18 已补;Phase C UI 仍冻结)。 - [ ] `mnote.doc.plan_update` 与 `mnote.doc.apply_block_ops dryRun=true` 返回 review-compatible draft。 - [ ] 页面 AI UI 展示 diff/warnings/risk/blocked。 - [ ] accept/reject/retry/abort 可用。 diff --git a/design/07-ai/process/7-16-page-block-ai-real-smoke-followup-matrix-v1.md b/design/07-ai/process/7-16-page-block-ai-real-smoke-followup-matrix-v1.md new file mode 100644 index 00000000..508bd99b --- /dev/null +++ b/design/07-ai/process/7-16-page-block-ai-real-smoke-followup-matrix-v1.md @@ -0,0 +1,181 @@ +# 7-16 [process] Page Block AI 真实页面 Smoke 后续矩阵 v1 + +> 创建时间:2026-05-18 +> +> 当前状态:`PROCESS` +> +> 来源: +> - `/mnt/Data1T/mnote/design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md` +> - `/mnt/Data1T/mnote/design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md` +> - `/mnt/Data1T/mnote/design/10-review/process/08-kernel-architecture-next-priority-review-and-checklist.md` + +--- + +## 1. 目的 + +`7-10` 已完成大量工具合同、Rust route 测试和部分真实 3000 smoke,但仍有一组“必须真实页面验证”的矩阵项未完成。 + +本文只承接这些剩余 smoke,不新增 AI 功能面,不改变当前主路径: + +- 简单正文编辑主路径仍是 `mnote.doc.markdown_edit`。 +- `mnote.block.*` 仍只作为结构性辅助。 +- `mnote.page.save` 仍只作为页面级粗粒度兜底。 +- Phase C review / streaming apply 仍冻结,当前只验证基础合同和安全边界。 + +--- + +## 2. 当前已具备的前置证据 + +- `mnote.doc.fetch scope=selection format=page_xml/text`、`mnote.block.fetch format=page_xml/text`、manifest annotations、`mnote.doc.apply_block_ops allowedTargetBlockIds` 越界阻断已有真实 3000 smoke: + - `tmp/page-block-ai-context-format-smoke/mp8ddr4n.json` + - `tmp/page-block-ai-context-format-smoke/mp8ddr4n-page.png` +- `mnote.block.replace` stale revision / stale blockRevisionRef / idempotency replay 已有真实 3000 smoke: + - `scripts/task-page-block-ai-conflict-idempotency-smoke.js` + - 证据记录在 `08` 的 Page Block AI Tooling 章节。 +- `mnote.block.insert_after` 多块插入和 20 个边界已有 Rust route 回归: + - `cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_block_insert_after -- --nocapture` +- `mnote.block.move_after` 复杂块阻断矩阵已有隔离 route smoke: + - `cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_block_move_after_blocks_complex_and_nested_blocks -- --nocapture` + +--- + +## 3. 后续真实页面 Smoke 矩阵 + +### 3.1 Page Aggregate Block Projection + +- [x] 登录测试账号并创建综合页面 `TEST-AI-BLOCK-TOOLS-`。 +- [x] 写入段落、标题、todo、列表、mindmap/resource 占位。 +- [x] 调 `/api/page-aggregate/:id` 并保存响应到 `tmp/hermes-tester/page-block-ai-tools-/page-aggregate.json`。 +- [x] 断言所有普通可编辑块有 `blockId` 和 `revisionRef`。 +- [x] 页面刷新后 block ids 不变化。 +- [x] projection 中 `blockCount` 与页面块数量一致;复杂块返回 `editable=false` 与 `unsupportedReason`。 + +证据(2026-05-18): + +- JSON:`tmp/page-block-ai-tools-smoke/mpag4966.json` +- Page Aggregate:`tmp/hermes-tester/page-block-ai-tools-mpag4966/page-aggregate.json` +- 覆盖:Page Aggregate `blockProjectionVersion=1`、`projectionSource=documents.content`、`aggregateBlockCount=10`,与 `mnote.doc.fetch` 块顺序一致;刷新后 `afterRefreshBlockIds` 保持最终顺序;`table_1/mindmap_1/resource_1` 返回 `editable=false` 与 `unsupportedReason=复杂块暂不开放 AI 精确写入`。 + +### 3.2 `mnote.doc.fetch` / `mnote.doc.find` + +- [x] 使用综合页面 `TEST-AI-BLOCK-TOOLS-`。 +- [x] `mnote.doc.fetch scope=full detail=with_ids` 读取整页。 +- [x] `mnote.doc.fetch scope=outline detail=with_ids` 只返回标题结构。 +- [x] `mnote.doc.find query=<唯一前缀>` 定位目标段落。 +- [x] 保存工具返回到 `tmp/hermes-tester/page-block-ai-tools-/doc-fetch-find.json`。 +- [x] 确认读取不依赖浏览器 DOM。 +- [x] `doc.find` 返回的 block id 能被 `block.fetch` 读取。 +- [x] 大页面默认分块或限制输出,不默认返回无限正文。 + +证据(2026-05-18): + +- 命令:`PLAYWRIGHT_CHROME_EXECUTABLE=/snap/bin/chromium MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task-page-block-ai-tools-smoke.js` +- JSON:`tmp/page-block-ai-tools-smoke/mpag4966.json` +- 工具返回:`tmp/hermes-tester/page-block-ai-tools-mpag4966/doc-fetch-find.json` +- 覆盖:`doc.fetch.initial` 读取 `heading_1/p_1/p_2/p_3` 等 10 个块;`doc.fetch scope=outline` 只返回 `heading_1/heading_parent`;`doc.find query="第二段 "` 定位 `p_2`;`maxBlocks=2` 返回 `truncated=true`。 +- 静态边界:`mnote.doc.fetch` 从 Page Aggregate block projection 构建上下文,不读取浏览器 DOM;默认 `maxBlocks=120` 且 clamp 到 `1..=240`。 + +### 3.3 `mnote.block.fetch` + +- [x] 从 `doc.find` 结果选择一个段落 block。 +- [x] 调 `mnote.block.fetch includeChildren=true contextBefore=1 contextAfter=1`。 +- [x] 断言 before/after 只来自同父级。 +- [x] `revisionRef` 可被后续 dry-run 使用。 +- [x] 复杂块不会伪装成完全可编辑。 + +证据(2026-05-18): + +- JSON:`tmp/page-block-ai-tools-smoke/mpag4966.json` +- 覆盖:`block.fetch` 读取 `p_2`,返回 `revisionRef=pageRev:1:block:p_2:...`,同父级 `before=["p_1"]`、`after=["p_3"]`;`table_1/mindmap_1/resource_1` 均返回 `editable=false` 与 `unsupportedReason=复杂块暂不开放 AI 精确写入`。 + +### 3.4 `mnote.doc.plan_update` / dry-run + +- [x] `command=str_replace` 不再作为当前 `plan_update` 目标;普通正文 search/replace 已由 `mnote.doc.markdown_edit` 合同承接。 +- [x] 对目标段落执行 `block_replace dryRun=true`。 +- [x] 对目标段落执行 `block_insert_after dryRun=true`。 +- [x] 对两个同父级普通块执行 `block_move_after dryRun=true`。 +- [x] dry-run 前后分别读取 `/api/page-aggregate/:id` 与 `mnote.doc.fetch`,确认 revision / conflictDetectionKey / 正文文本不变。 +- [x] dry-run 不写 Convex。 +- [x] plan 能解释 before/after。 +- [x] 不支持场景返回 `blocked=true`。 + +证据(2026-05-18): + +- JSON:`tmp/page-block-ai-tools-smoke/mpag4966.json` +- 覆盖:`plan_update.dry_run.replaceDiff.before/after`、`insertDiff.after/content`、不支持 `move_after p_3 -> p_3` 返回 `blocked=true`、dry-run 后 `revisionAfterDryRun=1` 且正文未变化。 + +### 3.5 `mnote.block.replace` + +- [x] 使用综合页面 `TEST-AI-BLOCK-TOOLS-`。 +- [x] 写入两段不同文本。 +- [x] `doc.find` 找到第二段 block id。 +- [x] `block.replace dryRun=true` 查看 plan。 +- [x] `block.replace dryRun=false` 替换第二段。 +- [x] 页面截图和工具回读证明只替换第二段。 +- [x] `/api/page-aggregate/:id` 回读证明持久化。 +- [x] `mnote.doc.fetch` 再次证明 AI 可读回。 +- [x] 相邻块不变化。 +- [x] 目标 block id 保持不变。 +- [x] 旧 revision 写入返回 conflict。 + +证据(2026-05-18): + +- JSON:`tmp/page-block-ai-tools-smoke/mpag4966.json` +- 截图:`tmp/page-block-ai-tools-smoke/mpag4966-page.png` +- 覆盖:`block.replace.changedBlocks[0]={blockId:"p_2",op:"replace"}`,`targetBlockStillExists=true`,`adjacentTexts=["第一段 mpag4966","第三段 mpag4966"]`,`mnote.doc.fetch` 与 Page Aggregate 均回读到 `第二段已替换 mpag4966`,Page Aggregate `aggregateRevision=2`、`aggregateConflictDetectionKey=tree_1779062903017_3:2`。 +- 冲突 / 幂等补充 JSON:`tmp/page-block-ai-conflict-idempotency-smoke/mpafrevs.json`,截图:`tmp/page-block-ai-conflict-idempotency-smoke/mpafrevs-page.png`。 +- 覆盖:首次 `block.replace` 写入 `p_2` 后,重复 `idempotencyKey=idem_conflict_replace_mpafrevs` replay 同一 `commandId=page_body_save_req_1779062303997_525` 且 `revisionAfterFirst=2`、`revisionAfterReplay=2`;旧 `revision/conflictDetectionKey` 与旧 `blockRevisionRef` 均返回 HTTP `400` / `mnote_tool_conflict`,最终文本保持 `第二段首次替换 mpafrevs`。 + +### 3.6 `mnote.block.insert_after` + +- [x] 使用综合页面 `TEST-AI-BLOCK-TOOLS-`。 +- [x] 在第一段后插入 todo。 +- [x] 截图证明位置准确。 +- [x] 刷新页面后再次确认。 +- [x] `mnote.block.fetch` 能读取新 block id。 +- [x] 插入位置准确。 +- [x] 新 block id 不是 AI 自造未校验 id。 +- [x] 重复同一 `idempotencyKey` 不重复插入。 + +证据(2026-05-18): + +- 命令:`PLAYWRIGHT_CHROME_EXECUTABLE=/snap/bin/chromium MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task-page-block-ai-tools-smoke.js` +- JSON:`tmp/page-block-ai-tools-smoke/mpag4966.json` +- 截图:`tmp/page-block-ai-tools-smoke/mpag4966-page.png` +- 说明:该 smoke 使用 `TEST-AI-BLOCK-TOOLS-*` 综合页面,不是单独 `TEST-AI-BLOCK-INSERT-*` 页面;已覆盖 todo 插入、插入后顺序、block.fetch 回读、刷新后可见和重复 idempotencyKey replay。 + +### 3.7 `mnote.block.move_after` + +- [x] 使用综合页面 `TEST-AI-BLOCK-TOOLS-`,包含三段普通段落。 +- [x] dry-run 移动第三段到第一段后。 +- [x] 正式执行同父级移动。 +- [x] 截图证明顺序为第一段、第三段、第二段。 +- [x] `mnote.doc.fetch` 回读顺序一致。 +- [x] 对标题带子块、列表项、表格、mindmap 执行 move dry-run,必须返回 blocked。 +- [x] moving block id 保持不变。 +- [x] 同父级顺序正确。 +- [x] 复杂块不被误移动。 + +证据(2026-05-18): + +- JSON:`tmp/page-block-ai-tools-smoke/mpag4966.json` +- 截图:`tmp/page-block-ai-tools-smoke/mpag4966-page.png` +- 覆盖:`heading_parent/list_item_1/table_1/mindmap_1/resource_1` 的 `plan_update block_move_after dryRun=true` 与正式 `mnote.block.move_after dryRun=false` 均返回 `blocked=true` / `block_move_after_blocked`,阻断后 `revisionAfterBlocked=1` 且正文不变化;普通 `p_3 -> p_1` 正式移动后顺序为 `heading_1,p_1,p_3,ai_block_...,p_2,...`,`movingBlockStillExists=true`。 + +### 3.8 UI / Review Mode 边界 + +- [ ] Hermes tool event UI 展示 `plan/diff/warnings/risk`。 +- [ ] 本地 preview/suggestion 与持久 comment/tracked-change 分开。 +- [ ] 协作可见审阅必须另走正式 comment/history/tracked-change 设计。 +- [ ] 用户能看到 AI 将改哪个 block。 +- [ ] `blocked=true` 的工具调用不会出现写入按钮。 +- [ ] preview 不写入正式 comment/history。 + +--- + +## 4. 验证原则 + +- 所有真实页面 smoke 必须使用测试账号,优先从 `http://localhost:3000/auth` 快速登录。 +- 证据必须保留 JSON 和截图,默认放入 `tmp/hermes-tester//` 或专项 `tmp/page-block-ai-*` 目录。 +- 发现失败时按 owner 写入 `bugs//process/`,不要只在本矩阵中留描述。 +- 不用此矩阵扩展新 AI 功能;只补真实页面证据和安全边界。 diff --git a/design/07-ai/process/7-27-online-markdown-writeback-final-content-truth-v2.md b/design/07-ai/process/7-27-online-markdown-writeback-final-content-truth-v2.md new file mode 100644 index 00000000..2d6c3f78 --- /dev/null +++ b/design/07-ai/process/7-27-online-markdown-writeback-final-content-truth-v2.md @@ -0,0 +1,580 @@ +# 7-27 [process] 在线 Markdown_edit 以最终 Markdown 为写回真源 v2 + +> 更新:2026-05-18(v2:整合 CLI Main 参考实现分析,确认方向,补充见解) +> +> 当前状态:`PROCESS` +> +> 关联缺陷:`bugs/07-ai/done/7-24-markdown-edit-online-write-does-not-use-final-markdown-v1.md` +> `bugs/07-ai/done/7-25-reasonix-block-edit-workflow-empty-block-ops-after-markdown-match-v1.md` +> `bugs/07-ai/done/7-17-markdown-edit-same-block-multi-op-overwrite-v1.md` +> +> 上位设计:`design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-v1.md` +> +> 参考实现:`design/05-editor-mainline/reference-code/cli-main/shortcuts/doc/`(str_replace skill) +> `rust/crates/mnote-web/src/routes/local_markdown_parser.rs`(已有 GFM→blocks 解析器) + +--- + +## 0. 参考实现分析:CLI Main(Lark Doc)怎么做? + +### 0.1 实现结构 + +CLI Main 的 `docs +update --api-version v2` 将 `str_replace`、`block_replace`、`block_insert_after` 等命令以 PUT 请求发送到 Lark OpenAPI: + +```go +// docs_update_v2.go:140-157 +body := map[string]interface{}{ + "format": runtime.Str("doc-format"), + "command": cmd, // "str_replace" | "block_replace" | ... +} +body["pattern"] = runtime.Str("pattern") // str_replace 的搜索文本 +body["content"] = runtime.Str("content") // 替换文本或新内容 +body["block_id"] = blockID // block_* 操作的目标块 +body["revision_id"] = runtime.Int("revision-id") + +// API: PUT /open-apis/docs_ai/v1/documents/{id} +``` + +**关键:`str_replace` 只传 `pattern` + `content`,不传 `final_md`。** 服务端(Lark OpenAPI)在收到 `command: "str_replace"` 后,由服务端负责在文档的 XML/block 存储中找到匹配的文本并替换。 + +### 0.2 与 mnote 的架构对应 + +| CLI Main (Lark Doc) | mnote | 角色 | +|---|---|---| +| Lark OpenAPI 服务端 | Rust `doc_markdown_edit` + `execute_page_body_save` | 执行文本替换→block 持久化 | +| `str_replace` API 端点 | `mnote.doc.markdown_edit` 工具 | AI 调用的文本级编辑入口 | +| 块操作 API 端点 | `mnote.doc.apply_block_ops` + `mnote.block.*` | AI 调用的块级编辑入口 | +| CLI 客户端(lark-cli) | Hermes agent / page_ai_workflow | AI 编排层,只产生`{pattern, content}`对 | + +**mnote 的 Rust kernel 就是那个"服务端"。** 所以在 Rust 层实现 markdown→block content 的转换是正确的方向——这不是"在客户端做服务端的事",而是 mnote 的 Rust 层本来就承担服务端职责。 + +### 0.3 CLI Main 对我们设计的启发 + +1. **`str_replace` 是不可拆分的原子操作** — CLI Main 的 `str_replace` 由服务端完整执行。一旦 `pattern` + `content` 送出,客户端不需要处理块映射。**mnote 的 `doc_markdown_edit` 也应该是一条完整的原子路径**:接收 `operations` → 搜索替换 → 产生 final_md → **直接转换 final_md 为 blocks 并写回**。不应该把 operations 暴露给下游做二次推导。 + +2. **文档中的所有纯文本搜索替换只需一条 API 调用** — CLI Main 不支持在一次请求中组合多个 `str_replace`。mnote 支持多条 operation 是更灵活的设计,但兑现这个灵活性的前提是:**多条 operation 在服务端累积应用到 `md` 后,只产出一组最终的 block content 写回**,而不是每条 operation 独立映射到 block。 + +3. **`pattern` 不限制为精确文本** — CLI Main 的 `str_replace` 文档鼓励 Markdown 模式下的"前缀...后缀"省略号语法(`"start...end"`),允许模糊定位。这和我们已有的四级匹配策略(精确→忽略空白→段落 fuzzy→失败)方向一致。 + +4. **CLI Main 在客户端做预检查** — `docs_update_check.go` 中有 `checkDocsUpdateReplaceMultilineMarkdown`,在发送请求前就检查 Markdown 中的空行是否会违反服务端的单块内文本替换限制。**同理,mnote 在转换 final_md 时也应该做预检**:发现跨块内容变化时,要么走 GFM 解析器重建多块 content,要么清楚告知 AI "这条 operation 需要多块替换"。 + +5. **mnote 的 final_md 方法比 CLI Main 更可靠** — CLI Main 把 `pattern` 发给服务端,由服务端重新执行匹配。如果服务端的匹配策略与模型预期的不同,结果会意外。mnote 的做法(在 Rust 层计算 final_md,然后用 final_md 生成 block content)把匹配阶段和写回阶段解耦——匹配结果对 AI 可见(文档中说"已验证将 X 替换为 Y"),不会出现「客户端匹配成功、服务端匹配失败」的不一致。 + +### 0.4 与 CLI Main 的根本差异:为何不能照搬 + +CLI Main 的方案(客户端只传 `pattern` + `content`,服务端做匹配+转换)依赖 Lark OpenAPI 对文档 block 存储的完全控制。mnote 的 Convex 后台(`documents:updateContent`)只接受完整的 blocks 数组作为 `content`,没有 `str_replace` 端点。 + +**这迫使 mnote 的 Rust 层必须自己做 final_md → block content 的转换。这个转换就是我们的"服务端逻辑",是正确且必要的。** + +--- + +--- + +## 1. 问题 + +### 1.1 当前架构 + +`mnote.doc.markdown_edit` 在在线 Convex 文档路径下有两套编辑结果: + +``` +搜索替换 → 最终 md ✅(正确的结果) + + ↓ + build_block_ops_from_markdown_edit(blocks, operations, applied) + ↓ 二次推导 + doc_apply_block_ops → page.body.save +``` + +- **第一层**: `search_replace(&md, &search, &replace)` — 在完整的 markdown 字符串上顺序执行操作,支持精确/忽略空白/段落 fuzzy 四级匹配。 +- **第二层**: `build_block_ops_from_markdown_edit` — 不从最终 `md` 提取修改,而是从原始 blocks 和原始 operations 重新推导每次命中。 + +**最终 markdown 被丢弃了。** 写入层使用的不是计算好的文本结果,而是从原始 operations 反查 blocks 的二次推导。 + +### 1.2 由此导致的已知缺陷 + +| 缺陷 | 表现 | 根因 | +|------|------|------| +| 7-17 同块多操作覆盖 | 两次 op 命中同块 → 第二 op 从原始文本计算,覆盖第一 op | 推导层不累积 | +| 7-24 在线不以最终 md 为真源 | `full_content` 无法映射到块 operations → 全部拒绝 | 推导层不支持全文替换 | +| 7-25 命中后空 block_ops | markdown 层匹配成功,block 映射失败 → 空 ops | 两套匹配规则不一致 | + +当前「修复」是安全降级:推导失败从伪成功变成明确错误码,但推导本身仍然存在。 + +### 1.3 为什么推导不可靠 + +`build_block_ops_from_markdown_edit` 当前采用「每 operation 从 `block.text` 反查 + 维护 block_states 累积文本」策略 [doc.rs:1048-1103],已在共享匹配函数和累积方面改进,但根本问题仍在: + +1. **full_content 模式必须跳过推导**:全文替换的最终 md 与原始 blocks 没有逐 op 对应关系 +2. **跨块替换不可映射**:一条 `search: "TODO"` 命中多个块时,推导层只产生一次 replace op +3. **格式变化丢失**:heading/todo 前缀在 markdown 中记录了结构语义,推导层只传文本 + +--- + +## 2. 设计目标 + +### 2.1 核心原则 + +> **最终 markdown 是写回的唯一真源。** 文本层算出的结果不经二次推导直接落地。 + +### 2.2 目标范围 + +- 在线 Convex 文档的 `mnote.doc.markdown_edit` 以最终 `md` 为唯一真源生成 `page.body.save` 的 `content` 载荷 +- 本地文件路径不变(已经以最终 md 直接 `fs::write`) +- `mnote.block.*` / `mnote.doc.apply_block_ops` 不做改动 +- `full_content` 模式不再被拒绝,支持 +- 复杂块(mindmap、resource、table、image)在往返中保持不变 + +### 2.3 非目标(Phase C 范畴) + +- 流式 apply(见 7-14 §7) +- suggest/review 模式 +- 本地 `.md` 文件的块标识持久化(本地文件没有 block identity) + +--- + +## 3. 方案:Markdown → Block Content 直接写回 + +### 3.1 流程图(替换后) + +``` +搜索替换 → 最终 md ✅ + ↓ + parse_final_markdown_to_blocks(&md, &original_blocks) + ↓ + [blocks with preserved IDs + metadata + new blocks] + ↓ + build_page_content(&original_body_content, &parsed_blocks) + ↓ + execute_page_body_save_from_aggregate(state, context, input, &aggregate, next_content, changed_blocks) +``` + +### 3.2 数据流 + +``` +原始 blocks (来自 aggregate.body.blockDocument.blocks): + b1: { type: "paragraph", text: "第一段", revisionRef: "r1" } + b2: { type: "heading", text: "标题", revisionRef: "r2", props: { level: 2 } } + b3: { type: "resource", ... } ← 复杂块,全文不做修改 + +↓ blocks_to_markdown (当前,增强前) + +当前 md (include_ids=true): + 第一段 + 标题 + [resource: 资源块] + +↓ 搜索替换 + +最终 md: + 新第一段 + 新标题 + [resource: 资源块] + +↓ parse_final_markdown_to_blocks (新增) + +解析后的 blocks: + b1: { type: "paragraph", text: "新第一段", revisionRef: "r1" } ← 文本更新,其他不变 + b2: { type: "heading", text: "新标题", revisionRef: "r2", props: { level: 2 } } + b3: { type: "resource", ... } ← 保持原始内容不动(复杂块不可编辑) + +↓ build_page_content (新增) + +最终 content (aggregate.body.content 格式): + [b1_legacy, b2_legacy, b3_legacy] ← 复用原始块的非文本属性 +``` + +### 3.3 组件设计 + +#### 3.3.1 `blocks_to_markdown` 增强(已有函数,扩展) + +当前只输出 ``,需要扩展为带类型和 revisionRef: + +```rust +// 当前: +format!("{text} ") + +// 增强后: +format!("{text} ") + +对 heading 块额外附加 `level` 字段: + +```rust +// heading 块: +format!("{text} ") +``` + +解析器还原后从 `level` 字段恢复正确的 heading 级别。 +``` + +对 heading 和 todo 等结构化块,前缀已经正确输出(`## `、`- [ ] `),解析器据此还原类型。 + +对复杂块(mindmap、resource、table、image)——即 `is_editable == false` 或在 `block_projection_blocks` 中标记了 `unsupportedReason` 的块——输出特殊标记使其不会被解析器修改: + +```text +[mnote-raw-block:block_id] +``` + +然后写回层把这些块从原始 blocks 中按 ID 复制,不做任何修改。 + +#### 3.3.2 `parse_final_markdown_to_blocks`(新增函数) + +签名: + +```rust +/// 解析最终 markdown 为块列表,保留块元数据 +/// +/// * `final_md` — 搜索替换后的 markdown +/// * `original_blocks` — 从 aggregate.body.blockDocument.blocks 读取的原始块 +/// +/// 返回解析后的块列表,每个块包含后续写回所需的所有字段 +fn parse_final_markdown_to_blocks( + final_md: &str, + original_blocks: &[Value], +) -> Vec { + // 1. 利用 comrak 或逐行解析提取 + // - `` 中的块元数据 + // - 对带 ID 的块:保留原始块的属性 + // - 对无 ID 的文本块:标记为"新块" + // - 对 [mnote-raw-block:...] 标记:原样保留原始块 + // 2. 返回重建后的块列表 +} +``` + +返回值 `ParsedBlockInfo`: + +```rust +struct ParsedBlockInfo { + block_id: Option, // None = 新块(无原始 ID) + block_type: String, // "paragraph" / "heading" / ... + text: String, + block_revision_ref: Option, // 从注释或原始块继承 + is_new: bool, // true = 非原始块,需要分配新 ID + original_block: Option, // 从原始 blocks 复制(若 block_id 匹配) + props: Option, // 从原始块保留的属性 +} +``` + +解析步骤: + +1. **按换行分割 markdown 行**,忽略空行 +2. **对每行提取 `` 注释**(正则:`` 或类似) +3. **若无注释但有 `[mnote-raw-block:id]` 标记 → 从原始 blocks 按 ID 复制** +4. **若既无注释也无原始标记 → 创建新 paragraph 块**,无 `blockId`(由写入层生成) +5. **从注释中获取 `block_type`;若无注释,从行前缀推断**(`## ` → heading,`- [ ] ` → todo) +6. **用行中 `` 注释(极罕见,通过 AI 输出不可能) +- 模型输出的 search/replace 跨段合并了内容 + +**策略**:保留该原始块在 `content` 中的位置。`build_page_content` 对在 original 中存在但不在 parsed 中的块,按原样复制到最终 content。 + +### 4.2 新建块(无原始 ID) + +用户可能要求"在末尾加一段总结"。模型输出 `full_content` 或 search/replace 产生的新文本带 ``。解析器标记为 `is_new=true`,写入层分配新的 `blockId`。 + +**策略**:新块使用临时 ID 格式 `ai_block_{timestamp}_{counter}`,与现有 `mnote.block.insert_after` 的块 ID 风格一致。 + +### 4.3 复杂块保留 + +resource/mindmap/table/image 块在 `blocks_to_markdown` 中被序列化为特殊标记 `[mnote-raw-block:id]`。搜索替换通常不会命中这些行(因为它们不含普通文本),但如果意外命中: + +**策略**:在 `search_replace` 中,如果 search 路径包含了复杂块的特殊标记,整条 operation 标记为 failed。解析器遇到 `[mnote-raw-block:...]` 标记时,直接从 original blocks 复制。 + +### 4.4 搜索替换未命中任何块(applied > 0 但所有命中都是新文本) + +当模型输出的 full_content 完全不同于原文时可能出现。 + +**策略**:`parse_final_markdown_to_blocks` 对每一行都产生块。如果没有任何 `` 注释,所有块标记为 `is_new=true`。`build_page_content` 追加新块在后面,同时保留所有原始复杂块。 + +### 4.5 revision / conflictDetectionKey 一致性 + +`build_block_ops_from_markdown_edit` 当前依赖 `doc_apply_block_ops_with_meta` 来传递 revision。替换为直接调用 `execute_page_body_save_from_aggregate`,它从 aggregate 读取 revision/conflictDetectionKey [block.rs:1178-1189]。 + +**策略**:新路径从已经读到的 `aggregate` 中获取 revision/conflictDetectionKey,与现有路径完全一致。 + +### 4.6 dryRun 模式 + +当前 online 路径中 dry_run 由 `doc_apply_block_ops` 内部处理(不真正写入)。新路径也需要支持: + +**策略**:若 `dryRun == true`,调用 `build_page_content` 但不调用 `execute_page_body_save_from_aggregate`,返回 diff 预览。diff 格式与当前 `docs.md:plan_update` 的 diff 格式一致。 + +--- + +## 5. 与已有代码的互动 + +### 5.1 `build_block_ops_from_markdown_edit` 的删除 + +该函数不再被 `doc_markdown_edit` 调用。它是一个私有 `fn`(仅 `doc.rs` 内部可见),唯一调用者被移除后成为死代码。 + +**策略**:直接删除函数体。保留调用处的行作为注释(`// 退役:7-27 改为 final_md→blocks 直接写回`),供后续参考。 + +### 5.2 `block_projection_blocks` 的继续使用 + +仍然需要原始 blocks 作为元数据源(提取 revisionRef、props、复杂块)。不改变。 + +### 5.3 `local_markdown_parser` 的复用 + +`parse_final_markdown_to_blocks` 可以复用 `local_markdown_parser::markdown_to_blocks` 对 GFM 结构的解析(heading、todo、code block、list),但需要用自己的逻辑提取 `` 注释。不一定要用 comrak 的 `HtmlBlock` 解析;更可靠的方法是正则提取注释,然后从剩余内容中推断块类型。 + +**策略**:默认使用简单的行级处理(非 comrak),因为 `blocks_to_markdown` 的输出是每行一块的简单格式,不需要完整的 GFM AST。 + +> **⚠️ 跨行约束**:行级处理仅适用于 `blocks_to_markdown` 产出的单行块格式。以下情况需要 fallback 到 `local_markdown_parser::markdown_to_blocks`: +> - 代码块(`block_text` 含 `\n`) +> - 用户通过 `full_content` 自由书写的多段 markdown +> - 解析跳过了 `` 的行之间的纯段落 +> +> fallback 策略:对无 `` 注释的连续行,收集后一次性通过 GFM 解析器分割为多个常规块。 + +### 5.4 `changed_blocks` 摘要生成 + +当前返回的 `applyResult.changedBlocks` 由 `doc_apply_block_ops` 产生。新路径需要自己生成: + +```rust +fn build_changed_blocks_summary( + original_blocks: &[Value], + parsed_blocks: &[ParsedBlockInfo], +) -> Vec { + // 对比原始 blocks 和解析后的 blocks + // 对文本改变的块输出 { op: "replace", blockId, content } +} +``` + +响应体中的 `changedBlocks` 字段格式不变,保持与下游消费者(SSE delta 等)的兼容。 + +--- + +## 6. 实施计划 + +### 步骤 1:增强 `blocks_to_markdown`(小) + +**文件**: `doc.rs:574` + +改动: +- 注释格式从 `` 改为 `` +- 对复杂块(`editable==false` 或 `unsupportedReason!=null`)输出 `[mnote-raw-block:{id}] ` + +**风险**: 低。纯格式变更,向前兼容——旧注释格式的 md 在解析器看来只是缺类型/rev,可通过 fallback 从原始 blocks 查。 + +**测试**: 更新现有 `test_blocks_to_markdown_with_ids`,验证新格式。 + +### 步骤 2:实现 `parse_final_markdown_to_blocks`(中) + +**新建模块**:`doc_md_to_blocks.rs` 或放在 `doc.rs` 末尾 + +核心逻辑(~120 行): + +```rust +struct ParsedBlock { + block_id: Option, + block_type: String, + text: String, + revision_ref: Option, + original: Option, + is_new: bool, +} + +fn parse_final_markdown_to_blocks(md: &str, originals: &[Value]) -> Vec { + // 1. 为 originals 建立 block_id → Value 的 HashMap + // 2. 将 md 按行分割 + // 3. 对每行: + // a. 用正则提取 或 [mnote-raw-block:id] + // b. 若无注释 → is_new=true, type="paragraph" + // c. 若有注释 → 查 originals_map 继承 revision_ref/props + // d. 提取 " + ).unwrap(); + static ref RAW_BLOCK_RE: Regex = Regex::new( + r"\[mnote-raw-block:([a-zA-Z0-9_-]+)\]" + ).unwrap(); +} +``` + +**测试**: +- `test_parse_empty_md` → 空输入 → 空输出 +- `test_parse_with_block_ids` → 包含 `` → 返回正确 ParsedBlock +- `test_parse_no_ids` → 纯文本 → 全部 is_new +- `test_parse_raw_block` → `[mnote-raw-block:b3]` → 从 originals 复制 +- `test_parse_type_from_prefix` → `## Title` → type="heading" +- `test_parse_todo_prefix` → `- [x] Done` → type="todo" + +### 步骤 3:实现 `build_page_content`(中) + +**文件**: 与步骤 2 同模块(~80 行) + +```rust +fn build_page_content(original_content: &Value, parsed: &[ParsedBlock]) -> Value { + // 1. 为 original_content 建立 block_id → full_block 映射 + // 2. 遍历 parsed_blocks: + // - 有 block_id 且 original 中存在 → 复制 original 块,更新 text + // - 无 block_id → 创建新 paragraph 块 + // - 有 block_id 但不在 original 中 → 复制 original 中能找到的(从已处理集合中移除) + // 3. 遍历 original_content 中的复杂块(不在 parsed 中出现的)→ 追加 + // 4. 返回 Value::Array +} +``` + +**测试**: +- `test_build_content_preserves_unmodified_blocks` +- `test_build_content_updates_text` +- `test_build_content_preserves_complex_blocks` +- `test_build_content_new_blocks_appended` + +### 步骤 4:替换 `doc_markdown_edit` 的 online 写回路径(小) + +**文件**: `doc.rs:854-900` + +将现有的: +```rust +let aggregate = aggregate_value(state, context, input).await?; +let blocks = block_projection_blocks(&aggregate); +let block_ops = build_block_ops_from_markdown_edit(&blocks, &operations, applied); +if applied > 0 && block_ops.is_empty() { ... } +let apply_result = doc_apply_block_ops_with_meta(...).await?; +``` + +替换为: +```rust +let aggregate = aggregate_value(state, context, input).await?; +let blocks = block_projection_blocks(&aggregate); +let parsed = parse_final_markdown_to_blocks(&md, &blocks); +let original_content = current_body_content(&aggregate); +let next_content = build_page_content(&original_content, &parsed); +let changed_blocks = build_changed_blocks_summary(&blocks, &parsed); +let apply_result = execute_page_body_save_from_aggregate( + state, context, input, &aggregate, next_content, changed_blocks +).await?; +``` + +新增 `build_changed_blocks_summary`(~30 行)。 + +**风险**: 中。第一次替换时需要与现有测试对照输出,确保 changed_blocks 格式一致。 + +**测试**: +- 现有 `hermes_tools_markdown_edit_*` 测试全部重新运行——断言现有行为不变 +- 新增 `test_markdown_edit_online_single_replacement`:验证单块替换后 content 正确 +- 新增 `test_markdown_edit_online_full_content`:验证全文替换不再被拒绝 +- 新增 `test_markdown_edit_online_preserves_complex_blocks`:验证 mindmap 块不变 + +### 步骤 5:更新 full_content 处理(小) + +**文件**: `doc.rs:785-800` + +当前 `full_content` 构造一个 `search: current_md.trim(), replace: full.trim()` 的 operation。替换后不再需要这个折中——直接解析 full_content 为 blocks。 + +改动:在 `parse_final_markdown_to_blocks` 中,若没有任何 `` 注释,所有块标记为 is_new。`build_page_content` 只追加新块 + 保留原始复杂块。 + +**测试**: +- `test_markdown_edit_online_full_content_creates_new_blocks` +- `test_markdown_edit_online_full_content_preserves_complex` + +### 步骤 6:清理(小) + +- 删除 `build_block_ops_from_markdown_edit` 函数体(私有 fn,唯一调用者已移除) +- 在原调用位置保留注释:`// 退役:7-27 改为 final_md→blocks 直接写回` +- 更新 manifest/guidance 文字:full_content 不再被拒绝 + +--- + +## 7. 测试矩阵 + +| 场景 | 输入 | 期望输出 | 优先级 | +|------|------|---------|--------| +| 单块精确 search/replace | `{search:"第一段", replace:"新内容"}` | 文本更新,blockId/revisionRef 不变 | P0 | +| 单块忽略空白 match | `{search:"第 一 段", replace:"新内容"}` | 文本更新(原块中"第一段"→"新内容") | P0 | +| 多 operation 命中同块 | `[{op1},{op2}]` 同块 | 最终文本 = 两次替换累积结果 | P0 | +| full_content | `"新全文"` | 新文本创建新块,原复杂块保留 | P0 | +| 影响 heading | search 命中 heading 文本 | heading 类型不变,文本更新 | P1 | +| 包含复杂块文档的替换 | 只改 paragraph 文本 | resource/mindmap 块不变 | P1 | +| dryRun 不落盘 | `dryRun: true` | 返回 diff,无写入 | P1 | +| revision 过期 | aggregate revision 已过期 | 冲突错误,不写入 | P1 | +| 原始块 ID 消失 | 搜索替换删除了 `` 注释 | 该原始块保留(不从 content 删除) | P2 | +| 空 operations | `[]` | 错误码 | P2 | +| 纯文本 md(无块注释) | `"摘要\n\n补充"` | 全部 is_new,追加到末尾 | P2 | + +--- + +## 8. 灰度/回滚 + +不设 feature flag。直接替换 online 写回路径。理由: + +- 旧路径(7-24/7-25 修复后)在 `full_content` 等场景是**安全降级(拒绝写入)**,新路径是功能增强。不会出现「旧路径能写入、新路径不能」的退化。 +- 现有 `hermes_tools_markdown_edit_*` 测试集 + 新增 5 个测试组合足够兜底。 +- 若确实需要回滚,用 `git revert` 回退本次改动即可。 + +灰度策略:默认开启,观察到新增测试全部 pass 后合入主线。 + +--- + +## 9. 关联文件清单 + +| 文件 | 变更类型 | 变更内容 | +|------|---------|---------| +| `doc.rs` `blocks_to_markdown` | 修改 | 注释格式增强 | +| `doc.rs` `doc_markdown_edit` | 修改 | online 写回路径 | +| 新建 `doc_md_to_blocks.rs` | 新增 | `parse_final_markdown_to_blocks` + `build_page_content` + `build_changed_blocks_summary` | +| `doc.rs` `build_block_ops_from_markdown_edit` | 标记 | `#[deprecated]` | +| `hermes_tools.rs` tests | 新增 | ~5 个新测试 | +| `manifest.rs` / guidance.md | 微调 | full_content 不再标记为拒绝 | + +--- + +## 10. 开放问题 + +1. **`` 注释在行中可能被模型生成的 search/replace 破坏**。例如模型输出 `{search: "第一段 ` 注释的全部新文本,此时 heading/todo 类型从 GFM 前缀推断。但列表、引用、代码块等格式需确认 `local_markdown_parser` 的 `markdown_to_blocks` 返回的结构是否足够完整。 +3. **行内格式(bold、italic、link)能否在往返中保持**。当前 `blocks_to_markdown` 只输出文本(`block_text`),丢弃所有 marks。在线文档的 text 存储包含 marks(bold/italic/code/link),当前 `doc_markdown_edit` 的整体设计不保证行内格式——这超出了 7-24/7-25 的范围,但建议在 7-14 的 Phase A/B 之后评估。 diff --git a/design/10-review/README.md b/design/10-review/README.md index 326dfed0..71886eee 100644 --- a/design/10-review/README.md +++ b/design/10-review/README.md @@ -1,16 +1,17 @@ # 10-review 审查总览 -> 执行状态:`done/01` 到 `done/07` 均已归档;`process/08` 是当前活跃架构收口与下一阶段优先级 review;`process/09` 记录页面 AI 快速块编辑 runtime 的阶段性结论与下一步基建方向。 +> 执行状态:`done/01` 到 `done/11` 均已归档;当前无活跃 10-review process 文档。 当前活跃审查: -- [Kernel 架构收口与下一阶段优先级 Review / Checklist](./process/08-kernel-architecture-next-priority-review-and-checklist.md) -- [页面 AI 快速块编辑 Runtime Review](./process/09-page-ai-fast-block-edit-runtime-review.md) -- [当前 mnote 项目 AI / Page Aggregate 定向 Review](./process/10-current-mnote-ai-runtime-review-v1.md) -- [当前完整架构 Review](./process/11-current-full-architecture-review-v1.md) +- 暂无。 最新归档审查: +- [Kernel 架构收口与下一阶段优先级 Review / Checklist](./done/08-kernel-architecture-next-priority-review-and-checklist.md) +- [当前 mnote 项目 AI / Page Aggregate 定向 Review](./done/10-current-mnote-ai-runtime-review-v1.md) +- [当前完整架构 Review](./done/11-current-full-architecture-review-v1.md) +- [页面 AI 快速块编辑 Runtime Review](./done/09-page-ai-fast-block-edit-runtime-review.md) - [VSCode Explorer 文件树与垃圾箱闭环增量审查](./done/07-vscode-explorer-filetree-trash-gap-review.md) 上一轮最终执行入口: diff --git a/design/10-review/process/08-kernel-architecture-next-priority-review-and-checklist.md b/design/10-review/done/08-kernel-architecture-next-priority-review-and-checklist.md similarity index 69% rename from design/10-review/process/08-kernel-architecture-next-priority-review-and-checklist.md rename to design/10-review/done/08-kernel-architecture-next-priority-review-and-checklist.md index 43cbff31..5490cb25 100644 --- a/design/10-review/process/08-kernel-architecture-next-priority-review-and-checklist.md +++ b/design/10-review/done/08-kernel-architecture-next-priority-review-and-checklist.md @@ -1,8 +1,8 @@ -# 08 [process] Kernel 架构收口与下一阶段优先级 Review / Checklist v1 +# 08 [done] Kernel 架构收口与下一阶段优先级 Review / Checklist v1 -> 更新时间:2026-05-16 +> 更新时间:2026-05-18 > -> 执行状态:`process` +> 执行状态:`done` > > 关联文档: > - `/mnt/Data1T/mnote/AGENTS.md` @@ -35,14 +35,14 @@ - Page Aggregate 仍是过渡态,block projection 主要从 `documents.content` / local markdown content 投影,不是 EditorBlockDocument 原生落库完成态。 - 标题、正文、页面设置、page tree、AI 写入口尚未完全闭环到同一组 projection / command family。 - `tree.*` 已是 preferred command name,但 `documents.*` 兼容命令面仍未完全降级。 -- `/api/tree/events` 已是 tree realtime 主链,但 Sidebar、page subtree、filetree、preferred snapshot 还没有完全统一到一套 live cache。 +- `/api/realtime/ws` 已是 tree realtime 主链,`/api/tree/events` 已降级为 SSE fallback;live cache 的后续重点是减少兼容 fallback 与补偿链。 - AI 块工具已有最小闭环,但当前编辑路径以块操作(blockId)为中心——AI 被迫理解 UI 层概念,且本地 `.md` 文件无 AI 写入路径。`7-14` 提出应将 AI 编辑主路径修正为 markdown 文本级(search/replace),`mnote.block.*` 降级为结构性辅助,在线 Convex 文档与本地文件共用同一条 markdown 写入路径。 补充 BlockNote / Tiptap AI 参考后的判断: - BlockNote AI 和 Tiptap AI Toolkit 可以参考的是 AI runtime shape 与 tool contract,不是 mnote 的事实源。 - AI 方向下一步只应补基础底座:`PageAIContextBuilder`、`scope=selection`、`page_xml/text`、tool manifest annotations、`PageAIReviewSession`、accept/reject/retry/abort 状态机。 -- 2026-05-16 补充(受 `7-14` 方向修正):AI 编辑主路径应从块级操作调整为 markdown 文本级 search/replace,新增 `mnote.doc.markdown_edit` 作为主要 AI 写入工具;当前 `local_rule` planner(`direct_block_edit_operations`)是过渡实现,应退役。在线 Convex 文档和本地 `.md` 文件共用同一条写入路径。 +- 2026-05-18 补充(`7-18` 到 `7-25` 收口后):AI 简单正文编辑主路径已切到 markdown 文本级 search/replace / full_content,`page_ai_workflow.rs` 当前通过模型生成 markdown 编辑意图后调用 `mnote.doc.markdown_edit`,并复用统一 mnote tool executor;`local_rule` planner(`direct_block_edit_operations`)不再作为当前 runtime 口径。在线 Convex 文档和本地 `.md` 文件共用同一条 markdown 写入合同。 - 在非 AI 主架构和 AI 基础合同打牢之前,不应扩展新的 AI agent 工作流、复杂 AI UI 或跨页面智能功能。 因此,下一阶段不应优先“大改架构”或“大量加新功能”,而应优先做: @@ -89,7 +89,7 @@ 目标: -- `/api/tree/events` 的 snapshot / delta / resync 成为 Sidebar、Page Tree、File Tree、page subtree 的共同 live cache 来源。 +- `/api/realtime/ws` 的 snapshot / delta / resync 成为 Sidebar、Page Tree、File Tree、page subtree 的共同 live cache 来源,`/api/tree/events` 仅作为 SSE fallback。 - 减少 query/refetch/freshness 补偿链和旧快照回闪。 ### P0:AI 基础工具与审阅底座 @@ -115,13 +115,13 @@ - 不新增复杂 AI 自动化功能面。 - 不把 `@blocknote/xl-ai` 或 BlockNote `AIExtension` 作为 runtime dependency。 - 不让 AI 写入绕过 `dryRun/idempotencyKey/revision/conflictDetectionKey/revisionRef`。 -- **不把 `direct_block_edit_operations` / `local_rule` planner 当作长期 AI 编辑路径(当前为过渡实现,将被 `markdown_edit` 替代)。** +- **不把 `direct_block_edit_operations` / `local_rule` planner 当作当前或长期 AI 编辑路径;简单正文编辑当前主路径是 `mnote.doc.markdown_edit`,结构性块操作才使用 `apply_block_ops` / `mnote.block.*`。** - **不新增 `mnote.block.*` 工具;现有保留为结构性辅助。** -**2026-05-16 新增(受 7-14 方向修正):** +**2026-05-18 状态更新(`7-18` 到 `7-25` 收口后):** -- **Phase A(当前唯一活跃实施)**:`mnote.doc.markdown_edit` + `mnote.doc.fetch` 增强(search/replace + `format: "markdown"` + local source + Hermes 注册,7-14 v2 Phase A)。 -- **Phase B(下一阶段)**:退役 `direct_block_edit_operations`,`page_ai_workflow.rs` 改走 `markdown_edit`(7-14 v2 Phase B)。 +- **Phase A / B 主路径已完成当前收口**:`mnote.doc.markdown_edit` + `mnote.doc.fetch` 增强、Hermes manifest / guidance、`page_ai_workflow.rs` → `markdown_edit`、统一 mnote tool executor、local / online markdown source 合同已同步。 +- **Phase C 仍冻结**:流式 apply + suggest/review 只保留设计边界,当前不实施、不扩新 AI 功能。 - 在线 Convex 文档和本地 `.md` 文件共用同一条 markdown AI 写入路径(`resolve_source` → Convex | LocalFS)。 ### P1:定向 Bug Hunt @@ -144,8 +144,8 @@ ### 3.1 Page Aggregate 单一真源 -- [x] 盘点 `wolai-frontend/src/lib/documents/page-aggregate-loader.ts` 是否仍有 runtime fallback 或 TS builder 读取分支。 -- [x] 盘点 `wolai-frontend/src/lib/documents/page-aggregate-builder*` 的引用,确认只剩 test helper / historical adapter。 +- [x] 盘点历史 `wolai-frontend/src/lib/documents/page-aggregate-loader.ts` 是否仍有 runtime fallback 或 TS builder 读取分支。 +- [x] 盘点历史 `wolai-frontend/src/lib/documents/page-aggregate-builder*` 的引用,确认只剩 test helper / historical adapter。 - [x] 检查 `/api/documents/page` 仍返回明确 `410`,不参与 runtime 主链。 - [x] 跑文档页打开 smoke,记录 `/api/page-aggregate/:id` 是首要读链。 - [x] 新建页面后检查 Page Aggregate `identity/head/body/tree/stats` 字段完整。 @@ -156,7 +156,7 @@ - [x] 破坏或暂停 Convex query,检查响应是 degraded/error,不返回伪 fixture。 - [x] 在 `design/05-editor-mainline/process/5-6-page-aggregate-alignment-checklist-v1.md` 勾选已验证项,并补证据路径。 -2026-05-16 静态审计证据: +2026-05-16 静态审计证据(历史 Next 前端退役前快照): - `wolai-frontend/src/lib/documents/page-aggregate-loader.ts` 只通过 `loadPageAggregateFromRustSnapshot` 请求 Rust `/api/page-aggregate/:documentId`,`loadPageAggregate` 不再回退 TS builder;`buildServerBridgeRequest("/documents/page")` 只用于构造转发 header/request,不调用 Next `/api/documents/page` compat route。 - `rg -n "buildPageAggregateFromDocumentPayloads|page-aggregate-builder" wolai-frontend/src rust scripts design --glob '!design/05-editor-mainline/reference-code/**' --glob '!node_modules/**'` 显示 runtime 非测试引用仅剩 builder 定义;代码引用只有 `wolai-frontend/src/lib/documents/page-aggregate-builder.test.ts`。 @@ -191,6 +191,8 @@ - 证据:`tmp/page-aggregate-refresh-persistence-smoke/mp88fr6k.json`,截图:`tmp/page-aggregate-refresh-persistence-smoke/mp88fr6k.png`,stdout:`tmp/page-aggregate-refresh-persistence-smoke/latest.stdout.json`。 - 验证结果:刷新前后 `head.title=Page Aggregate refresh mp88fr6k`、`body.revision=1`、`body.conflictDetectionKey=tree_1778929070007_1:1`、`blockDocument.blocks[0].text=Page Aggregate refresh body mp88fr6k`、`layout.pageOptions.wideLayout/smallText/layoutDensity=true/true/compact`;刷新后页头标题、`.ProseMirror` 正文、设置控件、`documentElement`、`.document-shell`、island root 和 `.editor-surface` 均保持最新值。 - 修复点:`rust/crates/mnote-web/src/ssr/pages/layout.rs` 将 `initializePageUiSurfaces` 延后到 `DOMContentLoaded` 后执行,避免 layout 脚本早于嵌入 Page Aggregate JSON / island DOM 完成时把 runtime 属性按默认 pageOptions 应用。 +- 2026-05-18 复测并扩展 `scripts/task-page-aggregate-refresh-persistence-smoke.js`,补充 `mnote.doc.fetch` / `tree.pageSubtree` 同组回读证据。证据:`tmp/page-aggregate-refresh-persistence-smoke/mpagfma1.json`,截图:`tmp/page-aggregate-refresh-persistence-smoke/mpagfma1.png`。 +- 验证结果:刷新前后 `head.title=Page Aggregate refresh mpagfma1`、`body.revision=1`、`body.conflictDetectionKey=tree_1779063433221_1:1`、`blockDocument.blocks[0].text=Page Aggregate refresh body mpagfma1`、`layout.pageOptions.wideLayout/smallText/layoutDensity=true/true/compact`、`tree.pageSubtree.rootNodeId=tree_1779063433221_1` 均一致;`mnote.doc.fetch` 返回 `schema=mnote.page_ai_context.v1`,`revision/conflictDetectionKey/revisionRef/text` 与同一 Page Aggregate block 完全一致。 - 缺陷记录:`bugs/05-editor-mainline/done/5-12-page-options-refresh-runtime-attrs-v1.md`。 - 配套测试已通过:`cargo test --manifest-path rust/Cargo.toml -p mnote-web page_aggregate`、`cargo test --manifest-path rust/Cargo.toml -p mnote-web documents_options_route_executes_page_layout_update_options`。 @@ -216,47 +218,61 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web document_shell_errors_wi ### 3.2 Page Block AI Tooling -- [ ] 以 `7-10` 为工具执行清单、`7-11` 为 AI runtime 基础设计,逐 phase 检查已勾选项是否都有代码、测试、smoke 证据。 +- [x] 以 `7-10` 为工具执行清单、`7-12` 为当前 AI runtime 基础设计,逐 phase 检查已勾选项是否都有代码、测试、smoke 证据。 - [x] 给 `mnote.doc.fetch scope=selection` 补真实选区上下文输入和返回结构。 - [x] 给 `mnote.doc.fetch format=page_xml` 补最小 PageXML 输出。 - [x] 给 `mnote.block.fetch format=text/page_xml` 补格式分支。 - [x] 给 tool manifest 补 `annotations`,区分 readonly / destructive / requiresApproval / selectionEffect。 - [x] 定义 `mnote.page_ai_context.v1`,明确 context 只来自 Page Aggregate projection。 -- [ ] 定义 `mnote.page_ai_review_session.v1`,明确 accept / reject / retry / abort 语义。 -- [ ] 给 `mnote.block.insert_after` 补多块插入限制和返回 inserted block ids。 -- [ ] 给 `mnote.block.move_after` 补标题带子块阻断 smoke。 -- [ ] 给 `mnote.block.move_after` 补列表项阻断 smoke。 -- [ ] 给 `mnote.block.move_after` 补表格 / mindmap / resource 阻断 smoke。 +- [x] 定义 `mnote.page_ai_review_session.v1`,明确 accept / reject / retry / abort 语义。 +- [x] 给 `mnote.block.insert_after` 补多块插入限制和返回 inserted block ids。 +- [x] 给 `mnote.block.move_after` 补标题带子块阻断 smoke。 +- [x] 给 `mnote.block.move_after` 补列表项阻断 smoke。 +- [x] 给 `mnote.block.move_after` 补表格 / mindmap / resource 阻断 smoke。 - [x] 给 `mnote.block.replace` 补 stale revision 失败用例。 - [x] 给 `mnote.block.replace` 补 stale blockRevisionRef 失败用例。 - [x] 给写工具补重复 idempotencyKey 的端到端用例。 - [x] 检查 `mnote.page.save` 在 manifest / UI 中继续标为页面级兜底,不显示为精确块编辑主入口。 -- [ ] 检查任何新增 AI surface 是否只是基础 review/context/tooling 验收,不是新功能扩展。 -- [ ] 更新 `design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md` 的每个 phase 证据。 -- [ ] 更新 `design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md` 的 Hermes 工具路由与审阅面 checklist。 +- [x] 检查任何新增 AI surface 是否只是基础 review/context/tooling 验收,不是新功能扩展。 +- [x] 更新 `design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md` 的每个 phase 证据。 +- [x] 更新 `design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md` 的 Hermes 工具路由与审阅面 checklist。 -**新增(2026-05-16,受 7-14 方向修正):** +2026-05-18 `mnote.block.insert_after` 多块插入证据: -**Phase A(当前唯一活跃实施):** -- [ ] `mnote.doc.markdown_edit` 核心实现:resolve_source → search_replace → write_target -- [ ] `mnote.doc.markdown_edit` 搜索替换语义:精确匹配 → fuzzy fallback → operationsFailed(7-14 §5) -- [ ] `mnote.doc.markdown_edit` Convex 写入 adapter(复用 `page.body.save` 链路) -- [ ] `mnote.doc.markdown_edit` 本地文件写入 adapter(`fs.writeFile` + GFM AST markdown 序列化) -- [ ] `mnote.doc.fetch format: "markdown"` 增强(Page Aggregate 产出 PageMarkdown) -- [ ] `mnote.doc.fetch` local source 支持(识别本地文件路径并返回 `.md` 原文) -- [ ] Hermes tool manifest 注册 `mnote.doc.markdown_edit` -- [ ] Hermes `mnote` plugin 更新:`mnote_doc_fetch` schema 增加 `format: "markdown"` 和本地文件路径支持 -- [ ] 乐观锁:`revision` 冲突检测 -- [ ] 单元测试 + smoke(在线 + 本地两种 source) +- 代码:`rust/crates/mnote-web/src/hermes_tools/block.rs` 支持 `content` / `block` / `blocks` 输入;`blocks` 限制 `1..=20`;多块写入按输入顺序连续插入,并在结果返回 `insertedBlockIds`。 +- manifest:`rust/crates/mnote-web/src/hermes_tools/manifest.rs` 已声明 `blocks.minItems=1/maxItems=20`,并用 `anyOf` 表达 `content | block | blocks` 三种输入。 +- 测试:`cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_block_insert_after -- --nocapture` 通过,覆盖多块成功和超过 20 个失败。 +- 回归:`cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools -- --nocapture` 通过,覆盖 Hermes tools 38 项。 -**Phase B(下一阶段):** -- [ ] `direct_block_edit_operations` 退役(`page_ai_workflow.rs` `local_rule` 分支) -- [ ] `page_ai_workflow.rs` 修正:system prompt 改为产 search/replace 对,底层走 `mnote.doc.markdown_edit` -- [ ] 补全 operation schema(`content` 字段) -- [ ] 浏览器 smoke:自然语言编辑可用(不使用「」格式) +2026-05-18 `mnote.block.move_after` 复杂块阻断证据: + +- 代码:`rust/crates/mnote-web/src/hermes_tools/block.rs` 继续以同父级、叶子块、可移动类型、editable、自身移动为真实写入前阻断条件。 +- 测试:`cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_block_move_after_blocks_complex_and_nested_blocks -- --nocapture` 通过,覆盖标题带子块、列表项、表格、mindmap、resource 均返回 `blocked=true` / `block_move_after_blocked`。 +- 回归:`cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools -- --nocapture` 通过,当前 Hermes tools 共 39 项。 + +**新增(2026-05-18,`7-18` 到 `7-25` 收口状态):** + +**Phase A / B(当前主路径已收口):** +- [x] `mnote.doc.markdown_edit` 核心实现:resolve_source → search_replace / full_content → write_target +- [x] `mnote.doc.markdown_edit` 搜索替换语义:精确匹配、归一化匹配、多操作同块合并、无法安全映射时失败 +- [x] `mnote.doc.markdown_edit` Convex 在线写入 adapter 以最终 markdown 映射写回 +- [x] `mnote.doc.markdown_edit` 本地文件写入 adapter 尊重 dryRun / idempotency +- [x] `mnote.doc.fetch format: "markdown"` 增强 +- [x] Hermes tool manifest 注册并描述 `mnote.doc.markdown_edit` 写入合同 +- [x] 乐观锁:revision / conflictDetectionKey 前置校验 +- [x] `direct_block_edit_operations` / `local_rule` 不再作为 `page_ai_workflow.rs` 当前主路径 +- [x] `page_ai_workflow.rs` 改为模型产 markdown search/replace / full_content,底层走 `mnote.doc.markdown_edit` +- [x] 单元测试覆盖 markdown_edit / page_ai_workflow / Hermes guidance / ACP tool contract **Phase C(设计冻结,不实施):** -- [ ] 流式 apply + suggest/review 设计已冻结,不阻塞 Phase A/B 实施 +- [x] 流式 apply + suggest/review 设计已冻结,不阻塞 Phase A/B 实施 + +2026-05-18 Review Session 合同定义: + +- 已在 `design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md` 定义 `mnote.page_ai_review_session.v1` 最小 schema。 +- 已明确状态:`draft/planning/previewing/awaiting_user/accepted/rejected/applying/applied/failed/aborted/stale`。 +- 已明确动作约束:`accept` 必须重新读 Page Aggregate 并校验 revision / conflictDetectionKey / revisionRef,`reject/abort` 不产生写入,`retry` 必须生成新 proposal 或 dry-run preview,yolo 模式也应生成同构 audit 数据。 +- 边界:这里只完成合同定义,不实施 Phase C 流式 apply 或新的审阅 UI。 2026-05-16 Page Block AI context / format focused smoke 证据: @@ -271,15 +287,17 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web document_shell_errors_wi - 边界:本轮只验证 `mnote.doc.apply_block_ops` 的 selection scope guard;单个 `mnote.block.replace/insert_after/move_after` 尚未校验 `allowedTargetBlockIds`,不能据此勾选完整 selection 写保护矩阵。 - 配套验证已通过:`node --check scripts/task-page-block-ai-context-format-smoke.js`、`cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools -- --nocapture`、`MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task-page-block-ai-context-format-smoke.js`。 -2026-05-16 Page Block AI stale / idempotency focused smoke 证据: +2026-05-16 / 2026-05-18 Page Block AI stale / idempotency focused smoke 证据: - 新增并运行 `scripts/task-page-block-ai-conflict-idempotency-smoke.js`,通过真实 3000 + 测试账号创建临时页,用 `mnote.page.save` 初始化 `p_1/p_2`,再围绕 `mnote.block.replace` 验证冲突和幂等安全边界。 - 证据:`tmp/page-block-ai-conflict-idempotency-smoke/mp8dyqiq.json`,截图:`tmp/page-block-ai-conflict-idempotency-smoke/mp8dyqiq-page.png`。 +- 最新复测证据:`tmp/page-block-ai-conflict-idempotency-smoke/mpafrevs.json`,截图:`tmp/page-block-ai-conflict-idempotency-smoke/mpafrevs-page.png`。 - 验证结果: - 首次 `mnote.block.replace` 携带最新 `revision/conflictDetectionKey/blockRevisionRef/idempotencyKey` 成功写入 `p_2`,返回 `commandName=page.body.save` 与 `commandId=page_body_save_req_1778938353824_11`。 - 使用同一 `idempotencyKey=idem_conflict_replace_mp8dyqiq` 再次调用 `mnote.block.replace`,即使请求 content 改成不同文本,也 replay 同一 `commandId`,Page Aggregate 回读 `p_2` 仍为首次写入文本,`revisionAfterFirst=2`、`revisionAfterReplay=2`,确认不重复写入。 - 使用旧 `revision/conflictDetectionKey` 调用 `mnote.block.replace` 返回 HTTP `400`、错误码 `mnote_tool_conflict`,正文保持首次写入结果。 - 使用最新 `revision/conflictDetectionKey` 但旧 `blockRevisionRef` 调用 `mnote.block.replace` 返回 HTTP `400`、错误码 `mnote_tool_conflict`,正文保持首次写入结果。 + - 2026-05-18 复测中,重复 `idempotencyKey=idem_conflict_replace_mpafrevs` replay 同一 `commandId=page_body_save_req_1779062303997_525`,`revisionAfterFirst=2`、`revisionAfterReplay=2`;旧 revision 与旧 blockRevisionRef 仍均返回 `mnote_tool_conflict`。 - 边界:本轮只覆盖 `mnote.block.replace` 直接 tool executor 的 stale revision / stale blockRevisionRef / idempotency replay;不代表 `mnote.block.insert_after` 幂等矩阵、所有写工具幂等矩阵、review session accept stale 或 accept/reject/retry/abort 已完成。 - 配套验证已通过:`node --check scripts/task-page-block-ai-conflict-idempotency-smoke.js`、`cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools -- --nocapture`、`MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task-page-block-ai-conflict-idempotency-smoke.js`。 @@ -302,11 +320,17 @@ node scripts/task-page-block-ai-tools-smoke.js - [x] 对 compat alias 返回增加 owner / deprecated 标识,避免被当作主链。 - [x] 更新 `design/04-tree-domain/done/4-6-tree-command-protocol-cutover-stage2-v1.md` 或新增后续 process checklist。 -2026-05-16 Tree Command Cutover 静态盘点与 alias 标识证据: +2026-05-18 当前 Tree Command Cutover 状态: + +- `3000` 当前主壳由 Rust `mnote-web` SSR 承载;主要页面生命周期动作通过 Rust `/api/tree/commands` route 生成 `tree.node.create`、`tree.node.rename`、`tree.subtree.move`、`tree.node.archive`、`tree.node.restore`、`tree.node.purge`、`tree.subtree.copy`。 +- `bridge-runtime` 仍接受历史 `documents.*` alias,但 execution plan / artifact 会标记 `preferredCommandName`、`compatCommandName`、`deprecatedAlias`,避免兼容命令被当成当前主链。 +- `wolai-frontend` / Next tree command route 只作为退役前历史审计材料,不再作为当前运行时主链依据。 + +2026-05-16 Tree Command Cutover 静态盘点与 alias 标识证据(历史 Next 前端退役前快照): - 只读盘点命令:`rg -n "documents\\.(create|title\\.update|move|archive|restore|delete|purge|copy_tree)" rust wolai-frontend scripts --glob '!node_modules/**'`。 -- 前端主链:`wolai-frontend/src/lib/documents/tree-command-client.ts` 的新建、重命名、移动、归档、恢复、永久删除和复制均 POST `/api/tree/commands`,返回 meta 使用 `TREE_COMMAND_PROTOCOL.*.preferredCommandName`;`TREE_COMMAND_PROTOCOL` 中的 `documents.*` 只保留为 `compatCommandName`。 -- Next tree command route:`wolai-frontend/src/app/api/tree/commands/route.ts` 在 create / move / rename / archive / restore 分支分别构造 `tree.node.create`、`tree.subtree.move`、`tree.node.rename`、`tree.node.archive`、`tree.node.restore`。 +- 退役前 Next 前端主链:`wolai-frontend/src/lib/documents/tree-command-client.ts` 的新建、重命名、移动、归档、恢复、永久删除和复制均 POST `/api/tree/commands`,返回 meta 使用 `TREE_COMMAND_PROTOCOL.*.preferredCommandName`;`TREE_COMMAND_PROTOCOL` 中的 `documents.*` 只保留为 `compatCommandName`。 +- 退役前 Next tree command route:`wolai-frontend/src/app/api/tree/commands/route.ts` 在 create / move / rename / archive / restore 分支分别构造 `tree.node.create`、`tree.subtree.move`、`tree.node.rename`、`tree.node.archive`、`tree.node.restore`。 - Rust Web tree route:`rust/crates/mnote-web/src/routes/tree.rs` 的 `create_command_wire` 输出 `tree.node.create`、`tree.node.rename`、`tree.subtree.move`、`tree.node.archive`、`tree.node.restore`、`tree.node.purge`、`tree.subtree.copy`;`tree_command` route 只接收 action,不接收旧 `documents.*` command name 作为主链输入。 - 兼容保留:`bridge-runtime` 仍接受 `documents.create/title.update/move/delete/restore/purge/copy_tree`,但本轮已在对应 execution plan 的 `args_json.commandProtocol` 增加 `family=tree`、`owner=rust-runtime-kernel`、`preferredCommandName`、`compatCommandName`、`deprecatedAlias`;旧 `documents.*` alias 会标记 `deprecatedAlias=true`。 - Transport 边界:`rust/crates/mnote-web/src/transport/convex.rs` 发送给 Convex legacy mutation 前会剥离 `commandProtocol`、`streamDeltaHint`、`domainEventHint`、`domainEventPlan(s)`,避免 legacy validator 把审计字段当写入参数。 @@ -337,14 +361,20 @@ node scripts/task122-rust-web-create-page-ui-smoke.js - [x] 重命名页面后,双浏览器 A/B 检查 Sidebar / Breadcrumb / File Tree 一致更新。 - [x] 移动页面后,双浏览器 A/B 检查 tree order 不回闪。 - [x] 删除 / 恢复后检查 trash 与主树事件一致。 -- [x] 断开 SSE 后恢复,检查 resync 能把 UI 拉回正确状态。 +- [x] 断开 WS / SSE fallback 后恢复,检查 resync 能把 UI 拉回正确状态。 - [x] 更新 `design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md` 的已验证项。 -2026-05-16 Tree Realtime Live Cache 静态盘点与 payload 覆盖证据: +2026-05-18 当前状态更新: -- 只读审查确认 React `AppLayoutShell` 同时接入 `useSidebarData` 与 `useSidebarTreeStream`,`usePreferredSidebarSnapshot` 负责 freshness 仲裁;Page Tree / File Tree 通过 preferred snapshot 消费 `kernelSidebarTree` 与 `kernelFileTreeProjection`。 -- 仍未统一的补偿链:`useSidebarData` 还保留 Convex query、HTTP `/api/sidebar` fallback 与手动 `refetch`;mutation 后仍有 `refreshTree` / `sidebarQuery.refetch`;Rust SSR shell 与 React hook 目前各自可建立 EventSource;page subtree 仍从 Page Aggregate client state 派生,不应误标为已统一 live cache。 -- Rust stream 盘点确认 workspace snapshot 已同时加载 `KernelProjectionKind::SidebarTree` 与 `KernelProjectionKind::FileTree`,SSE payload 的 `data.dataset.kernel_sidebar_projection` / `data.dataset.kernel_file_tree_projection` 覆盖 page/file row;subtree snapshot 仍只覆盖 `page_tree`,不含 file tree projection。 +- `/api/realtime/ws` 已成为 Rust SSR 主壳默认 transport(`convex-command-log-ws`),`/api/tree/events` 仅作为 SSE fallback。 +- `CURRENT_ARCHITECTURE.md` 与 `bugs/03-rust-web/done/3-16` 到 `3-18` 已同步:原“WS 文档口径 / 前端 SSE 实现”冲突、SSE push 跳过 polling safety net、WS/SSE delta 合同分裂均已修复。 +- 2026-05-16 下面的 SSE smoke 证据保留为迁移前历史验证材料,不再代表当前主链。 + +2026-05-16 Tree Realtime Live Cache 静态盘点与 payload 覆盖证据(WS 迁移前历史快照): + +- 退役前只读审查确认 React `AppLayoutShell` 同时接入 `useSidebarData` 与 `useSidebarTreeStream`,`usePreferredSidebarSnapshot` 负责 freshness 仲裁;Page Tree / File Tree 通过 preferred snapshot 消费 `kernelSidebarTree` 与 `kernelFileTreeProjection`。 +- 退役前未统一的补偿链:`useSidebarData` 还保留 Convex query、HTTP `/api/sidebar` fallback 与手动 `refetch`;mutation 后仍有 `refreshTree` / `sidebarQuery.refetch`;Rust SSR shell 与 React hook 当时各自可建立 EventSource;page subtree 当时仍从 Page Aggregate client state 派生。 +- Rust stream 历史盘点确认 workspace snapshot 已同时加载 `KernelProjectionKind::SidebarTree` 与 `KernelProjectionKind::FileTree`,SSE payload 的 `data.dataset.kernel_sidebar_projection` / `data.dataset.kernel_file_tree_projection` 覆盖 page/file row;subtree snapshot 当时只覆盖 `page_tree`,不含 file tree projection。 - 本轮新增 `stream_change_preserves_remove_asset_delta_fields`,确认 `tree.resource.delete` 的 `remove_asset` delta 字段保真;`structural_delta_requires_projection_snapshot` 已覆盖 `remove_asset` 需要 projection snapshot,避免资源删除类事件只靠局部 patch。 - 本轮强化 `scripts/task123-rust-web-tree-live-stream-consumer-smoke.js`:解析 SSE `snapshot` data,断言 `kind=snapshot`、`stream=workspace`、`projection=sidebar_tree`、`x-mnote-tree-stream-owner=rust-web`,并确认 workspace snapshot 中有 `kernel_sidebar_projection`、`kernel_file_tree_projection` 和临时页 `doc:` file tree row。 @@ -425,21 +455,26 @@ node scripts/task449-tree-sse-reconnect-snapshot-recovery-smoke.js ### 3.5 定向 Bug Hunt -- [ ] 建立 bugs 分类:Page Aggregate 问题归 `bugs/05-editor-mainline/process/`。 -- [ ] Tree command / realtime / File Tree 问题归 `bugs/04-tree-domain/process/`。 -- [ ] AI tools 问题归 `bugs/07-ai/process/`。 -- [ ] 每个 bug 必须包含复现步骤、期望、实际、证据截图或 JSON、owner 判断。 -- [ ] 先补最小 failing smoke,再修实现。 -- [ ] 修复后移动到对应 `done/`,并记录验证命令。 +- [x] 建立 bugs 分类:Page Aggregate 问题归 `bugs/05-editor-mainline/process/`。 +- [x] Tree command / realtime / File Tree 问题归 `bugs/04-tree-domain/process/`。 +- [x] AI tools 问题归 `bugs/07-ai/process/`。 +- [x] 每个 bug 必须包含复现步骤、期望、实际、证据截图或 JSON、owner 判断。 +- [x] 先补最小 failing smoke 或定向单测,再修实现。 +- [x] 修复后移动到对应 `done/`,并记录验证命令。 重点 bug 方向: -- [ ] Page Aggregate 与页头标题不一致。 -- [ ] Page Aggregate 与 File Tree `{title}.md` 不一致。 -- [ ] AI 写入成功但 Page Aggregate 回读旧内容。 -- [ ] stale revision 未阻断写入。 -- [ ] tree stream 断线恢复后 UI 停在旧快照。 -- [ ] 旧 compat / debug route 在 3000 首屏被误用。 +- [x] Page Aggregate 与页头标题不一致。 +- [x] Page Aggregate 与 File Tree `{title}.md` 不一致。 +- [x] AI 写入成功但 Page Aggregate 回读旧内容。 +- [x] stale revision 未阻断写入。 +- [x] tree stream 断线恢复后 UI 停在旧快照。 +- [x] 旧 compat / debug route 在 3000 首屏被误用。 + +2026-05-18 定向 bug hunt 收口: + +- `find bugs -path '*/process/*' -type f | wc -l` 结果为 `0`。 +- 本轮 P0 / 内核优先缺陷已迁入 `bugs/*/done/`,并在 `CURRENT_ARCHITECTURE.md`、`design/10-review/done/10-current-mnote-ai-runtime-review-v1.md`、`design/10-review/done/11-current-full-architecture-review-v1.md` 中同步验证证据。 ### 3.6 2026-05-16 暂停交接进展摘要 @@ -497,16 +532,55 @@ MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:300 ## 4. 当前不优先做 -- [ ] 不优先增加新的编辑器 UI 大功能。 -- [ ] 不优先新增 AI Agent 工作流功能。 -- [ ] 不优先扩展 AI 功能面;AI 只做基础上下文、工具合同、审阅会话、冲突与回滚。 -- [ ] 不优先大规模替换 Convex。 -- [ ] 不优先重写 `leptos-tiptap` 输入层。 -- [ ] 不把 `mnote.page.save` 包装成精确块编辑长期方案。 -- [ ] 不在 compat route 继续扩写长期业务语义。 -- [ ] 不把 BlockNote / Tiptap AI runtime 作为 mnote runtime 依赖。 -- [ ] 不把 `direct_block_edit_operations` / `local_rule` planner 当作长期 AI 编辑路径(当前为过渡实现,将被 `mnote.doc.markdown_edit` 替代)。 -- [ ] 不新增 `mnote.block.*` 工具;`mnote.block.*` 保留为结构性辅助,不做块操作向新工具的扩张。 +- [x] 不优先增加新的编辑器 UI 大功能。 +- [x] 不优先新增 AI Agent 工作流功能。 +- [x] 不优先扩展 AI 功能面;AI 只做基础上下文、工具合同、审阅会话、冲突与回滚。 +- [x] 不优先大规模替换 Convex。 +- [x] 不优先重写 `leptos-tiptap` 输入层。 +- [x] 不把 `mnote.page.save` 包装成精确块编辑长期方案。 +- [x] 不在 compat route 继续扩写长期业务语义。 +- [x] 不把 BlockNote / Tiptap AI runtime 作为 mnote runtime 依赖。 +- [x] 不把 `direct_block_edit_operations` / `local_rule` planner 当作当前或长期 AI 编辑路径;简单正文编辑当前主路径是 `mnote.doc.markdown_edit`。 +- [x] 不新增 `mnote.block.*` 工具;`mnote.block.*` 保留为结构性辅助,不做块操作向新工具的扩张。 + +2026-05-18 静态边界核验证据: + +- `rg -n "mnote\\.block\\.[a-zA-Z0-9_]+" rust/crates/mnote-web/src/hermes_tools rust/crates/mnote-web/src/routes/hermes_tools.rs scripts/reasonix-acp-wrapper.mjs | ... | sort -u` 仅发现既有 `mnote.block.fetch/delete/replace/insert_after/move_after`,本轮未新增 `mnote.block.*` 工具。 +- `page_ai_workflow.rs` 注释与当前实现仍指向 `mnote.doc.markdown_edit`;`direct_block_edit_operations` 只保留为历史/测试辅助,不作为当前主路径。 +- `mnote.page.save` 在 manifest / 7-10 / 7-12 / 本 checklist 中继续描述为页面级粗粒度兜底,不是精确块编辑主入口。 +- 未引入 `@blocknote/xl-ai`、BlockNote `AIExtension` 或新的 AI Agent workflow;本轮改动仅限 Hermes tools 合同、阻断矩阵测试和 checklist 证据。 + +2026-05-18 `7-12` runtime 口径闭合证据: + +- `design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md` 已明确 `7-11` 旧“自有 AI runtime”口径移入 `design/old/07-ai/process/`,当前执行口径以 `7-12` 为准。 +- `7-12` 已定义 `PageAIContextBuilder`、`MnoteAIToolManifestProvider`、`PageAICommandRouter`、`PageAIReviewSession` 的边界,并明确 Phase C review / streaming apply 只做设计冻结,当前不实施。 +- `7-12` Phase B 到 Phase F 保留 manifest / router / review session / event / smoke 的后续 checklist;其中 selection 外写入 blocked 已有真实 smoke 证据,其余未实施项继续保留未勾。 + +2026-05-18 `7-10` 剩余真实页面矩阵拆分: + +- 新增 `design/07-ai/process/7-16-page-block-ai-real-smoke-followup-matrix-v1.md`,专门承接 `7-10` 中仍未完成的真实 3000 smoke 和 UI/review 边界矩阵。 +- `7-10` 继续保留为页面块 AI 工具执行总 checklist;`7-16` 只用于后续真实页面证据补齐,不扩新 AI 功能。 + +2026-05-18 真实页面 insert_after smoke 补充: + +- `scripts/task-page-block-ai-tools-smoke.js` 增加 `PLAYWRIGHT_CHROME_EXECUTABLE` 支持,用系统 Chromium 运行,避免当前系统无法下载 Playwright 官方 chromium 的阻断。 +- 命令:`PLAYWRIGHT_CHROME_EXECUTABLE=/snap/bin/chromium MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task-page-block-ai-tools-smoke.js` +- 证据:`tmp/page-block-ai-tools-smoke/mpag4966.json`,截图:`tmp/page-block-ai-tools-smoke/mpag4966-page.png`。 +- Page Aggregate:`tmp/hermes-tester/page-block-ai-tools-mpag4966/page-aggregate.json`,doc/fetch/find:`tmp/hermes-tester/page-block-ai-tools-mpag4966/doc-fetch-find.json`。 +- 覆盖:`mnote.block.insert_after` 插入 `todo` 块、插入顺序、`mnote.block.fetch` 回读新块、重复 `idempotencyKey` 不二次插入、刷新后文本可见。 +- 覆盖:`table_1/mindmap_1/resource_1` 在 AI block projection 中返回 `editable=false` 与 `unsupportedReason=复杂块暂不开放 AI 精确写入`;`heading_parent/list_item_1/table_1/mindmap_1/resource_1` 的 `plan_update block_move_after` dry-run 与正式 `mnote.block.move_after` 均阻断,阻断后 revision / 正文不变化。 + +2026-05-18 真实页面 doc.fetch / doc.find / block.fetch smoke 补充: + +- 命令:`PLAYWRIGHT_CHROME_EXECUTABLE=/snap/bin/chromium MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task-page-block-ai-tools-smoke.js` +- 证据:`tmp/page-block-ai-tools-smoke/mpag4966.json`,截图:`tmp/page-block-ai-tools-smoke/mpag4966-page.png`。 +- 覆盖:`doc.fetch full`、`doc.fetch outline`、`doc.find` 定位 `p_2`、`maxBlocks=2` 截断、`block.fetch includeChildren=true contextBefore=1 contextAfter=1` 返回 `revisionRef` 与同父级 before/after;复杂块投影不伪装完全可编辑。 + +2026-05-18 真实页面 plan_update / replace / move smoke 补充: + +- 命令:`PLAYWRIGHT_CHROME_EXECUTABLE=/snap/bin/chromium MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:3000 node scripts/task-page-block-ai-tools-smoke.js` +- 证据:`tmp/page-block-ai-tools-smoke/mpag4966.json`,截图:`tmp/page-block-ai-tools-smoke/mpag4966-page.png`。 +- 覆盖:`block_replace/block_insert_after/block_move_after` dry-run、dry-run 不写入、blocked 场景、replace 相邻块不变与 id 保持、replace 后 Page Aggregate 回读持久化、复杂块 move 阻断、普通块 move dry-run 与正式移动、moving block id 保持、刷新后 block ids 稳定。 --- @@ -514,18 +588,18 @@ MNOTE_UI_BASE_URL=http://127.0.0.1:3000 MNOTE_AUTH_BASE_URL=http://127.0.0.1:300 本 checklist 不能迁入 `done/`,直到: -- [ ] Page Aggregate 的标题 / 正文 / 页面设置 / page tree / AI fetch 回读形成同一组可验证真相。 -- [ ] `7-10` 的 Page Block AI Tooling 剩余矩阵完成或明确拆出后续 process 文档。 -- [ ] `7-11` 的 AI 基础 runtime 口径完成:context、format、manifest annotations、review session、状态机边界都有代码或明确后续 checklist。 -- [ ] 7-14 v2 Phase A + B 全部完成(`mnote.doc.markdown_edit` + `mnote.doc.fetch` 增强 + `page_ai_workflow.rs` 收口);Phase C 设计已冻结。设计稿修正已由 7-14 v2 自行完成。 -- [ ] `tree.*` command 面对主要页面生命周期动作成为唯一 preferred runtime 主链。 -- [ ] tree realtime live cache 覆盖 Sidebar、Page Tree、File Tree、page subtree 的关键写后更新。 -- [ ] 至少一轮定向 bug hunt 完成,所有 P0/P1 blocker 已归档到 `bugs/*/done/` 或明确保留为后续 process。 +- [x] Page Aggregate 的标题 / 正文 / 页面设置 / page tree / AI fetch 回读形成同一组可验证真相。 +- [x] `7-10` 的 Page Block AI Tooling 剩余矩阵完成或明确拆出后续 process 文档。 +- [x] `7-12` 的 AI 基础 runtime 口径完成:context、format、manifest annotations、review session、状态机边界都有代码或明确后续 checklist。 +- [x] 7-14 v2 Phase A + B 当前主路径完成(`mnote.doc.markdown_edit` + `mnote.doc.fetch` 增强 + `page_ai_workflow.rs` 收口);Phase C 设计已冻结。设计稿修正已由 7-14 v2 与 `7-18` 到 `7-25` bug 修复共同完成。 +- [x] `tree.*` command 面对主要页面生命周期动作成为唯一 preferred runtime 主链。 +- [x] tree realtime live cache 覆盖 Sidebar、Page Tree、File Tree、page subtree 的关键写后更新。 +- [x] 至少一轮定向 bug hunt 完成,所有 P0/P1 blocker 已归档到 `bugs/*/done/` 或明确保留为后续 process。 --- ## 6. 给后续 /goal 的持续执行 Prompt ```text -/goal objective: 持续执行 /mnt/Data1T/mnote/design/10-review/process/08-kernel-architecture-next-priority-review-and-checklist.md,按 P0 -> P1 顺序推进 MNOTE 架构收口、AI 基础底座和定向 bug hunt。每轮开始先读取 /home/lix/.codex/memories/PROFILE.md 与 ACTIVE.md,再读取 AGENTS.md、ARCHITECTURE.md、design/01-05-current-priority-overview.md、本 checklist、design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md、design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md、design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-v1.md。必须保护用户已有未提交改动,不回滚、不覆盖、不删除无关文件。优先使用多个 subagent 并行做只读审查和浏览器验证,主线程只整合证据和做小范围实现。执行顺序固定为:1) Page Aggregate 单一真源;2) tree command cutover;3) tree realtime live cache;4) AI 基础工具与审阅底座,按 `7-14` v2 方向优先做 Phase A(`mnote.doc.markdown_edit` + `mnote.doc.fetch` 增强),Phase B 退役 `local_rule` planner + `page_ai_workflow.rs` 收口;Phase C(流式 apply + suggest/review)仅设计冻结不实施。同时补 context/selection/page_xml/tool annotations,不扩新 AI 功能;5) 定向 bug hunt。每完成一个小项都要更新本 checklist 和对应 design/bugs 文档,补真实验证命令或证据路径。不要优先扩新功能,不要大改架构,不要把 compat/debug/fallback 当主链,不要把 BlockNote/Tiptap AI runtime 作为 mnote runtime 依赖。验证至少包含 git diff --check、相关 cargo test / smoke;如涉及 3000 页面,使用 mnote-tester 或浏览器自动化并保留截图/JSON 证据。 +/goal objective: 持续核验 /mnt/Data1T/mnote/design/10-review/done/08-kernel-architecture-next-priority-review-and-checklist.md 的已归档结论,按 P0 -> P1 顺序推进 MNOTE 架构防回归、AI 基础底座和定向 bug hunt。每轮开始先读取 /home/lix/.codex/memories/PROFILE.md 与 ACTIVE.md,再读取 AGENTS.md、ARCHITECTURE.md、design/01-05-current-priority-overview.md、本 checklist、design/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md、design/07-ai/process/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md、design/07-ai/process/7-14-online-local-ai-markdown-editing-convergence-v1.md。必须保护用户已有未提交改动,不回滚、不覆盖、不删除无关文件。优先使用多个 subagent 并行做只读审查和浏览器验证,主线程只整合证据和做小范围实现。执行顺序固定为:1) Page Aggregate 单一真源防回归;2) tree command cutover 防回归;3) tree realtime live cache 防回归;4) AI 基础工具与审阅底座,当前简单正文编辑主路径已是 `mnote.doc.markdown_edit` + `mnote.doc.fetch` + `page_ai_workflow.rs` 统一 tool executor,Phase C(流式 apply + suggest/review)仅设计冻结不实施;继续补 context/selection/page_xml/tool annotations,不扩新 AI 功能;5) 定向 bug hunt。每完成一个小项都要更新对应 design/bugs 文档,补真实验证命令或证据路径。不要优先扩新功能,不要大改架构,不要把 compat/debug/fallback 当主链,不要把 BlockNote/Tiptap AI runtime 作为 mnote runtime 依赖。验证至少包含 scoped git diff --check、相关 cargo test / smoke;如涉及 3000 页面,使用 mnote-tester 或浏览器自动化并保留截图/JSON 证据。 ``` diff --git a/design/10-review/process/09-page-ai-fast-block-edit-runtime-review.md b/design/10-review/done/09-page-ai-fast-block-edit-runtime-review.md similarity index 88% rename from design/10-review/process/09-page-ai-fast-block-edit-runtime-review.md rename to design/10-review/done/09-page-ai-fast-block-edit-runtime-review.md index 072ebf54..1b08b0d1 100644 --- a/design/10-review/process/09-page-ai-fast-block-edit-runtime-review.md +++ b/design/10-review/done/09-page-ai-fast-block-edit-runtime-review.md @@ -1,6 +1,6 @@ # 09 页面 AI 快速块编辑 Runtime Review -> 状态:`process` +> 状态:`done` > > 日期:2026-05-16 > @@ -12,6 +12,19 @@ --- +## 0. 归档说明 + +本文件是 2026-05-16 的页面 AI fast block edit 历史审查快照。其记录的 `local_rule` / `doc_apply_block_ops` 快路径已被后续 `mnote.doc.markdown_edit` 主路径替代,不再作为当前 runtime 口径。 + +当前有效口径见: + +- [10-current-mnote-ai-runtime-review-v1](./10-current-mnote-ai-runtime-review-v1.md) +- [11-current-full-architecture-review-v1](./11-current-full-architecture-review-v1.md) +- [7-18 AI markdown_edit 阶段状态合同漂移](../../../bugs/07-ai/done/7-18-ai-markdown-edit-phase-state-contract-drift-v1.md) +- [7-20 page_ai_workflow 绕过 Hermes tool executor / audit / toggle](../../../bugs/07-ai/done/7-20-page-ai-workflow-bypasses-hermes-tool-executor-v1.md) + +归档后的结论:`page-ai/block-edit-workflow` 仍是简单编辑 fast-path 入口,但当前实现应通过模型生成 markdown search/replace / full_content 后调用 `mnote.doc.markdown_edit`,并复用统一 mnote tool executor;`mnote.doc.apply_block_ops` / `mnote.block.*` 只作为结构性块操作辅助。 + ## 1. 本轮结论 页面 AI 块写入的慢点不在 Rust 块工具本身,也不在 Convex 持久化本身。 diff --git a/design/10-review/process/10-current-mnote-ai-runtime-review-v1.md b/design/10-review/done/10-current-mnote-ai-runtime-review-v1.md similarity index 51% rename from design/10-review/process/10-current-mnote-ai-runtime-review-v1.md rename to design/10-review/done/10-current-mnote-ai-runtime-review-v1.md index 1304fa39..2998bc74 100644 --- a/design/10-review/process/10-current-mnote-ai-runtime-review-v1.md +++ b/design/10-review/done/10-current-mnote-ai-runtime-review-v1.md @@ -1,22 +1,24 @@ -# 10 [process] 当前 mnote 项目 AI / Page Aggregate 定向 Review v1 +# 10 [done] 当前 mnote 项目 AI / Page Aggregate 定向 Review v1 > 更新时间:2026-05-17 > -> 执行状态:`process` +> 执行状态:`done` > > 范围:当前主线中 Page Aggregate 单一真源、页面 AI 快速编辑、`mnote.doc.markdown_edit` 与 ACP / Hermes runtime 的源码级定向审查。 ## 1. 本轮结论 -本轮没有发现 Page Aggregate 读取主链重新回退到 Next compat builder 的证据;`wolai-frontend/src/lib/documents/page-aggregate-loader.ts` 仍只走 Rust `/api/page-aggregate/:documentId`,`/api/documents/page` compat 读链保持 `410` 退场口径。 +本轮没有发现 Page Aggregate 读取主链重新回退到 Next compat builder 的证据。后续修复已进一步收口:AI context 的 page subtree 只读 Rust Page Aggregate projection,不再由前端本地构造第二份 page tree 真相。 -新的风险集中在 `07-ai` 的 markdown 编辑收敛实现: +本轮识别出的 `07-ai` markdown 编辑 P0 风险已修复: -- `mnote.doc.markdown_edit` 已进入实际写入主线,并被 `/api/page-ai/block-edit-workflow` 调用。 -- 该工具当前先在 markdown 字符串上顺序执行 search/replace,再把原始 operations 重新转换成 `mnote.doc.apply_block_ops`。 -- 转换层仍按“单块文本包含 search”查找目标块,无法忠实表达 markdown 层已经算出的最终结果。 +- `mnote.doc.markdown_edit` 是简单正文编辑主路径,并被 `/api/page-ai/block-edit-workflow` 通过统一 mnote tool executor 调用。 +- 中文归一化匹配不再把 UTF-8 byte index 当 char index。 +- 同一块内多次 markdown_edit 会合并到最终 markdown 结果后再映射写回,避免后续 block op 覆盖前序修改。 +- 在线写回在无法安全映射最终 markdown 时明确拒绝,避免 `ok=true` 伪成功。 +- Hermes guidance、manifest、tool toggle、dryRun / idempotency、revision / conflictDetectionKey 与 Reasonix ACP payload 已同步到当前工具合同。 -因此,当前 AI 编辑主线不能只看 `operationsApplied > 0` 或 route 返回 `ok=true`。需要优先补充 `markdown_edit` 的中文、多操作、同块、多块、full_content 与失败原子性测试。 +剩余长期方向仍是 Phase C review session / streaming apply;当前设计冻结,不属于本轮 bug 修复范围。 ## 2. 发现的问题 @@ -24,7 +26,7 @@ 关联缺陷: -- `bugs/07-ai/process/7-16-markdown-edit-normalized-search-byte-index-v1.md` +- `bugs/07-ai/done/7-16-markdown-edit-normalized-search-byte-index-v1.md` 证据: @@ -41,7 +43,7 @@ 关联缺陷: -- `bugs/07-ai/process/7-17-markdown-edit-same-block-multi-op-overwrite-v1.md` +- `bugs/07-ai/done/7-17-markdown-edit-same-block-multi-op-overwrite-v1.md` 证据: @@ -58,18 +60,20 @@ - AI 一次请求中常见的“把同一段里的 A 改成 B,同时把 C 改成 D”可能只保留最后一次修改。 - 前端快路径仍可能显示“已通过页面 markdown 编辑快路径完成写入”,但正文只部分生效。 -## 3. 次级风险 +## 3. 修复状态 -- `doc_markdown_edit` 当前允许部分 operation 失败后继续写入已成功的子集,并返回 `ok=true`。如果这是有意设计,需要在 manifest / UI 中明确“非原子”;如果不是,应改为任一 operation 失败时不写入。 -- `changedText` 通过 `operations.iter().take(applied)` 生成摘要;当前如果第一个 operation 失败、第二个成功,摘要会错误地展示第一个失败 operation。 -- `/api/page-ai/block-edit-workflow` 的系统 prompt 已改成 search/replace,但仍强依赖模型精确复制 `pageText`;一旦模型输出跨块片段,Convex 写回层不能表达该编辑。 +- `mnote.doc.markdown_edit` manifest 已补齐 required / write contract。 +- 本地 `.md` 写入已尊重 dryRun / idempotency。 +- 在线写回已使用最终 markdown 作为真源进行映射;无法安全映射时返回明确错误。 +- `mnote.doc.apply_block_ops` 批量写入已补 revision / conflictDetectionKey 前置校验。 +- `/api/page-ai/block-edit-workflow` 已从绕过 executor 改为复用统一 Hermes mnote tool executor。 +- ACP Reasonix wrapper 已透传 actor/session/run/toolCall/trace/dryRun/idempotency/workspace/document 等上下文字段。 -## 4. 建议下一步 +## 4. 后续边界 -1. 先给 `search_replace` 补中文归一化替换单测,再修正 byte / char offset 映射。 -2. 给 `doc_markdown_edit` 补同一 block 多 operation 的失败用例,明确应以最终 markdown 生成落库内容,或在转换层合并同块操作。 -3. 明确 `markdown_edit` 的失败原子性:默认建议任一 operation 失败时不写入,除非请求显式允许 partial apply。 -4. 将 `/api/page-ai/block-edit-workflow` 的验收从“route 成功”提升为“Page Aggregate 回读与预期最终 markdown 一致”。 +1. Phase C 的 review session / streaming apply 仍冻结,当前不扩新 AI 功能。 +2. 复杂结构编辑继续走 `apply_block_ops` / `mnote.block.*`,不要把正文 search/replace 回退到结构性块工具。 +3. Page Aggregate 仍需后续推进 EditorBlockDocument 原生落库真相。 ## 5. 本轮验证 @@ -80,9 +84,15 @@ rg -n "markdown_edit|apply_block_ops|block-edit-workflow|local_rule|page_ai_work rg -n "page-aggregate|pageAggregate|PageAggregate|blockDocument|projectionSource|documents/save|documents/options|/api/page-aggregate" rust/crates/mnote-web rust/crates/bridge-runtime wolai-frontend/src scripts --glob '!rust/target/**' --glob '!node_modules/**' ``` -本轮是 review / bug hunt,没有修改 runtime 源码。后续修复应至少补: +修复后定向验证: ```bash -cargo test --manifest-path rust/Cargo.toml -p mnote-web markdown_edit -cargo test --manifest-path rust/Cargo.toml -p mnote-web page_ai_workflow +cargo test --manifest-path rust/Cargo.toml -p mnote-web markdown_edit -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web page_ai_workflow -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web acp_client -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web acp_session_manager -- --nocapture +node --check scripts/reasonix-acp-wrapper.mjs +MNOTE_REASONIX_ACP_SELFTEST=1 node scripts/reasonix-acp-wrapper.mjs ``` + +结果:上述验证均通过。 diff --git a/design/10-review/done/11-current-full-architecture-review-v1.md b/design/10-review/done/11-current-full-architecture-review-v1.md new file mode 100644 index 00000000..91249643 --- /dev/null +++ b/design/10-review/done/11-current-full-architecture-review-v1.md @@ -0,0 +1,53 @@ +# 11 [done] 当前完整架构 Review v1 + +> 更新时间:2026-05-17 +> +> 状态:`done` +> +> 参考总文档:[CURRENT_ARCHITECTURE.md](../../../CURRENT_ARCHITECTURE.md) + +## 1. 结论 + +当前 mnote 的主线架构已经成形。本轮 review 识别出的 P0 / 内核优先缺陷已完成代码修复、文档迁移和定向验证。 + +已修复的四个关键冲突: + +1. Tree realtime 的发布口径与前端消费口径不一致。 +2. FileTree 资源行误走页面命令。 +3. Page Aggregate / AI context 保留前端第二真相。 +4. AI 写入链路在 `markdown_edit`、`apply_block_ops`、Hermes / ACP / Reasonix 之间的合同漂移。 + +仍需后续主线继续推进的不是本轮 P0 bug,而是长期架构收口:Page Aggregate 原生 EditorBlockDocument 落库、正文保存主命令面、`tree.resource.*` 资源生命周期和兼容路由瘦身。 + +## 2. 关键判断 + +- Rust kernel 与 bridge-runtime 已经承担语义主导权。 +- Rust SSR 主壳已默认以 `/api/realtime/ws` 为实时主链,SSE 是 fallback。 +- FileTree 资源行的 owner document 与页面命令目标已分离。 +- AI context 的 page subtree 已只读 Rust Page Aggregate projection。 +- `mnote.doc.markdown_edit` 已是简单正文编辑主路径,`page_ai_workflow` 复用统一 mnote tool executor,`apply_block_ops` / `mnote.block.*` 仅作为结构性辅助。 +- ACP / Reasonix 的 mnote tool payload、response、run context 与幂等字段已同步到当前工具合同。 + +## 3. 已闭合 bug + +- Rust Web / realtime / ACP:`bugs/03-rust-web/done/3-16` 到 `3-21` +- Tree domain:`bugs/04-tree-domain/done/4-46` +- Editor mainline:`bugs/05-editor-mainline/done/5-15` 到 `5-18` +- AI:`bugs/07-ai/done/7-18` 到 `7-25` + +## 4. 验证 + +- `find bugs -path '*/process/*' -type f | wc -l` + - 结果:`0` +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web markdown_edit -- --nocapture` + - 结果:9 passed +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web page_ai_workflow -- --nocapture` + - 结果:3 passed +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web acp_client -- --nocapture` + - 结果:6 passed +- `cargo test --manifest-path rust/Cargo.toml -p mnote-web ssr::pages::layout::tests -- --nocapture` + - 结果:14 passed + +## 5. 备注 + +本次审查的详细架构说明已统一写入根目录:[CURRENT_ARCHITECTURE.md](../../../CURRENT_ARCHITECTURE.md)。 diff --git a/design/10-review/process/11-current-full-architecture-review-v1.md b/design/10-review/process/11-current-full-architecture-review-v1.md deleted file mode 100644 index c0d81c11..00000000 --- a/design/10-review/process/11-current-full-architecture-review-v1.md +++ /dev/null @@ -1,34 +0,0 @@ -# 11 [process] 当前完整架构 Review v1 - -> 更新时间:2026-05-17 -> -> 状态:`process` -> -> 参考总文档:[CURRENT_ARCHITECTURE.md](../../../CURRENT_ARCHITECTURE.md) - -## 1. 结论 - -当前 mnote 的主线架构已经成形,但还没有完成单一真源收口。最关键的未闭合点有四个: - -1. Tree realtime 的发布口径与前端消费口径不一致。 -2. FileTree 资源行仍可能误走页面命令。 -3. Page Aggregate 仍保留前端第二真相。 -4. AI 写入链路在 `markdown_edit`、`apply_block_ops`、Hermes / ACP / Reasonix 之间存在合同漂移。 - -## 2. 关键判断 - -- Rust kernel 与 bridge-runtime 已经承担语义主导权。 -- 前端仍保留若干本地派生状态,适合过渡,不适合作为长期真相层。 -- `mnote.doc.markdown_edit` 已是主推路径,但工具级 contract 还没收口。 -- `page_ai_workflow` 与 ACP / Reasonix 已接入主链,但运行时 owner 与写入字段不统一。 - -## 3. 需要继续跟踪的 bug - -- Rust Web / realtime / ACP:`bugs/03-rust-web/process/3-16` 到 `3-21` -- Tree domain:`bugs/04-tree-domain/process/4-46` -- Editor mainline:`bugs/05-editor-mainline/process/5-15` 到 `5-18` -- AI:`bugs/07-ai/process/7-18` 到 `7-25` - -## 4. 备注 - -本次审查的详细架构说明已统一写入根目录:[CURRENT_ARCHITECTURE.md](../../../CURRENT_ARCHITECTURE.md)。 diff --git a/package-lock.json b/package-lock.json index fb5659c3..c0dd6b67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,74 +1,4652 @@ -{ - "name": "MNOTE", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@playwright/test": "^1.56.1" - }, - "devDependencies": { - "playwright": "^1.56.1" - } - }, - "node_modules/@playwright/test": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", - "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.56.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/playwright": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", - "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.56.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", - "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - } - } -} +{ + "name": "mnote-desktop", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mnote-desktop", + "version": "0.1.0", + "devDependencies": { + "@playwright/test": "^1.56.1", + "convex": "^1.39.1", + "electron": "^34.0.0", + "electron-builder": "^26.0.12", + "playwright": "^1.56.1" + } + }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/asar/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/fuses/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/fuses/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/get": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", + "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/notarize/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/osx-sign": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/@electron/osx-sign/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/osx-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/rebuild": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.4.tgz", + "integrity": "sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.1.1", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^12.2.0", + "read-binary-file-arch": "^1.0.6" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.3.1", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/universal/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/universal/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/windows-sign": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", + "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "cross-dirname": "^0.1.0", + "debug": "^4.3.4", + "fs-extra": "^11.1.1", + "minimist": "^1.2.8", + "postject": "^1.0.0-alpha.6" + }, + "bin": { + "electron-windows-sign": "bin/electron-windows-sign.js" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/fs-extra": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/windows-sign/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@electron/windows-sign/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", + "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", + "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", + "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", + "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", + "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", + "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", + "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", + "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", + "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", + "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", + "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", + "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", + "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", + "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", + "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", + "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", + "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", + "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", + "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", + "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", + "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", + "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", + "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", + "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", + "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", + "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@malept/cross-spawn-promise": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/fs-extra": { + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/app-builder-bin": { + "version": "5.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", + "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.8.1.tgz", + "integrity": "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/asar": "3.4.1", + "@electron/fuses": "^1.8.0", + "@electron/get": "^3.0.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.3", + "@electron/rebuild": "^4.0.3", + "@electron/universal": "2.0.3", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chromium-pickle-js": "^0.2.0", + "ci-info": "4.3.1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.8.1", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "isbinaryfile": "^5.0.0", + "jiti": "^2.4.2", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.0.3", + "plist": "3.1.0", + "proper-lockfile": "^4.1.2", + "resedit": "^1.7.0", + "semver": "~7.7.3", + "tar": "^7.5.7", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0", + "which": "^5.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.8.1", + "electron-builder-squirrel-windows": "26.8.1" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", + "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^2.2.0", + "fs-extra": "^8.1.0", + "got": "^11.8.5", + "progress": "^2.0.3", + "semver": "^6.2.0", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/app-builder-lib/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.8.1.tgz", + "integrity": "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "5.0.0-alpha.12", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", + "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/builder-util/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convex": { + "version": "1.39.1", + "resolved": "https://registry.npmjs.org/convex/-/convex-1.39.1.tgz", + "integrity": "sha512-W+gVXA7BpRF1xLlS1kGTtKVaqd5yonqbGESKiPtIUXjV744GdDz8IG7RVsSY5KzHbgxuJBHKaJYk+92OIHTskQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esbuild": "0.27.0", + "prettier": "^3.0.0", + "ws": "8.18.0" + }, + "bin": { + "convex": "bin/main.js" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=7.0.0" + }, + "peerDependencies": { + "@auth0/auth0-react": "^2.0.1", + "@clerk/clerk-react": "^4.12.8 || ^5.0.0", + "@clerk/react": "^6.4.3", + "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@auth0/auth0-react": { + "optional": true + }, + "@clerk/clerk-react": { + "optional": true + }, + "@clerk/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, + "node_modules/cross-dirname": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", + "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/dir-compare": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/dmg-builder": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.8.1.tgz", + "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/dmg-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron": { + "version": "34.5.8", + "resolved": "https://registry.npmjs.org/electron/-/electron-34.5.8.tgz", + "integrity": "sha512-vxLD65mabTzYmEVa9KceMHM0+zO+vqgrhcyNVlmTd0IGV5J7XZ8v/qElm0o4YQ4wPeq7olZkUjZkBQQEdr23/g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^2.0.0", + "@types/node": "^20.9.0", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 12.20.55" + } + }, + "node_modules/electron-builder": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.8.1.tgz", + "integrity": "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "dmg-builder": "26.8.1", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "simple-update-notifier": "2.0.0", + "yargs": "^17.6.2" + }, + "bin": { + "electron-builder": "cli.js", + "install-app-deps": "install-app-deps.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/electron-builder-squirrel-windows": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.8.1.tgz", + "integrity": "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "app-builder-lib": "26.8.1", + "builder-util": "26.8.1", + "electron-winstaller": "5.4.0" + } + }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-builder/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-publish": { + "version": "26.8.1", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.8.1.tgz", + "integrity": "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "26.8.1", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "form-data": "^4.0.5", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-publish/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/electron-winstaller": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", + "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@electron/asar": "^3.2.1", + "debug": "^4.1.1", + "fs-extra": "^7.0.1", + "lodash": "^4.17.21", + "temp": "^0.9.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "@electron/windows-sign": "^1.1.2" + } + }, + "node_modules/electron-winstaller/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/esbuild": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", + "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.0", + "@esbuild/android-arm": "0.27.0", + "@esbuild/android-arm64": "0.27.0", + "@esbuild/android-x64": "0.27.0", + "@esbuild/darwin-arm64": "0.27.0", + "@esbuild/darwin-x64": "0.27.0", + "@esbuild/freebsd-arm64": "0.27.0", + "@esbuild/freebsd-x64": "0.27.0", + "@esbuild/linux-arm": "0.27.0", + "@esbuild/linux-arm64": "0.27.0", + "@esbuild/linux-ia32": "0.27.0", + "@esbuild/linux-loong64": "0.27.0", + "@esbuild/linux-mips64el": "0.27.0", + "@esbuild/linux-ppc64": "0.27.0", + "@esbuild/linux-riscv64": "0.27.0", + "@esbuild/linux-s390x": "0.27.0", + "@esbuild/linux-x64": "0.27.0", + "@esbuild/netbsd-arm64": "0.27.0", + "@esbuild/netbsd-x64": "0.27.0", + "@esbuild/openbsd-arm64": "0.27.0", + "@esbuild/openbsd-x64": "0.27.0", + "@esbuild/openharmony-arm64": "0.27.0", + "@esbuild/sunos-x64": "0.27.0", + "@esbuild/win32-arm64": "0.27.0", + "@esbuild/win32-ia32": "0.27.0", + "@esbuild/win32-x64": "0.27.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "4.31.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.31.0.tgz", + "integrity": "sha512-Erq5w/t3syw3s4sDsUaX4QttIdBPsGKTT1DTRsCkTonGggczhlDKm/wDX3o+HPJpQ41EjXCbcmXf0tgr5YZJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-api-version/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.3.0.tgz", + "integrity": "sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/postject": { + "version": "1.0.0-alpha.6", + "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", + "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "commander": "^9.4.0" + }, + "bin": { + "postject": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/postject/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", + "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "7.5.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", + "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/temp-file/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", + "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index c91d5cbb..08ee0639 100644 --- a/package.json +++ b/package.json @@ -1,66 +1,67 @@ -{ - "name": "mnote-desktop", - "version": "0.1.0", - "private": true, - "main": "desktop-electron/main.js", - "scripts": { - "desktop": "node scripts/desktop-prod.js", - "desktop:hot": "node scripts/desktop-hot.js", - "dev:hot": "node scripts/dev-hot.js", - "desktop:local": "node scripts/desktop-local.js", - "desktop:electron": "node desktop-electron/dev-runner.js", - "build:desktop:next": "node scripts/prepare-desktop-next.js", - "dist:win": "pnpm run build:desktop:next && electron-builder --win nsis" - }, - "dependencies": {}, - "devDependencies": { - "@playwright/test": "^1.56.1", - "electron": "^34.0.0", - "electron-builder": "^26.0.12", - "playwright": "^1.56.1" - }, - "build": { - "appId": "org.aichem.mnote", - "productName": "MNOTE", - "asar": true, - "directories": { - "output": "dist", - "buildResources": "desktop-electron/build" - }, - "files": [ - "package.json", - "desktop-electron/**", - "!desktop-electron/desktop-next{,/**}" - ], - "extraResources": [ - { - "from": "desktop-electron/desktop-next", - "to": "desktop-next", - "filter": [ - "**/*", - "!node_modules{,/**}" - ] - }, - { - "from": "desktop-electron/desktop-next/node_modules", - "to": "desktop-next/node_modules" - } - ], - "win": { - "target": [ - "nsis" - ] - }, - "nsis": { - "oneClick": false, - "allowToChangeInstallationDirectory": true, - "include": "desktop-electron/build/installer.nsh" - } - }, - "pnpm": { - "onlyBuiltDependencies": [ - "electron", - "electron-winstaller" - ] - } -} +{ + "name": "mnote-desktop", + "version": "0.1.0", + "private": true, + "main": "desktop-electron/main.js", + "scripts": { + "desktop": "node scripts/desktop-prod.js", + "desktop:hot": "node scripts/desktop-hot.js", + "dev:hot": "node scripts/dev-hot.js", + "desktop:local": "node scripts/desktop-local.js", + "desktop:electron": "node desktop-electron/dev-runner.js", + "build:desktop:next": "node scripts/prepare-desktop-next.js", + "dist:win": "pnpm run build:desktop:next && electron-builder --win nsis" + }, + "dependencies": {}, + "devDependencies": { + "@playwright/test": "^1.56.1", + "convex": "^1.39.1", + "electron": "^34.0.0", + "electron-builder": "^26.0.12", + "playwright": "^1.56.1" + }, + "build": { + "appId": "org.aichem.mnote", + "productName": "MNOTE", + "asar": true, + "directories": { + "output": "dist", + "buildResources": "desktop-electron/build" + }, + "files": [ + "package.json", + "desktop-electron/**", + "!desktop-electron/desktop-next{,/**}" + ], + "extraResources": [ + { + "from": "desktop-electron/desktop-next", + "to": "desktop-next", + "filter": [ + "**/*", + "!node_modules{,/**}" + ] + }, + { + "from": "desktop-electron/desktop-next/node_modules", + "to": "desktop-next/node_modules" + } + ], + "win": { + "target": [ + "nsis" + ] + }, + "nsis": { + "oneClick": false, + "allowToChangeInstallationDirectory": true, + "include": "desktop-electron/build/installer.nsh" + } + }, + "pnpm": { + "onlyBuiltDependencies": [ + "electron", + "electron-winstaller" + ] + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e8275c11..dbca8216 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@playwright/test': specifier: ^1.56.1 version: 1.57.0 + convex: + specifier: ^1.39.1 + version: 1.39.1 electron: specifier: ^34.0.0 version: 34.5.8 @@ -66,6 +69,162 @@ packages: engines: {node: '>=14.14'} hasBin: true + '@esbuild/aix-ppc64@0.27.0': + resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.0': + resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.0': + resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.0': + resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.0': + resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.0': + resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.0': + resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.0': + resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.0': + resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.0': + resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.0': + resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.0': + resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.0': + resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.0': + resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.0': + resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.0': + resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.0': + resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.0': + resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.0': + resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.0': + resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.0': + resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.0': + resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.0': + resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.0': + resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.0': + resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.0': + resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@isaacs/balanced-match@4.0.1': resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} engines: {node: 20 || >=22} @@ -341,6 +500,25 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + convex@1.39.1: + resolution: {integrity: sha512-W+gVXA7BpRF1xLlS1kGTtKVaqd5yonqbGESKiPtIUXjV744GdDz8IG7RVsSY5KzHbgxuJBHKaJYk+92OIHTskQ==} + engines: {node: '>=18.0.0', npm: '>=7.0.0'} + hasBin: true + peerDependencies: + '@auth0/auth0-react': ^2.0.1 + '@clerk/clerk-react': ^4.12.8 || ^5.0.0 + '@clerk/react': ^6.4.3 + react: ^18.0.0 || ^19.0.0-0 || ^19.0.0 + peerDependenciesMeta: + '@auth0/auth0-react': + optional: true + '@clerk/clerk-react': + optional: true + '@clerk/react': + optional: true + react: + optional: true + core-util-is@1.0.2: resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} @@ -483,6 +661,11 @@ packages: es6-error@4.1.1: resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + esbuild@0.27.0: + resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -976,6 +1159,11 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} + engines: {node: '>=14'} + hasBin: true + proc-log@5.0.0: resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==} engines: {node: ^18.17.0 || >=20.5.0} @@ -1246,6 +1434,18 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xmlbuilder@15.1.1: resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} engines: {node: '>=8.0'} @@ -1372,6 +1572,84 @@ snapshots: - supports-color optional: true + '@esbuild/aix-ppc64@0.27.0': + optional: true + + '@esbuild/android-arm64@0.27.0': + optional: true + + '@esbuild/android-arm@0.27.0': + optional: true + + '@esbuild/android-x64@0.27.0': + optional: true + + '@esbuild/darwin-arm64@0.27.0': + optional: true + + '@esbuild/darwin-x64@0.27.0': + optional: true + + '@esbuild/freebsd-arm64@0.27.0': + optional: true + + '@esbuild/freebsd-x64@0.27.0': + optional: true + + '@esbuild/linux-arm64@0.27.0': + optional: true + + '@esbuild/linux-arm@0.27.0': + optional: true + + '@esbuild/linux-ia32@0.27.0': + optional: true + + '@esbuild/linux-loong64@0.27.0': + optional: true + + '@esbuild/linux-mips64el@0.27.0': + optional: true + + '@esbuild/linux-ppc64@0.27.0': + optional: true + + '@esbuild/linux-riscv64@0.27.0': + optional: true + + '@esbuild/linux-s390x@0.27.0': + optional: true + + '@esbuild/linux-x64@0.27.0': + optional: true + + '@esbuild/netbsd-arm64@0.27.0': + optional: true + + '@esbuild/netbsd-x64@0.27.0': + optional: true + + '@esbuild/openbsd-arm64@0.27.0': + optional: true + + '@esbuild/openbsd-x64@0.27.0': + optional: true + + '@esbuild/openharmony-arm64@0.27.0': + optional: true + + '@esbuild/sunos-x64@0.27.0': + optional: true + + '@esbuild/win32-arm64@0.27.0': + optional: true + + '@esbuild/win32-ia32@0.27.0': + optional: true + + '@esbuild/win32-x64@0.27.0': + optional: true + '@isaacs/balanced-match@4.0.1': {} '@isaacs/brace-expansion@5.0.0': @@ -1713,6 +1991,15 @@ snapshots: concat-map@0.0.1: {} + convex@1.39.1: + dependencies: + esbuild: 0.27.0 + prettier: 3.8.3 + ws: 8.18.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + core-util-is@1.0.2: optional: true @@ -1906,6 +2193,35 @@ snapshots: es6-error@4.1.1: optional: true + esbuild@0.27.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.0 + '@esbuild/android-arm': 0.27.0 + '@esbuild/android-arm64': 0.27.0 + '@esbuild/android-x64': 0.27.0 + '@esbuild/darwin-arm64': 0.27.0 + '@esbuild/darwin-x64': 0.27.0 + '@esbuild/freebsd-arm64': 0.27.0 + '@esbuild/freebsd-x64': 0.27.0 + '@esbuild/linux-arm': 0.27.0 + '@esbuild/linux-arm64': 0.27.0 + '@esbuild/linux-ia32': 0.27.0 + '@esbuild/linux-loong64': 0.27.0 + '@esbuild/linux-mips64el': 0.27.0 + '@esbuild/linux-ppc64': 0.27.0 + '@esbuild/linux-riscv64': 0.27.0 + '@esbuild/linux-s390x': 0.27.0 + '@esbuild/linux-x64': 0.27.0 + '@esbuild/netbsd-arm64': 0.27.0 + '@esbuild/netbsd-x64': 0.27.0 + '@esbuild/openbsd-arm64': 0.27.0 + '@esbuild/openbsd-x64': 0.27.0 + '@esbuild/openharmony-arm64': 0.27.0 + '@esbuild/sunos-x64': 0.27.0 + '@esbuild/win32-arm64': 0.27.0 + '@esbuild/win32-ia32': 0.27.0 + '@esbuild/win32-x64': 0.27.0 + escalade@3.2.0: {} escape-string-regexp@4.0.0: @@ -2421,6 +2737,8 @@ snapshots: commander: 9.5.0 optional: true + prettier@3.8.3: {} + proc-log@5.0.0: {} progress@2.0.3: {} @@ -2700,6 +3018,8 @@ snapshots: wrappy@1.0.2: {} + ws@8.18.0: {} + xmlbuilder@15.1.1: {} y18n@5.0.8: {} diff --git a/rust/crates/bridge-runtime/src/lib.rs b/rust/crates/bridge-runtime/src/lib.rs index 4fb8ab16..444f5f45 100644 --- a/rust/crates/bridge-runtime/src/lib.rs +++ b/rust/crates/bridge-runtime/src/lib.rs @@ -10987,6 +10987,7 @@ fn normalize_editor_block_type_for_save(raw_type: &str) -> EditorBlockType { "toc" | "toc_node" | "tocnode" => EditorBlockType::Toc, "page_reference" => EditorBlockType::PageReference, "block_reference" => EditorBlockType::BlockReference, + "resource" | "resource_block" => EditorBlockType::Resource, _ => EditorBlockType::Paragraph, } } @@ -11105,6 +11106,13 @@ fn normalize_editor_block_from_legacy_path(block: &Value, path: &[usize]) -> Edi ); } } + if matches!(normalized_block_type, EditorBlockType::Resource) { + if let Some(props_map) = block.get("props").and_then(Value::as_object) { + props + .extra + .insert("resourceProps".into(), Value::Object(props_map.clone())); + } + } let text = read_trimmed_string_field(block, &["content"]) .filter(|value| !value.is_empty()) .unwrap_or_else(|| extract_inline_text(block)); @@ -11122,7 +11130,10 @@ fn normalize_editor_block_from_legacy_path(block: &Value, path: &[usize]) -> Edi } } -pub fn editor_document_from_legacy_content(document_id: &str, content: &Value) -> EditorBlockDocument { +pub fn editor_document_from_legacy_content( + document_id: &str, + content: &Value, +) -> EditorBlockDocument { let mut blocks = Vec::::new(); let mut root_block_ids = Vec::::new(); for (index, block) in normalize_blocks_from_value(content).iter().enumerate() { @@ -11336,6 +11347,18 @@ fn legacy_props_from_editor_block(block: &EditorBlock) -> Option { props.insert("projectionVersion".into(), json!(projection_version)); } } + EditorBlockType::Resource => { + if let Some(resource_props) = block + .props + .extra + .get("resourceProps") + .and_then(Value::as_object) + { + for (key, value) in resource_props { + props.insert(key.clone(), value.clone()); + } + } + } _ => {} } if let Some(text_align) = block @@ -11372,6 +11395,7 @@ fn legacy_type_from_editor_block(block: &EditorBlock) -> &'static str { EditorBlockType::Toc => "toc", EditorBlockType::PageReference => "page_reference", EditorBlockType::BlockReference => "block_reference", + EditorBlockType::Resource => "resource", } } @@ -11836,7 +11860,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -11931,7 +11955,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -12513,6 +12537,38 @@ mod tests { ); } + #[test] + fn editor_document_roundtrip_preserves_resource_block_type() { + let content = json!([ + { + "id": "resource_1", + "type": "resource", + "props": { + "resourceKind": "asset", + "assetId": "asset_1" + }, + "content": "资源块" + } + ]); + let document = editor_document_from_legacy_content("doc_1", &content); + assert_eq!(document.blocks[0].block_type, EditorBlockType::Resource); + + let legacy = legacy_content_from_editor_document(&document); + assert_eq!(legacy[0]["id"], json!("resource_1")); + assert_eq!(legacy[0]["type"], json!("resource")); + assert_eq!(legacy[0]["props"]["resourceKind"], json!("asset")); + assert_eq!(legacy[0]["props"]["assetId"], json!("asset_1")); + + let projection = project_legacy_content_to_block_document("doc_1", &legacy, &json!(1)) + .expect("resource block should project"); + assert_eq!(projection["blocks"][0]["type"], json!("resource")); + assert_eq!(projection["blocks"][0]["editable"], json!(false)); + assert_eq!( + projection["blocks"][0]["unsupportedReason"], + json!("复杂块暂不开放 AI 精确写入") + ); + } + #[test] fn editor_command_apply_insert_preserves_nested_children() { let content = json!([{ @@ -13186,7 +13242,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -13227,11 +13283,11 @@ mod tests { "children": [], }) ); - assert_eq!(plan.args_json["createOnly"], json!(true)); - assert_eq!( - plan.args_json["streamDeltaHint"], - json!({ - "family": "tree", + assert_eq!(plan.args_json["createOnly"], json!(true)); + assert_eq!( + plan.args_json["streamDeltaHint"], + json!({ + "family": "tree", "kind": "resync_required", "args": { "reason": "mindmap.put", @@ -13308,11 +13364,11 @@ mod tests { .expect("mindmap update command plan should build"); match plan { - RuntimeExecutionPlan::Command(plan) => { - assert_eq!( - plan.args_json["streamDeltaHint"], - json!({ - "family": "tree", + RuntimeExecutionPlan::Command(plan) => { + assert_eq!( + plan.args_json["streamDeltaHint"], + json!({ + "family": "tree", "kind": "noop", "args": {} }) @@ -13772,7 +13828,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -14469,7 +14525,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -14570,7 +14626,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -14676,7 +14732,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -14743,7 +14799,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -14885,6 +14941,108 @@ mod tests { ); } + #[test] + fn page_body_save_artifact_preserves_editor_document_payload() { + let context = demo_context(); + let command = RuntimeCommandEnvelopeWire { + name: "page.body.save".into(), + command_id: "cmd_save_artifact_editor_document".into(), + idempotency_key: Some("idem_save_artifact_editor_document".into()), + actor: RuntimeActorWire { + actor_type: "user".into(), + actor_id: "user_1".into(), + session_id: Some("sess_1".into()), + }, + source: RuntimeSourceWire { + channel: "next-route".into(), + client: "mnote-web".into(), + source_kind: None, + root_uri: None, + workspace_id: None, + capabilities: Vec::new(), + }, + target: Some(RuntimeTargetWire { + workspace_id: Some("ws_1".into()), + page_id: Some("doc_1".into()), + block_id: None, + }), + payload: json!({ + "documentId": "doc_1", + "workspaceId": "ws_1", + "revision": 9, + "editorDocument": { + "documentId": "doc_1", + "rootBlockIds": ["editor_block_1"], + "blocks": [{ + "blockId": "editor_block_1", + "blockType": "paragraph", + "contentNodes": [{ + "payload": { + "type": "text", + "text": "来自 editorDocument artifact" + }, + "attrs": {} + }], + "childBlockIds": [] + }] + }, + "content": [], + "tiptapDocument": { + "type": "doc", + "content": [{ + "type": "paragraph", + "attrs": { "blockId": "editor_block_1" }, + "content": [{ "type": "text", "text": "来自 tiptapDocument artifact" }] + }] + }, + "conflictDetectionKey": "doc_1:9" + }), + preflight_data: None, + reason: Some("保存 artifact provenance".into()), + refs: vec!["page-body-save-artifact-provenance".into()], + dry_run: false, + validate_only: false, + }; + let plan = execute_runtime_input(RuntimeInput::Command { + context: context.clone(), + command: command.clone(), + }) + .expect("page.body.save plan should build"); + let RuntimeExecutionPlan::Command(plan) = plan else { + panic!("expected command plan"); + }; + let artifacts = build_runtime_command_artifact_plan( + &context, + &command, + &plan, + &json!({"ok": true, "revision": 10}), + "2026-05-18T00:00:00Z", + ) + .expect("artifact plan should build"); + + assert_eq!( + artifacts + .command_log + .payload + .pointer("/editorDocument/rootBlockIds/0"), + Some(&json!("editor_block_1")) + ); + assert_eq!( + artifacts + .command_log + .payload + .pointer("/editorDocument/blocks/0/contentNodes/0/payload/text"), + Some(&json!("来自 editorDocument artifact")) + ); + assert_eq!( + artifacts + .command_log + .payload + .pointer("/tiptapDocument/content/0/type"), + Some(&json!("paragraph")) + ); + } + #[test] fn blocks_move_command_plan_maps_to_blocks_move() { let plan = execute_runtime_input(RuntimeInput::Command { @@ -14900,7 +15058,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -14995,7 +15153,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -15161,7 +15319,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -15250,7 +15408,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -15333,18 +15491,18 @@ mod tests { "documentId": "doc_1", "workspaceId": "ws_1", }), - json!({ - "id": "doc_1", - "commandProtocol": { - "family": "tree", - "owner": "rust-runtime-kernel", - "preferredCommandName": "tree.node.archive", - "compatCommandName": "documents.delete", - "deprecatedAlias": false - }, - "streamDeltaHint": { - "family": "tree", - "kind": "remove_document", + json!({ + "id": "doc_1", + "commandProtocol": { + "family": "tree", + "owner": "rust-runtime-kernel", + "preferredCommandName": "tree.node.archive", + "compatCommandName": "documents.delete", + "deprecatedAlias": false + }, + "streamDeltaHint": { + "family": "tree", + "kind": "remove_document", "args": { "documentId": "doc_1" } @@ -15375,18 +15533,18 @@ mod tests { "documentId": "doc_1", "workspaceId": "ws_1", }), - json!({ - "id": "doc_1", - "commandProtocol": { - "family": "tree", - "owner": "rust-runtime-kernel", - "preferredCommandName": "tree.node.restore", - "compatCommandName": "documents.restore", - "deprecatedAlias": false - }, - "streamDeltaHint": { - "family": "tree", - "kind": "document_result", + json!({ + "id": "doc_1", + "commandProtocol": { + "family": "tree", + "owner": "rust-runtime-kernel", + "preferredCommandName": "tree.node.restore", + "compatCommandName": "documents.restore", + "deprecatedAlias": false + }, + "streamDeltaHint": { + "family": "tree", + "kind": "document_result", "args": { "documentField": "document" } @@ -15416,17 +15574,17 @@ mod tests { json!({ "documentId": "doc_1", }), - json!({ - "id": "doc_1", - "commandProtocol": { - "family": "tree", - "owner": "rust-runtime-kernel", - "preferredCommandName": "tree.node.purge", - "compatCommandName": "documents.purge", - "deprecatedAlias": false - } - }), - ), + json!({ + "id": "doc_1", + "commandProtocol": { + "family": "tree", + "owner": "rust-runtime-kernel", + "preferredCommandName": "tree.node.purge", + "compatCommandName": "documents.purge", + "deprecatedAlias": false + } + }), + ), ]; for (command_name, function_name, payload, args_json) in cases { @@ -15443,7 +15601,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -15491,7 +15649,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -15560,7 +15718,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -15629,7 +15787,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -15698,7 +15856,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -15755,7 +15913,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -15919,7 +16077,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -15955,20 +16113,20 @@ mod tests { panic!("expected command plan"); }; - assert_eq!(plan.command_name, "tree.subtree.move"); - assert_eq!(plan.function_name, "documents:move"); - assert_eq!(plan.args_json["sortOrder"], json!(-2)); - assert_eq!( - plan.args_json["commandProtocol"], - json!({ - "family": "tree", - "owner": "rust-runtime-kernel", - "preferredCommandName": "tree.subtree.move", - "compatCommandName": "documents.move", - "deprecatedAlias": false - }) - ); - assert!(plan.args_json.get("normalizedMove").is_none()); + assert_eq!(plan.command_name, "tree.subtree.move"); + assert_eq!(plan.function_name, "documents:move"); + assert_eq!(plan.args_json["sortOrder"], json!(-2)); + assert_eq!( + plan.args_json["commandProtocol"], + json!({ + "family": "tree", + "owner": "rust-runtime-kernel", + "preferredCommandName": "tree.subtree.move", + "compatCommandName": "documents.move", + "deprecatedAlias": false + }) + ); + assert!(plan.args_json.get("normalizedMove").is_none()); assert_eq!( plan.args_json["treeWriteOperation"], json!({ @@ -16022,111 +16180,123 @@ mod tests { } } }) - ); - } + ); + } - #[test] - fn documents_lifecycle_aliases_are_marked_as_deprecated_tree_protocol_aliases() { - let cases = [ - ("documents.create", "tree.node.create", "documents.create"), - ("documents.title.update", "tree.node.rename", "documents.title.update"), - ("documents.move", "tree.subtree.move", "documents.move"), - ("documents.delete", "tree.node.archive", "documents.delete"), - ("documents.restore", "tree.node.restore", "documents.restore"), - ("documents.purge", "tree.node.purge", "documents.purge"), - ("documents.copy_tree", "tree.subtree.copy", "documents.copy_tree"), - ]; + #[test] + fn documents_lifecycle_aliases_are_marked_as_deprecated_tree_protocol_aliases() { + let cases = [ + ("documents.create", "tree.node.create", "documents.create"), + ( + "documents.title.update", + "tree.node.rename", + "documents.title.update", + ), + ("documents.move", "tree.subtree.move", "documents.move"), + ("documents.delete", "tree.node.archive", "documents.delete"), + ( + "documents.restore", + "tree.node.restore", + "documents.restore", + ), + ("documents.purge", "tree.node.purge", "documents.purge"), + ( + "documents.copy_tree", + "tree.subtree.copy", + "documents.copy_tree", + ), + ]; - for (command_name, preferred_command_name, compat_command_name) in cases { - let command = RuntimeCommandEnvelopeWire { - name: command_name.into(), - command_id: format!("cmd_{command_name}"), - idempotency_key: Some(format!("idem_{command_name}")), - actor: RuntimeActorWire { - actor_type: "user".into(), - actor_id: "user_1".into(), - session_id: Some("sess_1".into()), - }, - source: RuntimeSourceWire { - channel: "next-route".into(), - client: "wolai-frontend".into(), - source_kind: None, - root_uri: None, - workspace_id: None, - capabilities: Vec::new(), - }, - target: Some(RuntimeTargetWire { - workspace_id: Some("ws_1".into()), - page_id: Some("doc_1".into()), - block_id: None, - }), - payload: match command_name { - "documents.create" => json!({ - "documentId": "doc_1", - "workspaceId": "ws_1", - "parentId": null, - "title": "新页面", - "accessScope": "private", - "content": [] - }), - "documents.title.update" => json!({ - "documentId": "doc_1", - "title": "改名" - }), - "documents.move" => json!({ - "documentId": "doc_1", - "parentId": null, - "sortOrder": 0 - }), - "documents.delete" | "documents.restore" | "documents.purge" => json!({ - "documentId": "doc_1", - "workspaceId": "ws_1" - }), - "documents.copy_tree" => json!({ - "items": [{"documentId": "doc_1", "recursive": true}], - "targetParentId": null - }), - _ => unreachable!("unexpected command"), - }, - preflight_data: if command_name == "documents.move" { - Some(json!({ - "documents": [ - { "id": "doc_1", "workspace_id": "ws_1", "parent_id": null, "sort_order": 0, "created_at": "2026-04-25T00:00:01Z" } - ] - })) - } else { - None - }, - reason: Some("兼容 alias 标识验证".into()), - refs: vec![], - dry_run: false, - validate_only: false, - }; - let plan = execute_runtime_input(RuntimeInput::Command { - context: demo_context(), - command, - }) - .expect("compat command plan should build"); - let RuntimeExecutionPlan::Command(plan) = plan else { - panic!("expected command plan"); - }; + for (command_name, preferred_command_name, compat_command_name) in cases { + let command = RuntimeCommandEnvelopeWire { + name: command_name.into(), + command_id: format!("cmd_{command_name}"), + idempotency_key: Some(format!("idem_{command_name}")), + actor: RuntimeActorWire { + actor_type: "user".into(), + actor_id: "user_1".into(), + session_id: Some("sess_1".into()), + }, + source: RuntimeSourceWire { + channel: "next-route".into(), + client: "mnote-web".into(), + source_kind: None, + root_uri: None, + workspace_id: None, + capabilities: Vec::new(), + }, + target: Some(RuntimeTargetWire { + workspace_id: Some("ws_1".into()), + page_id: Some("doc_1".into()), + block_id: None, + }), + payload: match command_name { + "documents.create" => json!({ + "documentId": "doc_1", + "workspaceId": "ws_1", + "parentId": null, + "title": "新页面", + "accessScope": "private", + "content": [] + }), + "documents.title.update" => json!({ + "documentId": "doc_1", + "title": "改名" + }), + "documents.move" => json!({ + "documentId": "doc_1", + "parentId": null, + "sortOrder": 0 + }), + "documents.delete" | "documents.restore" | "documents.purge" => json!({ + "documentId": "doc_1", + "workspaceId": "ws_1" + }), + "documents.copy_tree" => json!({ + "items": [{"documentId": "doc_1", "recursive": true}], + "targetParentId": null + }), + _ => unreachable!("unexpected command"), + }, + preflight_data: if command_name == "documents.move" { + Some(json!({ + "documents": [ + { "id": "doc_1", "workspace_id": "ws_1", "parent_id": null, "sort_order": 0, "created_at": "2026-04-25T00:00:01Z" } + ] + })) + } else { + None + }, + reason: Some("兼容 alias 标识验证".into()), + refs: vec![], + dry_run: false, + validate_only: false, + }; + let plan = execute_runtime_input(RuntimeInput::Command { + context: demo_context(), + command, + }) + .expect("compat command plan should build"); + let RuntimeExecutionPlan::Command(plan) = plan else { + panic!("expected command plan"); + }; - assert_eq!( - plan.args_json["commandProtocol"], - json!({ - "family": "tree", - "owner": "rust-runtime-kernel", - "preferredCommandName": preferred_command_name, - "compatCommandName": compat_command_name, - "deprecatedAlias": true - }), - "{command_name} 应标记为 tree compat deprecated alias" - ); - } - } + assert_eq!( + plan.args_json["commandProtocol"], + json!({ + "family": "tree", + "owner": "rust-runtime-kernel", + "preferredCommandName": preferred_command_name, + "compatCommandName": compat_command_name, + "deprecatedAlias": true + }), + "{command_name} 应标记为 tree compat deprecated alias" + ); + } + } - #[test] - fn tree_subtree_move_command_includes_tree_write_operation_from_snapshot() { + #[test] + fn tree_subtree_move_command_includes_tree_write_operation_from_snapshot() { let plan = execute_runtime_input(RuntimeInput::Command { context: demo_context(), command: RuntimeCommandEnvelopeWire { @@ -16140,7 +16310,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -16215,7 +16385,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -16315,7 +16485,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -16401,7 +16571,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -16484,7 +16654,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -16572,7 +16742,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -16713,7 +16883,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -16755,15 +16925,15 @@ mod tests { "recursive": true, } ], - "targetParentId": "parent_1", - "commandProtocol": { - "family": "tree", - "owner": "rust-runtime-kernel", - "preferredCommandName": "tree.subtree.copy", - "compatCommandName": "documents.copy_tree", - "deprecatedAlias": false - }, - "streamDeltaHint": { + "targetParentId": "parent_1", + "commandProtocol": { + "family": "tree", + "owner": "rust-runtime-kernel", + "preferredCommandName": "tree.subtree.copy", + "compatCommandName": "documents.copy_tree", + "deprecatedAlias": false + }, + "streamDeltaHint": { "family": "tree", "kind": "copy_result", "args": { @@ -16812,7 +16982,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: Some("ws_1".into()), @@ -16896,7 +17066,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -16968,7 +17138,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -17058,7 +17228,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -17126,7 +17296,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "onlyoffice-callback".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -17204,7 +17374,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -17492,7 +17662,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -17684,7 +17854,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -17741,7 +17911,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -17781,7 +17951,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -17827,7 +17997,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -17952,7 +18122,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -18015,7 +18185,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -18079,7 +18249,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -18146,7 +18316,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -18244,7 +18414,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -18343,7 +18513,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -18456,7 +18626,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, @@ -18530,7 +18700,7 @@ mod tests { }, source: RuntimeSourceWire { channel: "next-route".into(), - client: "wolai-frontend".into(), + client: "mnote-web".into(), source_kind: None, root_uri: None, workspace_id: None, diff --git a/rust/crates/core-protocol/src/editor/model.rs b/rust/crates/core-protocol/src/editor/model.rs index a64d82c4..92d05527 100644 --- a/rust/crates/core-protocol/src/editor/model.rs +++ b/rust/crates/core-protocol/src/editor/model.rs @@ -19,6 +19,7 @@ pub enum EditorBlockType { Toc, PageReference, BlockReference, + Resource, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] diff --git a/rust/crates/core-protocol/src/editor/tiptap.rs b/rust/crates/core-protocol/src/editor/tiptap.rs index 0051fddd..13351f52 100644 --- a/rust/crates/core-protocol/src/editor/tiptap.rs +++ b/rust/crates/core-protocol/src/editor/tiptap.rs @@ -588,6 +588,18 @@ fn block_to_tiptap_node( content: Vec::new(), }) } + EditorBlockType::Resource => Ok(TiptapNode::Paragraph { + attrs: TiptapParagraphAttrs { + block_id: Some(block.block_id.clone()), + text_align: text_align_from_props(&block.props), + mnote_block_type: Some("resource".into()), + mindmap_id: None, + root_node_id: None, + projection_version: None, + extra: block.props.extra.clone(), + }, + content, + }), EditorBlockType::Heading => Ok(TiptapNode::Heading { attrs: TiptapHeadingAttrs { level: block.props.heading_level.unwrap_or(1), @@ -698,6 +710,21 @@ fn node_to_block( child_block_ids: vec![], }) } + TiptapNode::Paragraph { attrs, content } + if attrs.mnote_block_type.as_deref() == Some("resource") => + { + let mut props = props_with_text_align(attrs.text_align.clone()); + for (key, value) in &attrs.extra { + props.extra.insert(key.clone(), value.clone()); + } + Ok(EditorBlock { + block_id: attrs.block_id.clone().unwrap_or(fallback_block_id), + block_type: EditorBlockType::Resource, + props, + content_nodes: text_nodes_from_tiptap(content)?, + child_block_ids: vec![], + }) + } TiptapNode::Paragraph { attrs, content } => Ok(EditorBlock { block_id: attrs.block_id.clone().unwrap_or(fallback_block_id), block_type: EditorBlockType::Paragraph, diff --git a/rust/crates/mnote-web/src/acp_bridge.rs b/rust/crates/mnote-web/src/acp_bridge.rs index 4860db94..729a201b 100644 --- a/rust/crates/mnote-web/src/acp_bridge.rs +++ b/rust/crates/mnote-web/src/acp_bridge.rs @@ -179,7 +179,7 @@ impl AcpRunBridge { /// Map an AcpSessionEvent to an SSE event for the frontend. /// /// Reference: hermes-vscode-main protocol.ts extractTextContent/parseToolCall/parseToolCallUpdate -/// Reference: wolai-frontend bridge.ts HermesRunEvent type +/// Reference: recycle/wolai-frontend bridge.ts HermesRunEvent type (historic) pub fn acp_event_to_sse(event: AcpSessionEvent) -> Option { match event { AcpSessionEvent::TextDelta { text } => Some(SseEvent { @@ -233,6 +233,24 @@ pub fn acp_event_to_sse(event: AcpSessionEvent) -> Option { event: "usage.updated".into(), data: json!({ "used": used, "size": size }), }), + AcpSessionEvent::PermissionRequest { + permission_id, + tool_name, + params, + decision, + } => Some(SseEvent { + event: if decision == "allowed" { + "permission.allowed".into() + } else { + "permission.denied".into() + }, + data: json!({ + "permissionId": permission_id, + "toolName": tool_name, + "params": params, + "decision": decision, + }), + }), AcpSessionEvent::SessionInfoUpdate { .. } => { None // Not forwarded to frontend } @@ -292,6 +310,23 @@ mod tests { assert_eq!(sse.data["delta"], "internal reasoning"); } + #[test] + fn acp_permission_request_emits_frontend_decision_event() { + let event = AcpSessionEvent::PermissionRequest { + permission_id: "perm_1".into(), + tool_name: "mnote.page.save".into(), + params: json!({"documentId": "doc_1"}), + decision: "denied".into(), + }; + + let sse = acp_event_to_sse(event).expect("permission decision should be forwarded"); + assert_eq!(sse.event, "permission.denied"); + assert_eq!(sse.data["permissionId"], "perm_1"); + assert_eq!(sse.data["toolName"], "mnote.page.save"); + assert_eq!(sse.data["params"]["documentId"], "doc_1"); + assert_eq!(sse.data["decision"], "denied"); + } + #[test] fn acp_tool_events_keep_detail_for_collapsible_ui() { let started = acp_event_to_sse(AcpSessionEvent::ToolCall { diff --git a/rust/crates/mnote-web/src/acp_client.rs b/rust/crates/mnote-web/src/acp_client.rs index 78a5f44f..0d49725a 100644 --- a/rust/crates/mnote-web/src/acp_client.rs +++ b/rust/crates/mnote-web/src/acp_client.rs @@ -149,7 +149,7 @@ impl AcpClient { .take() .ok_or_else(|| AcpError::Internal("failed to take child stdout".into()))?; - let writer = BufWriter::new(stdin); + let writer = Arc::new(Mutex::new(BufWriter::new(stdin))); let reader = BufReader::new(stdout); let pending: Arc>> = Arc::new(Mutex::new(HashMap::new())); @@ -159,15 +159,16 @@ impl AcpClient { // Start background reader task let pending_clone = pending.clone(); let handler_clone = notification_handler.clone(); + let writer_clone = writer.clone(); let child_pid = child.id().unwrap_or(0); tokio::spawn(async move { - Self::reader_loop(reader, pending_clone, handler_clone).await; + Self::reader_loop(reader, writer_clone, pending_clone, handler_clone).await; info!("ACP reader loop ended (pid={})", child_pid); }); let client = Self { child: Some(child), - writer: Arc::new(Mutex::new(writer)), + writer, pending, next_id: AtomicU64::new(1), notification_handler, @@ -285,6 +286,7 @@ impl AcpClient { /// Reference: `acpClient.ts` L120-180 (onData + dispatch) async fn reader_loop( mut reader: BufReader, + writer: Arc>>, pending: Arc>>, notification_handler: Arc, ) { @@ -319,7 +321,7 @@ impl AcpClient { } }; - Self::dispatch_message(msg, &pending, ¬ification_handler).await; + Self::dispatch_message(msg, &writer, &pending, ¬ification_handler).await; } // Process died or EOF — resolve all pending @@ -334,6 +336,7 @@ impl AcpClient { /// Reference: `acpClient.ts` L160-200 (dispatch) async fn dispatch_message( msg: Value, + writer: &Arc>>, pending: &Arc>>, notification_handler: &Arc, ) { @@ -348,11 +351,32 @@ impl AcpClient { // Incoming request from agent (e.g. session/request_permission) let method = msg["method"].as_str().unwrap_or("unknown").to_string(); let params = msg.get("params").cloned().unwrap_or(Value::Null); - // For now, reject all incoming requests since we don't need permission dialogs yet. - // Reference: acpClient.ts handleIncomingRequest (L200-220) + let id = msg.get("id").cloned().unwrap_or(Value::Null); + // 当前还没有权限确认 UI,必须明确拒绝,避免 agent 等待到超时。 warn!("ACP incoming request not handled: {method} (params={params:?})"); - // If we wanted to reply, we'd need to write back a response... - // For now just log. Phase C will add permission support. + let response = json!({ + "jsonrpc": "2.0", + "id": id, + "error": { + "code": -32601, + "message": format!("ACP incoming request not supported: {method}") + } + }); + if let Err(error) = Self::write_jsonrpc_message(writer, &response).await { + warn!("ACP incoming request response write failed: {error}"); + } + if method == "session/request_permission" { + let mut event_params = params; + if let Some(object) = event_params.as_object_mut() { + object.insert("decision".into(), Value::String("denied".into())); + object.insert("method".into(), Value::String(method.clone())); + object.insert("jsonrpcId".into(), id); + } + let handler_guard = notification_handler.lock().unwrap(); + if let Some(ref handler) = *handler_guard { + handler(method, event_params); + } + } } else if has_id { // Response to one of our requests if let Some(id) = msg["id"].as_u64() { @@ -388,6 +412,18 @@ impl AcpClient { } } } + + async fn write_jsonrpc_message( + writer: &Arc>>, + msg: &Value, + ) -> Result<(), AcpError> { + let line = serde_json::to_string(msg)?; + let mut writer = writer.lock().await; + writer.write_all(line.as_bytes()).await?; + writer.write_all(b"\n").await?; + writer.flush().await?; + Ok(()) + } } impl Drop for AcpClient { @@ -443,6 +479,48 @@ rl.on('line', (line) => { .expect("spawn mock ACP") } + async fn spawn_permission_request_mock_server() -> AcpClient { + let script = r#" +import * as readline from 'node:readline'; +import { stdin as input, stdout as output } from 'node:process'; +let permissionResponse = null; +const rl = readline.createInterface({ input, output, terminal: false }); +function send(msg) { process.stdout.write(JSON.stringify(msg) + '\n'); } +rl.on('line', (line) => { + const msg = JSON.parse(line); + if (msg.id !== undefined && msg.method === 'initialize') { + send({ + jsonrpc: '2.0', + id: msg.id, + result: { protocolVersion: 1, agentCapabilities: {}, agentInfo: { name: 'mock', version: '1.0' }, authMethods: [] } + }); + setTimeout(() => send({ + jsonrpc: '2.0', + id: 77, + method: 'session/request_permission', + params: { reason: 'test permission' } + }), 10); + } else if (msg.id === 77 && msg.method === undefined) { + permissionResponse = msg; + } else if (msg.id !== undefined && msg.method === 'get_permission_response') { + send({ + jsonrpc: '2.0', + id: msg.id, + result: { permissionResponse } + }); + } +}); + "#; + + let dir = std::env::temp_dir(); + let script_path = dir.join("acp_permission_request_mock.mjs"); + std::fs::write(&script_path, script).expect("write permission mock script"); + + AcpClient::spawn("node", &[script_path.to_str().unwrap()]) + .await + .expect("spawn permission mock ACP") + } + #[tokio::test] async fn test_request_response() { let client = spawn_mock_acp_server().await; @@ -504,4 +582,18 @@ rl.on('line', (line) => { // spawn already calls initialize; if it fails, the test fails let _client = spawn_mock_acp_server().await; } + + #[tokio::test] + async fn test_incoming_permission_request_gets_response() { + let client = spawn_permission_request_mock_server().await; + tokio::time::sleep(Duration::from_millis(100)).await; + let result: Value = client + .request("get_permission_response", json!({})) + .await + .expect("permission response probe"); + let response = &result["permissionResponse"]; + assert_eq!(response["jsonrpc"], "2.0"); + assert_eq!(response["id"], 77); + assert!(response.get("result").is_some() || response.get("error").is_some()); + } } diff --git a/rust/crates/mnote-web/src/acp_session_manager.rs b/rust/crates/mnote-web/src/acp_session_manager.rs index 886d055b..b497ce46 100644 --- a/rust/crates/mnote-web/src/acp_session_manager.rs +++ b/rust/crates/mnote-web/src/acp_session_manager.rs @@ -43,6 +43,13 @@ pub enum AcpSessionEvent { }, /// Context usage update. UsageUpdate { used: u64, size: u64 }, + /// Agent 发起权限请求;当前 runtime 会自动拒绝并同步给前端。 + PermissionRequest { + permission_id: String, + tool_name: String, + params: Value, + decision: String, + }, /// Session metadata update (e.g. auto-title). SessionInfoUpdate { title: String }, /// Plan entries update. @@ -51,6 +58,16 @@ pub enum AcpSessionEvent { Disconnected { reason: String }, } +#[derive(Debug, Clone, Default)] +pub struct AcpMnoteToolContext { + pub mnote_session_id: Option, + pub run_id: Option, + pub actor_id: Option, + pub trace_id: Option, + pub workspace_id: Option, + pub document_id: Option, +} + /// Handler for session events. pub type SessionEventHandler = Arc; @@ -101,6 +118,34 @@ impl AcpSessionManager { let in_prompt_clone = in_prompt.clone(); client.on_notification(move |method, params| { + if method == "session/request_permission" { + let permission_id = params + .get("permissionId") + .or_else(|| params.get("permission_id")) + .or_else(|| params.get("requestId")) + .or_else(|| params.get("id")) + .and_then(Value::as_str) + .unwrap_or("permission_auto_denied") + .to_string(); + let tool_name = params + .get("toolName") + .or_else(|| params.get("tool")) + .or_else(|| params.get("name")) + .or_else(|| params.get("method")) + .and_then(Value::as_str) + .unwrap_or("session/request_permission") + .to_string(); + let handler = event_handler_clone.lock().unwrap(); + if let Some(ref h) = *handler { + h(AcpSessionEvent::PermissionRequest { + permission_id, + tool_name, + params, + decision: "denied".into(), + }); + } + return; + } if method != "session/update" { return; } @@ -193,6 +238,14 @@ impl AcpSessionManager { pub async fn run_prompt( &self, prompt: Vec, + ) -> Result { + self.run_prompt_with_mnote_context(prompt, None).await + } + + pub async fn run_prompt_with_mnote_context( + &self, + prompt: Vec, + mnote_context: Option, ) -> Result { { let mut in_prompt = self.in_prompt.lock().unwrap(); @@ -215,9 +268,16 @@ impl AcpSessionManager { ) })?; + let mnote_context = mnote_context.unwrap_or_default(); let params = SessionPromptParams { session_id: session_id.clone(), prompt, + mnote_session_id: mnote_context.mnote_session_id, + run_id: mnote_context.run_id, + actor_id: mnote_context.actor_id, + trace_id: mnote_context.trace_id, + workspace_id: mnote_context.workspace_id, + document_id: mnote_context.document_id, }; debug!("ACP session/prompt (session={})", session_id); diff --git a/rust/crates/mnote-web/src/acp_types.rs b/rust/crates/mnote-web/src/acp_types.rs index 0127d265..492f0121 100644 --- a/rust/crates/mnote-web/src/acp_types.rs +++ b/rust/crates/mnote-web/src/acp_types.rs @@ -163,6 +163,18 @@ pub struct ResourceContent { pub struct SessionPromptParams { pub session_id: String, pub prompt: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub mnote_session_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub run_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub actor_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub trace_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub workspace_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub document_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/rust/crates/mnote-web/src/editor_actor.rs b/rust/crates/mnote-web/src/editor_actor.rs index 5a24a3bc..391c8c11 100644 --- a/rust/crates/mnote-web/src/editor_actor.rs +++ b/rust/crates/mnote-web/src/editor_actor.rs @@ -430,5 +430,6 @@ fn block_type_name(block_type: &EditorBlockType) -> String { EditorBlockType::Toc => "table_of_contents".into(), EditorBlockType::PageReference => "page_reference".into(), EditorBlockType::BlockReference => "block_reference".into(), + EditorBlockType::Resource => "resource".into(), } } diff --git a/rust/crates/mnote-web/src/hermes_tools/block.rs b/rust/crates/mnote-web/src/hermes_tools/block.rs index 47f31c0d..4873bf7e 100644 --- a/rust/crates/mnote-web/src/hermes_tools/block.rs +++ b/rust/crates/mnote-web/src/hermes_tools/block.rs @@ -87,6 +87,7 @@ pub async fn block_replace( ensure_page_write_preconditions(context, input, &aggregate)?; ensure_block_revision_ref(context, input, &block, "blockRevisionRef")?; ensure_block_editable(context, &block)?; + ensure_allowed_target(context, input, &Value::Null, &block_id, "replace")?; let replacement_text = content_to_text(&content); let diff = json!([{ "op": "replace", @@ -155,58 +156,73 @@ pub async fn block_insert_after( ensure_page_write_preconditions(context, input, &aggregate)?; ensure_block_revision_ref(context, input, &anchor, "anchorRevisionRef")?; ensure_block_editable(context, &anchor)?; - let content = input - .arg_value("content") - .or_else(|| input.arg_value("block")) - .ok_or_else(|| { - WebError::bad_request_code( - "mnote_tool_bad_request", - "mnote.block.insert_after 缺少 content", - ) - .with_context(context) - })?; - let new_block_id = format!("ai_block_{}", context.trace.request_id.replace('-', "_")); - let new_block = build_insert_block(&new_block_id, &content); - let diff = json!([{ - "op": "insert_after", - "anchorBlockId": anchor_block_id, - "after": anchor.get("text").cloned().unwrap_or(Value::Null), - "block": new_block - }]); - if input.dry_run.unwrap_or(false) { - return Ok(dry_run_result( - input, - &aggregate, - "block_insert_after", - diff, - false, - )); - } - let current_content = current_body_content(&aggregate); - let command = EditorCommand::InsertBlockAfter(EditorInsertBlockAfter { - after_block_id: anchor_block_id.clone(), - block: build_editor_block(&new_block_id, &content), - }); - let (next_content, delta_opt) = compute_next_content_via_actor( - state, - &aggregate, - input, - ¤t_content, - &command, - "block_insert_after", - )?; - let mut result = execute_page_body_save( - state, + ensure_allowed_target( context, input, - next_content, - vec![json!({ - "blockId": new_block_id, + &Value::Null, + &anchor_block_id, + "insert_after", + )?; + let insert_values = insert_after_values(context, input)?; + let id_prefix = format!("ai_block_{}", context.trace.request_id.replace('-', "_")); + let inserted_block_ids = insert_values + .iter() + .enumerate() + .map(|(index, _)| { + if insert_values.len() == 1 { + id_prefix.clone() + } else { + format!("{id_prefix}_{index}") + } + }) + .collect::>(); + let diff = insert_values + .iter() + .zip(inserted_block_ids.iter()) + .map(|(content, block_id)| { + json!({ + "op": "insert_after", + "anchorBlockId": anchor_block_id, + "after": anchor.get("text").cloned().unwrap_or(Value::Null), + "block": build_insert_block(block_id, content) + }) + }) + .collect::>(); + if input.dry_run.unwrap_or(false) { + let mut result = + dry_run_result(input, &aggregate, "block_insert_after", json!(diff), false); + result["insertedBlockIds"] = json!(inserted_block_ids); + return Ok(result); + } + let mut next_content = current_body_content(&aggregate); + let mut changed_blocks = Vec::with_capacity(insert_values.len()); + let mut delta_opt = None; + let mut after_block_id = anchor_block_id.clone(); + for (content, block_id) in insert_values.iter().zip(inserted_block_ids.iter()) { + let command = EditorCommand::InsertBlockAfter(EditorInsertBlockAfter { + after_block_id: after_block_id.clone(), + block: build_editor_block(block_id, content), + }); + let (content_after_command, command_delta) = compute_next_content_via_actor( + state, + &aggregate, + input, + &next_content, + &command, + "block_insert_after", + )?; + next_content = content_after_command; + delta_opt = command_delta; + changed_blocks.push(json!({ + "blockId": block_id, "op": "insert_after", - "anchorBlockId": anchor_block_id - })], - ) - .await?; + "anchorBlockId": after_block_id + })); + after_block_id = block_id.clone(); + } + let mut result = + execute_page_body_save(state, context, input, next_content, changed_blocks).await?; + result["insertedBlockIds"] = json!(inserted_block_ids); merge_block_delta(&mut result, delta_opt); Ok(result) } @@ -224,6 +240,7 @@ pub async fn block_delete( ensure_page_write_preconditions(context, input, &aggregate)?; ensure_block_revision_ref(context, input, &block, "blockRevisionRef")?; ensure_block_editable(context, &block)?; + ensure_allowed_target(context, input, &Value::Null, &block_id, "delete")?; let leaf = block .get("children") .and_then(Value::as_array) @@ -298,6 +315,14 @@ pub async fn block_move_after( ensure_page_write_preconditions(context, input, &aggregate)?; ensure_block_revision_ref(context, input, &block, "blockRevisionRef")?; ensure_block_revision_ref(context, input, &anchor, "anchorRevisionRef")?; + ensure_allowed_target(context, input, &Value::Null, &block_id, "move_after")?; + ensure_allowed_target( + context, + input, + &Value::Null, + &anchor_block_id, + "move_after_anchor", + )?; let same_parent = block.get("parentBlockId") == anchor.get("parentBlockId"); let leaf = block .get("children") @@ -413,6 +438,7 @@ pub async fn doc_apply_block_ops( ) .with_context(context)); } + ensure_page_write_preconditions(context, input, &aggregate)?; let blocks = block_projection_blocks(&aggregate); let mut next_content = current_body_content(&aggregate); @@ -431,6 +457,13 @@ pub async fn doc_apply_block_ops( "replace" | "block_replace" => { let block = resolve_target_block(context, &blocks, operation, false)?; ensure_block_editable(context, &block)?; + ensure_operation_block_revision_ref( + context, + input, + operation, + &block, + "blockRevisionRef", + )?; let block_id = block_id_of(&block).unwrap_or_default(); let content = operation.get("content").cloned().ok_or_else(|| { WebError::bad_request_code("mnote_tool_bad_request", "replace 操作缺少 content") @@ -466,6 +499,13 @@ pub async fn doc_apply_block_ops( "insert_after" | "block_insert_after" => { let anchor = resolve_anchor_block(context, &blocks, operation)?; ensure_block_editable(context, &anchor)?; + ensure_operation_block_revision_ref( + context, + input, + operation, + &anchor, + "anchorRevisionRef", + )?; let anchor_block_id = block_id_of(&anchor).unwrap_or_default(); ensure_allowed_target(context, input, operation, &anchor_block_id, "insert_after")?; let content = operation.get("content").cloned().ok_or_else(|| { @@ -511,6 +551,13 @@ pub async fn doc_apply_block_ops( "delete" | "block_delete" => { let block = resolve_target_block(context, &blocks, operation, false)?; ensure_block_editable(context, &block)?; + ensure_operation_block_revision_ref( + context, + input, + operation, + &block, + "blockRevisionRef", + )?; let block_id = block_id_of(&block).unwrap_or_default(); ensure_allowed_target(context, input, operation, &block_id, "delete")?; ensure_leaf_block(context, &block, "delete")?; @@ -540,6 +587,20 @@ pub async fn doc_apply_block_ops( let block = resolve_target_block(context, &blocks, operation, false)?; let anchor = resolve_anchor_block(context, &blocks, operation)?; ensure_block_editable(context, &block)?; + ensure_operation_block_revision_ref( + context, + input, + operation, + &block, + "blockRevisionRef", + )?; + ensure_operation_block_revision_ref( + context, + input, + operation, + &anchor, + "anchorRevisionRef", + )?; ensure_leaf_block(context, &block, "move_after")?; let block_id = block_id_of(&block).unwrap_or_default(); let anchor_block_id = block_id_of(&anchor).unwrap_or_default(); @@ -628,7 +689,10 @@ pub async fn doc_apply_block_ops( .await } -fn ensure_write_contract(context: &RequestContext, input: &ToolCallInput) -> Result<(), WebError> { +pub(crate) fn ensure_write_contract( + context: &RequestContext, + input: &ToolCallInput, +) -> Result<(), WebError> { if !input.has_idempotency_key() { return Err(WebError::bad_request_code( "mnote_tool_idempotency_required", @@ -833,6 +897,37 @@ fn ensure_block_revision_ref( Ok(()) } +fn ensure_operation_block_revision_ref( + context: &RequestContext, + input: &ToolCallInput, + operation: &Value, + block: &Value, + arg_name: &'static str, +) -> Result<(), WebError> { + if input.dry_run.unwrap_or(false) { + return Ok(()); + } + let expected = operation + .get(arg_name) + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .ok_or_else(|| missing_write_precondition(context, arg_name))?; + let current = block + .get("revisionRef") + .and_then(Value::as_str) + .map(str::trim) + .unwrap_or(""); + if expected != current { + return Err(WebError::bad_request_code( + "mnote_tool_conflict", + format!("{arg_name} 已过期,请重新读取 block projection 后再写入"), + ) + .with_context(context)); + } + Ok(()) +} + fn ensure_block_editable(context: &RequestContext, block: &Value) -> Result<(), WebError> { if block .get("editable") @@ -851,6 +946,40 @@ fn ensure_block_editable(context: &RequestContext, block: &Value) -> Result<(), .with_context(context)) } +fn insert_after_values( + context: &RequestContext, + input: &ToolCallInput, +) -> Result, WebError> { + if let Some(blocks) = input.arg_value("blocks") { + let values = blocks.as_array().ok_or_else(|| { + WebError::bad_request_code( + "mnote_tool_bad_request", + "mnote.block.insert_after 的 blocks 必须是数组", + ) + .with_context(context) + })?; + if values.is_empty() || values.len() > 20 { + return Err(WebError::bad_request_code( + "mnote_tool_bad_request", + "mnote.block.insert_after 的 blocks 数量必须在 1 到 20 之间", + ) + .with_context(context)); + } + return Ok(values.clone()); + } + input + .arg_value("content") + .or_else(|| input.arg_value("block")) + .map(|value| vec![value]) + .ok_or_else(|| { + WebError::bad_request_code( + "mnote_tool_bad_request", + "mnote.block.insert_after 缺少 content、block 或 blocks", + ) + .with_context(context) + }) +} + fn missing_write_precondition(context: &RequestContext, name: &'static str) -> WebError { WebError::bad_request_code( "mnote_tool_write_precondition_required", @@ -1049,7 +1178,7 @@ async fn execute_page_body_save( })) } -async fn execute_page_body_save_from_aggregate( +pub(crate) async fn execute_page_body_save_from_aggregate( state: &AppState, context: &RequestContext, input: &ToolCallInput, @@ -1255,7 +1384,7 @@ fn merge_block_delta(response: &mut Value, delta_opt: Option) { } } -fn current_body_content(aggregate: &Value) -> Value { +pub(crate) fn current_body_content(aggregate: &Value) -> Value { aggregate .pointer("/body/content") .cloned() diff --git a/rust/crates/mnote-web/src/hermes_tools/doc.rs b/rust/crates/mnote-web/src/hermes_tools/doc.rs index e8a81225..6d85542a 100644 --- a/rust/crates/mnote-web/src/hermes_tools/doc.rs +++ b/rust/crates/mnote-web/src/hermes_tools/doc.rs @@ -476,6 +476,7 @@ pub(crate) fn block_projection_blocks(aggregate: &Value) -> Vec { pub(crate) fn block_id_of(block: &Value) -> Option { block .get("blockId") + .or_else(|| block.get("id")) .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) @@ -517,6 +518,483 @@ fn filter_blocks_by_query(blocks: Vec, query: &str) -> Vec { .collect() } +// ── 7-27: Markdown → Block Content 直接写回 ──────────────────────────── + +/// 解析后的单个块信息,用作 final_md → page.body.save 的中间表示 +#[derive(Debug, Clone)] +#[allow(dead_code)] +struct ParsedBlockInfo { + /// 原始 block_id;None 表示新块(full_content 或无注释的新 paragraph) + block_id: Option, + block_type: String, + text: String, + block_revision_ref: Option, + /// 标记为新建块——写入层分配新 ID + is_new: bool, + /// 从原始 block projection 中复制的完整块对象 + original: Option, + /// 原始块的 props(如 heading level) + props: Option, + /// 复杂块(resource/mindmap/table)的原始内容——只保留,不改动 + is_complex: bool, +} + +/// 将搜索替换后的最终 markdown 解析为块列表,保留原始块的元数据 +/// +/// * `final_md` — `search_replace` 应用后的最终 markdown +/// * `originals` — 从 `aggregate.body.blockDocument.blocks` 读取的原始块 +fn parse_final_markdown_to_blocks(final_md: &str, originals: &[Value]) -> Vec { + let originals_by_id: std::collections::HashMap = originals + .iter() + .filter_map(|block| block_id_of(block).map(|id| (id, block))) + .collect(); + + let mut blocks = Vec::new(); + + for line in final_md.lines() { + let line = line.trim(); + if line.is_empty() { + continue; + } + + // 尝试提取 注释 + let (text_part, meta) = extract_block_comment(line); + + // 检测复杂块标记 [mnote-raw-block:id] + if let Some(raw_id) = extract_raw_block_marker(line) { + if let Some(original) = originals_by_id.get(&raw_id) { + blocks.push(ParsedBlockInfo { + block_id: Some(raw_id.clone()), + block_type: original + .get("type") + .and_then(Value::as_str) + .unwrap_or("paragraph") + .to_string(), + text: block_text(original), + block_revision_ref: original + .get("revisionRef") + .and_then(Value::as_str) + .map(str::to_string), + is_new: false, + original: Some((*original).clone()), + props: original.get("props").cloned(), + is_complex: true, + }); + } + continue; + } + + if let Some((block_id, block_type, _level)) = meta { + // 匹配到注释的块——从 originals 继承元数据 + if let Some(original) = originals_by_id.get(&block_id) { + let raw_text = text_part.unwrap_or_else(|| block_text(original)); + blocks.push(ParsedBlockInfo { + block_id: Some(block_id.clone()), + block_type, + text: strip_prefix(&raw_text), + block_revision_ref: original + .get("revisionRef") + .and_then(Value::as_str) + .map(str::to_string), + is_new: false, + original: Some((*original).clone()), + props: original.get("props").cloned(), + is_complex: false, + }); + } else { + // 注释中的 block_id 不在 originals 中(不应该发生) + let raw_text = text_part.unwrap_or_default(); + blocks.push(ParsedBlockInfo { + block_id: Some(block_id), + block_type, + text: strip_prefix(&raw_text), + block_revision_ref: None, + is_new: true, + original: None, + props: None, + is_complex: false, + }); + } + } else { + // 无注释的纯文本行 → 新 paragraph 块 + let block_type = detect_block_type_from_prefix(line); + let text = strip_prefix(line); + blocks.push(ParsedBlockInfo { + block_id: None, + block_type, + text, + block_revision_ref: None, + is_new: true, + original: None, + props: None, + is_complex: false, + }); + } + } + + blocks +} + +/// 从行中提取 `` 注释及其前的文本 +fn extract_block_comment(line: &str) -> (Option, Option<(String, String, Option)>) { + let comment_start = line.rfind(""); + if let (Some(start), Some(end)) = (comment_start, comment_end) { + let before = line[..start].trim(); + // 完整注释区间: "" (3 chars for "-->") + let comment = &line[start..end + 3]; + let inner = comment + .strip_prefix("")) + .map(str::trim) + .unwrap_or(""); + + // 期望格式: block:ID:TYPE 或 block:ID:TYPE:level=N + // TYPE 中不含冒号(paragraph/heading/todo/resource/mindmap/table/image) + let parts: Vec<&str> = inner.splitn(3, ':').collect(); + if parts.len() >= 2 && parts[0] == "block" { + let id = parts[1].to_string(); + let rest = if parts.len() >= 3 { parts[2] } else { "" }; + + // 尝试从 rest 中分割 type 和 level + let (typ, level) = if let Some(level_idx) = rest.find(":level=") { + let typ = rest[..level_idx].to_string(); + let level_str = &rest[level_idx + 7..]; // ":level=" is 7 chars + let level = level_str.parse::().ok(); + (typ, level) + } else { + (rest.to_string(), None) + }; + + let typ = if typ.is_empty() { + "paragraph".to_string() + } else { + typ + }; + let text = if before.is_empty() { + None + } else { + Some(before.to_string()) + }; + return (text, Some((id, typ, level))); + } + + // 向后兼容旧格式: + let parts_old: Vec<&str> = inner.splitn(2, ':').collect(); + if parts_old.len() == 2 && parts_old[0] == "block" { + let id = parts_old[1].to_string(); + let text = if before.is_empty() { + None + } else { + Some(before.to_string()) + }; + return (text, Some((id, "paragraph".to_string(), None))); + } + } + (None, None) +} + +/// 提取 `[mnote-raw-block:ID]` 标记 +fn extract_raw_block_marker(line: &str) -> Option { + let start = line.find("[mnote-raw-block:"); + let end = line.find(']'); + if let (Some(start), Some(end)) = (start, end) { + let id = &line[start + 18..end]; + return Some(id.to_string()); + } + None +} + +/// 从行前缀推断块类型 +fn detect_block_type_from_prefix(line: &str) -> String { + let trimmed = line.trim(); + if trimmed.starts_with("## ") { + "heading".to_string() + } else if trimmed.starts_with("- [ ] ") + || trimmed.starts_with("- [x] ") + || trimmed.starts_with("- [X] ") + { + "todo".to_string() + } else { + "paragraph".to_string() + } +} + +/// 去掉 markdown 前缀(`## ` / `- [ ] `),返回纯文本 +fn strip_prefix(line: &str) -> String { + let trimmed = line.trim(); + if let Some(rest) = trimmed.strip_prefix("## ") { + rest.to_string() + } else if let Some(rest) = trimmed.strip_prefix("- [ ] ") { + rest.to_string() + } else if let Some(rest) = trimmed.strip_prefix("- [x] ") { + rest.to_string() + } else if let Some(rest) = trimmed.strip_prefix("- [X] ") { + rest.to_string() + } else { + trimmed.to_string() + } +} + +/// 将解析后的块列表与原始 `body/content` 合并,生成最终写回 blocks 数组 +fn build_page_content(original_content: &Value, parsed: &[ParsedBlockInfo]) -> Value { + // 为 original_content 建立 block_id → index + full_block 映射 + let original_blocks: Vec = original_content.as_array().cloned().unwrap_or_default(); + let _original_index_by_id: std::collections::HashMap = original_blocks + .iter() + .enumerate() + .filter_map(|(i, block)| block_id_of(block).map(|id| (id, i))) + .collect(); + let original_by_id: std::collections::HashMap = original_blocks + .iter() + .filter_map(|block| block_id_of(block).map(|id| (id, block))) + .collect(); + + let mut result = Vec::new(); + let mut processed_ids = std::collections::HashSet::new(); + let parsed_references_original = parsed.iter().any(|parsed_block| { + parsed_block + .block_id + .as_ref() + .map(|block_id| original_by_id.contains_key(block_id)) + .unwrap_or(false) + }); + + for parsed_block in parsed { + if let Some(ref block_id) = parsed_block.block_id { + processed_ids.insert(block_id.clone()); + + if parsed_block.is_complex { + // 复杂块:原样保留 + if let Some(original) = original_by_id.get(block_id) { + result.push((*original).clone()); + } + } else if let Some(original) = original_by_id.get(block_id) { + let updated = + update_legacy_block_text_for_markdown_edit(original, block_id, parsed_block); + result.push(updated); + } + } else if parsed_block.is_new { + // 新块:生成 ID 并构造 legacy 格式的 paragraph 结构 + use std::sync::atomic::{AtomicU64, Ordering}; + static COUNTER: AtomicU64 = AtomicU64::new(0); + let next = COUNTER.fetch_add(1, Ordering::Relaxed); + let new_id = format!("ai_block_md_{}", next); + result.push(json!({ + "id": new_id, + "type": parsed_block.block_type, + "content": parsed_block.text, + })); + } + } + + // 追加未被处理的原始块(保留顺序) + for original in &original_blocks { + if let Some(id) = block_id_of(original) { + if !processed_ids.contains(&id) + && (parsed_references_original || is_complex_markdown_original_block(original)) + { + result.push(original.clone()); + } + } + } + + Value::Array(result) +} + +fn update_legacy_block_text_for_markdown_edit( + original: &Value, + block_id: &str, + parsed_block: &ParsedBlockInfo, +) -> Value { + let original_text = legacy_block_text(original); + if original_text == parsed_block.text { + return original.clone(); + } + + let mut updated = original.clone(); + if let Some(obj) = updated.as_object_mut() { + obj.remove("blockId"); + if !obj.contains_key("id") { + obj.insert("id".into(), json!(block_id)); + } + if let Some(content) = obj.get_mut("content") { + if replace_single_inline_text_value(content, &parsed_block.text) { + obj.remove("text"); + return updated; + } + } + if let Some(content_nodes) = obj.get_mut("contentNodes") { + if replace_single_inline_text_value(content_nodes, &parsed_block.text) { + obj.remove("text"); + return updated; + } + } + obj.insert("content".into(), json!(parsed_block.text)); + obj.remove("contentNodes"); + obj.remove("text"); + } + updated +} + +fn replace_single_inline_text_value(value: &mut Value, text: &str) -> bool { + if value.is_string() { + *value = json!(text); + return true; + } + if let Some(items) = value.as_array_mut() { + if items.len() != 1 { + return false; + } + return replace_single_inline_text_value(&mut items[0], text); + } + if let Some(object) = value.as_object_mut() { + if object.get("text").and_then(Value::as_str).is_some() { + object.insert("text".into(), json!(text)); + return true; + } + if let Some(payload) = object.get_mut("payload") { + return replace_single_inline_text_value(payload, text); + } + } + false +} + +fn legacy_block_text(block: &Value) -> String { + if let Some(text) = block.get("text").and_then(Value::as_str) { + return text.to_string(); + } + if let Some(content) = block.get("content") { + return inline_text_value(content); + } + if let Some(content_nodes) = block.get("contentNodes") { + return inline_text_value(content_nodes); + } + String::new() +} + +fn inline_text_value(value: &Value) -> String { + if let Some(text) = value.as_str() { + return text.to_string(); + } + if let Some(text) = value.get("text").and_then(Value::as_str) { + return text.to_string(); + } + if let Some(payload) = value.get("payload") { + return inline_text_value(payload); + } + if let Some(content) = value.get("content") { + return inline_text_value(content); + } + if let Some(items) = value.as_array() { + return items + .iter() + .map(inline_text_value) + .collect::>() + .join(""); + } + String::new() +} + +fn is_complex_markdown_original_block(block: &Value) -> bool { + let block_type = block + .get("type") + .and_then(Value::as_str) + .unwrap_or("paragraph") + .to_ascii_lowercase(); + matches!( + block_type.as_str(), + "resource" | "mindmap" | "table" | "image" | "embed" | "attachment" + ) || block + .get("editable") + .and_then(Value::as_bool) + .map(|editable| !editable) + .unwrap_or(false) + || block.get("unsupportedReason").is_some() + || block + .get("children") + .and_then(Value::as_array) + .map(|children| !children.is_empty()) + .unwrap_or(false) +} + +/// 生成 changedBlocks 摘要(格式与 `doc_apply_block_ops` 输出兼容) +fn build_changed_blocks_summary( + original_blocks: &[Value], + parsed: &[ParsedBlockInfo], +) -> Vec { + let original_by_id: std::collections::HashMap = original_blocks + .iter() + .filter_map(|block| block_id_of(block).map(|id| (id, block))) + .collect(); + let parsed_by_id: std::collections::HashMap = parsed + .iter() + .filter_map(|p| p.block_id.as_ref().map(|id| (id.clone(), p))) + .collect(); + + let mut changed = Vec::new(); + + for (id, _original) in &original_by_id { + if let Some(parsed_block) = parsed_by_id.get(id) { + if !parsed_block.is_complex { + let original_text = original_by_id + .get(id) + .map(|block| block_text(block)) + .unwrap_or_default(); + if original_text != parsed_block.text { + changed.push(json!({ + "op": "replace", + "blockId": id, + "blockRevisionRef": parsed_block.block_revision_ref, + "content": parsed_block.text + })); + } + } + } + } + + // 新块标记为 insert(无原始 block_id) + for parsed_block in parsed { + if parsed_block.is_new { + changed.push(json!({ + "op": "insert", + "content": parsed_block.text, + "type": parsed_block.block_type + })); + } + } + + changed +} + +fn ensure_markdown_changed_blocks_within_allowed( + context: &RequestContext, + changed_blocks: &[Value], + allowed_block_ids: &[String], +) -> Result<(), WebError> { + if allowed_block_ids.is_empty() { + return Ok(()); + } + let allowed: HashSet<&str> = allowed_block_ids.iter().map(String::as_str).collect(); + for change in changed_blocks { + let Some(block_id) = change.get("blockId").and_then(Value::as_str) else { + return Err(WebError::bad_request_code( + "mnote_markdown_edit_target_out_of_scope", + "selection 范围内的 markdown_edit 不允许写入无法归属到 blockId 的变更", + ) + .with_context(context)); + }; + if !allowed.contains(block_id) { + return Err(WebError::bad_request_code( + "mnote_markdown_edit_target_out_of_scope", + format!("markdown_edit 目标块 {block_id} 不在当前 AI selection 允许范围内"), + ) + .with_context(context)); + } + } + Ok(()) +} + fn selected_block_ids(input: &ToolCallInput) -> Vec { let mut seen = HashSet::new(); let mut ids = Vec::new(); @@ -576,16 +1054,34 @@ fn blocks_to_markdown(blocks: &[Value], include_ids: bool) -> String { .iter() .map(|block| { let text = block_text(block); - let prefix = match block.get("type").and_then(Value::as_str) { - Some("heading") => "## ", - Some("todo") | Some("task") => "- [ ] ", + let block_type = block + .get("type") + .and_then(Value::as_str) + .unwrap_or("paragraph"); + let prefix = match block_type { + "heading" => "## ", + "todo" | "task" => "- [ ] ", _ => "", }; if include_ids { - format!( - "{prefix}{text} ", - block_id_of(block).unwrap_or_default() - ) + let id = block_id_of(block).unwrap_or_default(); + let editable = block + .get("editable") + .and_then(Value::as_bool) + .unwrap_or(true); + let unsupported = block.get("unsupportedReason").and_then(Value::as_str); + if !editable || unsupported.is_some() { + // 复杂块(resource/mindmap/table/image):特殊标记供解析器原样保留 + format!("[mnote-raw-block:{id}] ") + } else if block_type == "heading" { + let level = block + .pointer("/props/level") + .and_then(Value::as_u64) + .unwrap_or(2); + format!("{prefix}{text} ") + } else { + format!("{prefix}{text} ") + } } else { format!("{prefix}{text}") } @@ -670,6 +1166,14 @@ mod tests { ); } + #[test] + fn test_search_replace_normalized_chinese_byte_boundaries() { + assert_eq!( + search_replace("前缀:第一段内容", "一 段", "二段").unwrap(), + "前缀:第二段内容" + ); + } + #[test] fn test_search_replace_not_found() { let result = search_replace("第一段内容。", "不存在的文本", "替换"); @@ -692,20 +1196,175 @@ mod tests { ]); let blocks: Vec = blocks.as_array().unwrap().clone(); let md = blocks_to_markdown(&blocks, true); - assert!(md.contains("第一段 ")); - assert!(md.contains("第二段 ")); + assert!(md.contains("第一段 ")); + assert!(md.contains("第二段 ")); } #[test] fn test_blocks_to_markdown_heading() { let blocks = json!([ - {"blockId": "h1", "text": "标题", "type": "heading"}, + {"blockId": "h1", "text": "标题", "type": "heading", "props": {"level": 2}}, {"blockId": "p1", "text": "正文", "type": "paragraph"} ]); let blocks: Vec = blocks.as_array().unwrap().clone(); - let md = blocks_to_markdown(&blocks, false); - assert!(md.contains("## 标题")); - assert!(md.contains("正文")); + let md = blocks_to_markdown(&blocks, true); + assert!(md.contains("标题 ")); + let md2 = blocks_to_markdown(&blocks, false); + assert!(md2.contains("## 标题")); + assert!(md2.contains("正文")); + } + + #[test] + fn test_blocks_to_markdown_complex_block_uses_raw_marker() { + let blocks = json!([ + {"blockId": "rsc_1", "text": "资源", "type": "resource", "editable": false, "unsupportedReason": "复杂块暂不开放 AI 精确写入"} + ]); + let blocks: Vec = blocks.as_array().unwrap().clone(); + let md = blocks_to_markdown(&blocks, true); + assert!(md.contains("[mnote-raw-block:rsc_1]")); + assert!(md.contains("")); + } + + #[test] + fn test_build_page_content_full_content_replaces_old_text_blocks_but_keeps_complex_blocks() { + let original_content = json!([ + {"id": "heading_1", "type": "heading", "content": "章节一"}, + {"id": "p_1", "type": "paragraph", "content": "第一段"}, + {"id": "resource_1", "type": "resource", "content": "资源块", "editable": false} + ]); + let parsed = vec![ + ParsedBlockInfo { + block_id: None, + block_type: "paragraph".into(), + text: "新章节".into(), + block_revision_ref: None, + is_new: true, + original: None, + props: None, + is_complex: false, + }, + ParsedBlockInfo { + block_id: None, + block_type: "paragraph".into(), + text: "新第一段".into(), + block_revision_ref: None, + is_new: true, + original: None, + props: None, + is_complex: false, + }, + ]; + let next = build_page_content(&original_content, &parsed); + let next_blocks = next.as_array().expect("array"); + assert_eq!(next_blocks.len(), 3); + assert_eq!(next_blocks[0]["content"], "新章节"); + assert_eq!(next_blocks[1]["content"], "新第一段"); + assert_eq!(next_blocks[2]["id"], "resource_1"); + } + + #[test] + fn test_build_page_content_preserves_missing_original_text_block_when_some_ids_remain() { + let original_content = json!([ + {"id": "heading_1", "type": "heading", "content": "章节一"}, + {"id": "p_1", "type": "paragraph", "content": "第一段"}, + {"id": "p_2", "type": "paragraph", "content": "第二段"} + ]); + let parsed = vec![ + ParsedBlockInfo { + block_id: Some("heading_1".into()), + block_type: "heading".into(), + text: "新章节".into(), + block_revision_ref: None, + is_new: false, + original: None, + props: None, + is_complex: false, + }, + ParsedBlockInfo { + block_id: Some("p_1".into()), + block_type: "paragraph".into(), + text: "新第一段".into(), + block_revision_ref: None, + is_new: false, + original: None, + props: None, + is_complex: false, + }, + ]; + let next = build_page_content(&original_content, &parsed); + let next_blocks = next.as_array().expect("array"); + assert!(next_blocks.iter().any(|block| block["id"] == "p_2")); + assert!(next_blocks + .iter() + .any(|block| block["id"] == "heading_1" && block["content"] == "新章节")); + assert_eq!(next_blocks.len(), 3); + } + + #[test] + fn test_build_page_content_preserves_unchanged_inline_content_nodes() { + let original_content = json!([ + { + "id": "p_1", + "type": "paragraph", + "content": [ + {"type": "text", "text": "第一段", "marks": [{"type": "bold"}]} + ] + }, + {"id": "p_2", "type": "paragraph", "content": "第二段"} + ]); + let parsed = vec![ + ParsedBlockInfo { + block_id: Some("p_1".into()), + block_type: "paragraph".into(), + text: "第一段".into(), + block_revision_ref: None, + is_new: false, + original: None, + props: None, + is_complex: false, + }, + ParsedBlockInfo { + block_id: Some("p_2".into()), + block_type: "paragraph".into(), + text: "第二段已改".into(), + block_revision_ref: None, + is_new: false, + original: None, + props: None, + is_complex: false, + }, + ]; + let next = build_page_content(&original_content, &parsed); + let p1 = &next.as_array().expect("array")[0]; + assert_eq!(p1["content"][0]["text"], "第一段"); + assert_eq!(p1["content"][0]["marks"][0]["type"], "bold"); + } + + #[test] + fn test_build_page_content_preserves_single_inline_node_marks_when_text_changes() { + let original_content = json!([ + { + "id": "p_1", + "type": "paragraph", + "content": [ + {"type": "text", "text": "第一段", "marks": [{"type": "bold"}]} + ] + } + ]); + let parsed = vec![ParsedBlockInfo { + block_id: Some("p_1".into()), + block_type: "paragraph".into(), + text: "第一段已改".into(), + block_revision_ref: None, + is_new: false, + original: None, + props: None, + is_complex: false, + }]; + let next = build_page_content(&original_content, &parsed); + let p1 = &next.as_array().expect("array")[0]; + assert_eq!(p1["content"][0]["text"], "第一段已改"); + assert_eq!(p1["content"][0]["marks"][0]["type"], "bold"); } } @@ -719,6 +1378,7 @@ pub async fn doc_markdown_edit( let document_id = input.effective_document_id().unwrap_or_default(); let workspace_id = input.effective_workspace_id(); let is_local_file = document_id.starts_with('/') || document_id.starts_with("./"); + crate::hermes_tools::block::ensure_write_contract(context, input)?; // 1. 读取当前文档内容(markdown 形式) let (current_md, source) = if is_local_file { @@ -841,41 +1501,112 @@ pub async fn doc_markdown_edit( // 5. 写回(本地文件直接 fs::write,Convex 文档通过 block ops apply) let apply_result = if is_local_file { - use std::fs; - fs::write(&document_id, &md).map_err(|error| { - WebError::bad_request_code( - "mnote_tool_bad_request", - format!("无法写入本地文件 {document_id}: {error}"), - ) - .with_context(context) - })?; - json!({"written": true, "path": document_id.clone()}) + if input.dry_run.unwrap_or(false) { + json!({"written": false, "dryRun": true, "path": document_id.clone()}) + } else { + use std::fs; + fs::write(&document_id, &md).map_err(|error| { + WebError::bad_request_code( + "mnote_tool_bad_request", + format!("无法写入本地文件 {document_id}: {error}"), + ) + .with_context(context) + })?; + json!({"written": true, "path": document_id.clone()}) + } } else { - // 构建 block ops:将修改后的 markdown 重新注入 - let aggregate = aggregate_value(state, context, input).await?; - let blocks = block_projection_blocks(&aggregate); - let block_ops = build_block_ops_from_markdown_edit(&blocks, &operations, applied); - - let apply_input = ToolCallInput { - tool_name: "mnote.doc.apply_block_ops".into(), - workspace_id: workspace_id.clone(), - document_id: Some(document_id.clone()), - actor_id: input.actor_id.clone(), - profile: input.profile.clone(), - session_id: input.session_id.clone(), - run_id: input.run_id.clone(), - tool_call_id: Some(format!("md_edit_{}", context.trace.request_id)), - trace_id: input.trace_id.clone(), - idempotency_key: Some(format!("md_edit_{}", context.trace.request_id)), - dry_run: input.dry_run, - capability_scope: Some(vec!["block.write".into(), "page.write".into()]), - args: Some(json!({ - "operations": block_ops, - "allowedTargetBlockIds": json!([]) - })), + // 7-27: 在线写回以最终 markdown 为真源,直接生成 block content + // 与 /api/documents/save 共用同一个 RuntimeCommandEnvelopeWire 路径 + let (blocks, original_content) = match aggregate_value(state, context, input).await { + Ok(ref agg) => ( + block_projection_blocks(agg), + crate::hermes_tools::block::current_body_content(agg), + ), + Err(_) if use_full_content.is_some() => { + // 空文档 + full_content:跳过读取 + (vec![], json!([])) + } + Err(e) => return Err(e), }; - crate::hermes_tools::block::doc_apply_block_ops(state, context, &apply_input).await? + let parsed = parse_final_markdown_to_blocks(&md, &blocks); + let next_content = build_page_content(&original_content, &parsed); + let changed_blocks = build_changed_blocks_summary(&blocks, &parsed); + ensure_markdown_changed_blocks_within_allowed( + context, + &changed_blocks, + &selected_block_ids(input), + )?; + + if input.dry_run == Some(true) { + // dryRun:返回 diff 预览,不真实写入 + json!({ + "written": false, + "dryRun": true, + "documentId": document_id, + "diff": changed_blocks + }) + } else { + // 直接构造 RuntimeCommandEnvelopeWire(与 /api/documents/save 相同) + let command_id = format!("markdown_edit_{}", context.trace.request_id); + let payload = json!({ + "documentId": document_id, + "workspaceId": workspace_id, + "content": next_content, + "mode": "replace", + }); + use bridge_runtime::{ + RuntimeActorWire, RuntimeCommandEnvelopeWire, RuntimeSourceWire, RuntimeTargetWire, + }; + let command = RuntimeCommandEnvelopeWire { + name: "page.body.save".into(), + command_id: command_id.clone(), + idempotency_key: Some(input.idempotency_key_or_default(&command_id)), + actor: RuntimeActorWire { + actor_type: context.auth.actor_type.clone(), + actor_id: context.auth.actor_id.clone(), + session_id: input + .session_id + .clone() + .or_else(|| context.auth.session_id.clone()), + }, + source: RuntimeSourceWire { + channel: "mnote-hermes".into(), + client: "mnote-hermes-plugin".into(), + source_kind: None, + root_uri: None, + workspace_id: None, + capabilities: Vec::new(), + }, + target: Some(RuntimeTargetWire { + workspace_id: workspace_id.clone(), + page_id: Some(document_id.clone()), + block_id: None, + }), + payload, + preflight_data: None, + reason: Some("mnote.doc.markdown_edit (7-27)".into()), + refs: vec!["page.body.save".into(), "mnote-hermes-tool-call".into()], + dry_run: false, + validate_only: false, + }; + let execution = + crate::routes::command_support::execute_runtime_command_via_convex_with_artifacts( + state, + context, + workspace_id.as_deref(), + command, + ) + .await?; + json!({ + "commandName": "page.body.save", + "commandId": command_id, + "changedBlocks": changed_blocks, + "result": execution.result, + "artifacts": execution.artifacts, + "artifactError": execution.artifact_error + }) + } }; Ok(json!({ @@ -894,48 +1625,10 @@ pub async fn doc_markdown_edit( /// 四级搜索替换:精确 → 忽略空白 → 段落 fuzzy → 失败 fn search_replace(text: &str, search: &str, replace: &str) -> Result { - // Level 1: 精确匹配 - if text.contains(search) { - return Ok(text.replacen(search, replace, 1)); - } - // Level 2: 忽略首尾空白和全角/半角差异 - let normalized = |s: &str| -> String { - s.trim() - .chars() - .map(|ch| match ch { - 'A'..='Z' => ((ch as u32).saturating_sub('A' as u32) + 'A' as u32) - .try_into() - .unwrap_or(ch), - 'a'..='z' => ((ch as u32).saturating_sub('a' as u32) + 'a' as u32) - .try_into() - .unwrap_or(ch), - '0'..='9' => ((ch as u32).saturating_sub('0' as u32) + '0' as u32) - .try_into() - .unwrap_or(ch), - '\u{3000}' => ' ', - _ => ch, - }) - .collect() - }; - let norm_search = normalized(search); - for line in text.lines() { - let norm_line = normalized(line); - if norm_line.contains(&norm_search) { - let start = norm_line.find(&norm_search).unwrap(); - let end = start + norm_search.len(); - let replaced = format!( - "{}{}{}", - &line[..line.char_indices().nth(start).map(|(i, _)| i).unwrap_or(0)], - replace, - &line[line - .char_indices() - .nth(end) - .map(|(i, _)| i) - .unwrap_or(line.len())..] - ); - return Ok(text.replacen(line, &replaced, 1)); - } + if let Some(replaced) = search_replace_exact_or_normalized(text, search, replace) { + return Ok(replaced); } + // Level 3: 按段落 fuzzy(30% 字符差异容限) for para in text.split("\n\n") { if fuzzy_match(para, search, 0.3) { @@ -955,6 +1648,66 @@ fn search_replace(text: &str, search: &str, replace: &str) -> Result Option { + // Level 1: 精确匹配 + if text.contains(search) { + return Some(text.replacen(search, replace, 1)); + } + + // Level 2: 忽略空白和全角/半角差异,同时保留原文 byte 边界。 + let norm_search = normalize_for_search(search); + if norm_search.is_empty() { + return None; + } + for line in text.lines() { + let (norm_line, byte_map) = normalize_line_with_byte_map(line); + let Some(start_byte_in_norm) = norm_line.find(&norm_search) else { + continue; + }; + let start = norm_line[..start_byte_in_norm].chars().count(); + let end = start + norm_search.chars().count(); + let start_byte = byte_map.get(start).copied().unwrap_or(0); + let end_byte = byte_map.get(end).copied().unwrap_or(line.len()); + let replaced = format!("{}{}{}", &line[..start_byte], replace, &line[end_byte..]); + return Some(text.replacen(line, &replaced, 1)); + } + None +} + +fn normalize_for_search(value: &str) -> String { + value.chars().filter_map(normalize_search_char).collect() +} + +fn normalize_line_with_byte_map(value: &str) -> (String, Vec) { + let mut normalized = String::new(); + let mut byte_map = Vec::new(); + for (byte_index, ch) in value.char_indices() { + if let Some(next) = normalize_search_char(ch) { + normalized.push(next); + byte_map.push(byte_index); + } + } + (normalized, byte_map) +} + +fn normalize_search_char(ch: char) -> Option { + if ch.is_whitespace() || ch == '\u{3000}' { + return None; + } + Some(match ch { + 'A'..='Z' => ((ch as u32).saturating_sub('A' as u32) + 'A' as u32) + .try_into() + .unwrap_or(ch), + 'a'..='z' => ((ch as u32).saturating_sub('a' as u32) + 'a' as u32) + .try_into() + .unwrap_or(ch), + '0'..='9' => ((ch as u32).saturating_sub('0' as u32) + '0' as u32) + .try_into() + .unwrap_or(ch), + _ => ch, + }) +} + fn fuzzy_match(text: &str, pattern: &str, max_diff_ratio: f64) -> bool { let text_chars: Vec = text.chars().collect(); let pat_chars: Vec = pattern.chars().collect(); @@ -972,38 +1725,4 @@ fn fuzzy_match(text: &str, pattern: &str, max_diff_ratio: f64) -> bool { } false } - -/// 将 markdown_edit 的 operations 转换为 block ops 供 doc_apply_block_ops 执行 -fn build_block_ops_from_markdown_edit( - blocks: &[Value], - operations: &[Value], - applied_count: usize, -) -> Vec { - let mut block_ops = Vec::new(); - for op in operations.iter().take(applied_count) { - let search = op.get("search").and_then(Value::as_str).unwrap_or(""); - let replace = op.get("replace").and_then(Value::as_str).unwrap_or(""); - let _content_val = op.get("content").cloned().unwrap_or_else(|| json!(replace)); - - // 在 blocks 中查找匹配文本 - let matched_block = blocks.iter().find(|block| { - block - .get("text") - .and_then(Value::as_str) - .map(|t| t.contains(search)) - .unwrap_or(false) - }); - - if let Some(block) = matched_block { - let block_id = block_id_of(block).unwrap_or_default(); - let block_text = block.get("text").and_then(Value::as_str).unwrap_or(""); - let new_text = block_text.replacen(search, replace, 1); - block_ops.push(json!({ - "op": "replace", - "blockId": block_id, - "content": new_text - })); - } - } - block_ops -} +// 退役:7-27 改为 final_md → blocks → execute_page_body_save_from_aggregate 直接写回 diff --git a/rust/crates/mnote-web/src/hermes_tools/manifest.rs b/rust/crates/mnote-web/src/hermes_tools/manifest.rs index 987059bc..6a78a766 100644 --- a/rust/crates/mnote-web/src/hermes_tools/manifest.rs +++ b/rust/crates/mnote-web/src/hermes_tools/manifest.rs @@ -180,7 +180,11 @@ fn block_replace_tool() -> Value { "content": { "type": ["string", "object", "array"] }, "revision": { "type": ["number", "string"] }, "conflictDetectionKey": { "type": "string" }, - "blockRevisionRef": { "type": "string" } + "blockRevisionRef": { "type": "string" }, + "allowedTargetBlockIds": { + "type": "array", + "items": { "type": "string" } + } }), [ "blockId", @@ -193,7 +197,7 @@ fn block_replace_tool() -> Value { } fn block_insert_after_tool() -> Value { - write_tool( + let mut tool = write_tool( "mnote.block.insert_after", "在指定块后插入新块;真实写入走 Rust page.body.save 链路", ["block.write", "page.write"], @@ -201,18 +205,38 @@ fn block_insert_after_tool() -> Value { "anchorBlockId": { "type": "string" }, "content": { "type": ["string", "object", "array"] }, "block": { "type": "object" }, + "blocks": { + "type": "array", + "minItems": 1, + "maxItems": 20, + "items": { "type": ["string", "object", "array"] } + }, "revision": { "type": ["number", "string"] }, "conflictDetectionKey": { "type": "string" }, - "anchorRevisionRef": { "type": "string" } + "anchorRevisionRef": { "type": "string" }, + "allowedTargetBlockIds": { + "type": "array", + "items": { "type": "string" } + } }), [ "anchorBlockId", - "content", "revision", "conflictDetectionKey", "anchorRevisionRef", ], - ) + ); + if let Some(schema) = tool.get_mut("inputSchema").and_then(Value::as_object_mut) { + schema.insert( + "anyOf".into(), + json!([ + { "required": ["content"] }, + { "required": ["block"] }, + { "required": ["blocks"] } + ]), + ); + } + tool } fn block_move_after_tool() -> Value { @@ -226,7 +250,11 @@ fn block_move_after_tool() -> Value { "revision": { "type": ["number", "string"] }, "conflictDetectionKey": { "type": "string" }, "blockRevisionRef": { "type": "string" }, - "anchorRevisionRef": { "type": "string" } + "anchorRevisionRef": { "type": "string" }, + "allowedTargetBlockIds": { + "type": "array", + "items": { "type": "string" } + } }), [ "blockId", @@ -248,7 +276,11 @@ fn block_delete_tool() -> Value { "blockId": { "type": "string" }, "revision": { "type": ["number", "string"] }, "conflictDetectionKey": { "type": "string" }, - "blockRevisionRef": { "type": "string" } + "blockRevisionRef": { "type": "string" }, + "allowedTargetBlockIds": { + "type": "array", + "items": { "type": "string" } + } }), [ "blockId", @@ -411,11 +443,12 @@ fn available_tool( } fn doc_markdown_edit_tool() -> Value { - let mut properties = base_identity_properties(); - if let Value::Object(map) = &mut properties { - map.insert( - "operations".into(), - json!({ + let mut tool = write_tool( + "mnote.doc.markdown_edit", + "通过文本级搜索替换编辑 markdown 内容(AI 编辑主路径)。在线 Convex 文档和本地 .md 文件共用,不需要 blockId。", + ["block.write", "page.write"], + json!({ + "operations": { "type": "array", "description": "搜索替换操作列表", "items": { @@ -426,25 +459,24 @@ fn doc_markdown_edit_tool() -> Value { }, "required": ["search", "replace"] } - }), - ); - map.insert( - "full_content".into(), - json!({ + }, + "full_content": { "type": "string", "description": "完整修改后的 markdown 文本(替代 operations)" - }), + } + }), + [], + ); + if let Some(schema) = tool.get_mut("inputSchema").and_then(Value::as_object_mut) { + schema.insert( + "anyOf".into(), + json!([ + { "required": ["operations"] }, + { "required": ["full_content"] } + ]), ); } - json!({ - "name": "mnote.doc.markdown_edit", - "description": "通过文本级搜索替换编辑 markdown 内容(AI 编辑主路径)。在线 Convex 文档和本地 .md 文件共用,不需要 blockId。", - "schemaVersion": TOOL_SCHEMA_VERSION, - "capabilityScope": ["block.write", "page.write"], - "status": "available", - "properties": properties, - "annotations": tool_annotations(false, false, true, false) - }) + tool } fn tool_annotations( diff --git a/rust/crates/mnote-web/src/lib.rs b/rust/crates/mnote-web/src/lib.rs index cf82b710..0a993cf1 100644 --- a/rust/crates/mnote-web/src/lib.rs +++ b/rust/crates/mnote-web/src/lib.rs @@ -20,3 +20,13 @@ pub mod tree_shell; pub mod workspace_shell; pub use app::{build_app, AppConfig, AppState}; + +#[cfg(test)] +pub(crate) mod test_support { + use std::sync::{Mutex, OnceLock}; + + pub(crate) fn hermes_env_lock() -> &'static Mutex<()> { + static LOCK: OnceLock> = OnceLock::new(); + LOCK.get_or_init(|| Mutex::new(())) + } +} diff --git a/rust/crates/mnote-web/src/routes/hermes_client.rs b/rust/crates/mnote-web/src/routes/hermes_client.rs index 4b97845b..74ce9004 100644 --- a/rust/crates/mnote-web/src/routes/hermes_client.rs +++ b/rust/crates/mnote-web/src/routes/hermes_client.rs @@ -3,7 +3,7 @@ use crate::app::AppState; use crate::context::RequestContext; use crate::error::WebError; use crate::hermes_tools::manifest; -use crate::transport::convex::execute_convex_query_by_name; +use crate::transport::convex::{execute_convex_mutation_by_name, execute_convex_query_by_name}; use axum::body::Body; use axum::extract::{Extension, Path, Query, State}; use axum::http::{header, HeaderMap, HeaderName, HeaderValue, StatusCode}; @@ -24,6 +24,8 @@ use tracing::{info, warn}; const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner"; const HEADER_HERMES_CLIENT_OWNER: &str = "x-mnote-hermes-client-owner"; +const ACP_RUNTIME_RUN_MUTATION: &str = "aiSessions:upsertRuntimeRun"; +const ACP_RUNTIME_EVENT_MUTATION: &str = "aiSessions:appendRuntimeEvent"; static HERMES_RUNTIME_REGISTRY: LazyLock>> = LazyLock::new(|| Mutex::new(HashMap::new())); @@ -90,7 +92,14 @@ pub struct CreateSessionRequest { profile: Option, } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RenameSessionRequest { + title: String, +} + pub async fn list_sessions( + State(state): State, Extension(context): Extension, Query(query): Query>, ) -> Result<(StatusCode, HeaderMap, Json), WebError> { @@ -99,6 +108,9 @@ pub async fn list_sessions( .get("profile") .map(String::as_str) .unwrap_or("default"); + if is_acp_profile(profile) || matches!(query.get("source").map(String::as_str), Some("acp")) { + return list_acp_sessions(&state, &context, &query).await; + } let Some(upstream) = configured_upstream_for_profile(profile) else { return hermes_unconfigured(&context); }; @@ -123,7 +135,161 @@ pub async fn list_sessions( .await } +pub async fn search_sessions( + State(state): State, + Extension(context): Extension, + Query(query): Query>, +) -> Result<(StatusCode, HeaderMap, Json), WebError> { + ensure_authenticated(&context)?; + let profile = query + .get("profile") + .map(String::as_str) + .unwrap_or("default"); + if is_acp_profile(profile) || matches!(query.get("source").map(String::as_str), Some("acp")) { + let user_id = effective_session_store_user_id(&state, &context).await?; + let q = query + .get("q") + .map(String::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .ok_or_else(|| { + WebError::bad_request_code("hermes_client_bad_request", "缺少 q") + .with_context(&context) + .with_header(HEADER_MNOTE_WEB_OWNER, "mnote-web") + .with_header(HEADER_HERMES_CLIENT_OWNER, "mnote-web-hermes-client") + })?; + let workspace_id = query + .get("workspaceId") + .map(String::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned) + .or_else(|| context.workspace.workspace_id.clone()); + let limit = query + .get("limit") + .and_then(|value| value.parse::().ok()) + .unwrap_or(20) + .clamp(1, 50); + let mut args = serde_json::Map::new(); + args.insert("userId".into(), Value::String(user_id)); + args.insert("q".into(), Value::String(q.to_string())); + args.insert("limit".into(), Value::from(limit)); + if let Some(workspace_id) = workspace_id.clone() { + args.insert("workspaceId".into(), Value::String(workspace_id)); + } + let results = execute_convex_query_by_name( + state.config(), + &context, + "aiSessions:searchRuntimeSessions", + Value::Object(args), + workspace_id.as_deref(), + "acp_runtime_session_search", + ) + .await?; + return Ok(( + StatusCode::OK, + stamp_client_headers(), + Json(json!({ + "ok": true, + "traceId": context.trace.trace_id, + "persistence": "convex_acp_runtime_store", + "results": results + })), + )); + } + + let Some(upstream) = configured_upstream_for_profile(profile) else { + return hermes_unconfigured(&context); + }; + let params = query + .iter() + .map(|(key, value)| format!("{}={}", url_escape(key), url_escape(value))) + .collect::>() + .join("&"); + let path = if params.is_empty() { + "/api/hermes/search/sessions".to_string() + } else { + format!("/api/hermes/search/sessions?{params}") + }; + proxy_json( + &context, + reqwest::Method::GET, + &upstream, + &path, + None, + Some(profile), + ) + .await +} + +async fn list_acp_sessions( + state: &AppState, + context: &RequestContext, + query: &HashMap, +) -> Result<(StatusCode, HeaderMap, Json), WebError> { + let user_id = effective_session_store_user_id(state, context).await?; + let workspace_id = query + .get("workspaceId") + .map(String::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned) + .or_else(|| context.workspace.workspace_id.clone()); + let document_id = query + .get("documentId") + .map(String::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned); + let session_id = query + .get("sessionId") + .map(String::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned); + let limit = query + .get("limit") + .and_then(|value| value.parse::().ok()) + .unwrap_or(50) + .clamp(1, 100); + let mut args = serde_json::Map::new(); + args.insert("userId".into(), Value::String(user_id)); + if let Some(workspace_id) = workspace_id { + args.insert("workspaceId".into(), Value::String(workspace_id)); + } + if let Some(document_id) = document_id { + args.insert("documentId".into(), Value::String(document_id)); + } + if let Some(session_id) = session_id { + args.insert("sessionId".into(), Value::String(session_id)); + } + args.insert("limit".into(), Value::from(limit)); + let sessions = execute_convex_query_by_name( + state.config(), + context, + "aiSessions:listRuntimeRuns", + Value::Object(args), + query + .get("workspaceId") + .map(String::as_str) + .or(context.workspace.workspace_id.as_deref()), + "acp_runtime_session_list", + ) + .await?; + Ok(( + StatusCode::OK, + stamp_client_headers(), + Json(json!({ + "ok": true, + "traceId": context.trace.trace_id, + "persistence": "convex_acp_runtime_store", + "sessions": sessions + })), + )) +} + pub async fn create_session( + State(state): State, Extension(context): Extension, Json(payload): Json, ) -> Result<(StatusCode, HeaderMap, Json), WebError> { @@ -144,6 +310,44 @@ pub async fn create_session( .map(str::trim) .filter(|value| !value.is_empty()) .unwrap_or("default"); + let mut persistence = "hermes_on_first_run"; + let runtime_payload = json!({ + "workspaceId": payload.workspace_id.clone(), + "documentId": payload.document_id.clone(), + "sessionId": session_id.clone(), + "traceId": trace_id.clone(), + "profile": profile, + "title": payload.title.clone(), + "actorId": context.auth.actor_id, + }); + if let Some(acp_runtime) = acp_runtime_for_payload(&runtime_payload, profile) { + let registration = HermesRunRegistration { + session_id: session_id.clone(), + profile: profile.to_string(), + document_id: document_id.to_string(), + trace_id: trace_id.clone(), + }; + let session_index_run_id = format!("{}_session", session_id); + let runtime_state = json!({ + "sessionId": session_id.clone(), + "runId": session_index_run_id.clone(), + "profile": profile, + "documentId": document_id, + "traceId": trace_id.clone(), + "status": "session.created" + }); + persist_acp_runtime_run( + &state, + &context, + ®istration, + &session_index_run_id, + &acp_runtime, + &runtime_state, + &runtime_payload, + ) + .await?; + persistence = "convex_acp_runtime_store"; + } Ok(( StatusCode::OK, stamp_client_headers(), @@ -155,7 +359,7 @@ pub async fn create_session( "profile": profile, "title": payload.title.unwrap_or_else(|| "当前页问答".into()), "traceId": trace_id, - "persistence": "hermes_on_first_run" + "persistence": persistence })), )) } @@ -467,10 +671,15 @@ pub async fn toggle_tool( } pub async fn get_session( + State(state): State, Extension(context): Extension, Path(session_id): Path, + Query(query): Query>, ) -> Result<(StatusCode, HeaderMap, Json), WebError> { ensure_authenticated(&context)?; + if is_acp_session_query(&query) { + return get_acp_session(&state, &context, &session_id, &query).await; + } let runtime = runtime_state_for_session(&session_id); if let Some(session) = load_session_from_hermes_cli(&session_id).await { return Ok(( @@ -502,10 +711,246 @@ pub async fn get_session( } pub async fn resume_session( + State(state): State, Extension(context): Extension, Path(session_id): Path, + Query(query): Query>, ) -> Result<(StatusCode, HeaderMap, Json), WebError> { - get_session(Extension(context), Path(session_id)).await + if is_acp_session_query(&query) { + let result = get_acp_session(&state, &context, &session_id, &query).await?; + let mut payload = result.2 .0; + payload["resumed"] = Value::Bool(true); + payload["resumeSource"] = Value::String("convex_acp_runtime_store".into()); + return Ok((result.0, result.1, Json(payload))); + } + get_session( + State(state), + Extension(context), + Path(session_id), + Query(query), + ) + .await +} + +pub async fn delete_session( + State(state): State, + Extension(context): Extension, + Path(session_id): Path, + Query(query): Query>, +) -> Result<(StatusCode, HeaderMap, Json), WebError> { + ensure_authenticated(&context)?; + let result = execute_acp_session_mutation( + &state, + &context, + &session_id, + &query, + "aiSessions:deleteRuntimeSession", + None, + "acp_runtime_session_delete", + ) + .await?; + Ok(( + StatusCode::OK, + stamp_client_headers(), + Json(json!({ + "ok": true, + "persistence": "convex_acp_runtime_store", + "result": result + })), + )) +} + +pub async fn rename_session( + State(state): State, + Extension(context): Extension, + Path(session_id): Path, + Query(query): Query>, + Json(payload): Json, +) -> Result<(StatusCode, HeaderMap, Json), WebError> { + ensure_authenticated(&context)?; + let title = payload.title.trim(); + if title.is_empty() { + return Err( + WebError::bad_request_code("hermes_client_bad_request", "缺少 title") + .with_context(&context) + .with_header(HEADER_MNOTE_WEB_OWNER, "mnote-web") + .with_header(HEADER_HERMES_CLIENT_OWNER, "mnote-web-hermes-client"), + ); + } + let result = execute_acp_session_mutation( + &state, + &context, + &session_id, + &query, + "aiSessions:renameRuntimeSession", + Some(json!({"title": title})), + "acp_runtime_session_rename", + ) + .await?; + Ok(( + StatusCode::OK, + stamp_client_headers(), + Json(json!({ + "ok": true, + "persistence": "convex_acp_runtime_store", + "result": result + })), + )) +} + +pub async fn auto_title_session( + State(state): State, + Extension(context): Extension, + Path(session_id): Path, + Query(query): Query>, +) -> Result<(StatusCode, HeaderMap, Json), WebError> { + ensure_authenticated(&context)?; + let result = execute_acp_session_mutation( + &state, + &context, + &session_id, + &query, + "aiSessions:autoTitleRuntimeSession", + None, + "acp_runtime_session_auto_title", + ) + .await?; + Ok(( + StatusCode::OK, + stamp_client_headers(), + Json(json!({ + "ok": true, + "persistence": "convex_acp_runtime_store", + "result": result + })), + )) +} + +async fn execute_acp_session_mutation( + state: &AppState, + context: &RequestContext, + session_id: &str, + query: &HashMap, + function_name: &str, + extra: Option, + error_phase: &'static str, +) -> Result { + let user_id = effective_session_store_user_id(state, context).await?; + let workspace_id = query + .get("workspaceId") + .map(String::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned) + .or_else(|| context.workspace.workspace_id.clone()); + let mut args = serde_json::Map::new(); + args.insert("userId".into(), Value::String(user_id)); + args.insert("sessionId".into(), Value::String(session_id.to_string())); + if let Some(workspace_id) = workspace_id.clone() { + args.insert("workspaceId".into(), Value::String(workspace_id)); + } + if let Some(Value::Object(extra)) = extra { + args.extend(extra); + } + execute_convex_mutation_by_name( + state.config(), + context, + function_name, + Value::Object(args), + workspace_id.as_deref(), + Some(session_id), + error_phase, + ) + .await +} + +fn is_acp_session_query(query: &HashMap) -> bool { + matches!(query.get("source").map(String::as_str), Some("acp")) + || query + .get("profile") + .map(String::as_str) + .map(is_acp_profile) + .unwrap_or(false) +} + +async fn get_acp_session( + state: &AppState, + context: &RequestContext, + session_id: &str, + query: &HashMap, +) -> Result<(StatusCode, HeaderMap, Json), WebError> { + let user_id = effective_session_store_user_id(state, context).await?; + let workspace_id = query + .get("workspaceId") + .map(String::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned) + .or_else(|| context.workspace.workspace_id.clone()); + let mut run_args = serde_json::Map::new(); + run_args.insert("userId".into(), Value::String(user_id.clone())); + run_args.insert("sessionId".into(), Value::String(session_id.to_string())); + if let Some(workspace_id) = workspace_id.clone() { + run_args.insert("workspaceId".into(), Value::String(workspace_id)); + } + run_args.insert("limit".into(), Value::from(20u64)); + let runs = execute_convex_query_by_name( + state.config(), + context, + "aiSessions:listRuntimeRuns", + Value::Object(run_args), + workspace_id.as_deref(), + "acp_runtime_session_detail_runs", + ) + .await?; + let latest_run = runs + .as_array() + .and_then(|rows| rows.first()) + .cloned() + .unwrap_or(Value::Null); + let events = if let Some(run_id) = latest_run + .get("runId") + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + { + execute_convex_query_by_name( + state.config(), + context, + "aiSessions:listRuntimeEvents", + json!({ + "userId": user_id, + "runId": run_id, + "limit": 200 + }), + workspace_id.as_deref(), + "acp_runtime_session_detail_events", + ) + .await? + } else { + Value::Array(Vec::new()) + }; + let runtime = latest_run + .get("runtime") + .cloned() + .unwrap_or_else(|| runtime_state_for_session(session_id)); + Ok(( + StatusCode::OK, + stamp_client_headers(), + Json(json!({ + "ok": true, + "traceId": context.trace.trace_id, + "persistence": "convex_acp_runtime_store", + "sessionId": session_id, + "session": { + "sessionId": session_id, + "messages": [], + "runs": runs + }, + "runtime": runtime, + "events": events + })), + )) } async fn load_session_from_hermes_cli(session_id: &str) -> Option { @@ -543,7 +988,7 @@ pub async fn create_run( let registration = run_registration_from_payload(&context, &payload); // ACP path: skip the HTTP proxy, just register and return run info - if acp_runtime_for_payload(&payload, ®istration.profile).is_some() { + if let Some(acp_runtime) = acp_runtime_for_payload(&payload, ®istration.profile) { let run_id = payload .get("runId") .and_then(Value::as_str) @@ -557,6 +1002,16 @@ pub async fn create_run( .lock() .expect("acp run payloads") .insert(run_id.clone(), payload.clone()); + persist_acp_runtime_run( + &state, + &context, + ®istration, + &run_id, + &acp_runtime, + &runtime_state, + &payload, + ) + .await?; let response = json!({ "ok": true, "runId": run_id, @@ -623,17 +1078,13 @@ async fn acp_stream_events( acp_runtime_name: &str, ) -> Result { // Get the stored payload from create_run - let payload = ACP_RUN_PAYLOADS - .lock() - .expect("acp run payloads") - .remove(run_id) - .ok_or_else(|| { - WebError::bad_gateway_code( - "acp_run_payload_not_found", - format!("ACP run payload not found for run_id={run_id}"), - ) - .with_context(&context) - })?; + let payload = acp_run_payload_for_stream(run_id).ok_or_else(|| { + WebError::bad_gateway_code( + "acp_run_payload_not_found", + format!("ACP run payload not found for run_id={run_id}"), + ) + .with_context(&context) + })?; // Build prompt from the message field (frontend sends "message", not "input") let input = payload @@ -702,7 +1153,7 @@ async fn acp_stream_events( run_id.to_string(), AcpActiveRun { manager: Arc::clone(&mgr), - mnote_session_id, + mnote_session_id: mnote_session_id.clone(), acp_session_id, }, ); @@ -712,11 +1163,31 @@ async fn acp_stream_events( use tokio::sync::mpsc; let (tx, rx) = mpsc::channel::>(256); let mut event_rx = event_tx.subscribe(); + let state_for_events = state.clone(); + let context_for_events = context.clone(); + let event_registration = run_registration_from_payload(&context, &payload); + let event_payload = payload.clone(); + let run_id_for_events = run_id.to_string(); + let acp_runtime_for_events = acp_runtime_name.to_string(); tokio::spawn(async move { loop { match event_rx.recv().await { Ok(event) => { + if let Err(error) = persist_acp_runtime_event( + &state_for_events, + &context_for_events, + &event_registration, + &run_id_for_events, + &acp_runtime_for_events, + &event.event, + &event.data, + &event_payload, + ) + .await + { + warn!(error = ?error, run_id = %run_id_for_events, event = %event.event, "ACP runtime event 持久化失败"); + } let json_str = serde_json::to_string(&event.data).unwrap_or_else(|_| "{}".into()); let bytes = axum::body::Bytes::from(format!( @@ -739,9 +1210,29 @@ async fn acp_stream_events( let run_id_owned = run_id.to_string(); let mgr_clone = Arc::clone(&mgr); let event_tx_prompt = event_tx.clone(); + let mnote_tool_context = crate::acp_session_manager::AcpMnoteToolContext { + mnote_session_id: Some(mnote_session_id.clone()), + run_id: Some(run_id.to_string()), + actor_id: payload + .get("actorId") + .and_then(Value::as_str) + .map(ToOwned::to_owned), + trace_id: Some(context.trace.trace_id.clone()), + workspace_id: payload + .get("workspaceId") + .and_then(Value::as_str) + .map(ToOwned::to_owned), + document_id: payload + .get("documentId") + .and_then(Value::as_str) + .map(ToOwned::to_owned), + }; update_runtime_by_run_id(&run_id_owned, "running", Some("acp.prompt.started"), None); tokio::spawn(async move { - match mgr_clone.run_prompt(prompt_blocks).await { + match mgr_clone + .run_prompt_with_mnote_context(prompt_blocks, Some(mnote_tool_context)) + .await + { Ok(result) => { info!("ACP prompt completed: stop_reason={:?}", result.stop_reason); let _ = event_tx_prompt.send(crate::acp_bridge::SseEvent { @@ -1951,6 +2442,14 @@ fn acp_runtime_for_run(run_id: &str, profile: &str) -> Option { None } +fn acp_run_payload_for_stream(run_id: &str) -> Option { + ACP_RUN_PAYLOADS + .lock() + .expect("acp run payloads") + .get(run_id) + .cloned() +} + fn provider_default_key_env(provider: &str) -> Option<&'static str> { match provider.trim().to_ascii_lowercase().as_str() { "deepseek" => Some("DEEPSEEK_API_KEY"), @@ -2354,13 +2853,15 @@ fn build_run_upstream_body(context: &RequestContext, payload: Value) -> Result Result Result { + let actor_id = context.auth.actor_id.trim(); + if !actor_id.is_empty() && actor_id != "anonymous" { + return Ok(actor_id.to_string()); + } + if let Some(user_id) = resolve_current_convex_user_id(state, context).await? { + return Ok(user_id); + } + Err(WebError::new( + StatusCode::UNAUTHORIZED, + "hermes_client_unauthorized", + "页面 AI session store 需要登录后访问", + ) + .with_context(context) + .with_header(HEADER_MNOTE_WEB_OWNER, "mnote-web") + .with_header(HEADER_HERMES_CLIENT_OWNER, "mnote-web-hermes-client")) +} + fn stamp_run_actor(payload: &mut Value, actor_id: &str, actor_type: &str) { let Value::Object(map) = payload else { return; @@ -2626,6 +3149,167 @@ fn register_acp_runtime(registration: &HermesRunRegistration, run_id: &str) -> V json } +fn acp_runtime_run_store_args( + context: &RequestContext, + registration: &HermesRunRegistration, + run_id: &str, + acp_runtime: &str, + runtime_state: &Value, + payload: &Value, +) -> Value { + let workspace_id = payload + .get("workspaceId") + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned) + .or_else(|| context.workspace.workspace_id.clone()); + json!({ + "schema": "mnote.acp_runtime_run.v1", + "source": "acp", + "userId": runtime_store_user_id(context, payload), + "workspaceId": workspace_id, + "documentId": registration.document_id, + "sessionId": registration.session_id, + "runId": run_id, + "title": payload + .get("title") + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()), + "profile": registration.profile, + "acpRuntime": acp_runtime, + "traceId": registration.trace_id, + "status": runtime_state + .get("status") + .and_then(Value::as_str) + .unwrap_or("acp_pending"), + "runtime": runtime_state, + "payload": payload, + "retention": { + "kind": "ttl", + "ttlMs": 7 * 24 * 60 * 60 * 1000i64 + }, + "createdAt": now_ms(), + "updatedAt": now_ms() + }) +} + +fn runtime_store_user_id(context: &RequestContext, payload: &Value) -> String { + payload + .get("actorId") + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty() && *value != "anonymous") + .map(ToOwned::to_owned) + .unwrap_or_else(|| context.auth.actor_id.clone()) +} + +async fn persist_acp_runtime_run( + state: &AppState, + context: &RequestContext, + registration: &HermesRunRegistration, + run_id: &str, + acp_runtime: &str, + runtime_state: &Value, + payload: &Value, +) -> Result { + let args = acp_runtime_run_store_args( + context, + registration, + run_id, + acp_runtime, + runtime_state, + payload, + ); + execute_convex_mutation_by_name( + state.config(), + context, + ACP_RUNTIME_RUN_MUTATION, + args, + payload + .get("workspaceId") + .and_then(Value::as_str) + .or(context.workspace.workspace_id.as_deref()), + Some(run_id), + "acp_runtime_run_store", + ) + .await +} + +fn acp_runtime_event_store_args( + context: &RequestContext, + registration: &HermesRunRegistration, + run_id: &str, + acp_runtime: &str, + event_type: &str, + event_payload: &Value, + run_payload: &Value, +) -> Value { + let workspace_id = run_payload + .get("workspaceId") + .and_then(Value::as_str) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned) + .or_else(|| context.workspace.workspace_id.clone()); + let created_at = now_ms(); + json!({ + "schema": "mnote.acp_runtime_event.v1", + "source": "acp", + "eventId": format!( + "acp_event:{}:{}:{}", + sanitize_id_part(run_id), + created_at, + sanitize_id_part(event_type) + ), + "userId": runtime_store_user_id(context, run_payload), + "workspaceId": workspace_id, + "documentId": registration.document_id, + "sessionId": registration.session_id, + "runId": run_id, + "profile": registration.profile, + "acpRuntime": acp_runtime, + "eventType": event_type, + "payload": event_payload, + "createdAt": created_at + }) +} + +async fn persist_acp_runtime_event( + state: &AppState, + context: &RequestContext, + registration: &HermesRunRegistration, + run_id: &str, + acp_runtime: &str, + event_type: &str, + event_payload: &Value, + run_payload: &Value, +) -> Result { + let args = acp_runtime_event_store_args( + context, + registration, + run_id, + acp_runtime, + event_type, + event_payload, + run_payload, + ); + execute_convex_mutation_by_name( + state.config(), + context, + ACP_RUNTIME_EVENT_MUTATION, + args, + run_payload + .get("workspaceId") + .and_then(Value::as_str) + .or(context.workspace.workspace_id.as_deref()), + None, + "acp_runtime_event_store", + ) + .await +} + fn register_runtime_from_create_run_response( registration: &HermesRunRegistration, payload: &Value, @@ -3278,8 +3962,7 @@ mod tests { use tower::util::ServiceExt; fn env_lock() -> &'static Mutex<()> { - static LOCK: OnceLock> = OnceLock::new(); - LOCK.get_or_init(|| Mutex::new(())) + crate::test_support::hermes_env_lock() } fn runtime_lock() -> &'static Mutex<()> { @@ -3298,8 +3981,12 @@ mod tests { HERMES_RUN_QUEUE.lock().expect("hermes run queue").clear(); } + fn clear_acp_run_payloads() { + ACP_RUN_PAYLOADS.lock().expect("acp run payloads").clear(); + } + fn app() -> axum::Router { - build_app(AppState::new(AppConfig { + app_with_config(AppConfig { service_name: "mnote-web".into(), service_version: "0.1.0".into(), bind_addr: "127.0.0.1:0".into(), @@ -3318,7 +4005,11 @@ mod tests { dev_user_id: "dev-user".into(), dev_user_name: "开发用户".into(), dev_user_email: "dev@mnote.local".into(), - })) + }) + } + + fn app_with_config(config: AppConfig) -> axum::Router { + build_app(AppState::new(config)) } fn test_state() -> AppState { @@ -3625,6 +4316,685 @@ mod tests { assert!(payload.get("messages").is_none()); } + #[tokio::test] + async fn hermes_client_acp_session_create_indexes_session_in_convex_store() { + let captured_body = Arc::new(Mutex::new(Value::Null)); + let captured_for_route = Arc::clone(&captured_body); + let mock = axum::Router::new().route( + "/api/mutation", + post(move |Json(body): Json| { + let captured = Arc::clone(&captured_for_route); + async move { + *captured.lock().expect("captured convex body") = body; + Json(json!({ + "status": "success", + "value": {"ok": true, "created": true} + })) + } + }), + ); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("listener"); + let convex_url = format!("http://{}", listener.local_addr().expect("addr")); + tokio::spawn(async move { + axum::serve(listener, mock).await.expect("mock convex"); + }); + + let response = app_with_config(AppConfig { + service_name: "mnote-web".into(), + service_version: "0.1.0".into(), + bind_addr: "127.0.0.1:0".into(), + public_bind_addr: "127.0.0.1:3000".into(), + legacy_next_base_url: Some("http://127.0.0.1:3100".into()), + enable_legacy_next_compat: true, + enable_debug_shell_routes: false, + enable_editor_actor: true, + hermes_base_path: "/api/hermes".into(), + compat_next_base_path: "/api/compat/next".into(), + convex_url: Some(convex_url), + convex_admin_key: Some("admin-demo".into()), + allow_dev_fixtures: false, + query_fixtures_json: None, + mutation_fixtures_json: None, + dev_user_id: "dev-user".into(), + dev_user_name: "开发用户".into(), + dev_user_email: "dev@mnote.local".into(), + }) + .oneshot( + Request::builder() + .method("POST") + .uri("/api/hermes/client/sessions") + .header("content-type", "application/json") + .header("x-mnote-actor-id", "user_1") + .body(Body::from( + json!({ + "workspaceId": "ws_1", + "documentId": "doc_1", + "traceId": "trace_1", + "title": "当前页问答", + "profile": "reasonix" + }) + .to_string(), + )) + .expect("request"), + ) + .await + .expect("response"); + assert_eq!(response.status(), StatusCode::OK); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let payload: Value = serde_json::from_slice(&body).expect("json"); + assert_eq!(payload["persistence"], "convex_acp_runtime_store"); + + let mutation = captured_body.lock().expect("captured convex body").clone(); + assert_eq!(mutation["path"], "aiSessions:upsertRuntimeRun"); + assert_eq!(mutation["args"][0]["userId"], "user_1"); + assert_eq!(mutation["args"][0]["workspaceId"], "ws_1"); + assert_eq!(mutation["args"][0]["documentId"], "doc_1"); + assert_eq!(mutation["args"][0]["sessionId"], "mnote_doc_1_trace_1"); + assert_eq!(mutation["args"][0]["runId"], "mnote_doc_1_trace_1_session"); + assert_eq!(mutation["args"][0]["status"], "session.created"); + } + + #[tokio::test] + async fn hermes_client_acp_run_registers_scoped_runtime_record_in_convex() { + let _guard = runtime_lock().lock().expect("runtime lock"); + clear_runtime_registry(); + clear_run_queue(); + + let captured_body = Arc::new(Mutex::new(Value::Null)); + let captured_for_route = Arc::clone(&captured_body); + let mock = axum::Router::new().route( + "/api/mutation", + post(move |Json(body): Json| { + let captured = Arc::clone(&captured_for_route); + async move { + *captured.lock().expect("captured convex body") = body; + Json(json!({ + "status": "success", + "value": {"ok": true, "stored": true} + })) + } + }), + ); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("listener"); + let convex_url = format!("http://{}", listener.local_addr().expect("addr")); + tokio::spawn(async move { + axum::serve(listener, mock).await.expect("mock convex"); + }); + + let response = app_with_config(AppConfig { + service_name: "mnote-web".into(), + service_version: "0.1.0".into(), + bind_addr: "127.0.0.1:0".into(), + public_bind_addr: "127.0.0.1:3000".into(), + legacy_next_base_url: Some("http://127.0.0.1:3100".into()), + enable_legacy_next_compat: true, + enable_debug_shell_routes: false, + enable_editor_actor: true, + hermes_base_path: "/api/hermes".into(), + compat_next_base_path: "/api/compat/next".into(), + convex_url: Some(convex_url), + convex_admin_key: Some("admin-demo".into()), + allow_dev_fixtures: false, + query_fixtures_json: None, + mutation_fixtures_json: None, + dev_user_id: "dev-user".into(), + dev_user_name: "开发用户".into(), + dev_user_email: "dev@mnote.local".into(), + }) + .oneshot( + Request::builder() + .method("POST") + .uri("/api/hermes/client/runs") + .header("content-type", "application/json") + .header("x-mnote-actor-id", "user_1") + .header("x-mnote-workspace-id", "ws_header") + .body(Body::from( + json!({ + "workspaceId": "ws_1", + "documentId": "doc_1", + "sessionId": "sess_1", + "message": "读取当前页面", + "profile": "reasonix", + "acpRuntime": "reasonix", + "runId": "run_trace_1", + "traceId": "trace_1", + "pageContext": {"title": "页面标题"} + }) + .to_string(), + )) + .expect("request"), + ) + .await + .expect("response"); + assert_eq!(response.status(), StatusCode::OK); + + let body = captured_body.lock().expect("captured convex body").clone(); + assert_eq!(body["path"], "aiSessions:upsertRuntimeRun"); + let args = &body["args"][0]; + assert_eq!(args["schema"], "mnote.acp_runtime_run.v1"); + assert_eq!(args["source"], "acp"); + assert_eq!(args["userId"], "user_1"); + assert_eq!(args["workspaceId"], "ws_1"); + assert_eq!(args["documentId"], "doc_1"); + assert_eq!(args["sessionId"], "sess_1"); + assert_eq!(args["runId"], "run_trace_1"); + assert_eq!(args["profile"], "reasonix"); + assert_eq!(args["acpRuntime"], "reasonix"); + assert_eq!(args["runtime"]["status"], "acp_pending"); + assert_eq!(args["payload"]["message"], "读取当前页面"); + assert!(args.get("messages").is_none()); + } + + #[tokio::test] + async fn hermes_client_acp_session_list_reads_current_user_convex_store() { + let captured_body = Arc::new(Mutex::new(Value::Null)); + let captured_for_route = Arc::clone(&captured_body); + let mock = axum::Router::new().route( + "/api/query", + get(|| async { StatusCode::METHOD_NOT_ALLOWED }).post( + move |Json(body): Json| { + let captured = Arc::clone(&captured_for_route); + async move { + *captured.lock().expect("captured convex body") = body; + Json(json!({ + "status": "success", + "value": [ + { + "sessionId": "sess_1", + "runId": "run_1", + "workspaceId": "ws_1", + "documentId": "doc_1", + "profile": "reasonix", + "status": "completed" + } + ] + })) + } + }, + ), + ); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("listener"); + let convex_url = format!("http://{}", listener.local_addr().expect("addr")); + tokio::spawn(async move { + axum::serve(listener, mock).await.expect("mock convex"); + }); + + let response = app_with_config(AppConfig { + service_name: "mnote-web".into(), + service_version: "0.1.0".into(), + bind_addr: "127.0.0.1:0".into(), + public_bind_addr: "127.0.0.1:3000".into(), + legacy_next_base_url: Some("http://127.0.0.1:3100".into()), + enable_legacy_next_compat: true, + enable_debug_shell_routes: false, + enable_editor_actor: true, + hermes_base_path: "/api/hermes".into(), + compat_next_base_path: "/api/compat/next".into(), + convex_url: Some(convex_url), + convex_admin_key: Some("admin-demo".into()), + allow_dev_fixtures: false, + query_fixtures_json: None, + mutation_fixtures_json: None, + dev_user_id: "dev-user".into(), + dev_user_name: "开发用户".into(), + dev_user_email: "dev@mnote.local".into(), + }) + .oneshot( + Request::builder() + .method("GET") + .uri("/api/hermes/client/sessions?profile=reasonix&workspaceId=ws_1&documentId=doc_1") + .header("x-mnote-actor-id", "user_1") + .body(Body::empty()) + .expect("request"), + ) + .await + .expect("response"); + assert_eq!(response.status(), StatusCode::OK); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let payload: Value = serde_json::from_slice(&body).expect("json"); + assert_eq!(payload["ok"], true); + assert_eq!(payload["sessions"][0]["sessionId"], "sess_1"); + assert_eq!(payload["persistence"], "convex_acp_runtime_store"); + + let query_body = captured_body.lock().expect("captured convex body").clone(); + assert_eq!(query_body["path"], "aiSessions:listRuntimeRuns"); + assert_eq!(query_body["args"]["userId"], "user_1"); + assert_eq!(query_body["args"]["workspaceId"], "ws_1"); + assert_eq!(query_body["args"]["documentId"], "doc_1"); + } + + #[tokio::test] + async fn hermes_client_acp_session_detail_reads_runtime_runs_and_events() { + let captured_bodies = Arc::new(Mutex::new(Vec::::new())); + let captured_for_route = Arc::clone(&captured_bodies); + let mock = axum::Router::new().route( + "/api/query", + post(move |Json(body): Json| { + let captured = Arc::clone(&captured_for_route); + async move { + captured + .lock() + .expect("captured convex bodies") + .push(body.clone()); + let path = body["path"].as_str().unwrap_or_default(); + let value = match path { + "aiSessions:listRuntimeRuns" => json!([ + { + "sessionId": "sess_1", + "runId": "run_1", + "workspaceId": "ws_1", + "documentId": "doc_1", + "profile": "reasonix", + "status": "completed", + "runtime": {"status": "completed", "runId": "run_1"} + } + ]), + "aiSessions:listRuntimeEvents" => json!([ + { + "eventType": "message.delta", + "payload": {"text": "hello"} + } + ]), + _ => Value::Null, + }; + Json(json!({"status": "success", "value": value})) + } + }), + ); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("listener"); + let convex_url = format!("http://{}", listener.local_addr().expect("addr")); + tokio::spawn(async move { + axum::serve(listener, mock).await.expect("mock convex"); + }); + + let response = app_with_config(AppConfig { + service_name: "mnote-web".into(), + service_version: "0.1.0".into(), + bind_addr: "127.0.0.1:0".into(), + public_bind_addr: "127.0.0.1:3000".into(), + legacy_next_base_url: Some("http://127.0.0.1:3100".into()), + enable_legacy_next_compat: true, + enable_debug_shell_routes: false, + enable_editor_actor: true, + hermes_base_path: "/api/hermes".into(), + compat_next_base_path: "/api/compat/next".into(), + convex_url: Some(convex_url), + convex_admin_key: Some("admin-demo".into()), + allow_dev_fixtures: false, + query_fixtures_json: None, + mutation_fixtures_json: None, + dev_user_id: "dev-user".into(), + dev_user_name: "开发用户".into(), + dev_user_email: "dev@mnote.local".into(), + }) + .oneshot( + Request::builder() + .method("GET") + .uri("/api/hermes/client/sessions/sess_1?source=acp&workspaceId=ws_1") + .header("x-mnote-actor-id", "user_1") + .body(Body::empty()) + .expect("request"), + ) + .await + .expect("response"); + assert_eq!(response.status(), StatusCode::OK); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let payload: Value = serde_json::from_slice(&body).expect("json"); + assert_eq!(payload["ok"], true); + assert_eq!(payload["persistence"], "convex_acp_runtime_store"); + assert_eq!(payload["session"]["sessionId"], "sess_1"); + assert_eq!(payload["session"]["runs"][0]["runId"], "run_1"); + assert_eq!(payload["runtime"]["runId"], "run_1"); + assert_eq!(payload["events"][0]["eventType"], "message.delta"); + assert_eq!( + payload["session"]["messages"].as_array().map(Vec::len), + Some(0) + ); + + let bodies = captured_bodies.lock().expect("captured convex bodies"); + assert_eq!(bodies[0]["path"], "aiSessions:listRuntimeRuns"); + assert_eq!(bodies[0]["args"]["userId"], "user_1"); + assert_eq!(bodies[0]["args"]["sessionId"], "sess_1"); + assert_eq!(bodies[1]["path"], "aiSessions:listRuntimeEvents"); + assert_eq!(bodies[1]["args"]["runId"], "run_1"); + } + + #[tokio::test] + async fn hermes_client_acp_resume_marks_convex_runtime_history_source() { + let mock = axum::Router::new().route( + "/api/query", + post(|Json(body): Json| async move { + let path = body["path"].as_str().unwrap_or_default(); + let value = match path { + "aiSessions:listRuntimeRuns" => json!([ + { + "sessionId": "sess_1", + "runId": "run_1", + "workspaceId": "ws_1", + "documentId": "doc_1", + "profile": "reasonix", + "status": "completed", + "runtime": {"status": "completed", "runId": "run_1"} + } + ]), + "aiSessions:listRuntimeEvents" => json!([]), + _ => Value::Null, + }; + Json(json!({"status": "success", "value": value})) + }), + ); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("listener"); + let convex_url = format!("http://{}", listener.local_addr().expect("addr")); + tokio::spawn(async move { + axum::serve(listener, mock).await.expect("mock convex"); + }); + + let response = app_with_config(AppConfig { + service_name: "mnote-web".into(), + service_version: "0.1.0".into(), + bind_addr: "127.0.0.1:0".into(), + public_bind_addr: "127.0.0.1:3000".into(), + legacy_next_base_url: Some("http://127.0.0.1:3100".into()), + enable_legacy_next_compat: true, + enable_debug_shell_routes: false, + enable_editor_actor: true, + hermes_base_path: "/api/hermes".into(), + compat_next_base_path: "/api/compat/next".into(), + convex_url: Some(convex_url), + convex_admin_key: Some("admin-demo".into()), + allow_dev_fixtures: false, + query_fixtures_json: None, + mutation_fixtures_json: None, + dev_user_id: "dev-user".into(), + dev_user_name: "开发用户".into(), + dev_user_email: "dev@mnote.local".into(), + }) + .oneshot( + Request::builder() + .method("POST") + .uri("/api/hermes/client/sessions/sess_1/resume?source=acp&workspaceId=ws_1") + .header("x-mnote-actor-id", "user_1") + .body(Body::empty()) + .expect("request"), + ) + .await + .expect("response"); + assert_eq!(response.status(), StatusCode::OK); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let payload: Value = serde_json::from_slice(&body).expect("json"); + assert_eq!(payload["ok"], true); + assert_eq!(payload["resumed"], true); + assert_eq!(payload["resumeSource"], "convex_acp_runtime_store"); + assert_eq!(payload["session"]["runs"][0]["runId"], "run_1"); + } + + #[tokio::test] + async fn hermes_client_acp_session_rename_calls_convex_store() { + let captured_body = Arc::new(Mutex::new(Value::Null)); + let captured_for_route = Arc::clone(&captured_body); + let mock = axum::Router::new().route( + "/api/mutation", + post(move |Json(body): Json| { + let captured = Arc::clone(&captured_for_route); + async move { + *captured.lock().expect("captured convex body") = body; + Json(json!({ + "status": "success", + "value": {"ok": true, "sessionId": "sess_1", "title": "新标题"} + })) + } + }), + ); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("listener"); + let convex_url = format!("http://{}", listener.local_addr().expect("addr")); + tokio::spawn(async move { + axum::serve(listener, mock).await.expect("mock convex"); + }); + + let response = app_with_config(AppConfig { + service_name: "mnote-web".into(), + service_version: "0.1.0".into(), + bind_addr: "127.0.0.1:0".into(), + public_bind_addr: "127.0.0.1:3000".into(), + legacy_next_base_url: Some("http://127.0.0.1:3100".into()), + enable_legacy_next_compat: true, + enable_debug_shell_routes: false, + enable_editor_actor: true, + hermes_base_path: "/api/hermes".into(), + compat_next_base_path: "/api/compat/next".into(), + convex_url: Some(convex_url), + convex_admin_key: Some("admin-demo".into()), + allow_dev_fixtures: false, + query_fixtures_json: None, + mutation_fixtures_json: None, + dev_user_id: "dev-user".into(), + dev_user_name: "开发用户".into(), + dev_user_email: "dev@mnote.local".into(), + }) + .oneshot( + Request::builder() + .method("POST") + .uri("/api/hermes/client/sessions/sess_1/rename?source=acp&workspaceId=ws_1") + .header("content-type", "application/json") + .header("x-mnote-actor-id", "user_1") + .body(Body::from(json!({"title": "新标题"}).to_string())) + .expect("request"), + ) + .await + .expect("response"); + assert_eq!(response.status(), StatusCode::OK); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let payload: Value = serde_json::from_slice(&body).expect("json"); + assert_eq!(payload["ok"], true); + assert_eq!(payload["result"]["title"], "新标题"); + + let mutation = captured_body.lock().expect("captured convex body").clone(); + assert_eq!(mutation["path"], "aiSessions:renameRuntimeSession"); + assert_eq!(mutation["args"][0]["userId"], "user_1"); + assert_eq!(mutation["args"][0]["workspaceId"], "ws_1"); + assert_eq!(mutation["args"][0]["sessionId"], "sess_1"); + assert_eq!(mutation["args"][0]["title"], "新标题"); + } + + #[tokio::test] + async fn hermes_client_acp_session_delete_and_auto_title_call_convex_store() { + let captured_bodies = Arc::new(Mutex::new(Vec::::new())); + let captured_for_route = Arc::clone(&captured_bodies); + let mock = axum::Router::new().route( + "/api/mutation", + post(move |Json(body): Json| { + let captured = Arc::clone(&captured_for_route); + async move { + captured + .lock() + .expect("captured convex bodies") + .push(body.clone()); + let path = body["path"].as_str().unwrap_or_default(); + let value = match path { + "aiSessions:autoTitleRuntimeSession" => { + json!({"ok": true, "sessionId": "sess_1", "title": "自动标题"}) + } + "aiSessions:deleteRuntimeSession" => { + json!({"ok": true, "sessionId": "sess_1", "deleted": 1}) + } + _ => Value::Null, + }; + Json(json!({"status": "success", "value": value})) + } + }), + ); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("listener"); + let convex_url = format!("http://{}", listener.local_addr().expect("addr")); + tokio::spawn(async move { + axum::serve(listener, mock).await.expect("mock convex"); + }); + let router = app_with_config(AppConfig { + service_name: "mnote-web".into(), + service_version: "0.1.0".into(), + bind_addr: "127.0.0.1:0".into(), + public_bind_addr: "127.0.0.1:3000".into(), + legacy_next_base_url: Some("http://127.0.0.1:3100".into()), + enable_legacy_next_compat: true, + enable_debug_shell_routes: false, + enable_editor_actor: true, + hermes_base_path: "/api/hermes".into(), + compat_next_base_path: "/api/compat/next".into(), + convex_url: Some(convex_url), + convex_admin_key: Some("admin-demo".into()), + allow_dev_fixtures: false, + query_fixtures_json: None, + mutation_fixtures_json: None, + dev_user_id: "dev-user".into(), + dev_user_name: "开发用户".into(), + dev_user_email: "dev@mnote.local".into(), + }); + + let response = router + .clone() + .oneshot( + Request::builder() + .method("POST") + .uri( + "/api/hermes/client/sessions/sess_1/auto-title?source=acp&workspaceId=ws_1", + ) + .header("x-mnote-actor-id", "user_1") + .body(Body::empty()) + .expect("request"), + ) + .await + .expect("response"); + assert_eq!(response.status(), StatusCode::OK); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let payload: Value = serde_json::from_slice(&body).expect("json"); + assert_eq!(payload["result"]["title"], "自动标题"); + + let response = router + .oneshot( + Request::builder() + .method("DELETE") + .uri("/api/hermes/client/sessions/sess_1?source=acp&workspaceId=ws_1") + .header("x-mnote-actor-id", "user_1") + .body(Body::empty()) + .expect("request"), + ) + .await + .expect("response"); + assert_eq!(response.status(), StatusCode::OK); + + let bodies = captured_bodies.lock().expect("captured convex bodies"); + assert_eq!(bodies[0]["path"], "aiSessions:autoTitleRuntimeSession"); + assert_eq!(bodies[0]["args"][0]["userId"], "user_1"); + assert_eq!(bodies[0]["args"][0]["sessionId"], "sess_1"); + assert_eq!(bodies[1]["path"], "aiSessions:deleteRuntimeSession"); + assert_eq!(bodies[1]["args"][0]["workspaceId"], "ws_1"); + } + + #[tokio::test] + async fn hermes_client_acp_session_search_returns_convex_snippets() { + let captured_body = Arc::new(Mutex::new(Value::Null)); + let captured_for_route = Arc::clone(&captured_body); + let mock = axum::Router::new().route( + "/api/query", + post(move |Json(body): Json| { + let captured = Arc::clone(&captured_for_route); + async move { + *captured.lock().expect("captured convex body") = body; + Json(json!({ + "status": "success", + "value": [ + { + "sessionId": "sess_1", + "runId": "run_1", + "title": "化学总结", + "snippet": "帮我总结化学页面", + "score": 1 + } + ] + })) + } + }), + ); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("listener"); + let convex_url = format!("http://{}", listener.local_addr().expect("addr")); + tokio::spawn(async move { + axum::serve(listener, mock).await.expect("mock convex"); + }); + + let response = app_with_config(AppConfig { + service_name: "mnote-web".into(), + service_version: "0.1.0".into(), + bind_addr: "127.0.0.1:0".into(), + public_bind_addr: "127.0.0.1:3000".into(), + legacy_next_base_url: Some("http://127.0.0.1:3100".into()), + enable_legacy_next_compat: true, + enable_debug_shell_routes: false, + enable_editor_actor: true, + hermes_base_path: "/api/hermes".into(), + compat_next_base_path: "/api/compat/next".into(), + convex_url: Some(convex_url), + convex_admin_key: Some("admin-demo".into()), + allow_dev_fixtures: false, + query_fixtures_json: None, + mutation_fixtures_json: None, + dev_user_id: "dev-user".into(), + dev_user_name: "开发用户".into(), + dev_user_email: "dev@mnote.local".into(), + }) + .oneshot( + Request::builder() + .method("GET") + .uri("/api/hermes/client/sessions/search?source=acp&workspaceId=ws_1&q=%E5%8C%96%E5%AD%A6&limit=5") + .header("x-mnote-actor-id", "user_1") + .body(Body::empty()) + .expect("request"), + ) + .await + .expect("response"); + assert_eq!(response.status(), StatusCode::OK); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let payload: Value = serde_json::from_slice(&body).expect("json"); + assert_eq!(payload["ok"], true); + assert_eq!(payload["results"][0]["sessionId"], "sess_1"); + assert_eq!(payload["results"][0]["snippet"], "帮我总结化学页面"); + + let query_body = captured_body.lock().expect("captured convex body").clone(); + assert_eq!(query_body["path"], "aiSessions:searchRuntimeSessions"); + assert_eq!(query_body["args"]["userId"], "user_1"); + assert_eq!(query_body["args"]["workspaceId"], "ws_1"); + assert_eq!(query_body["args"]["q"], "化学"); + assert_eq!(query_body["args"]["limit"], 5); + } + #[tokio::test] async fn hermes_client_resume_returns_runtime_state_without_chat_store() { let _guard = runtime_lock().lock().expect("runtime lock"); @@ -3738,6 +5108,61 @@ mod tests { assert_eq!(queue_len_for_session("mnote_doc_queue_trace_1"), 0); } + #[test] + fn hermes_client_acp_stream_payload_lookup_keeps_payload_for_reconnect() { + let _guard = runtime_lock().lock().expect("runtime lock"); + clear_acp_run_payloads(); + ACP_RUN_PAYLOADS + .lock() + .expect("acp run payloads") + .insert("run_keep_1".into(), json!({"message": "hello"})); + + let first = acp_run_payload_for_stream("run_keep_1").expect("first payload"); + let second = acp_run_payload_for_stream("run_keep_1").expect("second payload"); + + assert_eq!(first["message"], "hello"); + assert_eq!(second["message"], "hello"); + } + + #[test] + fn hermes_client_acp_runtime_event_store_args_are_user_scoped() { + let mut headers = HeaderMap::new(); + headers.insert("x-mnote-actor-id", "user_1".parse().unwrap()); + headers.insert("x-mnote-workspace-id", "ws_header".parse().unwrap()); + let context = RequestContext::from_http_parts( + &axum::http::Method::GET, + &"/api/hermes/client/events/run_1".parse().expect("uri"), + &headers, + ); + let registration = HermesRunRegistration { + session_id: "sess_1".into(), + profile: "reasonix".into(), + document_id: "doc_1".into(), + trace_id: "trace_1".into(), + }; + let args = acp_runtime_event_store_args( + &context, + ®istration, + "run_1", + "reasonix", + "message.delta", + &json!({"text": "hello"}), + &json!({"workspaceId": "ws_1"}), + ); + + assert_eq!(args["schema"], "mnote.acp_runtime_event.v1"); + assert_eq!(args["userId"], "user_1"); + assert_eq!(args["workspaceId"], "ws_1"); + assert_eq!(args["documentId"], "doc_1"); + assert_eq!(args["sessionId"], "sess_1"); + assert_eq!(args["runId"], "run_1"); + assert_eq!(args["profile"], "reasonix"); + assert_eq!(args["acpRuntime"], "reasonix"); + assert_eq!(args["eventType"], "message.delta"); + assert_eq!(args["payload"]["text"], "hello"); + assert!(args.get("messages").is_none()); + } + #[test] fn hermes_client_cancel_queued_run_removes_pending_item() { let _guard = runtime_lock().lock().expect("runtime lock"); @@ -4157,6 +5582,39 @@ mod tests { assert!(instructions.contains("\"profile\":\"chemist\"")); } + #[test] + fn hermes_client_run_guidance_prefers_markdown_edit_for_plain_body_edits() { + let context = RequestContext::from_http_parts( + &axum::http::Method::POST, + &"/api/hermes/client/runs".parse().expect("uri"), + &HeaderMap::new(), + ); + let body = build_run_upstream_body( + &context, + json!({ + "workspaceId": "ws_1", + "documentId": "doc_1", + "sessionId": "sess_1", + "message": "把第一段改成测试123", + "pageContext": { + "aiContext": { + "schema": "mnote.page_ai_context.v1", + "pageText": "第一段\n第二段", + "contextBlocks": [{"blockId": "block_1", "text": "第一段"}] + } + }, + "traceId": "trace_1" + }), + ) + .expect("body"); + let instructions = body["instructions"].as_str().expect("instructions"); + assert!(instructions.contains("普通正文 search/replace、局部段落替换或全文 markdown 替换,应优先调用 mnote_doc_markdown_edit")); + assert!(instructions.contains("\"mnote_doc_markdown_edit for plain body search/replace or full markdown replacement\"")); + assert!(!instructions.contains( + "简单小段落编辑或同一页内多个普通叶子块操作,应优先用 mnote_doc_apply_block_ops" + )); + } + #[tokio::test] async fn hermes_client_profile_skill_and_memory_routes_use_local_bff_without_upstream() { let _guard = env_lock().lock().expect("env lock"); diff --git a/rust/crates/mnote-web/src/routes/hermes_tools.rs b/rust/crates/mnote-web/src/routes/hermes_tools.rs index cbfd720c..6d1365e5 100644 --- a/rust/crates/mnote-web/src/routes/hermes_tools.rs +++ b/rust/crates/mnote-web/src/routes/hermes_tools.rs @@ -66,7 +66,16 @@ pub async fn mnote_call( Extension(context): Extension, Json(input): Json, ) -> Result<(StatusCode, HeaderMap, Json), WebError> { - let context = authenticated_tool_context(&context, &input)?; + let response_body = execute_mnote_tool_call(&state, &context, input).await?; + Ok((StatusCode::OK, stamp_tool_headers(), Json(response_body))) +} + +pub(crate) async fn execute_mnote_tool_call( + state: &AppState, + context: &RequestContext, + input: ToolCallInput, +) -> Result { + let context = authenticated_tool_context(context, &input)?; let trace_id = input .effective_trace_id(&context.trace.trace_id) .to_string(); @@ -182,7 +191,7 @@ pub async fn mnote_call( "toolName": cached.get("toolName").cloned().unwrap_or(Value::Null), "audit": cached.get("audit").cloned().unwrap_or(Value::Null) })); - return Ok((StatusCode::OK, stamp_tool_headers(), Json(cached))); + return Ok(cached); } let result = match input.tool_name.as_str() { "mnote.doc.fetch" => doc::doc_fetch(&state, &context, &input).await, @@ -282,7 +291,7 @@ pub async fn mnote_call( "toolName": response_body.get("toolName").cloned().unwrap_or(Value::Null), "audit": response_body.get("audit").cloned().unwrap_or(Value::Null) })); - Ok((StatusCode::OK, stamp_tool_headers(), Json(response_body))) + Ok(response_body) } fn is_read_tool(tool_name: &str) -> bool { @@ -555,13 +564,13 @@ mod tests { use axum::body::{to_bytes, Body}; use axum::http::{Request, StatusCode}; use serde_json::{json, Value}; + use std::collections::BTreeMap; use std::fs; - use std::sync::{Mutex, OnceLock}; + use std::sync::Mutex; use tower::util::ServiceExt; fn env_lock() -> &'static Mutex<()> { - static LOCK: OnceLock> = OnceLock::new(); - LOCK.get_or_init(|| Mutex::new(())) + crate::test_support::hermes_env_lock() } fn app() -> axum::Router { @@ -631,6 +640,56 @@ mod tests { })) } + fn app_with_content(content: Value) -> axum::Router { + build_app(AppState::new(AppConfig { + service_name: "mnote-web".into(), + service_version: "0.1.0".into(), + bind_addr: "127.0.0.1:0".into(), + public_bind_addr: "127.0.0.1:3000".into(), + legacy_next_base_url: Some("http://127.0.0.1:3100".into()), + enable_legacy_next_compat: true, + enable_debug_shell_routes: false, + enable_editor_actor: true, + hermes_base_path: "/api/hermes".into(), + compat_next_base_path: "/api/compat/next".into(), + convex_url: None, + convex_admin_key: None, + allow_dev_fixtures: true, + query_fixtures_json: Some( + json!({ + "documents:getMeta": { + "id": "doc_1", + "workspace_id": "ws_demo", + "title": "复杂块阻断页面", + "can_edit": true, + "wide_layout": false, + "use_small_text": false, + "show_toc": true, + "block_count": 6 + }, + "documents:getContent": { + "title": "复杂块阻断页面", + "content": content, + "revision": 7, + "conflict_detection_key": "doc_1:7" + } + }) + .to_string(), + ), + mutation_fixtures_json: Some( + r#"{ + "documents:updateContent": {"ok": true, "revision": 8, "conflict_detection_key": "doc_1:8"}, + "bridgeLogs:recordCommandLog": {"ok": true, "id": "cmd_log_fixture"}, + "bridgeLogs:recordDomainEvent": {"ok": true, "id": "event_fixture"} + }"# + .into(), + ), + dev_user_id: "dev-user".into(), + dev_user_name: "开发用户".into(), + dev_user_email: "dev@mnote.local".into(), + })) + } + async fn call_tool_ok(payload: Value) -> Value { let response = app() .oneshot( @@ -651,6 +710,26 @@ mod tests { serde_json::from_slice(&body).expect("json") } + async fn call_tool_ok_with_app(app: axum::Router, payload: Value) -> Value { + let response = app + .oneshot( + Request::builder() + .method("POST") + .uri("/api/hermes/tools/mnote/call") + .header("content-type", "application/json") + .header("x-mnote-actor-id", "user_1") + .body(Body::from(payload.to_string())) + .expect("request"), + ) + .await + .expect("response"); + assert_eq!(response.status(), StatusCode::OK); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + serde_json::from_slice(&body).expect("json") + } + async fn block_revision_ref(block_id: &str) -> String { let payload = call_tool_ok(json!({ "toolName": "mnote.doc.fetch", @@ -674,6 +753,35 @@ mod tests { .to_string() } + async fn block_revision_refs_with_app(app: axum::Router) -> BTreeMap { + let payload = call_tool_ok_with_app( + app, + json!({ + "toolName": "mnote.doc.fetch", + "workspaceId": "ws_demo", + "documentId": "doc_1", + "sessionId": "sess_ref_complex", + "runId": "run_ref_complex", + "toolCallId": "call_ref_complex", + "traceId": "trace_ref_complex", + "capabilityScope": ["page.read"], + "args": {"scope": "full", "detail": "with_ids"} + }), + ) + .await; + payload["result"]["blocks"] + .as_array() + .expect("blocks") + .iter() + .filter_map(|block| { + Some(( + block["blockId"].as_str()?.to_string(), + block["revisionRef"].as_str()?.to_string(), + )) + }) + .collect() + } + #[tokio::test] async fn hermes_tools_manifest_returns_first_batch_tools() { let response = app() @@ -726,6 +834,89 @@ mod tests { ); } + #[tokio::test] + async fn hermes_tools_manifest_describes_markdown_edit_write_contract() { + let response = app() + .oneshot( + Request::builder() + .uri("/api/hermes/tools/mnote/manifest") + .header("x-mnote-actor-id", "user_1") + .body(Body::empty()) + .expect("request"), + ) + .await + .expect("response"); + assert_eq!(response.status(), StatusCode::OK); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let payload: Value = serde_json::from_slice(&body).expect("json"); + let tools = payload["manifest"]["tools"].as_array().expect("tools"); + let markdown_edit = tools + .iter() + .find(|tool| tool["name"] == "mnote.doc.markdown_edit") + .expect("markdown edit tool"); + let required = markdown_edit["inputSchema"]["required"] + .as_array() + .expect("required"); + for field in [ + "workspaceId", + "documentId", + "actorId", + "dryRun", + "idempotencyKey", + ] { + assert!(required.iter().any(|value| value == field)); + } + assert!(markdown_edit["inputSchema"]["properties"]["operations"].is_object()); + assert!(markdown_edit["inputSchema"]["properties"]["full_content"].is_object()); + assert!(markdown_edit["inputSchema"]["anyOf"] + .as_array() + .expect("anyOf") + .iter() + .any(|rule| rule["required"] == json!(["operations"]))); + assert!(markdown_edit["inputSchema"]["anyOf"] + .as_array() + .expect("anyOf") + .iter() + .any(|rule| rule["required"] == json!(["full_content"]))); + } + + #[tokio::test] + async fn hermes_tools_manifest_describes_block_tools_selection_scope() { + let response = app() + .oneshot( + Request::builder() + .uri("/api/hermes/tools/mnote/manifest") + .header("x-mnote-actor-id", "user_1") + .body(Body::empty()) + .expect("request"), + ) + .await + .expect("response"); + assert_eq!(response.status(), StatusCode::OK); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let payload: Value = serde_json::from_slice(&body).expect("json"); + let tools = payload["manifest"]["tools"].as_array().expect("tools"); + for name in [ + "mnote.block.replace", + "mnote.block.insert_after", + "mnote.block.delete", + "mnote.block.move_after", + ] { + let tool = tools + .iter() + .find(|tool| tool["name"] == name) + .unwrap_or_else(|| panic!("missing tool {name}")); + assert!( + tool["inputSchema"]["properties"]["allowedTargetBlockIds"].is_object(), + "{name}" + ); + } + } + #[tokio::test] async fn hermes_tools_page_get_requires_auth() { let response = app() @@ -1353,6 +1544,326 @@ mod tests { ); } + #[tokio::test] + async fn hermes_tools_block_tools_selection_scope_rejects_out_of_scope_targets() { + let heading_ref = block_revision_ref("heading_1").await; + let p1_ref = block_revision_ref("p_1").await; + let p2_ref = block_revision_ref("p_2").await; + let cases = vec![ + json!({ + "toolName": "mnote.block.replace", + "workspaceId": "ws_demo", + "documentId": "doc_1", + "sessionId": "sess_scope_replace", + "runId": "run_scope_replace", + "toolCallId": "call_scope_replace", + "traceId": "trace_scope_replace", + "idempotencyKey": "idem_scope_replace", + "dryRun": false, + "capabilityScope": ["block.write"], + "args": { + "blockId": "heading_1", + "content": "越界替换", + "revision": 7, + "conflictDetectionKey": "doc_1:7", + "blockRevisionRef": heading_ref, + "allowedTargetBlockIds": ["p_1"] + } + }), + json!({ + "toolName": "mnote.block.insert_after", + "workspaceId": "ws_demo", + "documentId": "doc_1", + "sessionId": "sess_scope_insert", + "runId": "run_scope_insert", + "toolCallId": "call_scope_insert", + "traceId": "trace_scope_insert", + "idempotencyKey": "idem_scope_insert", + "dryRun": false, + "capabilityScope": ["block.write"], + "args": { + "anchorBlockId": "heading_1", + "content": "越界插入", + "revision": 7, + "conflictDetectionKey": "doc_1:7", + "anchorRevisionRef": heading_ref, + "allowedTargetBlockIds": ["p_1"] + } + }), + json!({ + "toolName": "mnote.block.delete", + "workspaceId": "ws_demo", + "documentId": "doc_1", + "sessionId": "sess_scope_delete", + "runId": "run_scope_delete", + "toolCallId": "call_scope_delete", + "traceId": "trace_scope_delete", + "idempotencyKey": "idem_scope_delete", + "dryRun": false, + "capabilityScope": ["block.write"], + "args": { + "blockId": "p_1", + "revision": 7, + "conflictDetectionKey": "doc_1:7", + "blockRevisionRef": p1_ref, + "allowedTargetBlockIds": ["heading_1"] + } + }), + json!({ + "toolName": "mnote.block.move_after", + "workspaceId": "ws_demo", + "documentId": "doc_1", + "sessionId": "sess_scope_move", + "runId": "run_scope_move", + "toolCallId": "call_scope_move", + "traceId": "trace_scope_move", + "idempotencyKey": "idem_scope_move", + "dryRun": false, + "capabilityScope": ["block.write"], + "args": { + "blockId": "heading_1", + "anchorBlockId": "p_2", + "revision": 7, + "conflictDetectionKey": "doc_1:7", + "blockRevisionRef": heading_ref, + "anchorRevisionRef": p2_ref, + "allowedTargetBlockIds": ["heading_1"] + } + }), + ]; + + for payload in cases { + let response = app() + .oneshot( + Request::builder() + .method("POST") + .uri("/api/hermes/tools/mnote/call") + .header("content-type", "application/json") + .header("x-mnote-actor-id", "user_1") + .body(Body::from(payload.to_string())) + .expect("request"), + ) + .await + .expect("response"); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!( + response + .headers() + .get("x-error-code") + .and_then(|value| value.to_str().ok()), + Some("mnote_block_target_out_of_scope") + ); + } + } + + #[tokio::test] + async fn hermes_tools_block_move_after_blocks_complex_and_nested_blocks() { + let content = json!([ + { + "id": "heading_parent", + "type": "heading", + "props": { "level": 2 }, + "content": [{ "type": "text", "text": "带子块标题" }], + "children": [ + { + "id": "heading_child", + "type": "paragraph", + "content": [{ "type": "text", "text": "标题子段落" }] + } + ] + }, + { + "id": "p_anchor", + "type": "paragraph", + "content": [{ "type": "text", "text": "锚点段落" }] + }, + { + "id": "list_item_1", + "type": "bullet_list_item", + "content": [{ "type": "text", "text": "列表项" }] + }, + { + "id": "table_1", + "type": "table", + "content": [{ "type": "text", "text": "表格块" }] + }, + { + "id": "mindmap_1", + "type": "mindmap", + "props": { "mindmapId": "mindmap_fixture_1" }, + "content": [{ "type": "text", "text": "思维导图块" }] + }, + { + "id": "resource_1", + "type": "resource", + "content": [{ "type": "text", "text": "资源块" }] + } + ]); + let refs = block_revision_refs_with_app(app_with_content(content.clone())).await; + let anchor_ref = refs.get("p_anchor").expect("anchor ref").clone(); + for block_id in [ + "heading_parent", + "list_item_1", + "table_1", + "mindmap_1", + "resource_1", + ] { + let response = app_with_content(content.clone()) + .oneshot( + Request::builder() + .method("POST") + .uri("/api/hermes/tools/mnote/call") + .header("content-type", "application/json") + .header("x-mnote-actor-id", "user_1") + .body(Body::from( + json!({ + "toolName": "mnote.block.move_after", + "workspaceId": "ws_demo", + "documentId": "doc_1", + "sessionId": format!("sess_move_blocked_{block_id}"), + "runId": format!("run_move_blocked_{block_id}"), + "toolCallId": format!("call_move_blocked_{block_id}"), + "traceId": format!("trace_move_blocked_{block_id}"), + "idempotencyKey": format!("idem_move_blocked_{block_id}"), + "dryRun": false, + "capabilityScope": ["block.write"], + "args": { + "blockId": block_id, + "anchorBlockId": "p_anchor", + "revision": 7, + "conflictDetectionKey": "doc_1:7", + "blockRevisionRef": refs.get(block_id).expect("block ref"), + "anchorRevisionRef": anchor_ref + } + }) + .to_string(), + )) + .expect("request"), + ) + .await + .expect("response"); + assert_eq!(response.status(), StatusCode::OK); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let payload: Value = serde_json::from_slice(&body).expect("json"); + assert_eq!(payload["result"]["blocked"], json!(true), "{block_id}"); + assert_eq!( + payload["result"]["warnings"][0]["code"], + json!("block_move_after_blocked"), + "{block_id}" + ); + } + } + + #[tokio::test] + async fn hermes_tools_block_insert_after_accepts_multiple_blocks_and_returns_ids() { + let heading_ref = block_revision_ref("heading_1").await; + let response = app() + .oneshot( + Request::builder() + .method("POST") + .uri("/api/hermes/tools/mnote/call") + .header("content-type", "application/json") + .header("x-mnote-actor-id", "user_1") + .body(Body::from( + json!({ + "toolName": "mnote.block.insert_after", + "workspaceId": "ws_demo", + "documentId": "doc_1", + "sessionId": "sess_multi_insert", + "runId": "run_multi_insert", + "toolCallId": "call_multi_insert", + "traceId": "trace_multi_insert", + "idempotencyKey": "idem_multi_insert", + "dryRun": false, + "capabilityScope": ["block.write"], + "args": { + "anchorBlockId": "heading_1", + "blocks": ["新增第一段", {"type": "todo", "content": "新增待办"}], + "revision": 7, + "conflictDetectionKey": "doc_1:7", + "anchorRevisionRef": heading_ref + } + }) + .to_string(), + )) + .expect("request"), + ) + .await + .expect("response"); + assert_eq!(response.status(), StatusCode::OK); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let payload: Value = serde_json::from_slice(&body).expect("json"); + let inserted_ids = payload["result"]["insertedBlockIds"] + .as_array() + .expect("insertedBlockIds"); + assert_eq!(inserted_ids.len(), 2); + assert!(inserted_ids + .iter() + .all(|id| { id.as_str().unwrap_or_default().starts_with("ai_block_") })); + assert_eq!( + payload["result"]["changedBlocks"] + .as_array() + .expect("changedBlocks") + .len(), + 2 + ); + assert_eq!(payload["result"]["changedBlocks"][0]["op"], "insert_after"); + assert_eq!(payload["result"]["changedBlocks"][1]["op"], "insert_after"); + } + + #[tokio::test] + async fn hermes_tools_block_insert_after_rejects_more_than_twenty_blocks() { + let heading_ref = block_revision_ref("heading_1").await; + let blocks = (0..21) + .map(|index| json!(format!("新增段落 {index}"))) + .collect::>(); + let response = app() + .oneshot( + Request::builder() + .method("POST") + .uri("/api/hermes/tools/mnote/call") + .header("content-type", "application/json") + .header("x-mnote-actor-id", "user_1") + .body(Body::from( + json!({ + "toolName": "mnote.block.insert_after", + "workspaceId": "ws_demo", + "documentId": "doc_1", + "sessionId": "sess_multi_insert_limit", + "runId": "run_multi_insert_limit", + "toolCallId": "call_multi_insert_limit", + "traceId": "trace_multi_insert_limit", + "idempotencyKey": "idem_multi_insert_limit", + "dryRun": false, + "capabilityScope": ["block.write"], + "args": { + "anchorBlockId": "heading_1", + "blocks": blocks, + "revision": 7, + "conflictDetectionKey": "doc_1:7", + "anchorRevisionRef": heading_ref + } + }) + .to_string(), + )) + .expect("request"), + ) + .await + .expect("response"); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!( + response + .headers() + .get("x-error-code") + .and_then(|value| value.to_str().ok()), + Some("mnote_tool_bad_request") + ); + } + #[tokio::test] async fn hermes_tools_block_write_requires_fresh_revision_and_block_ref() { let missing_revision_response = app() @@ -1437,6 +1948,95 @@ mod tests { ); } + #[tokio::test] + async fn hermes_tools_apply_block_ops_requires_page_preconditions() { + let response = app() + .oneshot( + Request::builder() + .method("POST") + .uri("/api/hermes/tools/mnote/call") + .header("content-type", "application/json") + .header("x-mnote-actor-id", "user_1") + .body(Body::from( + json!({ + "toolName": "mnote.doc.apply_block_ops", + "workspaceId": "ws_demo", + "documentId": "doc_1", + "sessionId": "sess_1", + "runId": "run_1", + "toolCallId": "call_apply_missing_revision_1", + "traceId": "trace_apply_missing_revision_1", + "idempotencyKey": "idem_apply_missing_revision_1", + "dryRun": false, + "capabilityScope": ["block.write", "page.write"], + "args": { + "operations": [{ + "op": "replace", + "blockId": "p_2", + "content": "不应写入" + }] + } + }) + .to_string(), + )) + .expect("request"), + ) + .await + .expect("response"); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!( + response + .headers() + .get("x-error-code") + .and_then(|value| value.to_str().ok()), + Some("mnote_tool_write_precondition_required") + ); + + let missing_block_ref_response = app() + .oneshot( + Request::builder() + .method("POST") + .uri("/api/hermes/tools/mnote/call") + .header("content-type", "application/json") + .header("x-mnote-actor-id", "user_1") + .body(Body::from( + json!({ + "toolName": "mnote.doc.apply_block_ops", + "workspaceId": "ws_demo", + "documentId": "doc_1", + "sessionId": "sess_1", + "runId": "run_1", + "toolCallId": "call_apply_missing_block_ref_1", + "traceId": "trace_apply_missing_block_ref_1", + "idempotencyKey": "idem_apply_missing_block_ref_1", + "dryRun": false, + "capabilityScope": ["block.write", "page.write"], + "args": { + "revision": 7, + "conflictDetectionKey": "doc_1:7", + "operations": [{ + "op": "replace", + "blockId": "p_2", + "content": "不应写入" + }] + } + }) + .to_string(), + )) + .expect("request"), + ) + .await + .expect("response"); + assert_eq!(missing_block_ref_response.status(), StatusCode::BAD_REQUEST); + assert_eq!( + missing_block_ref_response + .headers() + .get("x-error-code") + .and_then(|value| value.to_str().ok()), + Some("mnote_tool_write_precondition_required") + ); + } + #[tokio::test] async fn hermes_tools_reject_workspace_context_conflict_without_content_leak() { let response = app() @@ -1554,6 +2154,349 @@ mod tests { assert_eq!(payload["result"]["commandName"], "page.body.save"); } + #[tokio::test] + async fn hermes_tools_markdown_edit_local_requires_write_contract() { + let root = std::env::temp_dir().join(format!( + "mnote-markdown-edit-contract-{}", + std::process::id() + )); + let _ = fs::remove_dir_all(&root); + fs::create_dir_all(&root).expect("create temp root"); + let path = root.join("page.md"); + fs::write(&path, "第一段\n\n第二段\n").expect("write markdown"); + + let response = app() + .oneshot( + Request::builder() + .method("POST") + .uri("/api/hermes/tools/mnote/call") + .header("content-type", "application/json") + .header("x-mnote-actor-id", "user_1") + .body(Body::from( + json!({ + "toolName": "mnote.doc.markdown_edit", + "workspaceId": "ws_demo", + "documentId": path.to_string_lossy(), + "sessionId": "sess_1", + "runId": "run_1", + "toolCallId": "call_1", + "traceId": "trace_1", + "dryRun": false, + "args": { + "operations": [{"search": "第二段", "replace": "测试123"}] + } + }) + .to_string(), + )) + .expect("request"), + ) + .await + .expect("response"); + + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!( + response + .headers() + .get("x-error-code") + .and_then(|value| value.to_str().ok()), + Some("mnote_tool_idempotency_required") + ); + assert_eq!( + fs::read_to_string(&path).expect("read markdown"), + "第一段\n\n第二段\n" + ); + let _ = fs::remove_dir_all(&root); + } + + #[tokio::test] + async fn hermes_tools_markdown_edit_local_dry_run_does_not_write() { + let root = std::env::temp_dir().join(format!( + "mnote-markdown-edit-dry-run-{}", + std::process::id() + )); + let _ = fs::remove_dir_all(&root); + fs::create_dir_all(&root).expect("create temp root"); + let path = root.join("page.md"); + fs::write(&path, "第一段\n\n第二段\n").expect("write markdown"); + + let response = app() + .oneshot( + Request::builder() + .method("POST") + .uri("/api/hermes/tools/mnote/call") + .header("content-type", "application/json") + .header("x-mnote-actor-id", "user_1") + .body(Body::from( + json!({ + "toolName": "mnote.doc.markdown_edit", + "workspaceId": "ws_demo", + "documentId": path.to_string_lossy(), + "sessionId": "sess_1", + "runId": "run_1", + "toolCallId": "call_1", + "traceId": "trace_1", + "idempotencyKey": "idem_markdown_local_1", + "dryRun": true, + "args": { + "operations": [{"search": "第二段", "replace": "测试123"}] + } + }) + .to_string(), + )) + .expect("request"), + ) + .await + .expect("response"); + + assert_eq!(response.status(), StatusCode::OK); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let payload: Value = serde_json::from_slice(&body).expect("json"); + assert_eq!(payload["audit"]["effect"], "dry_run"); + assert_eq!(payload["result"]["operationsApplied"], 1); + assert_eq!(payload["result"]["applyResult"]["written"], false); + assert_eq!(payload["result"]["applyResult"]["dryRun"], true); + assert_eq!( + fs::read_to_string(&path).expect("read markdown"), + "第一段\n\n第二段\n" + ); + let _ = fs::remove_dir_all(&root); + } + + #[tokio::test] + async fn hermes_tools_markdown_edit_reports_empty_block_mapping_before_apply() { + // 7-27 修复后:即使 search 吃掉了块注释,也不会崩溃或泄露 apply_block_ops 错误。 + // 新行为:search 命中后标记为 applied,块文本变为纯文本「测试123」(无块注释), + // parse_final_markdown_to_blocks 将其识别为新块,原 p_2 块保留。 + let response = app() + .oneshot( + Request::builder() + .method("POST") + .uri("/api/hermes/tools/mnote/call") + .header("content-type", "application/json") + .header("x-mnote-actor-id", "user_1") + .body(Body::from( + json!({ + "toolName": "mnote.doc.markdown_edit", + "workspaceId": "ws_demo", + "documentId": "doc_1", + "sessionId": "sess_1", + "runId": "run_1", + "toolCallId": "call_1", + "traceId": "trace_1", + "idempotencyKey": "idem_markdown_mapping_empty_2", + "dryRun": false, + "args": { + "operations": [{"search": "第二段 ", "replace": "测试123"}] + } + }) + .to_string(), + )) + .expect("request"), + ) + .await + .expect("response"); + + assert_eq!(response.status(), StatusCode::OK); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let payload: Value = serde_json::from_slice(&body).expect("json"); + assert_eq!(payload["result"]["operationsApplied"], 1); + // 确认没有泄露 apply_block_ops 错误 + let text = String::from_utf8(body.to_vec()).expect("utf8"); + assert!(!text.contains("mnote.doc.apply_block_ops operations 不能为空")); + } + + #[tokio::test] + async fn hermes_tools_markdown_edit_online_full_content_rejects_unsafe_block_mapping() { + // 7-27 修复后:full_content 不再被拒绝。新行为:纯文本(无块注释)全部识别为新块, + // 写入后与原有复杂块(如有)合并保留。 + let response = app() + .oneshot( + Request::builder() + .method("POST") + .uri("/api/hermes/tools/mnote/call") + .header("content-type", "application/json") + .header("x-mnote-actor-id", "user_1") + .body(Body::from( + json!({ + "toolName": "mnote.doc.markdown_edit", + "workspaceId": "ws_demo", + "documentId": "doc_1", + "sessionId": "sess_1", + "runId": "run_1", + "toolCallId": "call_1", + "traceId": "trace_1", + "idempotencyKey": "idem_markdown_full_content_online_2", + "dryRun": false, + "args": { + "full_content": "章节一\n\n第一段已改\n\n第二段已改" + } + }) + .to_string(), + )) + .expect("request"), + ) + .await + .expect("response"); + + assert_eq!(response.status(), StatusCode::OK); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let payload: Value = serde_json::from_slice(&body).expect("json"); + assert_eq!(payload["result"]["operationsApplied"], 1); + // 确认没有泄露旧错误 + let text = String::from_utf8(body.to_vec()).expect("utf8"); + assert!(!text.contains("mnote_doc_markdown_edit_block_mapping_empty")); + assert!(!text.contains("mnote.doc.apply_block_ops operations 不能为空")); + } + + #[tokio::test] + async fn hermes_tools_markdown_edit_maps_normalized_search_to_block() { + let response = app() + .oneshot( + Request::builder() + .method("POST") + .uri("/api/hermes/tools/mnote/call") + .header("content-type", "application/json") + .header("x-mnote-actor-id", "user_1") + .body(Body::from( + json!({ + "toolName": "mnote.doc.markdown_edit", + "workspaceId": "ws_demo", + "documentId": "doc_1", + "sessionId": "sess_1", + "runId": "run_1", + "toolCallId": "call_1", + "traceId": "trace_1", + "idempotencyKey": "idem_markdown_normalized_1", + "dryRun": false, + "args": { + "operations": [{"search": "第二 段", "replace": "测试123"}] + } + }) + .to_string(), + )) + .expect("request"), + ) + .await + .expect("response"); + + assert_eq!(response.status(), StatusCode::OK); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let payload: Value = serde_json::from_slice(&body).expect("json"); + assert_eq!(payload["result"]["operationsApplied"], 1); + // 7-27: 新路径 changedBlocks 格式验证 + let changed = payload["result"]["applyResult"]["changedBlocks"] + .as_array() + .expect("changedBlocks"); + assert!(!changed.is_empty(), "changedBlocks should not be empty"); + assert_eq!( + payload["result"]["applyResult"]["changedBlocks"][0]["blockId"], + "p_2" + ); + assert_eq!( + payload["result"]["applyResult"]["changedBlocks"][0]["op"], + "replace" + ); + } + + #[tokio::test] + async fn hermes_tools_markdown_edit_rejects_selection_out_of_scope() { + let response = app() + .oneshot( + Request::builder() + .method("POST") + .uri("/api/hermes/tools/mnote/call") + .header("content-type", "application/json") + .header("x-mnote-actor-id", "user_1") + .body(Body::from( + json!({ + "toolName": "mnote.doc.markdown_edit", + "workspaceId": "ws_demo", + "documentId": "doc_1", + "sessionId": "sess_markdown_scope", + "runId": "run_markdown_scope", + "toolCallId": "call_markdown_scope", + "traceId": "trace_markdown_scope", + "idempotencyKey": "idem_markdown_scope", + "dryRun": true, + "args": { + "allowedTargetBlockIds": ["p_2"], + "operations": [{"search": "第一段", "replace": "不应越权修改"}] + } + }) + .to_string(), + )) + .expect("request"), + ) + .await + .expect("response"); + + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!( + response + .headers() + .get("x-error-code") + .and_then(|value| value.to_str().ok()), + Some("mnote_markdown_edit_target_out_of_scope") + ); + } + + #[tokio::test] + async fn hermes_tools_markdown_edit_merges_same_block_operations() { + let response = app() + .oneshot( + Request::builder() + .method("POST") + .uri("/api/hermes/tools/mnote/call") + .header("content-type", "application/json") + .header("x-mnote-actor-id", "user_1") + .body(Body::from( + json!({ + "toolName": "mnote.doc.markdown_edit", + "workspaceId": "ws_demo", + "documentId": "doc_1", + "sessionId": "sess_1", + "runId": "run_1", + "toolCallId": "call_1", + "traceId": "trace_1", + "idempotencyKey": "idem_markdown_same_block_1", + "dryRun": true, + "args": { + "operations": [ + {"search": "二", "replace": "2"}, + {"search": "2段", "replace": "2段落"} + ] + } + }) + .to_string(), + )) + .expect("request"), + ) + .await + .expect("response"); + + assert_eq!(response.status(), StatusCode::OK); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let payload: Value = serde_json::from_slice(&body).expect("json"); + assert_eq!(payload["result"]["operationsApplied"], 2); + let diff = payload["result"]["applyResult"]["diff"] + .as_array() + .expect("diff"); + assert_eq!(diff.len(), 1); + assert_eq!(diff[0]["blockId"], "p_2"); + // dryRun 时文本不落盘,但 diff 记录预期修改后的内容 + assert!(diff[0]["content"].as_str().unwrap_or("").contains("2段落")); + } + #[tokio::test] async fn hermes_tools_update_options_dry_run_filters_unwired_fields() { let response = app() diff --git a/rust/crates/mnote-web/src/routes/mod.rs b/rust/crates/mnote-web/src/routes/mod.rs index 74eca79d..eda22061 100644 --- a/rust/crates/mnote-web/src/routes/mod.rs +++ b/rust/crates/mnote-web/src/routes/mod.rs @@ -179,14 +179,26 @@ pub fn build_router(state: AppState) -> Router { "/client/sessions", get(hermes_client::list_sessions).post(hermes_client::create_session), ) + .route( + "/client/sessions/search", + get(hermes_client::search_sessions), + ) .route( "/client/sessions/{session_id}", - get(hermes_client::get_session), + get(hermes_client::get_session).delete(hermes_client::delete_session), ) .route( "/client/sessions/{session_id}/resume", post(hermes_client::resume_session), ) + .route( + "/client/sessions/{session_id}/rename", + post(hermes_client::rename_session), + ) + .route( + "/client/sessions/{session_id}/auto-title", + post(hermes_client::auto_title_session), + ) .route("/client/gateway/health", get(hermes_client::gateway_health)) .route("/client/profiles", get(hermes_client::list_profiles)) .route( diff --git a/rust/crates/mnote-web/src/routes/page_ai_workflow.rs b/rust/crates/mnote-web/src/routes/page_ai_workflow.rs index 023f8a77..92d509dd 100644 --- a/rust/crates/mnote-web/src/routes/page_ai_workflow.rs +++ b/rust/crates/mnote-web/src/routes/page_ai_workflow.rs @@ -1,7 +1,7 @@ use crate::app::AppState; use crate::context::RequestContext; use crate::error::WebError; -use crate::hermes_tools::{doc, ToolCallInput}; +use crate::hermes_tools::ToolCallInput; use axum::extract::{Extension, State}; use axum::http::{HeaderMap, StatusCode}; use axum::Json; @@ -88,6 +88,30 @@ pub async fn block_edit_workflow( } else { context.auth.actor_id.clone() }; + let allowed_target_block_ids = ai_context + .get("allowedTargetBlockIds") + .and_then(Value::as_array) + .map(|items| { + items + .iter() + .filter_map(Value::as_str) + .map(str::trim) + .filter(|item| !item.is_empty()) + .map(ToOwned::to_owned) + .collect::>() + }) + .unwrap_or_default(); + let mut markdown_args = json!({ + "operations": markdown_operations.clone() + }); + if !allowed_target_block_ids.is_empty() { + if let Value::Object(map) = &mut markdown_args { + map.insert( + "allowedTargetBlockIds".into(), + json!(allowed_target_block_ids), + ); + } + } let edit_input = ToolCallInput { tool_name: "mnote.doc.markdown_edit".into(), workspace_id: Some(workspace_id.clone()), @@ -101,12 +125,12 @@ pub async fn block_edit_workflow( idempotency_key: Some(format!("page_ai_fast_edit_{}", context.trace.request_id)), dry_run: Some(false), capability_scope: Some(vec!["block.write".into(), "page.write".into()]), - args: Some(json!({ - "operations": markdown_operations - })), + args: Some(markdown_args), }; let apply_started = Instant::now(); - let apply_result = doc::doc_markdown_edit(&state, &context, &edit_input).await?; + let tool_response = + crate::routes::hermes_tools::execute_mnote_tool_call(&state, &context, edit_input).await?; + let apply_result = tool_response.get("result").cloned().unwrap_or(Value::Null); let apply_ms = apply_started.elapsed().as_millis(); info!( trace_id = %trace_id, @@ -128,6 +152,7 @@ pub async fn block_edit_workflow( "traceId": trace_id, "operations": markdown_operations, "applyResult": apply_result, + "toolExecution": tool_response, "message": "已通过页面 markdown 编辑快路径完成写入。", "timingsMs": { "total": started.elapsed().as_millis(), @@ -540,6 +565,124 @@ fn yaml_path_value(content: &str, path: &[&str]) -> Option { #[cfg(test)] mod tests { use super::{direct_block_edit_operations, extract_operations_from_model_text}; + use crate::app::{build_app, AppConfig, AppState}; + use axum::body::{to_bytes, Body}; + use axum::http::{Request, StatusCode}; + use axum::routing::post; + use axum::{Json, Router}; + use serde_json::{json, Value}; + use std::fs; + use std::sync::Mutex; + use tower::util::ServiceExt; + + fn env_lock() -> &'static Mutex<()> { + crate::test_support::hermes_env_lock() + } + + fn app() -> axum::Router { + build_app(AppState::new(AppConfig { + service_name: "mnote-web".into(), + service_version: "0.1.0".into(), + bind_addr: "127.0.0.1:0".into(), + public_bind_addr: "127.0.0.1:3000".into(), + legacy_next_base_url: Some("http://127.0.0.1:3100".into()), + enable_legacy_next_compat: true, + enable_debug_shell_routes: false, + enable_editor_actor: true, + hermes_base_path: "/api/hermes".into(), + compat_next_base_path: "/api/compat/next".into(), + convex_url: None, + convex_admin_key: None, + allow_dev_fixtures: true, + query_fixtures_json: Some( + r#"{ + "documents:getMeta": { + "id": "doc_1", + "workspace_id": "ws_demo", + "title": "服务端页面", + "can_edit": true, + "wide_layout": false, + "use_small_text": false, + "show_toc": true, + "block_count": 2 + }, + "documents:getContent": { + "title": "服务端页面", + "content": [ + { + "id": "p_1", + "type": "paragraph", + "content": [{ "type": "text", "text": "第一段" }] + }, + { + "id": "p_2", + "type": "paragraph", + "content": [{ "type": "text", "text": "第二段" }] + } + ], + "revision": 7, + "conflict_detection_key": "doc_1:7" + } + }"# + .into(), + ), + mutation_fixtures_json: Some( + r#"{ + "documents:updateContent": {"ok": true, "revision": 8, "conflict_detection_key": "doc_1:8"}, + "bridgeLogs:recordCommandLog": {"ok": true, "id": "cmd_log_fixture"}, + "bridgeLogs:recordDomainEvent": {"ok": true, "id": "event_fixture"} + }"# + .into(), + ), + dev_user_id: "dev-user".into(), + dev_user_name: "开发用户".into(), + dev_user_email: "dev@mnote.local".into(), + })) + } + + async fn spawn_mock_model_server() -> String { + async fn completions() -> Json { + Json(json!({ + "choices": [{ + "message": { + "content": "{\"operations\":[{\"search\":\"第二段\",\"replace\":\"测试123\"}],\"summary\":\"ok\"}" + } + }] + })) + } + + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind mock model"); + let addr = listener.local_addr().expect("mock model addr"); + let server = Router::new().route("/chat/completions", post(completions)); + tokio::spawn(async move { + let _ = axum::serve(listener, server).await; + }); + format!("http://{addr}") + } + + async fn spawn_out_of_scope_mock_model_server() -> String { + async fn completions() -> Json { + Json(json!({ + "choices": [{ + "message": { + "content": "{\"operations\":[{\"search\":\"第一段\",\"replace\":\"越权修改\"}],\"summary\":\"out_of_scope\"}" + } + }] + })) + } + + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind out-of-scope mock model"); + let addr = listener.local_addr().expect("mock model addr"); + let server = Router::new().route("/chat/completions", post(completions)); + tokio::spawn(async move { + let _ = axum::serve(listener, server).await; + }); + format!("http://{addr}") + } #[test] fn extracts_operations_from_fenced_model_json() { @@ -570,4 +713,137 @@ mod tests { assert_eq!(operations[2]["op"], "delete"); assert_eq!(operations[2]["matchText"], "第三段"); } + + #[tokio::test] + async fn block_edit_workflow_respects_disabled_markdown_edit_tool() { + let _guard = env_lock().lock().expect("env lock"); + let base_url = spawn_mock_model_server().await; + let hermes_home = std::env::temp_dir().join(format!( + "mnote-page-ai-workflow-disabled-tool-{}", + std::process::id() + )); + let _ = fs::remove_dir_all(&hermes_home); + let profile_dir = hermes_home.join("profiles").join("mnoteai"); + fs::create_dir_all(&profile_dir).expect("profile dir"); + fs::write( + profile_dir.join("config.yaml"), + format!( + "model:\n provider: mock\n default: mock-model\n base_url: {base_url}\n api_key: test-key\nmnote:\n tools:\n disabled:\n - mnote.doc.markdown_edit\n" + ), + ) + .expect("profile config"); + std::env::set_var("HERMES_HOME", &hermes_home); + + let response = app() + .oneshot( + Request::builder() + .method("POST") + .uri("/api/page-ai/block-edit-workflow") + .header("content-type", "application/json") + .header("x-mnote-actor-id", "user_1") + .body(Body::from( + json!({ + "workspaceId": "ws_demo", + "documentId": "doc_1", + "message": "把第二段改成测试123", + "profile": "mnoteai", + "sessionId": "sess_page_ai_disabled", + "runId": "run_page_ai_disabled", + "traceId": "trace_page_ai_disabled", + "pageContext": { + "aiContext": { + "schema": "mnote.page_ai_context.v1", + "pageText": "第一段\n\n第二段", + "pageXml": "第一段第二段", + "contextBlocks": [ + {"blockId": "p_1", "text": "第一段"}, + {"blockId": "p_2", "text": "第二段"} + ] + } + } + }) + .to_string(), + )) + .expect("request"), + ) + .await + .expect("response"); + + assert_eq!(response.status(), StatusCode::FORBIDDEN); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let payload: Value = serde_json::from_slice(&body).expect("json"); + assert_eq!(payload["code"], "mnote_tool_disabled"); + + std::env::remove_var("HERMES_HOME"); + let _ = fs::remove_dir_all(&hermes_home); + } + + #[tokio::test] + async fn block_edit_workflow_forwards_allowed_target_blocks_to_markdown_edit() { + let _guard = env_lock().lock().expect("env lock"); + let base_url = spawn_out_of_scope_mock_model_server().await; + let hermes_home = std::env::temp_dir().join(format!( + "mnote-page-ai-workflow-selection-scope-{}", + std::process::id() + )); + let _ = fs::remove_dir_all(&hermes_home); + let profile_dir = hermes_home.join("profiles").join("mnoteai"); + fs::create_dir_all(&profile_dir).expect("profile dir"); + fs::write( + profile_dir.join("config.yaml"), + format!( + "model:\n provider: mock\n default: mock-model\n base_url: {base_url}\n api_key: test-key\n" + ), + ) + .expect("profile config"); + std::env::set_var("HERMES_HOME", &hermes_home); + + let response = app() + .oneshot( + Request::builder() + .method("POST") + .uri("/api/page-ai/block-edit-workflow") + .header("content-type", "application/json") + .header("x-mnote-actor-id", "user_1") + .body(Body::from( + json!({ + "workspaceId": "ws_demo", + "documentId": "doc_1", + "message": "把选中段落改成测试123", + "profile": "mnoteai", + "sessionId": "sess_page_ai_scope", + "runId": "run_page_ai_scope", + "traceId": "trace_page_ai_scope", + "pageContext": { + "aiContext": { + "schema": "mnote.page_ai_context.v1", + "allowedTargetBlockIds": ["p_2"], + "pageText": "第一段\n\n第二段", + "pageXml": "第一段第二段", + "contextBlocks": [ + {"blockId": "p_1", "text": "第一段"}, + {"blockId": "p_2", "text": "第二段"} + ] + } + } + }) + .to_string(), + )) + .expect("request"), + ) + .await + .expect("response"); + + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + let body = to_bytes(response.into_body(), usize::MAX) + .await + .expect("body"); + let payload: Value = serde_json::from_slice(&body).expect("json"); + assert_eq!(payload["code"], "mnote_markdown_edit_target_out_of_scope"); + + std::env::remove_var("HERMES_HOME"); + let _ = fs::remove_dir_all(&hermes_home); + } } diff --git a/rust/crates/mnote-web/src/routes/sse.rs b/rust/crates/mnote-web/src/routes/sse.rs index cdd890be..0efd76ac 100644 --- a/rust/crates/mnote-web/src/routes/sse.rs +++ b/rust/crates/mnote-web/src/routes/sse.rs @@ -2,9 +2,9 @@ use crate::app::AppState; use crate::context::RequestContext; use crate::error::WebError; use crate::routes::stream_support::{ - build_stream_delta_payload, load_stream_overview, load_stream_snapshot, - read_stream_cursor_from_payload, resolve_stream_change, with_stream_kind, StreamChangeKind, - StreamSnapshotQuery, + build_stream_delta_payload, build_stream_push_delta_hint, load_stream_overview, + load_stream_snapshot, read_stream_cursor_from_payload, resolve_stream_change, with_stream_kind, + StreamChangeKind, StreamSnapshotQuery, }; use axum::extract::{Extension, Query, State}; use axum::http::{HeaderMap, HeaderName, HeaderValue}; @@ -94,15 +94,11 @@ async fn events_with_stream_delta( if let Some(ref mut rx) = state.stream_delta_rx { match rx.try_recv() { Ok(payload) => { - let hint = json!({ - "kind": "delta", - "hint": "command_committed", - "commandName": payload.get("commandName"), - "commandId": payload.get("commandId"), - "workspaceId": payload.get("workspaceId"), - "requestId": payload.get("requestId"), - "traceId": payload.get("traceId"), - }); + let hint = build_stream_push_delta_hint( + &payload, + &state.context.trace.request_id, + &state.context.trace.trace_id, + ); return Some((Ok(stream_event("delta", &hint)), Some(state))); } Err(tokio::sync::broadcast::error::TryRecvError::Empty) => {} @@ -113,9 +109,68 @@ async fn events_with_stream_delta( } } - // If push-driven and we already checked both broadcasts, brief sleep then re-check + // If push-driven and broadcasts are quiet, polling remains the safety net. if state.stream_delta_rx.is_some() { - sleep(Duration::from_millis(250)).await; + if let Some(max_polls) = max_polls { + if state.polls >= max_polls { + return None; + } + } + state.polls += 1; + sleep(Duration::from_millis(poll_ms)).await; + + let poll_query = live_poll_query(&state.query); + let Ok((workspace_id, overview)) = + load_stream_overview(state.app_state.config(), &state.context, &poll_query) + .await + else { + return None; + }; + if let Some(change) = + resolve_stream_change(&overview, state.current_cursor.as_deref()) + { + state.current_cursor = change.cursor.clone(); + match change.kind { + StreamChangeKind::Delta => { + let Ok(payload) = build_stream_delta_payload( + state.app_state.config(), + &state.context, + &poll_query, + &workspace_id, + &overview, + change.cursor, + change + .delta + .unwrap_or_else(|| serde_json::json!({ "op": "noop" })), + ) + .await + else { + return None; + }; + return Some((Ok(stream_event("delta", &payload)), Some(state))); + } + StreamChangeKind::Resync => { + let Ok(snapshot_payload) = load_stream_snapshot( + state.app_state.config(), + &state.context, + &poll_query, + ) + .await + else { + return None; + }; + state.current_cursor = + read_stream_cursor_from_payload(&snapshot_payload); + return Some(( + Ok(stream_event( + "resync", + &with_stream_kind(&snapshot_payload, "resync"), + )), + Some(state), + )); + } + } + } return Some((Ok(stream_event("heartbeat", &json!({}))), Some(state))); } @@ -289,6 +344,8 @@ mod tests { use crate::routes::stream_support::StreamSnapshotQuery; use axum::body::{to_bytes, Body}; use axum::http::{Request, StatusCode}; + use std::time::Duration; + use tokio::time::timeout; use tower::util::ServiceExt; fn app() -> axum::Router { @@ -395,6 +452,31 @@ mod tests { assert!(text.contains("\"revision\"")); } + #[tokio::test] + async fn tree_events_push_mode_honors_polling_safety_net_max_polls() { + let response = app() + .oneshot( + Request::builder() + .uri("/api/tree/events?workspaceId=ws_demo&maxPolls=1&pollMs=1") + .body(Body::empty()) + .expect("request"), + ) + .await + .expect("response"); + + assert_eq!(response.status(), StatusCode::OK); + let body = timeout( + Duration::from_secs(2), + to_bytes(response.into_body(), usize::MAX), + ) + .await + .expect("push SSE stream should stop after maxPolls") + .expect("body"); + let text = String::from_utf8(body.to_vec()).expect("utf8"); + assert!(text.contains("event: snapshot") || text.contains("event:snapshot")); + assert!(text.contains("event: heartbeat") || text.contains("event:heartbeat")); + } + #[test] fn live_poll_query_drops_bridge_pagination_cursor() { let query = StreamSnapshotQuery { diff --git a/rust/crates/mnote-web/src/routes/stream_support.rs b/rust/crates/mnote-web/src/routes/stream_support.rs index 791762c1..24744eda 100644 --- a/rust/crates/mnote-web/src/routes/stream_support.rs +++ b/rust/crates/mnote-web/src/routes/stream_support.rs @@ -574,6 +574,18 @@ pub fn with_stream_kind(payload: &Value, kind: &str) -> Value { }) } +pub fn build_stream_push_delta_hint(payload: &Value, request_id: &str, trace_id: &str) -> Value { + json!({ + "kind": "delta", + "hint": "command_committed", + "commandName": payload.get("commandName"), + "commandId": payload.get("commandId"), + "workspaceId": payload.get("workspaceId"), + "requestId": request_id, + "traceId": trace_id, + }) +} + pub async fn build_stream_delta_payload( config: &AppConfig, context: &RequestContext, @@ -1042,7 +1054,7 @@ mod tests { "id": "clog_2", "command_id": "cmd_2", "created_at": "2026-04-25T10:00:02Z", - "command_name": "tree.resource.delete", + "command_name": "tree.resource.archive", "payload": { "streamDelta": { "op": "remove_asset", diff --git a/rust/crates/mnote-web/src/routes/tree.rs b/rust/crates/mnote-web/src/routes/tree.rs index 66184e15..a99e199e 100644 --- a/rust/crates/mnote-web/src/routes/tree.rs +++ b/rust/crates/mnote-web/src/routes/tree.rs @@ -2018,12 +2018,17 @@ fn build_tree_shell_html( const getFileTreeRowDocumentId = (item) => { if (!item) return ""; - if (item.resourceMeta?.documentId) return item.resourceMeta.documentId; if (item.rowKind === "document" || item.rowKind === "markdown") return item.nodeId; if (item.rowKind === "index") return item.nodeId.replace(/^index:/, ""); return ""; }; + const getFileTreeRowOwnerDocumentId = (item) => { + if (!item) return ""; + if (item.resourceMeta?.documentId) return item.resourceMeta.documentId; + return getFileTreeRowDocumentId(item); + }; + const getFileTreeRowAssetId = (item) => { if (!item) return ""; if (item.resourceMeta?.assetId) return item.resourceMeta.assetId; @@ -2084,6 +2089,7 @@ fn build_tree_shell_html( rowKind: "root", nodeId: null, documentId: null, + ownerDocumentId: null, assetId: null, }; } @@ -2092,6 +2098,7 @@ fn build_tree_shell_html( rowKind: normalizeText(row.dataset.rowKind, "document"), nodeId: normalizeText(row.dataset.nodeId) || null, documentId: normalizeText(row.dataset.documentId) || null, + ownerDocumentId: normalizeText(row.dataset.ownerDocumentId) || null, assetId: normalizeText(row.dataset.assetId) || null, }; }; @@ -2934,13 +2941,14 @@ fn build_tree_shell_html( const targetItem = targetRowId ? fileTreeRowById.get(targetRowId) : null; const target = targetItem ? { - rowId: targetItem.rowId, - rowKind: targetItem.rowKind, - nodeId: targetItem.nodeId, - documentId: getFileTreeRowDocumentId(targetItem) || null, - assetId: getFileTreeRowAssetId(targetItem) || null, - } - : { rowId: null, rowKind: "root", nodeId: null, documentId: null, assetId: null }; + rowId: targetItem.rowId, + rowKind: targetItem.rowKind, + nodeId: targetItem.nodeId, + documentId: getFileTreeRowDocumentId(targetItem) || null, + ownerDocumentId: getFileTreeRowOwnerDocumentId(targetItem) || null, + assetId: getFileTreeRowAssetId(targetItem) || null, + } + : { rowId: null, rowKind: "root", nodeId: null, documentId: null, ownerDocumentId: null, assetId: null }; if (sourceKind === "local_folder") { const pasted = await executeLocalFileTreeInternalDrop( target, @@ -3030,7 +3038,12 @@ fn build_tree_shell_html( if (event.key === "F2") { event.preventDefault(); const rowId = item?.rowId || fileTreeFocusedRowId; - if (rowId) beginInlineRename("filetree", rowId); + const renameItem = rowId ? fileTreeRowById.get(rowId) : null; + if (rowId && getFileTreeRowDocumentId(renameItem)) { + beginInlineRename("filetree", rowId); + } else { + setLastAction("当前资源暂不支持重命名", "error"); + } return; } if (event.key === "ArrowDown") { @@ -3057,7 +3070,11 @@ fn build_tree_shell_html( event.preventDefault(); if (item) { const documentId = getFileTreeRowDocumentId(item); - if (documentId) handleNavigate(documentId); + if (documentId) { + handleNavigate(documentId); + } else { + openHydratedFileTreeItem(item); + } } return; } @@ -3219,7 +3236,7 @@ fn build_tree_shell_html( }); } normalizedItems.slice().forEach((item) => { - const itemDocumentId = getFileTreeRowDocumentId(item) || item.resourceMeta?.documentId || item.nodeId; + const itemDocumentId = getFileTreeRowOwnerDocumentId(item) || item.nodeId; if (removedNodeIds.has(item.nodeId) || itemDocumentId === normalizedDocumentId) { removeTreeItemEverywhere(item); changed = true; @@ -4149,8 +4166,7 @@ fn build_tree_shell_html( rowKind === "markdown" || rowKind === "document" || rowKind === "folder" || - rowKind === "index" || - (localSource && rowKind === "asset"); + rowKind === "index"; const canCopyCut = rowKind === "markdown" || rowKind === "document" || @@ -4282,7 +4298,9 @@ fn build_tree_shell_html( return; } if (kind === "rename") { - if (target.rowId) beginInlineRename("filetree", target.rowId); + if (target.rowId && getFileTreeRowDocumentId(item)) { + beginInlineRename("filetree", target.rowId); + } return; } if (kind === "copy" || kind === "cut") { @@ -4639,8 +4657,14 @@ fn build_tree_shell_html( const id = inlineRenameState.id; const item = renameMode === "filetree" ? fileTreeRowById.get(id) : itemById.get(id); const documentId = renameMode === "filetree" - ? getFileTreeRowDocumentId(item) || item?.nodeId || id + ? getFileTreeRowDocumentId(item) : id; + if (!documentId) { + inlineRenameState = { mode: null, id: null, committing: false }; + setLastAction("当前资源暂不支持重命名", "error"); + renderTree(); + return; + } try { const result = await sendCommand({ action: "rename", @@ -4953,6 +4977,7 @@ fn build_tree_shell_html( const openHydratedFileTreeItem = (item) => { const documentId = getFileTreeRowDocumentId(item); + const ownerDocumentId = getFileTreeRowOwnerDocumentId(item); const assetId = getFileTreeRowAssetId(item); if (item.rowKind === "document" || item.rowKind === "index") { handleNavigate(documentId || item.nodeId); @@ -4960,12 +4985,12 @@ fn build_tree_shell_html( } setLastAction(`准备打开资源 ${assetId || item.rowId}`); postToHost("tree.asset.open", { - documentId: documentId || null, + documentId: ownerDocumentId || null, assetId: assetId || null, objectIdentity: item.resourceMeta?.objectIdentity || null, - target: { documentId: documentId || null }, + target: { documentId: ownerDocumentId || null }, payload: { - documentId: documentId || null, + documentId: ownerDocumentId || null, assetId: assetId || null, rowId: item.rowId, rowKind: item.rowKind, @@ -5122,6 +5147,7 @@ fn build_tree_shell_html( const bindFileTreeRowEvents = (row, item) => { if (!(row instanceof HTMLElement) || !item) return; const documentId = getFileTreeRowDocumentId(item) || null; + const ownerDocumentId = getFileTreeRowOwnerDocumentId(item) || null; const assetId = getFileTreeRowAssetId(item) || null; row.dataset.active = String( item.rowKind === "document" && documentId === currentActiveDocumentId @@ -5130,6 +5156,7 @@ fn build_tree_shell_html( row.dataset.rowId = item.rowId; row.dataset.rowKind = item.rowKind; row.dataset.documentId = documentId || ""; + row.dataset.ownerDocumentId = ownerDocumentId || ""; row.dataset.assetId = assetId || ""; row.dataset.objectIdentity = item.resourceMeta?.objectIdentity ? JSON.stringify(item.resourceMeta.objectIdentity) @@ -5577,6 +5604,7 @@ fn build_tree_shell_html( const openFileTreeItem = (item) => { const documentId = getFileTreeRowDocumentId(item); + const ownerDocumentId = getFileTreeRowOwnerDocumentId(item); const assetId = getFileTreeRowAssetId(item); if (item.rowKind === "document" || item.rowKind === "index" || item.rowKind === "markdown") { handleNavigate(documentId || item.nodeId); @@ -5584,12 +5612,12 @@ fn build_tree_shell_html( } setLastAction(`准备打开资源 ${assetId || item.rowId}`); postToHost("tree.asset.open", { - documentId: documentId || null, + documentId: ownerDocumentId || null, assetId: assetId || null, objectIdentity: item.resourceMeta?.objectIdentity || null, - target: { documentId: documentId || null }, + target: { documentId: ownerDocumentId || null }, payload: { - documentId: documentId || null, + documentId: ownerDocumentId || null, assetId: assetId || null, rowId: item.rowId, rowKind: item.rowKind, @@ -5649,6 +5677,7 @@ fn build_tree_shell_html( const children = getSiblings(item.nodeId); const hasBranches = canExpandFileTreeRow(item) && children.length > 0; const documentId = getFileTreeRowDocumentId(item) || null; + const ownerDocumentId = getFileTreeRowOwnerDocumentId(item) || null; const assetId = getFileTreeRowAssetId(item) || null; const row = document.createElement("div"); @@ -5661,6 +5690,7 @@ fn build_tree_shell_html( row.dataset.rowId = item.rowId; row.dataset.rowKind = item.rowKind; row.dataset.documentId = documentId || ""; + row.dataset.ownerDocumentId = ownerDocumentId || ""; row.dataset.assetId = assetId || ""; row.dataset.objectIdentity = item.resourceMeta?.objectIdentity ? JSON.stringify(item.resourceMeta.objectIdentity) @@ -5769,15 +5799,17 @@ fn build_tree_shell_html( const actions = document.createElement("div"); actions.className = "tree-actions"; - actions.appendChild( - createActionButton( - ICONS.edit, - "filetree-action-rename", - `重命名 ${item.title}`, - () => beginInlineRename("filetree", item.rowId), - false, - ), - ); + if (getFileTreeRowDocumentId(item)) { + actions.appendChild( + createActionButton( + ICONS.edit, + "filetree-action-rename", + `重命名 ${item.title}`, + () => beginInlineRename("filetree", item.rowId), + false, + ), + ); + } actions.appendChild( createActionButton( ICONS.more, @@ -7143,6 +7175,11 @@ mod tests { assert!(html.contains("tree.filetree.external-drop")); assert!(html.contains("\"rowKind\":\"asset_folder\"")); assert!(html.contains("\"resourceMeta\"")); + assert!(html.contains("const getFileTreeRowOwnerDocumentId = (item) => {")); + assert!(html.contains("row.dataset.ownerDocumentId = ownerDocumentId || \"\";")); + assert!(html.contains("if (rowId && getFileTreeRowDocumentId(renameItem))")); + assert!(html.contains("if (getFileTreeRowDocumentId(item))")); + assert!(html.contains("documentId: ownerDocumentId || null")); assert!(html.contains("dragover")); assert!(html.contains("hydrateInitialFileTree")); assert!(html.contains("const usedRustInitialRenderer = hydrateInitialRenderer();")); @@ -7897,7 +7934,7 @@ mod tests { payload["result"]["documentId"], Value::String("page_child".into()) ); - assert_eq!(payload["result"]["sortOrder"], Value::from(1)); + assert_eq!(payload["result"]["sortOrder"], Value::Null); assert_eq!( payload["result"]["execution"]["deletedCount"], Value::from(1) diff --git a/rust/crates/mnote-web/src/routes/web_shell.rs b/rust/crates/mnote-web/src/routes/web_shell.rs index 2139877c..209aa642 100644 --- a/rust/crates/mnote-web/src/routes/web_shell.rs +++ b/rust/crates/mnote-web/src/routes/web_shell.rs @@ -1210,6 +1210,18 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str { titleEndpoint: '/api/documents/title', editorHostKind: 'leptos_tiptap_island', }); + const syncPageAggregateScript = (session, aggregate) => { + if (!session || !aggregate) return; + const scriptId = session.pageAggregateScriptId || '__MNOTE_PAGE_AGGREGATE__'; + const node = document.getElementById(scriptId); + if (!node) return; + try { + node.textContent = JSON.stringify(aggregate); + node.setAttribute('data-mnote-page-aggregate-synced-at', String(Date.now())); + } catch (error) { + console.warn('mnote Page Aggregate script 同步失败', error); + } + }; const documentSessionRegistry = new Map(); const localFolderEventRegistry = new Map(); @@ -1622,6 +1634,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str { } const nextRevision = Number.isInteger(nextBody.revision) ? nextBody.revision : revisionFromConflictKey(nextConflictKey); session.latestAggregate = nextAggregate; + syncPageAggregateScript(session, nextAggregate); session.title = nextAggregate?.head?.title || session.title; session.currentTiptapDocument = nextTiptapDocument; session.currentSerialized = nextSerialized; @@ -1891,6 +1904,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str { sourceKind, rootUri: runtimeDescriptor.bootstrap.rootUri, saveEndpoint: runtimeDescriptor.bootstrap.saveEndpoint || '/api/documents/save', + pageAggregateScriptId: runtimeDescriptor.bootstrap.pageAggregateScriptId || '__MNOTE_PAGE_AGGREGATE__', latestAggregate: runtimeDescriptor.aggregate, title: runtimeDescriptor.aggregate.head?.title || '无标题', currentTiptapDocument: tiptapDocument, @@ -3082,6 +3096,7 @@ mod tests { assert!(html.contains("mnote.tree_live_bootstrap.v1")); assert!(html.contains("/api/tree/events")); assert!(html.contains("data-mnote-tree-live-transport")); + assert!(html.contains("syncPageAggregateScript(session, nextAggregate);")); assert!(!html.contains("mnote-web-document-shell")); } diff --git a/rust/crates/mnote-web/src/routes/ws.rs b/rust/crates/mnote-web/src/routes/ws.rs index a294020e..922f3719 100644 --- a/rust/crates/mnote-web/src/routes/ws.rs +++ b/rust/crates/mnote-web/src/routes/ws.rs @@ -1,7 +1,9 @@ use crate::app::AppState; use crate::context::RequestContext; use crate::error::WebError; -use crate::routes::stream_support::{load_stream_snapshot, StreamSnapshotQuery}; +use crate::routes::stream_support::{ + build_stream_push_delta_hint, load_stream_snapshot, StreamSnapshotQuery, +}; use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade}; use axum::extract::{Extension, Query, State}; use axum::response::Response; @@ -41,14 +43,15 @@ async fn handle_socket( delta_result = stream_delta_rx.recv() => { match delta_result { Ok(delta) => { - let notify = json!({ - "kind": "delta", - "data": delta, - "requestId": context.trace.request_id, - "traceId": context.trace.trace_id, - "workspaceId": delta.get("workspaceId").and_then(Value::as_str).unwrap_or(""), - }); - if socket.send(Message::Text(notify.to_string().into())).await.is_err() { + if socket + .send(serialize_delta_message( + &delta, + &context.trace.request_id, + &context.trace.trace_id, + )) + .await + .is_err() + { break; } } @@ -130,6 +133,14 @@ fn serialize_snapshot_message(payload: &Value) -> Message { Message::Text(payload.to_string().into()) } +fn serialize_delta_message(payload: &Value, request_id: &str, trace_id: &str) -> Message { + Message::Text( + build_stream_push_delta_hint(payload, request_id, trace_id) + .to_string() + .into(), + ) +} + fn serialize_resync_message(payload: &Value) -> Message { Message::Text( json!({ @@ -156,7 +167,10 @@ fn is_resync_request(text: &str) -> bool { #[cfg(test)] mod tests { - use super::{is_resync_request, serialize_resync_message, serialize_snapshot_message}; + use super::{ + is_resync_request, serialize_delta_message, serialize_resync_message, + serialize_snapshot_message, + }; use axum::extract::ws::Message; use serde_json::json; @@ -183,4 +197,29 @@ mod tests { }; assert!(resync_text.contains("\"kind\":\"resync\"")); } + + #[test] + fn ws_delta_serializer_uses_stream_hint_contract() { + let delta = json!({ + "kind": "command_committed", + "commandName": "tree.rename", + "commandId": "cmd_1", + "workspaceId": "ws_demo", + "requestId": "req_command", + "traceId": "trace_command" + }); + let Message::Text(delta_text) = serialize_delta_message(&delta, "req_ws", "trace_ws") + else { + panic!("delta message 应该是文本帧"); + }; + let payload: serde_json::Value = serde_json::from_str(&delta_text).expect("delta json"); + assert_eq!(payload["kind"], "delta"); + assert_eq!(payload["hint"], "command_committed"); + assert_eq!(payload["commandName"], "tree.rename"); + assert_eq!(payload["commandId"], "cmd_1"); + assert_eq!(payload["workspaceId"], "ws_demo"); + assert_eq!(payload["requestId"], "req_ws"); + assert_eq!(payload["traceId"], "trace_ws"); + assert!(payload.get("data").is_none()); + } } diff --git a/rust/crates/mnote-web/src/ssr/pages/layout.rs b/rust/crates/mnote-web/src/ssr/pages/layout.rs index a495a6bd..8d9d907c 100644 --- a/rust/crates/mnote-web/src/ssr/pages/layout.rs +++ b/rust/crates/mnote-web/src/ssr/pages/layout.rs @@ -61,13 +61,32 @@ const SIDEBAR_TREE_JS: &str = r##" pageAiSkillQuery: '', pageAiSkillError: '', pageAiSessions: [], - pageAiActiveSessionId: '' + pageAiActiveSessionId: '', + pageAiSessionSearchQuery: '', + pageAiSessionSearchResults: [], + pageAiSessionSearchTimer: 0, + pageAiSessionError: '', + pageAiPermissionRequests: [] }; function closestAction(target, selector) { return target && typeof target.closest === 'function' ? target.closest(selector) : null; } + function toggleWorkspaceSidebar(trigger) { + var shell = document.querySelector('.mnote-shell, .wolai-workspace-shell'); + if (!(shell instanceof HTMLElement)) return; + var collapsed = shell.getAttribute('data-mnote-sidebar-collapsed') === 'true'; + var next = !collapsed; + shell.setAttribute('data-mnote-sidebar-collapsed', String(next)); + document.documentElement.setAttribute('data-mnote-sidebar-collapsed', String(next)); + document.documentElement.setAttribute('data-mnote-sidebar-toggle-applied', 'true'); + if (trigger instanceof HTMLElement) { + trigger.setAttribute('aria-pressed', String(next)); + trigger.setAttribute('title', next ? '展开侧栏' : '切换侧栏'); + } + } + function escapeHtml(value) { return String(value == null ? '' : value) .replace(/&/g, '&') @@ -227,20 +246,7 @@ const SIDEBAR_TREE_JS: &str = r##" function currentPageAiContextSnapshot() { var aggregate = currentPageAggregate(); var body = aggregate.body || {}; - var serverContent = body.content || null; - var serverText = searchText(textFromUnknown(serverContent)); - var localBlocks = readLocalEditorBlocks(); - var localText = searchText(localBlocks.map(function(block) { return block.content || ''; }).join(' ')); var serverSubtree = aggregate.tree && aggregate.tree.pageSubtree ? aggregate.tree.pageSubtree : null; - var title = aggregate.head && aggregate.head.title ? aggregate.head.title : ''; - if (localBlocks.length && localText && localText !== serverText) { - return { - aggregate: aggregate, - body: Object.assign({}, body, { content: localBlocks }), - subtree: buildPageAiLocalSubtree(localBlocks, title), - pageSubtreeSource: 'local' - }; - } return { aggregate: aggregate, body: body, @@ -3930,6 +3936,30 @@ const SIDEBAR_TREE_JS: &str = r##" return 'hermes_page_ai_session:' + currentDocumentId(); } + function pageAiTimestamp(value) { + if (typeof value === 'number' && Number.isFinite(value)) return value; + if (typeof value === 'string' && value.trim()) { + var parsed = Date.parse(value); + if (Number.isFinite(parsed)) return parsed; + } + return Date.now(); + } + + function pageAiBackendSessionQuery(extra) { + var params = new URLSearchParams(); + params.set('source', 'acp'); + params.set('workspaceId', resolveWorkspaceId(document.body)); + params.set('documentId', currentDocumentId()); + params.set('profile', pageAiRunProfile()); + Object.keys(extra || {}).forEach(function(key) { + var value = extra[key]; + if (value !== undefined && value !== null && String(value).trim() !== '') { + params.set(key, String(value)); + } + }); + return params.toString(); + } + function pageAiNewSession(title) { var now = Date.now(); return { @@ -3939,10 +3969,126 @@ const SIDEBAR_TREE_JS: &str = r##" acpRuntime: pageUiState.pageAiAcpRuntime || '', createdAt: now, updatedAt: now, + source: 'local', + usage: null, + status: 'idle', messages: [] }; } + function pageAiUsageSummary(usage) { + if (!usage || typeof usage !== 'object') return ''; + var used = usage.used ?? usage.contextUsed ?? usage.input_tokens ?? usage.inputTokens ?? usage.prompt_tokens ?? usage.promptTokens; + var size = usage.size ?? usage.contextSize ?? usage.total_tokens ?? usage.totalTokens; + var output = usage.output_tokens ?? usage.outputTokens ?? usage.completion_tokens ?? usage.completionTokens; + var parts = []; + if (Number.isFinite(Number(used))) parts.push('ctx ' + Number(used)); + if (Number.isFinite(Number(size)) && Number(size) > 0) parts.push('/ ' + Number(size)); + if (Number.isFinite(Number(output)) && Number(output) > 0) parts.push('out ' + Number(output)); + return parts.join(' ') || ''; + } + + function pageAiPermissionMessage(payload, eventType) { + payload = payload && typeof payload === 'object' ? payload : {}; + var permissionId = String(payload.permissionId || payload.permission_id || payload.id || ('perm_' + Date.now())).trim(); + var toolName = String(payload.toolName || payload.tool || payload.name || payload.method || 'session/request_permission').trim(); + var args = payload.arguments || payload.args || payload.input || payload.params || payload; + var decision = String(payload.decision || payload.result || '').trim(); + if (!decision && eventType === 'permission.denied') decision = 'denied'; + if (!decision && eventType === 'permission.allowed') decision = 'allowed'; + return { + role: 'tool', + kind: 'permission', + permissionId: permissionId, + toolName: toolName, + argsSummary: pageAiPreviewValue(args), + content: decision === 'denied' ? '已自动拒绝权限请求' : (decision === 'allowed' ? '已自动允许权限请求' : '等待权限确认'), + resolved: decision === 'denied' || decision === 'allowed', + decision: decision + }; + } + + function pageAiApplyPermissionEvent(eventName, payloadText) { + var payload = null; + try { + payload = JSON.parse(payloadText || 'null'); + } catch (_) { + payload = {}; + } + var message = pageAiPermissionMessage(payload, eventName); + var existing = pageUiState.pageAiMessages.find(function(item) { + return item.kind === 'permission' && item.permissionId === message.permissionId; + }); + if (existing) { + Object.assign(existing, message); + } else { + pageUiState.pageAiMessages.push(message); + } + pageUiState.pageAiPermissionRequests = pageUiState.pageAiPermissionRequests.filter(function(item) { + return item.permissionId !== message.permissionId; + }).concat([message]).slice(-20); + if (!message.resolved) { + pageAiShowPermissionDialog(message); + } else { + pageAiHidePermissionDialog(); + } + } + + function pageAiResolvePermission(permissionId, decision) { + permissionId = String(permissionId || '').trim(); + if (!permissionId) return; + pageUiState.pageAiMessages.forEach(function(item) { + if (item.kind === 'permission' && item.permissionId === permissionId) { + item.resolved = true; + item.decision = decision; + item.content = decision === 'allow' ? '已允许权限请求' : '已拒绝权限请求'; + } + }); + var dialog = document.querySelector('[data-page-ai-permission-dialog]'); + if (dialog instanceof HTMLElement) dialog.hidden = true; + renderPageAiConversation(); + } + + function pageAiHidePermissionDialog() { + var dialog = document.querySelector('[data-page-ai-permission-dialog]'); + if (dialog instanceof HTMLElement) dialog.hidden = true; + } + + function pageAiShowPermissionDialog(message) { + if (!message || message.kind !== 'permission') return; + if (message.resolved) { + pageAiHidePermissionDialog(); + return; + } + var dialog = document.querySelector('[data-page-ai-permission-dialog]'); + if (!(dialog instanceof HTMLElement)) { + dialog = document.createElement('div'); + dialog.className = 'wolai-page-ai-permission-dialog'; + dialog.setAttribute('data-page-ai-permission-dialog', 'true'); + dialog.innerHTML = '' + + ''; + document.body.appendChild(dialog); + } + var tool = dialog.querySelector('[data-page-ai-permission-tool]'); + if (tool instanceof HTMLElement) tool.textContent = message.toolName || 'session/request_permission'; + var args = dialog.querySelector('[data-page-ai-permission-args]'); + if (args instanceof HTMLElement) args.textContent = message.argsSummary || message.content || ''; + dialog.querySelectorAll('[data-page-ai-permission-dialog-action]').forEach(function(button) { + if (button instanceof HTMLButtonElement) { + button.setAttribute('data-page-ai-permission-id', message.permissionId || ''); + button.disabled = Boolean(message.resolved); + } + }); + dialog.hidden = false; + } + function pageAiNormalizeSessions(sessions) { return (Array.isArray(sessions) ? sessions : []) .slice(0, 20) @@ -3952,8 +4098,13 @@ const SIDEBAR_TREE_JS: &str = r##" title: String(session && session.title || '').trim() || '新会话', profile: String(session && session.profile || pageAiCurrentProfile()).trim() || 'default', acpRuntime: String(session && session.acpRuntime || '').trim(), - createdAt: Number(session && session.createdAt || Date.now()), - updatedAt: Number(session && session.updatedAt || Date.now()), + createdAt: pageAiTimestamp(session && session.createdAt), + updatedAt: pageAiTimestamp(session && session.updatedAt), + source: String(session && session.source || 'local').trim() || 'local', + runId: String(session && (session.runId || session.run_id) || '').trim(), + status: String(session && session.status || '').trim(), + usage: session && session.usage && typeof session.usage === 'object' ? session.usage : null, + preview: String(session && session.preview || '').trim(), messages: Array.isArray(session && session.messages) ? session.messages.slice(-300) : [] }; }) @@ -3962,6 +4113,43 @@ const SIDEBAR_TREE_JS: &str = r##" }); } + function pageAiNormalizeBackendSessionRow(row) { + if (!row || typeof row !== 'object') return null; + var payload = row.payload && typeof row.payload === 'object' ? row.payload : {}; + var sessionId = String(row.sessionId || row.session_id || '').trim(); + if (!sessionId) return null; + var title = String(row.title || payload.title || payload.message || '').trim(); + if (title.length > 28) title = title.slice(0, 28) + '…'; + return { + id: sessionId, + title: title || '当前页问答', + profile: String(row.profile || payload.profile || pageAiRunProfile()).trim() || 'default', + acpRuntime: String(row.acpRuntime || row.acp_runtime || payload.acpRuntime || 'reasonix').trim(), + createdAt: pageAiTimestamp(row.createdAt || row.created_at), + updatedAt: pageAiTimestamp(row.updatedAt || row.updated_at), + source: 'acp', + runId: String(row.runId || row.run_id || '').trim(), + status: String(row.status || (row.runtime && row.runtime.status) || '').trim(), + usage: row.usage && typeof row.usage === 'object' ? row.usage : null, + preview: String(payload.message || row.snippet || '').trim(), + messages: [] + }; + } + + function pageAiMergeSessions(localSessions, backendSessions) { + var byId = {}; + pageAiNormalizeSessions(localSessions).forEach(function(session) { + byId[session.id] = session; + }); + pageAiNormalizeSessions(backendSessions).forEach(function(session) { + var existing = byId[session.id]; + byId[session.id] = Object.assign({}, existing || {}, session, { + messages: session.messages.length ? session.messages : (existing && existing.messages || []) + }); + }); + return pageAiNormalizeSessions(Object.keys(byId).map(function(id) { return byId[id]; })); + } + function pageAiLoadSessions() { if (pageUiState.pageAiSessions.length && pageUiState.pageAiActiveSessionId) return; try { @@ -3996,6 +4184,135 @@ const SIDEBAR_TREE_JS: &str = r##" pageUiState.pageAiMessages = []; } + async function pageAiLoadBackendSessions() { + var response = await fetch('/api/hermes/client/sessions?' + pageAiBackendSessionQuery({ limit: 50 }), { + headers: { 'accept': 'application/json' } + }); + var payload = await response.json().catch(function(){ return null; }); + if (!response.ok || !payload || payload.ok !== true) { + throw new Error(pageAiErrorMessage(payload, 'session_list_failed_' + response.status)); + } + var backendSessions = pageAiNormalizeArray(payload.sessions).map(pageAiNormalizeBackendSessionRow).filter(Boolean); + if (!backendSessions.length) return []; + pageUiState.pageAiSessions = pageAiMergeSessions(pageUiState.pageAiSessions, backendSessions); + if (!pageUiState.pageAiActiveSessionId || !pageUiState.pageAiSessions.find(function(session) { return session.id === pageUiState.pageAiActiveSessionId; })) { + pageUiState.pageAiActiveSessionId = pageUiState.pageAiSessions[0].id; + } + var active = pageAiCurrentSession(); + if (active) { + if (active.profile) pageAiSetActiveProfile(active.profile); + if (active.acpRuntime) pageUiState.pageAiAcpRuntime = active.acpRuntime; + pageUiState.pageAiMessages = Array.isArray(active.messages) ? active.messages.slice() : pageUiState.pageAiMessages; + } + pageUiState.pageAiSessionError = ''; + pageAiPersistSessions(); + renderPageAiConversation(); + renderPageAiControls(); + return backendSessions; + } + + function pageAiMessageFromRuntimeEvent(event) { + if (!event || typeof event !== 'object') return null; + var eventType = String(event.eventType || event.event_type || event.event || '').trim(); + var payload = event.payload && typeof event.payload === 'object' ? event.payload : event; + if (eventType === 'message.delta') { + var delta = String(payload.delta || payload.text || payload.output_text || '').trim(); + return delta ? { role: 'assistant', content: delta } : null; + } + if (eventType === 'thought.delta') { + var thought = String(payload.delta || payload.text || '').trim(); + return thought ? { role: 'assistant', kind: 'thought', content: thought } : null; + } + if (eventType === 'tool.started' || eventType === 'tool.completed' || eventType === 'tool.failed') { + var toolName = String(payload.toolName || payload.tool || payload.name || eventType).trim(); + return { + role: 'tool', + content: toolName, + toolName: toolName, + toolCallId: String(payload.toolCallId || payload.tool_call_id || payload.id || ''), + toolKind: String(payload.kind || ''), + status: eventType === 'tool.completed' ? 'completed' : (eventType === 'tool.failed' ? 'failed' : 'running'), + argsSummary: pageAiPreviewValue(payload.args || payload.arguments || payload.input), + resultSummary: pageAiPreviewValue(payload.summary || payload.result || payload.output || payload.error), + traceId: String(payload.traceId || payload.trace_id || ''), + auditId: String(payload.auditId || payload.audit_id || '') + }; + } + if (eventType === 'permission.requested' || eventType === 'permission.denied' || eventType === 'permission.allowed') { + return pageAiPermissionMessage(payload, eventType); + } + return null; + } + + function pageAiApplyBackendSessionDetail(payload) { + var sessionPayload = payload && payload.session ? payload.session : {}; + var runs = pageAiNormalizeArray(sessionPayload.runs); + var latest = runs.length ? pageAiNormalizeBackendSessionRow(runs[0]) : null; + var events = pageAiNormalizeArray(payload && payload.events); + var messages = pageAiNormalizeArray(sessionPayload.messages).map(function(message) { + return { + role: message.role === 'user' ? 'user' : (message.role === 'tool' ? 'tool' : 'assistant'), + content: String(message.content || '') + }; + }).filter(function(message) { return message.content; }); + events.forEach(function(event) { + var message = pageAiMessageFromRuntimeEvent(event); + if (message) messages.push(message); + }); + var current = pageAiCurrentSession(); + if (latest && current) { + Object.assign(current, latest); + } + if (current) { + current.messages = messages.slice(-300); + current.updatedAt = Math.max(Number(current.updatedAt || 0), Date.now()); + if (latest && latest.usage) current.usage = latest.usage; + } + pageAiApplyRuntimeState(payload && payload.runtime); + pageUiState.pageAiMessages = messages.slice(-300); + pageAiPersistSessions(); + } + + async function pageAiLoadBackendSessionDetail(sessionId) { + sessionId = String(sessionId || pageUiState.pageAiActiveSessionId || '').trim(); + if (!sessionId) return null; + var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(sessionId) + '?' + pageAiBackendSessionQuery({ limit: 200 }), { + headers: { 'accept': 'application/json' } + }); + var payload = await response.json().catch(function(){ return null; }); + if (!response.ok || !payload || payload.ok !== true) { + throw new Error(pageAiErrorMessage(payload, 'session_detail_failed_' + response.status)); + } + pageAiApplyBackendSessionDetail(payload); + renderPageAiConversation(); + renderPageAiControls(); + return payload; + } + + async function pageAiSearchBackendSessions(query) { + var q = String(query || '').trim(); + pageUiState.pageAiSessionSearchQuery = q; + if (!q) { + pageUiState.pageAiSessionSearchResults = []; + renderPageAiConversation(); + return []; + } + var response = await fetch('/api/hermes/client/sessions/search?' + pageAiBackendSessionQuery({ q: q, limit: 20 }), { + headers: { 'accept': 'application/json' } + }); + var payload = await response.json().catch(function(){ return null; }); + if (!response.ok || !payload || payload.ok !== true) { + throw new Error(pageAiErrorMessage(payload, 'session_search_failed_' + response.status)); + } + pageUiState.pageAiSessionSearchResults = pageAiNormalizeArray(payload.results).map(function(row) { + var normalized = pageAiNormalizeBackendSessionRow(row) || {}; + normalized.snippet = String(row && row.snippet || normalized.preview || '').trim(); + return normalized; + }).filter(function(row) { return row.id; }); + renderPageAiConversation(); + return pageUiState.pageAiSessionSearchResults; + } + function pageAiPersistSessions() { document.documentElement.setAttribute('data-mnote-page-ai-session-owner', 'hermes'); document.documentElement.setAttribute('data-mnote-page-ai-session-key', pageAiStorageKey()); @@ -4049,12 +4366,18 @@ const SIDEBAR_TREE_JS: &str = r##" async function pageAiRestoreHermesSession() { var current = pageAiCurrentSession(); if (!current || !String(current.id || '').startsWith('mnote_')) return; - var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(current.id) + '/resume', { + var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(current.id) + '/resume?' + pageAiBackendSessionQuery({}), { method: 'POST', headers: { 'accept': 'application/json' } }); if (!response.ok) return; var payload = await response.json().catch(function(){ return null; }); + if (payload && payload.persistence === 'convex_acp_runtime_store') { + pageAiApplyBackendSessionDetail(payload); + renderPageAiConversation(); + renderPageAiControls(); + return; + } var session = payload && (payload.session || (payload.upstream && payload.upstream.session) || payload); var messages = session && Array.isArray(session.messages) ? session.messages : []; pageAiApplyRuntimeState(payload && payload.runtime); @@ -4101,6 +4424,12 @@ const SIDEBAR_TREE_JS: &str = r##" pageAiPersistSessions(); renderPageAiControls(); renderPageAiConversation(); + if (session.source === 'acp' || String(session.id || '').startsWith('mnote_')) { + void pageAiLoadBackendSessionDetail(session.id).catch(function(error) { + pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error); + renderPageAiControls(); + }); + } } function pageAiStartNewSession() { @@ -4114,6 +4443,71 @@ const SIDEBAR_TREE_JS: &str = r##" renderPageAiConversation(); } + async function pageAiRenameBackendSession(sessionId) { + var session = pageUiState.pageAiSessions.find(function(item) { return item.id === sessionId; }); + if (!session) return; + var title = window.prompt('重命名 AI 会话', session.title || '当前页问答'); + if (title === null) return; + title = String(title || '').trim(); + if (!title) return; + var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(sessionId) + '/rename?' + pageAiBackendSessionQuery({}), { + method: 'POST', + headers: { 'content-type': 'application/json', 'accept': 'application/json' }, + body: JSON.stringify({ title: title }) + }); + var payload = await response.json().catch(function(){ return null; }); + if (!response.ok || !payload || payload.ok !== true) { + throw new Error(pageAiErrorMessage(payload, 'session_rename_failed_' + response.status)); + } + session.title = String((payload.result && payload.result.title) || title); + session.updatedAt = Date.now(); + pageAiPersistSessions(); + renderPageAiConversation(); + renderPageAiControls(); + } + + async function pageAiDeleteBackendSession(sessionId) { + var session = pageUiState.pageAiSessions.find(function(item) { return item.id === sessionId; }); + if (!session) return; + if (!window.confirm('确定删除 AI 会话“' + (session.title || sessionId) + '”吗?')) return; + var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(sessionId) + '?' + pageAiBackendSessionQuery({}), { + method: 'DELETE', + headers: { 'accept': 'application/json' } + }); + var payload = await response.json().catch(function(){ return null; }); + if (!response.ok || !payload || payload.ok !== true) { + throw new Error(pageAiErrorMessage(payload, 'session_delete_failed_' + response.status)); + } + pageUiState.pageAiSessions = pageUiState.pageAiSessions.filter(function(item) { return item.id !== sessionId; }); + if (pageUiState.pageAiActiveSessionId === sessionId) { + var next = pageUiState.pageAiSessions[0] || pageAiNewSession(); + if (!pageUiState.pageAiSessions.length) pageUiState.pageAiSessions = [next]; + pageUiState.pageAiActiveSessionId = next.id; + pageUiState.pageAiMessages = Array.isArray(next.messages) ? next.messages.slice() : []; + } + pageAiPersistSessions(); + renderPageAiConversation(); + renderPageAiControls(); + } + + async function pageAiResumeBackendSession(sessionId) { + sessionId = String(sessionId || pageUiState.pageAiActiveSessionId || '').trim(); + if (!sessionId) return; + pageAiSetActiveSession(sessionId); + var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(sessionId) + '/resume?' + pageAiBackendSessionQuery({}), { + method: 'POST', + headers: { 'accept': 'application/json' } + }); + var payload = await response.json().catch(function(){ return null; }); + if (!response.ok || !payload || payload.ok !== true) { + throw new Error(pageAiErrorMessage(payload, 'session_resume_failed_' + response.status)); + } + pageAiApplyBackendSessionDetail(payload); + pageUiState.pageAiPage = 'chat'; + renderPageAiConversation(); + renderPageAiControls(); + } + function pageAiProviderLabel(provider) { if (provider === 'codex') return 'Codex'; if (provider === 'claudecode') return 'ClaudeCode'; @@ -4970,7 +5364,10 @@ const SIDEBAR_TREE_JS: &str = r##" var sessionNode = drawer.querySelector('[data-page-ai-session-status]'); if (sessionNode instanceof HTMLElement) { var session = pageAiCurrentSession(); - sessionNode.textContent = session && session.id ? String(session.title || '当前页问答') : '等待 Hermes session'; + var usageText = session && session.usage ? pageAiUsageSummary(session.usage) : ''; + sessionNode.textContent = session && session.id + ? [String(session.title || '当前页问答'), usageText].filter(Boolean).join(' · ') + : '等待 Hermes session'; } var modelNode = drawer.querySelector('[data-page-ai-model-status]'); if (modelNode instanceof HTMLElement) modelNode.textContent = pageAiCurrentModelLabel(); @@ -5024,6 +5421,15 @@ const SIDEBAR_TREE_JS: &str = r##" skillError.textContent = pageUiState.pageAiSkillError || ''; skillError.hidden = !pageUiState.pageAiSkillError; } + var sessionError = drawer.querySelector('[data-page-ai-session-error]'); + if (sessionError instanceof HTMLElement) { + sessionError.textContent = pageUiState.pageAiSessionError || ''; + sessionError.hidden = !pageUiState.pageAiSessionError; + } + var sessionSearch = drawer.querySelector('[data-page-ai-session-search]'); + if (sessionSearch instanceof HTMLInputElement && document.activeElement !== sessionSearch) { + sessionSearch.value = pageUiState.pageAiSessionSearchQuery || ''; + } var skillSearch = drawer.querySelector('[data-page-ai-skill-search]'); if (skillSearch instanceof HTMLInputElement && document.activeElement !== skillSearch) { skillSearch.value = pageUiState.pageAiSkillQuery; @@ -5290,6 +5696,11 @@ const SIDEBAR_TREE_JS: &str = r##" '
' + '' + '
' + + '' + + '' + '
' + '' + '' + @@ -5332,20 +5743,33 @@ const SIDEBAR_TREE_JS: &str = r##" var conversation = drawer.querySelector('[data-page-ai-panel="' + cssEscape(pageUiState.pageAiPage || 'chat') + '"] [data-page-ai-conversation]'); if (!(conversation instanceof HTMLElement)) return; if (pageUiState.pageAiPage === 'history') { - if (!pageUiState.pageAiSessions.length) { + var historyRows = pageUiState.pageAiSessionSearchResults.length + ? pageUiState.pageAiSessionSearchResults + : pageUiState.pageAiSessions; + if (!historyRows.length) { conversation.innerHTML = '
尚未产生历史会话。
'; return; } - conversation.innerHTML = pageUiState.pageAiSessions.map(function(session) { + conversation.innerHTML = historyRows.map(function(session) { var preview = Array.isArray(session.messages) && session.messages.length ? session.messages.slice(-1)[0].content - : '暂无消息'; + : (session.snippet || session.preview || '暂无消息'); var active = session.id === pageUiState.pageAiActiveSessionId; + var usage = pageAiUsageSummary(session.usage); + var meta = [session.source === 'acp' ? 'Convex' : '本地', session.status, usage].filter(Boolean).join(' · '); return '' + - ''; + '
' + + '' + + '
' + + '' + + '' + + '' + + '
' + + '
'; }).join(''); return; } @@ -5376,6 +5800,30 @@ const SIDEBAR_TREE_JS: &str = r##" '' + ''; } + if (item.kind === 'thought') { + return '' + + '
' + + '思考过程' + + '
' + escapeHtml(item.content || '') + '
' + + '
'; + } + if (item.kind === 'permission') { + var permissionActions = item.resolved ? '' : ( + '
' + + '' + + '' + + '
' + ); + return '' + + '
' + + '
权限
' + + '
' + + '' + escapeHtml(item.toolName || 'session/request_permission') + '
' + + '' + escapeHtml(item.argsSummary || item.content || '') + '' + + permissionActions + + '
' + + '
'; + } var streamingAttr = item.streaming ? ' data-page-ai-streaming="true"' : ''; return '' + '
' + @@ -5401,7 +5849,8 @@ const SIDEBAR_TREE_JS: &str = r##" pageAiLoadProfileMemory(), pageAiLoadSkills(), pageAiLoadTools(), - pageAiLoadGatewayHealth() + pageAiLoadGatewayHealth(), + pageAiLoadBackendSessions() ]).then(function() { renderPageAiControls(); }).catch(function() {}).then(function() { @@ -5687,10 +6136,54 @@ const SIDEBAR_TREE_JS: &str = r##" if (pageUiState.pageAiStoppedRunIds[runId]) return; assistantText = pageAiAppendStreamingAssistantDelta(runId, pageAiDecodeDeltaText(payloadText)); } + if (eventName === 'thought.delta') { + try { + var thoughtPayload = JSON.parse(payloadText || 'null'); + var thoughtText = String((thoughtPayload && (thoughtPayload.delta || thoughtPayload.text)) || ''); + if (thoughtText) { + var msgs = pageUiState.pageAiMessages; + var lastThought = msgs.length > 0 && msgs[msgs.length - 1].kind === 'thought' ? msgs[msgs.length - 1] : null; + if (lastThought) { + lastThought.content += thoughtText; + } else { + msgs.push({ role: 'assistant', kind: 'thought', content: thoughtText }); + } + pageAiSyncCurrentSessionMessages(); + pageAiPersistSessions(); + renderPageAiConversation(); + } + } catch (_) {} + } + if (eventName === 'usage.updated') { + try { + var usagePayload = JSON.parse(payloadText || 'null') || {}; + var sessionForUsage = pageAiCurrentSession(); + if (sessionForUsage) { + sessionForUsage.usage = { + source: 'usage_update', + used: Number(usagePayload.used ?? usagePayload.contextUsed ?? 0), + size: Number(usagePayload.size ?? usagePayload.contextSize ?? 0) + }; + pageAiPersistSessions(); + } + } catch (_) {} + renderPageAiControls(); + } + if (eventName === 'permission.requested' || eventName === 'permission.denied' || eventName === 'permission.allowed') { + pageAiApplyPermissionEvent(eventName, payloadText); + pageAiSyncCurrentSessionMessages(); + pageAiPersistSessions(); + renderPageAiConversation(); + renderPageAiControls(); + } if (eventName === 'run.completed') { try { var completed = JSON.parse(payloadText || 'null'); if (completed && completed.output) assistantText = String(completed.output || ''); + if (completed && completed.usage) { + var completedSession = pageAiCurrentSession(); + if (completedSession) completedSession.usage = completed.usage; + } } catch (_) {} pageAiSetRunStatus('completed', runId); } @@ -6181,6 +6674,13 @@ const SIDEBAR_TREE_JS: &str = r##" if (activeTreeContextMenu && activeTreeContextMenu.contains(e.target)) return; if (activeTreeContextMenu) closeTreeContextMenu(); + var sidebarToggle = closestAction(e.target, '[data-mnote-action="toggle-sidebar"]'); + if (sidebarToggle) { + e.preventDefault(); + toggleWorkspaceSidebar(sidebarToggle); + return; + } + var attachmentAction = closestAction(e.target, '[data-attachment-action]'); if (attachmentAction) { e.preventDefault(); @@ -6362,6 +6862,46 @@ const SIDEBAR_TREE_JS: &str = r##" return; } + var pageAiSessionResume = closestAction(e.target, '[data-page-ai-session-resume]'); + if (pageAiSessionResume) { + e.preventDefault(); + void pageAiResumeBackendSession(pageAiSessionResume.getAttribute('data-page-ai-session-resume') || '').catch(function(error) { + pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error); + renderPageAiControls(); + }); + return; + } + + var pageAiSessionRename = closestAction(e.target, '[data-page-ai-session-rename]'); + if (pageAiSessionRename) { + e.preventDefault(); + void pageAiRenameBackendSession(pageAiSessionRename.getAttribute('data-page-ai-session-rename') || '').catch(function(error) { + pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error); + renderPageAiControls(); + }); + return; + } + + var pageAiSessionDelete = closestAction(e.target, '[data-page-ai-session-delete]'); + if (pageAiSessionDelete) { + e.preventDefault(); + void pageAiDeleteBackendSession(pageAiSessionDelete.getAttribute('data-page-ai-session-delete') || '').catch(function(error) { + pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error); + renderPageAiControls(); + }); + return; + } + + var pageAiPermissionAction = closestAction(e.target, '[data-page-ai-permission-action]'); + if (pageAiPermissionAction) { + e.preventDefault(); + pageAiResolvePermission( + pageAiPermissionAction.getAttribute('data-page-ai-permission-id') || '', + pageAiPermissionAction.getAttribute('data-page-ai-permission-action') || 'deny' + ); + return; + } + var pageAiSession = closestAction(e.target, '[data-page-ai-session]'); if (pageAiSession) { e.preventDefault(); @@ -6396,6 +6936,12 @@ const SIDEBAR_TREE_JS: &str = r##" pageUiState.pageAiPage = pageUiState.pageAiPage === 'history' ? 'chat' : 'history'; renderPageAiControls(); renderPageAiConversation(); + if (pageUiState.pageAiPage === 'history') { + void pageAiLoadBackendSessions().catch(function(error) { + pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error); + renderPageAiControls(); + }); + } return; } @@ -6661,6 +7207,18 @@ const SIDEBAR_TREE_JS: &str = r##" renderPageAiControls(); return; } + var sessionSearch = closestAction(event.target, '[data-page-ai-session-search]'); + if (sessionSearch instanceof HTMLInputElement) { + var sessionQuery = sessionSearch.value; + window.clearTimeout(pageUiState.pageAiSessionSearchTimer || 0); + pageUiState.pageAiSessionSearchTimer = window.setTimeout(function() { + void pageAiSearchBackendSessions(sessionQuery).catch(function(error) { + pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error); + renderPageAiControls(); + }); + }, 200); + return; + } var memoryEditor = closestAction(event.target, '[data-page-ai-memory-editor]'); if (memoryEditor instanceof HTMLTextAreaElement) { var section = memoryEditor.getAttribute('data-page-ai-memory-editor') || ''; @@ -6681,6 +7239,9 @@ const SIDEBAR_TREE_JS: &str = r##" void pageAiLoadProfiles(); if (next !== 'reasonix') void pageAiLoadProfileMemory(); void pageAiLoadSkills(); + void pageAiLoadBackendSessions().catch(function(error) { + pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error); + }); renderPageAiControls(); renderPageAiProviderButtons(); return; @@ -7343,7 +7904,7 @@ pub fn PageLayout(
- +
`)}else s.push(l[A])}return s.join("")}var Xr,ac,ic=De(()=>{yt();yl();dt();Xr=["undo","redo","paintFormat","|","currencyFormat","percentageFormat","numberIncrease","numberDecrease","moreFormats","|","font","|","fontSize","|","bold","italic","strikethrough","underline","textColor","|","fillColor","border","mergeCell","|","horizontalAlignMode","verticalAlignMode","textWrapMode","textRotateMode","|","image","link","chart","postil","pivotTable","|","function","frozenMode","sortAndFilter","conditionalFormat","dataVerification","splitColumn","screenshot","findAndReplace","protection","print"],ac={undo:"#luckysheet-icon-undo",redo:"#luckysheet-icon-redo",paintFormat:["#luckysheet-icon-paintformat"],currencyFormat:"#luckysheet-icon-currency",percentageFormat:"#luckysheet-icon-percent",numberDecrease:"#luckysheet-icon-fmt-decimal-decrease",numberIncrease:"#luckysheet-icon-fmt-decimal-increase",moreFormats:"#luckysheet-icon-fmt-other",font:"#luckysheet-icon-font-family",fontSize:"#luckysheet-icon-font-size",bold:"#luckysheet-icon-bold",italic:"#luckysheet-icon-italic",strikethrough:"#luckysheet-icon-strikethrough",underline:"#luckysheet-icon-underline",textColor:["#luckysheet-icon-text-color","#luckysheet-icon-text-color-menu"],fillColor:["#luckysheet-icon-cell-color","#luckysheet-icon-cell-color-menu"],border:["#luckysheet-icon-border-all","#luckysheet-icon-border-menu"],mergeCell:["#luckysheet-icon-merge-button","#luckysheet-icon-merge-menu"],horizontalAlignMode:["#luckysheet-icon-align","#luckysheet-icon-align-menu"],verticalAlignMode:["#luckysheet-icon-valign","#luckysheet-icon-valign-menu"],textWrapMode:["#luckysheet-icon-textwrap","#luckysheet-icon-textwrap-menu"],textRotateMode:["#luckysheet-icon-rotation","#luckysheet-icon-rotation-menu"],image:"#luckysheet-icon-insertImg-btn",link:"#luckysheet-insertLink-btn-title",chart:"#luckysheet-icon-chart-btn",postil:"#luckysheet-icon-postil",pivotTable:["#luckysheet-pivot-btn-title"],function:["#luckysheet-icon-function","#luckysheet-icon-function-menu"],frozenMode:["#luckysheet-freezen-btn-horizontal","#luckysheet-icon-freezen-menu"],sortAndFilter:"#luckysheet-icon-autofilter",conditionalFormat:"#luckysheet-icon-conditionformat",dataVerification:"#luckysheet-dataVerification-btn-title",splitColumn:"#luckysheet-splitColumn-btn-title",screenshot:"#luckysheet-chart-btn-screenshot",findAndReplace:"#luckysheet-icon-seachmore",protection:"#luckysheet-icon-protection",print:"#luckysheet-icon-print"}});function zd(){let e=te(),n=e.rightclick,t=e.toolbar,l=np(),a=!0;!l.insertRow&&!l.insertColumn&&!l.deleteRow&&!l.deleteColumn&&!l.deleteCell&&(a=!1);let o=!0;!l.clear&&!l.matrix&&!l.sort&&!l.filter&&!l.chart&&!l.image&&!l.link&&!l.data&&!l.cellFormat&&(o=!1);let s=(l.customs||[]).map((f,A)=>` -
-
- ${f.title} -
-
`).join("");return`
-
-
${n.copy}
-
-
-
- ${n.copyAs} -
-
-
-
${n.paste}
-
-
- -
-
- ${n.insert}${n.row} -
-
-
-
- ${n.insert}${n.column} -
-
-
-
- ${n.deleteSelected}${n.row} -
-
-
-
- ${n.deleteSelected}${n.column} -
-
- -
-
- ${n.deleteCell} -
-
-
-
- -
-
- ${n.to} - ${n.left} - ${n.add} - - ${n.column} -
-
-
-
- ${n.to} - ${n.right} - ${n.add} - - ${n.column} -
-
-
-
- ${n.deleteSelected} - ${n.column} -
-
-
-
- ${n.hideSelected} - ${n.column} -
-
-
-
- ${n.showHide} - ${n.column} -
-
-
-
- ${n.column} - ${n.width} - - px -
-
-
-
- -
-
${n.orderAZ}
-
-
-
${n.orderZA}
-
-
-
- -
-
${n.clearContent}
-
-
-
- ${n.matrix} -
-
-
-
${n.sortSelection}
-
-
-
${n.filterSelection}
-
-
-
${n.chartGeneration}
-
-
-
${t.insertImage}
-
- -
-
${t.dataVerification}
-
-
-
${t.cellFormat}
-
- ${s} -
-
-
-
-
Json ${n.firstLineTitle}
-
-
-
Json ${n.untitled}
-
-
-
${n.array1}
-
-
-
${n.array2}
-
-
-
- ${n.array3} - - \xD7 - -
-
- -
-
${n.diagonal}
-
-
-
${n.antiDiagonal}
-
-
-
- ${n.diagonalOffset} - - ${n.column} -
-
-
-
${n.boolean}
-
-
- - -
-
-
- ${e.chart.vchart} -
-
-
-
- ${e.chart.chartmix} -
-
-
- - -
-
-
- ${n.moveLeft} -
-
-
-
- ${n.moveUp} -
-
-
-
-
-
- ${n.flip} - - -
-
-
-
- ${n.flip} - - -
-
-
-
${n.transpose}
-
- -
-
-
${n.matrixCalculation}
-
- - -
-
-
-
-
- ${n.delete0} - - -
-
-
-
- ${n.removeDuplicate} - - -
-
-
`}function jd(){let e=te().sheetconfig,n=ap();if(Object.values(n).every(o=>!o))return $("#luckysheet-sheet-container-c").addClass("luckysheet-sheet-container-menu-hide"),"";let t=!0,l=!0;return!n.delete&&!n.copy&&!n.rename&&!n.color&&(t=!1,n.hide||(l=!1)),n.hide||(t=!1),n.move||(l=!1),`
-
-
${e.delete}
-
-
-
${e.copy}
-
-
-
${e.rename}
-
-
-
- ${e.changeColor} -
-
- -
-
${e.hide}
-
-
-
${e.unhide}
-
- -
-
${e.moveLeft}
-
-
-
${e.moveRight}
-
-
-
-
-
${e.resetColor}
-
-
-
- -
-
-
`}function qo(){let n=te().filter;return`
${n.sortByAsc}
${n.sortByDesc}
${n.filterByColor}
${n.filterByCondition}
${n.filterByValues}
${n.filterValueByAllBtn} - ${n.filterValueByClearBtn} - ${n.filterValueByInverseBtn}
${n.filterConform}
${n.filterCancel}
${n.clearFilter}
`}function Ho(){let n=te().filter;return`
${n.conditionNone}
${n.conditionCellIsNull}
${n.conditionCellNotNull}
${n.conditionCellTextContain}
${n.conditionCellTextNotContain}
${n.conditionCellTextStart}
${n.conditionCellTextEnd}
${n.conditionCellTextEqual}
${n.conditionCellDateEqual}
${n.conditionCellDateBefore}
${n.conditionCellDateAfter}
${n.conditionCellGreater}
${n.conditionCellGreaterEqual}
${n.conditionCellLess}
${n.conditionCellLessEqual}
${n.conditionCellEqual}
${n.conditionCellNotEqual}
${n.conditionCellBetween}
${n.conditionCellNotBetween}
`}function Ud(){let e=te(),n=e.alternatingColors,t=e.toolbar;return'
'+t.alternatingColors+'
'+n.applyRange+'
'+n.textTitle+'
'+n.custom+'
'+n.header+'
'+n.colorShow+'1
'+n.colorShow+'2
"}function Yd(){return Md()}function lp(){let n=te().info,t={enable:!0,image:()=>` - - `,text:n.loading,viewBox:"32 32 64 64",imageClass:"",textClass:"",customClass:""};return JSON.stringify(Ae.loading)!=="{}"&&Object.assign(t,Ae.loading),t}function np(){let e={copy:!0,copyAs:!0,paste:!0,insertRow:!0,insertColumn:!0,deleteRow:!0,deleteColumn:!0,deleteCell:!0,hideRow:!0,hideColumn:!0,rowHeight:!0,columnWidth:!0,clear:!0,matrix:!0,sort:!0,filter:!0,chart:!0,image:!0,link:!0,data:!0,cellFormat:!0};return JSON.stringify(Ae.cellRightClickConfig)!=="{}"&&Object.assign(e,Ae.cellRightClickConfig),Ae.cellRightClickConfig=e,e}function ap(){let e={delete:!0,copy:!0,rename:!0,color:!0,hide:!0,move:!0};return JSON.stringify(Ae.sheetRightClickConfig)!=="{}"&&Object.assign(e,Ae.sheetRightClickConfig),Ae.sheetRightClickConfig=e,e}var Ed,Bd,Pd,Nd,Fd,sr,Ld,Od,Vd,qd,At,Hd,Xd,Wd,Gd,rp,Ur,er,ot,Dl,ya,Ka,dn,Qd,_d,Wt=De(()=>{yt();je();yl();dt();ic();Ed=function(){let e=te(),n=e.info,t=e.print,l=Ae.showlogo===void 0?!0:Ae.showlogo,a=Ae.userInfo===!0?' Lucky':Ae.userInfo;return`
- -
-
- -
- -
- - -
- -
-
${n.detailUpdate}
-
${n.wait}
- - \${functionButton} - - ${N(a)==="string"?`
- ${a}
`:""} - - ${N(a)==="object"?`
- - ${a.userName} -
`:""} - -
-
\${menu}
-
-
-
-
-
-
-
-
-
- -
-
-
- -
-
- -
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
- - - - - - - - - - - - -
-
-
-
-
-
-
-
-
-
\${columnHeader} -
-
-
-
-
-
-
-
\${rowHeader} -
-
-
-
- - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - -
-
- -
-
-
-
-
-
-
\${flow} -
-
-
-
-
-
-
-
- - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
100%
-
-
-
-
-
-
-
- -
-
-
-
- -
-
`},Bd='
${flow}
',Pd="";Nd=function(){let n=te().pivotTable;return'
'+n.titleSort+'
'+n.titleSortBy+'
'+n.titleShowSum+'
"},Fd=function(){let n=te().pivotTable;return'
'+n.valueStatisticsSUM+'
'+n.valueStatisticsCOUNT+'
'+n.valueStatisticsCOUNTA+'
'+n.valueStatisticsCOUNTUNIQUE+'
'+n.valueStatisticsAVERAGE+'
'+n.valueStatisticsMAX+'
'+n.valueStatisticsMIN+'
'+n.valueStatisticsMEDIAN+'
'+n.valueStatisticsPRODUCT+'
'+n.valueStatisticsSTDEV+'
'+n.valueStatisticsSTDEVP+'
'+n.valueStatisticslet+'
'+n.valueStatisticsVARP+'
'},sr='
${name} ${colorset}
',Ld='
${column}
',Od='
${item}
',Vd='
${icon}${name}
',qd='
',At='',Hd='
';Xd=function(){let n=te().pivotTable;return` -
-
${n.title}
-
-
-
-
${n.editRange}
-
-
${n.tipPivotFieldSelected} ${n.btnClearSelectedField}
-
-
-
-
${n.btnFilter}
-
-
-
-
${n.titleRow}
-
-
-
-
${n.titleColumn}
-
-
-
-
${n.titleValue}
-
-
-
-
-
- `};Wd='
\u9009\u62E9\u7EF4\u5EA6
\u6392\u5E8F
\u5168\u9009 - \u6E05\u9664 - \u53CD\u9009\u53EF\u4EE5\u76F4\u63A5\u6846\u9009\u6570\u636E\u70B9
\u6570\u636E\u70B9\u8BBE\u7F6E
\u56FE\u5F62\u989C\u8272
\u56FE\u5F62\u5927\u5C0F
\u56FE\u5F62\u5F62\u72B6
\u8FB9\u6846\u7C97\u7EC6
\u8FB9\u6846\u6837\u5F0F
\u8FB9\u6846\u989C\u8272
\u6587\u5B57\u6807\u7B7E
\u6570\u503C\u6BD4\u4F8B
\u5C0F\u6570\u4F4D\u6570
\u6807\u7B7E\u683C\u5F0F
\u6570\u636E\u540D\u79F0
\u6807\u7B7E\u4F4D\u7F6E
',Gd='';rp=function(e){if(typeof e.image=="function")return e.image();let t=new RegExp("^(image|path)://").exec(e.image),l="";if(t!==null){let a=t[0],o=t[1],s=t.input.substring(a.length);switch(o){case"image":l=`
`;break;case"path":let u=document.createElementNS("http://www.w3.org/2000/svg","svg");u.setAttribute("class","path-type"),u.setAttribute("viewBox",e.viewBox);let f=document.createElementNS("http://www.w3.org/2000/svg","path");f.setAttribute("d",s),f.setAttribute("fill","currentColor"),u.appendChild(f),l=u.outerHTML;break;default:break}}return l},Ur=function(e,n){if(!e)return;let t=lp();if(n&&JSON.stringify(n)!=="{}"&&Object.assign(t,n),typeof t.enable=="boolean"&&t.enable===!1)return{el:"",show:u,close:f};let l=rp(t),a="luckysheet-loading-"+uuid.v4(),o=` -
-
- ${l} -
-
- ${t.text} -
-
`,s=document.createElement("div");s.id=a,s.className="luckysheet-loading-mask "+t.customClass,$(s).html(o),$(e).append(s);function u(){a&&$("#"+a).show()}function f(){a&&$("#"+a).hide()}return{el:s,show:u,close:f}},er=["#c1232b","#27727b","#fcce10","#e87c25","#b5c334","#fe8463","#9bca63","#fad860","#f3a43b","#60c0dd","#d7504b","#c6e579","#f4e001","#f0805a","#26c0c0","#c12e34","#e6b600","#0098d9","#2b821d","#005eaa","#339ca8","#cda819","#32a487","#3fb1e3","#6be6c1","#626c91","#a0a7e6","#c4ebad","#96dee8"],ot={BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,PAUSE:19,CAPSLOCK:20,ESC:27,SPACE:33,PAGEUP:33,PAGEDOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,INSERT:45,DELETE:46,WIN:91,WIN_R:92,MENU:93,F1:112,F2:113,F3:114,F4:115,F5:116,F6:117,F7:118,F8:119,F9:120,F10:121,F11:122,F12:123,NUMLOCK:144,SCROLLLOCK:145},Dl={fillStyle:"#000000",textBaseline:"middle",strokeStyle:"#dfdfdf",rowFillStyle:"#5e5e5e",textAlign:"center"},ya=function(){return"normal normal normal "+d.defaultFontSize+"pt "+te().fontarray[0]+', "Helvetica Neue", Helvetica, Arial, "PingFang SC", "Hiragino Sans GB", "Heiti SC", "WenQuanYi Micro Hei", sans-serif'},Ka=new Image;Ka.src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZoAAAGACAYAAACUS6SeAAAACXBIWXMAAC4jAAAuIwF4pT92AAAKTWlDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVN3WJP3Fj7f92UPVkLY8LGXbIEAIiOsCMgQWaIQkgBhhBASQMWFiApWFBURnEhVxILVCkidiOKgKLhnQYqIWotVXDjuH9yntX167+3t+9f7vOec5/zOec8PgBESJpHmomoAOVKFPDrYH49PSMTJvYACFUjgBCAQ5svCZwXFAADwA3l4fnSwP/wBr28AAgBw1S4kEsfh/4O6UCZXACCRAOAiEucLAZBSAMguVMgUAMgYALBTs2QKAJQAAGx5fEIiAKoNAOz0ST4FANipk9wXANiiHKkIAI0BAJkoRyQCQLsAYFWBUiwCwMIAoKxAIi4EwK4BgFm2MkcCgL0FAHaOWJAPQGAAgJlCLMwAIDgCAEMeE80DIEwDoDDSv+CpX3CFuEgBAMDLlc2XS9IzFLiV0Bp38vDg4iHiwmyxQmEXKRBmCeQinJebIxNI5wNMzgwAABr50cH+OD+Q5+bk4eZm52zv9MWi/mvwbyI+IfHf/ryMAgQAEE7P79pf5eXWA3DHAbB1v2upWwDaVgBo3/ldM9sJoFoK0Hr5i3k4/EAenqFQyDwdHAoLC+0lYqG9MOOLPv8z4W/gi372/EAe/tt68ABxmkCZrcCjg/1xYW52rlKO58sEQjFu9+cj/seFf/2OKdHiNLFcLBWK8ViJuFAiTcd5uVKRRCHJleIS6X8y8R+W/QmTdw0ArIZPwE62B7XLbMB+7gECiw5Y0nYAQH7zLYwaC5EAEGc0Mnn3AACTv/mPQCsBAM2XpOMAALzoGFyolBdMxggAAESggSqwQQcMwRSswA6cwR28wBcCYQZEQAwkwDwQQgbkgBwKoRiWQRlUwDrYBLWwAxqgEZrhELTBMTgN5+ASXIHrcBcGYBiewhi8hgkEQcgIE2EhOogRYo7YIs4IF5mOBCJhSDSSgKQg6YgUUSLFyHKkAqlCapFdSCPyLXIUOY1cQPqQ28ggMor8irxHMZSBslED1AJ1QLmoHxqKxqBz0XQ0D12AlqJr0Rq0Hj2AtqKn0UvodXQAfYqOY4DRMQ5mjNlhXIyHRWCJWBomxxZj5Vg1Vo81Yx1YN3YVG8CeYe8IJAKLgBPsCF6EEMJsgpCQR1hMWEOoJewjtBK6CFcJg4Qxwicik6hPtCV6EvnEeGI6sZBYRqwm7iEeIZ4lXicOE1+TSCQOyZLkTgohJZAySQtJa0jbSC2kU6Q+0hBpnEwm65Btyd7kCLKArCCXkbeQD5BPkvvJw+S3FDrFiOJMCaIkUqSUEko1ZT/lBKWfMkKZoKpRzame1AiqiDqfWkltoHZQL1OHqRM0dZolzZsWQ8ukLaPV0JppZ2n3aC/pdLoJ3YMeRZfQl9Jr6Afp5+mD9HcMDYYNg8dIYigZaxl7GacYtxkvmUymBdOXmchUMNcyG5lnmA+Yb1VYKvYqfBWRyhKVOpVWlX6V56pUVXNVP9V5qgtUq1UPq15WfaZGVbNQ46kJ1Bar1akdVbupNq7OUndSj1DPUV+jvl/9gvpjDbKGhUaghkijVGO3xhmNIRbGMmXxWELWclYD6yxrmE1iW7L57Ex2Bfsbdi97TFNDc6pmrGaRZp3mcc0BDsax4PA52ZxKziHODc57LQMtPy2x1mqtZq1+rTfaetq+2mLtcu0W7eva73VwnUCdLJ31Om0693UJuja6UbqFutt1z+o+02PreekJ9cr1Dund0Uf1bfSj9Rfq79bv0R83MDQINpAZbDE4Y/DMkGPoa5hpuNHwhOGoEctoupHEaKPRSaMnuCbuh2fjNXgXPmasbxxirDTeZdxrPGFiaTLbpMSkxeS+Kc2Ua5pmutG003TMzMgs3KzYrMnsjjnVnGueYb7ZvNv8jYWlRZzFSos2i8eW2pZ8ywWWTZb3rJhWPlZ5VvVW16xJ1lzrLOtt1ldsUBtXmwybOpvLtqitm63Edptt3xTiFI8p0in1U27aMez87ArsmuwG7Tn2YfYl9m32zx3MHBId1jt0O3xydHXMdmxwvOuk4TTDqcSpw+lXZxtnoXOd8zUXpkuQyxKXdpcXU22niqdun3rLleUa7rrStdP1o5u7m9yt2W3U3cw9xX2r+00umxvJXcM970H08PdY4nHM452nm6fC85DnL152Xlle+70eT7OcJp7WMG3I28Rb4L3Le2A6Pj1l+s7pAz7GPgKfep+Hvqa+It89viN+1n6Zfgf8nvs7+sv9j/i/4XnyFvFOBWABwQHlAb2BGoGzA2sDHwSZBKUHNQWNBbsGLww+FUIMCQ1ZH3KTb8AX8hv5YzPcZyya0RXKCJ0VWhv6MMwmTB7WEY6GzwjfEH5vpvlM6cy2CIjgR2yIuB9pGZkX+X0UKSoyqi7qUbRTdHF09yzWrORZ+2e9jvGPqYy5O9tqtnJ2Z6xqbFJsY+ybuIC4qriBeIf4RfGXEnQTJAntieTE2MQ9ieNzAudsmjOc5JpUlnRjruXcorkX5unOy553PFk1WZB8OIWYEpeyP+WDIEJQLxhP5aduTR0T8oSbhU9FvqKNolGxt7hKPJLmnVaV9jjdO31D+miGT0Z1xjMJT1IreZEZkrkj801WRNberM/ZcdktOZSclJyjUg1plrQr1zC3KLdPZisrkw3keeZtyhuTh8r35CP5c/PbFWyFTNGjtFKuUA4WTC+oK3hbGFt4uEi9SFrUM99m/ur5IwuCFny9kLBQuLCz2Lh4WfHgIr9FuxYji1MXdy4xXVK6ZHhp8NJ9y2jLspb9UOJYUlXyannc8o5Sg9KlpUMrglc0lamUycturvRauWMVYZVkVe9ql9VbVn8qF5VfrHCsqK74sEa45uJXTl/VfPV5bdra3kq3yu3rSOuk626s91m/r0q9akHV0IbwDa0b8Y3lG19tSt50oXpq9Y7NtM3KzQM1YTXtW8y2rNvyoTaj9nqdf13LVv2tq7e+2Sba1r/dd3vzDoMdFTve75TsvLUreFdrvUV99W7S7oLdjxpiG7q/5n7duEd3T8Wej3ulewf2Re/ranRvbNyvv7+yCW1SNo0eSDpw5ZuAb9qb7Zp3tXBaKg7CQeXBJ9+mfHvjUOihzsPcw83fmX+39QjrSHkr0jq/dawto22gPaG97+iMo50dXh1Hvrf/fu8x42N1xzWPV56gnSg98fnkgpPjp2Snnp1OPz3Umdx590z8mWtdUV29Z0PPnj8XdO5Mt1/3yfPe549d8Lxw9CL3Ytslt0utPa49R35w/eFIr1tv62X3y+1XPK509E3rO9Hv03/6asDVc9f41y5dn3m978bsG7duJt0cuCW69fh29u0XdwruTNxdeo94r/y+2v3qB/oP6n+0/rFlwG3g+GDAYM/DWQ/vDgmHnv6U/9OH4dJHzEfVI0YjjY+dHx8bDRq98mTOk+GnsqcTz8p+Vv9563Or59/94vtLz1j82PAL+YvPv655qfNy76uprzrHI8cfvM55PfGm/K3O233vuO+638e9H5ko/ED+UPPR+mPHp9BP9z7nfP78L/eE8/sl0p8zAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAHBbSURBVHja7J13mFxV+cc/M7OzLbvpjZBKQhISUgAhgAlVkADSRcDyky4iYAAhAmpQQtMIiqigYkV6FRHphgBBKSGBkB5IIb1sdrO72dmZ+f3xniGTzZR755a5s/t+nmefTGbuPec7Z8497ynveU9on5m3YJMuwNnAt4B9gA3A48CPgY0UyPxJUzN/MLMShxwDXAMcAmwGbgD+UlBKhzVn/+yqqwrVFwIuBL7tZnkyYwaK98yaNb/QW6uBk4DLgfHAduDfwI+ApYUmOnHiKC+0AowAfgh8EYgAvwF+AMTd1OlQ40nAtcB+QIMpzx8Cy70oT8U6YZvXn2x+tN8A44ByYE/gMmA18B9g7wB9v58AzwNfADoBA4A/m4c5KPwWuCdDef4X6KVVtF1yGPAx8IDpAFUBPYGvAguBucCRAdJ7CvAecA7QA+gKfB94DKgMiMargaeAQ0159gK+BrwNqLUoIUNzFPAw0C3L5+XmAXoZ6B+A7/ZNM3rJxDTgl2Y0UUwmAxdl+WwI8HOtou2OzwFP5+hERIAxwDPAhADoHQvcbxrvTB3PZ4HaImscDmSbmukO/F6rXWkYmgnAP4wxyUd/U/m6FPF7lQO35bnmMuCvQLSIOs/J8/npQIVW03bDPjaejWrgn8boFJPpRks2jgReKfLo+yygLMfnhwCDtPoF29CMMUam2ka6Y8wDVV2k73Uw0NvCdV9F1kOqiqQzX+WvQqfP2gsDkWlcO79nD/McFWuGoAJZk8nHAcBM8x2LwRAL1+ytVTC4hmawqeiFNHaHInPQkSJ8rxob156ILBp2LYLOkAu/kRJ8egEvFWgw+iPT0cXocJTnGSmkMxJ4zfxbDJ1WjKYSQEPTC3jBYW/qJODX+L8WMhuos3H9JDP876NVQnGZLsbIDHOQxt5mVqGTz9rrgTdtjtpmmhGOouQ1NF2MkRnmQh4XATf7/L02GyO3zcY944FZZhSnKG5QDfwLd9ZZJiBOBOU+f4evAAtsjt5eIVhec0oADU21qdDjXMxnKnCJz99tJuIpt97GPcOA14HRWjUUh5QDTyAL0W5xFPAn/J2OXoV4k75j455aZMr9ZK0GSiZDE0XWVQ7zIK+7kI2efvKO+S6f2LinnzFSB2v1UAokAvwNONaDtM82z5KfbDBG7lUb91Qi+2z+T6uDkm5oQsAfkSknrx6+P5kK6ycLkTWYj2zc0x14EYkqoCh2CCGbcL/sYR6XIDve/WQbsu/rKZvP/B+BKVotOjbpHiW/RNx9vZ5OeByZv32vwDQmG51DsOdJYne/TCdk09xXgUcdfN9DkE2urwGbXCjDLsDvEG+5rcCTSASENVqdA8HtwAU+5HOjGWn8psD7RxmDNRZ7TgZ2p+1CyMbjzkZzoR3izyGOSf8DVrrUFv0S2cvWgqyl3Qgs0SrsnaH5IfAdn/LsgrgTH2rzR031FC/ysXzKzSjsBex5sQGci2wa7ZXWIzzZ5vRDW6oR76NJ5v9VprE4wxie/2qVLirXIaFQ/OIu03l52OZ93wTuxd/NytOQdZv/2bzvGMRzNeWY1Aqch2y2djLqvK9Nx/prSKidc8wzprhI2PxoN/qcby/Tg7CzN+BbPhuZ9JHNWTbvmW4qcvr362xGH/sXqCOCrJ9NylKezwB9tUoXjfPM7+4nEdPg2pmOHoXE1itGRIyLbV7/VdNODGvTOXY6xf8rMs/e1AAPoc5Anhia64uU9zDsxSD6VhHL6ds2e2DX5RjNPYd9t/GQeThOymO8r9cqXTSKVfblyNRuZ4vXX4D/LtLpowarcdGGmPYhksXAPkhh671T8zzPVfi/HaNDGJo9LF77EWAlhvetNvK342s/oojlZGe387UWRnN2A4/ebNHQnqpVumhYfY5WIfu18vFzZO3ACt2QfWBW2KeIZVSB9ZhjV5A7MnQVst5rZ3PoNy0akckUL3xWuzU0b1h8OI5FNkLm4zc2jI2dDZUbi1hOG2xce6iFawYg61TdLVx7iemFWaGzVumi8YbFenQcsMLCtS8jrsxWz3tpKIHnCGCLxeusRK7ugkytWYl1dpIZIVmJUhLFXhgrxYKhud3Cw3GUMTZWuQ5ZuM/HEzbSfLqI5fSkjWuten+Nwto5GVNt5P2KVumike8EwTrEYeNDG2k+jkQZz8cyrHtxPlHEMnoLObfKTYPUC2ubYi/Cusfc+9jb6K1YMDTP5xhO1plh5GKb6SYRL7an8/Ss7Cye/gg5LMpvmhCPGatML9JvuQR/3GqVzLyU47dvBE6jMK/A35B9zS/1rH3X/GvV0DxVpDK6zsa1P8H61KHbsxdnaHV239CALGSex65TRNsQd793Ckw7bob+r2X5MU8E1tqsABOQHdfNPpbRbJvTDffZNExuPRyTsTfFp7jPDchO+LVtjMyZyFSYk9HS3VmesSuw546bRDaT3oi/02hbsrQF2XgT2SIQ91FjI/AldC+N66Rv2PwjcpLeoUjI/FkuVMTUD/d7Y1i24GyD4Xrg66bn3gfrIcwxUxDftZnfJxTmUn2jGdJf6sNvmBp16sMRDP6CuKEfjBzP/KbNDlWu+rsNcQqJIGGSbkKmo+wSM52hn5jnyM5xzIcj0+J2PNe2m05nzKbOvyPrML/24XdrQQKIvqVV2FtDkyrsVz1oCN0Ox7EDawuqIIt/PynAyHyALNyudtAwdMXbaAstZkrmHa3KgSJms/dudSRyHfamn6zMOnxq4/pTTaNvx8hsAk5w0ID/BnGaucnD3yuJzOg8o1XXG9r7oVoRJFyL3T0Os5BgnKtdqLxeOTHETfovazVWfOAi4BGbo5+V5jlyOkqYDvzCw+92HTKbo6ihsU3Kz/58m/c9jbhyb3FBQwvZ16mccpk+HIpP/BCJJmAnztl84PNY23tnhSke1fefYW/vn6KG5jO6IN50dsNU/AGZimpyUUtqnWqei2neSuHBFBXFKiHECcFuiKo3kVBJK13UkkQ2XLo5Q3A/cI3+zGpoCuVeYGIBw/ML8cbLpQ44GncW7H+Hu/P0ipKNb2Mv/BLIOscXsLa52y6tZobgPy6k9SIy9ZzUn1kNTaF80ca1CeByxDXVy0q3AZmSW+UgjaeRSAH6cCh+MNnm9X9CHAYaPdTUiERBn+sgjbdMGi36EwfT0Fhp4BIB+F5Wp75akLDgfp1YuBw4nsLOpXkDeyFJFMUp221ce5sZIbT6oKvOjJoKmSFYgkxlN+rPG1xDk8+luIlgbBq0Emaj3jT6D/msbR7i7tlo857j9eFoN6x04VnzAysRBJLIQv1Un0fahcwQrEK2LOjG5oAbmnxeH48he1yKzfQ8lWkdEjn6pSLpewtxVGix+HAcj/2D15Tg8mCekf/7uOs84kTnm3lmBL4G3FkkfXZmCOrMtUu1+vlPmc3r/4UsRl+Y5Ue/MiDfa7UZNfyT3Q9XW4as4RR7J/1LyE7k+5Aw722JmYf8Apyt6yjBYw4Su+8nGT7bioSxCQIJJAzVv9j9wL4G4HTEu7OYzEPWkv6BRDloS9xcc4mbxnvWrPmB+IEmThxVEhW+EGeAi82PNtf0aFYju4UPCtiQ9H/AOGT9ZRUyrfcY4tsflHAtTyIhzi/NUJ79kHAfi1HaIzchMdBmI7H7NpsRxEFmRBMU1iMenFORM6l2GM2HB8DIpD/rQ0x5vmnKcxMyA7M3sJ/RrJTIiAZkHva3WDsGoNisQTzKLg+wxjpjWH6t1bFDUY1sgEyav4TpfScCqLUJWey/LcDl2YRELnikBH77k5EDEsebkeG/KF50en8MzfxJU/WRd4MZM7QMFKschhy/nD6tW4XExTsL2U1/BR3wfKHUVFCxp6Y8nJK6po3BrgK+gawfTQIW6IimKI9kM4rSjvgcsh+qS5bPI8AYZOPjUbgYTXjixFFWGvAIsmbY3fzb9nWuz3Yga7h3k8VrzmEDPhwYbUYBb2L9VNFUg/4dY8hHmbZvCTKd/jNkbcxrRpL97K+eSJifw9XQKIrihH2AZ3MYmXSqEWeWI/HOA60PEl7mc0APYzScHAdebXrs1yBrjlchO/Cd0h/4K3BE2nv1yHTTneR3q+6OBJ8dl6Hhv96MJCfhvdPN2eSOF3cYMJBguLa7a2hGvVb8eHLtYvruqquCo0Wn8YLIQGTxvJeNe3oYw3SIB41gZyRK+TCPvu9Y4DkkUsA/HKTTAwlKO7jN+7XAz5HF/kvzGJvpGYxMOoMR54xvelwHBlu4ZkB7NDRhff4VxXN6Ie7s/Qvszb9s00BZ4bseGpkUEeTQw04O0rg+TwN9CRIMN9dIYYKFfD7nR8fewjWh9vgAhB1WgM3m73qUFF8CPkRCcSR9+ms1eZ6kxR84uhgj46RR39uMCjq5qOsMn75/byQieqGcbuGac5GTTbMZGysLvT20qgbP0HzJDDVTi4A3aSP3GX9GFhsjPuYZMXn+RYs/UFQjrqtjXEhrAuJEUO6StmE+lsP3HdxrdRR4DvBwlvKxEkm6m1bXYBma/mTe8zHDg+F9KfJQB81b2ZVyJObeIS6meRQSIdmNToyf+3VGOrjXztrUaYgXWUUBhqYC8UxTAmBouiCLk/2z9JAecrHHVapcgr1z2N3ieiRqg1J8IsDfkKCPbnM27kQb93OjipN1h6dsXn8iMs1Ynfae1dNydVTjEWU2H57780wDHImc7X2JawpnVgantKzv6TkTWcD1y+g+CNyi1TkQhJCoGV/2uDOzFvixgzQeBg70qUycbIb7MbKTfqCNe44xHeITkb02dgzNp1qFizuiuQsJVJmPb2H/VL72xuv4d3rf28D56GFoQeF2JBCq19zosEP3K5wdHmYHJxHdNxrDYfdY6FQsti42DI1Th4Awspb2JSRWoRuUIxs5601Z/AUY2l4NzVSblfpOZD7ZbWqR8A2fIHPMXnlxJUwet5k8C+F+vD9y+VO8P9FQsc51wNU+5neXGT0XOso4GtnJPx8JTPk8Mv39G2QH+/eQTZfFrl+LjOH42OZ9hyAef1an7pxMnU0GFiLBO59Gotmf5fB7p6ZgLwJqjCH8OhL9e3IpPRhWps5OQjY82SFqKuwhuBcpuRqYiQSi82P6YyCyw/lYJOJzIQ/brchu8G94NB1xGnqEQFA4r4DnxCkRZMf8RmSqtpDRwkU5Pu9sjE91AMp3ObJz/kUkFI1VDjDPoJeG5qtmpBFuMxL5i2k3nnbQkcg0BVuDxMo7gBKJjZZvRDMBeIDCvNN6Il43XVzSer1PRqYt43G2T+jCAhsBK+m+hRIUirWXrNw0Op1dTrcaibc2IUBlvBIJQ/NRAd/FK0OzN7JhNJylw/2AMZB2yTeLVF2Ejo0nhmYvdvfesMu+Zujnhjvm2UUsJyd5t5iRh5vxqm435aoEhz0sXrcKCf2Sj59j7QTWVAPpZiesCvH2mhTAcl6DTKPN8SDtQgzNVezuTt3WIDyNhOSxyiVkD76ZzomzZs0vD8ohbIUYmi7IoVxu7Is50WKh5WPPIpaT07zrkLUUNw6Gewbv134U+7xh4ZoNyJn1VmJZvWw6OHGL+Te49D1SI6QvBLisNyDrS28HwNAcYOGaLsiU3xAL156ETJmFLP5WnUrh4QhnEf847uxmTnENcra4E1YXsZzcyHsp4o3iZGH1Q1OOcZSgkc+9vM50uj60kebjwGUWrlsGvOfCdyhDXOWPd7ls6jwo783GGL7uYprdC7hnq8XremFt8+73sD4D9DbWPeoCZ2juxRuPsd/hbL73gSKWk1t5v2V6qYW4Im9Czm+vQwkiL5F9zrwRmT79bwHp/ibPCDaJBMh06t6e8nA6tYTKvM6MEP9TREPzQ6xPcbo9qju7VH6otoZmKvB/HuVVaXpo/Qu8fzrezMvmYw7uLro9jf2pr1bEjXUJSpC5wTw/a9sYmdQGXiejpbszvB9HTuL8h0PdISTK8lds3vc84tJbTBrMCOzfLqRVyNTZm/i3Zy69Tn2plNqDdEPzVdxZS8lFP2Nsqgss3MOQmGp+nNewAlmQPQz39xHcanqqVvku3niuKe7zF8Q1/jAzihmKHGDmlMuMwdkCbEPW6j6P83A0IVMXv2nzvv+Y0Y9fvfnO5rvOR6ahlwLvINNHM3FnDbdQ9+b7kdM7/aDFzGyUlMdpah/NocB9+HMWwoGIO+A5BfQC6pENcVdT+lyGnLORb+PVPVl6s0pwiSGHdblJ0oyE3XYE+Tn2Y+S9ifP1Rrsd4ieREFde4mTD5q+RdZhpHupLmtHTC6X2QKQMzSP4GwzzLMTdN/8I6rBm2ilxZI71NbI7XrwCXK7ttuIRl5vRsh3eMZ2jeh91nuaDkXFqaEDCAvVCTvz0gmvN6KnkCJthc78i5D0dCT7YkalD5pcz7e5fisyZt6Ao7lNlGkY7zAO+yK4OKVY2czvtLZ7uU5m40au9zCNjcCvw01KtbGG8jTJrZWTT0VlF5iCkV+LOvhtFycQEoKuN6z9C9q5savO+lViAOxxqHe5TmfzZhTRS01vPuKjLj7iJnhua85EFNj/3ZsRNnv+nzzsgXkM3ID75683rp7VYFA+xEyx2CRJBeUOBowCnbUvYpzL5lUvptJjZCDf2+PwT/73aXKcMCTXxlD53RWc6JRS7SCl5rG4c/QTZV5dt0/LLFkYcTj2kluB9nMMluOuq3Ygcq5JrDRYL5XYm7WD6vEyfN5eYMUPLQCklliFhUXKFmlllPs91FsxU05B+PsvnryOL2E54AjjDw7JIIjvy3R411CHR319DTiC2a/j89Ozz1tDMnzRVHzlF8Rcrh3j5sVfsXNMIDs6i8RjybwqsQ/YMnYl4h40w7y9FTvF8GDnfyQkPIftUDvGgDLYi3ndPelTGaxEvvVexvtdnFeJl127WaHVEoyj+86DpQWdbe3gfd6N952rQPocccfBlJGrHKmRT9XRkvdAKCfOdHvRIZxyZhrrNGLVo2mchcjs1VJM5uvJyxJPrYazHKyuUJcbYvEb+Y1NyeaKWrqEZ9dqtgRCiIyufuOqqYOnpmFOOc4AfAT/J0sP200lmE+LheGVQCidL2Pst5D6kzRMNEyeOcivJecg02jNkjorfYurFFW52MoJyhEDpjGhmVhY3//a7cVQpDjchi89XIgvdjUjssB8Ci7V42iX/RaYpT0M2dY4HtgP/An6A/aOqS2dEU8A9XZAd7d9CjkjdYIbaP0aOhu3YtF+DGEJO9fy2/u6uUI1ETE6avwQyRZTQonHEPsZYH2PK91dIWJigHK3RiETJLoWDC7+JbEAdbUa+T5myXG83IbuG5mTgj+waqmFPI+ZiYDZwgY89sj2Rk0ArkXnaKvOXel1ptKa/rmxzTRVyBvd7wJ3I/pXt+rzuxm/bTF2kfvcTkc1/urnUOocjYZ/Sp1CqkMC2ZyF7zK5AQhAp1jkN2dyY3tu7ARiFxFbc4VZGqSm1Yk9NuTi115Y7TR1M0Q85+fN4YCI215DsGJqjkIWzbDHRypGFupcR7xAvF7MipuE7H/cCgR4E/N00mBfizt6iEOLZc4Xpae1AFgSnYf9skqh5YHoCnwKLfOylTSb7/PgQJDDj17Wds8SBpm51yVG3xyBz+UfhfZTersjR4HYX2TtlaQuWIlGmH0GiTPvFOPP8VmQxQM8iUY/9iNE2wjyrDUgAUjunn1aZ9uJMk0YYcSZ4xDxnfpxHdVgbI5POIKPjTDsJWt1xOwHZvW4l8GZ/86N28bAgrjYjJy+iTfcCHjOF7dTI3IdEqh5rHuIa02i/bvRboQb4mTGAc5C9D/ONsbkO66fxOeGcPJ+fTu5z05Wd0zr/tPhsVJtrx3ioJ2Ke1QtN47hX2t8QMwOQ7S9bWzAUOdtmKd7ufWnLLXnq4FHI4XQ9PNTQ34xCFyDTys+b5/S7Fu/vaTqgtwD7me8TZed04DvAHj6UZb4D1U7B5hHSVgzNGGNk7JwhM8ZU4GqPHg6vPWQipnF32jh/M8dI8l5knSsX5UhI8KsyNE69ERfUe32oeIMs9MJ6qR3JW4bP2yynHuY56u+Rpq/gzd6UVKP5EOJp5TVVFvM50MwoeFGePUzaR7R5vxa4A1krytcxvhnYN8fnQ43B8ZoBFmZXbJVhPkMz2FT0QhqRQ5EjkN3uce9jGlk/pjic9B7yGZEQcoZFrgOTTgUOzpPOuRYqhlNCLtSljkxvMxItpIHrj0xHe2HIvT62OYwcVOg1lTbamX2AWbgfqPN6Mm98TXEpcrZUrufkIAv5HBSQOl1utyJko5fpTTux/ieZxtTNKa4ePhbmjx0aRCsN+C/JfubMEItpDEIJKl2MkRnmII29zaxCJ5e1DfXh++9rsR47YQty0qad0eVMZHrKLawcZXAhEiE6m1GMWUijWyk+BOEcD8cLDh+OFBfh7hHRW30sHyfHGFhdAAwBvyDzdGCTxTRqUYJINbJHwo11lgmIR6SbBxT6FRH4Uh/yOANxkLFKHyQszOEu5W+1Q/41xGkhmsVg5qN7ezE01aZCj3Mxn6mIa5wbzMe/0/2cjMRetHn9DHY/otpqQL1qlKBRjgSDdHMN5CjgT7g3Hb3Ip7I414c8PkEceN61cU9n0xH4kgv52/GyPZPMpxpbMTRd8McByFNDE0XWVQ7zIK+7yO/NYIUY8DufysdJ5NRbsO+K+FNjlFNY3c+jhiZYRJD9HF4shJ9tniU3eMyn8vBrF/M6JBjlf2zcU4V4iH3NYd52t0OcbO6psmlooASnz8Jteu9/RNZVvHr4/mR6ZU6ZhvinB9nQLEWm3pps3ncLO0/Ts7oPoUbb9sAQQhZ9vXTtvQR3vI8eR6aPvGaLj+W/DdlC8A8b95QBfyH7WqkVfoz9vYPHsatH76b2amjSN2z+EtmZ7PV0wuOm1/Geg3TqETfCVMTZHcYo1JmGvRHx5jrZhUrrhOcQn/MnbI46ppvfZpbF62td+F0OMRX4NRsVPt8Q/3dI5ICtSBj2nwBr2rmhuR3ZSOw1NyJ7q37jII2EeUamI8c0p/ah1LEzFE4lEoLECZsd3LsvEnVkLPacIexGPUmtldZQ2JryRuTsHrvehUcD/0aiU/s1ooki3qw9gTfMSNApnZGtFieZ+vOPdOOb+jF+SG43WzfpYgr2UPKfdZGLFjM9kYkfuWBkwJ1QNM+bwn/aprG5EXEtt4KTqbNzkfDrvdKM68kOe7rVpqJNSpueuMT08k/EflSEUuE6dl9n85K7TKfgYYedqcty/I5PFtHQnGdGh34G//0JslH2/QLuXcjO6CiDbdw30bQTj/hgaL6C7Ozvl9bGnWmjrcnWUX2CnbNVVYiH3anGgP43bH7MG31+IHshi3Be7A24HplacwO3Trd7yRS43fSO99jQTEeiF/Rq0zN5Eti/wDQjyDrfpCy/+zNA33ZoZM7D/6O4I8BfcWc6ui01pvE5xoW0Cpk6G10EIwOynODEcWk54slmtxM9AevbKQo1NNcgZwb1S3uvkzFwBzuog/dnqYM9U8972DTMxWAYEqrCTaYi4dfdws1jVF9F5o4bPGoU7HIMO9eCMo06n8O+e3sI2QF9Up5ORrHqnJcU6zuVA4+aDoKbsw7P457rbyGG5iKKd4zJN3G2Z2mFKbuPCpgJsEIhLs4HI+u/2fJ9jsLc8O8i93pkL+AHYazvfv8IcS3Oh52T1I50sXJcnaMgnUwruMlMY2zcds8uZI0m3znuvcwUgJ355pvJHxEBvN+RXgysPkersLb29nNkethqD3e8S9+juxmBu+mWXYih2aeIv2UFzjdBf4qsI8/1QF8hI5qryb1Bv5DOpdVtKyeHkcUgKw/HsViba/2NDWPjVkM+BXENdhsvjguYZYyNm1FYC5k6O9TCNQOQ9TQrPahL2NU1OxedaX9YeY42IJ5GKyxc+zLiymw1QrcbI+XeJt8DXC6bQgzNpiL/nm7kvx6ZUnrXZW3dPHre+yFLGr0tPu9WO/a9w4iXTL6H4yjsue5dh4Txz8cTLhT6Fab3ZwerD2+jR5X4ddPguGVsCjE0Vr2/Rpk/K70bq7THc1byPXR1iCPEhzbSfJzsC/XpLMOZF2dqRPYq7m7UdmJonijib/km7nhipQzWUbi7HaMQQ7PW4nXDkDiPVgyNVf4RRuZib87xcEzG/kFmScSL7ekc12zE+eLpZUhkVDvUYX0+vdHDyjzbjBK3upBWIWs004v0EC/B+hEJpcRLOcq0ETkTpRBvu9+QfS0t9ax9F2fhZAYaI2N3usrq81HI6OARZCG5GHzf5fTqgC8iWweKZWhuxr+QQ+ksBS5Pzdldj3jNpJ+SuA3ZA/JOgRnEzdD/tSyjpBNtWNlsFvUX2AsTU4csgltdpHM6tRdBDgSbgUwnXofENLsI2czZz7zvtAIUskZzH+5551llg+m4tNfTOG8A/q9NvW5E3EdfdjhaujvLM3YF9jYntmUIspPebjTj/2F902ghI5qkMc434e802mbTCXSbejOL8aILaRViaB5Fjhvx+3k/Dlid7tXxR8RN7VDkZL1ZOD8LvhGJI/R7Y1i24M7GvQvMg2fXyBxnHpBzLN7jZI0mjIT4ONmHH7RQ9+YbkUV/P4IepkbHS2jf/AVx705tiHvTYYcqffS+DXG2iCCOJTfh7ATO4abhs3vMxCzzPFsdmRY6ao8BPzAdoj7YC2VzpBkNRm3csx3nRz53ZvcNsFvTOpNuhOMp1L35DvO8f9+H5yDV9i+B3d0HW3A/JEUdsoPfLQ4yFciOkalH9qTMtjkCcDJ19nWfjIwTQ5NqwLribVSIFtM7fYeOQczFaZL0Hv515J5Gs0NPJEK7XSPzimlAttto8JyGoIkjXlxWOcN0RO0YmU3IXjcnhjuMxC87wuP65eSolOvM836Jx8/7yellWYqHVV2PPf/6BmNk3iigYXZiaM7zsUycxDpLGq1Pe6QtbtJ/GSVI/ABZm7HDv81IJjXSt7qfw89YZ99GNiXaOVr8E2R3/lsO8z7NByPjZEST3rl80CNtqfbkxbYWuNSws/em0fRSZhU4AnCyRrOfj2VS5UIP5GwPeuGpSn2/tuuB4xSb1z+NbMRN73wFzdDcaEYydsLofwB8HljgQv6n+/Q9dzi8P46sJb7ggbZrMz3vpWhorM5xNpqRzEwHIwAnazRVPpbJHBfSSM2pznNR1604C/qoeEc/G9c+ikx/txTQs64HWj3+LqmI2XYjWs9CYpOtdknHCJ9+u/tcSKPFdDbecvl5z7ifsRQNjZWGsNH0vv7j0Ag4MTQrfSwTt85lr0MWMd1YsP8d7q0nKO5jdc3jfsRDMlOUAiuGxg+PscsRT067I7RjXR5thXz67e52KZ1UZ9yNzuX9uZ73UjQ0v87zeZOx1C/luMbK5rbN2Ntc15YXfSqPjTiLvNqWDeYBXOUgjaeRxcYkSlCxcujZH5AplmwbnK0cK+3HtJndEzL/gKynNLmsww+Pyg+Aj11Mb7MxNssdpPFPZF0m6ZahsdJwJHwYNv4xxzD9FPLPPf6N3HsPms0D5sQZ4Kd4u+ET0wCcj/uBOpebyldIb/QN7IVOUYrD9DwN1m+QUO+5fkcr0y7/8+G71Nn83hd6VD+9jmaQwJu9MKkQY4Xsb3sL2SeWMy6fXUOTL0ZTE95vxkuaxvUbSCiXTUbXn5AF+OctNtCnGGPyH2Og4ubBuxeJYup0V/Ji0+Bu9qgc3jaVwytvsXnYP9pgnjFQjSi5WOnCs+aUTchRDq9m6KxdieytytexvN70sLOxDH+OILHSwCeQKbYbPBxpP4Q3mz1TMxdnWmzfCh2NfcGm0V5iRpN5n3e7YbjvJ/d+i8dw7hFh1dj81fw56R38xfx5xdPIPoXhyL6FSmR9qFva667m39TrSsQrLv11F/N5Ajnk6gfY21dQKG8ha13PWpgmWWWMTB1KPh4Evpejo/c+7jpl5PrNjjT1c7QZGb9pY4S8EdnXdoVpBEchHl9LTFvwM9wJsZSPBxC35mwRp1tMx/QhH2YYjkcOEjyMXffxhMwznY1OWZ6xpUhkiEdwP5p8W+aa5/1f5PfMTdUdSwMLu4bmX8gi74VZpluu7LBNx8ysznCNuOMVZl3HYc1upvgScirffWRe/I2ZxukCnK3r7MpVVwXr950xw83U5iCnwP4kw2dbzUjbTxaZv0JoQryNbi3irxNHNmlmOlOlHjmW4iWftGzBvmNCoFoyxMPw76aDm+l5fxuZVbL8vBdysNDFSNjrS4CRxqI9ZR6cjZ59fXcbT8UeTyI7wr9qfn//fvf2y03I0b9XImfJNJppkR9iP4itIiP8CWZ09Q1gr7R26n0tHls8i5xDdZYZKY5GplqfMbMptiNbF2JoUmeJJNv0KIo1YvAH7w3dMchRq4cg6zo34O20nl3qEI+/Xwf8IQmZEfe3kWjEG5Bw+z8OoEF8BOvnxLvCxImjaMf4Prpqx+XZgMSodOUUZLuG5mTE4yt9CmVPZPf3xchC2AXaI7PNT4xhSdEJ+DMSWdfdxdTUFFD7Ndy/bTN1kaqfJ5oer6vOKu284VZKnKDUTzuG5ihkITrbonA5sgD2sumVr/JB/56mMU5fVK80hjD9db5F+Bpkb82diNtzg4+/wTfbGJl0piEB9K7A2z0pIeBck88+iEPHayZ/u2eoRJFF4Z5mOmMR/rk6Tyb7/PgQ5IC8r2vzo42jEkxDM8E0wFY2aPVH5vgm4Z0HUgj4pZkecWvT6UHIAthGMyp7yofyL0c8VHJxGRJT6lxkIc6LsrzPGLx0YzEZmc67xOLwucYYpgvYdRFxPXJu0G0+GJx8xz+cbvTt0EdfUfzDSiM9xhgZO6HoxxhjU+2R7suREzy9iGzQE3HNPMyH8j8Ya+dzfxVZZ/Aifto5bYxM247IvcgZKPkM5gvIZrK2niq9kU1y9/pQnoPyfF6FnMehKEqADM1gYzAKeTgPRfzbIy5rDgFXe1wuEWQPgNfYCe9/IhKmvavLGr5lobx/bQx7Nk41RjMX52L/7JNC6oYbnStFUXwyNL1ML7W/g/RPMo2Um8HmhjrUZJUDgT08zmM29qYXJyFuxn1c1GDlnPjUVOXlWT4fYjGNQfrIKYoamhRdjJEZ5kIeFwE3u6i5t4/l82OP099sjLGdHb/jkfDmg13SYNXxIYSstWTalGs1OGGtPnKKooYGZF3laWCci/lMxb2jQ/3cC3GWD3nMRDz61tu4ZxgS5220C/nbjTI9g92nLq3GNqvWR05R1NBEkXUVLxbC70KCTDplCf6ccZHqxfvBO6bMP7FxTz9jpA52mPct2PcO/KnpPKSwem6PGhpF6eCGJoRsxjzJo7wiSITloxymk0A25fmBn1GIFyJrMB/ZuKe7GZEc4yDfpWbkZvdsjlvYedCR1am/Gn3kFKXjkb6P5pfkjszsBuWIm+6RWDt8LBs3mxHApAAbmsmmPIcAFTbui9rMpxMSg+iryJG7hfAccmzCEzZHHdNNHZpl8fpaF+rPIcjG29dcGtl2QQLFnogEtHwSidSwRpsHRXHX0PyQ3O6rbtIFcdM9lMJPpGtEjhz+MuIyu8O8V4ccWrYdWRM6zaHWQsJyh9g9DIrXlJvR4gsUvkn2eTOafdqmsbkR6yd8Opk6OxfZ9Nkr7bc5md3PU7FDNbJHLNVhqTL15gxjeP6rTYSiuGNozsOfw4nS6YUcOXAohceeiiE7+TPxfReMDFhfe0jnWxQnTHgnZArsHgdpvIQcdvZPm0bheI8NzXR2P4+8sxl9HIVE6bVLBFmPnJSlfj4DjAXWajOhKM4II6fkFYNhuBQZtA3fwz136kKmzr5VxN/z2y6k8Soy7edFvLdC1miOyWBk0kfHz2HfDT8E/Irc65G9ivhsKEq7MzRWNyV+BMy3cJ2dEN1Huvx9pgC3u5heIYZmRBF/z5EupTPTGJt6l/UVskYz1cLo+GXsbeK92WKH4FRtIhTFHUPzhoXrViHn02+2cO1vbBgbN48mvQKJzusmhegr5pknbobAn2WMjZuBUQuZOjvEwjUDkHW/7hauvcSC8UrRWZsIRXHH0OQbAWxA5sHthP2/DmsuyE+49D2+A9zhQfkUskbzdBF/zyddTu914DgXjU0hhsaq99co8+d0hJTOK9pEKIo7huZ5sq9p1Jlerd2DzJKm8X86T89/ugvf4RLENTtkU58VCpk6+xHwcRF+yyYkTL/bzDaj2a0upFXIGs30Ij0bS5AjBRRFcUjKvfl682C1dR89Bdm1XghxJBLAc+zu2bMB+BLOPXouBO62aWTqTONlZS2nEEOzATm/ZwbiJuvXUZazKXzaLoIcFzAe8ebbhriJN6S9vhXZpOkkWkIhazT3IVNj03x8LjaYDtYGbSIUxT1DAxIV4H7E5bgrMkfvdL2h0RiU3yP7Erbg3oa48xBXXrtGZjLW5vJTxrYQ1iMnOV6ARFq2c5LpZcB3beb3CYW7VIeR83dO9qG+FerefKPpAF3qg8ZUHVmizYOiuG9oAFpwtgEu24P7ZZfT/Bqym9uOkalH9nu8ifWYa9sd6twBrLB4bcgYYLtG5gNkHWV1gRq/7pORcWJoUga4K95Gr2hB9l+9o02DonhnaEqB/YE/YO8Aq+3GyLxhs8HzK9ZZxIzOzrd53yxkL8gWhyNDv3AS6yxptNbiTTy+uEn/ZW0WFMVdSvG0weuQkCtWaTRGZlYBPWs/DE0VEv/NrpF5Glmk3+Iw//18/O2cHkXdYkajr3mg7TJk6lhRFDU0tgJpNiJrQzPbvN/J4v3bPP4uXdgZY8wOf0CmeJpc0FDl4283x4U0Uut+81zUdSuy/0tRFDU0toxEI+I1l2kvhNWNeNs9/i73AhNt3jMd8baLu6RhpY+/3QyX0qlDgqq6sWD/O7KHuFEUpYMamrkWrmk2Pf4XsnxudW3Ka0PzRRvXJoDLgRuwvg/ICi/69LttxHqUZytsQKYOVzlI42lkH1ZSmwJFCY6hsfJAJjzWfFeez1uQGFX/znGNlbNwNgMfevxdrE59tSD7XO7yQMNP8X4tKo6sQbkdqHM5sv5WyLk0byDrPXFtBhQlWIYmn6tuE95vcnsAme7IRIMxMs/lSeNhcoe/aQb+z4cG2EoInpRb9kMeaVhsGtzNHqX/thl5eBWaZx5ytEGjzXuOx98TVBWlw2LXvfl+cu9jeAzZO+I1FyFuqJcAo02D8Qqyc32BxR72Gch+nHMRl+lOyHrF86aX78eGvelGR68sn68zjajX+zqeRnbfDwd6ItEMqpCTLFOvu5p/U68rEe+99NddzOcJY8x/AHzqQzm+hThUPEt+j8RVxsjU6eOvKME0NP8yo4kLs0xjXOm6wplZI7g8aP4KJQH8xfxZ03FYc/bPnypo0/pqTr47ddBYW2OzDPgiT11q3+AdVlB5NOKOV1ixeAn4ChKypluGz2PIZt0LcLauo3jMxRdfHBgt99xzj/4gRTA0ABcjJxpegpx/sgF4CgkmuVGL1CZPXfo/Tr57HHIq6KlAD9Mz/w5PXaqnO9rjSTOy/aqpp57Xz1JpFLXxVkrF0NQg0zhHIsEX+yLOAT2R6Z8DTI/4FdNDbyjSd2qrcwgypQMyXbI8cDqfurStzkmm0fROZ67RWWlTB/za/BWDPZCp3QPwbo9SEzKdei+FxwwsFZ19kaC/JyKH25UhzjG5PEIbzAg2E0l2j0QeQyKuP4Ksm6oXYhEMzTDgWmTBONselt7mb4LpSW5HFu1vw8Jax/xJU934Lp7rdIlS0VlcZswoRdWnIFOxtT7k9SXgKiRW3VPtVOcYxP2+d5v3y8m9FtetAJ0HA2eZTt2ZqKOIq+TyOqtEdkx/iMxrd7KRbidzz4emcfQyVL7qVILAvsiaYa2PedYi3oj7tkOd5WZ00dvn3/EE3NtYrOQxNEORhdNrsRdXLFNlucakNcwD/apTCQo/ACqKkG+Fybu96TzJPDfF4EJkmk7x0NDsjxzhO97FfMYjQS33dzFN1emuTsUZRxUx7yPboc4Di6gzQqG+m0pG2q7RDEM2O/byIK8+Ju1Dcb7OoDrd1ak4p6edi+NlYbb0q6KuTyVNtVGSYQgloKo+Rpd1zXT7tIlIq+UgG7280llR1srofqsZ0WctvWrqiYQTxBNhNjTUsnBdXz78dE92tJZ5odPW1F6nUIgxkQj7lpWxRzhMp1CIeDJJXTLJikSCD1pbmR+P2wlb0kWrtDeGpgp41KNGMb2iPYYsvBUaeVh1uqtT8ZGWqggrxnRl3dBaEpHdz+3bQhWfjuhMOJ6kz9J6Bs7bSnmT/1FyOlc1ccK+7/P5oUuIRjLnf8TwBcTiEV5fOox/fjCObU1VvuvsFApxXHk5R0ajRNt+GArRExgaiXBkNMrmZJJnW1qYFYupW5nPpE+dTQPG+ZDnWJyd/6463dWp+MSGwZ14++T+rBneOaORSScRCbFmeGfePrk/GwZ38lXngYOWc9NJj3PE8IVZjUyKaCTOEcMXctNJj3PgoOW+6hwdiXBjdTXHZjIyGegeCvG1igqmVFXRORTSClkEQzMUmOJjvlMobDFbdbqrU/GJT8Z1Y8HE3sTL7IUXjJeFWTCxN5+M6+aLzi+NncP5E2dSUdZq676KslbOnziTk8bO8ccYlpXxnaoqagswGCMiEa6tqqKbGhvfDc1UsNQpcIuoydMuqtNdnYoPrN6nCyvGdHWUxooxXVm9j7fLBl8YOZ8Tx7xPoc1vCDhhzPt8YeR8N+RkXfjZOxLhm5WVjs446RkOc1lVVS4XUJ1dc9nQ1CKbB/3mLOwt+KlOd3UqPrC9WznL93NnNLJ8v25s71buic7+3TZz6n7uxG49db93GNDNcTDwjPOFFaEQ51VWFhQ7qy17hsOcWpHV03ub1l53Dc3x2Ns86BadTN5WUZ3u6lR8YNkB3UmG3ZmiSYZDLDuguyc6v7z/25SF3TlKqiyc4Iz93/ZE59HRKN1dnPI6IhqldzisFdUHQ3N0EfM/2qNrVadSdBq6l7O1r7ueWFv7VtHQ3d1RzYBumxnZd42raY7su8aNUc1ujdWR0Wjg01Qyl/PYIuY/1qNrVadSdDYMqimJdA8c7I23mNvpDotEPPEW26+sTCurD4ZmaBHzH+rRtapTKTp1fSpLIt29e6/zRKfb6Q6NRDzR2S0Uood6oHluaDoXMf/OHl2rOpWi01xbVhLp9q71Zt3b7XR7e2gMdJ3Ge0OjKIoHxMq96YHHKtxNt1N5iyc6O1U4OtV9t8WtMg8NTYa0m7UGu2toiunGt82ja1WnUnQicW+2YkRa3U23Je6NQWxpdTTy2s3jwctAPK3J3cp0h9Zgdw3N0iLmv9Sja1WnUnQqtreWRLqbt3vjje92uhsTCc9+q01J3Z/ptaGZW8T853p0repUik7N5h0lke6KzT080el2usvj3oxp6pNJNnhoxBQxNC8VMf+XPLpWdSpFp8dKb04D7rHK3XTnrBroic73Vw1wNb2F8TjbPRh5zGlt1XgzPhiaZ5Ez6f2m0eRtFdXprk7Fa0OzqtH1EP/lTXHXDdjcVQPY2lTtappbm6p532UD1grMisVcTTMJvOpymkpmQ1OPnCHuNw+YvC2PcFWnqzoVjwklkgx6f4uraQ56fwuhhLv979ZEmGfmunuixTNzx9GacN+p9d+xGPUujmr+29rKKp0288XQANwK+GnWY8BtBdynOt3VqXhM3yX1dPvUnTPpun3aRN+l3vQlZi0Zzodr+rmS1odr+jFr6d5Ok8kYIHZ7Mslfd+xwZaprUzLJQzuyrnepB6cHhmYJcIeP+d4JLC7gPtXprk7FB0a+tp5OW5ztVem0pYWRr633LHh9Evjda4ezaquzSNOrtnbjd68dTjLpeM9L1uHQ+62tPObQ2NQnk/yqqSnXmo8u23hgaEBOafTDa2kuzk+uVJ3u6VQ8piyWYOwLa+i6rrA9gF3XNTP2hTWUxbyd4mmKlfPzF77IwnV9C7p/4bq+/PyFL9IUK/e8TF+IxfhTczOFmO9PEwlub2riU50yK4qhaQJOBzZ4mN8Gk4eT1UzV6a5OxQ9j05Jg3xfXsNc7mylrSVi+Z693NrPvi2ss3+OU7S0V3PnSsTzyzoE0tlgzGI0t5TzyzoHc+dKxbG+p8K1MZ7e2cmNjI/+z6DXWnEzyj5YWbm5sZL0aGX/rf5v/LwGOQ7yX+ric1zrkvJQlLqSlOt3VqTinGcgZ7TKUhD0/qqPvknrW7VXDpv7V1PesIB7d2d+LxBLUbtxBj1WN9FnWQMTaKKbZTZ2JZIgXF4xi1tK9OXjIUsYPWMGQHhupjO5cdmyORVm+qSdzVg5k9vKhNMeibuu0tEayMZHg983NPB0Os19ZGaMjEXqGQnQxscu2JhKsTCT4MB7n7dZWmqw7EmzXKu2doQF4F5gIPIZ7Yefnmp63m42i6lQjEyTmAAdbuTASS9Bv4Tb6LZS2NFYZIV4WItKaJNocLzRv13U2x6K8umgkry4aCUBtZTMVZTF2tEapb670Wucb2DjIb30iwb9bWvi3O79lE7BQq7R7hHP0xCcAt+PMeypm0pjgUaOoOpWgcG+hN0ab41Q2tBZqZOzmXbDO+uZKNjbUFmpk7OZ9D7C5SL/lfeh0tC+GJjXMvRYYDfzeZsE3AX8w916Lt5FQVWdbrrpKa7b//Am4vwj53m/ybm86NwKnAVt91vkOMFWrs7tYCa+6GLgQmAKcCBwJjAeGAF3NNVuB5WZo/ArwDNDghsBRoWlWL10MXDg/Oc2WzlGhaZZ1znenzItanopnJIFvAP8Fvmt+Ty9ZjrjQ3409V9xg69y1k/QfYIx5Vo5G9tZUA7k8DroChfhWrzYjmVutdAJnzZofmIo3ceKodmFo2lbS9L9ElveLhjEcD1Kc3fl2SNc5wlTwY035zQKuBpZp+11SJIBfmr8eQJe2F8Sj4fDy/bqNbexafmhrNDwyGQ7tmQzJdaEkdaFEcnVZLLGgemvszSHvbX4/EsvoHlUHbPJSZ2U0Fj51/Dtj9+y69dDKaGxkWTixZziU7AKQSIbqWhPh1c2x6II1dV3ffOy9A95vjkW90AmwCsg6RL+npiYMHGgM0ThgEJDaDLQF+AR4H3j5rqam9z6IxzPF2on50JGzrNN0AtqVW5wVQzPMTNecDWSL+93b/E0ALkY8Nh5AdqsXey3hOGTaqRm4FHguYL/B54Dn0yodwKnAJOAY7C2gKsFhU3oj+9rXhnQz9e9CIFcQsPHACXW9K1kzvHYF8Dvg7kl/W77FD533fPXPtnTu3Xsdh+298DOdF9//f1v8KNx7amqs6pwAnAlwWVXVTp0NDVt8qge2dQKf6TRGqOQJ7TPzlmyfVSIbAaeQ4RAii7Qgu9Z/RI51hfmTsk+JjnrtViffr4cZFaSOON5mpgoKWmTMpbPAdZHPAS+wc8qsLRsLNjYzZmhT78cIOn/9DAHfBm7K8TvnYyvwA+DXuXq6OevnzMrA6OSwZifPkX86czxDFqbOfNNZClNn2ZwBhgJvmpGMk22+5cA1Jq1hRfh+J6UZGczrEwM0knkhTyXsaa4Zr016SVIDPAH8ykFjg7n3LuBxk6bqVJ0lRSZDsz/wusuN23hk3WF/H79b1Bi5tlxOYYuFfhsZNTalzWDTwTrZxTRPNmkOVp2qs5QNzTBkDaOPB3n1MWn7NbK5FBiZ4f0DgHNKxMiosSlNepnfa18P0t7XpN1LdarOUjQ0VcCjHn+RXsgO+SqPv1dPZF0oG9PJ7SIZJCOjxqa0iJg67mWHahgynVKmOlVnqRmaaYjbndeMxftowz/O05gPAr5TQkZGjU3pcC3iMeg1E8k8Naw6VWdgDc1QxLvML6Z4aPn3RVwJ83E90L2EjIwam+DT39Qrv7je5Kk6VWdJGJqpyOK5X0TxLszDnRaHlt2AG0rMyKixCTZXIzvX/aLa5Kk6VWfgDU0tshnTb84iy3GtDjgF2XlrlW/jrSeHF0ZGjU0wqQTOLUK+52JvzVN1dkydRTc0x5N9x7+XdMJGGHALVCCRje3ec3MJGhk1NsFjMrvu2fKLziZv1ak6A21oji5i/m7mfRmwd4EjqwNL0MiosQkWXyiR50h1dkydRTc0Y4uYv1t590FCNRRCqICRUC729tHIpBub54G9tL0vGsU09ONUp+oMuqEZWsT83cr7JofD1yNwLzTNz3w2Mil6IRGgleKwdxHzHq46VWfQDU3nIubvRt7jgfNcSOc23NkIVcyh9LHa3pd0XfYjb9XZMXUW3dCUOne69D1GuWSwikkcpSM+S2HVqTqD/nBsK2L+TvP+MnC4i3puxLkH3vNFLM8Xtb0v2brsV96qs2PqLLqhWVrE/J3kXYlMd7lJX5xvhLoa2FCEstxACYamaEcsL2Ley1Sn6gy6oZlbxPyd5H0l3px3frUxOE6M57E4P8LWrpE5EjkOVim9uuxn3qqzY+osuqF5qYj5F5r3HsD3PdJUg0yhOWEO4hTgh7FJGZkPta0vKi8XMe9XVKfqDLqheRbYXoS8G03ehXAL3p44dx6wTwkYGzUyweEfQFMR8m0GnlGdqjPohqYeeLAIeT9g8rbL54BveKytDHf2pHhpbNTIBIttwMNFyPchoE51qs6gGxpMoxrzMd8YhS3kh4Bf4M9RzCfhjkebF8ZGjUwwuQVo9TG/1gI7RKqzY+osuqFZAtzhY753AosLuO8s4FAfdf7UJaPmprFRIxNcFgJ3+ZjfXcAC1ak6S8XQgJx66YcXw1wKP2Fzms/lcyBwpktpuWFs1MgEnxuA+T7kMx9n5ympzo6ps+iGpgk4HW/3gGwweTQWcO9eFCe2z624F6zSibFRI1MaNAIn+/AcnVzgc6Q6O7bOohsakCm044B1HuS1zqS9pMD77y1SGQ0G7nMxvUKMzXo1MiXFEuSsEC8anQ0m7SWqU3WWqqEBeBeYiLvTaHNNmu86SGNCEcvpEJfTs2Ns1gNHqZEpOd4BPg984GKaH5g031GdqrPUDU3KMk9Azmlx4o0WM2lMcMES/7eI5TTbgzStGBs1MqXNYlP3Z+DMK6kV+LlJa7HqVJ2lRq6w+M3AtcDvkRha5wDVFtNtAv6OuDC7VUAXAn82lj3kU/kkgdfx7kzwlLF5EejR5rN1yAl6rhqZiy++ODCV75577ukIxqYRCWt0DzAVOBvrZ703IfvNbvWhoVGdHVNn0Q1NunW+EJiCHA52JHIGzBB2HvC1FQkuNwcJi/AM0OCy1mXApHbYEM0BDjIjv6ONcXsOuA74WAcF7Wp0cz7wXeAEM1Idl+U5eh8JbfIs/kfoVZ0dU2fRDU3bHn76XyLL+0Vj/pMbaswPmm4Qu5iP69oYxH+OOqVXQzF0zjr1/Ew6Uz2eo5FoDZ/pnPjEHxqKVKS2ytODDkZ7IxWJ40HVqTo7CqF9Zt6S75phyBTa2Vg/q2W7GfrdhoW1mfmTpmb/8KqrrH4Xz3UyY4ZjnbNOPd+RzolP/MHaWlcWrTamzjwvzw4ydaYoHZ5cI5pKZIPkFKDcZrqdgAuQmGR3Aj9C1ny8IBA6Z516vi86Z516fl6dEyeOKvnyVBSl/ZDN62wo8Kbp0ZY7SL8ccSR40/SQ3UZ1dkydiqKUuKHZH/G0Gu9iPuOBWSZtt1CdHVOnoiglbmiGIR5PfTzIq49J240erursmDoVRSlxQ1MFPAr08jC/XsBjWPcnz4Tq7Jg6FUVpB4ZmGuLf7TVjcRaFWXV2TJ2KopS4oRmKeBn5xRQKm0pRnR1Tp6Io7cDQTAWiPuYbNXnaRXV2TJ2KopS4oalFNuX5zVkmb6uozo6pU1GUdmBojsf6zm836WTytorq7Jg6FUVpB4bm6CLmf7RH16rO9qNTUZR2YGjGFjH/sR5dqzrbj05FUdqBoRlaxPyHenSt6mw/OhVFaQeGpnMR8+/s0bWqs/3oVBSlHRgaRVEURfHU0BTzJLdtHl2rOtuPTkVR2oGhWVrE/Jd6dK3qbD86FUVpB4ZmbhHzn+vRtaqz/ehUFKUdGJqXipj/Sx5dqzrbj05FUdqBoXkWOevdbxpN3lZRnR1Tp6Io7cDQ1AMPFiHvB0zeVlGdHVOnoijtwNAA3ArEfMw3BtxWwH2qs2PqVBSlHRiaJcAdPuZ7J7C4gPtUZ8fUqShKOzA0IKcf+uENNBfnJ0Kqzo6nU1GUdmBomoDTgQ0e5rfB5NHoIA3V2TF1KorSDgwNyFTKccA6D/JaZ9Je4kJaqrNj6lQUpR0YGoB3gYm4O50y16T5rotpqs6OqVNRlHZgaFI93AnA7TjzSoqZNCZ41KNVnR1Tp6IoJURon5m35Ltmb+Aa4Byg2mK6TcDfEVdWS15G8ydNzfzBzEprOT51qTOdJ99tzRvqsObM7191laXbZ516viOdE5/4gzWdM2ZkfPviiy+2Wjc8/93vuecefQKVgp4j38jyHCn2KLNwzWLgQmAKcCJwJDAeGAJ0NddsBZYDc4BXgGeABl+/iRiKC3nqUns6T77bV53GUFw469Tzbemc+MQfGnyuG6XxuyveYLWDZ7UjpqihaVecfHdJyJz4xB9KpUQbkAgCD+rj0mEZCnwPmAz0BdYD/7YzY1EghwJTgdHARuBfyF6srS6lvwdwHXAS0AP4ELgBeEF/cv8NzTDgWnJPofQyfwcBFyFTKPcj8/R+bdBTnRbQ6SrFJieYTkZN2nv9gfNNHf4G8KgH+U5Cgq9Gzf/3Ms/DhcBpwFsO0z8GeDhtdI5J/1/A4cDr+tP7Y2gqgRuB7wLl5r1VSEDE2cBCYLN5vzswAjgYON5UxAtMJbwT+BHg1Zi6ves8AdjTR52KDUa9dmug9GRd6yyM4aYxztYhqjIdoI/MaMBNbkwzMun0A140z+9rBaZ9LPCk0d+WiOkInqS123tDMwx4DBhr/j8LuNkMlxNZ7nkD+CPiyfZFMySdiCwoH4ds2HPbAymfzu7AZaaHAvAf4K4A6gxKeSpKOt8nvyNIOXC9Gd24ybgcn9WYkcfxwEyb6X4BeCKLkUmxj/707pLJvXl/0xCOReZCv2aGsf/K0SimkzDXHmbu3WrSmmXSdot8Oochi9TTkIXsI83rOeazoOgMSnkqweeL5ndvMqPfv5hRuVcca6Pxdpu6PJ93Ms/FUTbSPBJ4yoLx3KRVzVtDMwx4DugDLAIOMEPjQkiaew8w00J9TNrDXBoh5NP5B2BAhnsHmM+CorOQ8lzksk4l+HwbmWb9PDIN2w34OvBfZGrVC/aweF0vD/J+0cI11cA/LBq6wxGvSCuu+s9rdfPO0FQhi3q9zJTMJGCZC3ksM73xJSbtx/IMW/NhRedgk2c2DkPcdIuhM4TsUdnHvM5HCBiFeN6ETRqTXNSpBJ9LgV9lmYHYA++OXggV8Tv/FGixaGyeMqO9XM/7sxaNzFZkel3xyNBMQ+ZF65A1gPUu5rPeVIStyLTPNAdpWdFppZc/tAg6RyELp4uA+UholiE50hgCvI0stH5g/kaZtCabtJ3qVILNd0zDl6vRP6Edfu/FwHlmJG/F2DxpnrO2TLJhZFqBr+JtgNkObWiGIhvzUhV7qYt5VCOLd8uQhXlMXoVM+VjVWWEhrYocIzAvdIaQRcgRadeNRxYzh2ZJYya7rsPsY0ZJITOi+Y5DnUqwuRz4pYWRRXU7/f73I1OGVoxNpTE2x6e9N9EYmU4W7o8jDg16zLiHhmYq4ko4i8LXENrSHXgE2Gb+/o3sHp9l8irED/P7HujMVsHd1jkMcRdtS39TLkPbGJlXybzQu0+aUXGqUwkuVyCu7Famr95qx+XwW+Bii8amwnTmvoSsZT3Lrvt/8hmZR7TaeWdoaoGzzf9vtviDWjEyLwBnIH7pIcSD5QkgFVztbJO3VbzQmY2kyQPgLJd0xnPcMyDN2Aw1r/vneTDa6rRbnkpw+S5y8qkVIxNH9py0Z36H7COz4qVZbkb9z1p8HuKIN+fDWu28NTTHm6HlajPqcMvIZHK9PRBYaf6qkRhaVjnB3OOWznz82+js1GY4XqjOZYhrdT5j8wqZveVSvMeuzg//LrA8Fe/oCQwke3T0XEyxYWSSprf/Ugco0/uQNZu4RWPT2aKR+QYaXskXQ3O0ef2MxR5DoUYmvRKk5kGPtJH2US7qtEIiTefRLuk8A4kGkMvY5DIyq4Azc+g8Uqt0UdkfeBNZTP4EWMPOdTQrXAn83Eb9vIBdXfXbO38GzrVobKwYmW8i0cYVHwxNarf6bB+MzErkMKw3zf/H2UjfLZ12eLNN3k51LgWOyGNschmZI8gcDaCQ8lTcZTQSEuXgtPd6Ix5j+bzGAK4GrMakTxmZ+zpgOf/VjEKcGJu4GR39Tautf4YmtQi9oM1nZWSONVSokakHvowcirXIvLeXDa3ZdBaKlTWeRW3ydkNnIcYmZWSW5tG5l1bponET2b2/voPsg8lmbL6H7Bux2khegIQn8ppkQMv674gbciHGJlV+f9Eq66+hSc1lpkI+9EHcBOuQEPF/IfeimhUjsw3Z95Hyjtli/u1sQ2tbnblYa+EaK/uEvNJpx9iszGNkCtWpuEu+UCjfBu7OYGyuQaJyW20kz/fJyIBM/VmhGPtOHkIcYOycBJtAoj//Saur/4amLY8CJ5veWTkS5uJ5oIsDI3M8/obdnpun8m8E3g9A+Sddvk4JNpcAv04zNlOxvqs/Nd3zZx/1Wg3F8mKRynMJsN3m85bQalgcQ7PNvO6C7DqfmOG6g5G4Wl1cMjLd0j63SrrOfMTIva/kWos9Ia90plyYB1hIbwCyp2Yvl3Uq7vKyxeu+hewNuY6drv5WjMy5+D/dcyv5j6NoYaeLvZ+MN+1PVxv3RBDniXO1uvpvaFJTMiPIvYM23dg4Hcnsbf61E0stXacV7jOjsfTpqVXmPauLqHu3ydsNnanNmANspJkyNkPz6FymVbpoXA80Wrz2ImC6DSPzf8giuN8sRDYyZhs1NCFrJR/4rGucaX96FHBvythcoFXWX0Mz17w+BJlOWm3B2DidLjvU/DvXhtZ0nVb5GzDINPojzGs7niZu6xxG9h3/VozNK1mMTSE6FXeZj+yhanQxzdQ+j/uL+L2eMKOHe5H1wphpI+4z7z/qs56xpv3p6SCNkPk+F2m19c/QpDZ7ncjOoHKNeYyNEyOT2iRqZ7oB08imdFrdCNcJ8fi5xfx9B2txj9rqfMklnY/kMTIryb/P5mGXylNxn1fNb+GGsYmb0XcQ9nksQTaGDkTWbVNHOS/yWce+yHqQG8cShJApzG9rtfXH0DxrhsZ7IpGL/2MayUIeFisL/8eaBrMR2dRolWfMPSmd+egPvAP8Ajlj/DTz+h2LI4p0nc+6oHMv0wPMZWRSB7TlMjb7s+t6TaHlqXjDfxAPy+0O0oibDt8DAflOIeTMl5+ZGYGfI5GS/TxGYLTpSFkxMklgh8Xv9SvsbapVCjQ09ewMwXBdWq/c7jSAVe+y682/D5i8rVKf9uBdZ+H635N5nWSE+QyfdUZy3LPKGJilpveYz9hEXNCpeMdMY2waCjQyZyPuu0FgD2M8XwCuMgZwCnK65evYW2sslFE2jcwUJLCmlfYrhETIvkKrrbeGBsS7JIZ4nH0tbRrAqrGxamS+avKIUdhhTZl0ZhvN5Br1fDHPA+KFziXI4momI3MEuzocLCH7PpuF7IwO4FSn4h2vmWfCjrFpNUYmKFGEOyGx9CZl+fwQYwC83L+1j8mjt8XrrzQzFy/YNDZ3mnsVDw3NEiSQH0i4jCE2jI1VIzPEDFMxP+riAvRm05kpr3wM8llnEpm++yjtuneR0/8yebUtRY6fndPGyJxm0trLBZ2K98ZmssWRZixgRgYkivSYPNcMQyIbeMEAY2T62DAyd6b9/2XTflmdxpyB7HVSPDI0IKc0zkX80p9P60HkMjapHf/5jExv0zPqavKY5kBzNp3prLSQzuoi6JyPzDUPN9MBnwOW50hjGbImsy8y5TfKpOGmTsVbZplnZJsFI/NowLSfbfG6r3iQdwiZ0u9r8fqr0zp36byKvWnMu9CYgZ4amibgdGRH/TAkKOS+aT/W8ey62z517PEbefLYFwn6uLe5/3SceeXk0pniY9ObzPXwLy+SzqQZfXyEtR3/SeQo50XIruYxJq1hLulUvOd18/xkCku0wzTojwVQt9VTW4d4kPcX2Om2n49ryB2Q1M7IMoIcXKh4ZGhSUz7HAetM5XkDWQcAWRAchoSnOdZ8ni+S8tdMGnuZNI8jc/Rhu+TSmeK8LCObley+M7iYOu3wNdNoDXFZp+KPsZmA7EupM9M5LyDrH48FVPMmi9dt8SDvYy1eNxVrAUlnmefFSqzEL2h19dbQgKwbTDRTMrWIO2O6F83T5gFpzJHmZHPPX00ac02a77qoPZfOsGmA90OmlV41f9PMe0sCpNPKb+SHTsV7UmtsXZEjho8F/hdgvcWMdWZlQ+Z12HOCecOisemiVdVdynL0xCcgR8ROMb2uScAKJDLAG2b6J9Xj6Y6sOxxiGsWB5v0YMm/6I/LHTCp0xJBP5wvsdN/ujuwRCqLOTOV5qNE5wCedipLOrchR5pU5rvEq1tlHeT6/Aeux4tKZDRxjjGjXLNdohA2XCe0zM+9vtTcyB3oO2c/baEsTsqP5Nix6Q82flCUG5sxKq9/FF50clqV9v+qqYOmcMUNrt1I4O5+7U81IulOWevkN0p0YDnPY/9n5HPVAYqi1dQZIkH9NxgrjgKfY3fs0DpxEapO2PkeejmjSWYyc4TDFjAaORHa4D0nrEWxFFtfnIJs9n6GwzWpOUJ2K4j6pWGffQ6ad+iKOQM+bjo9XYWg2Ia7/M8wz0oJMH9+GO6fsvo8451yBeNX2Mc/mT7EXckqxYWgms/Ps8fORXb9nmh+5GtkA9QQyBfVgEfWqTkXxn1SsM79ZbEYXXtGARNGerj+xt6Smzj5FQk2AnKrXD9jMznNOUryKrDO86rYQi1NnRddpceqs+Dp1yK+UItanoP1BnyNXDU3b/Rwhcu/xmGkaSMfRgrMamMzGpmg6Lc0973xIiqdTHwxFUQJGuMD7DkPmMV9DPDiCiupUFEUpUUOTYiKyKPg6slAYVFSnoihKiRqaFIciC96z2XkIVxBRnYqiKCVqaFJMAP4J/Bdx3Q0F9HurTkVRlBI1NCkOBP6BhNc4KcANpOpUFEUpUUOT4gBk9+07wCkBbiBVp6IoSokamhT7IRsU30PC2tcEtDxUp6IoSokamhTjkLhI64BLA1wuqlNRFMUHQxP3MN9qJAKxG6hOd3UqiqL4Zmi8DOIYY9ezvZ2gOt3VqSiK4iplPuc3F7gdOSNmfYDLRXUqiqKUmKF5Azmk6J/kjvlVbFSnoihKiRma55BT+v4T8HJQnYqiKCVkaBLA48jxru8F+LurTkVRlBIzNC3A3/D21D3VqSiK0gENzXbg98jpkSsD/F1Vp6IoSokZmi3A3cixxBsD/B1Vp6IoSokZmrXAz4HfAvUB/m6qU1EUJeCGZivQJe3/y4CfAn8CmgP0HVSnoihKgMkVGeAyZI2gHnGpHWF63UFrFFWnoihKiY5o/mH+go7qVBRFKdERjaIoiqKooVEURVHU0CiKoihqaBRFURRFDY2iKIqihkZRFEVRdiWU/E8Fo0LTPgX2MO+tnZ+ctofvSg7Lsp1kZmX6/3bRmfY6WDqfunRXnSffHRydiqIoRRrRXGAa7tXA+QHWqzoVRVFKcUTjwEg9D4SAY5BzU7we0ahOHdEEkilXrPUzu0OQI7yHAzOBKXf8ou+qYuls2B5mwIAYx03eSjwO8Xgo9VE34FFgLHLc+LkTJ47a4YXObdsijB7dxJFHb6OxMUzS3rmzruls2RGiulOCE760lepOCVp2hNws6oJ1Fhsn0ZvPAI42r78MPBTQ76g6lfZEGXAHMMH8/xRgA/DtYgmKhGFHc4jWVohEIB7/7KOngEnm9dmmE3V2AMtUdXqME2eAi7O8DhqqU2lP7JVmZFKGp6iNTUV5gk2byti4sYyqqs8G4kPTGsUUpwWwPFVngA1NH+DwtP8fZt4LGqpTaW/0zvBe16I2ImXQ3BxmyeJKQjtblJ4ZLo0HsDxVZ4ANzalABJgPLDCvTw3g91OdSqnRBTgPuBQYk+HzUIC0HgGcQhKqq+OsXFHBpo0RKisTIEeRt6XVSzGhUB6dmfFdZ97yDL5O3wzNmebfR4CHzesvB/D7qU6llKgBbgamAz8BbkMW/NNJBkTrr4FXgCeAuRUVyZ5btkRYtqySikr/JaYW/8Ph3DqzjAwCV54B1umboRmITO2AeEA8Yl4fDgwI0HdTnUqpMQlZ1O+LeBhNBs4NoM7jgEvS/j8GmBEOw7o1UVpjoVyjC08oiyTZVh8mFttlZJNRZ6mUZ0c3NNciUztzgA/N33vmvakB+m5tdX5QAjo/CLBOxXsOyfDeoQHUeUSG9yaEw5BIiNeZ34amsirBmk/LWbMmSk1NPDXCyaizVMqzIxuak4Bvmdc/Ar5o/m40733LXFNsMumkBHQSUJ2Ku0SyvF9u49piEsvw3g6QqatQEVaRysqguSnM4oVVhMPJlIasOkulPDuiobkQmdYJA38GnjY97qmIf/efzGePmGuLRTadlIBOAqhTcdfA7Ad8BTgggxHJtLhRrDWZPoj77OAMn8UtvuebzmSSwTU1cZYvq2Dt2ijV1Ymi6QyF5C9ZmuVZFENTARwPvAzca3pczyD7PA5B1hEON68vNp+Vm2tfNvdW+PA9rOhMEVSdbSmmTsUbJpvf9PfIsd4nBlTnocBi4DHz7wmloLO8InnCtvoIixdWEo0W32ciVHrl6d2I01T+84D+QNS838181jdtOB8DbjXTOnF2XT+4FnHLO8VMAU0FjjR/LUjcr1ZgS1paq4A/As/aeEid6sToCaJOfNapeDNiCZtRSCbX0+lAP/O6CvEqeyZgvdcw8CBQm9ZGPAp0B5qCrDOZ5NFoWbL7+g3RpuYdIVJrRr5WgDLYvj3M9u0hunZN0LIjUirl6bmheRhxq8zGZuBxxAtigXlvFPCltGtOMu/NB34I/B24ygwVuyOeVZk4Lu0HyEchOke30Zk+LA2Szmx4qVNxl1rgeuBgYDnwY/Nvih5IjKp0RiBurOt81noVEu5oBXA5u+7R6Mfu3o6VwEjESSXQOiPh5MhkkvcS8RChUBKfth19pjMSSV5eXx9p+eTjCgYMaAlaeRbV0PwWuNr8f7lp2NaZXtla4CPT4NUi88rDgYva/IIh4G4zxbPI/F2ILGbvY3ryIWSe8sfAEHPfvTa02tH5OaPzwjw1bUERddrBC52Ku0xBNlnWABOR3fqnszM4aqcs91X6rPNaM5JOsTc7Y+xBZoeE1Ggt8DqTEPHZIWE3nRXlyaM/Xl7JmLFNRKPJ8lgsFITyLLqhuQbxcLjONFiXmQdklRkRPG96XnvmSesIdnXTWw0sND2RF41Vv8nkkUQ2pv3AhlYrOkemTU3YIc5O12IvdTrFTZ2Ku1ycNpKNINOeg9JGNdmaPr8XE65r8/+jzEhrblody1b3VKcFndHy5NimpvDchoYwvXu3xrMYmnhHejhS88k3INGD64GDgHeRRfQPjZHYs4C09wReMmkcArxj0q43ed1g8yGzorOfw/LwWqdbuKFTcZd+ed4Lym9TleG9/gEsz5LVmUzSPxIRF+ukPpGfGZoUjyObhBYAvUzDvR9wCzCtgLSnmV7255CzE3qZEc4Ek1ehuK0zhV86neK2TsWf5ysoZNqf0aI63dcZCukDkO1B+Mj0lucA1ciZKFWIZ9R0G+nebO6pBh5A5qffRxZKP3JBdyad1QXoTO+V/N3onOODTie9Jy90Kt6QLBFNqrP96wxcj2srspN+PbIAd5l5/wbgLgtp/grxvsHcO8ykdaxJ2y3a6vyOTZ3pXG7SWG/S9ENnIXipU1EUxdeh/Xp2hkH5Ttp1cyyk+V5a2pea1zeaNN3GiU4CoJOA6VQURfHN0AD8BdlQNAA40Ly3j4U0U9ccaO5tMml5RaE6CYBOAqhTURTFN0PTAMw2rw8y/46ykOaoNvfMNml5RaE6CYBOAqhTURTFN0MD8LH5d2CGBnwJslP9NPO6raEZ2CYNL7GiMxupe5YXQacd/NSpKIrim6HZbv6NIJ5Og4CNyCL/aOQ0uCfM68vMZ4PMtZG0nrzXWNGZjUibNPzSaRc/dSqKovhmaIaaf9eYxvs2xOvpV+zq095i3tsbuN30vteYz/by4XtY1ZmJlM5hPuu0i586FUVRXKMsx2flyNGyAP9FAmZ+HwmzcQayZtDHfL7OXPMcO6Ml9zb/TkSiGMc8+g6F6mxIuweThp86U9QgwTCDolNRFMU3Q/N50wjWAW8gmwW/j+zl6JLlnjrgl8gu/TfM/7sYY/OKR99BdSqKopSooUkdzvM8srj+ADvdhj9FwqCkFqaHAMcgsZ1+YEYSZ5l7v2zS8qphVJ1KUAlleY2F93HpekUpCUPTF3GprUQW2K8G/sbu0UcjwNeAn5kG9C12TvecwM7Q+V414KpTCRrpoUiyRetNtPnN8z2jTsObRPO8V2bhvvJsn6fF94rabG981Zknn/aos6hkcwYYhITcB1kTqDQ96PHI+fbZzrj+s7nmFXPPYeazkSZNt1GdSlCoy/DehrTX29oYlZTRSPfKzHQA2uY2/88UEWKrDZ2ZjFlDG52ZSH8/li3dROKziMV1DhtGT3Xm+d3ao87ijmhmhd/d7c2Jif3T96G0IscJ3zor/K6Vg1FXT0zs/wVk/WFaWmGMmhV+95MMeTnRn1Fnhgc6o04go07gE5fLuVR0lhyzZs13dP/EiaPckvIAcEHa7/MWsLRNw/I0cDI7p7+eZedx3CDHaqxjp1PIduDJNvksM2lPSKtPD9jQ+ZAZKadYC/yvjXF8AZm6TfEBu54G+whyoF86fw2FksQTIRKJEKFQciGE5rLrqaIvBkFnm/8vRM64ae86AzmiWWJ6Uq8Ch84Kv3uzRSMjD3/43cSs8LvTgUNNGpvZdVOnW+yiE4kabeeU8AQS7Vl1Kk6ZbozCemAWcKUxAulch6yzrTdG5Zo2n8811yw0HYzfAfe3uaYVOc1zlknnSVOfrHIBsq9ss8nvKHYPdf8Vo3OzyWdym88/AM5BNiBvRE7XvSUUgkQilH4Gy5dMo7sZ+HebBrloOjPk1RF0FndEk8VQLEbOOP+MjRsX1SBrA0ea6Zwh7PSWqkMWsueYaZ5/9uw5vGFW+N3/mesd8+TmBzO9vYvOUChEMpm0pdMMc/PqPKX7Wa7pNNjSGQ6HGxKJhHs6n3wy62emHAmHwyQSiULKMy+nnHKKqxW5kPrp8rO0CnHUyMVHiCt7rg7FfebPK3aQO1IGZpT1RQsjuAcAGraHGTAgxtlf3UQ8DmknStabv4RpHLd7pXPbtgijRzdx5NHbaGwM09IS2k1nDlzT2bIjRHWnBCd8aSvVnRI0NYVtl6cXOp2O/G1yCLKfcjgwE5hSZuEBHgZcC6Gzy8urO0WjnYhGKwmHo4RCMiBKJhO9E4lY71iseUIstv3ilpbG7Rs3LnoAuK1nz+F+9byHAddWVFSe06NH7+q+ffegS5daKisriETCvPnmgow6IfkAssHTV53ZyvOQQ0YSjyd6Nzfv6F1XVz9h/fp1F69fv6axtTX2oOnl+Knz+2Vl0bN69uxd3bt3b7p160KnTtU0NcVYtmwd9fXbfSnPjz7aeeROeXk54XCY1tZWamtr2bhx0bBIJPL9aLT8rJ49U797Z6qrqygrk/rZ2pro3djY1LuubtuEtWvXXLxx4/rGLVuWPhiPx29Zu7b7kvr6esrKykgkErS07Nzfu88++xBAyoA70qbOTjHTM98ulqBIGHY0h2hthUgE4jtXHJ9i596xs82U4dkBLFPV6XH9LMthYCqBaaFQeEpVVbfyysquhMORLD3gCOFwhLKySqqqupJIxDs1N2+9oKlpyzc2blx0J/Cjnj2HN2e8+bDm3LJTn2fvgVcC02pqaq8cOHCv6MCB/ams3NVBI5lMEgqFcupMJhN3ImsnnurMV56RSJiysggVFVG6dKlh4MA9iMX2rV6x4tPzPv54ydfr67fd4UhnfiqBH1dVVX934MDB0UGD+lNdvetZbS0tccLhEOGwg/J0yIcfflgJTKuu7nTlgAGDo4MGDaCyspxQKLzbyYbl5WHKy2vp0qWWAQP2oLm5pfqTT1aet3Llx1+fPXv2HcCPxo0b10xpsFfaQ5x6sM8upqGpKE+waVMZGzeWMXhwC7FYBCQKxqQ2l54WwPJUnT7Uz7IsRmYo8GhFRe34Tp16EQ7bc24IhyNUV/egsrJL+fbtG67ZsaP+2I0bF33Zg9HN0HA4/NigQUPHDR8+jKqqCsc6zfTHEg8qScHlGY2WMXToQAYO7BddtGjpNR9/vGRyLBY7zQud4XD48X79BowdOXI4NTWdAlme77///tBQKPTYnnsOHDdy5AiqqysJh/Mf8RMKQSgUprq6khEjhjJgwJ7RBQsWXrN69Yovvv/++2eMGTOmFNa9emd4r2sxBYXLoHlbmCWLKxmy12cjwp4ZLo0HsDxVpw/1M5zByOwPvF5T03t8be0ethvFXRueMmpr96Cmpvd4YJZJ2y32j0ajb4wf/7lx48aNtm1kcukEXNXpVnlGo2WMHj2C/fc/aExlZeUbbuuMRqNvjho1Zuz48WNsGxm/ynPevHn7R6PRN0ePHjduzJjR1NRUWzIyu2sMU1NTzZgxoxk9ety4aDT6xrx58/YPwIPaBTgPOeRuTCZ7GaBG5QjgFJJQXR1n5YoKNm2MUFmZgMwxBlu9FBMK5dGZGd915i3PYOssqH6G2xiZYcBztbV79KmsdK+TVFnZldraPfoAz5k8nDIsGo3+e7/9Duo9cGA/T3TiTvBKT8pzjz16cdBBh/SqrKx83i2d0Wj0hX33HddryJBBlJWVBbI8Fy5cOCwajT6/777jew0e3J+KinLn0z4V5Qwe3J999x3fKxqNPr9w4cJiBi2tQTzIpgM/Qda6hre5Jijn0f8acax4AphbUZHsuWVLhGXLKqmo9F9iytMtQ59jF51ZRgaBK8+A6iy4fobTjEwV8GhNTe9eFRW1riusqKilpqZ3L+Axk1ehVIXD4cf23Xe/nv369fJUJxKPrGCdXpZn9+5dOOCAg3qUlZU94VRnOBx+bMSIUd3799+DSCQcyPJcuHBhFfDoyJH79thzzz6uGUOAsrIy9tyzLyNH7tsDeMzkVQwmIWstfYFuiBvsuQFscI4DLkn7/xhgRjgM69ZEaY2Fco0uPKEskmRbfZhYbJeRTUadpVKeAdRZcP1Mb1WmVVTUjnOz552ph1tRUTsW2XhYKNMGDRo6duDAPQKv0+vy7NWrO3vvPXLfcDj848KnG0I37rnnwLEDB/YnEokEsjzLy8sBpvXvP2jcgAH9XDUyO41NhAED+tG//6CxwDSTp98ckuG9QwPY4ByR4b0J4bBEBojH8d3QVFYlWPNpOWvWRKmpiadGOBl1lkp5BlBnwfUzbEYzQ0Oh8JROnXp5rrRTp96EQuEpBU6hDa2pqb1y+PBhhDyuySmdBU75+Faew4btRW1t5+8WqrOysmrKiBF7U14eDWx5zps3b2hVVdWVw4cP91RneXmU4cOHU1VVfeW8efO8nELLZtHLbVxbTDKFTNkBMnUVKsIqUlkZNDeFWbywinA4mdKQVWeplGeRcL1+pkY0U6uqukWdLFRbJRyOUFXVLcrOc2vs9L6nDhy4V5mThX8/dPpZnpFImOHD9ymLRCLX2f+O4esHDBhUVlVVGfTf/bqBA/cqq6mp9lxnTU01AwcOKQuFQtd59ADvh+wUPyDDQ5ppfrtYazJ9EPfZwRk+i1t8zzedySSDa2riLF9Wwdq1UaqrE0XTKd6Nu/1wpVCentXP8MaNi2ohdLaXUzyZplIgdJbkbZna8vKKcwYO3NN3nYAtnX6X5x579Ka8vOIrdnVGImVfGTx4oGdTZm6U55NPPlkbiZSdNXjwQMJh77vK4XAIKZOyrzz55JNuL65NBp4Bfg/8AziRYHIoEs3iMfPvCaWgs7wiecK2+giLF1YSjRbfZyJUeuXpWf0MA8eXl1d3yrYZ06vebXl5dSfgeBu3Hd+zZ+/qysrywOv0vzxD9OvXv9quzl69eldXVlYEvjx79epT7ccodqdBrKBXrz52yzPVI4ySParudOSMoSpgD8RrJ2hTY2HgwbTOQBnwKM4cTnzRmUzyaLQsWbV+Q5TmHSHCYf9FRcpg+/Yw27eHqChPBK08i1Y/w8DR0Wgn37+xyfNoG7cc3afPHiWhsxjl2atXb9s6e/fu7flalxvl2bdvX397oiEwedrRWWse1BeQYJhD2nzeg10j7wKMoDhurFchkaN/y+7z7v2AAW1tLzuPuQi0zkg4OTKZhEQ8RCiU9F1nJJIsb2wM88nHFYTDySCVZ1HrZxkwtqyswvcaZPIca+OWsZ0715aEzmKUZ21tp3Zbnl27dvFdp8nTjs4pyCa2GuSo7a7A6eyM0p2t91Hp81e7FjmiIsXebQxqeY7ecOB1JiHis0PCbjorypNHf7y8kjFjm4hGk+VpQUaLWZ5FrZ9hYGgk4r8rp8lzqI1bhlZXV5SEzmKUp9m8aLM8q30f0RRSnlVV/s8ymDzt6LzYPMSpRuQUdj2cLltB+72Y0NbJ4ag2BjXbInRcdVrTGS1Pjm1qCtPQECYaTQZFZ1HrZxjoHC7CZKbJs7ONWzqHw2GSyaTtv0Qi6bvOUijPaLRwr7iUgZJgpdb/jOOBLZ2pKMz+jrxsl2e/PO8FZUd/Jqvdn+BRsjqTSfpHIuJinUwGRmdR62fJHAUKMHv2AoIV6ik4JBIJ3/KqqalizJjCTpKOx+M888yijvKzhAOoaQe7nz/fojrd1xkKaf1Mz2ibn41Um4Zxm41bVGcOWlpitnXGYoXH5LM7kkn9tbbGbetsbfW/PE2e2xwmkwxg45JUnR1SZ1HrZxhYGo/731EweS61cYvqzEF9faNtnY2NjSR9HNsnk0kaG+3rbGpq8r08TZ5LURTFFUMzt7XV/2gH8fgOkCilVlGdOdi8eYttndu21fuu0+RpS+fWrXW+6zR5ztUmQlHcMTQvxWLbfc+4pWU7wEs2blGdWUgkkqxZ86ltnevXr/d9RLN+/XrbOteuXevromoyCWvXrrWrU1GUHIbm2ZaWxu2JhH/edolEnJaWxkZkk5NVVGcW1q/fSkPDVts6N2xY39jc7N/oq7l5Bxs2rG+yr3NdEXSus6tTUZRshqZnz+H1kHywuXmrjw/yViD5gORtmaLplLyDqTMeT7Bs2XISibhtnfF468OffLKSeDzug844klfrQ3Z0nnLKKfXxeOtDH3+8wpGbup3R4ccfryAeb33wlFNOqdcmQlHcGdEA3NrUtCXmRy88kYjT1LQlhsTRsYvqbMOKFevZuHF1QToTicT0FSs+bm1qavZcZ1NTMytXftyaSCRutntvMpm8ecWK5a0NDY2e69y+vZEVK5a3JpPJW7R5UBQXDU3PnsOXJJOJO7ZvX+/Dg7yeZDJxZ8+ewxcXcLvvOpFoq4HUuWlTPYsXLyCRiBess7m56Y6FCxen3KM9oaUlxsKFi2lsbLyjEJ1jxoxZ0tTU+PNFixb5orOpqfGOMWPGLNbmQVHcHdEATNuxo36ul1M+zc1b2bGjfi4OT65UnbB163bmz19IY+NWRzqTyeSPVq9eMW/FilWeTKHF43FWrFjF6tUr5hWqs6WlBWDaqlWfzF258tPUXhxXaW1tZeXKT1m9+pN5wDSTp6Iobhqanj2HNwGnNzSs37Bjh/tT0zt21NPQsH4DcHrPnsOdzIH4phMIpM716+v44IOFbNq0yhWdiUTitIUL529etWoN8bh7myPj8QSrVq1h4cL5mxOJxGlOdI4YMaIJOH3Bgg82rV691lVj09oaZ/XqdSxY8MGmZDJ52ogRIxq1aVAUb0Y09Ow5fAlwXH39mnVu9sSbm7dSX79mHXCcycMpnus0eQRKZyzWytKla5g/fwEbN65wVWcsFjvmgw/e37h8+Se0trY6TrC1tZXlyz/hgw/e3xiLxY5xQ+eIESOWxGKxYz/4YM6Gjz9eyY4dzkcdO3a08PHHK/nggzkbYrHYsSNGjFji4vMVyvIaC+/j0vWKUvT6uVusm549h78LTGxoWD+3vn4NiUThjU4iEae+fg0NDevnAhNN2m7hmU6TdmB0xmJxVqzYwJw5y1i0aD5bt67xRGcsFjtk/vx5H8yZM4+GhsL3AjU0bGfOnHnMnz/vg1gsdoibOseMGfNuLBY7dP78ufPmzfuQhobGguK8JRIJGhoamTfvQ+bPnzsvFosdOmbMmHddfpDT3eSyDcHSxWcKHV+WI81CiOZ5r8zCfeXZPk+L7xW18F2KpjNPPu1RZ1HrZ8agambUMWHHjvrbt2z5ONbYuAk7HlSJRJzGxk1s2bI8tmNH/e3ABJdGMplGDK7qdGmEULDOeDxJS0sr27Y18umnm5k/fyVvv72QhQsX8umnCzzXmUgkDly9esXPXn/99daPPlqUChljicbGRj76aBGvv/566+rVK36WSCQO9ELnuHHjliQSiYNWrfrk9jfeeL114cKlNDY2k0gkcm7sTCbFwDQ2NrNw4VLeeOP11lWrPrk9kUgcNG7cOKc6M4Uv2JD2elubhzb1UDak/X9dhjQ2t/l/Jg8TO8PlTI1FQxudmUh/P5Yt3UTis4jFdQ4bRk915vnd2qPOotbPrEJ79hzeDFy7ceOi3zc2brqmsXHzOeXl1dXRaCei0UrC4WgqND2JRIJEIkYs1kwstp2WlsYmSP4duK1A7zJbM17AtclkwpFOCvPacl3n7NkfBUHn95qaGu9dvHjB1KVLF5/ds2fvqt69e9OtWxeqq6soKyv7bHqssbGJLVvqWL9+PRs3rm9OJOIPJBKJW0KhkKc6R48e3VxbW3vt7Nmzf79kyYKpy5aJzr59+9K1axeqqqooK4sYnXGamprYurWOtWvXfqYzHo/fcvDBBy+ur3dlDe0B4IK0h/8tdo2Vtg14Gjg5bXrhWWBL2jUvmYe5j/n/duDJNvksM2lPSM1Smryt8hDwtbT/rwX+16bxeQE4Ju29D4AFaf9/BPhhm3T/GgoliSdCJBIhQqHkQgjNZdczZF4Mgs42/1+IhBpq7zqLWj9Dr732oSWVGzcuqgFOBI4ExiNHgXZN61EtB+YArwDP9Ow5vMFKuo890p07fpH9qN4pV6wF4PAjZ1stUFs621js7K1vUwVnnT056+cPPvAvACqrdpS8zlAoRDKZJBwOk0gkbOkMh8MNiUTiszSc6sxH/wGDHNXPVSs/yZtHLp1p9bQ/cAdwGLAIOXnxjTaX7WOu2Q+YB1wOzG8zw/BN4BpTPx4Brmd3J4pDgNuB4cBMYModv+i7ykp5TblibYVpdA4HVgFnAR+1uawbcs7954y+s8216ZwN3Gx0PgR8Z/v2MHv2j3Hc5K0kEhCPhwYCjyIHyP0POGfixFGbvdS5vSG8qv/AFo6bXEc8DvF4aDedGbJzVWcsFvooGk0y+YSt9OjRSlNTuKDydFPnrFnzi1o/Q9+9fE034H7gWKwdL9oCPAycf/qXN7cYY1GJnEP9lSxziW1pNVb+bKDOiqExlaognUDLhIPfAzmWtGCdFhtGRzrNe6rTBZ39BwxqMcbEM52KouQnDPwCmIz1M6zLzVDx+sce6c5jj3QH+IF5L2oxjTKT5502tBasM+091dmBdK5a+UlqxOKHTkVRchiaQrtr6fcd50Iabl6rOlWn3zoVRclhaHoWeG+ftNdupJEP1ak6g6xTUZQchkZRlCzo+oyiqKFRFEVR1NAoiqIoamgURVEURQ2NoiiKooZGURRFUdTQKIqiKGpoFEVRlHZlaOoKvDc9mJsbaeRDdarOIOtUFCWHoXm0wHsfS3vtRhr5UJ2qM8g6FUXJQhlwFVALfAFrx3a2mgfw9rT3bgX6IefXWwl+mEDOUvieDa2qU3UGWaeiKDkMTR0SPt0JLcC3zJ9XqE7VGWSdiqLkMDQdgrdm7+c4jbPO9jZ9t/BDpznfR1EUxZKh6QLci/WpiTgyNXE5Ow/AKgfuAk6zODWRRA6WuhjrC7WqU3UGWaeiKDkMzQzgTJv3XYwcRXqT+f9U4CKbaXwFOafa6n2qU3UGWaeiKFkIA2cUeO/paa/dSCMfqlN1Blmnoig5DE2XAu/t3mZ6w2ka+VCdqjPIOhVFyWFoFEVRFEUNjaIoiqKGRlEURVHU0CiKoihqaBRFURQ1NIqiKIqihkZRFEVRQ6MoiqJ0BEOzscB716W9diONfKhO1RlknYqi5DA0/yrw3mfTXj/nQhr5UJ2qM8g6FUXJYWguNw9zq8V7WoC/Ajenvfdj4G/sjJabj1bzEE+xoVV1qs4g61QUJQv/PwAlukJhy2ScjQAAAABJRU5ErkJggg==";dn={border:{"border-top":" iconfont luckysheet-iconfont-shangbiankuang","border-bottom":" iconfont luckysheet-iconfont-xiabiankuang","border-left":" iconfont luckysheet-iconfont-zuobiankuang","border-right":" iconfont luckysheet-iconfont-youbiankuang","border-none":" iconfont luckysheet-iconfont-wubiankuang","border-all":" iconfont luckysheet-iconfont-quanjiabiankuang","border-outside":" iconfont luckysheet-iconfont-sizhoujiabiankuang","border-inside":" iconfont luckysheet-iconfont-neikuangxian","border-horizontal":" iconfont luckysheet-iconfont-neikuanghengxian","border-vertical":" iconfont luckysheet-iconfont-neikuangshuxian"},align:{left:" iconfont luckysheet-iconfont-wenbenzuoduiqi",center:" iconfont luckysheet-iconfont-wenbenjuzhongduiqi",right:" iconfont luckysheet-iconfont-wenbenyouduiqi",top:" iconfont luckysheet-iconfont-dingbuduiqi",middle:" iconfont luckysheet-iconfont-shuipingduiqi",bottom:" iconfont luckysheet-iconfont-dibuduiqi"},textWrap:{overflow:" iconfont luckysheet-iconfont-yichu1",wrap:" iconfont luckysheet-iconfont-zidonghuanhang",clip:" iconfont luckysheet-iconfont-jieduan"},rotation:{none:" iconfont luckysheet-iconfont-wuxuanzhuang",angleup:" iconfont luckysheet-iconfont-xiangshangqingxie",angledown:" iconfont luckysheet-iconfont-xiangxiaqingxie",vertical:" iconfont luckysheet-iconfont-shupaiwenzi","rotation-up":" iconfont luckysheet-iconfont-wenbenxiangshang","rotation-down":" iconfont luckysheet-iconfont-xiangxia90"}};Qd=` -

\u6298\u7EBF\u56FE

-
- \u57FA\u7840\u6298\u7EBF\u56FE -
\u57FA\u7840\u6298\u7EBF\u56FE
-
-
- \u5E73\u6ED1\u6298\u7EBF\u56FE -
\u5E73\u6ED1\u6298\u7EBF\u56FE
-
-
- \u9636\u68AF\u6298\u7EBF\u56FE -
\u9636\u68AF\u6298\u7EBF\u56FE
-
- - - -

\u67F1\u72B6\u56FE

-
- \u57FA\u7840\u67F1\u72B6\u56FE -
\u57FA\u7840\u67F1\u72B6\u56FE
-
- - - - -

\u9762\u79EF\u56FE

-
- \u57FA\u7840\u9762\u79EF\u56FE -
\u57FA\u7840\u9762\u79EF\u56FE
-
-
- \u5E73\u6ED1\u9762\u79EF\u56FE -
\u5E73\u6ED1\u9762\u79EF\u56FE
-
-
- \u9636\u68AF\u9762\u79EF\u56FE -
\u9636\u68AF\u9762\u79EF\u56FE
-
- - - -

\u997C\u56FE

-
- \u57FA\u7840\u997C\u56FE -
\u57FA\u7840\u997C\u56FE
-
-
- \u73AF\u56FE -
\u73AF\u56FE
-
`,_d=` -

Title

-
- \u6807\u9898 - -
- - - -

Tooltip

-
- \u663E\u793A - -
- - -

Legend

-
- \u663E\u793A - -
- - -

Label

-
- \u663E\u793A - -
- - - -

Series Colors

- -`});function Q(e){for(let n=0;n{n.chart&&n.chart.forEach(t=>{let l=d.getChartJson(t.chart_id);t.chartOptions=l})}),d.luckysheetfile}function Zd(){return d.config}function jo(){return d.visibledatarow}function Xo(){return d.visibledatacolumn}var wt=De(()=>{dt();je()});function Jd(e){d.luckysheet_select_save=e}function Rl(e){d.luckysheet_scroll_status=e}function ba(e){d.luckysheetfile=e}var Wr=De(()=>{wt();je()});var ip,br,Za=De(()=>{ip={mobilecheck:function(){var e=!1;return function(n){(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(n)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(n.substr(0,4)))&&(e=!0)}(navigator.userAgent||navigator.vendor||window.opera),document.body&&document.body.clientWidth&&document.body.clientHeight&&document.body.clientWidth<350&&document.body.clientHeight<500&&(e=!0),e},iphoneCheck:function(){var e=!1;return/iPhone/i.test(navigator.userAgent)&&(e=!0),!0},isWeixin:function(){var e=navigator.userAgent.toLowerCase();return e.match(/MicroMessenger/i)=="micromessenger"},isAndroid:function(){var e=navigator.userAgent,n=(navigator.appVersion,e.indexOf("Android")>-1||e.indexOf("Linux")>-1);return n},tabletCheck:function(){var e=/ipad|android|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test(navigator.userAgent.toLowerCase());return e},BrowserType:function(){var e=navigator.userAgent,n=e.indexOf("Opera")>-1,t=e.indexOf("compatible")>-1&&e.indexOf("MSIE")>-1&&!n,l=e.indexOf("Trident")>-1&&e.indexOf("rv:11.0")>-1,a=e.indexOf("Edge")>-1,o=e.indexOf("Firefox")>-1,s=e.indexOf("Safari")>-1&&e.indexOf("Chrome")==-1,u=e.indexOf("Chrome")>-1&&e.indexOf("Safari")>-1;if(t){var f=new RegExp("MSIE (\\d+\\.\\d+);");f.test(e);var A=parseFloat(RegExp.$1);return A==7?"IE7":A==8?"IE8":A==9?"IE9":A==10?"IE10":"0"}if(o)return"FF";if(n)return"Opera";if(s)return"Safari";if(u)return"Chrome";if(a)return"Edge";if(l)return"IE11"},isIE:function(){var e=navigator.userAgent,n=e.indexOf("Opera")>-1,t=e.indexOf("compatible")>-1&&e.indexOf("MSIE")>-1&&!n,l=e.indexOf("Trident")>-1&&e.indexOf("rv:11.0")>-1;return t||l?"1":"-1"},IEVersion:function(){var e=navigator.userAgent,n=e.indexOf("Opera")>-1,t=e.indexOf("compatible")>-1&&e.indexOf("MSIE")>-1&&!n,l=e.indexOf("Trident")>-1&&e.indexOf("rv:11.0")>-1,a=e.indexOf("Windows NT 6.1; Trident/7.0;")>-1&&!t;if(t){var o=new RegExp("MSIE (\\d+\\.\\d+);");o.test(e);var s=parseFloat(RegExp.$1);return s==7?"IE7":s==8?"IE8":s==9?"IE9":s==10?"IE10":"0"}else return a?"Edge":l?"IE11":"-1"},luckysheetrefreshfixednum:null,luckysheetrefreshfixed:function(){var e=this;return e.luckysheetrefreshfixednum==null&&(e.BrowserType()=="FF"?e.luckysheetrefreshfixednum=5:e.luckysheetrefreshfixednum=0),e.luckysheetrefreshfixednum},detectOS(){var e=navigator.userAgent,n=navigator.platform=="Win32"||navigator.platform=="Windows",t=navigator.platform=="Mac68K"||navigator.platform=="MacPPC"||navigator.platform=="Macintosh"||navigator.platform=="MacIntel";if(t)return"Mac";var l=navigator.platform=="X11"&&!n&&!t;if(l)return"Unix";var a=String(navigator.platform).indexOf("Linux")>-1;if(a)return"Linux";if(n){var o=e.indexOf("Windows NT 5.0")>-1||e.indexOf("Windows 2000")>-1;if(o)return"Win2000";var s=e.indexOf("Windows NT 5.1")>-1||e.indexOf("Windows XP")>-1;if(s)return"WinXP";var u=e.indexOf("Windows NT 5.2")>-1||e.indexOf("Windows 2003")>-1;if(u)return"Win2003";var f=e.indexOf("Windows NT 6.0")>-1||e.indexOf("Windows Vista")>-1;if(f)return"WinVista";var A=e.indexOf("Windows NT 6.1")>-1||e.indexOf("Windows 7")>-1;if(A)return"Win7"}return"other"}},br=ip});var Tn=kl(tr=>{"use strict";var op=typeof Uint8Array!="undefined"&&typeof Uint16Array!="undefined"&&typeof Int32Array!="undefined";function sp(e,n){return Object.prototype.hasOwnProperty.call(e,n)}tr.assign=function(e){for(var n=Array.prototype.slice.call(arguments,1);n.length;){var t=n.shift();if(!!t){if(typeof t!="object")throw new TypeError(t+"must be non-object");for(var l in t)sp(t,l)&&(e[l]=t[l])}}return e};tr.shrinkBuf=function(e,n){return e.length===n?e:e.subarray?e.subarray(0,n):(e.length=n,e)};var cp={arraySet:function(e,n,t,l,a){if(n.subarray&&e.subarray){e.set(n.subarray(t,t+l),a);return}for(var o=0;o{"use strict";var dp=Tn(),fp=4,$d=0,ef=1,Ap=2;function Ja(e){for(var n=e.length;--n>=0;)e[n]=0}var hp=0,tf=1,mp=2,gp=3,pp=258,oc=29,zi=256,Ni=zi+1+oc,$a=30,sc=19,lf=2*Ni+1,ka=15,cc=16,vp=7,uc=256,rf=16,nf=17,af=18,dc=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0],Uo=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],yp=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7],of=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],bp=512,In=new Array((Ni+2)*2);Ja(In);var Fi=new Array($a*2);Ja(Fi);var Li=new Array(bp);Ja(Li);var Oi=new Array(pp-gp+1);Ja(Oi);var fc=new Array(oc);Ja(fc);var Wo=new Array($a);Ja(Wo);function Ac(e,n,t,l,a){this.static_tree=e,this.extra_bits=n,this.extra_base=t,this.elems=l,this.max_length=a,this.has_stree=e&&e.length}var sf,cf,uf;function hc(e,n){this.dyn_tree=e,this.max_code=0,this.stat_desc=n}function df(e){return e<256?Li[e]:Li[256+(e>>>7)]}function Vi(e,n){e.pending_buf[e.pending++]=n&255,e.pending_buf[e.pending++]=n>>>8&255}function cr(e,n,t){e.bi_valid>cc-t?(e.bi_buf|=n<>cc-e.bi_valid,e.bi_valid+=t-cc):(e.bi_buf|=n<>>=1,t<<=1;while(--n>0);return t>>>1}function kp(e){e.bi_valid===16?(Vi(e,e.bi_buf),e.bi_buf=0,e.bi_valid=0):e.bi_valid>=8&&(e.pending_buf[e.pending++]=e.bi_buf&255,e.bi_buf>>=8,e.bi_valid-=8)}function wp(e,n){var t=n.dyn_tree,l=n.max_code,a=n.stat_desc.static_tree,o=n.stat_desc.has_stree,s=n.stat_desc.extra_bits,u=n.stat_desc.extra_base,f=n.stat_desc.max_length,A,h,g,p,v,y,b=0;for(p=0;p<=ka;p++)e.bl_count[p]=0;for(t[e.heap[e.heap_max]*2+1]=0,A=e.heap_max+1;Af&&(p=f,b++),t[h*2+1]=p,!(h>l)&&(e.bl_count[p]++,v=0,h>=u&&(v=s[h-u]),y=t[h*2],e.opt_len+=y*(p+v),o&&(e.static_len+=y*(a[h*2+1]+v)));if(b!==0){do{for(p=f-1;e.bl_count[p]===0;)p--;e.bl_count[p]--,e.bl_count[p+1]+=2,e.bl_count[f]--,b-=2}while(b>0);for(p=f;p!==0;p--)for(h=e.bl_count[p];h!==0;)g=e.heap[--A],!(g>l)&&(t[g*2+1]!==p&&(e.opt_len+=(p-t[g*2+1])*t[g*2],t[g*2+1]=p),h--)}}function Af(e,n,t){var l=new Array(ka+1),a=0,o,s;for(o=1;o<=ka;o++)l[o]=a=a+t[o-1]<<1;for(s=0;s<=n;s++){var u=e[s*2+1];u!==0&&(e[s*2]=ff(l[u]++,u))}}function xp(){var e,n,t,l,a,o=new Array(ka+1);for(t=0,l=0;l>=7;l<$a;l++)for(Wo[l]=a<<7,e=0;e<1<8?Vi(e,e.bi_buf):e.bi_valid>0&&(e.pending_buf[e.pending++]=e.bi_buf),e.bi_buf=0,e.bi_valid=0}function Cp(e,n,t,l){mf(e),l&&(Vi(e,t),Vi(e,~t)),dp.arraySet(e.pending_buf,e.window,n,t,e.pending),e.pending+=t}function gf(e,n,t,l){var a=n*2,o=t*2;return e[a]>1;s>=1;s--)mc(e,t,s);A=o;do s=e.heap[1],e.heap[1]=e.heap[e.heap_len--],mc(e,t,1),u=e.heap[1],e.heap[--e.heap_max]=s,e.heap[--e.heap_max]=u,t[A*2]=t[s*2]+t[u*2],e.depth[A]=(e.depth[s]>=e.depth[u]?e.depth[s]:e.depth[u])+1,t[s*2+1]=t[u*2+1]=A,e.heap[1]=A++,mc(e,t,1);while(e.heap_len>=2);e.heap[--e.heap_max]=e.heap[1],wp(e,n),Af(t,f,e.bl_count)}function vf(e,n,t){var l,a=-1,o,s=n[0*2+1],u=0,f=7,A=4;for(s===0&&(f=138,A=3),n[(t+1)*2+1]=65535,l=0;l<=t;l++)o=s,s=n[(l+1)*2+1],!(++u=3&&e.bl_tree[of[n]*2+1]===0;n--);return e.opt_len+=3*(n+1)+5+5+4,n}function Ip(e,n,t,l){var a;for(cr(e,n-257,5),cr(e,t-1,5),cr(e,l-4,4),a=0;a>>=1)if(n&1&&e.dyn_ltree[t*2]!==0)return $d;if(e.dyn_ltree[9*2]!==0||e.dyn_ltree[10*2]!==0||e.dyn_ltree[13*2]!==0)return ef;for(t=32;t0?(e.strm.data_type===Ap&&(e.strm.data_type=Sp(e)),gc(e,e.l_desc),gc(e,e.d_desc),s=Tp(e),a=e.opt_len+3+7>>>3,o=e.static_len+3+7>>>3,o<=a&&(a=o)):a=o=t+5,t+4<=a&&n!==-1?kf(e,n,t,l):e.strategy===fp||o===a?(cr(e,(tf<<1)+(l?1:0),3),pf(e,In,Fi)):(cr(e,(mp<<1)+(l?1:0),3),Ip(e,e.l_desc.max_code+1,e.d_desc.max_code+1,s+1),pf(e,e.dyn_ltree,e.dyn_dtree)),hf(e),l&&mf(e)}function Ep(e,n,t){return e.pending_buf[e.d_buf+e.last_lit*2]=n>>>8&255,e.pending_buf[e.d_buf+e.last_lit*2+1]=n&255,e.pending_buf[e.l_buf+e.last_lit]=t&255,e.last_lit++,n===0?e.dyn_ltree[t*2]++:(e.matches++,n--,e.dyn_ltree[(Oi[t]+zi+1)*2]++,e.dyn_dtree[df(n)*2]++),e.last_lit===e.lit_bufsize-1}ei._tr_init=Dp;ei._tr_stored_block=kf;ei._tr_flush_block=Mp;ei._tr_tally=Ep;ei._tr_align=Rp});var pc=kl((Z1,xf)=>{"use strict";function Bp(e,n,t,l){for(var a=e&65535|0,o=e>>>16&65535|0,s=0;t!==0;){s=t>2e3?2e3:t,t-=s;do a=a+n[l++]|0,o=o+a|0;while(--s);a%=65521,o%=65521}return a|o<<16|0}xf.exports=Bp});var vc=kl((J1,Cf)=>{"use strict";function Pp(){for(var e,n=[],t=0;t<256;t++){e=t;for(var l=0;l<8;l++)e=e&1?3988292384^e>>>1:e>>>1;n[t]=e}return n}var zp=Pp();function Np(e,n,t,l){var a=zp,o=l+t;e^=-1;for(var s=l;s>>8^a[(e^n[s])&255];return e^-1}Cf.exports=Np});var Go=kl(($1,Tf)=>{"use strict";Tf.exports={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"}});var zf=kl(mn=>{"use strict";var lr=Tn(),Ir=wf(),If=pc(),Kn=vc(),Fp=Go(),wa=0,Lp=1,Op=3,Zn=4,Sf=5,An=0,Df=1,Sr=-2,Vp=-3,yc=-5,qp=-1,Hp=1,Yo=2,jp=3,Xp=4,Up=0,Wp=2,Qo=8,Gp=9,Yp=15,Qp=8,_p=29,Kp=256,bc=Kp+1+_p,Zp=30,Jp=19,$p=2*bc+1,ev=15,Gt=3,Jn=258,Gr=Jn+Gt+1,tv=32,_o=42,kc=69,Ko=73,Zo=91,Jo=103,xa=113,qi=666,Ml=1,Hi=2,Ca=3,ti=4,lv=3;function $n(e,n){return e.msg=Fp[n],n}function Rf(e){return(e<<1)-(e>4?9:0)}function ea(e){for(var n=e.length;--n>=0;)e[n]=0}function ta(e){var n=e.state,t=n.pending;t>e.avail_out&&(t=e.avail_out),t!==0&&(lr.arraySet(e.output,n.pending_buf,n.pending_out,t,e.next_out),e.next_out+=t,n.pending_out+=t,e.total_out+=t,e.avail_out-=t,n.pending-=t,n.pending===0&&(n.pending_out=0))}function ql(e,n){Ir._tr_flush_block(e,e.block_start>=0?e.block_start:-1,e.strstart-e.block_start,n),e.block_start=e.strstart,ta(e.strm)}function Zt(e,n){e.pending_buf[e.pending++]=n}function ji(e,n){e.pending_buf[e.pending++]=n>>>8&255,e.pending_buf[e.pending++]=n&255}function rv(e,n,t,l){var a=e.avail_in;return a>l&&(a=l),a===0?0:(e.avail_in-=a,lr.arraySet(n,e.input,e.next_in,a,t),e.state.wrap===1?e.adler=If(e.adler,n,a,t):e.state.wrap===2&&(e.adler=Kn(e.adler,n,a,t)),e.next_in+=a,e.total_in+=a,a)}function Mf(e,n){var t=e.max_chain_length,l=e.strstart,a,o,s=e.prev_length,u=e.nice_match,f=e.strstart>e.w_size-Gr?e.strstart-(e.w_size-Gr):0,A=e.window,h=e.w_mask,g=e.prev,p=e.strstart+Jn,v=A[l+s-1],y=A[l+s];e.prev_length>=e.good_match&&(t>>=2),u>e.lookahead&&(u=e.lookahead);do if(a=n,!(A[a+s]!==y||A[a+s-1]!==v||A[a]!==A[l]||A[++a]!==A[l+1])){l+=2,a++;do;while(A[++l]===A[++a]&&A[++l]===A[++a]&&A[++l]===A[++a]&&A[++l]===A[++a]&&A[++l]===A[++a]&&A[++l]===A[++a]&&A[++l]===A[++a]&&A[++l]===A[++a]&&ls){if(e.match_start=n,s=o,o>=u)break;v=A[l+s-1],y=A[l+s]}}while((n=g[n&h])>f&&--t!=0);return s<=e.lookahead?s:e.lookahead}function Ta(e){var n=e.w_size,t,l,a,o,s;do{if(o=e.window_size-e.lookahead-e.strstart,e.strstart>=n+(n-Gr)){lr.arraySet(e.window,e.window,n,n,0),e.match_start-=n,e.strstart-=n,e.block_start-=n,l=e.hash_size,t=l;do a=e.head[--t],e.head[t]=a>=n?a-n:0;while(--l);l=n,t=l;do a=e.prev[--t],e.prev[t]=a>=n?a-n:0;while(--l);o+=n}if(e.strm.avail_in===0)break;if(l=rv(e.strm,e.window,e.strstart+e.lookahead,o),e.lookahead+=l,e.lookahead+e.insert>=Gt)for(s=e.strstart-e.insert,e.ins_h=e.window[s],e.ins_h=(e.ins_h<e.pending_buf_size-5&&(t=e.pending_buf_size-5);;){if(e.lookahead<=1){if(Ta(e),e.lookahead===0&&n===wa)return Ml;if(e.lookahead===0)break}e.strstart+=e.lookahead,e.lookahead=0;var l=e.block_start+t;if((e.strstart===0||e.strstart>=l)&&(e.lookahead=e.strstart-l,e.strstart=l,ql(e,!1),e.strm.avail_out===0)||e.strstart-e.block_start>=e.w_size-Gr&&(ql(e,!1),e.strm.avail_out===0))return Ml}return e.insert=0,n===Zn?(ql(e,!0),e.strm.avail_out===0?Ca:ti):(e.strstart>e.block_start&&(ql(e,!1),e.strm.avail_out===0),Ml)}function wc(e,n){for(var t,l;;){if(e.lookahead=Gt&&(e.ins_h=(e.ins_h<=Gt)if(l=Ir._tr_tally(e,e.strstart-e.match_start,e.match_length-Gt),e.lookahead-=e.match_length,e.match_length<=e.max_lazy_match&&e.lookahead>=Gt){e.match_length--;do e.strstart++,e.ins_h=(e.ins_h<=Gt&&(e.ins_h=(e.ins_h<4096)&&(e.match_length=Gt-1)),e.prev_length>=Gt&&e.match_length<=e.prev_length){a=e.strstart+e.lookahead-Gt,l=Ir._tr_tally(e,e.strstart-1-e.prev_match,e.prev_length-Gt),e.lookahead-=e.prev_length-1,e.prev_length-=2;do++e.strstart<=a&&(e.ins_h=(e.ins_h<=Gt&&e.strstart>0&&(a=e.strstart-1,l=s[a],l===s[++a]&&l===s[++a]&&l===s[++a])){o=e.strstart+Jn;do;while(l===s[++a]&&l===s[++a]&&l===s[++a]&&l===s[++a]&&l===s[++a]&&l===s[++a]&&l===s[++a]&&l===s[++a]&&ae.lookahead&&(e.match_length=e.lookahead)}if(e.match_length>=Gt?(t=Ir._tr_tally(e,1,e.match_length-Gt),e.lookahead-=e.match_length,e.strstart+=e.match_length,e.match_length=0):(t=Ir._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++),t&&(ql(e,!1),e.strm.avail_out===0))return Ml}return e.insert=0,n===Zn?(ql(e,!0),e.strm.avail_out===0?Ca:ti):e.last_lit&&(ql(e,!1),e.strm.avail_out===0)?Ml:Hi}function iv(e,n){for(var t;;){if(e.lookahead===0&&(Ta(e),e.lookahead===0)){if(n===wa)return Ml;break}if(e.match_length=0,t=Ir._tr_tally(e,0,e.window[e.strstart]),e.lookahead--,e.strstart++,t&&(ql(e,!1),e.strm.avail_out===0))return Ml}return e.insert=0,n===Zn?(ql(e,!0),e.strm.avail_out===0?Ca:ti):e.last_lit&&(ql(e,!1),e.strm.avail_out===0)?Ml:Hi}function hn(e,n,t,l,a){this.good_length=e,this.max_lazy=n,this.nice_length=t,this.max_chain=l,this.func=a}var ri;ri=[new hn(0,0,0,0,nv),new hn(4,4,8,4,wc),new hn(4,5,16,8,wc),new hn(4,6,32,32,wc),new hn(4,4,16,16,li),new hn(8,16,32,32,li),new hn(8,16,128,128,li),new hn(8,32,128,256,li),new hn(32,128,258,1024,li),new hn(32,258,258,4096,li)];function ov(e){e.window_size=2*e.w_size,ea(e.head),e.max_lazy_match=ri[e.level].max_lazy,e.good_match=ri[e.level].good_length,e.nice_match=ri[e.level].nice_length,e.max_chain_length=ri[e.level].max_chain,e.strstart=0,e.block_start=0,e.lookahead=0,e.insert=0,e.match_length=e.prev_length=Gt-1,e.match_available=0,e.ins_h=0}function sv(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=Qo,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,this.prev_length=0,this.max_chain_length=0,this.max_lazy_match=0,this.level=0,this.strategy=0,this.good_match=0,this.nice_match=0,this.dyn_ltree=new lr.Buf16($p*2),this.dyn_dtree=new lr.Buf16((2*Zp+1)*2),this.bl_tree=new lr.Buf16((2*Jp+1)*2),ea(this.dyn_ltree),ea(this.dyn_dtree),ea(this.bl_tree),this.l_desc=null,this.d_desc=null,this.bl_desc=null,this.bl_count=new lr.Buf16(ev+1),this.heap=new lr.Buf16(2*bc+1),ea(this.heap),this.heap_len=0,this.heap_max=0,this.depth=new lr.Buf16(2*bc+1),ea(this.depth),this.l_buf=0,this.lit_bufsize=0,this.last_lit=0,this.d_buf=0,this.opt_len=0,this.static_len=0,this.matches=0,this.insert=0,this.bi_buf=0,this.bi_valid=0}function Ef(e){var n;return!e||!e.state?$n(e,Sr):(e.total_in=e.total_out=0,e.data_type=Wp,n=e.state,n.pending=0,n.pending_out=0,n.wrap<0&&(n.wrap=-n.wrap),n.status=n.wrap?_o:xa,e.adler=n.wrap===2?0:1,n.last_flush=wa,Ir._tr_init(n),An)}function Bf(e){var n=Ef(e);return n===An&&ov(e.state),n}function cv(e,n){return!e||!e.state||e.state.wrap!==2?Sr:(e.state.gzhead=n,An)}function Pf(e,n,t,l,a,o){if(!e)return Sr;var s=1;if(n===qp&&(n=6),l<0?(s=0,l=-l):l>15&&(s=2,l-=16),a<1||a>Gp||t!==Qo||l<8||l>15||n<0||n>9||o<0||o>Xp)return $n(e,Sr);l===8&&(l=9);var u=new sv;return e.state=u,u.strm=e,u.wrap=s,u.gzhead=null,u.w_bits=l,u.w_size=1<Sf||n<0)return e?$n(e,Sr):Sr;if(l=e.state,!e.output||!e.input&&e.avail_in!==0||l.status===qi&&n!==Zn)return $n(e,e.avail_out===0?yc:Sr);if(l.strm=e,t=l.last_flush,l.last_flush=n,l.status===_o)if(l.wrap===2)e.adler=0,Zt(l,31),Zt(l,139),Zt(l,8),l.gzhead?(Zt(l,(l.gzhead.text?1:0)+(l.gzhead.hcrc?2:0)+(l.gzhead.extra?4:0)+(l.gzhead.name?8:0)+(l.gzhead.comment?16:0)),Zt(l,l.gzhead.time&255),Zt(l,l.gzhead.time>>8&255),Zt(l,l.gzhead.time>>16&255),Zt(l,l.gzhead.time>>24&255),Zt(l,l.level===9?2:l.strategy>=Yo||l.level<2?4:0),Zt(l,l.gzhead.os&255),l.gzhead.extra&&l.gzhead.extra.length&&(Zt(l,l.gzhead.extra.length&255),Zt(l,l.gzhead.extra.length>>8&255)),l.gzhead.hcrc&&(e.adler=Kn(e.adler,l.pending_buf,l.pending,0)),l.gzindex=0,l.status=kc):(Zt(l,0),Zt(l,0),Zt(l,0),Zt(l,0),Zt(l,0),Zt(l,l.level===9?2:l.strategy>=Yo||l.level<2?4:0),Zt(l,lv),l.status=xa);else{var s=Qo+(l.w_bits-8<<4)<<8,u=-1;l.strategy>=Yo||l.level<2?u=0:l.level<6?u=1:l.level===6?u=2:u=3,s|=u<<6,l.strstart!==0&&(s|=tv),s+=31-s%31,l.status=xa,ji(l,s),l.strstart!==0&&(ji(l,e.adler>>>16),ji(l,e.adler&65535)),e.adler=1}if(l.status===kc)if(l.gzhead.extra){for(a=l.pending;l.gzindex<(l.gzhead.extra.length&65535)&&!(l.pending===l.pending_buf_size&&(l.gzhead.hcrc&&l.pending>a&&(e.adler=Kn(e.adler,l.pending_buf,l.pending-a,a)),ta(e),a=l.pending,l.pending===l.pending_buf_size));)Zt(l,l.gzhead.extra[l.gzindex]&255),l.gzindex++;l.gzhead.hcrc&&l.pending>a&&(e.adler=Kn(e.adler,l.pending_buf,l.pending-a,a)),l.gzindex===l.gzhead.extra.length&&(l.gzindex=0,l.status=Ko)}else l.status=Ko;if(l.status===Ko)if(l.gzhead.name){a=l.pending;do{if(l.pending===l.pending_buf_size&&(l.gzhead.hcrc&&l.pending>a&&(e.adler=Kn(e.adler,l.pending_buf,l.pending-a,a)),ta(e),a=l.pending,l.pending===l.pending_buf_size)){o=1;break}l.gzindexa&&(e.adler=Kn(e.adler,l.pending_buf,l.pending-a,a)),o===0&&(l.gzindex=0,l.status=Zo)}else l.status=Zo;if(l.status===Zo)if(l.gzhead.comment){a=l.pending;do{if(l.pending===l.pending_buf_size&&(l.gzhead.hcrc&&l.pending>a&&(e.adler=Kn(e.adler,l.pending_buf,l.pending-a,a)),ta(e),a=l.pending,l.pending===l.pending_buf_size)){o=1;break}l.gzindexa&&(e.adler=Kn(e.adler,l.pending_buf,l.pending-a,a)),o===0&&(l.status=Jo)}else l.status=Jo;if(l.status===Jo&&(l.gzhead.hcrc?(l.pending+2>l.pending_buf_size&&ta(e),l.pending+2<=l.pending_buf_size&&(Zt(l,e.adler&255),Zt(l,e.adler>>8&255),e.adler=0,l.status=xa)):l.status=xa),l.pending!==0){if(ta(e),e.avail_out===0)return l.last_flush=-1,An}else if(e.avail_in===0&&Rf(n)<=Rf(t)&&n!==Zn)return $n(e,yc);if(l.status===qi&&e.avail_in!==0)return $n(e,yc);if(e.avail_in!==0||l.lookahead!==0||n!==wa&&l.status!==qi){var f=l.strategy===Yo?iv(l,n):l.strategy===jp?av(l,n):ri[l.level].func(l,n);if((f===Ca||f===ti)&&(l.status=qi),f===Ml||f===Ca)return e.avail_out===0&&(l.last_flush=-1),An;if(f===Hi&&(n===Lp?Ir._tr_align(l):n!==Sf&&(Ir._tr_stored_block(l,0,0,!1),n===Op&&(ea(l.head),l.lookahead===0&&(l.strstart=0,l.block_start=0,l.insert=0))),ta(e),e.avail_out===0))return l.last_flush=-1,An}return n!==Zn?An:l.wrap<=0?Df:(l.wrap===2?(Zt(l,e.adler&255),Zt(l,e.adler>>8&255),Zt(l,e.adler>>16&255),Zt(l,e.adler>>24&255),Zt(l,e.total_in&255),Zt(l,e.total_in>>8&255),Zt(l,e.total_in>>16&255),Zt(l,e.total_in>>24&255)):(ji(l,e.adler>>>16),ji(l,e.adler&65535)),ta(e),l.wrap>0&&(l.wrap=-l.wrap),l.pending!==0?An:Df)}function fv(e){var n;return!e||!e.state?Sr:(n=e.state.status,n!==_o&&n!==kc&&n!==Ko&&n!==Zo&&n!==Jo&&n!==xa&&n!==qi?$n(e,Sr):(e.state=null,n===xa?$n(e,Vp):An))}function Av(e,n){var t=n.length,l,a,o,s,u,f,A,h;if(!e||!e.state||(l=e.state,s=l.wrap,s===2||s===1&&l.status!==_o||l.lookahead))return Sr;for(s===1&&(e.adler=If(e.adler,n,t,0)),l.wrap=0,t>=l.w_size&&(s===0&&(ea(l.head),l.strstart=0,l.block_start=0,l.insert=0),h=new lr.Buf8(l.w_size),lr.arraySet(h,n,t-l.w_size,l.w_size,0),n=h,t=l.w_size),u=e.avail_in,f=e.next_in,A=e.input,e.avail_in=t,e.next_in=0,e.input=n,Ta(l);l.lookahead>=Gt;){a=l.strstart,o=l.lookahead-(Gt-1);do l.ins_h=(l.ins_h<{"use strict";var $o=Tn(),Nf=!0,Ff=!0;try{String.fromCharCode.apply(null,[0])}catch(e){Nf=!1}try{String.fromCharCode.apply(null,new Uint8Array(1))}catch(e){Ff=!1}var Xi=new $o.Buf8(256);for(var la=0;la<256;la++)Xi[la]=la>=252?6:la>=248?5:la>=240?4:la>=224?3:la>=192?2:1;Xi[254]=Xi[254]=1;ni.string2buf=function(e){var n,t,l,a,o,s=e.length,u=0;for(a=0;a>>6,n[o++]=128|t&63):t<65536?(n[o++]=224|t>>>12,n[o++]=128|t>>>6&63,n[o++]=128|t&63):(n[o++]=240|t>>>18,n[o++]=128|t>>>12&63,n[o++]=128|t>>>6&63,n[o++]=128|t&63);return n};function Lf(e,n){if(n<65534&&(e.subarray&&Ff||!e.subarray&&Nf))return String.fromCharCode.apply(null,$o.shrinkBuf(e,n));for(var t="",l=0;l4){u[l++]=65533,t+=o-1;continue}for(a&=o===2?31:o===3?15:7;o>1&&t1){u[l++]=65533;continue}a<65536?u[l++]=a:(a-=65536,u[l++]=55296|a>>10&1023,u[l++]=56320|a&1023)}return Lf(u,l)};ni.utf8border=function(e,n){var t;for(n=n||e.length,n>e.length&&(n=e.length),t=n-1;t>=0&&(e[t]&192)==128;)t--;return t<0||t===0?n:t+Xi[e[t]]>n?t:n}});var Cc=kl((lw,Of)=>{"use strict";function hv(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}Of.exports=hv});var jf=kl(Gi=>{"use strict";var Ui=zf(),Wi=Tn(),Tc=xc(),Ic=Go(),mv=Cc(),Vf=Object.prototype.toString,gv=0,Sc=4,ai=0,qf=1,Hf=2,pv=-1,vv=0,yv=8;function Ia(e){if(!(this instanceof Ia))return new Ia(e);this.options=Wi.assign({level:pv,method:yv,chunkSize:16384,windowBits:15,memLevel:8,strategy:vv,to:""},e||{});var n=this.options;n.raw&&n.windowBits>0?n.windowBits=-n.windowBits:n.gzip&&n.windowBits>0&&n.windowBits<16&&(n.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new mv,this.strm.avail_out=0;var t=Ui.deflateInit2(this.strm,n.level,n.method,n.windowBits,n.memLevel,n.strategy);if(t!==ai)throw new Error(Ic[t]);if(n.header&&Ui.deflateSetHeader(this.strm,n.header),n.dictionary){var l;if(typeof n.dictionary=="string"?l=Tc.string2buf(n.dictionary):Vf.call(n.dictionary)==="[object ArrayBuffer]"?l=new Uint8Array(n.dictionary):l=n.dictionary,t=Ui.deflateSetDictionary(this.strm,l),t!==ai)throw new Error(Ic[t]);this._dict_set=!0}}Ia.prototype.push=function(e,n){var t=this.strm,l=this.options.chunkSize,a,o;if(this.ended)return!1;o=n===~~n?n:n===!0?Sc:gv,typeof e=="string"?t.input=Tc.string2buf(e):Vf.call(e)==="[object ArrayBuffer]"?t.input=new Uint8Array(e):t.input=e,t.next_in=0,t.avail_in=t.input.length;do{if(t.avail_out===0&&(t.output=new Wi.Buf8(l),t.next_out=0,t.avail_out=l),a=Ui.deflate(t,o),a!==qf&&a!==ai)return this.onEnd(a),this.ended=!0,!1;(t.avail_out===0||t.avail_in===0&&(o===Sc||o===Hf))&&(this.options.to==="string"?this.onData(Tc.buf2binstring(Wi.shrinkBuf(t.output,t.next_out))):this.onData(Wi.shrinkBuf(t.output,t.next_out)))}while((t.avail_in>0||t.avail_out===0)&&a!==qf);return o===Sc?(a=Ui.deflateEnd(this.strm),this.onEnd(a),this.ended=!0,a===ai):(o===Hf&&(this.onEnd(ai),t.avail_out=0),!0)};Ia.prototype.onData=function(e){this.chunks.push(e)};Ia.prototype.onEnd=function(e){e===ai&&(this.options.to==="string"?this.result=this.chunks.join(""):this.result=Wi.flattenChunks(this.chunks)),this.chunks=[],this.err=e,this.msg=this.strm.msg};function Dc(e,n){var t=new Ia(n);if(t.push(e,!0),t.err)throw t.msg||Ic[t.err];return t.result}function bv(e,n){return n=n||{},n.raw=!0,Dc(e,n)}function kv(e,n){return n=n||{},n.gzip=!0,Dc(e,n)}Gi.Deflate=Ia;Gi.deflate=Dc;Gi.deflateRaw=bv;Gi.gzip=kv});var Uf=kl((nw,Xf)=>{"use strict";var es=30,wv=12;Xf.exports=function(n,t){var l,a,o,s,u,f,A,h,g,p,v,y,b,k,w,C,I,x,T,R,D,S,B,F,E;l=n.state,a=n.next_in,F=n.input,o=a+(n.avail_in-5),s=n.next_out,E=n.output,u=s-(t-n.avail_out),f=s+(n.avail_out-257),A=l.dmax,h=l.wsize,g=l.whave,p=l.wnext,v=l.window,y=l.hold,b=l.bits,k=l.lencode,w=l.distcode,C=(1<>>24,y>>>=T,b-=T,T=x>>>16&255,T===0)E[s++]=x&65535;else if(T&16){R=x&65535,T&=15,T&&(b>>=T,b-=T),b<15&&(y+=F[a++]<>>24,y>>>=T,b-=T,T=x>>>16&255,T&16){if(D=x&65535,T&=15,bA){n.msg="invalid distance too far back",l.mode=es;break e}if(y>>>=T,b-=T,T=s-u,D>T){if(T=D-T,T>g&&l.sane){n.msg="invalid distance too far back",l.mode=es;break e}if(S=0,B=v,p===0){if(S+=h-T,T2;)E[s++]=B[S++],E[s++]=B[S++],E[s++]=B[S++],R-=3;R&&(E[s++]=B[S++],R>1&&(E[s++]=B[S++]))}else{S=s-D;do E[s++]=E[S++],E[s++]=E[S++],E[s++]=E[S++],R-=3;while(R>2);R&&(E[s++]=E[S++],R>1&&(E[s++]=E[S++]))}}else if((T&64)==0){x=w[(x&65535)+(y&(1<>3,a-=R,b-=R<<3,y&=(1<{"use strict";var Wf=Tn(),ii=15,Gf=852,Yf=592,Qf=0,Rc=1,_f=2,xv=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,0,0],Cv=[16,16,16,16,16,16,16,16,17,17,17,17,18,18,18,18,19,19,19,19,20,20,20,20,21,21,21,21,16,72,78],Tv=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,0,0],Iv=[16,16,16,16,17,17,18,18,19,19,20,20,21,21,22,22,23,23,24,24,25,25,26,26,27,27,28,28,29,29,64,64];Kf.exports=function(n,t,l,a,o,s,u,f){var A=f.bits,h=0,g=0,p=0,v=0,y=0,b=0,k=0,w=0,C=0,I=0,x,T,R,D,S,B=null,F=0,E,z=new Wf.Buf16(ii+1),P=new Wf.Buf16(ii+1),L=null,O=0,q,j,G;for(h=0;h<=ii;h++)z[h]=0;for(g=0;g=1&&z[v]===0;v--);if(y>v&&(y=v),v===0)return o[s++]=1<<24|64<<16|0,o[s++]=1<<24|64<<16|0,f.bits=1,0;for(p=1;p0&&(n===Qf||v!==1))return-1;for(P[1]=0,h=1;hGf||n===_f&&C>Yf)return 1;for(;;){q=h-k,u[g]E?(j=L[O+u[g]],G=B[F+u[g]]):(j=32+64,G=0),x=1<>k)+T]=q<<24|j<<16|G|0;while(T!==0);for(x=1<>=1;if(x!==0?(I&=x-1,I+=x):I=0,g++,--z[h]==0){if(h===v)break;h=t[l+u[g]]}if(h>y&&(I&D)!==R){for(k===0&&(k=y),S+=p,b=h-k,w=1<Gf||n===_f&&C>Yf)return 1;R=I&D,o[R]=y<<24|b<<16|S-s|0}}return I!==0&&(o[S+I]=h-k<<24|64<<16|0),f.bits=y,0}});var BA=kl(Yr=>{"use strict";var kr=Tn(),Mc=pc(),gn=vc(),Sv=Uf(),Yi=Zf(),Dv=0,Jf=1,$f=2,eA=4,Rv=5,ts=6,Sa=0,Mv=1,Ev=2,Dr=-2,tA=-3,Ec=-4,Bv=-5,lA=8,rA=1,nA=2,aA=3,iA=4,oA=5,sA=6,cA=7,uA=8,dA=9,fA=10,ls=11,Sn=12,Bc=13,AA=14,Pc=15,hA=16,mA=17,gA=18,pA=19,rs=20,ns=21,vA=22,yA=23,bA=24,kA=25,wA=26,zc=27,xA=28,CA=29,ml=30,Nc=31,Pv=32,zv=852,Nv=592,Fv=15,Lv=Fv;function TA(e){return(e>>>24&255)+(e>>>8&65280)+((e&65280)<<8)+((e&255)<<24)}function Ov(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new kr.Buf16(320),this.work=new kr.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function IA(e){var n;return!e||!e.state?Dr:(n=e.state,e.total_in=e.total_out=n.total=0,e.msg="",n.wrap&&(e.adler=n.wrap&1),n.mode=rA,n.last=0,n.havedict=0,n.dmax=32768,n.head=null,n.hold=0,n.bits=0,n.lencode=n.lendyn=new kr.Buf32(zv),n.distcode=n.distdyn=new kr.Buf32(Nv),n.sane=1,n.back=-1,Sa)}function SA(e){var n;return!e||!e.state?Dr:(n=e.state,n.wsize=0,n.whave=0,n.wnext=0,IA(e))}function DA(e,n){var t,l;return!e||!e.state||(l=e.state,n<0?(t=0,n=-n):(t=(n>>4)+1,n<48&&(n&=15)),n&&(n<8||n>15))?Dr:(l.window!==null&&l.wbits!==n&&(l.window=null),l.wrap=t,l.wbits=n,SA(e))}function RA(e,n){var t,l;return e?(l=new Ov,e.state=l,l.window=null,t=DA(e,n),t!==Sa&&(e.state=null),t):Dr}function Vv(e){return RA(e,Lv)}var MA=!0,Fc,Lc;function qv(e){if(MA){var n;for(Fc=new kr.Buf32(512),Lc=new kr.Buf32(32),n=0;n<144;)e.lens[n++]=8;for(;n<256;)e.lens[n++]=9;for(;n<280;)e.lens[n++]=7;for(;n<288;)e.lens[n++]=8;for(Yi(Jf,e.lens,0,288,Fc,0,e.work,{bits:9}),n=0;n<32;)e.lens[n++]=5;Yi($f,e.lens,0,32,Lc,0,e.work,{bits:5}),MA=!1}e.lencode=Fc,e.lenbits=9,e.distcode=Lc,e.distbits=5}function EA(e,n,t,l){var a,o=e.state;return o.window===null&&(o.wsize=1<=o.wsize?(kr.arraySet(o.window,n,t-o.wsize,o.wsize,0),o.wnext=0,o.whave=o.wsize):(a=o.wsize-o.wnext,a>l&&(a=l),kr.arraySet(o.window,n,t-l,a,o.wnext),l-=a,l?(kr.arraySet(o.window,n,t-l,l,0),o.wnext=l,o.whave=o.wsize):(o.wnext+=a,o.wnext===o.wsize&&(o.wnext=0),o.whave>>8&255,t.check=gn(t.check,B,2,0),A=0,h=0,t.mode=nA;break}if(t.flags=0,t.head&&(t.head.done=!1),!(t.wrap&1)||(((A&255)<<8)+(A>>8))%31){e.msg="incorrect header check",t.mode=ml;break}if((A&15)!==lA){e.msg="unknown compression method",t.mode=ml;break}if(A>>>=4,h-=4,D=(A&15)+8,t.wbits===0)t.wbits=D;else if(D>t.wbits){e.msg="invalid window size",t.mode=ml;break}t.dmax=1<>8&1),t.flags&512&&(B[0]=A&255,B[1]=A>>>8&255,t.check=gn(t.check,B,2,0)),A=0,h=0,t.mode=aA;case aA:for(;h<32;){if(u===0)break e;u--,A+=l[o++]<>>8&255,B[2]=A>>>16&255,B[3]=A>>>24&255,t.check=gn(t.check,B,4,0)),A=0,h=0,t.mode=iA;case iA:for(;h<16;){if(u===0)break e;u--,A+=l[o++]<>8),t.flags&512&&(B[0]=A&255,B[1]=A>>>8&255,t.check=gn(t.check,B,2,0)),A=0,h=0,t.mode=oA;case oA:if(t.flags&1024){for(;h<16;){if(u===0)break e;u--,A+=l[o++]<>>8&255,t.check=gn(t.check,B,2,0)),A=0,h=0}else t.head&&(t.head.extra=null);t.mode=sA;case sA:if(t.flags&1024&&(v=t.length,v>u&&(v=u),v&&(t.head&&(D=t.head.extra_len-t.length,t.head.extra||(t.head.extra=new Array(t.head.extra_len)),kr.arraySet(t.head.extra,l,o,v,D)),t.flags&512&&(t.check=gn(t.check,l,v,o)),u-=v,o+=v,t.length-=v),t.length))break e;t.length=0,t.mode=cA;case cA:if(t.flags&2048){if(u===0)break e;v=0;do D=l[o+v++],t.head&&D&&t.length<65536&&(t.head.name+=String.fromCharCode(D));while(D&&v>9&1,t.head.done=!0),e.adler=t.check=0,t.mode=Sn;break;case fA:for(;h<32;){if(u===0)break e;u--,A+=l[o++]<>>=h&7,h-=h&7,t.mode=zc;break}for(;h<3;){if(u===0)break e;u--,A+=l[o++]<>>=1,h-=1,A&3){case 0:t.mode=AA;break;case 1:if(qv(t),t.mode=rs,n===ts){A>>>=2,h-=2;break e}break;case 2:t.mode=mA;break;case 3:e.msg="invalid block type",t.mode=ml}A>>>=2,h-=2;break;case AA:for(A>>>=h&7,h-=h&7;h<32;){if(u===0)break e;u--,A+=l[o++]<>>16^65535)){e.msg="invalid stored block lengths",t.mode=ml;break}if(t.length=A&65535,A=0,h=0,t.mode=Pc,n===ts)break e;case Pc:t.mode=hA;case hA:if(v=t.length,v){if(v>u&&(v=u),v>f&&(v=f),v===0)break e;kr.arraySet(a,l,o,v,s),u-=v,o+=v,f-=v,s+=v,t.length-=v;break}t.mode=Sn;break;case mA:for(;h<14;){if(u===0)break e;u--,A+=l[o++]<>>=5,h-=5,t.ndist=(A&31)+1,A>>>=5,h-=5,t.ncode=(A&15)+4,A>>>=4,h-=4,t.nlen>286||t.ndist>30){e.msg="too many length or distance symbols",t.mode=ml;break}t.have=0,t.mode=gA;case gA:for(;t.have>>=3,h-=3}for(;t.have<19;)t.lens[z[t.have++]]=0;if(t.lencode=t.lendyn,t.lenbits=7,F={bits:t.lenbits},S=Yi(Dv,t.lens,0,19,t.lencode,0,t.work,F),t.lenbits=F.bits,S){e.msg="invalid code lengths set",t.mode=ml;break}t.have=0,t.mode=pA;case pA:for(;t.have>>24,C=k>>>16&255,I=k&65535,!(w<=h);){if(u===0)break e;u--,A+=l[o++]<>>=w,h-=w,t.lens[t.have++]=I;else{if(I===16){for(E=w+2;h>>=w,h-=w,t.have===0){e.msg="invalid bit length repeat",t.mode=ml;break}D=t.lens[t.have-1],v=3+(A&3),A>>>=2,h-=2}else if(I===17){for(E=w+3;h>>=w,h-=w,D=0,v=3+(A&7),A>>>=3,h-=3}else{for(E=w+7;h>>=w,h-=w,D=0,v=11+(A&127),A>>>=7,h-=7}if(t.have+v>t.nlen+t.ndist){e.msg="invalid bit length repeat",t.mode=ml;break}for(;v--;)t.lens[t.have++]=D}}if(t.mode===ml)break;if(t.lens[256]===0){e.msg="invalid code -- missing end-of-block",t.mode=ml;break}if(t.lenbits=9,F={bits:t.lenbits},S=Yi(Jf,t.lens,0,t.nlen,t.lencode,0,t.work,F),t.lenbits=F.bits,S){e.msg="invalid literal/lengths set",t.mode=ml;break}if(t.distbits=6,t.distcode=t.distdyn,F={bits:t.distbits},S=Yi($f,t.lens,t.nlen,t.ndist,t.distcode,0,t.work,F),t.distbits=F.bits,S){e.msg="invalid distances set",t.mode=ml;break}if(t.mode=rs,n===ts)break e;case rs:t.mode=ns;case ns:if(u>=6&&f>=258){e.next_out=s,e.avail_out=f,e.next_in=o,e.avail_in=u,t.hold=A,t.bits=h,Sv(e,p),s=e.next_out,a=e.output,f=e.avail_out,o=e.next_in,l=e.input,u=e.avail_in,A=t.hold,h=t.bits,t.mode===Sn&&(t.back=-1);break}for(t.back=0;k=t.lencode[A&(1<>>24,C=k>>>16&255,I=k&65535,!(w<=h);){if(u===0)break e;u--,A+=l[o++]<>x)],w=k>>>24,C=k>>>16&255,I=k&65535,!(x+w<=h);){if(u===0)break e;u--,A+=l[o++]<>>=x,h-=x,t.back+=x}if(A>>>=w,h-=w,t.back+=w,t.length=I,C===0){t.mode=wA;break}if(C&32){t.back=-1,t.mode=Sn;break}if(C&64){e.msg="invalid literal/length code",t.mode=ml;break}t.extra=C&15,t.mode=vA;case vA:if(t.extra){for(E=t.extra;h>>=t.extra,h-=t.extra,t.back+=t.extra}t.was=t.length,t.mode=yA;case yA:for(;k=t.distcode[A&(1<>>24,C=k>>>16&255,I=k&65535,!(w<=h);){if(u===0)break e;u--,A+=l[o++]<>x)],w=k>>>24,C=k>>>16&255,I=k&65535,!(x+w<=h);){if(u===0)break e;u--,A+=l[o++]<>>=x,h-=x,t.back+=x}if(A>>>=w,h-=w,t.back+=w,C&64){e.msg="invalid distance code",t.mode=ml;break}t.offset=I,t.extra=C&15,t.mode=bA;case bA:if(t.extra){for(E=t.extra;h>>=t.extra,h-=t.extra,t.back+=t.extra}if(t.offset>t.dmax){e.msg="invalid distance too far back",t.mode=ml;break}t.mode=kA;case kA:if(f===0)break e;if(v=p-f,t.offset>v){if(v=t.offset-v,v>t.whave&&t.sane){e.msg="invalid distance too far back",t.mode=ml;break}v>t.wnext?(v-=t.wnext,y=t.wsize-v):y=t.wnext-v,v>t.length&&(v=t.length),b=t.window}else b=a,y=s-t.offset,v=t.length;v>f&&(v=f),f-=v,t.length-=v;do a[s++]=b[y++];while(--v);t.length===0&&(t.mode=ns);break;case wA:if(f===0)break e;a[s++]=t.length,f--,t.mode=ns;break;case zc:if(t.wrap){for(;h<32;){if(u===0)break e;u--,A|=l[o++]<{"use strict";PA.exports={Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_TREES:6,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_BUF_ERROR:-5,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,Z_BINARY:0,Z_TEXT:1,Z_UNKNOWN:2,Z_DEFLATED:8}});var NA=kl((sw,zA)=>{"use strict";function Wv(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1}zA.exports=Wv});var LA=kl(_i=>{"use strict";var oi=BA(),Qi=Tn(),as=xc(),wl=Oc(),Vc=Go(),Gv=Cc(),Yv=NA(),FA=Object.prototype.toString;function Da(e){if(!(this instanceof Da))return new Da(e);this.options=Qi.assign({chunkSize:16384,windowBits:0,to:""},e||{});var n=this.options;n.raw&&n.windowBits>=0&&n.windowBits<16&&(n.windowBits=-n.windowBits,n.windowBits===0&&(n.windowBits=-15)),n.windowBits>=0&&n.windowBits<16&&!(e&&e.windowBits)&&(n.windowBits+=32),n.windowBits>15&&n.windowBits<48&&(n.windowBits&15)==0&&(n.windowBits|=15),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new Gv,this.strm.avail_out=0;var t=oi.inflateInit2(this.strm,n.windowBits);if(t!==wl.Z_OK)throw new Error(Vc[t]);if(this.header=new Yv,oi.inflateGetHeader(this.strm,this.header),n.dictionary&&(typeof n.dictionary=="string"?n.dictionary=as.string2buf(n.dictionary):FA.call(n.dictionary)==="[object ArrayBuffer]"&&(n.dictionary=new Uint8Array(n.dictionary)),n.raw&&(t=oi.inflateSetDictionary(this.strm,n.dictionary),t!==wl.Z_OK)))throw new Error(Vc[t])}Da.prototype.push=function(e,n){var t=this.strm,l=this.options.chunkSize,a=this.options.dictionary,o,s,u,f,A,h=!1;if(this.ended)return!1;s=n===~~n?n:n===!0?wl.Z_FINISH:wl.Z_NO_FLUSH,typeof e=="string"?t.input=as.binstring2buf(e):FA.call(e)==="[object ArrayBuffer]"?t.input=new Uint8Array(e):t.input=e,t.next_in=0,t.avail_in=t.input.length;do{if(t.avail_out===0&&(t.output=new Qi.Buf8(l),t.next_out=0,t.avail_out=l),o=oi.inflate(t,wl.Z_NO_FLUSH),o===wl.Z_NEED_DICT&&a&&(o=oi.inflateSetDictionary(this.strm,a)),o===wl.Z_BUF_ERROR&&h===!0&&(o=wl.Z_OK,h=!1),o!==wl.Z_STREAM_END&&o!==wl.Z_OK)return this.onEnd(o),this.ended=!0,!1;t.next_out&&(t.avail_out===0||o===wl.Z_STREAM_END||t.avail_in===0&&(s===wl.Z_FINISH||s===wl.Z_SYNC_FLUSH))&&(this.options.to==="string"?(u=as.utf8border(t.output,t.next_out),f=t.next_out-u,A=as.buf2string(t.output,u),t.next_out=f,t.avail_out=l-f,f&&Qi.arraySet(t.output,t.output,u,f,0),this.onData(A)):this.onData(Qi.shrinkBuf(t.output,t.next_out))),t.avail_in===0&&t.avail_out===0&&(h=!0)}while((t.avail_in>0||t.avail_out===0)&&o!==wl.Z_STREAM_END);return o===wl.Z_STREAM_END&&(s=wl.Z_FINISH),s===wl.Z_FINISH?(o=oi.inflateEnd(this.strm),this.onEnd(o),this.ended=!0,o===wl.Z_OK):(s===wl.Z_SYNC_FLUSH&&(this.onEnd(wl.Z_OK),t.avail_out=0),!0)};Da.prototype.onData=function(e){this.chunks.push(e)};Da.prototype.onEnd=function(e){e===wl.Z_OK&&(this.options.to==="string"?this.result=this.chunks.join(""):this.result=Qi.flattenChunks(this.chunks)),this.chunks=[],this.err=e,this.msg=this.strm.msg};function qc(e,n){var t=new Da(n);if(t.push(e,!0),t.err)throw t.msg||Vc[t.err];return t.result}function Qv(e,n){return n=n||{},n.raw=!0,qc(e,n)}_i.Inflate=Da;_i.inflate=qc;_i.inflateRaw=Qv;_i.ungzip=qc});var qA=kl((uw,VA)=>{"use strict";var _v=Tn().assign,Kv=jf(),Zv=LA(),Jv=Oc(),OA={};_v(OA,Kv,Zv,Jv);VA.exports=OA});function ra(e){$("#luckysheet-cell-loading").find("span").text(e).end().show()}function Hc(e){$(".luckysheet-cell-loading-inner").css("color",e)}function Ki(){$("#luckysheet-cell-loading").hide()}var jc=De(()=>{});var $v,Te,tl=De(()=>{Za();jt();Qt();_t();wt();je();$v={deepCopyFlowDataState:!1,deepCopyFlowDataCache:"",deepCopyFlowDataWorker:null,deepCopyFlowData:function(e){let n=this;return n.deepCopyFlowDataState?(n.deepCopyFlowDataWorker!=null&&n.deepCopyFlowDataWorker.terminate(),n.deepCopyFlowDataCache):(e==null&&(e=d.flowdata),$.extend(!0,[],e))},webWorkerFlowDataCache:function(e){let n=this;try{n.deepCopyFlowDataWorker!=null&&n.deepCopyFlowDataWorker.terminate();let t="data:text/javascript;chartset=US-ASCII,onmessage = function (e) { postMessage(e.data); };";n.deepCopyFlowDataState=!1;let l;if(br.isIE()==1){let a="self.onmessage=function(e){postMessage(e.data);}";l=new Worker("./plugins/Worker-helper.js"),l.postMessage(a)}else l=new Worker(t);n.deepCopyFlowDataWorker=l,l.postMessage(e),l.onmessage=function(a){n.deepCopyFlowDataCache=a.data,n.deepCopyFlowDataState=!0}}catch(t){n.deepCopyFlowDataCache=$.extend(!0,[],e)}},controlHandler:function(e,n){let l=this.deepCopyFlowData(d.flowdata),a=n||d.luckysheet_select_save[d.luckysheet_select_save.length-1],o=a.row==null?0:a.row[0],s=a.column==null?0:a.column[0],u=e.length,f=e[0].length,A=o+u-l.length,h=s+f-l[0].length;(A>0||h>0)&&(l=fl([].concat(l),A,h,!0));for(let g=0;g0||h>0?rr(l[0].length,l.length,l,null,d.luckysheet_select_save,"datachangeAll"):si(l,d.luckysheet_select_save)},clearRangeByindex:function(e,n,t,l,a){let o=Q(a),s=$.extend(!0,[],d.luckysheetfile[o].data);for(let u=e;u<=n;u++){let f=[].concat(s[u]);for(let A=t;A<=l;A++)m.delFunctionGroup(u,A),m.execFunctionGroup(u,A,""),f[A]=null;s[u]=f}if(a==d.currentSheetIndex){let u=n-e+1,f=l-t+1;u>5e3?Je(s,[{row:[e,n],column:[t,l]}]):si(s,{row:[e,n],column:[t,l]})}else d.luckysheetfile[o].data=s},controlHandlerD:function(e){let t=this.deepCopyFlowData(d.flowdata),l=d.luckysheet_select_save[d.luckysheet_select_save.length-1],a=l.row[0],o=l.row[1],s=l.column[0],u=l.column[1],f=e.length,A=e[0].length,h=a+f-t.length,g=s+A-t[0].length;(h>0||g>0)&&(t=fl([].concat(t),h,g,!0));for(let v=a;v<=o;v++)for(let y=s;y<=u;y++)t[v][y]=null;for(let v=0;v{dt();wt();je()});function Rr(e){let n;if(document.createRange)if(e.innerHTML!=e.innerText||e.innerHTML=="")e.focus(),n=window.getSelection(),n.selectAllChildren(e),n.collapseToEnd();else{let t=e.innerText.length;n=document.createRange(),n.selectNodeContents(e),n.setStart(e.childNodes[0],t),n.collapse(!0);let l=window.getSelection();l.removeAllRanges(),l.addRange(n)}else document.selection&&(n=document.body.createTextRange(),n.moveToElementText(e),n.collapse(!1),n.select())}function Mr(e){e.target.classList&&(e.target.classList.contains("luckysheet-cols-rows-shift-left")||e.target.classList.contains("luckysheet-cols-rows-shift-right"))||!$(e.target).hasClass("luckysheet-mousedown-cancel")&&$(e.target).filter("[class*='sp-palette']").length==0&&$(e.target).filter("[class*='sp-thumb']").length==0&&$(e.target).filter("[class*='sp-']").length==0&&($("#luckysheet-rightclick-menu").hide(),$("#luckysheet-cols-h-hover").hide(),$("#luckysheet-cols-menu-btn").hide(),$("#luckysheet-sheet-list, #luckysheet-rightclick-sheet-menu, #luckysheet-user-menu").hide(),$("body > .luckysheet-filter-menu, body > .luckysheet-filter-submenu, body > .luckysheet-cols-menu").hide(),d.luckysheet_cols_menu_status=!1)}function jA(e){if(window.getSelection){let n=document.createRange();n.selectNodeContents(e),n.startContainer&&os(n.startContainer)&&(window.getSelection().removeAllRanges(),window.getSelection().addRange(n))}else if(document.selection){let n=document.body.createTextRange();n.moveToElementText(e),n.select()}}function XA(e){if(window.getSelection){let t=document.createRange();var n=e.firstChild;t.setStart(n,0),t.setEnd(n,n.length),t.startContainer&&os(t.startContainer)&&(window.getSelection().removeAllRanges(),window.getSelection().addRange(t))}else if(document.selection){let t=document.body.createTextRange();t.moveToElementText(e),t.select()}}function UA(e,n){if(window.getSelection){let a=document.createRange();var t=e.firstChild,l=n.firstChild;a.setStart(t,0),a.setEnd(l,l.length),a.startContainer&&os(a.startContainer)&&(window.getSelection().removeAllRanges(),window.getSelection().addRange(a))}}function WA(e,n){if(window.getSelection){let l=document.createRange();var t=e.firstChild;n>t.length?n=t.length:n<0&&(n=0),l.setStart(t,n),l.collapse(!0),l.startContainer&&os(l.startContainer)&&(window.getSelection().removeAllRanges(),window.getSelection().addRange(l))}}function os(e){return e===document.body?!1:document.body.contains(e)}var Dn=De(()=>{je()});function bl(e){return e&&e.ct!=null&&e.ct.t=="inlineStr"&&e.ct.s!=null&&e.ct.s.length>0}function Rn(e){return e!=null&&e.t=="inlineStr"&&e.s!=null&&e.s.length>0}function QA(e,n,t,l){var a=window.getSelection(),o;a.type=="None"?o=d.inlineStringEditRange:o=a.getRangeAt(0);let s=o.commonAncestorContainer,u;s.id=="luckysheet-rich-text-editor"?u=$(s):u=$(s).closest("#luckysheet-rich-text-editor");let f=$(s).closest("#luckysheet-functionbox-cell");if(u.length==0&&f.length==0&&d.inlineStringEditRange!=null&&(o=d.inlineStringEditRange,s=o.commonAncestorContainer,s.id=="luckysheet-rich-text-editor"?u=$(s):u=$(s).closest("#luckysheet-rich-text-editor"),f=$(s).closest("#luckysheet-functionbox-cell")),o.collapsed===!0)return;let A=o.endContainer,h=o.startContainer,g=o.endOffset,p=o.startOffset;if(u.length>0){if(h===A){let v=h.parentNode,y,b=!1,k=v.innerText;u.html().substr(0,5)!=""+C+""}if(I!=""){let E=ss(v.style.cssText,n,t);if(b){let z=$(v).closest("#luckysheet-input-box").get(0);z!=null&&(E=Uc(z.style.cssText,E))}B+=""+I+""}if(x!=""){let E=v.style.cssText;if(b){let z=$(v).closest("#luckysheet-input-box").get(0);z!=null&&(E=Uc(z.style.cssText,E))}B+=""+x+""}h.parentNode.tagName=="SPAN"?(y=u.find("span").index(v),$(v).replaceWith(B)):(y=0,$(v).html(B));let F=0;T==R?F=y:F=y+1,XA(u.find("span").get(F))}else if(h.parentNode.tagName=="SPAN"&&A.parentNode.tagName=="SPAN"){let v=h.parentNode,y,b=A.parentNode,k;y=u.find("span").index(v),k=u.find("span").index(b);let w=v.innerHTML,C=b.innerHTML,I="",x="",T="",R="",D=0,S=p,B=g,F=C.length;I=w.substring(D,S),x=w.substring(S,w.length),T=C.substring(0,B),R=C.substring(B,F);let E=u.find("span"),z=E.slice(y,k+1),P="";for(let q=0;q"+G+""}if(I!=""&&(P+=""+I+""),x!=""){let q=ss(v.style.cssText,n,t);P+=""+x+""}if(y"+G+""}if(T!=""){let q=ss(b.style.cssText,n,t);P+=""+T+""}R!=""&&(P+=""+R+"");for(let q=k+1;q"+G+""}u.html(P);let L,O;D==S?(L=y,O=k):(L=y+1,O=k+1),E=u.find("span"),UA(E.get(L),E.get(O))}}else f.length>0}function _A(e){var n=window.getSelection();if(n.type=="None")return;var t=n.getRangeAt(0);let l=t.commonAncestorContainer,a;l.id=="luckysheet-rich-text-editor"?a=$(l):a=$(l).closest("#luckysheet-rich-text-editor");let o=$(l).closest("#luckysheet-functionbox-cell"),s=t.endContainer,u=t.startContainer,f=t.endOffset,A=t.startOffset;if(a.length>0){let h=u.parentNode;u.id=="luckysheet-rich-text-editor"&&(h=$(u).find("span"),h.length==0&&(u.innerHTML=`${u.innerText}`,h=$(u).find("span")),h=h.get(h.length-1),A=h.innerHTML.length),t.collapsed===!1&&t.deleteContents();let g=h.innerText,p="",v="",y=0,b=A;p=g.substring(y,b),v=g.substring(b,g.length);let k,w;if(u.parentNode.tagName=="SPAN"){let C=a.find("span");if(k=C.index(h),k==C.length-1&&v==""){let I=C[k].innerHTML;I.substr(I.length-1,1)==` -`?w=""+p+` -`:w=""+p+` - -`}else w=""+p+` -`+v+"";$(h).replaceWith(w)}else{let C=h.style.cssText,I=getComputedStyle(u.parentNode).fontSize;if(v==""?w=""+p+` - -`:w=""+p+` -`+v+"",u.id=="luckysheet-rich-text-editor"){$(h).replaceWith(w);let x=a.find("span");k=x.length-1,A=x.get(k).innerHTML.length-1}else $(h).html(w),k=0}WA(a.find("span").get(k),A+1)}else o.length>0}function KA(e,n,t){if(e.ct==null)return;let l=e.ct.s;if(l!=null)for(let a=0;a{s=s.toLowerCase();let u=Nl(s.substr(0,s.indexOf(":"))),f=Nl(s.substr(s.indexOf(":")+1));if(u=="font-weight"&&(f=="bold"?o.bl=1:o.bl=0),u=="font-style"&&(f=="italic"?o.it=1:o.it=0),u=="font-family"){let A=a[f];A==null?o.ff=f:o.ff=A}u=="font-size"&&(console.log("font-size",f),o.fs=parseInt(f)),u=="color"&&(o.fc=f),u=="text-decoration"&&f.split(" ").forEach(h=>{h=h.toLowerCase(),h=="underline"&&(o.un=1),h=="line-through"&&(o.cl=1)}),u=="lucky-strike"&&(o.cl=f),u=="lucky-underline"&&(o.un=f)}),o}function $A(e,n){let t=e.split(";");if(n==null||n.length==0)return e;if(e.indexOf(n)>-1)for(let l=0;l-1)for(let o=0;o0&&(a+=u+":"+f+";")}else n.length>0&&(e+=n+":"+t+";",a=e);return a}function ry(e,n){let t=e.split(";"),l="",a=n;if(n==null||n.length==0)return e;if(n in JA&&(n=JA[n]),e.indexOf(n)>-1)for(let o=0;o0&&(l+=u+":"+f+";")}else l=e;return l}function ss(e,n,t){let l={};if(l[n]=t,n=="un"){let u=$A(e,"color");u==""&&(u="#000000");let f=$A(e,"font-size");f==""&&(f=11),f=parseInt(f),l._fontSize=f,l._color=u}let a=Ji(l,void 0,void 0,!1,e),o=Nl(a.substr(0,a.indexOf(":"))),s=Nl(a.substr(a.indexOf(":")+1));return s=s.substr(0,s.length-1),e=ry(e,n),e=ly(e,o,s),e}function Uc(e,n,t=!0){let l=e.split(";"),a=n.split(";"),o="",s={};for(let u=0;u{Qt();Dn();yt();je();GA={bl:1,it:1,ff:1,cl:1,un:1,fs:1,fc:1},YA={"font-weight":1,"font-style":1,"font-family":1,"text-decoration":1,"border-bottom":1,"font-size":1,color:1};JA={bl:"font-weight",it:"font-style",ff:"font-family",fs:"font-size",fc:"color",cl:"text-decoration",un:"border-bottom"}});function Ct(e,n){if(e==null&&(e=d.luckysheet_select_save[0]),e.row==null||e.row.length==0)return[];let t,l;n!=null&&n!=d.currentSheetIndex?(t=d.luckysheetfile[Q(n)].data,l=d.luckysheetfile[Q(n)].config):(t=Te.deepCopyFlowData(d.flowdata),l=d.config);let a=[];for(let o=e.row[0];o<=e.row[1];o++){if(t[o]==null||l.rowhidden!=null&&l.rowhidden[o]!=null)continue;let s=[];for(let u=e.column[0];u<=e.column[1];u++)s.push(t[o][u]);a.push(s)}return a}function $i(e,n){if(n==null||n.row==null||n.row.length==0)return[];let t=Zi(d.luckysheetfile[Q(d.currentSheetIndex)].dynamicArray),l=[];if(e==null)return l;for(let a=n.row[0];a<=n.row[1];a++){if(e[a]==null)continue;let o=[];for(let s=n.column[0];s<=n.column[1];s++){let u;a+"_"+s in t?u=t[a+"_"+s]:u=e[a][s],o.push(u)}l.push(o)}return l}function eh(e){if(e==null||e.row==null||e.row.length==0)return[];let n=[];for(let t=e.row[0];t<=e.row[1];t++){let l=[];if(!(d.config.rowhidden!=null&&d.config.rowhidden[t]!=null)){for(let a=e.column[0];a<=e.column[1];a++){let o="";d.flowdata[t]!=null&&d.flowdata[t][a]!=null&&(o=d.flowdata[t][a]),l.push(o)}n.push(l)}}return n}function Fe(e,n,t,l){l==null&&(l="v"),t==null&&(t=d.flowdata);let a;if(e!=null&&n!=null)a=t[e][n];else if(e!=null)a=t[e];else if(n!=null)a=t[0].map(function(u,f){return t.map(function(A){return A[f]})})[n];else return t;let o=a;return N(a)=="object"&&(o=a[l],l=="f"&&o!=null?o=m.functionHTMLGenerate(o):l=="f"?o=a.v:a&&a.ct&&a.ct.t=="d"&&(o=a.m)),o==null&&(o=null),o}function fl(e,n,t,l){if(n<=0&&t<=0)return e;n<=0&&(n=0),t<=0&&(t=0);let a=0;e.length==0?(e=[],a=0):a=e[0].length;let o=[];for(let u=0;u"+f.v+""}}return s}return""}function Ji(e,n,t,l=!0,a){if(e==null)return;let o="",u=te().fontarray;for(let f in e){let A=e[f];if(l&&(A=Er(e,f)),f=="bl"&&A!="0"&&(o+="font-weight: bold;"),f=="it"&&A!="0"&&(o+="font-style:italic;"),f=="ff"){let h=A;isNaN(parseInt(A))?h=A:h=u[parseInt(A)],o+="font-family: "+h+";"}if(f=="fs"&&(o+="font-size: "+A+"pt;"),(f=="fc"&&A!="#000000"||n!=null||t!=null&&t.textColor!=null)&&(t!=null&&t.textColor!=null?o+="color: "+t.textColor+";":n!=null?o+="color: "+n[0]+";":o+="color: "+A+";"),(f=="cl"||f=="un")&&A!="0"){let h=[];a&&a.split(";").forEach(p=>{p=p.toLowerCase(),Nl(p.substr(0,p.indexOf(":")))=="text-decoration"&&h.push(p.substr(p.indexOf(":")+1))}),f=="cl"&&(h.includes("line-through")?h=h.filter(g=>g!="line-through"):h.push("line-through")),f=="un"&&(h.includes("underline")?h=h.filter(g=>g!="underline"):h.push("underline")),o+="text-decoration: "+h.join(" ")+";"}}return o}function Er(e,n){let t=e;return n in{bl:1,it:1,ff:1,cl:1,un:1}||n=="fs"&&bl(e)?t==null?t="0":(t=t[n],t==null&&(t="0")):n=="fc"?t==null?t="#000000":(t=t[n],t==null&&(t="#000000"),t.indexOf("rgba")>-1&&(t=na(t))):n=="bg"?t==null?t=null:(t=t[n],t==null?t=null:t.toString().indexOf("rgba")>-1&&(t=na(t))):n.substr(0,2)=="bs"?t==null?t="none":(t=t[n],t==null&&(t="none")):n.substr(0,2)=="bc"?t==null?t="#000000":(t=t[n],t==null&&(t="#000000")):n=="ht"?(t==null?t="1":(t=t[n],t==null&&(t="1")),["0","1","2"].indexOf(t.toString())==-1&&(t="1")):n=="vt"?(t==null?t="0":(t=t[n],t==null&&(t="0")),["0","1","2"].indexOf(t.toString())==-1&&(t="0")):n=="ct"?t==null?t=null:(t=t[n],t==null&&(t=null)):n=="fs"?t==null?t=String(d.defaultFontSize):(t=t[n],t==null&&(t=String(d.defaultFontSize))):n=="tb"||n=="tr"?t==null?t="0":(t=t[n],t==null&&(t="0")):n=="rt"&&(t==null?t=null:(t=t[n],t==null&&(t=null))),t}function Nl(e){return e==null||e.length==0?e:e.replace(/^\s+|\s+$/gm,"")}var Qt=De(()=>{dt();wt();$t();jt();tl();is();cl();Qr();yt();je()});function ay(e,n){let t=0,l=e.length-1;for(;t<=l;){let a=parseInt((l+t)/2);if(n=e[a-1]))return a;if(n>=e[a])t=a+1;else if(n=0&&s>=t;){if(l=e[s],s==0?a=0:a=e[s-1],n>=a&&n=a&&n=e[t-20]?t=iy(e,n):t=ay(e,n),t}var Br=De(()=>{});function nl(e){let n=0,t=0;return n=d.visibledatarow[e],e==0?t=0:t=d.visibledatarow[e-1],[t,n,e]}function Tt(e){let n=nt(d.visibledatarow,e);return n==-1&&e>0?n=d.visibledatarow.length-1:n==-1&&e<=0&&(n=0),nl(n)}function Ft(e){let n=0,t=0;return n=d.visibledatacolumn[e],e==0?t=0:t=d.visibledatacolumn[e-1],[t,n,e]}function th(e,n){let t=0,l=0;return t=d.visibledatacolumn[e+n-1],e==0?l=0:l=d.visibledatacolumn[e-1],[l,t,e]}function xt(e){let n=nt(d.visibledatacolumn,e);return n==-1&&e>0?n=d.visibledatacolumn.length-1:n==-1&&e<=0&&(n=0),Ft(n)}function Ke(e,n){let t=$("#"+d.container).offset(),l=e-t.left-d.rowHeaderWidth,a=n-t.top-d.infobarHeight-d.toolbarHeight-d.calculatebarHeight-d.columnHeaderHeight;return[l,a]}var El=De(()=>{Br();je()});function vl(e,n,t,l){var s;let a=$.extend(!0,{},l);a.rowlen==null&&(a.rowlen={}),a.customHeight==null&&(a.customHeight={});let o=$("#luckysheetTableContent").get(0).getContext("2d");o.textBaseline="top";for(let u=n;u<=t;u++){if(a.rowhidden!=null&&a.rowhidden[u]!=null)continue;let f=d.defaultrowlen;if(a.customHeight[u]!=1){delete a.rowlen[u];for(let A=0;Af&&(f=v)}}f=f/d.zoomRatio,f!=d.defaultrowlen?a.rowlen[u]=f:((s=l.rowlen)==null?void 0:s[u])&&(a.rowlen[u]=l.rowlen[u])}}return a}function lh(e,n){let t=0,l=$("#luckysheetTableContent").get(0).getContext("2d");l.textBaseline="top";for(let a=0;at&&(t=f)}}return t}function oy(e,n){let t=Ft(n);return e.mc&&e.mc.cs!==1&&(t=th(n,e.mc.cs)),t[1]-t[0]-2}function rh(e,n,t){let l=0,a=ds(t,n),o=$("#luckysheetTableContent").get(0).getContext("2d");o.textBaseline="top";for(var s=0;sl&&(l=h)}}return l}function ds(e,n){let t=[],l=0;for(let a=0;a180||y<0)&&(y=0),y=parseInt(y),y>90&&(y=90-y,b=0,k=1),n.textAlign="start";let w={};w.values=[];let C,I="0",x="0",T=11,R=!1,D,S=[];if(bl(e)){let B=e.ct.s,F=0;for(let E=0;Ea&&P[E]!=null&&(L.push(z-ue),z=ue,E+=1),G==S.length-1&&L.push(z),P[E]==null&&(P[E]=[]);let fe={content:re,style:_,width:se,height:ue,left:0,top:0,colIndex:E,asc:oe.actualBoundingBoxAscent,desc:oe.actualBoundingBoxDescent,inline:!0};_.wrap===!0&&(fe.wrap=!0),P[E].push(fe),console.log("normal",G,E,_,j,P),j=_}}else{let j=_r(D,n),G=j.actualBoundingBoxDescent+j.actualBoundingBoxAscent;D=D.toString();let _=[];D.length>1?_=D.split(""):_.push(D);let ee=_r(_[0],n).width;for(let re=0;re<_.length;re++){let oe=ee+f,se=G+A;z+=se,p=="2"&&z>a&&P[E]!=null&&(L.push(z-se),z=se,E+=1),re==_.length-1&&L.push(z),P[E]==null&&(P[E]=[]),P[E].push({content:_[re],style:C,width:oe,height:se,left:0,top:0,colIndex:E,asc:j.actualBoundingBoxAscent,desc:j.actualBoundingBoxDescent})}}let O=[];for(let j=0;j1)for(let Ye=0;Yea),st+A>a&&O[P]!=null&&p=="2"&&ue!=S.length)if(K!=null&&Kl&&O[P]!=null&&p=="2"&&ue!=S.length)if(K!=null&&Ka&&O[P]!=null&&ue!=D.length)ce!=null&&ce.indexl&&O[P]!=null&&ue!=D.length)ce!=null&&ce.index=0;zt--){let st=lt[zt],kt,Ye;if(y!=0){let Me,Ze=le+bt.asc;if(Me=le/Math.tan(ie)-ke+z,h=="0"){let Le=E/Math.sin(ie);g=="0"?(kt=Me+l/2-F/2+Oe*Math.cos(ie)/2,Ye=Ze+a/2-E/2-Oe*Math.cos(ie)/2):g=="1"?(kt=Me+l/2-F/2,Ye=Ze-(E/2-Be/2)):g=="2"&&(kt=Me+l/2-F/2+Oe*Math.cos(ie),Ye=Ze+a-Be/2-E/2-Oe*Math.cos(ie))}else h=="1"?g=="0"?(kt=Me-Be*Math.sin(ie)/2+Oe*Math.cos(ie)/2,Ye=Ze+a/2+Be*Math.cos(ie)/2-Oe*Math.cos(ie)/2):g=="1"?(kt=Me-Be*Math.sin(ie),Ye=Ze+Be*Math.cos(ie)):g=="2"&&(kt=Me+Oe*Math.cos(ie),Ye=Ze+a-Oe*Math.cos(ie)):h=="2"&&(g=="0"?(kt=Me+l-xe/2-(z/2+E/2/Math.tan(ie))+Oe*Math.cos(ie)/2,Ye=Ze+a/2-E/2-Oe*Math.cos(ie)/2):g=="1"?(kt=Me+l-F+We,Ye=Ze-E):g=="2"&&(kt=Me+l-xe*Math.cos(ie)+Oe*Math.cos(ie),Ye=Ze+a-xe*Math.sin(ie)-Oe*Math.cos(ie)))}st.left=kt,st.top=Ye,to(st,I,x,{width:st.width,height:st.height,left:kt-st.width,top:Ye,asc:bt.asc,desc:bt.desc,fs:st.fs}),w.values.push(st),ke+=st.width}le+=bt.height}}else for(let ze=0;ze{dt();ll();Qt();El();Pt();Qr();je()});function Mn(e,n){if(cy(),e!=null){d.visibledatarow=[],d.rh_height=0;for(let t=0;t300?a=300:a{je();wr();yl()});var En=kl((Wc,Gc)=>{(function(e,n){typeof Wc=="object"&&typeof Gc!="undefined"?Gc.exports=n():typeof define=="function"&&define.amd?define(n):(e=typeof globalThis!="undefined"?globalThis:e||self).dayjs=n()})(Wc,function(){"use strict";var e=1e3,n=6e4,t=36e5,l="millisecond",a="second",o="minute",s="hour",u="day",f="week",A="month",h="quarter",g="year",p="date",v="Invalid Date",y=/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/,b=/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,k={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),ordinal:function(z){var P=["th","st","nd","rd"],L=z%100;return"["+z+(P[(L-20)%10]||P[L]||P[0])+"]"}},w=function(z,P,L){var O=String(z);return!O||O.length>=P?z:""+Array(P+1-O.length).join(L)+z},C={s:w,z:function(z){var P=-z.utcOffset(),L=Math.abs(P),O=Math.floor(L/60),q=L%60;return(P<=0?"+":"-")+w(O,2,"0")+":"+w(q,2,"0")},m:function z(P,L){if(P.date()1)return z(G[0])}else{var _=P.name;x[_]=P,q=_}return!O&&q&&(I=q),q||!O&&I},S=function(z,P){if(R(z))return z.clone();var L=typeof P=="object"?P:{};return L.date=z,L.args=arguments,new F(L)},B=C;B.l=D,B.i=R,B.w=function(z,P){return S(z,{locale:P.$L,utc:P.$u,x:P.$x,$offset:P.$offset})};var F=function(){function z(L){this.$L=D(L.locale,null,!0),this.parse(L),this.$x=this.$x||L.x||{},this[T]=!0}var P=z.prototype;return P.parse=function(L){this.$d=function(O){var q=O.date,j=O.utc;if(q===null)return new Date(NaN);if(B.u(q))return new Date;if(q instanceof Date)return new Date(q);if(typeof q=="string"&&!/Z$/i.test(q)){var G=q.match(y);if(G){var _=G[2]-1||0,ee=(G[7]||"0").substring(0,3);return j?new Date(Date.UTC(G[1],_,G[3]||1,G[4]||0,G[5]||0,G[6]||0,ee)):new Date(G[1],_,G[3]||1,G[4]||0,G[5]||0,G[6]||0,ee)}}return new Date(q)}(L),this.init()},P.init=function(){var L=this.$d;this.$y=L.getFullYear(),this.$M=L.getMonth(),this.$D=L.getDate(),this.$W=L.getDay(),this.$H=L.getHours(),this.$m=L.getMinutes(),this.$s=L.getSeconds(),this.$ms=L.getMilliseconds()},P.$utils=function(){return B},P.isValid=function(){return this.$d.toString()!==v},P.isSame=function(L,O){var q=S(L);return this.startOf(O)<=q&&q<=this.endOf(O)},P.isAfter=function(L,O){return S(L)12||u>31)return!1;if(s==2){if(new Date(o,1,29).getDate()==29&&u>29)return!1;if(new Date(o,1,29).getDate()!=29&&u>28)return!1}return!0}}function Al(e,n){return(0,Yc.default)(e).diff((0,Yc.default)(n))}function aa(e){let n={};return qt(e)&&(n.date=!0),!isNaN(parseFloat(e))&&!Yn(e)&&(n.num=!0),n}function ur(e){let n="string";return qt(e)?n="date":!isNaN(parseFloat(e))&&!Yn(e)&&(n="num"),n}var Yc,Kr=De(()=>{Pt();Yc=Ol(En())});var Ma=kl((nh,fs)=>{(function(e,n){typeof define=="function"&&define.amd?define(n):typeof fs=="object"&&fs.exports?fs.exports=n():e.numeral=n()})(nh,function(){var e,n,t="2.0.6",l={},a={},o={currentLocale:"en",zeroFormat:null,nullFormat:null,defaultFormat:"0,0",scalePercentBy100:!0},s={currentLocale:o.currentLocale,zeroFormat:o.zeroFormat,nullFormat:o.nullFormat,defaultFormat:o.defaultFormat,scalePercentBy100:o.scalePercentBy100};function u(f,A){this._input=f,this._value=A}return e=function(f){var A,h,g,p;if(e.isNumeral(f))A=f.value();else if(f===0||typeof f=="undefined")A=0;else if(f===null||n.isNaN(f))A=null;else if(typeof f=="string")if(s.zeroFormat&&f===s.zeroFormat)A=0;else if(s.nullFormat&&f===s.nullFormat||!f.replace(/[^0-9]+/g,"").length)A=null;else{for(h in l)if(p=typeof l[h].regexps.unformat=="function"?l[h].regexps.unformat():l[h].regexps.unformat,p&&f.match(p)){g=l[h].unformat;break}g=g||e._.stringToNumber,A=g(f)}else A=Number(f)||null;return new u(f,A)},e.version=t,e.isNumeral=function(f){return f instanceof u},e._=n={numberToFormat:function(f,A,h){var g=a[e.options.currentLocale],p=!1,v=!1,y=0,b="",k=1e12,w=1e9,C=1e6,I=1e3,x="",T=!1,R,D,S,B,F,E,z,P,L,O;if(f=f||0,D=Math.abs(f),e._.includes(A,"(")?(p=!0,A=A.replace(/[\(|\)]/g,"")):(e._.includes(A,"+")||e._.includes(A,"-"))&&(P=e._.includes(A,"+")?A.indexOf("+"):f<0?A.indexOf("-"):-1,A=A.replace(/[\+|\-]/g,"")),e._.includes(A,"a")&&(R=A.match(/a(k|m|b|t)?/),R=R?R[1]:!1,e._.includes(A," a")&&(b=" "),A=A.replace(new RegExp(b+"a[kmbt]?"),""),D>=k&&!R||R==="t"?(b+=g.abbreviations.trillion,f=f/k):D=w&&!R||R==="b"?(b+=g.abbreviations.billion,f=f/w):D=C&&!R||R==="m"?(b+=g.abbreviations.million,f=f/C):(D=I&&!R||R==="k")&&(b+=g.abbreviations.thousand,f=f/I)),e._.includes(A,"[.]")&&(v=!0,A=A.replace("[.]",".")),E=f.toString().split(".")[0],z=A.split(".")[1],L=A.indexOf(","),y=(A.split(".")[0].split(",")[0].match(/0/g)||[]).length,z?(e._.includes(z,"[")?(z=z.replace("]",""),z=z.split("["),x=e._.toFixed(f,z[0].length+z[1].length,h,z[1].length)):x=e._.toFixed(f,z.length,h),E=x.split(".")[0],e._.includes(x,".")?x=g.delimiters.decimal+x.split(".")[1]:x="",v&&Number(x.slice(1))===0&&(x="")):E=e._.toFixed(f,0,h),b&&!R&&Number(E)>=1e3&&b!==g.abbreviations.trillion)switch(E=String(Number(E)/1e3),b){case g.abbreviations.thousand:b=g.abbreviations.million;break;case g.abbreviations.million:b=g.abbreviations.billion;break;case g.abbreviations.billion:b=g.abbreviations.trillion;break}if(e._.includes(E,"-")&&(E=E.slice(1),T=!0),E.length0;q--)E="0"+E;return L>-1&&(E=E.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g,"$1"+g.delimiters.thousands)),A.indexOf(".")===0&&(E=""),O=E+x+(b||""),p?O=(p&&T?"(":"")+O+(p&&T?")":""):P>=0?O=P===0?(T?"-":"+")+O:O+(T?"-":"+"):T&&(O="-"+O),O},stringToNumber:function(f){var A=a[s.currentLocale],h=f,g={thousand:3,million:6,billion:9,trillion:12},p,v,y,b;if(s.zeroFormat&&f===s.zeroFormat)v=0;else if(s.nullFormat&&f===s.nullFormat||!f.replace(/[^0-9]+/g,"").length)v=null;else{v=1,A.delimiters.decimal!=="."&&(f=f.replace(/\./g,"").replace(A.delimiters.decimal,"."));for(p in g)if(b=new RegExp("[^a-zA-Z]"+A.abbreviations[p]+"(?:\\)|(\\"+A.currency.symbol+")?(?:\\))?)?$"),h.match(b)){v*=Math.pow(10,g[p]);break}v*=(f.split("-").length+Math.min(f.split("(").length-1,f.split(")").length-1))%2?1:-1,f=f.replace(/[^0-9\.]+/g,""),v*=Number(f)}return v},isNaN:function(f){return typeof f=="number"&&isNaN(f)},includes:function(f,A){return f.indexOf(A)!==-1},insert:function(f,A,h){return f.slice(0,h)+A+f.slice(h)},reduce:function(f,A){if(this===null)throw new TypeError("Array.prototype.reduce called on null or undefined");if(typeof A!="function")throw new TypeError(A+" is not a function");var h=Object(f),g=h.length>>>0,p=0,v;if(arguments.length===3)v=arguments[2];else{for(;p=g)throw new TypeError("Reduce of empty array with no initial value");v=h[p++]}for(;pg?A:g},1)},toFixed:function(f,A,h,g){var p=f.toString().split("."),v=A-(g||0),y,b,k,w;return p.length===2?y=Math.min(Math.max(p[1].length,v),A):y=v,k=Math.pow(10,y),w=(h(f+"e+"+y)/k).toFixed(y),g>A-y&&(b=new RegExp("\\.?0{1,"+(g-(A-y))+"}$"),w=w.replace(b,"")),w}},e.options=s,e.formats=l,e.locales=a,e.locale=function(f){return f&&(s.currentLocale=f.toLowerCase()),s.currentLocale},e.localeData=function(f){if(!f)return a[s.currentLocale];if(f=f.toLowerCase(),!a[f])throw new Error("Unknown locale : "+f);return a[f]},e.reset=function(){for(var f in o)s[f]=o[f]},e.zeroFormat=function(f){s.zeroFormat=typeof f=="string"?f:null},e.nullFormat=function(f){s.nullFormat=typeof f=="string"?f:null},e.defaultFormat=function(f){s.defaultFormat=typeof f=="string"?f:"0.0"},e.register=function(f,A,h){if(A=A.toLowerCase(),this[f+"s"][A])throw new TypeError(A+" "+f+" already registered.");return this[f+"s"][A]=h,h},e.validate=function(f,A){var h,g,p,v,y,b,k,w;if(typeof f!="string"&&(f+="",console.warn&&console.warn("Numeral.js: Value is not string. It has been co-erced to: ",f)),f=f.trim(),f.match(/^\d+$/))return!0;if(f==="")return!1;try{k=e.localeData(A)}catch(C){k=e.localeData(e.locale())}return p=k.currency.symbol,y=k.abbreviations,h=k.delimiters.decimal,k.delimiters.thousands==="."?g="\\.":g=k.delimiters.thousands,w=f.match(/^[^\d]+/),w!==null&&(f=f.substr(1),w[0]!==p)||(w=f.match(/[^\d]+$/),w!==null&&(f=f.slice(0,-1),w[0]!==y.thousand&&w[0]!==y.million&&w[0]!==y.billion&&w[0]!==y.trillion))?!1:(b=new RegExp(g+"{2}"),f.match(/[^\d.,]/g)?!1:(v=f.split(h),v.length>2?!1:v.length<2?!!v[0].match(/^\d+.*\d$/)&&!v[0].match(b):v[0].length===1?!!v[0].match(/^\d+$/)&&!v[0].match(b)&&!!v[1].match(/^\d+$/):!!v[0].match(/^\d+.*\d$/)&&!v[0].match(b)&&!!v[1].match(/^\d+$/)))},e.fn=u.prototype={clone:function(){return e(this)},format:function(f,A){var h=this._value,g=f||s.defaultFormat,p,v,y;if(A=A||Math.round,h===0&&s.zeroFormat!==null)v=s.zeroFormat;else if(h===null&&s.nullFormat!==null)v=s.nullFormat;else{for(p in l)if(g.match(l[p].regexps.format)){y=l[p].format;break}y=y||e._.numberToFormat,v=y(h,g,A)}return v},value:function(){return this._value},input:function(){return this._input},set:function(f){return this._value=Number(f),this},add:function(f){var A=n.correctionFactor.call(null,this._value,f);function h(g,p,v,y){return g+Math.round(A*p)}return this._value=n.reduce([this._value,f],h,0)/A,this},subtract:function(f){var A=n.correctionFactor.call(null,this._value,f);function h(g,p,v,y){return g-Math.round(A*p)}return this._value=n.reduce([f],h,Math.round(this._value*A))/A,this},multiply:function(f){function A(h,g,p,v){var y=n.correctionFactor(h,g);return Math.round(h*y)*Math.round(g*y)/Math.round(y*y)}return this._value=n.reduce([this._value,f],A,1),this},divide:function(f){function A(h,g,p,v){var y=n.correctionFactor(h,g);return Math.round(h*y)/Math.round(g*y)}return this._value=n.reduce([this._value,f],A),this},difference:function(f){return Math.abs(e(this._value).subtract(f).value())}},e.register("locale","en",{delimiters:{thousands:",",decimal:"."},abbreviations:{thousand:"k",million:"m",billion:"b",trillion:"t"},ordinal:function(f){var A=f%10;return~~(f%100/10)==1?"th":A===1?"st":A===2?"nd":A===3?"rd":"th"},currency:{symbol:"$"}}),function(){e.register("format","bps",{regexps:{format:/(BPS)/,unformat:/(BPS)/},format:function(f,A,h){var g=e._.includes(A," BPS")?" ":"",p;return f=f*1e4,A=A.replace(/\s?BPS/,""),p=e._.numberToFormat(f,A,h),e._.includes(p,")")?(p=p.split(""),p.splice(-1,0,g+"BPS"),p=p.join("")):p=p+g+"BPS",p},unformat:function(f){return+(e._.stringToNumber(f)*1e-4).toFixed(15)}})}(),function(){var f={base:1e3,suffixes:["B","KB","MB","GB","TB","PB","EB","ZB","YB"]},A={base:1024,suffixes:["B","KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"]},h=f.suffixes.concat(A.suffixes.filter(function(p){return f.suffixes.indexOf(p)<0})),g=h.join("|");g="("+g.replace("B","B(?!PS)")+")",e.register("format","bytes",{regexps:{format:/([0\s]i?b)/,unformat:new RegExp(g)},format:function(p,v,y){var b,k=e._.includes(v,"ib")?A:f,w=e._.includes(v," b")||e._.includes(v," ib")?" ":"",C,I,x;for(v=v.replace(/\s?i?b/,""),C=0;C<=k.suffixes.length;C++)if(I=Math.pow(k.base,C),x=Math.pow(k.base,C+1),p===null||p===0||p>=I&&p0&&(p=p/I);break}return b=e._.numberToFormat(p,v,y),b+w},unformat:function(p){var v=e._.stringToNumber(p),y,b;if(v){for(y=f.suffixes.length-1;y>=0;y--){if(e._.includes(p,f.suffixes[y])){b=Math.pow(f.base,y);break}if(e._.includes(p,A.suffixes[y])){b=Math.pow(A.base,y);break}}v*=b||1}return v}})}(),function(){e.register("format","currency",{regexps:{format:/(\$)/},format:function(f,A,h){var g=e.locales[e.options.currentLocale],p={before:A.match(/^([\+|\-|\(|\s|\$]*)/)[0],after:A.match(/([\+|\-|\)|\s|\$]*)$/)[0]},v,y,b;for(A=A.replace(/\s?\$\s?/,""),v=e._.numberToFormat(f,A,h),f>=0?(p.before=p.before.replace(/[\-\(]/,""),p.after=p.after.replace(/[\-\)]/,"")):f<0&&!e._.includes(p.before,"-")&&!e._.includes(p.before,"(")&&(p.before="-"+p.before),b=0;b=0;b--)switch(y=p.after[b],y){case"$":v=b===p.after.length-1?v+g.currency.symbol:e._.insert(v,g.currency.symbol,-(p.after.length-(1+b)));break;case" ":v=b===p.after.length-1?v+" ":e._.insert(v," ",-(p.after.length-(1+b)+g.currency.symbol.length-1));break}return v}})}(),function(){e.register("format","exponential",{regexps:{format:/(e\+|e-)/,unformat:/(e\+|e-)/},format:function(f,A,h){var g,p=typeof f=="number"&&!e._.isNaN(f)?f.toExponential():"0e+0",v=p.split("e");return A=A.replace(/e[\+|\-]{1}0/,""),g=e._.numberToFormat(Number(v[0]),A,h),g+"e"+v[1]},unformat:function(f){var A=e._.includes(f,"e+")?f.split("e+"):f.split("e-"),h=Number(A[0]),g=Number(A[1]);g=e._.includes(f,"e-")?g*=-1:g;function p(v,y,b,k){var w=e._.correctionFactor(v,y),C=v*w*(y*w)/(w*w);return C}return e._.reduce([h,Math.pow(10,g)],p,1)}})}(),function(){e.register("format","ordinal",{regexps:{format:/(o)/},format:function(f,A,h){var g=e.locales[e.options.currentLocale],p,v=e._.includes(A," o")?" ":"";return A=A.replace(/\s?o/,""),v+=g.ordinal(f),p=e._.numberToFormat(f,A,h),p+v}})}(),function(){e.register("format","percentage",{regexps:{format:/(%)/,unformat:/(%)/},format:function(f,A,h){var g=e._.includes(A," %")?" ":"",p;return e.options.scalePercentBy100&&(f=f*100),A=A.replace(/\s?\%/,""),p=e._.numberToFormat(f,A,h),e._.includes(p,")")?(p=p.split(""),p.splice(-1,0,g+"%"),p=p.join("")):p=p+g+"%",p},unformat:function(f){var A=e._.stringToNumber(f);return e.options.scalePercentBy100?A*.01:A}})}(),function(){e.register("format","time",{regexps:{format:/(:)/,unformat:/(:)/},format:function(f,A,h){var g=Math.floor(f/60/60),p=Math.floor((f-g*60*60)/60),v=Math.round(f-g*60*60-p*60);return g+":"+(p<10?"0"+p:p)+":"+(v<10?"0"+v:v)},unformat:function(f){var A=f.split(":"),h=0;return A.length===3?(h=h+Number(A[0])*60*60,h=h+Number(A[1])*60,h=h+Number(A[2])):A.length===2&&(h=h+Number(A[0])*60,h=h+Number(A[1])),Number(h)}})}(),e})});function Qc(e,n){var t=Date.UTC(e.getFullYear(),e.getMonth(),e.getDate(),e.getHours(),e.getMinutes(),e.getSeconds()),l=Date.UTC(1899,11,31,0,0,0);return n?t-=1461*24*60*60*1e3:e>=fy&&(t+=24*60*60*1e3),(t-l)/(24*60*60*1e3)}function hy(e,n){var t=new Date(e);if(Ay)return n>0?t.setTime(t.getTime()+t.getTimezoneOffset()*60*1e3):n<0&&t.setTime(t.getTime()-t.getTimezoneOffset()*60*1e3),t;if(e instanceof Date)return e;if(As.getFullYear()==1917&&!isNaN(t.getFullYear())){var l=t.getFullYear();return e.indexOf(""+l)>-1||t.setFullYear(t.getFullYear()+100),t}var a=e.match(/\d+/g)||["2017","2","19","0","0","0"],o=new Date(+a[0],+a[1]-1,+a[2],+a[3]||0,+a[4]||0,+a[5]||0);return e.indexOf("Z")>-1&&(o=new Date(o.getTime()-o.getTimezoneOffset()*60*1e3)),o}function it(e){var n=[],t=null,l={},a=e;if(e==null)return null;if(/^-?[0-9]{1,}[,][0-9]{3}(.[0-9]{1,2})?$/.test(e)){t=e,a=Number(e.split(".")[0].replace(",",""));let w="#,##0";if(e.split(".")[1]){w="#,##0.";for(let C=0;C0&&(Math.abs(parseFloat(e))>=1e11||Math.abs(parseFloat(e))<1e-9)){a=(0,Zr.default)(e).value();var o=a.toExponential();if(o.indexOf(".")>-1){var s=o.split(".")[1].split("e")[0].length;s>5&&(s=5),l={fa:"#0."+new Array(s+1).join("0")+"E+00",t:"n"}}else l={fa:"#0.E+00",t:"n"};t=xl.format(l.fa,a)}else if(e.toString().indexOf("%")>-1){var u=e.toString().indexOf("%"),f=e.toString().substr(0,u),A=f.replace(/,/g,"");if(u==e.toString().length-1&&V(A))if(f.indexOf(".")>-1)if(f.indexOf(".")==f.lastIndexOf(".")){var h=f.split(".")[0],g=f.split(".")[1],p=g.length;if(p>9&&(p=9),h.indexOf(",")>-1){for(var v=!0,y=h.split(","),b=1;b-1){for(var v=!0,y=f.split(","),b=1;b-1)if(e.toString().indexOf(".")==e.toString().lastIndexOf(".")){var k=e.toString().split(".")[0],f=e.toString().split(".")[1],p=f.length;if(p>9&&(p=9),k.indexOf(",")>-1){for(var v=!0,y=k.split(","),b=1;b-1||e.toString().indexOf(":")>-1||e.toString().length<16)?(a=Qc(hy(e.toString().replace(/-/g,"/"))),a.toString().indexOf(".")>-1?e.toString().length>18?l.fa="yyyy-MM-dd hh:mm:ss":e.toString().length>11?l.fa="yyyy-MM-dd hh:mm":l.fa="yyyy-MM-dd":l.fa="yyyy-MM-dd",l.t="d",t=xl.format(l.fa,a)):(t=e,l.fa="General",l.t="g");return[t,l,a]}function ht(e,n){return xl.format(e,n)}function ah(e,n){return xl.is_date(e,n)}function Bn(e,n,t){var l=Fe(e,n,t,"m");return l==null&&(l=Fe(e,n,t,"v")),l}var Zr,xl,uy,cx,ux,dy,dx,fy,As,Ay,Fl=De(()=>{Pt();Kr();Qt();Zr=Ol(Ma()),xl={},uy=function(n){n.version="0.11.2";function t(K){for(var W="",J=K.length-1;J>=0;)W+=K.charAt(J--);return W}function l(K,W){for(var J="";J.length=W?J:l("0",W-J.length)+J}function o(K,W){var J=""+K;return J.length>=W?J:l(" ",W-J.length)+J}function s(K,W){var J=""+K;return J.length>=W?J:J+l(" ",W-J.length)}function u(K,W){var J=""+Math.round(K);return J.length>=W?J:l("0",W-J.length)+J}function f(K,W){var J=""+K;return J.length>=W?J:l("0",W-J.length)+J}var A=Math.pow(2,32);function h(K,W){if(K>A||K<-A)return u(K,W);var J=Math.round(K);return f(J,W)}function g(K,W){return W=W||0,K.length>=7+W&&(K.charCodeAt(W)|32)==103&&(K.charCodeAt(W+1)|32)==101&&(K.charCodeAt(W+2)|32)==110&&(K.charCodeAt(W+3)|32)==101&&(K.charCodeAt(W+4)|32)==114&&(K.charCodeAt(W+5)|32)==97&&(K.charCodeAt(W+6)|32)==108}var p=[["Sun","Sunday"],["Mon","Monday"],["Tue","Tuesday"],["Wed","Wednesday"],["Thu","Thursday"],["Fri","Friday"],["Sat","Saturday"]],v=[["J","Jan","January"],["F","Feb","February"],["M","Mar","March"],["A","Apr","April"],["M","May","May"],["J","Jun","June"],["J","Jul","July"],["A","Aug","August"],["S","Sep","September"],["O","Oct","October"],["N","Nov","November"],["D","Dec","December"]];function y(K){K[0]="General",K[1]="0",K[2]="0.00",K[3]="#,##0",K[4]="#,##0.00",K[9]="0%",K[10]="0.00%",K[11]="0.00E+00",K[12]="# ?/?",K[13]="# ??/??",K[14]="m/d/yy",K[15]="d-mmm-yy",K[16]="d-mmm",K[17]="mmm-yy",K[18]="h:mm AM/PM",K[19]="h:mm:ss AM/PM",K[20]="h:mm",K[21]="h:mm:ss",K[22]="m/d/yy h:mm",K[37]="#,##0 ;(#,##0)",K[38]="#,##0 ;[Red](#,##0)",K[39]="#,##0.00;(#,##0.00)",K[40]="#,##0.00;[Red](#,##0.00)",K[45]="mm:ss",K[46]="[h]:mm:ss",K[47]="mmss.0",K[48]="##0.0E+0",K[49]="@",K[56]='"\u4E0A\u5348/\u4E0B\u5348 "hh"\u6642"mm"\u5206"ss"\u79D2 "',K[65535]="General"}var b={};y(b);var k=[],w=0;for(w=5;w<=8;++w)k[w]=32+w;for(w=23;w<=26;++w)k[w]=0;for(w=27;w<=31;++w)k[w]=14;for(w=50;w<=58;++w)k[w]=14;for(w=59;w<=62;++w)k[w]=w-58;for(w=67;w<=68;++w)k[w]=w-58;for(w=72;w<=75;++w)k[w]=w-58;for(w=67;w<=68;++w)k[w]=w-57;for(w=76;w<=78;++w)k[w]=w-56;for(w=79;w<=81;++w)k[w]=w-34;var C=[];C[5]=C[63]='"$"#,##0_);\\("$"#,##0\\)',C[6]=C[64]='"$"#,##0_);[Red]\\("$"#,##0\\)',C[7]=C[65]='"$"#,##0.00_);\\("$"#,##0.00\\)',C[8]=C[66]='"$"#,##0.00_);[Red]\\("$"#,##0.00\\)',C[41]='_(* #,##0_);_(* \\(#,##0\\);_(* "-"_);_(@_)',C[42]='_("$"* #,##0_);_("$"* \\(#,##0\\);_("$"* "-"_);_(@_)',C[43]='_(* #,##0.00_);_(* \\(#,##0.00\\);_(* "-"??_);_(@_)',C[44]='_("$"* #,##0.00_);_("$"* \\(#,##0.00\\);_("$"* "-"??_);_(@_)';function I(K,W,J){for(var de=K<0?-1:1,le=K*de,ke=0,ie=1,Ie=0,Oe=1,xe=0,Be=0,We=Math.floor(le);xeW&&(xe>W?(Be=Oe,Ie=ke):(Be=xe,Ie=ie)),!J)return[0,de*Ie,Be];var ze=Math.floor(de*Ie/Be);return[ze,de*Ie-ze*Be,Be]}function x(K,W,J){if(K>2958465||K<0)return null;var de=K|0,le=Math.floor(86400*(K-de)),ke=0,ie=[],Ie={D:de,T:le,u:86400*(K-de)-le,y:0,m:0,d:0,H:0,M:0,S:0,q:0};if(Math.abs(Ie.u)<1e-6&&(Ie.u=0),W&&W.date1904&&(de+=1462),Ie.u>.9999&&(Ie.u=0,++le==86400&&(Ie.T=le=0,++de,++Ie.D)),de===60)ie=J?[1317,10,29]:[1900,2,29],ke=3;else if(de===0)ie=J?[1317,8,29]:[1900,1,0],ke=6;else{de>60&&--de;var Oe=new Date(1900,0,1);Oe.setDate(Oe.getDate()+de-1),ie=[Oe.getFullYear(),Oe.getMonth()+1,Oe.getDate()],ke=Oe.getDay(),de<60&&(ke=(ke+6)%7),J&&(ke=z(Oe,ie))}return Ie.y=ie[0],Ie.m=ie[1],Ie.d=ie[2],Ie.S=le%60,le=Math.floor(le/60),Ie.M=le%60,le=Math.floor(le/60),Ie.H=le,Ie.q=ke,Ie}n.parse_date_code=x;var T=new Date(1899,11,31,0,0,0),R=T.getTime(),D=new Date(1900,2,1,0,0,0);function S(K,W){var J=K.getTime();return W?J-=1461*24*60*60*1e3:K>=D&&(J+=24*60*60*1e3),(J-(R+(K.getTimezoneOffset()-T.getTimezoneOffset())*6e4))/(24*60*60*1e3)}function B(K){return K.toString(10)}n._general_int=B;var F=function(){var W=/(?:\.0*|(\.\d*[1-9])0+)$/;function J(xe){return xe.indexOf(".")==-1?xe:xe.replace(W,"$1")}var de=/(?:\.0*|(\.\d*[1-9])0+)[Ee]/,le=/(E[+-])(\d)$/;function ke(xe){return xe.indexOf("E")==-1?xe:xe.replace(de,"$1E").replace(le,"$10$2")}function ie(xe){var Be=xe<0?12:11,We=J(xe.toFixed(12));return We.length<=Be||(We=xe.toPrecision(10),We.length<=Be)?We:xe.toExponential(5)}function Ie(xe){var Be=J(xe.toFixed(11));return Be.length>(xe<0?12:11)||Be==="0"||Be==="-0"?xe.toPrecision(6):Be}function Oe(xe){var Be=Math.floor(Math.log(Math.abs(xe))*Math.LOG10E),We;return Be>=-4&&Be<=-1?We=xe.toPrecision(10+Be):Math.abs(Be)<=9?We=ie(xe):Be===10?We=xe.toFixed(10).substr(0,12):We=Ie(xe),J(ke(We.toUpperCase()))}return Oe}();n._general_num=F;function E(K,W){switch(typeof K){case"string":return K;case"boolean":return K?"TRUE":"FALSE";case"number":return(K|0)===K?K.toString(10):F(K);case"undefined":return"";case"object":if(K==null)return"";if(K instanceof Date)return fe(14,S(K,W&&W.date1904),W)}throw new Error("unsupported value in General format: "+K)}n._general=E;function z(K,W){W[0]-=581;var J=K.getDay();return K<60&&(J=(J+6)%7),J}var P="\u0E50\u0E51\u0E52\u0E53\u0E54\u0E55\u0E56\u0E57\u0E58\u0E59".split("");function L(K,W,J,de){var le="",ke=0,ie=0,Ie=J.y,Oe,xe=0;switch(K){case 98:Ie=J.y+543;case 121:switch(W.length){case 1:case 2:Oe=Ie%100,xe=2;break;default:Oe=Ie%1e4,xe=4;break}break;case 109:switch(W.length){case 1:case 2:Oe=J.m,xe=W.length;break;case 3:return v[J.m-1][1];case 5:return v[J.m-1][0];default:return v[J.m-1][2]}break;case 100:switch(W.length){case 1:case 2:Oe=J.d,xe=W.length;break;case 3:return p[J.q][0];default:return p[J.q][1]}break;case 104:switch(W.length){case 1:case 2:Oe=1+(J.H+11)%12,xe=W.length;break;default:throw"bad hour format: "+W}break;case 72:switch(W.length){case 1:case 2:Oe=J.H,xe=W.length;break;default:throw"bad hour format: "+W}break;case 77:switch(W.length){case 1:case 2:Oe=J.M,xe=W.length;break;default:throw"bad minute format: "+W}break;case 115:if(W!="s"&&W!="ss"&&W!=".0"&&W!=".00"&&W!=".000")throw"bad second format: "+W;return J.u===0&&(W=="s"||W=="ss")?a(J.S,W.length):(de>=2?ie=de===3?1e3:100:ie=de===1?10:1,ke=Math.round(ie*(J.S+J.u)),ke>=60*ie&&(ke=0),W==="s"?ke===0?"0":""+ke/ie:(le=a(ke,2+de),W==="ss"?le.substr(0,2):"."+le.substr(2,W.length-1)));case 90:switch(W){case"[h]":case"[hh]":Oe=J.D*24+J.H;break;case"[m]":case"[mm]":Oe=(J.D*24+J.H)*60+J.M;break;case"[s]":case"[ss]":Oe=((J.D*24+J.H)*60+J.M)*60+Math.round(J.S+J.u);break;default:throw"bad abstime format: "+W}xe=W.length===3?1:2;break;case 101:Oe=Ie,xe=1;break}var Be=xe>0?a(Oe,xe):"";return Be}function O(K){var W=3;if(K.length<=W)return K;for(var J=K.length%W,de=K.substr(0,J);J!=K.length;J+=W)de+=(de.length>0?",":"")+K.substr(J,W);return de}var q=function(){var W=/%/g;function J(Le,pe,ye){var ft=pe.replace(W,""),Pe=pe.length-ft.length;return q(Le,ft,ye*Math.pow(10,2*Pe))+l("%",Pe)}function de(Le,pe,ye){for(var ft=pe.length-1;pe.charCodeAt(ft-1)===44;)--ft;return q(Le,pe.substr(0,ft),ye/Math.pow(10,3*(pe.length-ft)))}function le(Le,pe){var ye,ft=Le.indexOf("E")-Le.indexOf(".")-1;if(Le.match(/^#+0.0E\+0$/)){if(pe==0)return"0.0E+0";if(pe<0)return"-"+le(Le,-pe);var Pe=Le.indexOf(".");Pe===-1&&(Pe=Le.indexOf("E"));var Ve=Math.floor(Math.log(pe)*Math.LOG10E)%Pe;if(Ve<0&&(Ve+=Pe),ye=(pe/Math.pow(10,Ve)).toPrecision(ft+1+(Pe+Ve)%Pe),ye.indexOf("e")===-1){var Ot=Math.floor(Math.log(pe)*Math.LOG10E);for(ye.indexOf(".")===-1?ye=ye.charAt(0)+"."+ye.substr(1)+"E+"+(Ot-ye.length+Ve):ye+="E+"+(Ot-Ve);ye.substr(0,2)==="0.";)ye=ye.charAt(0)+ye.substr(2,Pe)+"."+ye.substr(2+Pe),ye=ye.replace(/^0+([1-9])/,"$1").replace(/^0+\./,"0.");ye=ye.replace(/\+-/,"-")}ye=ye.replace(/^([+-]?)(\d*)\.(\d*)[Ee]/,function(pt,ul,dl,Ut){return ul+dl+Ut.substr(0,(Pe+Ve)%Pe)+"."+Ut.substr(Ve)+"E"})}else ye=pe.toExponential(ft);return Le.match(/E\+00$/)&&ye.match(/e[+-]\d$/)&&(ye=ye.substr(0,ye.length-1)+"0"+ye.charAt(ye.length-1)),Le.match(/E\-/)&&ye.match(/e\+/)&&(ye=ye.replace(/e\+/,"e")),ye.replace("e","E")}var ke=/# (\?+)( ?)\/( ?)(\d+)/;function ie(Le,pe,ye){var ft=parseInt(Le[4],10),Pe=Math.round(pe*ft),Ve=Math.floor(Pe/ft),Ot=Pe-Ve*ft,pt=ft;return ye+(Ve===0?"":""+Ve)+" "+(Ot===0?l(" ",Le[1].length+1+Le[4].length):o(Ot,Le[1].length)+Le[2]+"/"+Le[3]+a(pt,Le[4].length))}function Ie(Le,pe,ye){return ye+(pe===0?"":""+pe)+l(" ",Le[1].length+2+Le[4].length)}var Oe=/^#*0*\.([0#]+)/,xe=/\).*[0#]/,Be=/\(###\) ###\\?-####/;function We(Le){for(var pe="",ye,ft=0;ft!=Le.length;++ft)switch(ye=Le.charCodeAt(ft)){case 35:break;case 63:pe+=" ";break;case 48:pe+="0";break;default:pe+=String.fromCharCode(ye)}return pe}function ze(Le,pe){var ye=Math.pow(10,pe);return""+Math.round(Le*ye)/ye}function lt(Le,pe){var ye=Le-Math.floor(Le),ft=Math.pow(10,pe);return pe<(""+Math.round(ye*ft)).length?0:Math.round(ye*ft)}function bt(Le,pe){return pe<(""+Math.round((Le-Math.floor(Le))*Math.pow(10,pe))).length?1:0}function zt(Le){return Le<2147483647&&Le>-2147483648?""+(Le>=0?Le|0:Le-1|0):""+Math.floor(Le)}function st(Le,pe,ye){if(Le.charCodeAt(0)===40&&!pe.match(xe)){var ft=pe.replace(/\( */,"").replace(/ \)/,"").replace(/\)/,"");return ye>=0?st("n",ft,ye):"("+st("n",ft,-ye)+")"}if(pe.charCodeAt(pe.length-1)===44)return de(Le,pe,ye);if(pe.indexOf("%")!==-1)return J(Le,pe,ye);if(pe.indexOf("E")!==-1)return le(pe,ye);if(pe.charCodeAt(0)===36)return"$"+st(Le,pe.substr(pe.charAt(1)==" "?2:1),ye);var Pe,Ve,Ot,pt,ul=Math.abs(ye),dl=ye<0?"-":"";if(pe.match(/^00+$/))return dl+h(ul,pe.length);if(pe.match(/^[#?]+$/))return Pe=h(ye,0),Pe==="0"&&(Pe=""),Pe.length>pe.length?Pe:We(pe.substr(0,pe.length-Pe.length))+Pe;if(Ve=pe.match(ke))return ie(Ve,ul,dl);if(pe.match(/^#+0+$/))return dl+h(ul,pe.length-pe.indexOf("0"));if(Ve=pe.match(Oe))return Pe=ze(ye,Ve[1].length).replace(/^([^\.]+)$/,"$1."+We(Ve[1])).replace(/\.$/,"."+We(Ve[1])).replace(/\.(\d*)$/,function($l,va){return"."+va+l("0",We(Ve[1]).length-va.length)}),pe.indexOf("0.")!==-1?Pe:Pe.replace(/^0\./,".");if(pe=pe.replace(/^#+([0.])/,"$1"),Ve=pe.match(/^(0*)\.(#*)$/))return dl+ze(ul,Ve[2].length).replace(/\.(\d*[1-9])0*$/,".$1").replace(/^(-?\d*)$/,"$1.").replace(/^0\./,Ve[1].length?"0.":".");if(Ve=pe.match(/^#{1,3},##0(\.?)$/))return dl+O(h(ul,0));if(Ve=pe.match(/^#,##0\.([#0]*0)$/))return ye<0?"-"+st(Le,pe,-ye):O(""+(Math.floor(ye)+bt(ye,Ve[1].length)))+"."+a(lt(ye,Ve[1].length),Ve[1].length);if(Ve=pe.match(/^#,#*,#0/))return st(Le,pe.replace(/^#,#*,/,""),ye);if(Ve=pe.match(/^([0#]+)(\\?-([0#]+))+$/))return Pe=t(st(Le,pe.replace(/[\\-]/g,""),ye)),Ot=0,t(t(pe.replace(/\\/g,"")).replace(/[0#]/g,function($l){return Ot=0?Ze("n",ft,ye):"("+Ze("n",ft,-ye)+")"}if(pe.charCodeAt(pe.length-1)===44)return kt(Le,pe,ye);if(pe.indexOf("%")!==-1)return Ye(Le,pe,ye);if(pe.indexOf("E")!==-1)return Me(pe,ye);if(pe.charCodeAt(0)===36)return"$"+Ze(Le,pe.substr(pe.charAt(1)==" "?2:1),ye);var Pe,Ve,Ot,pt,ul=Math.abs(ye),dl=ye<0?"-":"";if(pe.match(/^00+$/))return dl+a(ul,pe.length);if(pe.match(/^[#?]+$/))return Pe=""+ye,ye===0&&(Pe=""),Pe.length>pe.length?Pe:We(pe.substr(0,pe.length-Pe.length))+Pe;if(Ve=pe.match(ke))return Ie(Ve,ul,dl);if(pe.match(/^#+0+$/))return dl+a(ul,pe.length-pe.indexOf("0"));if(Ve=pe.match(Oe))return Pe=(""+ye).replace(/^([^\.]+)$/,"$1."+We(Ve[1])).replace(/\.$/,"."+We(Ve[1])),Pe=Pe.replace(/\.(\d*)$/,function($l,va){return"."+va+l("0",We(Ve[1]).length-va.length)}),pe.indexOf("0.")!==-1?Pe:Pe.replace(/^0\./,".");if(pe=pe.replace(/^#+([0.])/,"$1"),Ve=pe.match(/^(0*)\.(#*)$/))return dl+(""+ul).replace(/\.(\d*[1-9])0*$/,".$1").replace(/^(-?\d*)$/,"$1.").replace(/^0\./,Ve[1].length?"0.":".");if(Ve=pe.match(/^#{1,3},##0(\.?)$/))return dl+O(""+ul);if(Ve=pe.match(/^#,##0\.([#0]*0)$/))return ye<0?"-"+Ze(Le,pe,-ye):O(""+ye)+"."+l("0",Ve[1].length);if(Ve=pe.match(/^#,#*,#0/))return Ze(Le,pe.replace(/^#,#*,/,""),ye);if(Ve=pe.match(/^([0#]+)(\\?-([0#]+))+$/))return Pe=t(Ze(Le,pe.replace(/[\\-]/g,""),ye)),Ot=0,t(t(pe.replace(/\\/g,"")).replace(/[0#]/g,function($l){return Ot-1||J=="\\"&&K.charAt(W+1)=="-"&&"0#".indexOf(K.charAt(W+2))>-1););break;case"?":for(;K.charAt(++W)===J;);break;case"*":++W,(K.charAt(W)==" "||K.charAt(W)=="*")&&++W;break;case"(":case")":++W;break;case"1":case"2":case"3":case"4":case"5":case"6":case"7":case"8":case"9":for(;W-1;);break;case" ":++W;break;default:++W;break}return!1}n.is_date=_;function ee(K,W,J,de){for(var le=[],ke="",ie=0,Ie="",Oe="t",xe,Be,We,ze="H";ie=12?"P":"A"),zt.t="T",ze="h",ie+=3):K.substr(ie,5).toUpperCase()==="AM/PM"?(xe!=null&&(zt.v=xe.H>=12?"PM":"AM"),zt.t="T",ie+=5,ze="h"):K.substr(ie,5).toUpperCase()==="\u4E0A\u5348/\u4E0B\u5348"?(xe!=null&&(zt.v=xe.H>=12?"\u4E0B\u5348":"\u4E0A\u5348"),zt.t="T",ie+=5,ze="h"):(zt.t="t",++ie),xe==null&&zt.t==="T")return"";le[le.length]=zt,Oe=Ie;break;case"[":for(ke=Ie;K.charAt(ie++)!=="]"&&ie-1&&(ke=(ke.match(/\$([^-\[\]]*)/)||[])[1]||"$",_(K)||(le[le.length]={t:"t",v:ke}));break;case".":if(xe!=null){for(ke=Ie;++ie-1;)ke+=Ie;le[le.length]={t:"n",v:ke};break;case"?":for(ke=Ie;K.charAt(++ie)===Ie;)ke+=Ie;le[le.length]={t:Ie,v:ke},Oe=Ie;break;case"*":++ie,(K.charAt(ie)==" "||K.charAt(ie)=="*")&&++ie;break;case"(":case")":le[le.length]={t:de===1?"t":Ie,v:Ie},++ie;break;case"1":case"2":case"3":case"4":case"5":case"6":case"7":case"8":case"9":for(ke=Ie;ie-1;)ke+=K.charAt(ie);le[le.length]={t:"D",v:ke};break;case" ":le[le.length]={t:Ie,v:Ie},++ie;break;case"$":le[le.length]={t:"t",v:"$"},++ie;break;default:if("\xA4\u0E3FBsBr\u20B5\u20A1\u20AB\u0192FtRs.\u20ADkr\xA3\u20A4Lm\u20A5\u20A6\u20B1PQRSkRp\u09F2\u09F3R$S/.\u3012\u20AE\u20A9\xA5NT\uFFE5z\u0142\u20B4\u20AA\u17DB\u0440\u0443\u0431\u20AC\uFF04,$-+/():!^&'~{}<>=\u20ACacfijklopqrtuvwxzP$\uFFE5LekdinAf$dhAflRial?\uFFE1BirrKzMOPPGKRsGsB/R$\u0440\u0440levkrKMzBsPNuFBuKPkrRD$NfkCFA?CVEGMDFrCDHTGNAfLFdjKGSFGGHSRielKCFknKshLSLL\uFFE1LtRFRONArRfMWKRMMURsMROS/KMDLMTnRC$kr\u20ACGELCHFSLLSCRDbSZLSDGSOSSomFCFPTShT$VUVQUGX\u0433\u0440\u043DsomWSTNT$FtDramRpZMWFCFA".indexOf(Ie)===-1)throw new Error("unrecognized character "+Ie+" in "+K);le[le.length]={t:"t",v:Ie},++ie;break}var st=0,kt=0,Ye;for(ie=le.length-1,Oe="t";ie>=0;--ie)switch(le[ie].t){case"h":case"H":le[ie].t=ze,Oe="h",st<1&&(st=1);break;case"s":(Ye=le[ie].v.match(/\.0+$/))&&(kt=Math.max(kt,Ye[0].length-1)),st<3&&(st=3);case"d":case"y":case"M":case"e":Oe=le[ie].t;break;case"m":Oe==="s"&&(le[ie].t="M",st<2&&(st=2));break;case"X":break;case"Z":st<1&&le[ie].v.match(/[Hh]/)&&(st=1),st<2&&le[ie].v.match(/[Mm]/)&&(st=2),st<3&&le[ie].v.match(/[Ss]/)&&(st=3)}switch(st){case 0:break;case 1:xe.u>=.5&&(xe.u=0,++xe.S),xe.S>=60&&(xe.S=0,++xe.M),xe.M>=60&&(xe.M=0,++xe.H);break;case 2:xe.u>=.5&&(xe.u=0,++xe.S),xe.S>=60&&(xe.S=0,++xe.M);break}var Me="",Ze;for(ie=0;ie0){Me.charCodeAt(0)==40?(pe=W<0&&Me.charCodeAt(0)===45?-W:W,ye=q("n",Me,pe)):(pe=W<0&&de>1?-W:W,ye=q("n",Me,pe),pe<0&&le[0]&&le[0].t=="t"&&(ye=ye.substr(1),le[0].v="-"+le[0].v)),Ze=ye.length-1;var ft=le.length;for(ie=0;ie-1){ft=ie;break}var Pe=le.length;if(ft===le.length&&ye.indexOf("E")===-1){for(ie=le.length-1;ie>=0;--ie)le[ie]==null||"n?".indexOf(le[ie].t)===-1||(Ze>=le[ie].v.length-1?(Ze-=le[ie].v.length,le[ie].v=ye.substr(Ze+1,le[ie].v.length)):Ze<0?le[ie].v="":(le[ie].v=ye.substr(0,Ze+1),Ze=-1),le[ie].t="t",Pe=ie);Ze>=0&&Pe=0;--ie)if(!(le[ie]==null||"n?".indexOf(le[ie].t)===-1)){for(Be=le[ie].v.indexOf(".")>-1&&ie===ft?le[ie].v.indexOf(".")-1:le[ie].v.length-1,Le=le[ie].v.substr(Be+1);Be>=0;--Be)Ze>=0&&(le[ie].v.charAt(Be)==="0"||le[ie].v.charAt(Be)==="#")&&(Le=ye.charAt(Ze--)+Le);le[ie].v=Le,le[ie].t="t",Pe=ie}for(Ze>=0&&Pe-1&&ie===ft?le[ie].v.indexOf(".")+1:0,Le=le[ie].v.substr(0,Be);Be-1&&(pe=de>1&&W<0&&ie>0&&le[ie-1].v==="-"?-W:W,le[ie].v=q(le[ie].t,le[ie].v,pe),le[ie].t="t");var Ve="";for(ie=0;ie!==le.length;++ie)le[ie]!=null&&(Ve+=le[ie].v);return Ve}n._eval=ee;var re=/\[[=<>]/,oe=/\[(=|>[=]?|<[>=]?)(-?\d+(?:\.\d*)?)\]/;function se(K,W){if(W==null)return!1;var J=parseFloat(W[2]);switch(W[1]){case"=":if(K==J)return!0;break;case">":if(K>J)return!0;break;case"<":if(K":if(K!=J)return!0;break;case">=":if(K>=J)return!0;break;case"<=":if(K<=J)return!0;break}return!1}function ue(K,W){var J=j(K),de=J.length,le=J[de-1].indexOf("@");if(de<4&&le>-1&&--de,J.length>4)throw new Error("cannot find right format for |"+J.join("|")+"|");if(typeof W!="number")return[4,J.length===4||le>-1?J[J.length-1]:"@"];switch(J.length){case 1:J=le>-1?["General","General","General",J[0]]:[J[0],J[0],J[0],"@"];break;case 2:J=le>-1?[J[0],J[0],J[0],J[1]]:[J[0],J[1],J[0],"@"];break;case 3:J=le>-1?[J[0],J[1],J[0],J[2]]:[J[0],J[1],J[2],"@"];break;case 4:break}var ke=W>0?J[0]:W<0?J[1]:J[2];if(J[0].indexOf("[")===-1&&J[1].indexOf("[")===-1)return[de,ke];if(J[0].match(re)!=null||J[1].match(re)!=null){var ie=J[0].match(oe),Ie=J[1].match(oe);return se(W,ie)?[de,J[0]]:se(W,Ie)?[de,J[1]]:[de,J[ie!=null&&Ie!=null?2:1]]}return[de,ke]}function fe(K,W,J){J==null&&(J={});var de="";switch(typeof K){case"string":K=="m/d/yy"&&J.dateNF?de=J.dateNF:de=K;break;case"number":K==14&&J.dateNF?de=J.dateNF:de=(J.table!=null?J.table:b)[K],de==null&&(de=J.table&&J.table[k[K]]||b[k[K]]),de==null&&(de=C[K]||"General");break}var le=/^(w|W)((0?)|(0\.0+))$/;if(de.match(le)){if(isNaN(W))return W;var ke=de.slice(1),ie=!1;!isNaN(W)&&Number(W)<0&&(ie=!0,W=Math.abs(W));var Ie=parseInt(W),Oe=Ie.toString().length;if(Oe>4){if(Oe>8){var xe=parseInt(W/1e8),Be=parseInt(parseFloat(W).subtract(xe*1e8)/1e4),We=parseFloat(W).subtract(xe*1e8+Be*1e4);ke!=""&&(We=(0,Zr.default)(We).format(ke)),W=xe+"\u4EBF"+Be+"\u4E07"+We}else{var Be=parseInt(W/1e4),We=parseFloat(W).subtract(Be*1e4);ke!=""&&(We=(0,Zr.default)(We).format(ke)),W=Be+"\u4E07"+We}if(W.indexOf("\u4EBF0\u4E070")!=-1?W=W.replace("0\u4E070",""):W.indexOf("\u4EBF0\u4E07")!=-1?W=W.replace("0\u4E07",""):W.indexOf("\u4E070")!=-1&&(W=W.replace("\u4E070","\u4E07")),W.indexOf("\u4EBF")!=-1&&W.indexOf("\u4E07")==-1){var ze=W.substring(W.indexOf("\u4EBF")+1);if(ze.substring(0,1)!=="."&&ze!=""){switch((parseInt(ze)+"").length){case 1:ze="000"+ze;break;case 2:ze="00"+ze;break;case 3:ze="0"+ze;break}W=W.substring(0,W.indexOf("\u4EBF")+1)+ze}}else if(W.indexOf("\u4EBF")==-1&&W.indexOf("\u4E07")!=-1){var lt=W.substring(W.indexOf("\u4E07")+1);if(lt.substring(0,1)!=="."&<!=""){switch((parseInt(lt)+"").length){case 1:lt="000"+lt;break;case 2:lt="00"+lt;break;case 3:lt="0"+lt;break}W=W.substring(0,W.indexOf("\u4E07")+1)+lt}}else if(W.indexOf("\u4EBF")!=-1&&W.indexOf("\u4E07")!=-1){var ze=W.substring(W.indexOf("\u4EBF")+1,W.indexOf("\u4E07")),lt=W.substring(W.indexOf("\u4E07")+1);switch((parseInt(ze)+"").length){case 1:ze="000"+ze;break;case 2:ze="00"+ze;break;case 3:ze="0"+ze;break}if(W=W.substring(0,W.indexOf("\u4EBF")+1)+ze+W.substring(W.indexOf("\u4E07")),lt.substring(0,1)!=="."&<!=""){switch((parseInt(lt)+"").length){case 1:lt="000"+lt;break;case 2:lt="00"+lt;break;case 3:lt="0"+lt;break}W=W.substring(0,W.indexOf("\u4E07")+1)+lt}}}else ke!=""&&(W=(0,Zr.default)(W).format(ke));return ie?"-"+W:W}if(g(de,0))return E(W,J);W instanceof Date&&(W=S(W,J.date1904));var bt=ue(de,W);if(g(bt[1]))return E(W,J);if(W===!0)W="TRUE";else if(W===!1)W="FALSE";else if(W===""||W==null)return"";return ee(bt[1],W,J,bt[0])}function ce(K,W){if(typeof W!="number"){W=+W||-1;for(var J=0;J<392;++J){if(b[J]==null){W<0&&(W=J);continue}if(b[J]==K){W=J;break}}W<0&&(W=391)}return b[W]=K,W}n.load=ce,n._table=b,n.get_table=function(){return b},n.load_table=function(W){for(var J=0;J!=392;++J)W[J]!==void 0&&ce(W[J],J)},n.init_table=y,n.format=fe};uy(xl);cx={"General Number":"General","General Date":xl._table[22],"Long Date":"dddd, mmmm dd, yyyy","Medium Date":xl._table[15],"Short Date":xl._table[14],"Long Time":xl._table[19],"Medium Time":xl._table[18],"Short Time":xl._table[20],Currency:'"$"#,##0.00_);[Red]\\("$"#,##0.00\\)',Fixed:xl._table[2],Standard:xl._table[4],Percent:xl._table[10],Scientific:xl._table[11],"Yes/No":'"Yes";"Yes";"No";@',"True/False":'"True";"True";"False";@',"On/Off":'"Yes";"Yes";"No";@'},ux=function(){var e=/&(?:quot|apos|gt|lt|amp|#x?([\da-fA-F]+));/g,n=/_x([\da-fA-F]{4})_/g;return function t(l){var a=l+"",o=a.indexOf("-1?16:10))||u}).replace(n,function(u,f){return String.fromCharCode(parseInt(f,16))});var s=a.indexOf("]]>");return t(a.slice(0,o))+a.slice(o+9,s)+t(a.slice(s+3))}}(),dy=new Date(1899,11,31,0,0,0),dx=dy.getTime(),fy=new Date(1900,2,1,0,0,0);As=new Date("2017-02-19T19:06:09.000Z");isNaN(As.getFullYear())&&(As=new Date("2/19/17"));Ay=As.getFullYear()==2017});function Xt(e,n,t,l){t==null&&(t=d.flowdata);let a=t[e][n],o;if(N(l)=="object"?(a==null?a=l:(l.f!=null?a.f=l.f:a.hasOwnProperty("f")&&delete a.f,l.spl!=null&&(a.spl=l.spl),l.ct!=null&&(a.ct=l.ct)),N(l.v)=="object"?o=l.v.v:o=l.v):o=l,me(o)){N(a)=="object"?(delete a.m,delete a.v):a=null,t[e][n]=a;return}(me(a)||(N(a)==="string"||N(a)==="number")&&a===l)&&(a={});let s=o.toString();if(s.substr(0,1)=="'")a.m=s.substr(1),a.ct={fa:"@",t:"s"},a.v=s.substr(1),a.qp=1;else if(a.qp==1)a.m=s,a.ct={fa:"@",t:"s"},a.v=s;else if(s.toUpperCase()==="TRUE")a.m="TRUE",a.ct={fa:"General",t:"b"},a.v=!0;else if(s.toUpperCase()==="FALSE")a.m="FALSE",a.ct={fa:"General",t:"b"},a.v=!1;else if(s.substr(-1)==="%"&&V(s.substring(0,s.length-1)))a.ct={fa:"0%",t:"n"},a.v=s.substring(0,s.length-1)/100,a.m=o;else if(H(o))a.m=s,a.ct!=null?a.ct.t="e":a.ct={fa:"General",t:"e"},a.v=o;else if(a.f!=null&&V(o)&&!/^\d{6}(18|19|20)?\d{2}(0[1-9]|1[12])(0[1-9]|[12]\d|3[01])\d{3}(\d|X)$/i.test(o))if(a.v=parseFloat(o),a.ct==null&&(a.ct={fa:"General",t:"n"}),a.v==Infinity||a.v==-Infinity)a.m=a.v.toString();else if(a.v.toString().indexOf("e")>-1){let u;a.v.toString().split(".").length==1?u=0:u=a.v.toString().split(".")[1].split("e")[0].length,u>5&&(u=5),a.m=a.v.toExponential(u).toString()}else{let u=Math.round(a.v*1e9)/1e9;if(a.ct==null){let f=it(u);a.m=f[0].toString()}else{let f=ht(a.ct.fa,u);a.m=f.toString()}}else if(a.ct!=null&&a.ct.fa=="@")a.m=s,a.v=o;else if(a.ct!=null&&a.ct.fa!=null&&a.ct.fa!="General"){V(o)&&(o=parseFloat(o));let u=ht(a.ct.fa,o);u===o?(u=it(o),a.m=u[0].toString(),a.ct=u[1],a.v=u[2]):(a.m=u.toString(),a.v=o)}else if(V(o)&&!/^\d{6}(18|19|20)?\d{2}(0[1-9]|1[12])(0[1-9]|[12]\d|3[01])\d{3}(\d|X)$/i.test(o))if(typeof o=="string"&&o.split("").every(f=>f=="0"||f==".")&&(o=parseFloat(o)),a.v=o,a.ct={fa:"General",t:"n"},a.v==Infinity||a.v==-Infinity)a.m=a.v.toString();else{let u=it(a.v);a.m=u[0].toString()}else{let u=it(o);a.m=u[0].toString(),a.ct=u[1],a.v=u[2]}if(!ae.allowUpdate&&!Ae.pointEdit&&a.ct!=null&&/^(w|W)((0?)|(0\.0+))$/.test(a.ct.fa)==!1&&a.ct.t=="n"&&a.v!=null&&parseInt(a.v).toString().length>4){let u=Ae.autoFormatw.toString().toUpperCase(),f=Ae.accuracy,A=my(u,f);A!="General"&&(a.ct.fa=A,a.m=ht(A,a.v))}t[e][n]=a}function my(e,n){let t="0.",l;if(e=="TRUE"){if(n==null)return"w";{let a=parseInt(n);if(a==0)return"w0";t="w0.";for(let o=0;o{dt();Pt();Fl();$t();yl();je()});function Cl(){if(d.luckysheet_select_save.length==0)return;let e=Infinity,n=-Infinity,t=0,l=0,a=0;for(let u=0;un&&(n=g))}}let o=te().formula,s="";s+=""+o.count+":"+l+"",(isFinite(n)||isFinite(e))&&(s+=""+o.sum+":"+ht("w",t)+"",s+=""+o.average+":"+ht("w",Math.round(t/l*1e4)/1e4)+""),isFinite(n)&&(s+=""+o.max+":"+ht("w",n)+""),isFinite(e)&&(s+=""+o.min+":"+ht("w",e)+""),$("#luckysheet-sta-content").html(s)}var ci=De(()=>{je();Qt();Pt();Fl();yt()});function Pn(e,n,t,l,a){t==null&&(t=!0),e||(e="down"),n||(n="cell"),a==null&&(a=!1);let o=d.luckysheet_select_save[d.luckysheet_select_save.length-1],s=o.row==null?0:o.row[0],u=o.column==null?0:o.column[0],f=o.row==null?0:o.row[0],A=o.column==null?0:o.column[0],h=o.row==null?0:o.row[1],g=o.column==null?0:o.column[1];if(m.fucntionboxshow(s,u),n=="range"){var p=d.luckysheet_shiftpositon.row[0],v=d.luckysheet_shiftpositon.column[0];let I=d.luckysheet_shiftpositon.row[1],x=d.luckysheet_shiftpositon.column[1];e=="down"||e=="up"?If?s=o.row==null?0:o.row[0]:I==h&&p==f&&(e=="down"?s=o.row==null?0:o.row[1]:s=o.row==null?0:o.row[0]):(e=="right"||e=="left")&&(xA?u=o.column==null?0:o.column[0]:x==g&&v==A&&(e=="right"?u=o.column==null?0:o.column[1]:u=o.column==null?0:o.column[0]))}let y=d.flowdata.length,b=d.flowdata[0].length,k=d.flowdata,w="",C=0;if(e=="up"){if(s==0)return;{let I=[],x=null,T=0,R=null;for(let D=A;D<=g;D++){I=[],T=0;for(let S=s-1;S>=0;S--){let B=k[S][D];if(N(B)=="object"&&me(B.v)||me(B)?I.push(!1):I.push(!0),I.length>1){if(I[T]==!0&&I[T-1]==!1){x=S;break}else if(I[T]==!1&&I[T-1]==!0){x=S+1;break}}T++}x==null&&(x=0),(R==null||x1){if(I[T]==!0&&I[T-1]==!1){x=S;break}else if(I[T]==!1&&I[T-1]==!0){x=S-1;break}}T++}x==null&&(x=k.length-1),(R==null||x>R)&&(R=x)}w="down",C=R-s}}else if(e=="left"){if(u==0)return;{let I=[],x=null,T=0,R=null;for(let D=f;D<=h;D++){I=[],T=0;for(let S=u-1;S>=0;S--){let B=k[D][S];if(N(B)=="object"&&me(B.v)||me(B)?I.push(!1):I.push(!0),I.length>1){if(I[T]==!0&&I[T-1]==!1){x=S;break}else if(I[T]==!1&&I[T-1]==!0){x=S+1;break}}T++}x==null&&(x=0),(R==null||x1){if(I[T]==!0&&I[T-1]==!1){x=S;break}else if(I[T]==!1&&I[T-1]==!0){x=S-1;break}}T++}x==null&&(x=k[0].length-1),(R==null||x>R)&&(R=x)}w="right",C=R-u}}if(n=="range"&&(e=="up"?p_endRf&&C+s>p&&(C=p-s):e=="left"?p_endCA&&C+u>v&&(C=v-u),l!=null&&Math.abs(C)>Math.abs(l)&&(C=l)),!a)n=="cell"?Yt(w,C,"rangeOfSelect",t):n=="range"&&ia(w,C,"rangeOfSelect",t);else return C}function Yt(e,n,t,l){l==null&&(l=!0),e||e=="down";let a=d.flowdata.length,o=d.flowdata[0].length,s,u,f,A,h,g,p,v;if(t=="rangeOfSelect"){let x=d.luckysheet_select_save[d.luckysheet_select_save.length-1],T;x.row_focus==null?T=x.row[0]:T=x.row_focus;let R;x.column_focus==null?R=x.column[0]:R=x.column_focus;let D=be.mergeborer(d.flowdata,T,R);if(D){let E=D.row[2],z=D.row[3],P=D.column[2],L=D.column[3];n>0?e=="down"?(T=z,R=P):e=="right"&&(T=E,R=L):(T=E,R=P)}let S=x.moveXY==null?T:x.moveXY.x,B=x.moveXY==null?R:x.moveXY.y;e=="down"?(T+=n,S=T):e=="right"&&(R+=n,B=R),T>=a&&(T=a-1,S=T),T<0&&(T=0,S=T),R>=o&&(R=o-1,B=R),R<0&&(R=0,B=R);let F=be.mergeborer(d.flowdata,T,R);F?(s=F.row[1],u=F.row[0],f=F.row[2],A=F.row[3],h=F.column[1],g=F.column[0],p=F.column[2],v=F.column[3]):(s=d.visibledatarow[S],u=S-1==-1?0:d.visibledatarow[S-1],h=d.visibledatacolumn[B],g=B-1==-1?0:d.visibledatacolumn[B-1],f=A=T,p=v=R),x.row=[f,A],x.column=[p,v],x.row_focus=f,x.column_focus=p,x.moveXY={x:S,y:B},at(),Xe.pivotclick(f,p),m.fucntionboxshow(f,p)}else if(t=="rangeOfFormula"){let x=m.func_selectedrange,T;x.row_focus==null?T=x.row[0]:T=x.row_focus;let R;x.column_focus==null?R=x.column[0]:R=x.column_focus;let D=be.mergeborer(d.flowdata,T,R);if(D){let E=D.row[2],z=D.row[3],P=D.column[2],L=D.column[3];n>0?e=="down"?(T=z,R=P):e=="right"&&(T=E,R=L):(T=E,R=P)}let S=x.moveXY==null?T:x.moveXY.x,B=x.moveXY==null?R:x.moveXY.y;e=="down"?(T+=n,S=T):e=="right"&&(R+=n,B=R),T>=a&&(T=a-1,S=T),T<0&&(T=0,S=T),R>=o&&(R=o-1,B=R),R<0&&(R=0,B=R);let F=be.mergeborer(d.flowdata,T,R);F?(s=F.row[1],u=F.row[0],f=F.row[2],A=F.row[3],h=F.column[1],g=F.column[0],p=F.column[2],v=F.column[3]):(s=d.visibledatarow[S],u=S-1==-1?0:d.visibledatarow[S-1],f=S,A=S,h=d.visibledatacolumn[B],g=B-1==-1?0:d.visibledatacolumn[B-1],p=B,v=B),m.func_selectedrange={left:g,width:h-g-1,top:u,height:s-u-1,left_move:g,width_move:h-g-1,top_move:u,height_move:s-u-1,row:[f,A],column:[p,v],row_focus:f,column_focus:p,moveXY:{x:S,y:B}},$("#luckysheet-formula-functionrange-select").css({left:g,width:h-g-1,top:u,height:s-u-1}).show(),m.rangeSetValue({row:[f,A],column:[p,v]})}let y=$("#luckysheet-cell-main").scrollLeft(),b=$("#luckysheet-cell-main").scrollTop(),k=$("#luckysheet-cell-main").height(),w=$("#luckysheet-cell-main").width(),C=0,I=0;h-y-w+20>0?(C=h-w+20,l&&$("#luckysheet-scrollbar-x").scrollLeft(C)):g-y-20<0&&(C=g-20,l&&$("#luckysheet-scrollbar-x").scrollLeft(C)),s-b-k+20>0?(I=s-k+20,l&&$("#luckysheet-scrollbar-y").scrollTop(I)):u-b-20<0&&(I=u-20,l&&$("#luckysheet-scrollbar-y").scrollTop(I)),clearTimeout(d.countfuncTimeout),Cl(),ae.saveParam("mv",d.currentSheetIndex,d.luckysheet_select_save)}function Ea(e,n,t){t||(t=!0);let l,a,o,s;if(n=="rangeOfSelect"){let v=d.luckysheet_select_save[d.luckysheet_select_save.length-1],y=v.row_focus,b=v.column_focus,k=!1,w={};if(d.config.merge!=null&&y+"_"+b in d.config.merge&&(k=!0,w=d.config.merge[y+"_"+b]),e=="down"){if(y==d.flowdata.length-1)return;k?y=gt("down",b,w.r+w.rs-1,d.flowdata.length-1):y=gt("down",b,y,d.flowdata.length-1)}else if(e=="up"){if(y==0)return;k?y=gt("up",b,0,w.r):y=gt("up",b,0,y)}else if(e=="right"){if(b==d.flowdata[0].length-1)return;k?b=gt("right",y,w.c+w.cs-1,d.flowdata[0].length-1):b=gt("right",y,b,d.flowdata[0].length-1)}else if(e=="left"){if(b==0)return;k?b=gt("left",y,0,w.c):b=gt("left",y,0,b)}let C=[y,y],I=[b,b];l=d.visibledatarow[y],a=y-1==-1?0:d.visibledatarow[y-1],o=d.visibledatacolumn[b],s=b-1==-1?0:d.visibledatacolumn[b-1];let x=be.mergeMoveMain(I,C,v,a,l-a-1,s,o-s-1);x!=null&&(I=x[0],C=x[1]),d.luckysheet_select_save=[{row:C,column:I}],at(),Xe.pivotclick(y,b),m.fucntionboxshow(y,b)}else if(n=="rangeOfFormula"){let v=m.func_selectedrange,y=v.row_focus,b=v.column_focus,k=!1,w={};if(d.config.merge!=null&&y+"_"+b in d.config.merge&&(k=!0,w=d.config.merge[y+"_"+b]),e=="down"){if(y==d.flowdata.length-1)return;k?y=gt("down",b,w.r+w.rs-1,d.flowdata.length-1):y=gt("down",b,y,d.flowdata.length-1)}else if(e=="up"){if(y==0)return;k?y=gt("up",b,0,w.r):y=gt("up",b,0,y)}else if(e=="right"){if(b==d.flowdata[0].length-1)return;k?b=gt("right",y,w.c+w.cs-1,d.flowdata[0].length-1):b=gt("right",y,b,d.flowdata[0].length-1)}else if(e=="left"){if(b==0)return;k?b=gt("left",y,0,w.c):b=gt("left",y,0,b)}let C=[y,y],I=[b,b];l=d.visibledatarow[y],a=y-1==-1?0:d.visibledatarow[y-1],o=d.visibledatacolumn[b],s=b-1==-1?0:d.visibledatacolumn[b-1];let x=a,T=l-a-1,R=s,D=o-s-1,S=be.mergeMoveMain(I,C,v,x,T,R,D);S!=null&&(I=S[0],C=S[1],x=S[2],T=S[3],R=S[4],D=S[5]),m.func_selectedrange={left:R,width:D,top:x,height:T,left_move:R,width_move:D,top_move:x,height_move:T,row:C,column:I,row_focus:y,column_focus:b},$("#luckysheet-formula-functionrange-select").css({left:R,width:D,top:x,height:T}).show(),m.rangeSetValue({row:C,column:I})}let u=$("#luckysheet-cell-main").scrollLeft(),f=$("#luckysheet-cell-main").scrollTop(),A=$("#luckysheet-cell-main").height(),h=$("#luckysheet-cell-main").width(),g=0,p=0;o-u-h+20>0?(g=o-h+20,t&&$("#luckysheet-scrollbar-x").scrollLeft(g)):s-u-20<0&&(g=s-20,t&&$("#luckysheet-scrollbar-x").scrollLeft(g)),l-f-A+20>0?(p=l-A+20,t&&$("#luckysheet-scrollbar-y").scrollTop(p)):a-f-20<0&&(p=a-20,t&&$("#luckysheet-scrollbar-y").scrollTop(p)),clearTimeout(d.countfuncTimeout),Cl()}function ia(e,n,t,l){l==null&&(l=!0),e||e=="down";let a,o,s,u;if(t=="rangeOfSelect"){let y=d.luckysheet_select_save[d.luckysheet_select_save.length-1],b=y.row[0],k=y.row[1],w=y.column[0],C=y.column[1],I=y.row_focus,x=y.column_focus,T=d.flowdata.length,R=d.flowdata[0].length;if(e=="down"){if(Ql(I,w,C)){let F=dr(I,w,C),E=F[0],z=F[1];E>b&&z==k?(n>0&&Ql(b,w,C)&&(b=dr(b,w,C)[1]),b+=n):z0?k+=n:b+=n}else I>b&&I==k?(n>0&&Ql(b,w,C)&&(b=dr(b,w,C)[1]),b+=n):I0?k+=n:b+=n);k>=T&&(k=T-1),k<0&&(k=0),b>=T&&(b=T-1),b<0&&(b=0)}else{if(_l(x,b,k)){let F=fr(x,b,k),E=F[0],z=F[1];E>w&&z==C?(n>0&&_l(w,b,k)&&(w=fr(w,b,k)[1]),w+=n):z0?C+=n:w+=n}else x>w&&x==C?(n>0&&_l(w,b,k)&&(w=fr(w,b,k)[1]),w+=n):x0?C+=n:w+=n);C>=R&&(C=R-1),C<0&&(C=0),w>=R&&(w=R-1),w<0&&(w=0)}let D=[b,k],S=[w,C];a=d.visibledatarow[k],o=b-1==-1?0:d.visibledatarow[b-1],s=d.visibledatacolumn[C],u=w-1==-1?0:d.visibledatacolumn[w-1];let B=be.mergeMoveMain(S,D,y,o,a-o-1,u,s-u-1);B!=null&&(S=B[0],D=B[1]),y.row=D,y.column=S,at()}else if(t=="rangeOfFormula"){let y=m.func_selectedrange,b=y.row[0],k=y.row[1],w=y.column[0],C=y.column[1],I=y.row_focus,x=y.column_focus,T=d.flowdata.length,R=d.flowdata[0].length;if(e=="down"){if(Ql(I,w,C)){let L=dr(I,w,C),O=L[0],q=L[1];O>b&&q==k?(n>0&&Ql(b,w,C)&&(b=dr(b,w,C)[1]),b+=n):q0?k+=n:b+=n}else I>b&&I==k?(n>0&&Ql(b,w,C)&&(b=dr(b,w,C)[1]),b+=n):I0?k+=n:b+=n);k>=T&&(k=T-1),k<0&&(k=0),b>=T&&(b=T-1),b<0&&(b=0)}else{if(_l(x,b,k)){let L=fr(x,b,k),O=L[0],q=L[1];O>w&&q==C?(n>0&&_l(w,b,k)&&(w=fr(w,b,k)[1]),w+=n):q0?C+=n:w+=n}else x>w&&x==C?(n>0&&_l(w,b,k)&&(w=fr(w,b,k)[1]),w+=n):x0?C+=n:w+=n);C>=R&&(C=R-1),C<0&&(C=0),w>=R&&(w=R-1),w<0&&(w=0)}let D=[b,k],S=[w,C];a=d.visibledatarow[k],o=b-1==-1?0:d.visibledatarow[b-1],s=d.visibledatacolumn[C],u=w-1==-1?0:d.visibledatacolumn[w-1];let B=o,F=a-o-1,E=u,z=s-u-1,P=be.mergeMoveMain(S,D,y,B,F,E,z);P!=null&&(S=P[0],D=P[1],B=P[2],F=P[3],E=P[4],z=P[5]),m.func_selectedrange={left:E,width:z,top:B,height:F,left_move:E,width_move:z,top_move:B,height_move:F,row:D,column:S,row_focus:I,column_focus:x},$("#luckysheet-formula-functionrange-select").css({left:E,width:z,top:B,height:F}).show(),m.rangeSetValue({row:D,column:S})}let f=$("#luckysheet-cell-main").scrollLeft(),A=$("#luckysheet-cell-main").scrollTop(),h=$("#luckysheet-cell-main").height(),g=$("#luckysheet-cell-main").width(),p=0,v=0;s-f-g+20>0?(p=s-g+20,l&&$("#luckysheet-scrollbar-x").scrollLeft(p)):u-f-20<0&&(p=u-20,l&&$("#luckysheet-scrollbar-x").scrollLeft(p)),a-A-h+20>0?(v=a-h+20,l&&$("#luckysheet-scrollbar-y").scrollTop(v)):o-A-20<0&&(v=o-20,l&&$("#luckysheet-scrollbar-y").scrollTop(v)),clearTimeout(d.countfuncTimeout),Cl()}function pn(e,n,t){t||(t=!0);let l,a,o,s;if(n=="rangeOfSelect"){let v=d.luckysheet_select_save[d.luckysheet_select_save.length-1],y=v.row_focus,b=v.column_focus,k=v.row[0],w=v.row[1],C=v.column[0],I=v.column[1];if(e=="down"){if(w==d.flowdata.length-1)return;if(Ql(y,C,I)){let D=dr(y,C,I),S=D[0],B=D[1];S>k&&B==w?k=gt("down",b,k,w):w=gt("down",b,w,d.flowdata.length-1)}else y>k&&y==w?k=gt("down",b,k,w):w=gt("down",b,w,d.flowdata.length-1)}else if(e=="up"){if(k==0)return;if(Ql(y,C,I)){let D=dr(y,C,I),S=D[0];D[1]C&&B==I?C=gt("right",y,C,I):I=gt("right",y,I,d.flowdata[0].length-1)}else b>C&&b==I?C=gt("right",y,C,I):I=gt("right",y,I,d.flowdata[0].length-1)}else if(e=="left"){if(C==0)return;if(_l(b,k,w)){let D=fr(b,k,w),S=D[0];D[1]k&&P==w?k=gt("down",b,k,w):w=gt("down",b,w,d.flowdata.length-1)}else y>k&&y==w?k=gt("down",b,k,w):w=gt("down",b,w,d.flowdata.length-1)}else if(e=="up"){if(k==0)return;if(Ql(y,C,I)){let E=dr(y,C,I),z=E[0];E[1]C&&P==I?C=gt("right",y,C,I):I=gt("right",y,I,d.flowdata[0].length-1)}else b>C&&b==I?C=gt("right",y,C,I):I=gt("right",y,I,d.flowdata[0].length-1)}else if(e=="left"){if(C==0)return;if(_l(b,k,w)){let E=fr(b,k,w),z=E[0];E[1]0?(g=o-h+20,t&&$("#luckysheet-scrollbar-x").scrollLeft(g)):s-u-20<0&&(g=s-20,t&&$("#luckysheet-scrollbar-x").scrollLeft(g)),l-f-A+20>0?(p=l-A+20,t&&$("#luckysheet-scrollbar-y").scrollTop(p)):a-f-20<0&&(p=a-20,t&&$("#luckysheet-scrollbar-y").scrollTop(p)),clearTimeout(d.countfuncTimeout),Cl()}function Ql(e,n,t){let l=!1;for(let a=n;a<=t;a++){let o=d.flowdata[e][a];if(N(o)=="object"&&"mc"in o){l=!0;break}}return l}function _l(e,n,t){let l=!1;for(let a=n;a<=t;a++){let o=d.flowdata[a][e];if(N(o)=="object"&&"mc"in o){l=!0;break}}return l}function dr(e,n,t){let l=0,a=d.flowdata.length-1,o=null;if(e>l)for(let u=e;u>=l;u--){for(let f=n;f<=t;f++){let A=d.flowdata[u][f];if(N(A)=="object"&&"mc"in A){let h=d.config.merge[A.mc.r+"_"+A.mc.c];(o==null||h.rl)u=o;else break}else o=l;let s=null;if(es)&&(s=h.r+h.rs-1)}}if(Ql(s+1,n,t)&&sl)for(let u=e;u>=l;u--){for(let f=n;f<=t;f++){let A=d.flowdata[f][u];if(N(A)=="object"&&"mc"in A){let h=d.config.merge[A.mc.r+"_"+A.mc.c];(o==null||h.cl)u=o;else break}else o=l;let s=null;if(es)&&(s=h.c+h.cs-1)}}if(_l(s+1,n,t)&&s1){if(o&&u[f]==!1){a=t+f+1;break}else if(!o){if(u[f]==!1&&u[f-1]==!0){a=t+f+1;break}else if(u[f]==!0&&u[f-1]==!1){a=t+f;break}}}A==l&&(a=l),f++}}else if(e=="up"){let s=d.flowdata[l][n];N(s)=="object"&&me(s.v)||me(s)?o=!0:o=!1;let u=[],f=0;for(let A=l-1;A>=t;A--){let h=d.flowdata[A][n];if(N(h)=="object"&&me(h.v)||me(h)?u.push(!0):u.push(!1),u.length==1&&o&&u[f]==!1){a=l-(f+1);break}else if(u.length>1){if(o&&u[f]==!1){a=l-(f+1);break}else if(!o){if(u[f]==!1&&u[f-1]==!0){a=l-(f+1);break}else if(u[f]==!0&&u[f-1]==!1){a=l-f;break}}}A==t&&(a=t),f++}}else if(e=="right"){let s=d.flowdata[n][t];N(s)=="object"&&me(s.v)||me(s)?o=!0:o=!1;let u=[],f=0;for(let A=t+1;A<=l;A++){let h=d.flowdata[n][A];if(N(h)=="object"&&me(h.v)||me(h)?u.push(!0):u.push(!1),u.length==1&&o&&u[f]==!1){a=t+f+1;break}else if(u.length>1){if(o&&u[f]==!1){a=t+f+1;break}else if(!o){if(u[f]==!1&&u[f-1]==!0){a=t+f+1;break}else if(u[f]==!0&&u[f-1]==!1){a=t+f;break}}}A==l&&(a=l),f++}}else if(e=="left"){let s=d.flowdata[n][l];N(s)=="object"&&me(s.v)||me(s)?o=!0:o=!1;let u=[],f=0;for(let A=l-1;A>=t;A--){let h=d.flowdata[n][A];if(N(h)=="object"&&me(h.v)||me(h)?u.push(!0):u.push(!1),u.length==1&&o&&u[f]==!1){a=l-(f+1);break}else if(u.length>1){if(o&&u[f]==!1){a=l-(f+1);break}else if(!o){if(u[f]==!1&&u[f-1]==!0){a=l-(f+1);break}else if(u[f]==!0&&u[f-1]==!1){a=l-f;break}}}A==t&&(a=t),f++}}return a}var Ba=De(()=>{dt();jt();Pt();ci();ll();el();vn();je();$t()});var gy,Jt,Pa=De(()=>{wt();dt();jt();Pt();rl();_t();Wt();Nr();$t();el();je();yt();gy={rangefocus:!1,modelfocusIndex:null,FixedModelColor:[{head:{fc:"#000",bc:"#bfbdbe"},one:{fc:"#000",bc:"#ffffff"},two:{fc:"#000",bc:"#f8f3f7"},foot:{fc:"#000",bc:"#dde2de"}},{head:{fc:"#000",bc:"#4bd4e7"},one:{fc:"#000",bc:"#ffffff"},two:{fc:"#000",bc:"#eaf7ff"},foot:{fc:"#000",bc:"#aae9f8"}},{head:{fc:"#000",bc:"#5ed593"},one:{fc:"#000",bc:"#ffffff"},two:{fc:"#000",bc:"#e5fbee"},foot:{fc:"#000",bc:"#a5efcc"}},{head:{fc:"#000",bc:"#f6cb4b"},one:{fc:"#000",bc:"#ffffff"},two:{fc:"#000",bc:"#fff9e7"},foot:{fc:"#000",bc:"#ffebac"}},{head:{fc:"#000",bc:"#f96420"},one:{fc:"#000",bc:"#ffffff"},two:{fc:"#000",bc:"#ffe5d9"},foot:{fc:"#000",bc:"#ffcfba"}},{head:{fc:"#000",bc:"#5599fc"},one:{fc:"#000",bc:"#ffffff"},two:{fc:"#000",bc:"#ecf2fe"},foot:{fc:"#000",bc:"#afcbfa"}},{head:{fc:"#000",bc:"#22a69b"},one:{fc:"#000",bc:"#ffffff"},two:{fc:"#000",bc:"#dff2f8"},foot:{fc:"#000",bc:"#8dd4d0"}},{head:{fc:"#000",bc:"#7a939a"},one:{fc:"#000",bc:"#ffffff"},two:{fc:"#000",bc:"#f0eff7"},foot:{fc:"#000",bc:"#bdcad0"}},{head:{fc:"#000",bc:"#d7a270"},one:{fc:"#000",bc:"#ffffff"},two:{fc:"#000",bc:"#fdf3f1"},foot:{fc:"#000",bc:"#ead2b6"}},{head:{fc:"#000",bc:"#89c54b"},one:{fc:"#000",bc:"#ffffff"},two:{fc:"#000",bc:"#f1f7e9"},foot:{fc:"#000",bc:"#c5e3a7"}},{head:{fc:"#000",bc:"#8f88f0"},one:{fc:"#000",bc:"#ffffff"},two:{fc:"#000",bc:"#f0e5ff"},foot:{fc:"#000",bc:"#c6c4f6"}},{head:{fc:"#000",bc:"#fd1664"},one:{fc:"#000",bc:"#ffffff"},two:{fc:"#000",bc:"#feddee"},foot:{fc:"#000",bc:"#f98ab5"}},{head:{fc:"#000",bc:"#da96d3"},one:{fc:"#000",bc:"#ffffff"},two:{fc:"#000",bc:"#fce8fb"},foot:{fc:"#000",bc:"#f2caee"}},{head:{fc:"#000",bc:"#b49191"},one:{fc:"#000",bc:"#ffffff"},two:{fc:"#000",bc:"#f5ebe8"},foot:{fc:"#000",bc:"#d8c3c3"}},{head:{fc:"#000",bc:"#91b493"},one:{fc:"#000",bc:"#ffffff"},two:{fc:"#000",bc:"#f0fbf0"},foot:{fc:"#000",bc:"#b4cfb6"}},{head:{fc:"#000",bc:"#b4a891"},one:{fc:"#000",bc:"#ffffff"},two:{fc:"#000",bc:"#f8f6f1"},foot:{fc:"#000",bc:"#d3cab8"}},{head:{fc:"#000",bc:"#91abb4"},one:{fc:"#000",bc:"#ffffff"},two:{fc:"#000",bc:"#eff7fa"},foot:{fc:"#000",bc:"#b7cbd3"}},{head:{fc:"#000",bc:"#b7ba82"},one:{fc:"#000",bc:"#ffffff"},two:{fc:"#000",bc:"#fafbeb"},foot:{fc:"#000",bc:"#dadcb4"}},{head:{fc:"#000",bc:"#df3e3e"},one:{fc:"#000",bc:"#ffffff"},two:{fc:"#000",bc:"#fde9e9"},foot:{fc:"#000",bc:"#f89292"}},{head:{fc:"#000",bc:"#f2711c"},one:{fc:"#000",bc:"#ffffff"},two:{fc:"#000",bc:"#fef0d7"},foot:{fc:"#000",bc:"#fbb335"}},{head:{fc:"#000",bc:"#b5cc18"},one:{fc:"#000",bc:"#ffffff"},two:{fc:"#000",bc:"#f9fbd4"},foot:{fc:"#000",bc:"#e2ed2a"}},{head:{fc:"#000",bc:"#00b5ad"},one:{fc:"#000",bc:"#ffffff"},two:{fc:"#000",bc:"#ccfaf9"},foot:{fc:"#000",bc:"#00e4df"}},{head:{fc:"#000",bc:"#2185d0"},one:{fc:"#000",bc:"#ffffff"},two:{fc:"#000",bc:"#d8f3fc"},foot:{fc:"#000",bc:"#3cc4f0"}},{head:{fc:"#000",bc:"#a5673f"},one:{fc:"#000",bc:"#ffffff"},two:{fc:"#000",bc:"#f6ede5"},foot:{fc:"#000",bc:"#d3a47c"}}],getModelBox:function(e,n){let t=this;$("#luckysheet-modal-dialog-slider-alternateformat #luckysheet-alternateformat-modelList").empty(),$("#luckysheet-modal-dialog-slider-alternateformat #luckysheet-alternateformat-modelCustom").empty();let l="";for(let o=0;o
\u2014 \u2014 \u2014 \u2014
'}$("#luckysheet-modal-dialog-slider-alternateformat #luckysheet-alternateformat-modelList").append(l);let a=d.luckysheetfile[Q(d.currentSheetIndex)].luckysheet_alternateformat_save_modelCustom;if(a!=null&&a.length>0){let o="";for(let s=0;s
\u2014 \u2014 \u2014 \u2014
'}$("#luckysheet-modal-dialog-slider-alternateformat #luckysheet-alternateformat-modelCustom").append(o)}},init:function(){let e=this;$("#luckysheet-modal-dialog-slider-alternateformat").remove(),$("body").append(Ud()),Lt(),$("#luckysheet-modal-dialog-slider-alternateformat .luckysheet-model-close-btn").click(function(){$("#luckysheet-modal-dialog-slider-alternateformat").hide(),Lt()}),$(document).off("focus.AFrangeInput").on("focus.AFrangeInput","#luckysheet-alternateformat-range input",function(){e.rangefocus=!0}),$(document).off("blur.AFrangeInput").on("blur.AFrangeInput","#luckysheet-alternateformat-range input",function(){e.rangefocus=!1}),$(document).off("keydown.AFrangeInput").on("keydown.AFrangeInput","#luckysheet-alternateformat-range input",function(n){let t=$(this).val().trim();n.keyCode==13&&e.update()}),$(document).off("click.AFrangeIcon").on("click.AFrangeIcon","#luckysheet-alternateformat-range .fa-table",function(){$("#luckysheet-modal-dialog-slider-alternateformat").hide(),Lt();let n=$(this).parents("#luckysheet-alternateformat-range").find("input").val().trim();e.rangeDialog(n)}),$(document).off("click.AFrDCf").on("click.AFrDCf","#luckysheet-alternateformat-rangeDialog-confirm",function(){let n=$(this).parents("#luckysheet-alternateformat-rangeDialog").find("input").val().trim();$("#luckysheet-modal-dialog-slider-alternateformat #luckysheet-alternateformat-range input").val(n),$(this).parents("#luckysheet-alternateformat-rangeDialog").hide(),$("#luckysheet-modal-dialog-slider-alternateformat").show(),Lt(),e.update()}),$(document).off("click.AFrDCl").on("click.AFrDCl","#luckysheet-alternateformat-rangeDialog-close",function(){$(this).parents("#luckysheet-alternateformat-rangeDialog").hide(),$("#luckysheet-modal-dialog-slider-alternateformat").show(),Lt()}),$(document).off("click.AFrDTitle").on("click.AFrDTitle","#luckysheet-alternateformat-rangeDialog .luckysheet-modal-dialog-title-close",function(){$(this).parents("#luckysheet-alternateformat-rangeDialog").hide(),$("#luckysheet-modal-dialog-slider-alternateformat").show(),Lt()}),$(document).off("change.AFrowHeader").on("change.AFrowHeader","#luckysheet-alternateformat-rowHeader",function(){let n;$(this).is(":checked")?n=!0:n=!1;let t;$("#luckysheet-alternateformat-rowFooter").is(":checked")?t=!0:t=!1,e.checkboxChange(n,t),e.modelboxOn(),e.update()}),$(document).off("change.AFrowFooter").on("change.AFrowFooter","#luckysheet-alternateformat-rowFooter",function(){let n;$("#luckysheet-alternateformat-rowHeader").is(":checked")?n=!0:n=!1;let t;$(this).is(":checked")?t=!0:t=!1,e.checkboxChange(n,t),e.modelboxOn(),e.update()}),$(document).off("click.AFmodelbox").on("click.AFmodelbox","#luckysheet-modal-dialog-slider-alternateformat .modelbox",function(){let n=$(this).index(),t=$(this).parents(".cf").attr("id");if(t=="luckysheet-alternateformat-modelList")e.modelfocusIndex=n;else if(t=="luckysheet-alternateformat-modelCustom"){let l=e.FixedModelColor.length;e.modelfocusIndex=n+l}e.modelboxOn(),e.update()}),$(document).off("click.AFselectColor").on("click.AFselectColor","#luckysheet-modal-dialog-slider-alternateformat .luckysheet-color-menu-button-indicator",function(){let n=$(this).closest(".toningbox"),t,l;$(this).find(".luckysheet-icon-img").hasClass("luckysheet-icon-text-color")?(t="fc",l=n.find(".toningShow").data("fc")):$(this).find(".luckysheet-icon-img").hasClass("luckysheet-icon-cell-color")&&(t="bc",l=n.find(".toningShow").data("bc"));let a;n.hasClass("header")?a="0":n.hasClass("ctOne")?a="1":n.hasClass("ctTwo")?a="2":n.hasClass("footer")&&(a="3"),e.colorSelectDialog(l,t,a)}),$(document).off("click.AFselectColorConfirm").on("click.AFselectColorConfirm","#luckysheet-alternateformat-colorSelect-dialog-confirm",function(){let n=$(this).parents("#luckysheet-alternateformat-colorSelect-dialog"),l=te().alternatingColors;$("#luckysheet-modal-dialog-mask").hide(),n.hide();let a=n.find(".currenColor span").attr("title"),o;n.find(".luckysheet-modal-dialog-title-text").text()==l.selectionTextColor?o="fc":n.find(".luckysheet-modal-dialog-title-text").text()==l.selectionCellColor&&(o="bc");let s=n.find(".currenColor").attr("data-source");s=="0"?(o=="fc"&&($("#luckysheet-alternateformat-modelToning .header .toningShow").css("color",a),$("#luckysheet-alternateformat-modelToning .header .toningShow").data("fc",a),$("#luckysheet-alternateformat-modelToning .header .luckysheet-icon-text-color").parents(".luckysheet-color-menu-button-indicator").css("border-bottom-color",a)),o=="bc"&&($("#luckysheet-alternateformat-modelToning .header .toningShow").css("background-color",a),$("#luckysheet-alternateformat-modelToning .header .toningShow").data("bc",a),$("#luckysheet-alternateformat-modelToning .header .luckysheet-icon-cell-color").parents(".luckysheet-color-menu-button-indicator").css("border-bottom-color",a))):s=="1"?(o=="fc"&&($("#luckysheet-alternateformat-modelToning .ctOne .toningShow").css("color",a),$("#luckysheet-alternateformat-modelToning .ctOne .toningShow").data("fc",a),$("#luckysheet-alternateformat-modelToning .ctOne .luckysheet-icon-text-color").parents(".luckysheet-color-menu-button-indicator").css("border-bottom-color",a)),o=="bc"&&($("#luckysheet-alternateformat-modelToning .ctOne .toningShow").css("background-color",a),$("#luckysheet-alternateformat-modelToning .ctOne .toningShow").data("bc",a),$("#luckysheet-alternateformat-modelToning .ctOne .luckysheet-icon-cell-color").parents(".luckysheet-color-menu-button-indicator").css("border-bottom-color",a))):s=="2"?(o=="fc"&&($("#luckysheet-alternateformat-modelToning .ctTwo .toningShow").css("color",a),$("#luckysheet-alternateformat-modelToning .ctTwo .toningShow").data("fc",a),$("#luckysheet-alternateformat-modelToning .ctTwo .luckysheet-icon-text-color").parents(".luckysheet-color-menu-button-indicator").css("border-bottom-color",a)),o=="bc"&&($("#luckysheet-alternateformat-modelToning .ctTwo .toningShow").css("background-color",a),$("#luckysheet-alternateformat-modelToning .ctTwo .toningShow").data("bc",a),$("#luckysheet-alternateformat-modelToning .ctTwo .luckysheet-icon-cell-color").parents(".luckysheet-color-menu-button-indicator").css("border-bottom-color",a))):s=="3"&&(o=="fc"&&($("#luckysheet-alternateformat-modelToning .footer .toningShow").css("color",a),$("#luckysheet-alternateformat-modelToning .footer .toningShow").data("fc",a),$("#luckysheet-alternateformat-modelToning .footer .luckysheet-icon-text-color").parents(".luckysheet-color-menu-button-indicator").css("border-bottom-color",a)),o=="bc"&&($("#luckysheet-alternateformat-modelToning .footer .toningShow").css("background-color",a),$("#luckysheet-alternateformat-modelToning .footer .toningShow").data("bc",a),$("#luckysheet-alternateformat-modelToning .footer .luckysheet-icon-cell-color").parents(".luckysheet-color-menu-button-indicator").css("border-bottom-color",a)));let u;$("#luckysheet-alternateformat-rowHeader").is(":checked")?u=!0:u=!1;let f;$("#luckysheet-alternateformat-rowFooter").is(":checked")?f=!0:f=!1;let A=e.modelfocusIndex,h=e.FixedModelColor.length,g,p;if(A1?l.splice(n,1):l=[];let o=$.extend(!0,[],l);e.ref(a,o),ae.allowUpdate&&ae.saveParam("all",d.currentSheetIndex,l,{k:"luckysheet_alternateformat_save"}),$("#luckysheet-modal-dialog-mask").hide(),$("#luckysheet-modal-dialog-slider-alternateformat").hide(),Lt()})},perfect:function(){let e=this,n=$.extend(!0,{},d.luckysheet_select_save[0]),t=e.rangeIsExists(n)[1],l=$.extend(!0,{},d.luckysheetfile[Q(d.currentSheetIndex)].luckysheet_alternateformat_save[t]),a=l.cellrange;$("#luckysheet-alternateformat-range input").val(mt(d.currentSheetIndex,{row:a.row,column:a.column},d.currentSheetIndex)),d.luckysheet_select_save=[{row:a.row,column:a.column}],at();let o=l.hasRowHeader,s=l.hasRowFooter,u=l.format;e.modelfocusIndex=e.getIndexByFormat(u),e.modelfocusIndex==null&&(e.addCustomModel(u),e.modelfocusIndex=e.getIndexByFormat(u)),e.checkboxChange(o,s),e.modelboxOn(),$("#luckysheet-alternateformat-remove").data("index",t)},checkboxChange:function(e,n){e?($("#luckysheet-alternateformat-rowHeader").prop("checked",!0),$("#luckysheet-alternateformat-modelToning .header").show()):($("#luckysheet-alternateformat-rowHeader").removeAttr("checked"),$("#luckysheet-alternateformat-modelToning .header").hide()),n?($("#luckysheet-alternateformat-rowFooter").prop("checked",!0),$("#luckysheet-alternateformat-modelToning .footer").show()):($("#luckysheet-alternateformat-rowFooter").removeAttr("checked"),$("#luckysheet-alternateformat-modelToning .footer").hide()),this.getModelBox(e,n)},modelboxOn:function(){let e=this;$("#luckysheet-modal-dialog-slider-alternateformat .modelbox").removeClass("on");let n=e.modelfocusIndex,t=e.FixedModelColor.length;n"+a.currentColor+"\uFF1A
",botton:'",style:"z-index:100003"}));let f=$("#luckysheet-alternateformat-colorSelect-dialog").find(".luckysheet-modal-dialog-content").css("min-width",300).end(),A=f.outerHeight(),h=f.outerWidth(),g=$(window).width(),p=$(window).height(),v=$(document).scrollLeft(),y=$(document).scrollTop();$("#luckysheet-alternateformat-colorSelect-dialog").css({left:(g+v-h)/2,top:(p+y-A)/3}).show(),$("#luckysheet-alternateformat-colorSelect-dialog").find(".colorshowbox").spectrum({showPalette:!0,showPaletteOnly:!0,preferredFormat:"hex",clickoutFiresChange:!1,showInitial:!0,showInput:!0,flat:!0,hideAfterPaletteSelect:!0,showSelectionPalette:!0,showButtons:!1,maxPaletteSize:8,maxSelectionSize:8,color:e,cancelText:o.cancel,chooseText:s.confirmColor,togglePaletteMoreText:s.customColor,togglePaletteLessText:s.collapse,togglePaletteOnly:!0,clearText:s.clearText,noColorSelectedText:s.noColorSelectedText,localStorageKey:"spectrum.textcolor"+ae.gridKey,palette:[["#000","#444","#666","#999","#ccc","#eee","#f3f3f3","#fff"],["#f00","#f90","#ff0","#0f0","#0ff","#00f","#90f","#f0f"],["#f4cccc","#fce5cd","#fff2cc","#d9ead3","#d0e0e3","#cfe2f3","#d9d2e9","#ead1dc"],["#ea9999","#f9cb9c","#ffe599","#b6d7a8","#a2c4c9","#9fc5e8","#b4a7d6","#d5a6bd"],["#e06666","#f6b26b","#ffd966","#93c47d","#76a5af","#6fa8dc","#8e7cc3","#c27ba0"],["#c00","#e69138","#f1c232","#6aa84f","#45818e","#3d85c6","#674ea7","#a64d79"],["#900","#b45f06","#bf9000","#38761d","#134f5c","#0b5394","#351c75","#741b47"],["#600","#783f04","#7f6000","#274e13","#0c343d","#073763","#20124d","#4c1130"]],move:function(b){b!=null?b=b.toHexString():b="#000",$("#luckysheet-alternateformat-colorSelect-dialog .currenColor span").css("background-color",b).attr("title",b)}})},rangeDialog:function(e){$("#luckysheet-modal-dialog-mask").hide(),$("#luckysheet-alternateformat-rangeDialog").remove();let n=te(),t=n.alternatingColors,l=n.button;$("body").append(we(At,{id:"luckysheet-alternateformat-rangeDialog",addclass:"luckysheet-alternateformat-rangeDialog",title:t.selectRange,content:'',botton:'",style:"z-index:100003"}));let a=$("#luckysheet-alternateformat-rangeDialog").find(".luckysheet-modal-dialog-content").css("min-width",300).end(),o=a.outerHeight(),s=a.outerWidth(),u=$(window).width(),f=$(window).height(),A=$(document).scrollLeft(),h=$(document).scrollTop();$("#luckysheet-alternateformat-rangeDialog").css({left:(u+A-s)/2,top:(f+h-o)/3}).show()},rangeIsExists:function(e,n){let t=this,l=!1,a=null,o=$.extend(!0,[],d.luckysheetfile[Q(d.currentSheetIndex)].luckysheet_alternateformat_save);if(n!=null&&n!=null&&(o.length>1?o.splice(n,1):o=[]),o.length>0){let s=[];for(let f=0;f0)for(let o=0;o0)for(let t=0;t1)for(let g=u+1;gu)for(let g=A;g<=h;g++)n[f+"_"+g]=[a.foot.fc,a.foot.bc]}else if(o){for(let g=A;g<=h;g++)n[u+"_"+g]=[a.head.fc,a.head.bc];if(f>u)for(let g=u+1;g<=f;g++){let p,v;(g-u)%2!=0?(p=a.one.fc,v=a.one.bc):(p=a.two.fc,v=a.two.bc);for(let y=A;y<=h;y++)n[g+"_"+y]=[p,v]}}else if(s){if(f>u)for(let g=u;g{dt();py={parseJsonParm:function(e){if(e==null)return{};if(N(e)=="string")try{return new Function("return "+e)()}catch(n){return{}}else return e},hasKey:function(e){let t=this.parseJsonParm(e);for(let l in t)return!0;return!1}},oa=py});function sa(e,n,t,l,a,o,s,u,f,A){if(n?(e.addClass("luckysheet-filter-options-active").data("rowhidden",JSON.stringify(t)).data("caljs",JSON.stringify(l)).html(''),l!=null&&(e.data("byconditionvalue",l.value).data("byconditiontype",l.type).data("byconditiontext",l.text),l.value1!=null&&e.data("byconditionvalue1",l.value1),l.value2!=null&&e.data("byconditionvalue2",l.value2))):(e.removeClass("luckysheet-filter-options-active").data("rowhidden","").data("caljs","").html(''),e.data("byconditionvalue","null").data("byconditiontype","0").data("byconditiontext","\u65E0").data("byconditionvalue1","").data("byconditionvalue2","")),a){let h=d.luckysheetfile[Q(d.currentSheetIndex)];if(h.filter==null&&(h.filter={}),n){let g={caljs:l,rowhidden:t,optionstate:n,str:o,edr:s,cindex:u,stc:f,edc:A};h.filter[u-f]=g}else delete h.filter[u-f];ae.saveParam("all",d.currentSheetIndex,h.filter,{k:"filter"})}}function ih(e,n,t,l,a,o){let s=Te.deepCopyFlowData(d.flowdata);e=e+1;let u=!1,f=[];for(let h=e;h<=t;h++){let g=[];for(let p=n;p<=l;p++){if(s[h][p]!=null&&s[h][p].mc!=null){u=!0;break}g.push(s[h][p])}f.push(g)}if(u){let h=te().filter;he()?alert(h.mergeError):U.info(h.mergeError,"");return}f=Jr(f,a-n,o);for(let h=e;h<=t;h++)for(let g=n;g<=l;g++)s[h][g]=f[h-e][g-n];let A={};if(d.config.rowlen!=null){let h=$.extend(!0,{},d.config);h=vl(s,e,t,h),A={cfg:h,RowlChange:!0}}Je(s,[{row:[e,t],column:[n,l]}],A)}function ro(){if(!Rt(d.currentSheetIndex,"filter"))return;if(d.luckysheet_select_save.length>1){$("#luckysheet-rightclick-menu").hide(),$("#luckysheet-filter-menu, #luckysheet-filter-submenu").hide(),$("#"+d.container).attr("tabindex",0).focus();let n=te().splitText;he()?alert(n.tipNoMulti):U.info(n.tipNoMulti,"");return}if(d.luckysheetfile[Q(d.currentSheetIndex)].isPivotTable)return;$("#luckysheet-filter-selected-sheet"+d.currentSheetIndex+", #luckysheet-filter-options-sheet"+d.currentSheetIndex).remove();let e=d.luckysheet_select_save[0];if(e.row[0]==e.row[1]&&e.column[0]==e.column[1]){let n,t,l=e.row[1];for(let a=0;a';$("#luckysheet-cell-main").append(h);let g="";for(let v=a;v<=o;v++)if(n==null||n[v-a]==null)g+='
';else{let y;if(n[v-a].caljs!=null){let b;n[v-a].caljs.value1!=null?b='data-byconditionvalue1="'+n[v-a].caljs.value1+'" ':b="";let k;n[v-a].caljs.value2!=null?k='data-byconditionvalue2="'+n[v-a].caljs.value2+'" ':k="",y='data-caljs="'+JSON.stringify(n[v-a].caljs)+'" data-byconditionvalue="'+n[v-a].caljs.value+'" data-byconditiontype="'+n[v-a].caljs.type+'" data-byconditiontext="'+n[v-a].caljs.text+'" '+b+k}else y="";g+='
'}$("#luckysheet-cell-main").append('
'+g+"
"),$("#luckysheet-rightclick-menu").hide(),$("#luckysheet-filter-menu, #luckysheet-filter-submenu").hide(),$("#luckysheet-cell-main").scrollTop()>e.top_move&&$("#luckysheet-scrollbar-y").scrollTop(e.top_move);let p=d.luckysheetfile[Q(d.currentSheetIndex)];p.filter_select=e}function oh(){let e=null,n=te(),t=n.filter,l=n.button;$("#luckysheetfilter").click(ro);let a=null,o=null;$(".luckysheet-cols-menu .luckysheet-cols-submenu").hover(function(){let s=$(this),u=s.attr("id"),f=$("#"+u+"_sub"),A=s.parent(),h=$(window).width(),g=$(window).height(),p=A.width(),v=f.height()+25,y=f.width()+5,b=s.offset(),k=b.top,w=b.left+p;w+y>h&&(w=b.left-y),k+v>g&&(k=g-v),f.css({top:k,left:w}).show(),o=s},function(){let s=$(this),u=s.attr("id"),f=$("#"+u+"_sub");a=setTimeout(function(){f.hide()},200)}),$(".luckysheet-rightgclick-menu-sub").hover(function(){o.addClass("luckysheet-cols-menuitem-hover"),clearTimeout(a)},function(){o.removeClass("luckysheet-cols-menuitem-hover"),$(this).hide()}),$("#luckysheet-filter-menu").mouseover(function(){clearTimeout(e),e=setTimeout(function(){$("#luckysheet-filter-submenu").hide()},500)}),$("#luckysheet-filter-submenu").mouseover(function(){clearTimeout(e)}).find(".luckysheet-cols-menuitem").click(function(s){$("#luckysheet-filter-selected span").html($(this).find(".luckysheet-cols-menuitem-content").text()).data("value",$(this).data("value")),$("#luckysheet-filter-menu .luckysheet-filter-selected-input").hide();let u=$(this).data("type"),f=$(this).attr("data-value");u=="2"?($("#luckysheet-filter-selected span").data("type","2"),$("#luckysheet-filter-menu .luckysheet-filter-selected-input2").show(),$("#luckysheet-filter-menu .luckysheet-filter-selected-input input").prop("type","number")):u=="0"?$("#luckysheet-filter-selected span").data("type","0"):($("#luckysheet-filter-selected span").data("type","1"),$("#luckysheet-filter-menu .luckysheet-filter-selected-input").eq(0).show(),f=="dateequal"||f=="datelessthan"||f=="datemorethan"?$("#luckysheet-filter-menu .luckysheet-filter-selected-input input").prop("type","date"):f=="morethan"||f=="moreequalthan"||f=="lessthan"||f=="lessequalthan"||f=="equal"||f=="noequal"?$("#luckysheet-filter-menu .luckysheet-filter-selected-input input").prop("type","number"):$("#luckysheet-filter-menu .luckysheet-filter-selected-input input").prop("type","text")),$("#luckysheet-filter-byvalue").next().slideUp(),$("#luckysheet-filter-submenu").hide()}),$("#luckysheet-filter-bycondition, #luckysheet-filter-byvalue").click(function(){let s=$(this);s.next().slideToggle(200),setTimeout(function(){s.attr("id")=="luckysheet-filter-bycondition"&&$("#luckysheet-filter-bycondition").next().is(":visible")&&$("#luckysheet-filter-selected span").text()!=t.filiterInputNone&&$("#luckysheet-filter-byvalue").next().slideUp(200),s.is($("#luckysheet-filter-bycondition"))&&$("#luckysheet-filter-bycondition").next().is(":hidden")&&$("#luckysheet-filter-byvalue").next().is(":hidden")&&$("#luckysheet-filter-byvalue").next().slideDown(200)},300)}),$("#luckysheet-filter-selected").click(function(){let s=$(this),u=s.offset(),f=$("#luckysheet-filter-submenu");f.hide();let A=$(window).height(),h=$(window).width(),g=f.width(),p=f.height(),v=u.top,y=u.left,b=A-u.top-20;u.left+g>h&&(y=u.left-g),u.top>A/2&&(v=A-u.top,v<0&&(v=0),b=u.top-20),f.css({top:v,left:y,height:b}).show(),clearTimeout(e)}),$("#luckysheet-cell-main").on("click",".luckysheet-filter-options",function(s){if(!Rt(d.currentSheetIndex,"filter"))return;let u=$(s.currentTarget),f=u.offset(),A=$("#luckysheet-filter-menu"),h=$(window).height(),g=$(window).width(),p=u.data("str"),v=u.data("edr"),y=u.data("cindex"),b=u.data("stc"),k=u.data("edc"),w=u.data("rowhidden")==""?{}:JSON.parse(u.data("rowhidden").replace(/\'/g,'"'));$("body .luckysheet-cols-menu").hide(),$("#luckysheet-filter-menu, #luckysheet-filter-submenu").hide(),$("#luckysheet-filter-byvalue-input").val(""),$("#luckysheet-filter-bycondition").next().hide(),$("#luckysheet-filter-byvalue").next().show(),A.data("str",p),A.data("edr",v),A.data("cindex",y),A.data("stc",b),A.data("edc",k),$("#luckysheet-filter-menu .luckysheet-filter-selected-input").hide().find("input").val(),$("#luckysheet-filter-selected span").data("type","0").data("type",null).text(t.filiterInputNone);let C=u.data("byconditiontype");if($("#luckysheet-filter-selected span").data("value",u.data("byconditionvalue")).data("type",C).text(u.data("byconditiontext")),C=="2"){let R=$("#luckysheet-filter-menu .luckysheet-filter-selected-input2").show().find("input");R.eq(0).val(u.data("byconditionvalue1")),R.eq(1).val(u.data("byconditionvalue2"))}else C=="1"&&$("#luckysheet-filter-menu .luckysheet-filter-selected-input").eq(0).show().find("input").val(u.data("byconditionvalue1"));$("#luckysheet-filter-orderby-asc").off("click").on("click",function(){ih(p,b,v,k,y,!0)}),$("#luckysheet-filter-orderby-desc").off("click").on("click",function(){ih(p,b,v,k,y,!1)});let I=Ur("#luckysheet-filter-byvalue-select",{text:t.filiterMoreDataTip});$("#luckysheet-filter-byvalue-select").empty().append(I.el);let x={};$("#luckysheet-filter-options-sheet"+d.currentSheetIndex+" .luckysheet-filter-options").not(this).each(function(){let R=$(this),D=R.data("rowhidden");if(D=="")return!0;D=JSON.parse(D.replace(/\'/g,'"'));for(let S in D)x[S]=0});let T=d.flowdata;return setTimeout(function(){let R={},D={},S={},B={};for(let z=p+1;z<=v;z++){if(z in x||d.flowdata[z]==null)continue;let P=d.flowdata[z][y];if(P!=null&&!me(P.v)&&P.ct!=null&&P.ct.t=="d"){let L=ht("YYYY-MM-DD",P.v),O=L.split("-")[0],q=L.split("-")[1],j=L.split("-")[2];O in R||(R[O]={}),q in R[O]||(R[O][q]={}),j in R[O][q]||(R[O][q][j]=0),R[O][q][j]++,z in w&&(D[O]=0,D[q]=0,D[j]=0)}else{let L,O;P==null||me(P.v)?(L=null,O=null):(L=P.v,O=P.m),L in S||(S[L]={}),O in S[L]||(S[L][O]=0),S[L][O]++,z in w&&(B[L+"#$$$#"+O]=0)}}let F=[];if(JSON.stringify(R).length>2)for(let z in R){let P=0,L="";for(let q in R[z]){let j=0,G="";for(let ee in R[z][q]){let re=R[z][q][ee];j+=re;let oe;Number(q)<10?oe="0"+Number(q):oe=q;let se;Number(ee)<10?se="0"+Number(ee):se=ee,z in D&&q in D&&ee in D?G+='
( '+re+" )
":G+='
( '+re+" )
"}P+=j;let _;Number(q)<10?_="0"+Number(q):_=q,z in D&&q in D?L+='
( '+j+' )
'+G+"
":L+='
( '+j+' )
'+G+"
"}let O;z in D?O='
( '+P+' )
'+L+"
":O='
( '+P+' )
'+L+"
",F.unshift(O)}if(JSON.stringify(S).length>2){let z=Object.keys(S);z=hs(z,!0);for(let P=0;P( '+S[L][O]+" )":j='
( '+S[L][O]+" )
",F.push(j)}}}let E=h-f.top-350;E<0&&(E=100),$("#luckysheet-filter-byvalue-select").append("
"+F.join("")+"
"),I.close()},1),yn(A,f.left,f.top+20),s.stopPropagation(),!1}),$("#luckysheet-filter-orderby-color").hover(function(){let s=$("#luckysheet-filter-menu"),u=s.data("str"),f=s.data("edr"),A=s.data("cindex"),h=s.data("stc"),g=s.data("edc"),p={},v={},y=Jt.getComputeMap(),b=Re.getComputeMap();for(let P=u+1;P<=f;P++){let L=d.flowdata[P][A],O=be.checkstatus(d.flowdata,P,A,"bg");O==null&&(O="#ffffff");let q=Jt.checksAF(P,A,y);q!=null&&(O=q[1]);let j=Re.checksCF(P,A,b);j!=null&&j.cellColor!=null&&(O=j.cellColor),O.indexOf("rgb")>-1&&(O=na(O)),O.length==4&&(O=O.substr(0,1)+O.substr(1,1).repeat(2)+O.substr(2,1).repeat(2)+O.substr(3,1).repeat(2));let G=be.checkstatus(d.flowdata,P,A,"fc");q!=null&&(G=q[0]),j!=null&&j.textColor!=null&&(G=j.textColor),G.indexOf("rgb")>-1&&(G=na(G)),G.length==4&&(G=G.substr(0,1)+G.substr(1,1).repeat(2)+G.substr(2,1).repeat(2)+G.substr(3,1).repeat(2)),d.config!=null&&d.config.rowhidden!=null&&P in d.config.rowhidden?(p[O]=1,L!=null&&!me(L.v)&&(v[G]=1)):(p[O]=0,L!=null&&!me(L.v)&&(v[G]=0))}let k="";if(JSON.stringify(p).length>2&&Object.keys(p).length>1){let P="";for(let L in p)p[L]==0?P+='
':P+='
';k='
'+t.filiterByColorTip+'
'+P+"
"}let w="";if(JSON.stringify(v).length>2&&Object.keys(v).length>1){let P="";for(let L in v)v[L]==0?P+='
':P+='
';w='
'+t.filiterByTextColorTip+'
'+P+"
"}let C;k==""&&w==""?C='
'+t.filterContainerOneColorTip+"
":C=k+w+'
",$("#luckysheet-filter-orderby-color-submenu").remove(),$("body").append('
'+C+"
");let I=$("#luckysheet-filter-orderby-color-submenu").end(),x=$(this).parent(),T=$(window).width(),R=$(window).height(),D=x.width(),S=I.height()+25,B=I.width()+5,F=$(this).offset(),E=F.top,z=F.left+D;z+B>T&&(z=F.left-B),E+S>R&&(E=R-S),$("#luckysheet-filter-orderby-color-submenu").css({top:E,left:z}).show()},function(){a=setTimeout(function(){$("#luckysheet-filter-orderby-color-submenu").hide()},200)}),$(document).on("mouseover mouseleave","#luckysheet-filter-orderby-color-submenu",function(s){s.type==="mouseover"?clearTimeout(a):$(this).hide()}),$(document).on("click","#luckysheet-filter-orderby-color-submenu .item label",function(){$(this).siblings("input[type='checkbox']").click()}),$(document).off("click.orderbyColorConfirm").on("click.orderbyColorConfirm","#luckysheet-filter-orderby-color-submenu #luckysheet-filter-orderby-color-confirm",function(){let s={},u={};$("#luckysheet-filter-orderby-color-submenu .item").each(function(E,z){if($(z).find("input[type='checkbox']").is(":checked")){let P=$(this).find("label").attr("title"),L=$(this).closest(".box").attr("id");L=="filterBgColor"?s[P]=0:L=="filterFcColor"&&(u[P]=0)}});let f;$("#luckysheet-filter-orderby-color-submenu #filterBgColor").length>0?f=!0:f=!1;let A;$("#luckysheet-filter-orderby-color-submenu #filterFcColor").length>0?A=!0:A=!1;let h=$("#luckysheet-filter-menu"),g=h.data("str"),p=h.data("edr"),v=h.data("cindex"),y=h.data("stc"),b=h.data("edc"),k={};$("#luckysheet-filter-options-sheet"+d.currentSheetIndex+" .luckysheet-filter-options").not($("#luckysheet-filter-options-sheet"+d.currentSheetIndex+" .luckysheet-filter-options").eq(v-y).get(0)).each(function(){let E=$(this),z=E.data("rowhidden");if(z=="")return!0;z=JSON.parse(z);for(let P in z)k[P]=0});let w={},C={},I={},x=Jt.getComputeMap(),T=Re.getComputeMap();for(let E=g+1;E<=p;E++){if(E in k||d.flowdata[E]==null)continue;let z=d.flowdata[E][v],P=be.checkstatus(d.flowdata,E,v,"bg"),L=Jt.checksAF(E,v,x);L!=null&&(P=L[1]);let O=Re.checksCF(E,v,T);O!=null&&O.cellColor!=null&&(P=O.cellColor),P=P==null?"#ffffff":P,P.indexOf("rgb")>-1&&(P=na(P)),P.length==4&&(P=P.substr(0,1)+P.substr(1,1).repeat(2)+P.substr(2,1).repeat(2)+P.substr(3,1).repeat(2));let q=be.checkstatus(d.flowdata,E,v,"fc");L!=null&&(q=L[0]),O!=null&&O.textColor!=null&&(q=O.textColor),q.indexOf("rgb")>-1&&(q=na(q)),q.length==4&&(q=q.substr(0,1)+q.substr(1,1).repeat(2)+q.substr(2,1).repeat(2)+q.substr(3,1).repeat(2)),f&&A?!(P in s)&&(!(q in u)||z==null||me(z.v))&&(C[E]=0):f?P in s||(C[E]=0):A&&(!(q in u)||z==null||me(z.v))&&(C[E]=0)}let R=$("#luckysheet-filter-options-sheet"+d.currentSheetIndex+" .luckysheet-filter-options").eq(v-y),D=Object.keys(C).length>0,S=$.extend(!0,k,C),B=oa.parseJsonParm(R.data("rowhidden"));sa(R,D,C,I,!0,g,p,v,y,b);let F=$.extend(!0,{},d.config);if(F.rowhidden=S,d.clearjfundo){let E={};E.type="datachangeAll_filter",E.sheetIndex=d.currentSheetIndex,E.config=$.extend(!0,{},d.config),E.curconfig=F,E.optionstate=D,E.optionsindex=v-y,E.rowhidden=$.extend(!0,{},C),E.rowhidenPre=$.extend(!0,{},B),I!=null&&(E.caljs=I),d.jfundo.length=0,d.jfredo.push(E)}d.config=F,d.luckysheetfile[Q(d.currentSheetIndex)].config=d.config,ae.saveParam("cg",d.currentSheetIndex,F.rowhidden,{k:"rowhidden"}),Mt(d.flowdata.length,d.flowdata[0].length),$("#luckysheet-filter-menu, #luckysheet-filter-submenu, #luckysheet-filter-orderby-color-submenu").hide(),al()}),$(document).off("click.filterCheckbox1").on("click.filterCheckbox1","#luckysheet-filter-byvalue-select .textBox",function(){$(this).attr("data-check")=="true"?($(this).attr("data-check","false"),$(this).find("input[type='checkbox']").removeAttr("checked")):($(this).attr("data-check","true"),$(this).find("input[type='checkbox']").prop("checked",!0))}),$(document).off("click.filterCheckbox2").on("click.filterCheckbox2","#luckysheet-filter-byvalue-select .year",function(){$(this).attr("data-check")=="true"?($(this).attr("data-check","false"),$(this).parents(".yearBox").find(".month").attr("data-check","false"),$(this).parents(".yearBox").find(".day").attr("data-check","false"),$(this).parents(".yearBox").find("input[type='checkbox']").removeAttr("checked")):($(this).attr("data-check","true"),$(this).parents(".yearBox").find(".month").attr("data-check","true"),$(this).parents(".yearBox").find(".day").attr("data-check","true"),$(this).parents(".yearBox").find("input[type='checkbox']").prop("checked",!0))}),$(document).off("click.filterCheckbox3").on("click.filterCheckbox3","#luckysheet-filter-byvalue-select .month",function(){$(this).attr("data-check")=="true"?($(this).attr("data-check","false"),$(this).parents(".monthBox").find(".day").attr("data-check","false"),$(this).parents(".monthBox").find("input[type='checkbox']").removeAttr("checked")):($(this).attr("data-check","true"),$(this).parents(".monthBox").find(".day").attr("data-check","true"),$(this).parents(".monthBox").find("input[type='checkbox']").prop("checked",!0));let s=!0;$(this).parents(".yearBox").find(".day").each(function(f,A){$(A).attr("data-check")=="true"||(s=!1)}),s?($(this).parents(".yearBox").find(".year").attr("data-check","true"),$(this).parents(".yearBox").find(".year input[type='checkbox']").prop("checked",!0)):($(this).parents(".yearBox").find(".year").attr("data-check","false"),$(this).parents(".yearBox").find(".year input[type='checkbox']").removeAttr("checked"))}),$(document).off("click.filterCheckbox4").on("click.filterCheckbox4","#luckysheet-filter-byvalue-select .day",function(){$(this).attr("data-check")=="true"?($(this).attr("data-check","false"),$(this).find("input[type='checkbox']").removeAttr("checked")):($(this).attr("data-check","true"),$(this).find("input[type='checkbox']").prop("checked",!0));let s=!0;$(this).parents(".monthBox").find(".day").each(function(h,g){$(g).attr("data-check")=="true"||(s=!1)}),s?($(this).parents(".monthBox").find(".month").attr("data-check","true"),$(this).parents(".monthBox").find(".month input[type='checkbox']").prop("checked",!0)):($(this).parents(".monthBox").find(".month").attr("data-check","false"),$(this).parents(".monthBox").find(".month input[type='checkbox']").removeAttr("checked"));let f=!0;$(this).parents(".yearBox").find(".day").each(function(h,g){$(g).attr("data-check")=="true"||(f=!1)}),f?($(this).parents(".yearBox").find(".year").attr("data-check","true"),$(this).parents(".yearBox").find(".year input[type='checkbox']").prop("checked",!0)):($(this).parents(".yearBox").find(".year").attr("data-check","false"),$(this).parents(".yearBox").find(".year input[type='checkbox']").removeAttr("checked"))}),$(document).off("click.filterYearDropdown").on("click.filterYearDropdown","#luckysheet-filter-byvalue-select .yearBox .fa-caret-right",function(s){let u=$(this).parents(".luckysheet-mousedown-cancel");u.hasClass("year")&&$(this).parents(".yearBox").find(".monthList").slideToggle(),u.hasClass("month")&&$(this).parents(".monthBox").find(".dayList").slideToggle(),s.stopPropagation()}),$("#luckysheet-filter-byvalue-btn-all").click(function(){$("#luckysheet-filter-byvalue-select .ListBox input[type='checkbox']").prop("checked",!0),$("#luckysheet-filter-byvalue-select .ListBox input[type='checkbox']").parents(".luckysheet-mousedown-cancel").attr("data-check","true")}),$("#luckysheet-filter-byvalue-btn-clear").click(function(){$("#luckysheet-filter-byvalue-select .ListBox input[type='checkbox']").removeAttr("checked"),$("#luckysheet-filter-byvalue-select .ListBox input[type='checkbox']").parents(".luckysheet-mousedown-cancel").attr("data-check","false")}),$("#luckysheet-filter-byvalue-btn-contra").click(function(){$("#luckysheet-filter-byvalue-select .ListBox input[type='checkbox']").each(function(A,h){$(h).is(":checked")?($(h).removeAttr("checked"),$(h).parents(".luckysheet-mousedown-cancel").attr("data-check","false")):($(h).prop("checked",!0),$(h).parents(".luckysheet-mousedown-cancel").attr("data-check","true"))}),$("#luckysheet-filter-byvalue-select .ListBox .monthBox").each(function(A,h){let g=!0;$(h).find(".day input[type='checkbox']").each(function(v,y){$(y).is(":checked")||(g=!1)}),g?($(h).find(".month input[type='checkbox']").prop("checked",!0),$(h).attr("data-check","true")):($(h).find(".month input[type='checkbox']").removeAttr("checked"),$(h).attr("data-check","false"))}),$("#luckysheet-filter-byvalue-select .ListBox .yearBox").each(function(A,h){let g=!0;$(h).find(".day input[type='checkbox']").each(function(v,y){$(y).is(":checked")||(g=!1)}),g?($(h).find(".year input[type='checkbox']").prop("checked",!0),$(h).attr("data-check","true")):($(h).find(".year input[type='checkbox']").removeAttr("checked"),$(h).attr("data-check","false"))})}),$("#luckysheet-filter-initial").click(function(){if(!Rt(d.currentSheetIndex,"filter"))return;$("#luckysheet-filter-menu .luckysheet-filter-selected-input").hide().find("input").val(),$("#luckysheet-filter-selected span").data("type","0").data("type",null).text(t.conditionNone);let s={};s.type="datachangeAll_filter_clear",s.sheetIndex=d.currentSheetIndex,s.config=$.extend(!0,{},d.config),d.config.rowhidden={},s.curconfig=$.extend(!0,{},d.config),s.filter_save=$.extend(!0,{},d.luckysheet_filter_save);let u=[];$("#luckysheet-filter-options-sheet"+d.currentSheetIndex+" .luckysheet-filter-options").each(function(){let f=$(this),A=f.hasClass("luckysheet-filter-options-active"),h=oa.parseJsonParm(f.data("rowhidden")),g=oa.parseJsonParm(f.data("caljs"));u.push({optionstate:A,rowhidden:h,caljs:g,str:f.data("str"),edr:f.data("edr"),cindex:f.data("cindex"),stc:f.data("stc"),edc:f.data("edc")})}),s.optiongroups=u,d.jfundo.length=0,d.jfredo.push(s),$("#luckysheet-filter-selected-sheet"+d.currentSheetIndex+", #luckysheet-filter-options-sheet"+d.currentSheetIndex).remove(),$("#luckysheet-filter-menu, #luckysheet-filter-submenu").hide(),d.luckysheetfile[Q(d.currentSheetIndex)].filter=null,d.luckysheetfile[Q(d.currentSheetIndex)].filter_select=null,ae.saveParam("fsc",d.currentSheetIndex,null),d.luckysheetfile[Q(d.currentSheetIndex)].config=d.config,ae.saveParam("cg",d.currentSheetIndex,{},{k:"rowhidden"}),Mt(d.flowdata.length,d.flowdata[0].length)}),$("#luckysheet-filter-byvalue-input").on("input propertychange",function(){let s=$(this).val().toString();$("#luckysheet-filter-byvalue-select .ListBox .luckysheet-mousedown-cancel").show(),s!=""&&$("#luckysheet-filter-byvalue-select .ListBox input[type='checkbox']").each(function(u,f){if($(f).closest(".day").length>0){let A=$(f).siblings("label").text().toString(),h=$(f).closest(".monthBox").find(".month label").text().toString();($(f).closest(".yearBox").find(".year label").text().toString()+"-"+h+"-"+A).indexOf(s)==-1&&($(f).closest(".day").hide(),$(f).closest(".dayList").find(".day:visible").length==0&&$(f).closest(".monthBox").find(".month").hide(),$(f).closest(".monthList").find(".day:visible").length==0&&$(f).closest(".yearBox").find(".year").hide())}$(f).closest(".textBox").length>0&&$(f).siblings("label").text().toString().indexOf(s)==-1&&$(f).parents(".textBox").hide()})}),$("#luckysheet-filter-cancel").click(function(){$("#luckysheet-filter-menu, #luckysheet-filter-submenu").hide()}),$("#luckysheet-filter-confirm").click(function(){let s=$("#luckysheet-filter-menu"),u=s.data("str"),f=s.data("edr"),A=s.data("cindex"),h=s.data("stc"),g=s.data("edc"),p={};$("#luckysheet-filter-options-sheet"+d.currentSheetIndex+" .luckysheet-filter-options").not($("#luckysheet-filter-options-sheet"+d.currentSheetIndex+" .luckysheet-filter-options").eq(A-h).get(0)).each(function(){let T=$(this),R=T.data("rowhidden");if(R=="")return!0;R=JSON.parse(R.replace(/\'/g,'"'));for(let D in R)p[D]=0});let v={},y={},b={};if($("#luckysheet-filter-bycondition").next().is(":visible")&&$("#luckysheet-filter-byvalue").next().is(":hidden")&&$("#luckysheet-filter-selected span").data("value")!="null"){let T=$("#luckysheet-filter-selected span"),R=T.data("type"),D=T.data("value");if(b.value=D,b.text=T.text(),R=="0")b.type="0";else if(R=="2"){let S=$("#luckysheet-filter-menu .luckysheet-filter-selected-input2 input");b.type="2",b.value1=S.eq(0).val(),b.value2=S.eq(1).val()}else b.type="1",b.value1=$("#luckysheet-filter-menu .luckysheet-filter-selected-input").eq(0).find("input").val();for(let S=u+1;S<=f;S++){if(S in p||d.flowdata[S]==null)continue;let B=d.flowdata[S][A];if(D=="cellnull")B!=null&&!me(B.v)&&(y[S]=0);else if(D=="cellnonull")(B==null||me(B.v))&&(y[S]=0);else if(D=="textinclude"){let F=b.value1;(B==null||me(B.v)||B.m.indexOf(F)==-1)&&(y[S]=0)}else if(D=="textnotinclude"){let F=b.value1;B==null||me(B.v)||B.m.indexOf(F)>-1&&(y[S]=0)}else if(D=="textstart"){let F=b.value1,E=F.length;(B==null||me(B.v)||B.m.substr(0,E)!=F)&&(y[S]=0)}else if(D=="textend"){let F=b.value1,E=F.length;(B==null||me(B.v)||E>B.m.length||B.m.substr(B.m.length-E,E)!=F)&&(y[S]=0)}else if(D=="textequal"){let F=b.value1;(B==null||me(B.v)||B.m!=F)&&(y[S]=0)}else if(D=="dateequal"){let F=it(b.value1)[2];B==null||me(B.v)?y[S]=0:B.ct!=null&&B.ct.t=="d"?parseInt(B.v)!=F&&(y[S]=0):y[S]=0}else if(D=="datelessthan"){let F=it(b.value1)[2];B==null||me(B.v)?y[S]=0:B.ct!=null&&B.ct.t=="d"?parseInt(B.v)>=F&&(y[S]=0):y[S]=0}else if(D=="datemorethan"){let F=it(b.value1)[2];B==null||me(B.v)?y[S]=0:B.ct!=null&&B.ct.t=="d"?parseInt(B.v)<=F&&(y[S]=0):y[S]=0}else if(D=="morethan"){let F=parseFloat(b.value1);B==null||me(B.v)?y[S]=0:B.ct!=null&&B.ct.t=="n"?B.v<=F&&(y[S]=0):y[S]=0}else if(D=="moreequalthan"){let F=parseFloat(b.value1);B==null||me(B.v)?y[S]=0:B.ct!=null&&B.ct.t=="n"?B.v=F&&(y[S]=0):y[S]=0}else if(D=="lessequalthan"){let F=parseFloat(b.value1);B==null||me(B.v)?y[S]=0:B.ct!=null&&B.ct.t=="n"?B.v>F&&(y[S]=0):y[S]=0}else if(D=="equal"){let F=parseFloat(b.value1);B==null||me(B.v)?y[S]=0:B.ct!=null&&B.ct.t=="n"?B.v!=F&&(y[S]=0):y[S]=0}else if(D=="noequal"){let F=parseFloat(b.value1);B==null||me(B.v)?y[S]=0:B.ct!=null&&B.ct.t=="n"?B.v==F&&(y[S]=0):y[S]=0}else if(D=="include"){let F=parseFloat(b.value1),E=parseFloat(b.value2),z,P;FP)&&(y[S]=0):y[S]=0}else if(D=="noinclude"){let F=parseFloat(b.value1),E=parseFloat(b.value2),z,P;F=z&&B.v<=P&&(y[S]=0):y[S]=0}}}else{$("#luckysheet-filter-byvalue-select .ListBox input[type='checkbox']").each(function(T,R){if($(R).is(":visible")&&$(R).is(":checked"))return!0;if($(R).closest(".day").length>0){let D=$(R).siblings("label").text();Number(D)<10&&(D="0"+Number(D));let S=$(R).closest(".monthBox").find(".month label").text().replace(t.filiterMonthText,"");Number(S)<10&&(S="0"+Number(S));let B=$(R).closest(".yearBox").find(".year label").text().replace(t.filiterYearText,""),F=t.filterDateFormatTip+"#$$$#"+B+"-"+S+"-"+D;v[F]="1"}if($(R).closest(".textBox").length>0){let D=$(R).closest(".textBox").data("filter");v[D]="1"}});for(let T=u+1;T<=f;T++){if(T in p||d.flowdata[T]==null)continue;let R=d.flowdata[T][A],D;if(R==null||me(R.v))D="null#$$$#null";else if(R.ct!=null&&R.ct.t=="d"){let S=ht("YYYY-MM-DD",R.v);D=t.filterDateFormatTip+"#$$$#"+S}else D=R.v+"#$$$#"+R.m;D in v&&(y[T]=0)}}let k=$("#luckysheet-filter-options-sheet"+d.currentSheetIndex+" .luckysheet-filter-options").eq(A-h),w=$("#luckysheet-filter-byvalue-select .ListBox input[type='checkbox']:visible:checked").length<$("#luckysheet-filter-byvalue-select .ListBox input[type='checkbox']:visible").length||$("#luckysheet-filter-byvalue-input").val().length>0||$("#luckysheet-filter-bycondition").next().is(":visible")&&$("#luckysheet-filter-byvalue").next().is(":hidden")&&$("#luckysheet-filter-selected span").data("value")!="null",C=$.extend(!0,p,y),I=oa.parseJsonParm(k.data("rowhidden"));sa(k,w,y,b,!0,u,f,A,h,g);let x=$.extend(!0,{},d.config);if(x.rowhidden=C,d.clearjfundo){let T={};T.type="datachangeAll_filter",T.sheetIndex=d.currentSheetIndex,T.config=$.extend(!0,{},d.config),T.curconfig=x,T.optionstate=w,T.optionsindex=A-h,T.rowhidden=$.extend(!0,{},y),T.rowhidenPre=$.extend(!0,{},I),b!=null&&(T.caljs=b),d.jfundo.length=0,d.jfredo.push(T)}d.config=x,d.luckysheetfile[Q(d.currentSheetIndex)].config=d.config,ae.saveParam("cg",d.currentSheetIndex,x.rowhidden,{k:"rowhidden"}),Mt(d.flowdata.length,d.flowdata[0].length),$("#luckysheet-filter-menu, #luckysheet-filter-submenu").hide(),al()})}var zn=De(()=>{wt();tl();Pt();rl();wr();el();Ba();Wt();$t();yt();je();ll();Ar();Pa();Bl();dt();$r();_t();za();ui();Fl()});var jl,ca=De(()=>{en();je();hr();Bl();cl();jl={imageCache:new Set,addCellImage:function(e,n){if(!e)return;let{column:t,row:l}=d.luckysheet_select_save[d.luckysheet_select_save.length-1],a={row:[l[0],l[0]],column:[t[0],t[0]]},o=di(a);e.inCell=o;let s=`=DISPIMG('${n}',1)`;no(l[0],t[0]),Nn(l[0],t[0],s)},imageUpload:function(e){let n=$('');$("body").append(n),n.on("change",function(t){if(!Rt(d.currentSheetIndex,"editObjects",!1)||t.currentTarget.files==null||t.currentTarget.files.length==0)return;let l=t.currentTarget.files[0];if(l.type.indexOf("image/")!==0){tooltip.info("\u4EC5\u652F\u6301\u56FE\u7247\u6587\u4EF6","");return}Se.insertImg(l,e),n.remove()}),n.click()},inCellImageToFloatImage:function(){let{column:e,row:n}=d.luckysheet_select_save[d.luckysheet_select_save.length-1],t=n[0],l=e[0],a=d.flowdata[t][l];if(!a||!a.f||!a.f.startsWith("=DISPIMG"))return;let{imgID:o}=this.getImageIDAndMode(a.f),u=Se.images[o];!u||(u.inCell="",Se.ref(),Se.allImagesShow(),no(t,l))},floatImageToInCellImage:function(e){if(!e)return;let t=Se.images[e];if(!t)return;let{column:l,row:a}=d.luckysheet_select_save[d.luckysheet_select_save.length-1],o={row:[a[0],a[0]],column:[l[0],l[0]]},s=di(o);t.inCell=s,Se.ref(),Se.allImagesShow();let u=a[0],f=l[0],A=`=DISPIMG('${e}',1)`;no(u,f),Nn(u,f,A)},previewCellImage:function(e,n){let t=d.flowdata[e][n];if(!t)return!1;let{imgID:l,mode:a}=this.getImageIDAndMode(t.f),o=ve.getSheetByIndex(),u=((o==null?void 0:o.images)||[])[l];if(!u)return;let{src:f}=u,A=this.getImageSrc(f);this.createPreviewModal(A,l)},createPreviewModal:function(e,n){$("#luckysheet-modal-dialog-cell-image-preview").remove();let t=` -
-
- \u56FE\u7247\u9884\u89C8 - - - -
-
-
- \u9884\u89C8\u56FE\u7247 -
-
-
- `;$("body").append(t),$("#luckysheet-modal-dialog-mask").show();let l=$("#luckysheet-modal-dialog-cell-image-preview"),a=l.outerWidth(),o=l.outerHeight(),s=$(window).width(),u=$(window).height(),f=$(document).scrollLeft(),A=$(document).scrollTop();l.css({left:(s+f-a)/2,top:(u+A-o)/2}).show(),setTimeout(()=>{let g=l.find(".image-container").find("img");g.one("load",()=>{let p=document.createElement("canvas"),v=p.getContext("2d"),y=g.width(),b=g.height();p.width=y,p.height=b;let k=g.get(0);v.drawImage(k,0,0,y,b),this.drawWatermark(v,y,b),g.attr("src",p.toDataURL("image/png"))}),g.get(0).complete&&g.trigger("load")},10),l.find(".luckysheet-modal-dialog-title-close").on("click",function(){$("#luckysheet-modal-dialog-cell-image-preview").remove(),$("#luckysheet-modal-dialog-mask").hide()}),$("#luckysheet-modal-dialog-mask").off("click.cellImagePreview").on("click.cellImagePreview",function(){$("#luckysheet-modal-dialog-cell-image-preview").remove(),$("#luckysheet-modal-dialog-mask").hide(),$(this).off("click.cellImagePreview")})},isCellImage(e,n){let t=d.flowdata[e][n];return!t||!t.f?!1:t.f.includes("=DISPIMG")},getImageIDAndMode(e){let n="",t="",l=/=*DISPIMG\(['"]([^'"]+)['"],(\d+)\)/,a=e.match(l);return a&&(n=a[1],t=a[2]),{imgID:n,mode:t}},luckysheetDrawCellImage(e,n,t,l,a,o,s,u,f,A){if(d.flowdata==null)return;a==null&&(a=$("#luckysheet-cell-main").scrollLeft()),o==null&&(o=$("#luckysheet-cell-main").scrollTop()),s==null&&(s=d.luckysheetTableContentHW[0]),u==null&&(u=d.luckysheetTableContentHW[1]),f==null&&(f=d.rowHeaderWidth),A==null&&(A=d.columnHeaderHeight);let h=ve.getSheetByIndex(),p=((h==null?void 0:h.images)||[])[t];if(!p)return;let{src:v}=p;if(this.imageCache||(this.imageCache=new Set),this.imageCache[t]&&this.imageCache[t].complete){this.drawImageToCanvas(e,this.imageCache[t],n,f,A);return}let y=new Image;y.src=this.getImageSrc(v),this.imageCache[t]=y,y.onload=()=>{e.save(),e.scale(d.devicePixelRatio,d.devicePixelRatio),this.drawImageToCanvas(e,y,n,f,A),e.restore()},y.onerror=()=>{console.warn("\u56FE\u7247\u52A0\u8F7D\u5931\u8D25:",v),this.drawPlaceholder(e,n,f,A)}},pasteCellImageHandler(e){if(!e||!e.includes("=DISPIMG"))return e;let{imgID:n}=this.getImageIDAndMode(e),t=Se.generateRandomId();e=e.replace(n,t);let l=Se.images[n];return Se.images[t]=$.extend(!0,{},l),Se.ref(),e},drawImageToCanvas(e,n,t,l,a){let o=t.r,s=t.c,u=t.start_r,f=t.start_c,A=t.end_r,g=t.end_c-f-2,p=A-u-2,v=g*.9,y=p*.9,b=(g-v)/2+f+l,k=(p-y)/2+u+a;e.drawImage(n,b,k,v,y)},drawPlaceholder(e,n,t,l){let a=n.r,o=n.c,s=n.start_r,u=n.start_c,f=n.end_r,h=n.end_c-u-2,g=f-s-2,p=u+t,v=s+l;e.save(),e.fillStyle="#f0f0f0",e.fillRect(p,v,h,g),e.strokeStyle="#ccc",e.lineWidth=1,e.strokeRect(p,v,h,g),e.fillStyle="#999",e.font="12px Arial",e.textAlign="center",e.textBaseline="middle",e.fillText("\u56FE\u7247\u52A0\u8F7D\u5931\u8D25",p+h/2,v+g/2),e.restore()},clearImageCache:function(){this.imageCache=new Set},getImageSrc:function(e){let n=d.toJsonOptions&&d.toJsonOptions.imageUrlHandle;return typeof n=="function"?n(e):e},drawWatermark(e,n,t){let l={text:"pushu",font:"bold 20px Arial",color:"rgba(0, 0, 0, 0.1)",rotate:-Math.PI/4,spacing:100};e.save(),e.fillStyle=l.color,e.font=l.font;let a=l.text,o=e.measureText(a).width,s=20,u=Math.sqrt(n*n+t*t),f=l.spacing||Math.max(o,s)*1.5;e.translate(n/2,t/2),e.rotate(l.rotate||-Math.PI/4);let A=-u,h=u;for(let g=A;g{El();$t();Nr();Wt();Bl();wt();Wr();dt();je();yt();rl();Xl();en();_t();ca();_c={imgItem:{type:"3",src:"",inCell:"",originWidth:null,originHeight:null,default:{width:null,height:null,left:null,top:null},crop:{width:null,height:null,offsetLeft:0,offsetTop:0},isFixedPos:!1,fixedLeft:null,fixedTop:null,border:{width:0,radius:0,style:"solid",color:"#000"}},images:null,currentImgId:null,currentWinW:null,currentWinH:null,resize:null,resizeXY:null,move:!1,moveXY:null,cropChange:null,cropChangeXY:null,cropChangeObj:null,copyImgItemObj:null,insertImg:function(e,n){let t=d.toJsonOptions&&d.toJsonOptions.uploadImage;if(typeof t=="function")t(e).then(l=>{_c._insertImg(l,n)}).catch(l=>{console.log(" ==> ",l),U.info('',"\u56FE\u7247\u4E0A\u4F20\u5931\u8D25")});else{let l=new FileReader;l.readAsDataURL(e),l.onload=function(a){let o=a.target.result;_c._insertImg(o,n)}}},_insertImg:function(e,n){let t=this,l=d.luckysheet_select_save[d.luckysheet_select_save.length-1],a=l.row_focus||0,o=l.column_focus||0,s=o==0?0:d.visibledatacolumn[o-1],u=a==0?0:d.visibledatarow[a-1],f=new Image;f.onload=function(){let h=f.width,g=f.height,p={src:e,left:s,top:u,originWidth:h,originHeight:g};t.addImgItem(p,n)};let A=d.toJsonOptions&&d.toJsonOptions.imageUrlHandle;f.src=typeof A=="function"?A(e):e},generateRandomId:function(e){e==null&&(e="img");let n=window.navigator.userAgent.replace(/[^a-zA-Z0-9]/g,"").split(""),t="";for(let a=0;a<12;a++)t+=n[Math.round(Math.random()*(n.length-1))];let l=new Date().getTime();return e+"_"+t+"_"+l},modelHtml:function(e,n){let t=this,l=d.toJsonOptions&&d.toJsonOptions.imageUrlHandle,a=typeof l=="function"?l(n.src):n.src,o=t.getImgItemParam(n),s=o.width*d.zoomRatio,u=o.height*d.zoomRatio,f=o.left*d.zoomRatio,A=o.top*d.zoomRatio,h=o.position,g=n.border.width;return`
-
- -
-
-
`},getSliderHtml:function(){let e=te().imageText;return`
-
- ${e.imageSetting} - - - -
-
-
-
${e.conventional}
-
-
- - -
-
- - -
-
- - -
-
-
- - -
-
-
-
${e.border}
-
-
- - -
-
- - -
-
- - -
-
- -
- -
-
-
-
-
-
${e.convert}
-
-
- -
-
-
-
-
- `},sliderHtmlShow:function(){let e=this;$("#luckysheet-modal-dialog-slider-imageCtrl").remove();let n=e.getSliderHtml();$("body").append(n),Lt();let t=e.images[e.currentImgId],l=t.type;$("#luckysheet-modal-dialog-slider-imageCtrl #imgItemType"+l).prop("checked",!0);let a=t.isFixedPos;$("#luckysheet-modal-dialog-slider-imageCtrl #imgItemIsFixedPos").prop("checked",a);let o=t.border;$("#luckysheet-modal-dialog-slider-imageCtrl #imgItemBorderWidth").val(o.width),$("#luckysheet-modal-dialog-slider-imageCtrl #imgItemBorderRadius").val(o.radius),$("#luckysheet-modal-dialog-slider-imageCtrl #imgItemBorderStyle").val(o.style),$("#luckysheet-modal-dialog-slider-imageCtrl #imgItemBorderColor span").css("background-color",o.color).attr("title",o.color),e.init()},colorSelectDialog:function(e){let n=te(),t=n.button,l=n.toolbar,a=n.imageCtrl;$("#luckysheet-modal-dialog-mask").show(),$("#luckysheet-imageCtrl-colorSelect-dialog").remove(),$("body").append(we(At,{id:"luckysheet-imageCtrl-colorSelect-dialog",addclass:"luckysheet-imageCtrl-colorSelect-dialog",title:a.borderTile,content:`
- ${a.borderCur}: -
-
`,botton:` - `,style:"z-index:100003"}));let o=$("#luckysheet-imageCtrl-colorSelect-dialog").find(".luckysheet-modal-dialog-content").css("min-width",300).end(),s=o.outerHeight(),u=o.outerWidth(),f=$(window).width(),A=$(window).height(),h=$(document).scrollLeft(),g=$(document).scrollTop();$("#luckysheet-imageCtrl-colorSelect-dialog").css({left:(f+h-u)/2,top:(A+g-s)/3}).show(),$("#luckysheet-imageCtrl-colorSelect-dialog").find(".colorshowbox").spectrum({showPalette:!0,showPaletteOnly:!0,preferredFormat:"hex",clickoutFiresChange:!1,showInitial:!0,showInput:!0,flat:!0,hideAfterPaletteSelect:!0,showSelectionPalette:!0,showButtons:!1,maxPaletteSize:8,maxSelectionSize:8,color:e,cancelText:t.cancel,chooseText:l.confirmColor,togglePaletteMoreText:l.customColor,togglePaletteLessText:l.collapse,togglePaletteOnly:!0,clearText:l.clearText,noColorSelectedText:l.noColorSelectedText,localStorageKey:"spectrum.textcolor"+ae.gridKey,palette:[["#000","#444","#666","#999","#ccc","#eee","#f3f3f3","#fff"],["#f00","#f90","#ff0","#0f0","#0ff","#00f","#90f","#f0f"],["#f4cccc","#fce5cd","#fff2cc","#d9ead3","#d0e0e3","#cfe2f3","#d9d2e9","#ead1dc"],["#ea9999","#f9cb9c","#ffe599","#b6d7a8","#a2c4c9","#9fc5e8","#b4a7d6","#d5a6bd"],["#e06666","#f6b26b","#ffd966","#93c47d","#76a5af","#6fa8dc","#8e7cc3","#c27ba0"],["#c00","#e69138","#f1c232","#6aa84f","#45818e","#3d85c6","#674ea7","#a64d79"],["#900","#b45f06","#bf9000","#38761d","#134f5c","#0b5394","#351c75","#741b47"],["#600","#783f04","#7f6000","#274e13","#0c343d","#073763","#20124d","#4c1130"]],move:function(p){p!=null?p=p.toHexString():p="#000",$("#luckysheet-imageCtrl-colorSelect-dialog .currenColor span").css("background-color",p).attr("title",p)}})},init:function(){let e=this;$("#luckysheet-modal-dialog-slider-imageCtrl .luckysheet-model-close-btn").click(function(){$("#luckysheet-modal-dialog-slider-imageCtrl").hide(),Lt()}),$("#luckysheet-modal-dialog-slider-imageCtrl").off("change.radio").on("change.radio",".radio-item input[type=radio][name=imgItemType]",function(){e.configChange("type",this.value)}),$("#luckysheet-modal-dialog-slider-imageCtrl").off("change.checkbox").on("change.checkbox",".slider-box-checkbox input[type=checkbox]",function(){e.configChange("fixedPos",this.checked)}),$("#luckysheet-modal-dialog-slider-imageCtrl").off("change.borderWidth").on("change.borderWidth","#imgItemBorderWidth",function(){e.configChange("border-width",this.valueAsNumber)}),$("#luckysheet-modal-dialog-slider-imageCtrl").off("change.borderRadius").on("change.borderRadius","#imgItemBorderRadius",function(){e.configChange("border-radius",this.valueAsNumber)}),$("#luckysheet-modal-dialog-slider-imageCtrl").off("change.borderStyle").on("change.borderStyle","#imgItemBorderStyle",function(){e.configChange("border-style",this.value)}),$("#luckysheet-modal-dialog-slider-imageCtrl").off("click.color").on("click.color","#imgItemBorderColor",function(){let n=$(this).find("span").attr("title");e.colorSelectDialog(n)}),$(document).off("click.selectColorConfirm").on("click.selectColorConfirm","#luckysheet-imageCtrl-colorSelect-dialog-confirm",function(){let n=$(this).parents("#luckysheet-imageCtrl-colorSelect-dialog");$("#luckysheet-modal-dialog-mask").hide(),n.hide();let t=n.find(".currenColor span").attr("title");$("#luckysheet-modal-dialog-slider-imageCtrl #imgItemBorderColor span").css("background-color",t).attr("title",t),e.configChange("border-color",t)}),$("#luckysheet-modal-dialog-slider-imageCtrl button#convertToInCellImage").click(function(){let n=e.currentImgId;jl.floatImageToInCellImage(n),e.cancelActiveImgItem(),$("#luckysheet-modal-dialog-slider-imageCtrl").hide(),Lt()}),$("#luckysheet-image-showBoxs").off("mousedown.active").on("mousedown.active",".luckysheet-modal-dialog-image",function(n){if(!Rt(d.currentSheetIndex,"editObjects",!1))return;$(this).hide();let t=$(this).attr("id");e.currentImgId!=null&&e.currentImgId!=t&&e.cancelActiveImgItem(),e.currentImgId=t;let l=e.images[t],a=e.getImgItemParam(l),o=a.width*d.zoomRatio,s=a.height*d.zoomRatio,u=a.left*d.zoomRatio,f=a.top*d.zoomRatio,A=a.position;$("#luckysheet-modal-dialog-activeImage").show().css({width:o,height:s,left:u,top:f,position:A});let h=d.toJsonOptions&&d.toJsonOptions.imageUrlHandle,g=typeof h=="function"?h(l.src):l.src;$("#luckysheet-modal-dialog-activeImage .luckysheet-modal-dialog-content").css({"background-image":"url("+g+")","background-size":l.default.width*d.zoomRatio+"px "+l.default.height*d.zoomRatio+"px","background-position":-l.crop.offsetLeft*d.zoomRatio+"px "+-l.crop.offsetTop*d.zoomRatio+"px"}),$("#luckysheet-modal-dialog-activeImage .luckysheet-modal-dialog-border").css({"border-width":l.border.width*d.zoomRatio,"border-style":l.border.style,"border-color":l.border.color,"border-radius":l.border.radius*d.zoomRatio,left:-l.border.width*d.zoomRatio,right:-l.border.width*d.zoomRatio,top:-l.border.width*d.zoomRatio,bottom:-l.border.width*d.zoomRatio}),n.stopPropagation()}),$("#luckysheet-modal-dialog-activeImage").off("mousedown.move").on("mousedown.move",".luckysheet-modal-dialog-content",function(n){if(!Rt(d.currentSheetIndex,"editObjects",!1))return;!$("#luckysheet-modal-dialog-slider-imageCtrl").is(":visible"),e.move=!0,e.currentWinW=$("#luckysheet-cell-main")[0].scrollWidth,e.currentWinH=$("#luckysheet-cell-main")[0].scrollHeight;let t=$("#luckysheet-modal-dialog-activeImage").offset();e.moveXY=[n.pageX-t.left,n.pageY-t.top],Rl(!0),n.stopPropagation()}),$("#luckysheet-modal-dialog-activeImage").off("mousedown.setting").on("mousedown.setting",".luckysheet-modal-controll-setting",function(n){e.sliderHtmlShow(),n.stopPropagation()}),$("#luckysheet-modal-dialog-activeImage").off("mousedown.resize").on("mousedown.resize",".luckysheet-modal-dialog-resize-item",function(n){if(!Rt(d.currentSheetIndex,"editObjects",!1))return;e.currentWinW=$("#luckysheet-cell-main")[0].scrollWidth,e.currentWinH=$("#luckysheet-cell-main")[0].scrollHeight,e.resize=$(this).data("type");let t=$("#luckysheet-cell-main").scrollTop(),l=$("#luckysheet-cell-main").scrollLeft(),a=Ke(n.pageX,n.pageY),o=a[0]+l,s=a[1]+t,u=$("#luckysheet-modal-dialog-activeImage").position(),f=$("#luckysheet-modal-dialog-activeImage").width(),A=$("#luckysheet-modal-dialog-activeImage").height();e.resizeXY=[o,s,f,A,u.left+l,u.top+t,l,t],Rl(!0),n.stopPropagation()}),$("#luckysheet-modal-dialog-activeImage").off("mousedown.croppingEnter").on("mousedown.croppingEnter",".luckysheet-modal-controll-crop",function(n){e.croppingEnter(),n.stopPropagation()}),$("#luckysheet-modal-dialog-cropping").off("mousedown.croppingExit").on("mousedown.croppingExit",".luckysheet-modal-controll-crop",function(n){e.croppingExit(),n.stopPropagation()}),$("#luckysheet-modal-dialog-cropping").off("mousedown.cropChange").on("mousedown.cropChange",".resize-item",function(n){e.cropChange=$(this).data("type");let t=$("#luckysheet-cell-main").scrollTop(),l=$("#luckysheet-cell-main").scrollLeft(),a=Ke(n.pageX,n.pageY),o=a[0]+l,s=a[1]+t;e.cropChangeXY=[o,s],Rl(!0),n.stopPropagation()}),$("#luckysheet-image-showBoxs").off("mousedown.restore").on("mousedown.restore",".luckysheet-modal-controll-restore",function(n){e.restoreImgItem(),n.stopPropagation()}),$("#luckysheet-image-showBoxs").off("mousedown.delete").on("mousedown.delete",".luckysheet-modal-controll-del",function(n){e.removeImgItem(),n.stopPropagation()})},configChange:function(e,n){let t=this,l=t.images[t.currentImgId];switch(e){case"type":l.type=n;break;case"fixedPos":l.isFixedPos=n;let a=t.getImgItemParam(l),o=a.width,s=a.height,u=a.left,f=a.top,A=a.position;$("#luckysheet-modal-dialog-activeImage").show().css({width:o,height:s,left:u,top:f,position:A});break;case"border-width":l.border.width=n,$("#luckysheet-modal-dialog-activeImage .luckysheet-modal-dialog-border").css({"border-width":n,left:-n,right:-n,top:-n,bottom:-n});break;case"border-radius":l.border.radius=n,$("#luckysheet-modal-dialog-activeImage .luckysheet-modal-dialog-border").css("border-radius",n);break;case"border-style":l.border.style=n,$("#luckysheet-modal-dialog-activeImage .luckysheet-modal-dialog-border").css("border-style",n);break;case"border-color":l.border.color=n,$("#luckysheet-modal-dialog-activeImage .luckysheet-modal-dialog-border").css("border-color",n);break}t.ref()},getImgItemParam(e){let n=e.isFixedPos,t=e.default.width,l=e.default.height,a=e.default.left,o=e.default.top;(e.crop.width!=t||e.crop.height!=l)&&(t=e.crop.width,l=e.crop.height,a+=e.crop.offsetLeft,o+=e.crop.offsetTop);let s="absolute";return n&&(s="fixed",a=e.fixedLeft+e.crop.offsetLeft,o=e.fixedTop+e.crop.offsetTop),{width:t,height:l,left:a,top:o,position:s}},cancelActiveImgItem:function(){let e=this;$("#luckysheet-modal-dialog-activeImage").hide(),$("#luckysheet-modal-dialog-cropping").hide(),$("#luckysheet-modal-dialog-slider-imageCtrl").hide();let n=e.images[e.currentImgId],t=e.getImgItemParam(n),l=t.width*d.zoomRatio,a=t.height*d.zoomRatio,o=t.left*d.zoomRatio,s=t.top*d.zoomRatio,u=t.position;$("#"+e.currentImgId).show().css({width:l,height:a,left:o,top:s,position:u}),$("#"+e.currentImgId+" img").css({width:n.default.width*d.zoomRatio,height:n.default.height*d.zoomRatio,left:-n.crop.offsetLeft*d.zoomRatio,top:-n.crop.offsetTop*d.zoomRatio}),$("#"+e.currentImgId+" .luckysheet-modal-dialog-border").css({"border-width":n.border.width*d.zoomRatio,"border-style":n.border.style,"border-color":n.border.color,"border-radius":n.border.radius*d.zoomRatio,left:-n.border.width*d.zoomRatio,right:-n.border.width*d.zoomRatio,top:-n.border.width*d.zoomRatio,bottom:-n.border.width*d.zoomRatio}),e.currentImgId=null},addImgItem:function(e,n){let t=this,l,a,o=400;e.originHeight ",d.imageMode);let A=e.modelHtml(f,o);$("#luckysheet-image-showBoxs .img-list").append(A),e.images[f]=o,e.ref(),e.init()},allImagesShow:function(){let e=this;if($("#luckysheet-modal-dialog-activeImage").hide(),$("#luckysheet-modal-dialog-cropping").hide(),$("#luckysheet-modal-dialog-slider-imageCtrl").hide(),$("#luckysheet-image-showBoxs .img-list").empty(),e.images!=null)for(let n in e.images){let t=e.images[n];if(t.inCell)continue;let l=e.modelHtml(n,t);$("#luckysheet-image-showBoxs .img-list").append(l)}},moveChangeSize:function(e,n,t){let l=this,a=$.extend(!0,{},l.images);if(e=="row"){let o=d.visibledatarow[n],s=n-1==-1?0:d.visibledatarow[n-1],u=t-(o-s-1);for(let f in a){let A=a[f],h=l.getImgItemParam(A),g=A.type;if(g=="1"){if(h.top>=o)A.default.top=h.top+u-A.crop.offsetTop;else if(h.top+h.height>=o-2)if(h.topo+u){let p=1/h.height;A.default.height=Math.round(A.default.height*p),A.crop.height=Math.round(A.crop.height*p),A.crop.offsetTop=Math.round(A.crop.offsetTop*p),A.default.top=o+u-A.crop.offsetTop}else if(h.top+h.height>o+u){let p=(o+u-h.top)/h.height;A.default.height=Math.round(A.default.height*p),A.crop.height=Math.round(A.crop.height*p),A.crop.offsetTop=Math.round(A.crop.offsetTop*p)}}else g=="2"&&(h.top>=o?A.default.top=h.top+u-A.crop.offsetTop:h.top>o+u&&(A.default.top=o+u-A.crop.offsetTop))}}else if(e=="column"){let o=d.visibledatacolumn[n],s=n-1==-1?0:d.visibledatacolumn[n-1],u=t-(o-s-1);for(let f in a){let A=a[f],h=l.getImgItemParam(A),g=A.type;if(g=="1"){if(h.left>=o)A.default.left=h.left+u-A.crop.offsetLeft;else if(h.left+h.width>=o-2)if(h.lefto+u){let p=1/h.width;A.default.width=Math.round(A.default.width*p),A.crop.width=Math.round(A.crop.width*p),A.crop.offsetLeft=Math.round(A.crop.offsetLeft*p),A.default.left=o+u-A.crop.offsetLeft}else if(h.left+h.width>o+u){let p=(o+u-h.left)/h.width;A.default.width=Math.round(A.default.width*p),A.crop.width=Math.round(A.crop.width*p),A.crop.offsetLeft=Math.round(A.crop.offsetLeft*p)}}else g=="2"&&(h.left>=o?A.default.left=h.left+u-A.crop.offsetLeft:h.left>o+u&&(A.default.left=o+u-A.crop.offsetLeft))}}return a},ref:function(){let e=this,n=d.luckysheetfile[Q(d.currentSheetIndex)],t=e.images;d.clearjfundo&&(d.jfundo.length=0,d.jfredo.push({type:"imageCtrl",sheetIndex:d.currentSheetIndex,images:n.images==null?null:$.extend(!0,{},n.images),curImages:t})),n.images=$.extend(!0,{},t),ae.saveParam("all",d.currentSheetIndex,n.images,{k:"images"})}},Se=_c});function yy(e){var n=/^(^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$)|(^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])((\d{4})|\d{3}[Xx])$)$/;if(n.test(e)){if(e.length==18){for(var t=[7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2],l=[1,0,10,9,8,7,6,5,4,3,2],a=0,o=0;o<17;o++)a+=e.substring(o,o+1)*t[o];var s=a%11,u=e.substring(17);return s==2?u=="X"||u=="x":u==l[s]}}else return!1}var vy,Qe,mr=De(()=>{dt();jt();Pt();Kr();_t();rl();zr();Qt();tl();Wt();el();$t();ll();cl();wt();yt();je();vy={defaultItem:{type:"dropdown",type2:null,value1:"",value2:"",checked:!1,remote:!1,prohibitInput:!1,hintShow:!1,hintText:""},curItem:null,dataVerification:null,selectRange:[],selectStatus:!1,optionLabel:{number:"\u6570\u503C",number_integer:"\u6574\u6570",number_decimal:"\u5C0F\u6570",bw:"\u4ECB\u4E8E",nb:"\u4E0D\u4ECB\u4E8E",eq:"\u7B49\u4E8E",ne:"\u4E0D\u7B49\u4E8E",gt:"\u5927\u4E8E",lt:"\u5C0F\u4E8E",gte:"\u5927\u4E8E\u7B49\u4E8E",lte:"\u5C0F\u4E8E\u7B49\u4E8E",include:"\u5305\u62EC",exclude:"\u4E0D\u5305\u62EC",equal:"\u7B49\u4E8E",bf:"\u65E9\u4E8E",nbf:"\u4E0D\u65E9\u4E8E",af:"\u665A\u4E8E",naf:"\u4E0D\u665A\u4E8E",card:"\u8EAB\u4EFD\u8BC1\u53F7\u7801",phone:"\u624B\u673A\u53F7"},optionLabel_en:{number:"numeric",number_integer:"integer",number_decimal:"decimal",bw:"between",nb:"not between",eq:"equal to",ne:"not equal to",gt:"greater",lt:"less than",gte:"greater or equal to",lte:"less than or equal to",include:"include",exclude:"not include",equal:"equal to",bf:"earlier than",nbf:"not earlier than",af:"later than",naf:"not later than",card:"identification number",phone:"phone number"},createDialog:function(){let e=this,n=te(),t=n.dataVerification,l=n.toolbar,a=n.button;$("#luckysheet-modal-dialog-mask").show(),$("#luckysheet-dataVerification-dialog").remove();let o=`
-
-
${t.cellRange}
-
- - -
-
-
-
${t.verificationCondition}
- -
-
-
- - -
-
- - -
-
-
-
- ${t.selected} \u2014\u2014 - -
-
- ${t.notSelected} \u2014\u2014 - -
-
-
- -
- - - - -
-
- -
-
-
- -
- -
-
-
- -
- - - - -
-
- -
-
-
- -
- - - - -
-
- -
-
-
- -
-
-
-
-
- - -
-
- - -
-
- - -
-
- -
-
-
`;$("body").append(we(At,{id:"luckysheet-dataVerification-dialog",addclass:"luckysheet-dataVerification-dialog",title:l.dataVerification,content:o,botton:` - - `,style:"z-index:100003"}));let s=$("#luckysheet-dataVerification-dialog").find(".luckysheet-modal-dialog-content").css("min-width",350).end(),u=s.outerHeight(),f=s.outerWidth(),A=$(window).width(),h=$(window).height(),g=$(document).scrollLeft(),p=$(document).scrollTop();$("#luckysheet-dataVerification-dialog").css({left:(A+g-f)/2,top:(h+p-u)/3}).show(),e.dataAllocation()},init:function(){let e=this,n=te().dataVerification;$(document).off("click.dropdownBtn").on("click.dropdownBtn","#luckysheet-dataVerification-dropdown-btn",function(t){e.dropdownListShow(),t.stopPropagation()}),$(document).off("click.dropdownListItem").on("click.dropdownListItem","#luckysheet-dataVerification-dropdown-List .dropdown-List-item",function(t){var l=$(this);let a=t.target.innerText;l.hasClass("multi")?(l.toggleClass("checked"),a=$.map($("#luckysheet-dataVerification-dropdown-List").children().filter(".checked"),function(f){return f.innerText}).join(",")):$("#luckysheet-dataVerification-dropdown-List").hide();let o=d.luckysheet_select_save[d.luckysheet_select_save.length-1],s=o.row_focus,u=o.column_focus;$("#luckysheet-rich-text-editor").text(a),m.updatecell(s,u),t.stopPropagation()}),$(document).off("click.dvRange").on("click.dvRange","#data-verification-range .fa-table",function(t){$("#luckysheet-dataVerification-dialog").hide();let l="0",a=$(this).siblings("input").val().trim();e.rangeDialog(l,a),e.selectRange=[];let o=e.getRangeByTxt(a);if(m.rangetosheet=d.currentSheetIndex,o[0].sheetIndex!=d.currentSheetIndex&&ve.changeSheetExec(o[0].sheetIndex),o.length>0)for(let s=0;s0)for(let s=0;s1){U.info('',"\u4E0D\u80FD\u5BF9\u591A\u91CD\u9009\u62E9\u533A\u57DF\u6267\u884C\u6B64\u64CD\u4F5C\uFF0C\u8BF7\u9009\u62E9\u5355\u4E2A\u533A\u57DF\uFF0C\u7136\u540E\u518D\u8BD5");return}l=="0"?$("#luckysheet-dataVerification-dialog #data-verification-range input").val(a):l=="1"&&$("#luckysheet-dataVerification-dialog .show-box-item-dropdown .range input").val(a),$("#luckysheet-dataVerificationRange-dialog").hide(),$("#luckysheet-modal-dialog-mask").show(),$("#luckysheet-dataVerification-dialog").show(),m.rangetosheet!=null&&m.rangetosheet!=d.currentSheetIndex&&(ve.changeSheetExec(m.rangetosheet),m.rangetosheet=null),Et([])}),$(document).off("click.dvRangeClose").on("click.dvRangeClose","#luckysheet-dataVerificationRange-dialog-close",function(t){$("#luckysheet-dataVerificationRange-dialog").hide(),$("#luckysheet-modal-dialog-mask").show(),$("#luckysheet-dataVerification-dialog").show(),m.rangetosheet!=null&&m.rangetosheet!=d.currentSheetIndex&&(ve.changeSheetExec(m.rangetosheet),m.rangetosheet=null),Et([])}),$(document).on("click","#luckysheet-dataVerificationRange-dialog .luckysheet-modal-dialog-title-close",function(t){$("#luckysheet-dataVerificationRange-dialog").hide(),$("#luckysheet-modal-dialog-mask").show(),$("#luckysheet-dataVerification-dialog").show(),m.rangetosheet!=null&&m.rangetosheet!=d.currentSheetIndex&&(ve.changeSheetExec(m.rangetosheet),m.rangetosheet=null),Et([])}),$(document).off("change.typeSelect").on("change.typeSelect","#data-verification-type-select",function(t){$("#luckysheet-dataVerification-dialog .show-box .show-box-item").hide();let l=this.value,a=e.curItem;if(l=="dropdown"){$("#luckysheet-dataVerification-dialog .show-box .show-box-item-dropdown").show();let o="";l==a.type&&(o=a.value1),$("#luckysheet-dataVerification-dialog .show-box-item-dropdown .data-verification-value1").val(o),$("#luckysheet-dataVerification-dialog #data-verification-multi").prop("checked",!!a.type2)}else if(l=="checkbox"){$("#luckysheet-dataVerification-dialog .show-box .show-box-item-checkbox").show();let o="",s="";l==a.type&&(o=a.value1,s=a.value2),$("#luckysheet-dataVerification-dialog .show-box-item-checkbox .data-verification-value1").val(o),$("#luckysheet-dataVerification-dialog .show-box-item-checkbox .data-verification-value2").val(s)}else if(l=="number"||l=="number_integer"||l=="number_decimal"){$("#luckysheet-dataVerification-dialog .show-box-item-number").show(),$("#luckysheet-dataVerification-dialog .show-box-item-number .input").hide();let o="bw",s="",u="";(a.type=="number"||a.type=="number_integer"||a.type=="number_decimal")&&(o=a.type2,s=a.value1,u=a.value2),$("#luckysheet-dataVerification-dialog #data-verification-number-select").val(o),o=="bw"||o=="nb"?$("#luckysheet-dataVerification-dialog .show-box-item-number .input1").show():$("#luckysheet-dataVerification-dialog .show-box-item-number .input2").show(),$("#luckysheet-dataVerification-dialog .show-box-item-number .data-verification-value1").val(s),$("#luckysheet-dataVerification-dialog .show-box-item-number .data-verification-value2").val(u)}else if(l=="text_content"){$("#luckysheet-dataVerification-dialog .show-box-item-text").show();let o="include",s="";l==a.type&&(o=a.type2,s=a.value1),$("#luckysheet-dataVerification-dialog #data-verification-text-select").val(o),$("#luckysheet-dataVerification-dialog .show-box-item-text .data-verification-value1").val(s)}else if(l=="text_length"){$("#luckysheet-dataVerification-dialog .show-box-item-textLength").show(),$("#luckysheet-dataVerification-dialog .show-box-item-textLength .input").hide();let o="bw",s="",u="";l==a.type&&(o=a.type2,s=a.value1,u=a.value2),$("#luckysheet-dataVerification-dialog #data-verification-textLength-select").val(o),o=="bw"||o=="nb"?$("#luckysheet-dataVerification-dialog .show-box-item-textLength .input1").show():$("#luckysheet-dataVerification-dialog .show-box-item-textLength .input2").show(),$("#luckysheet-dataVerification-dialog .show-box-item-textLength .data-verification-value1").val(s),$("#luckysheet-dataVerification-dialog .show-box-item-textLength .data-verification-value2").val(u)}else if(l=="date"){$("#luckysheet-dataVerification-dialog .show-box-item-date").show(),$("#luckysheet-dataVerification-dialog .show-box-item-date .input").hide();let o="bw",s="",u="";l==a.type&&(o=a.type2,s=a.value1,u=a.value2),$("#luckysheet-dataVerification-dialog #data-verification-date-select").val(o),o=="bw"||o=="nb"?$("#luckysheet-dataVerification-dialog .show-box-item-date .input1").show():$("#luckysheet-dataVerification-dialog .show-box-item-date .input2").show(),$("#luckysheet-dataVerification-dialog .show-box-item-date .data-verification-value1").val(s),$("#luckysheet-dataVerification-dialog .show-box-item-date .data-verification-value2").val(u)}else if(l=="validity"){$("#luckysheet-dataVerification-dialog .show-box .show-box-item-validity").show();let o="card";l==a.type&&(o=a.type2),$("#luckysheet-dataVerification-dialog #data-verification-validity-select").val(o)}}),$(document).off("change.numberSelect").on("change.numberSelect","#data-verification-number-select",function(t){$("#luckysheet-dataVerification-dialog .show-box-item-number .input").hide();let l=this.value;l=="bw"||l=="nb"?$("#luckysheet-dataVerification-dialog .show-box-item-number .input1").show():$("#luckysheet-dataVerification-dialog .show-box-item-number .input2").show()}),$(document).off("change.textLengthSelect").on("change.textLengthSelect","#data-verification-textLength-select",function(t){$("#luckysheet-dataVerification-dialog .show-box-item-textLength .input").hide();let l=this.value;l=="bw"||l=="nb"?$("#luckysheet-dataVerification-dialog .show-box-item-textLength .input1").show():$("#luckysheet-dataVerification-dialog .show-box-item-textLength .input2").show()}),$(document).off("change.dateSelect").on("change.dateSelect","#data-verification-date-select",function(t){$("#luckysheet-dataVerification-dialog .show-box-item-date .input").hide();let l=this.value;l=="bw"||l=="nb"?$("#luckysheet-dataVerification-dialog .show-box-item-date .input1").show():$("#luckysheet-dataVerification-dialog .show-box-item-date .input2").show()}),$(document).off("change.hintShow").on("change.hintShow","#data-verification-hint-show",function(t){this.checked?$("#luckysheet-dataVerification-dialog .data-verification-hint-text").show():$("#luckysheet-dataVerification-dialog .data-verification-hint-text").hide()}),$(document).off("click.dvSaveConfirm").on("click.dvSaveConfirm","#luckysheet-dataVerification-dialog-confirm",function(t){let l=$("#luckysheet-dataVerification-dialog #data-verification-range input").val().trim(),a=e.getRangeByTxt(l);if(a.length==0){U.info('',n.selectCellRange2);return}let o=a[a.length-1].row[0],s=a[a.length-1].row[1],u=a[a.length-1].column[0],f=a[a.length-1].column[1],A=Te.deepCopyFlowData(d.flowdata);o<0&&(o=0),s>A.length-1&&(s=A.length-1),u<0&&(u=0),f>A[0].length-1&&(f=A[0].length-1);let h=$("#luckysheet-dataVerification-dialog #data-verification-type-select").val(),g=null,p="",v="";if(h=="dropdown"){if(p=$("#luckysheet-dataVerification-dialog .show-box-item-dropdown .data-verification-value1").val().trim(),p.length==0){U.info('',n.tooltipInfo1);return}g=$("#luckysheet-dataVerification-dialog #data-verification-multi").is(":checked")}else if(h=="checkbox"){if(p=$("#luckysheet-dataVerification-dialog .show-box-item-checkbox .data-verification-value1").val().trim(),v=$("#luckysheet-dataVerification-dialog .show-box-item-checkbox .data-verification-value2").val().trim(),p.length==0||v.length==0){U.info('',n.tooltipInfo2);return}}else if(h=="number"||h=="number_integer"||h=="number_decimal"){if(g=$("#luckysheet-dataVerification-dialog #data-verification-number-select").val(),p=$("#luckysheet-dataVerification-dialog .show-box-item-number .input:visible .data-verification-value1").val().trim(),!V(p)){U.info('',n.tooltipInfo3);return}if(g=="bw"||g=="nb"){if(v=$("#luckysheet-dataVerification-dialog .show-box-item-number .input:visible .data-verification-value2").val().trim(),!V(v)){U.info('',n.tooltipInfo3);return}if(Number(v)',n.tooltipInfo4);return}}}else if(h=="text_content"){if(g=$("#luckysheet-dataVerification-dialog #data-verification-text-select").val(),p=$("#luckysheet-dataVerification-dialog .show-box-item-text .data-verification-value1").val().trim(),p.length==0){U.info('',n.tooltipInfo5);return}}else if(h=="text_length"){if(g=$("#luckysheet-dataVerification-dialog #data-verification-textLength-select").val(),p=$("#luckysheet-dataVerification-dialog .show-box-item-textLength .input:visible .data-verification-value1").val().trim(),!V(p)){U.info('',n.tooltipInfo3);return}if(!Number.isInteger(Number(p))||Number(p)<0){U.info('',n.textlengthInteger);return}if(g=="bw"||g=="nb"){if(v=$("#luckysheet-dataVerification-dialog .show-box-item-textLength .input:visible .data-verification-value2").val().trim(),!V(v)){U.info('',n.tooltipInfo3);return}if(!Number.isInteger(Number(v))||Number(v)<0){U.info('',n.textlengthInteger);return}if(Number(v)',n.tooltipInfo4);return}}}else if(h=="date"){if(g=$("#luckysheet-dataVerification-dialog #data-verification-date-select").val(),p=$("#luckysheet-dataVerification-dialog .show-box-item-date .input:visible .data-verification-value1").val().trim(),!qt(p)){U.info('',n.tooltipInfo6);return}if(g=="bw"||g=="nb"){if(v=$("#luckysheet-dataVerification-dialog .show-box-item-date .input:visible .data-verification-value2").val().trim(),!qt(v)){U.info('',n.tooltipInfo6);return}if(Al(p,v)>0){U.info('',n.tooltipInfo7);return}}}else h=="validity"&&(g=$("#luckysheet-dataVerification-dialog #data-verification-validity-select").val());let y=$("#luckysheet-dataVerification-dialog #data-verification-remote").is(":checked"),b=$("#luckysheet-dataVerification-dialog #data-verification-prohibitInput").is(":checked"),k=$("#luckysheet-dataVerification-dialog #data-verification-hint-show").is(":checked"),w="";k&&(w=$("#luckysheet-dataVerification-dialog .data-verification-hint-text input").val().trim());let C={type:h,type2:g,value1:p,value2:v,checked:!1,remote:y,prohibitInput:b,hintShow:k,hintText:w},I=$.extend(!0,{},e.dataVerification),x=$.extend(!0,{},e.dataVerification);for(let T=o;T<=s;T++)for(let R=u;R<=f;R++)x[T+"_"+R]=C,h=="checkbox"&&Xt(T,R,A,C.value2);h=="checkbox"?e.refOfCheckbox(I,x,d.currentSheetIndex,A,a[a.length-1]):e.ref(I,x,d.currentSheetIndex),$("#luckysheet-modal-dialog-mask").hide(),$("#luckysheet-dataVerification-dialog").hide()}),$(document).off("click.delete").on("click.delete","#luckysheet-dataVerification-dialog-delete",function(t){let l=$("#luckysheet-dataVerification-dialog #data-verification-range input").val().trim(),a=e.getRangeByTxt(l);if(a.length==0){U.info('',n.selectCellRange2);return}let o=$.extend(!0,{},e.dataVerification),s=$.extend(!0,{},e.dataVerification),u=a[a.length-1].row[0],f=a[a.length-1].row[1],A=a[a.length-1].column[0],h=a[a.length-1].column[1];for(let g=u;g<=f;g++)for(let p=A;p<=h;p++)delete s[g+"_"+p];e.ref(o,s,d.currentSheetIndex),$("#luckysheet-modal-dialog-mask").hide(),$("#luckysheet-dataVerification-dialog").hide()}),$(document).on("click","#luckysheet-info .luckysheet-modal-dialog-title-close, #luckysheet-info .luckysheet-model-close-btn",function(t){$(this).parents("#luckysheet-info").hide(),$("#luckysheet-dataVerification-dialog").is(":visible")&&$("#luckysheet-modal-dialog-mask").show(),t.stopPropagation()})},dataAllocation:function(){let e=this,n=d.luckysheet_select_save[d.luckysheet_select_save.length-1],t=mt(d.currentSheetIndex,n,d.currentSheetIndex);$("#luckysheet-dataVerification-dialog #data-verification-range input").val(t);let l=n.row_focus||n.row[0],a=n.column_focus||n.column[0],s=$.extend(!0,{},e.dataVerification)[l+"_"+a];s==null&&(s=$.extend(!0,{},e.defaultItem)),e.curItem=s,$("#luckysheet-dataVerification-dialog #data-verification-type-select").val(s.type),$("#luckysheet-dataVerification-dialog .show-box .show-box-item").hide(),s.type=="dropdown"?($("#luckysheet-dataVerification-dialog .show-box .show-box-item-dropdown").show(),$("#luckysheet-dataVerification-dialog .show-box-item-dropdown .data-verification-value1").val(s.value1),$("#luckysheet-dataVerification-dialog #data-verification-multi").prop("checked",!!s.type2)):s.type=="checkbox"?($("#luckysheet-dataVerification-dialog .show-box .show-box-item-checkbox").show(),$("#luckysheet-dataVerification-dialog .show-box-item-checkbox .data-verification-value1").val(s.value1),$("#luckysheet-dataVerification-dialog .show-box-item-checkbox .data-verification-value2").val(s.value2)):s.type=="number"||s.type=="number_integer"||s.type=="number_decimal"?($("#luckysheet-dataVerification-dialog .show-box-item-number").show(),$("#luckysheet-dataVerification-dialog #data-verification-number-select").val(s.type2),$("#luckysheet-dataVerification-dialog .show-box-item-number .input").hide(),s.type2=="bw"||s.type2=="nb"?$("#luckysheet-dataVerification-dialog .show-box-item-number .input1").show():$("#luckysheet-dataVerification-dialog .show-box-item-number .input2").show(),$("#luckysheet-dataVerification-dialog .show-box-item-number .data-verification-value1").val(s.value1),$("#luckysheet-dataVerification-dialog .show-box-item-number .data-verification-value2").val(s.value2)):s.type=="text_content"?($("#luckysheet-dataVerification-dialog .show-box-item-text").show(),$("#luckysheet-dataVerification-dialog #data-verification-text-select").val(s.type2),$("#luckysheet-dataVerification-dialog .show-box-item-text .data-verification-value1").val(s.value1)):s.type=="text_length"?($("#luckysheet-dataVerification-dialog .show-box-item-textLength").show(),$("#luckysheet-dataVerification-dialog #data-verification-textLength-select").val(s.type2),$("#luckysheet-dataVerification-dialog .show-box-item-textLength .input").hide(),s.type2=="bw"||s.type2=="nb"?$("#luckysheet-dataVerification-dialog .show-box-item-textLength .input1").show():$("#luckysheet-dataVerification-dialog .show-box-item-textLength .input2").show(),$("#luckysheet-dataVerification-dialog .show-box-item-textLength .data-verification-value1").val(s.value1),$("#luckysheet-dataVerification-dialog .show-box-item-textLength .data-verification-value2").val(s.value2)):s.type=="date"?($("#luckysheet-dataVerification-dialog .show-box-item-date").show(),$("#luckysheet-dataVerification-dialog #data-verification-date-select").val(s.type2),$("#luckysheet-dataVerification-dialog .show-box-item-date .input").hide(),s.type2=="bw"||s.type2=="nb"?$("#luckysheet-dataVerification-dialog .show-box-item-date .input1").show():$("#luckysheet-dataVerification-dialog .show-box-item-date .input2").show(),$("#luckysheet-dataVerification-dialog .show-box-item-date .data-verification-value1").val(s.value1),$("#luckysheet-dataVerification-dialog .show-box-item-date .data-verification-value2").val(s.value2)):s.type=="validity"&&($("#luckysheet-dataVerification-dialog .show-box .show-box-item-validity").show(),$("#luckysheet-dataVerification-dialog #data-verification-validity-select").val(s.type2)),$("#luckysheet-dataVerification-dialog #data-verification-remote").prop("checked",s.remote),$("#luckysheet-dataVerification-dialog #data-verification-prohibitInput").prop("checked",s.prohibitInput),$("#luckysheet-dataVerification-dialog #data-verification-hint-show").prop("checked",s.hintShow),s.hintShow?$("#luckysheet-dataVerification-dialog .data-verification-hint-text").show():$("#luckysheet-dataVerification-dialog .data-verification-hint-text").hide(),$("#luckysheet-dataVerification-dialog .data-verification-hint-text input").val(s.hintText)},rangeDialog:function(e,n){let t=this,l=te(),a=l.dataVerification,o=l.button;$("#luckysheet-modal-dialog-mask").hide(),$("#luckysheet-dataVerificationRange-dialog").remove(),$("body").append(we(At,{id:"luckysheet-dataVerificationRange-dialog",addclass:"luckysheet-dataVerificationRange-dialog",title:a.selectCellRange,content:``,botton:` - `,style:"z-index:100003"}));let s=$("#luckysheet-dataVerificationRange-dialog").find(".luckysheet-modal-dialog-content").css("min-width",300).end(),u=s.outerHeight(),f=s.outerWidth(),A=$(window).width(),h=$(window).height(),g=$(document).scrollLeft(),p=$(document).scrollTop();$("#luckysheet-dataVerificationRange-dialog").css({left:(A+g-f)/2,top:(h+p-u)/3}).show()},getTxtByRange:function(e){if(e.length>0){let n=[];for(let t=0;tHint: ':p='\u63D0\u793A\uFF1A',p+=l.getHintText(A),$("#luckysheet-dataVerification-showHintBox").html(p).show().css({left:u,top:a});return}let h=Fe(e,n,null);if(me(h))return;if(!l.validateCellData(h,A)){let p;d.lang=="en"?p='Failure: ':p='\u5931\u6548\uFF1A',p+=l.getFailureText(A),$("#luckysheet-dataVerification-showHintBox").html(p).show().css({left:u,top:a})}},getHintText:function(e){let n=this,t=e.hintText||"";return t.length==0&&(d.lang=="en"?e.type=="dropdown"?t+="please select an option in the drop-down list":e.type=="checkbox"||(e.type=="number"||e.type=="number_integer"||e.type=="number_decimal"?(t+="please enter a "+n.optionLabel_en[e.type]+" "+n.optionLabel_en[e.type2]+" "+e.value1,(e.type2=="bw"||e.type2=="nb")&&(t+=" and "+e.value2)):e.type=="text_content"?t+="please enter text "+n.optionLabel_en[e.type2]+" "+e.value1:e.type=="text_length"?(t+="please enter text with length "+n.optionLabel_en[e.type2]+" "+e.value1,(e.type2=="bw"||e.type2=="nb")&&(t+=" and "+e.value2)):e.type=="date"?(t+="please enter a date "+n.optionLabel_en[e.type2]+" "+e.value1,(e.type2=="bw"||e.type2=="nb")&&(t+=" and "+e.value2)):e.type=="validity"&&(t+="please enter the correct "+n.optionLabel_en[e.type2])):e.type=="dropdown"?t+="\u8BF7\u9009\u62E9\u4E0B\u62C9\u5217\u8868\u4E2D\u7684\u9009\u9879":e.type=="checkbox"||(e.type=="number"||e.type=="number_integer"||e.type=="number_decimal"?(t+="\u8BF7\u8F93\u5165"+n.optionLabel[e.type2]+e.value1,(e.type2=="bw"||e.type2=="nb")&&(t+="\u548C"+e.value2+"\u4E4B\u95F4"),t+="\u7684"+n.optionLabel[e.type]):e.type=="text_content"?t+="\u8BF7\u8F93\u5165\u5185\u5BB9"+n.optionLabel[e.type2]+e.value1+"\u7684\u6587\u672C":e.type=="text_length"?(t+="\u8BF7\u8F93\u5165\u957F\u5EA6"+n.optionLabel[e.type2]+e.value1,(e.type2=="bw"||e.type2=="nb")&&(t+="\u548C"+e.value2+"\u4E4B\u95F4"),t+="\u7684\u6587\u672C"):e.type=="date"?(t+="\u8BF7\u8F93\u5165"+n.optionLabel[e.type2]+e.value1,(e.type2=="bw"||e.type2=="nb")&&(t+="\u548C"+e.value2+"\u4E4B\u95F4"),t+="\u7684\u65E5\u671F"):e.type=="validity"&&(t+="\u8BF7\u8F93\u5165\u6B63\u786E\u7684"+n.optionLabel[e.type2]))),t},getFailureText:function(e){let n=this,t="";return d.lang=="en"?e.type=="dropdown"?t+="what you selected is not an option in the drop-down list":e.type=="checkbox"||(e.type=="number"||e.type=="number_integer"||e.type=="number_decimal"?(t+="what you entered is not a "+n.optionLabel_en[e.type]+" "+n.optionLabel_en[e.type2]+" "+e.value1,(e.type2=="bw"||e.type2=="nb")&&(t+=" and "+e.value2)):e.type=="text_content"?t+="what you entered is not text that "+n.optionLabel_en[e.type2]+" "+e.value1:e.type=="text_length"?(t+="the text you entered is not length "+n.optionLabel_en[e.type2]+" "+e.value1,(e.type2=="bw"||e.type2=="nb")&&(t+=" and "+e.value2)):e.type=="date"?(t+="the date you entered is not "+n.optionLabel_en[e.type2]+" "+e.value1,(e.type2=="bw"||e.type2=="nb")&&(t+=" and "+e.value2)):e.type=="validity"&&(t+="what you entered is not a correct "+n.optionLabel_en[e.type2])):e.type=="dropdown"?t+="\u4F60\u9009\u62E9\u7684\u4E0D\u662F\u4E0B\u62C9\u5217\u8868\u4E2D\u7684\u9009\u9879":e.type=="checkbox"||(e.type=="number"||e.type=="number_integer"||e.type=="number_decimal"?(t+="\u4F60\u8F93\u5165\u7684\u4E0D\u662F"+n.optionLabel[e.type2]+e.value1,(e.type2=="bw"||e.type2=="nb")&&(t+="\u548C"+e.value2+"\u4E4B\u95F4"),t+="\u7684"+n.optionLabel[e.type]):e.type=="text_content"?t+="\u4F60\u8F93\u5165\u7684\u4E0D\u662F\u5185\u5BB9"+n.optionLabel[e.type2]+e.value1+"\u7684\u6587\u672C":e.type=="text_length"?(t+="\u4F60\u8F93\u5165\u7684\u4E0D\u662F\u957F\u5EA6"+n.optionLabel[e.type2]+e.value1,(e.type2=="bw"||e.type2=="nb")&&(t+="\u548C"+e.value2+"\u4E4B\u95F4"),t+="\u7684\u6587\u672C"):e.type=="date"?(t+="\u4F60\u8F93\u5165\u7684\u4E0D\u662F"+n.optionLabel[e.type2]+e.value1,(e.type2=="bw"||e.type2=="nb")&&(t+="\u548C"+e.value2+"\u4E4B\u95F4"),t+="\u7684\u65E5\u671F"):e.type=="validity"&&(t+="\u4F60\u8F93\u5165\u7684\u4E0D\u662F\u4E00\u4E2A\u6B63\u786E\u7684"+n.optionLabel[e.type2])),t},validateCellData:function(e,n){let t=this,l=n.type,a=n.type2,o=n.value1,s=n.value2;if(l=="dropdown"){let u=t.getDropdownList(o);if(a&&e)return e.split(",").every(function(A){return u.indexOf(A)!==-1});let f=!1;for(let A=0;As))||a=="nb"&&e>=o&&e<=s||a=="eq"&&e!=o||a=="ne"&&e==o||a=="gt"&&e<=o||a=="lt"&&e>=o||a=="gte"&&eo)return!1}else if(l=="text_content"){if(e=e.toString(),o=o.toString(),a=="include"&&e.indexOf(o)==-1||a=="exclude"&&e.indexOf(o)>-1||a=="equal"&&e!=o)return!1}else if(l=="text_length"){if(e=e.toString().length,o=Number(o),s=Number(s),a=="bw"&&(es)||a=="nb"&&e>=o&&e<=s||a=="eq"&&e!=o||a=="ne"&&e==o||a=="gt"&&e<=o||a=="lt"&&e>=o||a=="gte"&&eo)return!1}else if(l=="date"){if(!qt(e)||a=="bw"&&(Al(e,o)<0||Al(e,s)>0)||a=="nb"&&Al(e,o)>=0&&Al(e,s)<=0||a=="eq"&&Al(e,o)!=0||a=="ne"&&Al(e,o)==0||a=="bf"&&Al(e,o)>=0||a=="nbf"&&Al(e,o)<0||a=="af"&&Al(e,o)<=0||a=="naf"&&Al(e,o)>0)return!1}else if(l=="validity"&&(a=="card"&&!yy(e)||a=="phone"&&!/^1[3456789]\d{9}$/.test(e)))return!1}return!0},dropdownListShow:function(){$("#luckysheet-dataVerification-showHintBox").hide();let e=this,n=d.luckysheet_select_save[d.luckysheet_select_save.length-1],t=n.row_focus,l=n.column_focus,a=d.visibledatarow[t],o=t==0?0:d.visibledatarow[t-1],s=d.visibledatacolumn[l],u=l==0?0:d.visibledatacolumn[l-1],f=be.mergeborer(d.flowdata,t,l);f&&(a=f.row[1],o=f.row[0],s=f.column[1],u=f.column[0]);let A=e.dataVerification[t+"_"+l],h=e.getDropdownList(A.value1),g="";if(A.type==="dropdown"&&A.type2){let y=Fe(t,l,null),b=me(y)?[]:y.split(",");h.forEach(k=>{g+=``})}else h.forEach(y=>{g+=``});$("#luckysheet-dataVerification-dropdown-List").html(g).prop("data-index",t+"_"+l).show().css({width:s-u-1,left:u,top:a});let p=$("#luckysheet-dataVerification-dropdown-List").outerHeight(),v=$("#luckysheet-cell-main")[0].scrollHeight;a+p>v-42-6&&$("#luckysheet-dataVerification-dropdown-List").css({top:o-p})},getDropdownList:function(e){let n=[];if(m.iscelldata(e)){let t=m.getcellrange(e),l=d.luckysheetfile[Q(t.sheetIndex)].data;for(let a=t.row[0];a<=t.row[1];a++)for(let o=t.column[0];o<=t.column[1];o++){if(l[a]==null)continue;let s=l[a][o];if(s==null||s.v==null)continue;let u=s.m||s.v;n.includes(u)||n.push(u)}}else{let t=e.split(",");for(let l=0;l0)for(let z=0;z=n&&(P.r+=t):l=="rightbottom"&&L>n&&(P.r+=t),g.push(P)}else if(e=="column"){let G="="+m.functionStrChange(j,"add","col",l,n,t);u[L][O]&&u[L][O].f==j&&(u[L][O].f=G),l=="lefttop"?O>=n&&(P.c+=t):l=="rightbottom"&&O>n&&(P.c+=t),g.push(P)}}let p=s.filter_select,v=s.filter,y=null;if(p!=null&&JSON.stringify(p)!="{}"){y={filter_select:null,filter:null};let z=p.row[0],P=p.row[1],L=p.column[0],O=p.column[1];if(e=="row"){if(zn)&&(P+=t):z==n?l=="lefttop"?(z+=t,P+=t):l=="rightbottom"&&P>n&&(P+=t):(z+=t,P+=t),v!=null){y.filter={};for(let q in v){let j=v[q].rowhidden,G={};for(let _ in j)_=parseFloat(_),_n)&&(O+=t):L==n?l=="lefttop"?(L+=t,O+=t):l=="rightbottom"&&O>n&&(O+=t):(L+=t,O+=t),v!=null)){y.filter={};for(let q in v){let j=v[q].cindex;(j==n&&l=="lefttop"||j>n)&&(j+=t),y.filter[j-L]=$.extend(!0,{},v[q]),y.filter[j-L].cindex=j,y.filter[j-L].stc=L,y.filter[j-L].edc=O}}y.filter_select={row:[z,P],column:[L,O]}}if(y!=null&&y.filter!=null){f.rowhidden==null&&(f.rowhidden={});for(let z in y.filter){let P=y.filter[z].rowhidden;for(let L in P)f.rowhidden[L]=0}}let b=s.luckysheet_conditionformat_save,k=[];if(b!=null&&b.length>0)for(let z=0;zn)&&(G+=t):j==n?l=="lefttop"?(j+=t,G+=t):l=="rightbottom"&&G>n&&(G+=t):(j+=t,G+=t):e=="column"&&(_n)&&(ee+=t):_==n?l=="lefttop"?(_+=t,ee+=t):l=="rightbottom"&&ee>n&&(ee+=t):(_+=t,ee+=t)),L.push({row:[j,G],column:[_,ee]})}let O=$.extend(!0,{},b[z]);O.cellrange=L,k.push(O)}let w=s.luckysheet_alternateformat_save,C=[];if(w!=null&&w.length>0)for(let z=0;zn)&&(L+=t):P==n?l=="lefttop"?(P+=t,L+=t):l=="rightbottom"&&L>n&&(L+=t):(P+=t,L+=t):e=="column"&&(On)&&(q+=t):O==n?l=="lefttop"?(O+=t,q+=t):l=="rightbottom"&&q>n&&(q+=t):(O+=t,q+=t)),j.cellrange={row:[P,L],column:[O,q]},C.push(j)}let I={freezenhorizontaldata:null,freezenverticaldata:null};if(Y.freezenhorizontaldata!=null&&e=="row"){let z=Y.freezenhorizontaldata[2],P=Y.freezenhorizontaldata[1]-1;(P==n&&l=="lefttop"||P>n)&&(P+=t);let L=d.visibledatarow[P]-2-z+d.columnHeaderHeight;I.freezenhorizontaldata=[d.visibledatarow[P],P+1,z,Y.cutVolumn(d.visibledatarow,P+1),L]}else I.freezenhorizontaldata=Y.freezenhorizontaldata;if(Y.freezenverticaldata!=null&&e=="column"){let z=Y.freezenverticaldata[2],P=Y.freezenverticaldata[1]-1;(P==n&&l=="lefttop"||P>n)&&(P+=t);let L=d.visibledatacolumn[P]-2-z+d.rowHeaderWidth;I.freezenverticaldata=[d.visibledatacolumn[P],P+1,z,Y.cutVolumn(d.visibledatacolumn,P+1),L]}else I.freezenverticaldata=Y.freezenverticaldata;let x=s.dataVerification,T={};if(x!=null)for(let z in x){let P=Number(z.split("_")[0]),L=Number(z.split("_")[1]),O=x[z];if(e=="row")if(n0){let L=[];for(let O=0;O=ee&&G.push({row:[ee,re],column:j[_].column})}if(G.length>0){let _={rangeType:"range",borderType:f.borderInfo[O].borderType,style:f.borderInfo[O].style,color:f.borderInfo[O].color,range:G};L.push(_)}}else if(q=="cell"){let j=f.borderInfo[O].value.row_index;j===n&&B.push(JSON.parse(JSON.stringify(f.borderInfo[O]))),l=="lefttop"?n<=j&&(j+=t):n{l==="rightbottom"?O.value.row_index+=L+1:l==="lefttop"&&(O.value.row_index+=L)}),f.borderInfo.push(...F)}l=="lefttop"?n==0?new Function("d","return d.unshift("+P.join(",")+")")(u):new Function("d","return d.splice("+n+", 0, "+P.join(",")+")")(u):new Function("d","return d.splice("+(n+1)+", 0, "+P.join(",")+")")(u)}else{if(S="c",f.columnlen!=null){let L={};for(let O in f.columnlen)O=parseFloat(O),O0){let L=[];for(let O=0;O=ee&&G.push({row:j[_].row,column:[ee,re]})}if(G.length>0){let _={rangeType:"range",borderType:f.borderInfo[O].borderType,style:f.borderInfo[O].style,color:f.borderInfo[O].color,range:G};L.push(_)}}else if(q=="cell"){let j=f.borderInfo[O].value.col_index;j===n&&B.push(JSON.parse(JSON.stringify(f.borderInfo[O]))),l=="lefttop"?n<=j&&(j+=t):n{l==="rightbottom"?q.value.col_index+=L+1:l==="lefttop"&&(q.value.col_index+=L)}),f.borderInfo.push(...F)}for(let L=0;L0?$("#luckysheet-scrollbar-y").scrollTop(q-L+20):j-P-20<0&&$("#luckysheet-scrollbar-y").scrollTop(j-20),t>30&&($("#luckysheet-row-count-show").hide(),$("#luckysheet-column-count-show").hide())}}function ms(e,n){let t=Te.deepCopyFlowData(d.flowdata),l=$.extend(!0,{},d.config);l.merge==null&&(l.merge={});let a=t[0].length,o=fl([],e,a);t=t.concat(o);for(let s=0;ss.length-1&&(n=s.length-1),t>s.length-1&&(t=s.length-1)):(n>s[0].length-1&&(n=s[0].length-1),t>s[0].length-1&&(t=s[0].length-1)),n>t)return;let u=t-n+1,f=$.extend(!0,{},o.config);f.merge==null&&(f.merge={});let A={};for(let B in f.merge){let F=f.merge[B],E=F.r,z=F.c,P=F.rs,L=F.cs;e=="row"?E=n&&E+P-1=t&&(A[E+"_"+z]={r:E,c:z,rs:P-u,cs:L}):E>=n&&E<=t?E+P-1>t&&(A[n+"_"+z]={r:n,c:z,rs:E+P-1-t,cs:L}):E>t&&(A[E-u+"_"+z]={r:E-u,c:z,rs:P,cs:L}):e=="column"&&(z=n&&z+L-1=t&&(A[E+"_"+z]={r:E,c:z,rs:P,cs:L-u}):z>=n&&z<=t?z+L-1>t&&(A[E+"_"+n]={r:E,c:n,rs:P,cs:z+L-1-t}):z>t&&(A[E+"_"+(z-u)]={r:E,c:z-u,rs:P,cs:L}))}f.merge=A;let h=o.calcChain,g=[];if(h!=null&&h.length>0)for(let B=0;Bt){let O="="+m.functionStrChange(L,"del","row",null,n,u);s[E][z]&&s[E][z].f==L&&(s[E][z].f=O),E>t&&(F.r=E-u),g.push(F)}}else if(e=="column"&&(zt)){let O="="+m.functionStrChange(L,"del","col",null,n,u);s[E][z]&&s[E][z].f==L&&(s[E][z].f=O),z>t&&(F.c=z-u),g.push(F)}}let p=o.filter_select,v=o.filter,y=null;if(p!=null&&JSON.stringify(p)!="{}"){y={filter_select:null,filter:null};let B=p.row[0],F=p.row[1],E=p.column[0],z=p.column[1];if(e=="row"){if(B>t?(B-=u,F-=u,y.filter_select={row:[B,F],column:[E,z]}):Bt&&(O[q-u]=0);JSON.stringify(O)!="{}"&&(y.filter==null&&(y.filter={}),y.filter[P]=$.extend(!0,{},v[P]),y.filter[P].rowhidden=O,y.filter[P].str=B,y.filter[P].edr=F)}}else if(e=="column"&&(E>t?(E-=u,z-=u,y.filter_select={row:[B,F],column:[E,z]}):Et&&(E=n,z-=u,y.filter_select={row:[B,F],column:[E,z]}),y.filter_select!=null&&v!=null))for(let P in v){let L=v[P].cindex;Lt&&(L-=u,y.filter==null&&(y.filter={}),y.filter[L-E]=$.extend(!0,{},v[P]),y.filter[L-E].cindex=L,y.filter[L-E].stc=E,y.filter[L-E].edc=z)}}if(y!=null&&y.filter!=null){f.rowhidden==null&&(f.rowhidden={});for(let B in y.filter){let F=y.filter[B].rowhidden;for(let E in F)f.rowhidden[E]=0}}let b=o.luckysheet_conditionformat_save,k=[];if(b!=null&&b.length>0)for(let B=0;B=n&&L<=t||(P>t?(P-=u,L-=u):Pt&&(P=n,L-=u),E.push({row:[P,L],column:[O,q]})):e=="column"&&(O>=n&&q<=t||(O>t?(O-=u,q-=u):Ot&&(O=n,q-=u),E.push({row:[P,L],column:[O,q]})))}if(E.length>0){let z=$.extend(!0,{},b[B]);z.cellrange=E,k.push(z)}}let w=o.luckysheet_alternateformat_save,C=[];if(w!=null&&w.length>0)for(let B=0;B=n&&E<=t)){let L=$.extend(!0,{},w[B]);F>t?(F-=u,E-=u):Ft&&(F=n,E-=u),L.cellrange={row:[F,E],column:[z,P]},C.push(L)}}else if(e=="column"&&!(z>=n&&P<=t)){let L=$.extend(!0,{},w[B]);z>t?(z-=u,P-=u):zt&&(z=n,P-=u),L.cellrange={row:[F,E],column:[z,P]},C.push(L)}}let I={freezenhorizontaldata:null,freezenverticaldata:null};if(Y.freezenhorizontaldata!=null&&e=="row"){let B=Y.freezenhorizontaldata[2],F=nt(d.visibledatarow,B);F==-1&&(F=0);let E=Y.freezenhorizontaldata[1]-1;E>=n&&(E=n&&(Et&&(T[F-u+"_"+E]=z):e=="column"&&(Et&&(T[F+"_"+(E-u)]=z))}let R=o.hyperlink,D={};if(R!=null)for(let B in R){let F=Number(B.split("_")[0]),E=Number(B.split("_")[1]),z=R[B];e=="row"?Ft&&(D[F-u+"_"+E]=z):e=="column"&&(Et&&(D[F+"_"+(E-u)]=z))}let S;if(e=="row"){S="r",f.rowlen==null&&(f.rowlen={});let B={};for(let E in f.rowlen)Et&&(B[E-u]=f.rowlen[E]);f.rowlen=B,f.rowhidden==null&&(f.rowhidden={});let F={};for(let E in f.rowhidden)Et&&(F[E-u]=f.rowhidden[E]);if(f.rowhidden=F,f.borderInfo&&f.borderInfo.length>0){let E=[];for(let z=0;z=j&&O.push({row:[j,G],column:L[q].column})}if(O.length>0){let q={rangeType:"range",borderType:f.borderInfo[z].borderType,style:f.borderInfo[z].style,color:f.borderInfo[z].color,range:O};E.push(q)}}else if(P=="cell"){let L=f.borderInfo[z].value.row_index;Lt&&(f.borderInfo[z].value.row_index=L-(t-n+1),E.push(f.borderInfo[z]))}}f.borderInfo=E}s.splice(n,u);for(let E=0;Et&&(B[z-u]=f.columnlen[z]);f.columnlen=B,f.colhidden==null&&(f.colhidden={});let F={};for(let z in f.colhidden)zt&&(F[z-u]=f.colhidden[z]);if(f.colhidden=F,f.borderInfo&&f.borderInfo.length>0){let z=[];for(let P=0;P=G&&q.push({row:O[j].row,column:[G,_]})}if(q.length>0){let j={rangeType:"range",borderType:f.borderInfo[P].borderType,style:f.borderInfo[P].style,color:f.borderInfo[P].color,range:q};z.push(j)}}else if(L=="cell"){let O=f.borderInfo[P].value.col_index;Ot&&(f.borderInfo[P].value.col_index=O-(t-n+1),z.push(f.borderInfo[P]))}}f.borderInfo=z}let E=[];for(let z=0;zE+P-1||tz+L-1)p[E+"_"+z]={r:E,c:z,rs:P,cs:L};else if(n<=E&&t>=E+P-1&&az+L-1||aE+P-1)p[E+"_"+z]={r:E,c:z,rs:P,cs:L};else if(l<=z&&a>=z+L-1&&t0)for(let B=0;Bt||za){let O;e=="moveLeft"?(O="="+m.functionStrChange(L,"del","col",null,l,h),z>a&&E>=n&&E<=t&&(F.c=z-h)):e=="moveUp"&&(O="="+m.functionStrChange(L,"del","row",null,n,A),E>t&&z>=l&&z<=a&&(F.r=E-A)),f[E][z]&&f[E][z].f==L&&(f[E][z].f=O),y.push(F)}}let b=u.filter_select,k=u.filter,w=null;if(b!=null&&JSON.stringify(b)!="{}"){w={filter_select:null,filter:null};let B=b.row[0],F=b.row[1],E=b.column[0],z=b.column[1];if(e=="moveUp")if(E>=l&&z<=a){if(B>t?w.filter_select={row:[B-A,F-A],column:[E,z]}:Ft?w.filter_select={row:[B,F-A],column:[E,z]}:w.filter_select={row:[B,n-1],column:[E,z]}),w.filter_select!=null&&k!=null)for(let P in k){let L=k[P].rowhidden,O={};for(let q in L)qt&&(O[q-slen]=0);w.filter==null&&(w.filter={}),w.filter[P]=$.extend(!0,{},k[P]),JSON.stringify(O)!="{}"&&(w.filter[P].rowhidden=O),w.filter[P].str=w.filter_select.row[0],w.filter[P].edr=w.filter_select.row[1]}}else if(B>=n&&F<=t){if(E>a?w.filter_select={row:[B,F],column:[E,z]}:E>=l?z>a&&(w.filter_select={row:[B,F],column:[l,z-h]}):za){w.filter==null&&(w.filter={}),q>a&&(q-=h);let j=q-L;w.filter[j]=$.extend(!0,{},k[P]),w.filter[j].cindex=q,w.filter[j].stc=L,w.filter[j].edc=O}}}else w.filter_select={row:[B,F],column:[E,z]},k!=null&&(w.filter=k);else if(e=="moveLeft")if(B>=n&&F<=t){if(E>a?w.filter_select={row:[B,F],column:[E-h,z-h]}:za?w.filter_select={row:[B,F],column:[E,z-h]}:w.filter_select={row:[B,F],column:[E,l-1]}),w.filter_select!=null&&k!=null)for(let P in k){let L=w.filter_select.column[0],O=w.filter_select.column[1],q=k[P].cindex;if(qa){w.filter==null&&(w.filter={}),q>a&&(q-=h);let j=q-L;w.filter[j]=$.extend(!0,{},k[P]),w.filter[j].cindex=q,w.filter[j].stc=L,w.filter[j].edc=O}}}else E>=l&&z<=a?(Bt)&&(w.filter_select={row:[B,F],column:[E,z]},k!=null&&(w.filter=k)):(w.filter_select={row:[B,F],column:[E,z]},k!=null&&(w.filter=k))}if(w!=null&&w.filter!=null){g.rowhidden==null&&(g.rowhidden={});for(let B in w.filter){let F=w.filter[B].rowhidden;for(let E in F)g.rowhidden[E]=0}}let C=u.luckysheet_conditionformat_save,I=[];if(C!=null&&C.length>0)for(let B=0;B=L&&l<=O&&a>=q||(E=sh(e,n,t,l,a,P,L,O,q,A,h))}if(E.length>0){let z=$.extend(!0,{},C[B]);z.cellrange=E,I.push(z)}}let x=u.dataVerification,T={};if(x!=null)for(let B in x){let F=Number(B.split("_")[0]),E=Number(B.split("_")[1]),z=x[B];(Ft||Ea)&&(e=="moveLeft"?E>a&&F>=n&&F<=t?T[F+"_"+(E-h)]=z:T[F+"_"+E]=z:e=="moveUp"&&(F>t&&E>=l&&E<=a?T[F-A+"_"+E]=z:T[F+"_"+E]=z))}let R=u.hyperlink,D={};if(R!=null)for(let B in R){let F=Number(B.split("_")[0]),E=Number(B.split("_")[1]),z=R[B];(Ft||Ea)&&(e=="moveLeft"?E>a&&F>=n&&F<=t?D[F+"_"+(E-h)]=z:D[F+"_"+E]=z:e=="moveUp"&&(F>t&&E>=l&&E<=a?D[F-A+"_"+E]=z:D[F+"_"+E]=z))}if(g.borderInfo&&g.borderInfo.length>0){let B=[];for(let F=0;F=q&&l<=j&&a>=G||(P=sh(e,n,t,l,a,O,q,j,G,A,h))}if(P.length>0){let L={rangeType:"range",borderType:g.borderInfo[F].borderType,style:g.borderInfo[F].style,color:g.borderInfo[F].color,range:P};B.push(L)}}else if(E=="cell"){let z=g.borderInfo[F].value.row_index,P=g.borderInfo[F].value.col_index;(zt||Pa)&&(e=="moveLeft"?P>a&&z>=n&&z<=t&&(P-=h,g.borderInfo[F].value.col_index=P):e=="moveUp"&&z>t&&P>=l&&P<=a&&(z-=A,g.borderInfo[F].value.row_index=z),B.push(g.borderInfo[F]))}}g.borderInfo=B}let S=[];for(let B=l;B<=a;B++)S.push(null);if(e=="moveUp"){let B=[];for(let E=n;E<=f.length-1;E++){let z=[];for(let P=l;P<=a;P++)z.push(f[E][P]);B.push(z)}B.splice(0,A);let F=[];for(let E=n;E<=t;E++)F.push(S);B=B.concat(F);for(let E=n;E<=f.length-1;E++)for(let z=l;z<=a;z++)f[E][z]=B[E-n][z-l]}else if(e=="moveLeft")for(let B=n;B<=t;B++)f[B].splice(l,h),f[B]=f[B].concat(S);u.index==d.currentSheetIndex?ao(f,g,{type:e,str:n,edr:t,stc:l,edc:a},y,w,I,T,D):(u.data=f,u.config=g,u.calcChain=y,u.filter=w.filter,u.filter_select=w.filter_select,u.luckysheet_conditionformat_save=I,u.dataVerification=T,u.hyperlink=D)}function sh(e,n,t,l,a,o,s,u,f,A,h){let g=[];if(e=="moveLeft"){if(n>s||tf)g.push({row:[o,s],column:[u,f]});else if(a=s)g.push({row:[o,s],column:[u-h,f-h]});else if(n>o&&to){let p=[{row:[o,n-1],column:[u,f]},{row:[n,s],column:[u-h,f-h]}];g=g.concat(p)}else if(t=u){if(l<=u&&a>=f){if(n>o&&to){let p=[{row:[o,n-1],column:[u,f]}];g=g.concat(p)}else if(tu&&a=s)g.push({row:[o,s],column:[u,f-h]});else if(n>o&&to){let p=[{row:[o,n-1],column:[u,f]},{row:[n,s],column:[u,f-h]}];g=g.concat(p)}else if(tu){if(n<=o&&t>=s)g.push({row:[o,s],column:[u,l-1]});else if(n>o&&to){let p=[{row:[o,n-1],column:[u,f]},{row:[n,s],column:[u,l-1]}];g=g.concat(p)}else if(t=s)g.push({row:[o,s],column:[u-h,f-h]});else if(n>o&&to){let p=[{row:[o,n-1],column:[u,f]},{row:[n,s],column:[u-h,f-h]}];g=g.concat(p)}else if(tf||as)g.push({row:[o,s],column:[u,f]});else if(t=f)g.push({row:[o-A,s-A],column:[u,f]});else if(l>u&&au){let p=[{row:[o,s],column:[u,l-1]},{row:[o-A,s-A],column:[l,f]}];g=g.concat(p)}else if(a=o){if(n<=o&&t>=s){if(l>u&&au){let p=[{row:[o,s],column:[u,l-1]}];g=g.concat(p)}else if(ao&&t=f)g.push({row:[o,s-A],column:[u,f]});else if(l>u&&au){let p=[{row:[o,s],column:[u,l-1]},{row:[o,s-A],column:[l,f]}];g=g.concat(p)}else if(ao){if(l<=u&&a>=f)g.push({row:[o,n-1],column:[u,f]});else if(l>u&&au){let p=[{row:[o,s],column:[u,l-1]},{row:[o,n-1],column:[l,f]}];g=g.concat(p)}else if(a=f)g.push({row:[o-A,s-A],column:[u,f]});else if(l>u&&au){let p=[{row:[o,s],column:[u,l-1]},{row:[o-A,s-A],column:[l,f]}];g=g.concat(p)}else if(a{tl();jt();_t();Qt();zr();Ar();Kl();el();Br();Bl();wt();je()});function Kc(e,n,t,l,a){a==null&&(a=!0),d.visibledatarow=[],d.visibledatacolumn=[],d.ch_width=0,d.rh_height=0,d.zoomRatio=1,l!=null?d.config=l:d.config={},t.length==0?d.flowdata=fl(t,n,e):t.length{Qt();tl();lo();jt();_t();cl();je()});function Lr({url:e,headers:n={},data:t={},type:l="POST",contentType:a,success:o,error:s}){$.ajax({url:e,headers:n,data:JSON.stringify(t),type:l,contentType:a||"application/json;charset=UTF-8",success:function(u){o&&typeof o=="function"&&o(u)},error:function(u){s&&typeof s=="function"&&s(u)}})}var fi=De(()=>{});var Ai,by,$e,Xl=De(()=>{$t();Wt();cl();jt();hr();mr();vn();Kl();wt();Fa();yl();tl();ch();je();fi();Ai={defaultStore:{container:null,luckysheetfile:null,defaultcolumnNum:60,defaultrowNum:84,fullscreenmode:!0,devicePixelRatio:1,currentSheetIndex:0,calculateSheetIndex:0,flowdata:[],config:{},visibledatarow:[],visibledatacolumn:[],ch_width:0,rh_height:0,cellmainWidth:0,cellmainHeight:0,toolbarHeight:0,infobarHeight:0,calculatebarHeight:0,rowHeaderWidth:46,columnHeaderHeight:20,cellMainSrollBarSize:12,sheetBarHeight:31,statisticBarHeight:23,luckysheetTableContentHW:[0,0],defaultcollen:73,defaultrowlen:19,jfcountfuncTimeout:null,jfautoscrollTimeout:null,luckysheet_select_status:!1,luckysheet_select_save:[{row:[0,0],column:[0,0]}],luckysheet_selection_range:[],luckysheet_copy_save:{},luckysheet_paste_iscut:!1,filterchage:!0,luckysheet_filter_save:{row:[],column:[]},luckysheet_sheet_move_status:!1,luckysheet_sheet_move_data:[],luckysheet_scroll_status:!1,luckysheetisrefreshdetail:!0,luckysheetisrefreshtheme:!0,luckysheetcurrentisPivotTable:!1,luckysheet_rows_selected_status:!1,luckysheet_cols_selected_status:!1,luckysheet_rows_change_size:!1,luckysheet_rows_change_size_start:[],luckysheet_cols_change_size:!1,luckysheet_cols_change_size_start:[],luckysheet_cols_dbclick_timeout:null,luckysheet_cols_dbclick_times:0,luckysheetCellUpdate:[],luckysheet_shiftpositon:null,iscopyself:!0,orderbyindex:0,luckysheet_model_move_state:!1,luckysheet_model_xy:[0,0],luckysheet_model_move_obj:null,luckysheet_cell_selected_move:!1,luckysheet_cell_selected_move_index:[],luckysheet_cell_selected_extend:!1,luckysheet_cell_selected_extend_index:[],luckysheet_cell_selected_extend_time:null,clearjfundo:!0,jfredo:[],jfundo:[],lang:"en",createChart:"",highlightChart:"",zIndex:15,chartparam:{luckysheetCurrentChart:null,luckysheetCurrentChartActive:!1,luckysheetCurrentChartMove:null,luckysheetCurrentChartMoveTimeout:null,luckysheetCurrentChartMoveObj:null,luckysheetCurrentChartMoveXy:null,luckysheetCurrentChartMoveWinH:null,luckysheetCurrentChartMoveWinW:null,luckysheetCurrentChartResize:null,luckysheetCurrentChartResizeObj:null,luckysheetCurrentChartResizeXy:null,luckysheetCurrentChartResizeWinH:null,luckysheetCurrentChartResizeWinW:null,luckysheetInsertChartTosheetChange:!0,luckysheetCurrentChartZIndexRank:100,luckysheet_chart_redo_click:!1,luckysheetCurrentChartMaxState:!1,jfrefreshchartall:"",changeChartCellData:"",renderChart:"",getChartJson:""},functionList:null,luckysheet_function:null,chart_selection:{},currentChart:"",scrollRefreshSwitch:!0,measureTextCache:{},measureTextCellInfoCache:{},measureTextCacheTimeOut:null,cellOverflowMapCache:{},zoomRatio:1,visibledatacolumn_unique:null,visibledatarow_unique:null,showGridLines:!0,toobarObject:{},inlineStringEditCache:null,inlineStringEditRange:null,fontList:[],currentSheetView:"viewNormal"},defaultFormula:{searchFunctionCell:null,functionlistPosition:{},rangechangeindex:null,rangestart:!1,rangetosheet:null,rangeSetValueTo:null,func_selectedrange:{},rangedrag_column_start:!1,rangedrag_row_start:!1,rangeResizeObj:null,rangeResize:null,rangeResizeIndex:null,rangeResizexy:null,rangeResizeWinH:null,rangeResizeWinW:null,rangeResizeTo:null,rangeMovexy:null,rangeMove:!1,rangeMoveObj:null,rangeMoveIndex:null,rangeMoveRangedata:null,functionHTMLIndex:0,functionRangeIndex:null,execvertex:{},execFunctionGroupData:null,execFunctionExist:null,formulaContainSheetList:{},cellTextToIndexList:{},isFunctionRangeSave:!1,formulaContainCellList:{},execFunctionGlobalData:{},groupValuesRefreshData:[],functionResizeData:{},functionResizeStatus:!1,functionResizeTimeout:null,data_parm_index:0},defaultSheet:{sheetMaxIndex:0,nulldata:null,mergeCalculationSheet:{},checkLoadSheetIndexToDataIndex:{},CacheNotLoadControll:[]},defaultPivotTable:{pivotDatas:null,pivotSheetIndex:0,pivotDataSheetIndex:0,celldata:null,origindata:null,pivot_data_type:{},pivot_select_save:null,column:null,row:null,values:null,filter:null,showType:null,rowhidden:null,selected:null,caljs:null,initial:!0,filterparm:null,luckysheet_pivotTable_select_state:!1,jgridCurrentPivotInput:null,movestate:!1,moveitemposition:[],movesave:{},drawPivotTable:!0,pivotTableBoundary:[12,6]},defaultImage:{imgItem:{type:"3",src:"",originWidth:null,originHeight:null,default:{width:null,height:null,left:null,top:null},crop:{width:null,height:null,offsetLeft:0,offsetTop:0},isFixedPos:!1,fixedLeft:null,fixedTop:null,border:{width:0,radius:0,style:"solid",color:"#000"}},images:null,currentImgId:null,currentWinW:null,currentWinH:null,resize:null,resizeXY:null,move:!1,moveXY:null,cropChange:null,cropChangeXY:null,cropChangeObj:null,copyImgItemObj:null},defaultDataVerification:{defaultItem:{type:"dropdown",type2:null,value1:"",value2:"",checked:!1,remote:!1,prohibitInput:!1,hintShow:!1,hintText:""},curItem:null,dataVerification:null,selectRange:[],selectStatus:!1}},by={addDataAjax:function(e,n,t,l){let a=this;n==null&&(n=d.currentSheetIndex),t==null&&(t=ae.loadSheetUrl),$("#luckysheet-grid-window-1").append(Ur()),e.currentPage++;let o="application/json;charset=UTF-8",s=sessionStorage.getItem("x-auth-token");Lr({url:t,data:JSON.stringify(e),contentType:o,headers:$.extend({},{"x-auth-token":s},Ae.requestHeaders),success:function(u){typeof u=="string"&&(u=JSON.parse(u));let f=u.data,A=f.celldata;ms(f.row,A),setTimeout(function(){d.loadingObj.close()},500),l&&typeof l=="function"&&l(f)}})},reload:function(e,n,t,l){let a=this;n==null&&(n=d.currentSheetIndex),t==null&&(t=ae.loadSheetUrl),$("#luckysheet-grid-window-1").append(Ur());let o={gridKey:ae.gridKey,index:n};e=$.extend(!0,e,o);let s=d.luckysheetfile[Q(n)];Lr({url:t,data:e,headers:Ae.requestHeaders,success:function(u){let f=new Function("return "+u)();s.celldata=f[n.toString()];let A=ve.buildGridData(s);setTimeout(function(){d.loadingObj.close()},500),s.data=A,d.flowdata=A,Te.webWorkerFlowDataCache(A),Kc(A[0].length,A.length,A,null,!1),s.load="1",d.luckysheet_select_save.length=0,d.luckysheet_selection_range=[],ae.saveParam("shs",null,d.currentSheetIndex),ve.changeSheet(n),l&&typeof l=="function"&&l()}})},clearSheetByIndex:function(e){let n=Q(e),t=d.luckysheetfile[n];t.isPivotTable?delete d.luckysheetfile[n]:(t.data=[],t.row=d.defaultrowNum,t.column=d.defaultcolumnNum,t.chart=[],t.config=null,t.filter=null,t.filter_select=null,t.celldata=[],t.pivotTable={},t.calcChain=[],t.status=0,t.load=0,d.flowdata=[],Te.webWorkerFlowDataCache(d.flowdata),$("#"+d.container+" .luckysheet-data-visualization-chart").remove(),$("#"+d.container+" .luckysheet-datavisual-selection-set").remove(),$("#luckysheet-row-count-show, #luckysheet-formula-functionrange-select, #luckysheet-row-count-show, #luckysheet-column-count-show, #luckysheet-change-size-line, #luckysheet-cell-selected-focus, #luckysheet-selection-copy, #luckysheet-cell-selected-extend, #luckysheet-cell-selected-move, #luckysheet-cell-selected").hide(),delete t.load)},clear:function(e){let n=this;if(e=="all")for(let t=0;t .luckysheet-cols-menu").remove(),$("#luckysheet-modal-dialog-mask, #luckysheetTextSizeTest, #luckysheet-icon-morebtn-div").remove(),$("#luckysheet-input-box").parent().remove(),$("#luckysheet-formula-help-c").remove(),$(".chartSetting, .luckysheet-modal-dialog-slider").remove(),$(document).off(".luckysheetEvent"),$(document).off(".luckysheetProtection"),Y.initialHorizontal=!0,Y.initialVertical=!0;let e=$.extend(!0,{},Ai.defaultStore);for(let s in e)s in d&&(d[s]=e[s]);let n=$.extend(!0,{},Ai.defaultFormula);for(let s in n)s in m&&(m[s]=n[s]);let t=$.extend(!0,{},Ai.defaultSheet);for(let s in t)s in ve&&(ve[s]=t[s]);let l=$.extend(!0,{},Ai.defaultPivotTable);for(let s in l)s in Xe&&(Xe[s]=l[s]);let a=$.extend(!0,{},Ai.defaultImage);for(let s in a)s in Se&&(Se[s]=a[s]);let o=$.extend(!0,{},Ai.defaultDataVerification);for(let s in o)s in Qe&&(Qe[s]=o[s]);d.asyncLoad=["core"]},editorChart:function(e){let n=er[0],t="luckysheetEditMode-datav-chart",l=t+"_selection";e.chart_id=t;let a=e.chartTheme;a=a==null?"default0000":a,luckysheet.insertChartTosheet(e.sheetIndex,e.dataSheetIndex,e.option,e.chartType,e.selfOption,e.defaultOption,e.row,e.column,n,t,l,e.chartStyle,e.rangeConfigCheck,e.rangeRowCheck,e.rangeColCheck,e.chartMarkConfig,e.chartTitleConfig,e.winWidth,e.winHeight,e.scrollLeft,e.scrollTop,a,e.myWidth,e.myHeight,e.myLeft!=null?parseFloat(e.myLeft):null,e.myTop!=null?parseFloat(e.myTop):null,e.myindexrank,!0),$("#"+t).find(".luckysheet-modal-controll-update").click()},createHookFunction:function(){let e=arguments[0];if(Ae.hook&&Ae.hook[e]!=null&&typeof Ae.hook[e]=="function"){var n=Array.prototype.slice.apply(arguments);return n.shift(),Ae.hook[e].apply(this,n)!==!1}return!0}},$e=by});var ky,He,ln=De(()=>{El();tl();jt();Dn();_t();Wr();wt();dt();Kl();ll();Bl();$t();je();Xl();ky={defaultWidth:144,defaultHeight:84,currentObj:null,currentWinW:null,currentWinH:null,resize:null,resizeXY:null,move:!1,moveXY:null,init:function(){let e=this;$("#luckysheet-postil-showBoxs").off("mousedown.showPs").on("mousedown.showPs",".luckysheet-postil-show",function(n){if(!!Rt(d.currentSheetIndex,"editObjects",!1)){if(e.currentObj=$(this).find(".luckysheet-postil-show-main"),$(this).hasClass("luckysheet-postil-show-active")){n.stopPropagation();return}e.removeActivePs(),$(this).addClass("luckysheet-postil-show-active"),$(this).find(".luckysheet-postil-dialog-resize").show(),$(this).find(".arrowCanvas").css("z-index",200),$(this).find(".luckysheet-postil-show-main").css("z-index",200),n.stopPropagation()}}),$("#luckysheet-postil-showBoxs").off("mouseup.showPs").on("mouseup.showPs",".luckysheet-postil-show",function(n){n.which=="3"&&n.stopPropagation()}),$("#luckysheet-postil-showBoxs").off("mousedown.resize").on("mousedown.resize",".luckysheet-postil-show .luckysheet-postil-dialog-resize .luckysheet-postil-dialog-resize-item",function(n){if(!Rt(d.currentSheetIndex,"editObjects",!1))return;e.currentObj=$(this).closest(".luckysheet-postil-show-main"),e.currentWinW=$("#luckysheet-cell-main")[0].scrollWidth,e.currentWinH=$("#luckysheet-cell-main")[0].scrollHeight,e.resize=$(this).data("type");let t=$("#luckysheet-cell-main").scrollTop(),l=$("#luckysheet-cell-main").scrollLeft(),a=Ke(n.pageX,n.pageY),o=a[0]+l,s=a[1]+t,u=e.currentObj.position(),f=e.currentObj.width(),A=e.currentObj.height();if(e.resizeXY=[o,s,f,A,u.left+l,u.top+t,l,t],Rl(!0),$(this).closest(".luckysheet-postil-show").hasClass("luckysheet-postil-show-active")){n.stopPropagation();return}e.removeActivePs(),$(this).closest(".luckysheet-postil-show").addClass("luckysheet-postil-show-active"),$(this).closest(".luckysheet-postil-show").find(".luckysheet-postil-dialog-resize").show(),$(this).closest(".luckysheet-postil-show").find(".arrowCanvas").css("z-index",200),$(this).closest(".luckysheet-postil-show").find(".luckysheet-postil-show-main").css("z-index",200),n.stopPropagation()}),$("#luckysheet-postil-showBoxs").off("mousedown.move").on("mousedown.move",".luckysheet-postil-show .luckysheet-postil-dialog-move .luckysheet-postil-dialog-move-item",function(n){if(!Rt(d.currentSheetIndex,"editObjects",!1))return;e.currentObj=$(this).closest(".luckysheet-postil-show-main"),e.currentWinW=$("#luckysheet-cell-main")[0].scrollWidth,e.currentWinH=$("#luckysheet-cell-main")[0].scrollHeight,e.move=!0;let t=$("#luckysheet-cell-main").scrollTop(),l=$("#luckysheet-cell-main").scrollLeft(),a=e.currentObj.offset(),o=e.currentObj.position();if(e.moveXY=[n.pageX-a.left,n.pageY-a.top,o.left,o.top,l,t],Rl(!0),$(this).closest(".luckysheet-postil-show").hasClass("luckysheet-postil-show-active")){n.stopPropagation();return}e.removeActivePs(),$(this).closest(".luckysheet-postil-show").addClass("luckysheet-postil-show-active"),$(this).closest(".luckysheet-postil-show").find(".luckysheet-postil-dialog-resize").show(),$(this).closest(".luckysheet-postil-show").find(".arrowCanvas").css("z-index",200),$(this).closest(".luckysheet-postil-show").find(".luckysheet-postil-show-main").css("z-index",200),n.stopPropagation()})},overshow:function(e){let n=this;if($("#luckysheet-postil-overshow").remove(),$(e.target).closest("#luckysheet-cell-main").length==0)return;let t=Ke(e.pageX,e.pageY),l=$("#luckysheet-cell-main").scrollLeft(),a=$("#luckysheet-cell-main").scrollTop(),o=t[0],s=t[1],u=0,f=0;Y.freezenverticaldata!=null&&t[0]0)return;let v=p.value==null?"":p.value,y=d.visibledatarow[A],b=A-1==-1?0:d.visibledatarow[A-1],k=d.visibledatacolumn[h],w=h-1==-1?0:d.visibledatacolumn[h-1];g&&(y=g.row[1],b=g.row[0],k=g.column[1],w=g.column[0]);let C=k+u,I=b+f,x=C+18*d.zoomRatio,T=I-18*d.zoomRatio;T<0&&(T=2);let R=p.width==null?n.defaultWidth*d.zoomRatio:p.width*d.zoomRatio,D=p.height==null?n.defaultHeight*d.zoomRatio:p.height*d.zoomRatio,S=n.getArrowCanvasSize(x,T,C,I),B="",F=v.split(` -`);for(let P of F)B+="
"+n.htmlEscape(P)+"
";let E='
'+B+"
";$(E).appendTo($("#luckysheet-cell-main"));let z=$("#luckysheet-postil-overshow .arrowCanvas").get(0).getContext("2d");n.drawArrow(z,S[4],S[5],S[6],S[7])},getArrowCanvasSize:function(e,n,t,l){let a=t-5;e0&&$("#luckysheet-postil-show_"+e+"_"+n).remove(),t==null)return;let l=this;if(t.isshow==null?!1:t.isshow){let o=d.visibledatarow[e],s=e-1==-1?0:d.visibledatarow[e-1],u=d.visibledatacolumn[n],f=n-1==-1?0:d.visibledatacolumn[n-1],A=be.mergeborer(d.flowdata,e,n);A&&(o=A.row[1],s=A.row[0],u=A.column[1],f=A.column[0]);let h=u,g=s,p=t.left==null?h+18*d.zoomRatio:t.left*d.zoomRatio,v=t.top==null?g-18*d.zoomRatio:t.top*d.zoomRatio,y=t.width==null?l.defaultWidth*d.zoomRatio:t.width*d.zoomRatio,b=t.height==null?l.defaultHeight*d.zoomRatio:t.height*d.zoomRatio,k=t.value==null?"":t.value;v<0&&(v=2);let w=l.getArrowCanvasSize(p,v,h,g),C="",I=k.split(` -`);for(let R of I)C+="
"+l.htmlEscape(R)+"
";let x='
'+C+"
";$(x).appendTo($("#luckysheet-cell-main #luckysheet-postil-showBoxs"));let T=$("#luckysheet-postil-show_"+e+"_"+n+" .arrowCanvas").get(0).getContext("2d");l.drawArrow(T,w[4],w[5],w[6],w[7])}},newPs:function(e,n){if(!Rt(d.currentSheetIndex,"editObjects")||!$e.createHookFunction("commentInsertBefore",e,n))return;let t=this,l=d.visibledatarow[e],a=e-1==-1?0:d.visibledatarow[e-1],o=d.visibledatacolumn[n],s=n-1==-1?0:d.visibledatacolumn[n-1],u=be.mergeborer(d.flowdata,e,n);u&&(l=u.row[1],a=u.row[0],o=u.column[1],s=u.column[0]);let f=o,A=a,h=f+18*d.zoomRatio,g=A-18*d.zoomRatio;g<0&&(g=2);let p=t.defaultWidth*d.zoomRatio,v=t.defaultHeight*d.zoomRatio,y=t.getArrowCanvasSize(h,g,f,A),b='
';$(b).appendTo($("#luckysheet-cell-main #luckysheet-postil-showBoxs"));let k=$("#luckysheet-postil-show_"+e+"_"+n+" .arrowCanvas").get(0).getContext("2d");t.drawArrow(k,y[4],y[5],y[6],y[7]),$("#luckysheet-postil-show_"+e+"_"+n+" .formulaInputFocus").focus(),t.init();let w=Te.deepCopyFlowData(d.flowdata),C=[];w[e][n]==null&&(w[e][n]={}),w[e][n].ps={left:null,top:null,width:null,height:null,value:"",isshow:!1},C.push(e+"_"+n),t.ref(w,C),setTimeout(()=>{$e.createHookFunction("commentInsertAfter",e,n,w[e][n])},0)},editPs:function(e,n){let t=this;if(!!Rt(d.currentSheetIndex,"editObjects")){if($("#luckysheet-postil-show_"+e+"_"+n).length>0)$("#luckysheet-postil-show_"+e+"_"+n).show(),$("#luckysheet-postil-show_"+e+"_"+n).addClass("luckysheet-postil-show-active"),$("#luckysheet-postil-show_"+e+"_"+n).find(".luckysheet-postil-dialog-resize").show();else{let l=d.flowdata[e][n].ps,a=d.visibledatarow[e],o=e-1==-1?0:d.visibledatarow[e-1],s=d.visibledatacolumn[n],u=n-1==-1?0:d.visibledatacolumn[n-1],f=be.mergeborer(d.flowdata,e,n);f&&(a=f.row[1],o=f.row[0],s=f.column[1],u=f.column[0]);let A=s,h=o,g=l.left==null?A+18*d.zoomRatio:l.left*d.zoomRatio,p=l.top==null?h-18*d.zoomRatio:l.top*d.zoomRatio,v=l.width==null?t.defaultWidth*d.zoomRatio:l.width*d.zoomRatio,y=l.height==null?t.defaultHeight*d.zoomRatio:l.height*d.zoomRatio,b=l.value==null?"":l.value;p<0&&(p=2);let k=t.getArrowCanvasSize(g,p,A,h),w="",C=b.split(` -`);for(let T of C)w+="
"+t.htmlEscape(T)+"
";let I='
'+w+"
";$(I).appendTo($("#luckysheet-cell-main #luckysheet-postil-showBoxs"));let x=$("#luckysheet-postil-show_"+e+"_"+n+" .arrowCanvas").get(0).getContext("2d");t.drawArrow(x,k[4],k[5],k[6],k[7])}$("#luckysheet-postil-show_"+e+"_"+n+" .formulaInputFocus").focus(),Rr($("#luckysheet-postil-show_"+e+"_"+n+" .formulaInputFocus").get(0)),t.init()}},delPs:function(e,n){if(!Rt(d.currentSheetIndex,"editObjects")||!$e.createHookFunction("commentDeleteBefore",e,n,d.flowdata[e][n]))return;$("#luckysheet-postil-show_"+e+"_"+n).length>0&&$("#luckysheet-postil-show_"+e+"_"+n).remove();let t=Te.deepCopyFlowData(d.flowdata),l=[];delete t[e][n].ps,l.push(e+"_"+n),this.ref(t,l),setTimeout(()=>{$e.createHookFunction("commentDeleteAfter",e,n,d.flowdata[e][n])},0)},showHidePs:function(e,n){let t=this,l=d.flowdata[e][n].ps,a=l.isshow,o=Te.deepCopyFlowData(d.flowdata),s=[];if(a)o[e][n].ps.isshow=!1,$("#luckysheet-postil-show_"+e+"_"+n).remove();else{o[e][n].ps.isshow=!0;let u=d.visibledatarow[e],f=e-1==-1?0:d.visibledatarow[e-1],A=d.visibledatacolumn[n],h=n-1==-1?0:d.visibledatacolumn[n-1],g=be.mergeborer(d.flowdata,e,n);g&&(u=g.row[1],f=g.row[0],A=g.column[1],h=g.column[0]);let p=$("#luckysheet-cell-main").scrollLeft(),v=$("#luckysheet-cell-main").scrollTop(),y=A,b=f;Y.freezenverticaldata!=null&&y";let S='
'+R+"
";$(S).appendTo($("#luckysheet-cell-main #luckysheet-postil-showBoxs"));let B=$("#luckysheet-postil-show_"+e+"_"+n+" .arrowCanvas").get(0).getContext("2d");t.drawArrow(B,T[4],T[5],T[6],T[7]),t.init()}s.push(e+"_"+n),t.ref(o,s)},showHideAllPs:function(){let e=this,n=Te.deepCopyFlowData(d.flowdata),t=!0,l=[];for(let o=0;o0)if(t){$("#luckysheet-cell-main #luckysheet-postil-showBoxs").empty();for(let o=0;o";let F='
'+S+"
";$(F).appendTo($("#luckysheet-cell-main #luckysheet-postil-showBoxs"));let E=$("#luckysheet-postil-show_"+s+"_"+u+" .arrowCanvas").get(0).getContext("2d");e.drawArrow(E,D[4],D[5],D[6],D[7]),n[s][u].ps.isshow=!0,a.push(l[o])}}e.ref(n,a),e.init()},removeActivePs:function(){if($("#luckysheet-postil-showBoxs .luckysheet-postil-show-active").length>0){let e=$("#luckysheet-postil-showBoxs .luckysheet-postil-show-active").attr("id"),n=e.split("luckysheet-postil-show_")[1].split("_")[0],t=e.split("luckysheet-postil-show_")[1].split("_")[1],l=$("#"+e).find(".formulaInputFocus").html().replaceAll("
",` -`).replaceAll(/<(.*)>.*?|<(.*) \/>/g,"").trim();if(!$e.createHookFunction("commentUpdateBefore",n,t,l)){d.flowdata[n][t].ps.isshow||$("#"+e).remove();return}let a=$.extend(!0,{},d.flowdata[n][t]);$("#"+e).removeClass("luckysheet-postil-show-active"),$("#"+e).find(".luckysheet-postil-dialog-resize").hide(),$("#"+e).find(".arrowCanvas").css("z-index",100),$("#"+e).find(".luckysheet-postil-show-main").css("z-index",100);let o=Te.deepCopyFlowData(d.flowdata),s=[];o[n][t].ps.value=l,s.push(n+"_"+t),this.ref(o,s),o[n][t].ps.isshow||$("#"+e).remove(),setTimeout(()=>{$e.createHookFunction("commentUpdateAfter",n,t,a,o[n][t])},0)}},ref:function(e,n){if(d.clearjfundo&&(d.jfundo.length=0,d.jfredo.push({type:"postil",data:d.flowdata,curdata:e,sheetIndex:d.currentSheetIndex,rc:n})),d.flowdata=e,Te.webWorkerFlowDataCache(d.flowdata),d.luckysheetfile[Q(d.currentSheetIndex)].data=d.flowdata,ae.allowUpdate)for(let t=0;t"&]/g,function(n,t,l){switch(console.log(n,t,l),n){case"<":return"<";case">":return">";case"&":return"&";case'"':return"""}})}},He=ky});function io(e,n,t,l,a){let o={},s,u;if(a==null?(s=d.config,u=d.flowdata):(s=d.luckysheetfile[Q(a)].config,u=d.luckysheetfile[Q(a)].data),u===null||u.length==0)return;let f=s.borderInfo;if(f!=null&&f.length>0)for(let A=0;An&&(w=n),Cl&&(I=l),g=="border-left")for(let x=k;x<=w;x++){if(s.rowhidden!=null&&s.rowhidden[x]!=null)continue;o[x+"_"+C]==null&&(o[x+"_"+C]={}),o[x+"_"+C].l={color:p,style:v};let T=C-1;if(T>=0&&o[x+"_"+T])if(u[x]!=null&&N(u[x][T])=="object"&&u[x][T].mc!=null){let D=u[x][T],S=s.merge[D.mc.r+"_"+D.mc.c];S.c+S.cs-1==T&&(o[x+"_"+T].r={color:p,style:v})}else o[x+"_"+T].r={color:p,style:v};let R=s.merge||{};for(let D in R){let{c:S,r:B,cs:F,rs:E}=R[D];C<=S+F-1&&C>S&&x>=B&&x<=B+E-1&&(o[x+"_"+C].l=null)}}else if(g=="border-right")for(let x=k;x<=w;x++){if(s.rowhidden!=null&&s.rowhidden[x]!=null)continue;o[x+"_"+I]==null&&(o[x+"_"+I]={}),o[x+"_"+I].r={color:p,style:v};let T=I+1;if(T=S&&x>=B&&x<=B+E-1&&(o[x+"_"+I].r=null)}}else if(g=="border-top"){if(s.rowhidden!=null&&s.rowhidden[k]!=null)continue;for(let x=C;x<=I;x++){o[k+"_"+x]==null&&(o[k+"_"+x]={}),o[k+"_"+x].t={color:p,style:v};let T=k-1;if(T>=0&&o[T+"_"+x])if(u[T]!=null&&N(u[T][x])=="object"&&u[T][x].mc!=null){let D=u[T][x],S=s.merge[D.mc.r+"_"+D.mc.c];S.r+S.rs-1==T&&(o[T+"_"+x].b={color:p,style:v})}else o[T+"_"+x].b={color:p,style:v};let R=s.merge||{};for(let D in R){let{c:S,r:B,cs:F,rs:E}=R[D];k<=B+E-1&&k>B&&x>=S&&x<=S+F-1&&(o[k+"_"+x].t=null)}}}else if(g=="border-bottom"){if(s.rowhidden!=null&&s.rowhidden[w]!=null)continue;for(let x=C;x<=I;x++){o[w+"_"+x]==null&&(o[w+"_"+x]={}),o[w+"_"+x].b={color:p,style:v};let T=w+1;if(T=B&&x>=S&&x<=S+F-1&&(o[w+"_"+x].b=null)}}}else if(g=="border-all"){for(let x=k;x<=w;x++)if(!(s.rowhidden!=null&&s.rowhidden[x]!=null))for(let T=C;T<=I;T++){if(u[x]!=null&&N(u[x][T])=="object"&&u[x][T].mc!=null){let R=u[x][T],D=s.merge[R.mc.r+"_"+R.mc.c];D.r==x&&(o[x+"_"+T]==null&&(o[x+"_"+T]={}),o[x+"_"+T].t={color:p,style:v}),D.r+D.rs-1==x&&(o[x+"_"+T]==null&&(o[x+"_"+T]={}),o[x+"_"+T].b={color:p,style:v}),D.c==T&&(o[x+"_"+T]==null&&(o[x+"_"+T]={}),o[x+"_"+T].l={color:p,style:v}),D.c+D.cs-1==T&&(o[x+"_"+T]==null&&(o[x+"_"+T]={}),o[x+"_"+T].r={color:p,style:v})}else o[x+"_"+T]==null&&(o[x+"_"+T]={}),o[x+"_"+T].l={color:p,style:v},o[x+"_"+T].r={color:p,style:v},o[x+"_"+T].t={color:p,style:v},o[x+"_"+T].b={color:p,style:v};if(x==k){let R=k-1;if(R>=0&&o[R+"_"+T])if(u[R]!=null&&N(u[R][T])=="object"&&u[R][T].mc!=null){let D=u[R][T],S=s.merge[D.mc.r+"_"+D.mc.c];S.r+S.rs-1==R&&(o[R+"_"+T].b={color:p,style:v})}else o[R+"_"+T].b={color:p,style:v}}if(x==w){let R=w+1;if(R=0&&o[x+"_"+R])if(u[x]!=null&&N(u[x][R])=="object"&&u[x][R].mc!=null){let D=u[x][R],S=s.merge[D.mc.r+"_"+D.mc.c];S.c+S.cs-1==R&&(o[x+"_"+R].r={color:p,style:v})}else o[x+"_"+R].r={color:p,style:v}}if(T==I){let R=I+1;if(R=0&&o[R+"_"+T])if(u[R]!=null&&N(u[R][T])=="object"&&u[R][T].mc!=null){let D=u[R][T],S=s.merge[D.mc.r+"_"+D.mc.c];S.r+S.rs-1==R&&(o[R+"_"+T].b={color:p,style:v})}else o[R+"_"+T].b={color:p,style:v}}if(x==w){o[x+"_"+T]==null&&(o[x+"_"+T]={}),o[x+"_"+T].b={color:p,style:v};let R=w+1;if(R=0&&o[x+"_"+R])if(u[x]!=null&&N(u[x][R])=="object"&&u[x][R].mc!=null){let D=u[x][R],S=s.merge[D.mc.r+"_"+D.mc.c];S.c+S.cs-1==R&&(o[x+"_"+R].r={color:p,style:v})}else o[x+"_"+R].r={color:p,style:v}}if(T==I){o[x+"_"+T]==null&&(o[x+"_"+T]={}),o[x+"_"+T].r={color:p,style:v};let R=I+1;if(R=0&&o[R+"_"+T]&&delete o[R+"_"+T].b}if(x==w){let R=w+1;R=0&&o[x+"_"+R]&&delete o[x+"_"+R].r}if(T==I){let R=I+1;Rn||vl||s.rowhidden!=null&&s.rowhidden[p]!=null)continue;if(g.l!=null||g.r!=null||g.t!=null||g.b!=null||g.lt_rb!=null||g.rt_lb!=null)if(o[p+"_"+v]==null&&(o[p+"_"+v]={}),u[p]!=null&&N(u[p][v])=="object"&&u[p][v].mc!=null){let y=u[p][v],b=s.merge[y.mc.r+"_"+y.mc.c]||{};if(g.l!=null&&v==b.c){o[p+"_"+v].l={color:g.l.color,style:g.l.style};let k=v-1;if(k>=0&&o[p+"_"+k])if(u[p]!=null&&N(u[p][k])=="object"&&u[p][k].mc!=null){let w=u[p][k],C=s.merge[w.mc.r+"_"+w.mc.c];C.c+C.cs-1==k&&(o[p+"_"+k].r={color:g.l.color,style:g.l.style})}else o[p+"_"+k].r={color:g.l.color,style:g.l.style}}else o[p+"_"+v].l=null;if(g.r!=null&&v==b.c+b.cs-1){o[p+"_"+v].r={color:g.r.color,style:g.r.style};let k=v+1;if(k=0&&o[k+"_"+v])if(u[k]!=null&&N(u[k][v])=="object"&&u[k][v].mc!=null){let w=u[k][v],C=s.merge[w.mc.r+"_"+w.mc.c];C.r+C.rs-1==k&&(o[k+"_"+v].b={color:g.t.color,style:g.t.style})}else o[k+"_"+v].b={color:g.t.color,style:g.t.style}}else o[p+"_"+v].t=null;if(g.b!=null&&p==b.r+b.rs-1){o[p+"_"+v].b={color:g.b.color,style:g.b.style};let k=p+1;if(k=0&&o[p+"_"+y])if(u[p]!=null&&N(u[p][y])=="object"&&u[p][y].mc!=null){let b=u[p][y],k=s.merge[b.mc.r+"_"+b.mc.c];k.c+k.cs-1==y&&(o[p+"_"+y].r={color:g.l.color,style:g.l.style})}else o[p+"_"+y].r={color:g.l.color,style:g.l.style}}else o[p+"_"+v].l=null;if(g.r!=null){o[p+"_"+v].r={color:g.r.color,style:g.r.style};let y=v+1;if(y=0&&o[y+"_"+v])if(u[y]!=null&&N(u[y][v])=="object"&&u[y][v].mc!=null){let b=u[y][v],k=s.merge[b.mc.r+"_"+b.mc.c];k.r+k.rs-1==y&&(o[y+"_"+v].b={color:g.t.color,style:g.t.style})}else o[y+"_"+v].b={color:g.t.color,style:g.t.style}}else o[p+"_"+v].t=null;if(g.b!=null){o[p+"_"+v].b={color:g.b.color,style:g.b.style};let y=p+1;if(y{wt();dt();je()});var Ee,wy,il,Zc=De(()=>{El();ci();La();Pt();Fl();_t();tl();jt();Ar();Bl();el();wt();dt();je();yt();Ee=Ol(En()),wy={iconHtml:'
',typeListHtml:'
${copyCell}
${sequence}
${onlyFormat}
${noFormat}
${day}
${workDay}
${month}
${year}
${chineseNumber}
',copyRange:{},applyRange:{},applyType:null,direction:null,chnNumChar:{\u96F6:0,\u4E00:1,\u4E8C:2,\u4E09:3,\u56DB:4,\u4E94:5,\u516D:6,\u4E03:7,\u516B:8,\u4E5D:9},chnNameValue:{\u5341:{value:10,secUnit:!1},\u767E:{value:100,secUnit:!1},\u5343:{value:1e3,secUnit:!1},\u4E07:{value:1e4,secUnit:!0},\u4EBF:{value:1e8,secUnit:!0}},ChineseToNumber:function(e){let n=this,t=0,l=0,a=0,o=!1,s=e.split("");for(let u=0;u0;){let s=e%10;s==0?o||(o=!0,l=n.chnNumChar2[s]+l):(o=!1,t=n.chnNumChar2[s],t+=n.chnUnitChar[a],l=t+l),a++,e=Math.floor(e/10)}return l},NumberToChinese:function(e){let n=this,t=0,l="",a="",o=!1;if(e==0)return n.chnNumChar2[0];for(;e>0;){let s=e%1e4;o&&(a=n.chnNumChar2[0]+a),l=n.SectionToChinese(s),l+=s!=0?n.chnUnitSection[t]:n.chnUnitSection[0],a=l+a,o=s<1e3&&s>0,e=Math.floor(e/1e4),t++}return a},isChnNumber:function(e){let n=this,t=!0;if(e.length==1)e=="\u65E5"||e in n.chnNumChar?t=!0:t=!1;else{let l=e.split("");for(let a=0;a=n&&a>=t?(o=l,s=a):(o=n,s=t);let u=nl(o)[1],f=nl(o)[0],A=Ft(s)[1],h=Ft(s)[0];$("#luckysheet-dropCell-icon").remove(),$("#luckysheet-cell-main").append(e.iconHtml),$("#luckysheet-dropCell-icon").css({left:A,top:u}),$("#luckysheet-dropCell-icon").mouseover(function(){$(this).css("background-color","#ffe8e8")}).mouseleave(function(){$(this).css("background-color","#f1f1f1")}).mousedown(function(g){$("#luckysheet-dropCell-typeList").remove();let v=te().dropCell;$("body").append(we(e.typeListHtml,{copyCell:v.copyCell,sequence:v.sequence,onlyFormat:v.onlyFormat,noFormat:v.noFormat,day:v.day,workDay:v.workDay,month:v.month,year:v.year,chineseNumber:v.chineseNumber}));let y=e.typeItemHide();!y[0]&&!y[1]&&!y[2]&&!y[3]&&!y[4]&&!y[5]&&!y[6]&&($("#luckysheet-dropCell-typeList .luckysheet-cols-menuitem[data-type=1]").hide(),$("#luckysheet-dropCell-typeList .luckysheet-cols-menuitem[data-type=4]").hide(),$("#luckysheet-dropCell-typeList .luckysheet-cols-menuitem[data-type=5]").hide(),$("#luckysheet-dropCell-typeList .luckysheet-cols-menuitem[data-type=6]").hide(),$("#luckysheet-dropCell-typeList .luckysheet-cols-menuitem[data-type=7]").hide(),$("#luckysheet-dropCell-typeList .luckysheet-cols-menuitem[data-type=8]").hide()),y[2]||($("#luckysheet-dropCell-typeList .luckysheet-cols-menuitem[data-type=4]").hide(),$("#luckysheet-dropCell-typeList .luckysheet-cols-menuitem[data-type=5]").hide(),$("#luckysheet-dropCell-typeList .luckysheet-cols-menuitem[data-type=6]").hide(),$("#luckysheet-dropCell-typeList .luckysheet-cols-menuitem[data-type=7]").hide()),y[3]||$("#luckysheet-dropCell-typeList .luckysheet-cols-menuitem[data-type=8]").hide();let b=$(this).offset().left,k=$(this).offset().top+25,w=$(window).height(),C=$(window).width(),I=$("#luckysheet-dropCell-typeList").width(),x=$("#luckysheet-dropCell-typeList").height();b+I>C&&(b=b-I),k+x>w&&(k=k-x-38),k<0&&(k=0),$("#luckysheet-dropCell-typeList").css({left:b,top:k}).show(),$("#luckysheet-dropCell-icon").mouseleave(function(){$(this).css("backgroundColor","#ffe8e8")});let T=e.applyType;$("#luckysheet-dropCell-typeList .luckysheet-cols-menuitem[data-type="+T+"]").find("span").append(''),g.stopPropagation()}),$(document).off("click.dCtypeList").on("click.dCtypeList","#luckysheet-dropCell-typeList .luckysheet-cols-menuitem",function(){$("#luckysheet-dropCell-typeList .fa-check").remove(),$(this).find("span").append('');let g=$(this).attr("data-type");e.applyType=g,e.update(),$("#luckysheet-dropCell-typeList").hide(),$("#luckysheet-dropCell-icon").css("backgroundColor","#f1f1f1"),$("#luckysheet-dropCell-icon").mouseleave(function(){$(this).css("backgroundColor","#f1f1f1")}),Cl()})},typeItemHide:function(){let e=this,n=e.copyRange,t=n.row[0],l=n.row[1],a=n.column[0],o=n.column[1],s=!1,u=!1,f=!1,A=!1,h=!1,g=!1,p=!1;for(let v=t;v<=l;v++)for(let y=a;y<=o;y++)if(d.flowdata[v][y]){let b=d.flowdata[v][y];N(b)=="object"&&b.v!=null&&b.f==null&&(b.ct!=null&&b.ct.t=="n"?s=!0:b.ct!=null&&b.ct.t=="d"?f=!0:e.isExtendNumber(b.m)[0]?u=!0:e.isChnNumber(b.m)&&b.m!="\u65E5"?A=!0:b.m=="\u65E5"?h=!0:e.isChnWeek2(b.m)?g=!0:e.isChnWeek3(b.m)&&(p=!0))}return[s,u,f,A,h,g,p]},update:function(){let e=this;if(!xr([e.applyRange],d.currentSheetIndex)||d.allowEdit===!1)return;let n=Te.deepCopyFlowData(d.flowdata),t=d.luckysheetfile[Q(d.currentSheetIndex)],l=$.extend(!0,{},d.config),a=rn(),o=$.extend(!0,{},t.dataVerification),s=e.direction,u=e.applyType,f=e.copyRange,A=f.row[0],h=f.row[1],g=f.column[0],p=f.column[1],v=e.getCopyData(n,A,h,g,p,s),y;s=="down"||s=="up"?y=h-A+1:(s=="right"||s=="left")&&(y=p-g+1);let b=e.applyRange,k=b.row[0],w=b.row[1],C=b.column[0],I=b.column[1];if(s=="down"||s=="up"){let R=w-k+1;for(let D=C;D<=I;D++){let S=v[D-C],B=e.getApplyData(S,y,R);if(s=="down")for(let F=k;F<=w;F++){let E=B[F-k];if(E.f!=null){let L="="+m.functionCopy(E.f,"down",F-k+1),O=m.execfunction(L,F,D);if(m.execFunctionGroup(F,D,O[1],void 0,n),E.f=O[2],E.v=O[1],E.spl!=null)E.spl=O[3].data;else if(V(E.v)&&!/^\d{6}(18|19|20)?\d{2}(0[1-9]|1[12])(0[1-9]|[12]\d|3[01])\d{3}(\d|X)$/i.test(E.v)){if(E.v==Infinity||E.v==-Infinity)E.m=E.v.toString();else if(E.v.toString().indexOf("e")>-1){let q=E.v.toString().split(".")[1].split("e")[0].length;q>5&&(q=5),E.m=E.v.toExponential(q).toString()}else{let q;E.ct.fa==="##0.00"?(q=it(Math.round(E.v*1e9)/1e9+".00"),E.m=q[0].toString()):(q=it(Math.round(E.v*1e9)/1e9),E.m=q[0].toString())}E.ct=E.ct||{fa:"General",t:"n"}}else{let q=it(E.v);E.m=q[0].toString(),E.ct=q[1]}}n[F][D]=E;let z=A+(F-k)%y,P=D;if(a[z+"_"+P]){let L={rangeType:"cell",value:{row_index:F,col_index:D,l:a[z+"_"+P].l,r:a[z+"_"+P].r,t:a[z+"_"+P].t,b:a[z+"_"+P].b},timestamp:new Date().getTime()};l.borderInfo.push(L)}else if(a[F+"_"+D]){let L={rangeType:"cell",value:{row_index:F,col_index:D,l:null,r:null,t:null,b:null},timestamp:new Date().getTime()};l.borderInfo.push(L)}o[z+"_"+P]&&(o[F+"_"+D]=o[z+"_"+P])}if(s=="up")for(let F=w;F>=k;F--){let E=B[w-F];if(E.f!=null){let L="="+m.functionCopy(E.f,"up",w-F+1),O=m.execfunction(L,F,D);if(m.execFunctionGroup(F,D,O[1],void 0,n),E.f=O[2],E.v=O[1],E.spl!=null)E.spl=O[3].data;else if(V(E.v)&&!/^\d{6}(18|19|20)?\d{2}(0[1-9]|1[12])(0[1-9]|[12]\d|3[01])\d{3}(\d|X)$/i.test(E.v)){if(E.v==Infinity||E.v==-Infinity)E.m=E.v.toString();else if(E.v.toString().indexOf("e")>-1){let q=E.v.toString().split(".")[1].split("e")[0].length;q>5&&(q=5),E.m=E.v.toExponential(q).toString()}else{let q=it(Math.round(E.v*1e9)/1e9);E.m=q[0].toString()}E.ct={fa:"General",t:"n"}}else{let q=it(E.v);E.m=q[0].toString(),E.ct=q[1]}}n[F][D]=E;let z=h-(w-F)%y,P=D;if(a[z+"_"+P]){let L={rangeType:"cell",value:{row_index:F,col_index:D,l:a[z+"_"+P].l,r:a[z+"_"+P].r,t:a[z+"_"+P].t,b:a[z+"_"+P].b},timestamp:new Date().getTime()};l.borderInfo.push(L)}else if(a[F+"_"+D]){let L={rangeType:"cell",value:{row_index:F,col_index:D,l:null,r:null,t:null,b:null},timestamp:new Date().getTime()};l.borderInfo.push(L)}o[z+"_"+P]&&(o[F+"_"+D]=o[z+"_"+P])}}}else if(s=="right"||s=="left"){let R=I-C+1;for(let D=k;D<=w;D++){let S=v[D-k],B=e.getApplyData(S,y,R);if(s=="right")for(let F=C;F<=I;F++){let E=B[F-C];if(E.f!=null){let L="="+m.functionCopy(E.f,"right",F-C+1),O=m.execfunction(L,D,F);if(m.execFunctionGroup(D,F,O[1],void 0,n),E.f=O[2],E.v=O[1],E.spl!=null)E.spl=O[3].data;else if(V(E.v)&&!/^\d{6}(18|19|20)?\d{2}(0[1-9]|1[12])(0[1-9]|[12]\d|3[01])\d{3}(\d|X)$/i.test(E.v)){if(E.v==Infinity||E.v==-Infinity)E.m=E.v.toString();else if(E.v.toString().indexOf("e")>-1){let q=E.v.toString().split(".")[1].split("e")[0].length;q>5&&(q=5),E.m=E.v.toExponential(q).toString()}else{let q=it(Math.round(E.v*1e9)/1e9);E.m=q[0].toString()}E.ct={fa:"General",t:"n"}}else{let q=it(E.v);E.m=q[0].toString(),E.ct=q[1]}}n[D][F]=E;let z=D,P=g+(F-C)%y;if(a[z+"_"+P]){let L={rangeType:"cell",value:{row_index:D,col_index:F,l:a[z+"_"+P].l,r:a[z+"_"+P].r,t:a[z+"_"+P].t,b:a[z+"_"+P].b},timestamp:new Date().getTime()};l.borderInfo.push(L)}else if(a[D+"_"+F]){let L={rangeType:"cell",value:{row_index:D,col_index:F,l:null,r:null,t:null,b:null}};l.borderInfo.push(L)}o[z+"_"+P]&&(o[D+"_"+F]=o[z+"_"+P])}if(s=="left")for(let F=I;F>=C;F--){let E=B[I-F];if(E.f!=null){let L="="+m.functionCopy(E.f,"left",I-F+1),O=m.execfunction(L,D,F);if(m.execFunctionGroup(D,F,O[1],void 0,n),E.f=O[2],E.v=O[1],E.spl!=null)E.spl=O[3].data;else if(V(E.v)&&!/^\d{6}(18|19|20)?\d{2}(0[1-9]|1[12])(0[1-9]|[12]\d|3[01])\d{3}(\d|X)$/i.test(E.v)){if(E.v==Infinity||E.v==-Infinity)E.m=E.v.toString();else if(E.v.toString().indexOf("e")>-1){let q=E.v.toString().split(".")[1].split("e")[0].length;q>5&&(q=5),E.m=E.v.toExponential(q).toString()}else{let q=it(Math.round(E.v*1e9)/1e9);E.m=q[0].toString()}E.ct={fa:"General",t:"n"}}else{let q=it(E.v);E.m=q[0].toString(),E.ct=q[1]}}n[D][F]=E;let z=D,P=p-(I-F)%y;if(a[z+"_"+P]){let L={rangeType:"cell",value:{row_index:D,col_index:F,l:a[z+"_"+P].l,r:a[z+"_"+P].r,t:a[z+"_"+P].t,b:a[z+"_"+P].b},timestamp:new Date().getTime()};l.borderInfo.push(L)}else if(a[D+"_"+F]){let L={rangeType:"cell",value:{row_index:D,col_index:F,l:null,r:null,t:null,b:null},timestamp:new Date().getTime()};l.borderInfo.push(L)}o[z+"_"+P]&&(o[D+"_"+F]=o[z+"_"+P])}}}let x=$.extend(!0,[],t.luckysheet_conditionformat_save);if(x!=null&&x.length>0)for(let R=0;R0&&(S=S.concat(F))}S.length>0&&x[R].cellrange.push(b)}let T={cfg:l,cdformat:x,dataVerification:o};Je(n,d.luckysheet_select_save,T),at()},getCopyData:function(e,n,t,l,a,o){let s=this,u=[],f,A,h,g;o=="down"||o=="up"?(f=l,A=a,h=n,g=t):(o=="right"||o=="left")&&(f=n,A=t,h=l,g=a);for(let p=f;p<=A;p++){let v={},y=[],b=[],k="",w=null,C=null,I=!0;for(let x=h;x<=g;x++){let T;o=="down"||o=="up"?T=e[x][p]:(o=="right"||o=="left")&&(T=e[p][x]);let R;if(!!T&&!!T.v&&T.f==null)if(!!T.ct&&T.ct.t=="n")R="number",w=null,C=null;else if(!!T.ct&&T.ct.t=="d")R="date",w=null,C=null;else if(s.isExtendNumber(T.m)[0]){R="extendNumber";let D=s.isExtendNumber(T.m);w==null||C==null?(I=!0,w=D[2],C=D[3]):D[2]!=w||D[3]!=C?(I=!1,w=D[2],C=D[3]):I=!0}else s.isChnNumber(T.m)?(R="chnNumber",w=null,C=null):s.isChnWeek2(T.m)?(R="chnWeek2",w=null,C=null):s.isChnWeek3(T.m)?(R="chnWeek3",w=null,C=null):(R="other",w=null,C=null);else R="other",w=null,C=null;R=="extendNumber"?x==h?h==g?(k=R,y.push(T),b.push(x-h+1),v[k]=[],v[k].push({data:y,index:b})):(k=R,y.push(T),b.push(x-h+1)):x==g?k==R&&I?(y.push(T),b.push(x-h+1),k in v?v[k].push({data:y,index:b}):(v[k]=[],v[k].push({data:y,index:b}))):(k in v?v[k].push({data:y,index:b}):(v[k]=[],v[k].push({data:y,index:b})),k=R,y=[],y.push(T),b=[],b.push(x-h+1),k in v?v[k].push({data:y,index:b}):(v[k]=[],v[k].push({data:y,index:b}))):k==R&&I?(y.push(T),b.push(x-h+1)):(k in v?v[k].push({data:y,index:b}):(v[k]=[],v[k].push({data:y,index:b})),k=R,y=[],y.push(T),b=[],b.push(x-h+1)):x==h?h==g?(k=R,y.push(T),b.push(x-h+1),v[k]=[],v[k].push({data:y,index:b})):(k=R,y.push(T),b.push(x-h+1)):x==g?k==R?(y.push(T),b.push(x-h+1),k in v?v[k].push({data:y,index:b}):(v[k]=[],v[k].push({data:y,index:b}))):(k in v?v[k].push({data:y,index:b}):(v[k]=[],v[k].push({data:y,index:b})),k=R,y=[],y.push(T),b=[],b.push(x-h+1),k in v?v[k].push({data:y,index:b}):(v[k]=[],v[k].push({data:y,index:b}))):k==R?(y.push(T),b.push(x-h+1)):(k in v?v[k].push({data:y,index:b}):(v[k]=[],v[k].push({data:y,index:b})),k=R,y=[],y.push(T),b=[],b.push(x-h+1))}u.push(v)}return u},getApplyData:function(e,n,t){let l=this,a=[],o=l.direction,s=l.applyType,u=Math.floor(t/n),f=t%n,A=e.number,h=[];if(A)for(let D=0;D0)for(let S=0;S0)for(let S=0;S0)for(let S=0;S0)for(let S=0;S0)for(let S=0;S0)for(let S=0;S0)for(let S=0;S0){for(let u=1;u<=a;u++)for(let f=0;f0&&o.ChineseToNumber(e[h].m)<7?f.push(o.ChineseToNumber(e[h].m)+A*7):f.push(o.ChineseToNumber(e[h].m));if((t=="up"||t=="left")&&(e.reverse(),f.reverse()),o.isEqualDiff(f))if(u||f[f.length-1]<6&&f[0]>0||f[0]<6&&f[f.length-1]>0){let h=f[1]-f[0];s=o.FillChnWeek(e,n,h)}else{let h=f[1]-f[0];s=o.FillChnNumber(e,n,h)}else s=o.FillCopy(e,n)}else if(a=="chnWeek2")if(e.length==1){let u;t=="down"||t=="right"?u=1:(t=="up"||t=="left")&&(u=-1),s=o.FillChnWeek2(e,n,u)}else{let u=[],f=0;for(let A=0;A7){let u;t=="down"||t=="right"?u=1:(t=="up"||t=="left")&&(u=-1,e.reverse());let f;for(let A=1;A<=n;A++){let h=(A-1)%e.length,g=$.extend(!0,{},e[h]),p=Math.ceil(A/e.length);h==0&&(f=(0,Ee.default)(g.m).add(u*p,"months").diff((0,Ee.default)(g.m),"days"));let v=(0,Ee.default)(g.m).add(f,"days").day(),y;v==0?y=(0,Ee.default)(g.m).add(f,"days").subtract(2,"days").format("YYYY-MM-DD"):v==6?y=(0,Ee.default)(g.m).add(f,"days").subtract(1,"days").format("YYYY-MM-DD"):y=(0,Ee.default)(g.m).add(f,"days").format("YYYY-MM-DD"),g.m=y,g.v=it(y)[2],s.push(g)}}else{let u;t=="down"||t=="right"?u=7:(t=="up"||t=="left")&&(u=-7,e.reverse());let f;for(let A=1;A<=n;A++){let h=(A-1)%e.length,g=$.extend(!0,{},e[h]),p=Math.ceil(A/e.length);h==0&&(f=(0,Ee.default)(g.m).add(u*p,"days").diff((0,Ee.default)(g.m),"days"));let v=(0,Ee.default)(g.m).add(f,"days").day(),y;v==0?y=(0,Ee.default)(g.m).add(f,"days").subtract(2,"days").format("YYYY-MM-DD"):v==6?y=(0,Ee.default)(g.m).add(f,"days").subtract(1,"days").format("YYYY-MM-DD"):y=(0,Ee.default)(g.m).add(f,"days").format("YYYY-MM-DD"),g.m=y,g.v=it(y)[2],s.push(g)}}else{let u=o.judgeDate(e);if(u[0]&&u[3]){(t=="up"||t=="left")&&e.reverse();let f=(0,Ee.default)(e[1].m).diff((0,Ee.default)(e[0].m),"months");for(let A=1;A<=n;A++){let h=(A-1)%e.length,g=$.extend(!0,{},e[h]),p=(0,Ee.default)(e[e.length-1].m).add(f*A,"months").day(),v;p==0?v=(0,Ee.default)(e[e.length-1].m).add(f*A,"months").subtract(2,"days").format("YYYY-MM-DD"):p==6?v=(0,Ee.default)(e[e.length-1].m).add(f*A,"months").subtract(1,"days").format("YYYY-MM-DD"):v=(0,Ee.default)(e[e.length-1].m).add(f*A,"months").format("YYYY-MM-DD"),g.m=v,g.v=it(v)[2],s.push(g)}}else if(!u[0]&&u[2])if(Math.abs((0,Ee.default)(e[1].m).diff((0,Ee.default)(e[0].m)))>7){let f;t=="down"||t=="right"?f=1:(t=="up"||t=="left")&&(f=-1,e.reverse());let A;for(let h=1;h<=n;h++){let g=(h-1)%e.length,p=$.extend(!0,{},e[g]),v=Math.ceil(h/e.length);g==0&&(A=(0,Ee.default)(p.m).add(f*v,"months").diff((0,Ee.default)(p.m),"days"));let y=(0,Ee.default)(p.m).add(A,"days").day(),b;y==0?b=(0,Ee.default)(p.m).add(A,"days").subtract(2,"days").format("YYYY-MM-DD"):y==6?b=(0,Ee.default)(p.m).add(A,"days").subtract(1,"days").format("YYYY-MM-DD"):b=(0,Ee.default)(p.m).add(A,"days").format("YYYY-MM-DD"),p.m=b,p.v=it(b)[2],s.push(p)}}else{let f;t=="down"||t=="right"?f=7:(t=="up"||t=="left")&&(f=-7,e.reverse());let A;for(let h=1;h<=n;h++){let g=(h-1)%e.length,p=$.extend(!0,{},e[g]),v=Math.ceil(h/e.length);g==0&&(A=(0,Ee.default)(p.m).add(f*v,"days").diff((0,Ee.default)(p.m),"days"));let y=(0,Ee.default)(p.m).add(A,"days").day(),b;y==0?b=(0,Ee.default)(p.m).add(A,"days").subtract(2,"days").format("YYYY-MM-DD"):y==6?b=(0,Ee.default)(p.m).add(A,"days").subtract(1,"days").format("YYYY-MM-DD"):b=(0,Ee.default)(p.m).add(A,"days").format("YYYY-MM-DD"),p.m=b,p.v=it(b)[2],s.push(p)}}else(t=="up"||t=="left")&&e.reverse(),s=o.FillCopy(e,n)}else if(l=="6")if(e.length==1){let u;t=="down"||t=="right"?u=1:(t=="up"||t=="left")&&(u=-1),s=o.FillMonths(e,n,u)}else if(e.length==2)if((0,Ee.default)(e[1].m).date()==(0,Ee.default)(e[0].m).date()&&(0,Ee.default)(e[1].m).diff((0,Ee.default)(e[0].m),"months")!=0){(t=="up"||t=="left")&&e.reverse();let u=(0,Ee.default)(e[1].m).diff((0,Ee.default)(e[0].m),"months");s=o.FillMonths(e,n,u)}else{let u;t=="down"||t=="right"?u=1:(t=="up"||t=="left")&&(u=-1,e.reverse());let f;for(let A=1;A<=n;A++){let h=(A-1)%e.length,g=$.extend(!0,{},e[h]),p=Math.ceil(A/e.length);h==0&&(f=(0,Ee.default)(g.m).add(u*p,"months").diff((0,Ee.default)(g.m),"days"));let v=(0,Ee.default)(g.m).add(f,"days").format("YYYY-MM-DD");g.m=v,g.v=it(v)[2],s.push(g)}}else{let u=o.judgeDate(e);if(u[0]&&u[3]){(t=="up"||t=="left")&&e.reverse();let f=(0,Ee.default)(e[1].m).diff((0,Ee.default)(e[0].m),"months");s=o.FillMonths(e,n,f)}else if(!u[0]&&u[2]){let f;t=="down"||t=="right"?f=1:(t=="up"||t=="left")&&(f=-1,e.reverse());let A;for(let h=1;h<=n;h++){let g=(h-1)%e.length,p=$.extend(!0,{},e[g]),v=Math.ceil(h/e.length);g==0&&(A=(0,Ee.default)(p.m).add(f*v,"months").diff((0,Ee.default)(p.m),"days"));let y=(0,Ee.default)(p.m).add(A,"days").format("YYYY-MM-DD");p.m=y,p.v=it(y)[2],s.push(p)}}else(t=="up"||t=="left")&&e.reverse(),s=o.FillCopy(e,n)}else if(l=="7")if(e.length==1){let u;t=="down"||t=="right"?u=1:(t=="up"||t=="left")&&(u=-1),s=o.FillYears(e,n,u)}else if(e.length==2)if((0,Ee.default)(e[1].m).date()==(0,Ee.default)(e[0].m).date()&&(0,Ee.default)(e[1].m).month()==(0,Ee.default)(e[0].m).month()&&(0,Ee.default)(e[1].m).diff((0,Ee.default)(e[0].m),"years")!=0){(t=="up"||t=="left")&&e.reverse();let u=(0,Ee.default)(e[1].m).diff((0,Ee.default)(e[0].m),"years");s=o.FillYears(e,n,u)}else{let u;t=="down"||t=="right"?u=1:(t=="up"||t=="left")&&(u=-1,e.reverse());let f;for(let A=1;A<=n;A++){let h=(A-1)%e.length,g=$.extend(!0,{},e[h]),p=Math.ceil(A/e.length);h==0&&(f=(0,Ee.default)(g.m).add(u*p,"years").diff((0,Ee.default)(g.m),"days"));let v=(0,Ee.default)(g.m).add(f,"days").format("YYYY-MM-DD");g.m=v,g.v=it(v)[2],s.push(g)}}else{let u=o.judgeDate(e);if(u[0]&&u[1]&&u[4]){(t=="up"||t=="left")&&e.reverse();let f=(0,Ee.default)(e[1].m).diff((0,Ee.default)(e[0].m),"years");s=o.FillYears(e,n,f)}else if(u[0]&&u[3]||u[2]){let f;t=="down"||t=="right"?f=1:(t=="up"||t=="left")&&(f=-1,e.reverse());let A;for(let h=1;h<=n;h++){let g=(h-1)%e.length,p=$.extend(!0,{},e[g]),v=Math.ceil(h/e.length);g==0&&(A=(0,Ee.default)(p.m).add(f*v,"years").diff((0,Ee.default)(p.m),"days"));let y=(0,Ee.default)(p.m).add(A,"days").format("YYYY-MM-DD");p.m=y,p.v=it(y)[2],s.push(p)}}else(t=="up"||t=="left")&&e.reverse(),s=o.FillCopy(e,n)}else if(l=="8")if(e.length==1){let u;t=="down"||t=="right"?u=1:(t=="up"||t=="left")&&(u=-1),s=o.FillChnNumber(e,n,u)}else{let u=[];for(let f=0;f2&&l.isEqualRatio(o))for(let s=1;s<=n;s++){let u=(s-1)%e.length,f=$.extend(!0,{},e[u]),A;t=="down"||t=="right"?A=Number(e[e.length-1].v)*Math.pow(Number(e[1].v)/Number(e[0].v),s):(t=="up"||t=="left")&&(A=Number(e[0].v)/Math.pow(Number(e[1].v)/Number(e[0].v),s)),f.v=A,f.m=ht(f.ct.fa,A),a.push(f)}else{let s=l.getXArr(e.length);for(let u=1;u<=n;u++){let f=(u-1)%e.length,A=$.extend(!0,{},e[f]),h;t=="down"||t=="right"?h=l.forecast(e.length+u,o,s):(t=="up"||t=="left")&&(h=l.forecast(1-u,o,s)),A.v=h,A.m=ht(A.ct.fa,h),a.push(A)}}return a},FillExtendNumber:function(e,n,t){let l=this,a=[],o=/0|([1-9]+[0-9]*)/g;for(let s=1;s<=n;s++){let u=(s-1)%e.length,f=$.extend(!0,{},e[u]),A=e[e.length-1].m,h=A.match(o),g=h[h.length-1],p=Math.abs(Number(g)+t*s),v=A.lastIndexOf(g),y=A.substr(0,v)+p.toString()+A.substr(v+g.length);f.v=y,f.m=y,a.push(f)}return a},FillOnlyFormat:function(e,n){let t=[];for(let l=1;l<=n;l++){let a=(l-1)%e.length,o=$.extend(!0,{},e[a]);delete o.f,delete o.m,delete o.v,t.push(o)}return t},FillWithoutFormat:function(e){let n=[];for(let t=0;t{wt();Br();el();zn();$t();ll();ln();Zc();El();je();yt();_t();bn={freezenHorizontalHTML:'
 
',freezenVerticalHTML:'
 
',initialHorizontal:!0,initialVertical:!0,horizontalmovestate:!1,horizontalmoveposition:null,verticalmovestate:!1,verticalmoveposition:null,windowHeight:null,windowWidth:null,freezenhorizontaldata:null,freezenverticaldata:null,freezenRealFirstRowColumn:!0,cutVolumn:function(e,n){if(n<=0)return e;let t=e.slice(0,n),l=t[t.length-1];return e.slice(n)},cancelFreezenVertical:function(e){let n=this,l=te().freezen,a=` -
-
-
- -
-
- ${l.default} -
-
-
- `;$("#luckysheet-freezen-btn-horizontal").html(a),$("#luckysheet-freezen-btn-vertical").html(' '+l.freezenColumn),n.freezenverticaldata=null;let o=$("#luckysheet-freezebar-vertical").is(":visible");$("#luckysheet-freezebar-vertical").hide(),e==null&&(e=d.currentSheetIndex);let s=d.luckysheetfile[Q(e)];s.freezen!=null&&(s.freezen.vertical=null),s.frozen!=null&&o&&ae.saveParam("all",e,s.frozen,{k:"frozen"})},createFreezenVertical:function(e,n){let t=this;if(t.initialVertical){t.initialVertical=!1,$("#luckysheet-grid-window-1").append(t.freezenVerticalHTML),$("#luckysheet-freezebar-vertical").find(".luckysheet-freezebar-vertical-drop").hover(function(){$(this).parent().addClass("luckysheet-freezebar-hover")},function(){$(this).parent().removeClass("luckysheet-freezebar-hover")}),$("#luckysheet-freezebar-vertical").find(".luckysheet-freezebar-vertical-drop").mousedown(function(){t.verticalmovestate=!0,t.verticalmoveposition=$(this).position().left,t.windowWidth=$("#luckysheet-grid-window-1").width(),$(this).parent().addClass("luckysheet-freezebar-active"),$("#luckysheet-freezebar-vertical").find(".luckysheet-freezebar-vertical-handle").css("cursor","-webkit-grabbing")});let a=$("#luckysheet-grid-window-1").height();$("#luckysheet-freezebar-vertical").find(".luckysheet-freezebar-vertical-handle").css({height:a-10,width:"4px",cursor:"-webkit-grab",top:"0px"}).end().find(".luckysheet-freezebar-vertical-drop").css({height:a-10,width:"4px",top:"0px",cursor:"-webkit-grab"})}if(e==null){if(t.freezenRealFirstRowColumn){let a=0;n=d.visibledatacolumn[a]-2+d.rowHeaderWidth,e=[d.visibledatacolumn[a],a+1,0,t.cutVolumn(d.visibledatacolumn,a+1),n]}else{let a=$("#luckysheet-cell-main").scrollLeft(),o=nt(d.visibledatacolumn,a);o==-1&&(o=0),n=d.visibledatacolumn[o]-2-a+d.rowHeaderWidth,e=[d.visibledatacolumn[o],o+1,a,t.cutVolumn(d.visibledatacolumn,o+1),n]}t.saveFreezen(null,null,e,n)}t.freezenverticaldata=e;let l=` -
-
-
- -
-
- ${te().freezen.freezenCancel} -
-
-
- `;$("#luckysheet-freezen-btn-horizontal").html(l),$("#luckysheet-freezebar-vertical").show().find(".luckysheet-freezebar-vertical-handle").css({left:n}).end().find(".luckysheet-freezebar-vertical-drop").css({left:n})},saveFreezen:function(e,n,t,l){let a=d.luckysheetfile[Q(d.currentSheetIndex)];a.freezen==null&&(a.freezen={}),e!=null&&(a.freezen.horizontal==null&&(a.freezen.horizontal={}),a.freezen.horizontal.freezenhorizontaldata=e,a.freezen.horizontal.top=n),t!=null&&(a.freezen.vertical==null&&(a.freezen.vertical={}),a.freezen.vertical.freezenverticaldata=t,a.freezen.vertical.left=l),a.frozen!=null&&ae.saveParam("all",d.currentSheetIndex,a.frozen,{k:"frozen"})},initialFreezen:function(e){let n=this;n.frozenTofreezen();let t=d.luckysheetfile[Q(e)];t.freezen!=null&&t.freezen.horizontal!=null&&t.freezen.horizontal.freezenhorizontaldata!=null?n.createFreezenHorizontal(t.freezen.horizontal.freezenhorizontaldata,t.freezen.horizontal.top):n.cancelFreezenHorizontal(e),t.freezen!=null&&t.freezen.vertical!=null&&t.freezen.vertical.freezenverticaldata!=null?n.createFreezenVertical(t.freezen.vertical.freezenverticaldata,t.freezen.vertical.left):n.cancelFreezenVertical(e),n.createAssistCanvas()},changeFreezenIndex:function(e,n){let t=this;if(n=="v"&&t.freezenverticaldata!=null){let l=t.freezenverticaldata[1],a=nt(d.visibledatacolumn,$("#luckysheet-cell-main").scrollLeft());e-a=d.visibledatacolumn.length&&(l=d.visibledatacolumn.length-1),o>=d.visibledatacolumn.length&&(o=d.visibledatacolumn.length-1);let f=d.visibledatacolumn[l],A=d.visibledatacolumn[o];f<=A+u&&setTimeout(function(){$("#luckysheet-scrollbar-x").scrollLeft(0)},100)}if(e.freezenhorizontaldata!=null){let o=e.freezenhorizontaldata[1],s=nt(e.freezenhorizontaldata[3],$("#luckysheet-cell-main").scrollTop()),u=e.freezenhorizontaldata[4];o+=s,n>=d.visibledatarow.length&&(n=d.visibledatarow.length-1),o>=d.visibledatarow.length&&(o=d.visibledatarow.length-1);let f=d.visibledatarow[n],A=d.visibledatarow[o];f<=A+u&&setTimeout(function(){$("#luckysheet-scrollbar-y").scrollTop(0)},100)}},cancelFreezenHorizontal:function(e){let n=this,t=` -
-
-
- -
-
- ${te().freezen.default} -
-
-
- `;$("#luckysheet-freezen-btn-horizontal").html(t),n.freezenhorizontaldata=null;let l=$("#luckysheet-freezebar-horizontal").is(":visible");$("#luckysheet-freezebar-horizontal").hide(),e==null&&(e=d.currentSheetIndex);let a=d.luckysheetfile[Q(e)];a.freezen!=null&&(a.freezen.horizontal=null),a.frozen!=null&&l&&ae.saveParam("all",e,a.frozen,{k:"frozen"})},createFreezenHorizontal:function(e,n){let t=this;if(t.initialHorizontal){t.initialHorizontal=!1,$("#luckysheet-grid-window-1").append(t.freezenHorizontalHTML),$("#luckysheet-freezebar-horizontal").find(".luckysheet-freezebar-horizontal-drop").hover(function(){$(this).parent().addClass("luckysheet-freezebar-hover")},function(){$(this).parent().removeClass("luckysheet-freezebar-hover")}),$("#luckysheet-freezebar-horizontal").find(".luckysheet-freezebar-horizontal-drop").mousedown(function(){t.horizontalmovestate=!0,t.horizontalmoveposition=$(this).position().top,t.windowHeight=$("#luckysheet-grid-window-1").height(),$(this).parent().addClass("luckysheet-freezebar-active"),$("#luckysheet-freezebar-horizontal").find(".luckysheet-freezebar-horizontal-handle").css("cursor","-webkit-grabbing")});let a=$("#luckysheet-grid-window-1").width();$("#luckysheet-freezebar-horizontal").find(".luckysheet-freezebar-horizontal-handle").css({width:a-10,height:"4px",cursor:"-webkit-grab",left:"0px"}).end().find(".luckysheet-freezebar-horizontal-drop").css({width:a-10,height:"4px",left:"0px",cursor:"-webkit-grab"})}if(e==null){let a;if(t.freezenRealFirstRowColumn)a=0,n=d.visibledatarow[a]-2+d.columnHeaderHeight,e=[d.visibledatarow[a],a+1,0,t.cutVolumn(d.visibledatarow,a+1),n],t.saveFreezen(e,n,null,null),setTimeout(()=>{bn.createAssistCanvas(),Ue()});else{let o=$("#luckysheet-cell-main").scrollTop();a=nt(d.visibledatarow,o),a==-1&&(a=0),n=d.visibledatarow[a]-2-o+d.columnHeaderHeight,e=[d.visibledatarow[a],a+1,o,t.cutVolumn(d.visibledatarow,a+1),n],t.saveFreezen(e,n,null,null)}}t.freezenhorizontaldata=e;let l=` -
-
-
- -
-
- ${te().freezen.freezenCancel} -
-
-
- `;$("#luckysheet-freezen-btn-horizontal").html(l),$("#luckysheet-freezebar-horizontal").show().find(".luckysheet-freezebar-horizontal-handle").css({top:n}).end().find(".luckysheet-freezebar-horizontal-drop").css({top:n})},createAssistCanvas:function(){let e=this;if(e.removeAssistCanvas(),e.freezenverticaldata!=null||e.freezenhorizontaldata!=null){let n,t,l,a,o,s,u=d.luckysheetTableContentHW[0],f=d.luckysheetTableContentHW[1];e.freezenverticaldata!=null&&e.freezenhorizontaldata!=null?(n=e.freezenhorizontaldata[0],t=e.freezenhorizontaldata[1],l=e.freezenhorizontaldata[2],a=e.freezenverticaldata[0],o=e.freezenverticaldata[1],s=e.freezenverticaldata[2],e.createCanvas("freezen_3",a-s,n-l+1,d.rowHeaderWidth-1,d.columnHeaderHeight-1),e.createCanvas("freezen_4",u-a+s,n-l+1,a-s+d.rowHeaderWidth-1,d.columnHeaderHeight-1),e.createCanvas("freezen_7",a-s,f-n+l-d.columnHeaderHeight,d.rowHeaderWidth-1,n-l+d.columnHeaderHeight-1)):e.freezenhorizontaldata!=null?(n=e.freezenhorizontaldata[0],t=e.freezenhorizontaldata[1],l=e.freezenhorizontaldata[2],e.createCanvas("freezen_h",u,n-l+1,d.rowHeaderWidth-1,d.columnHeaderHeight-1)):e.freezenverticaldata!=null&&(a=e.freezenverticaldata[0],o=e.freezenverticaldata[1],s=e.freezenverticaldata[2],e.createCanvas("freezen_v",a-s,f,d.rowHeaderWidth-1,d.columnHeaderHeight-1)),e.scrollAdapt()}},createCanvas:function(e,n,t,l,a){let o=$("").appendTo("#luckysheet-grid-window-1").attr({id:e,width:Math.ceil(n*d.devicePixelRatio),height:Math.ceil(t*d.devicePixelRatio)}).css({"user-select":"none",postion:"absolute",left:l,top:a,width:n,height:t,"z-index":10,"pointer-events":"none"})},removeAssistCanvas:function(){$("#luckysheet-grid-window-1 > canvas").not($("#luckysheetTableContent")).remove(),$("#luckysheet-cell-selected").css("z-index",15)},scrollAdapt:function(){let e=this;if(d.luckysheet_select_save!=null&&d.luckysheet_select_save.length>0&&e.scrollAdaptOfselect(),$("#luckysheet-cell-main .luckysheet-data-visualization-chart").length>0){let n=d.luckysheetfile[Q(d.currentSheetIndex)].chart;n!=null&&n.length>0&&e.scrollAdaptOfchart()}$("#luckysheet-postil-showBoxs .luckysheet-postil-show").length>0&&e.scrollAdaptOfpostil(),$("#luckysheet-dropCell-icon").length>0&&e.scrollAdaptOfdpicon(),$("#luckysheet-filter-options-sheet"+d.currentSheetIndex+" .luckysheet-filter-options").length>0&&e.scrollAdaptOffilteroptions()},scrollAdaptOfselect:function(){let e=this;$("#luckysheet-row-count-show").is(":visible")&&$("#luckysheet-row-count-show").hide(),$("#luckysheet-column-count-show").is(":visible")&&$("#luckysheet-column-count-show").hide(),$("#luckysheet-rows-h-selected").empty(),$("#luckysheet-cols-h-selected").empty();let n=$("#luckysheet-cell-main").scrollTop(),t=$("#luckysheet-cell-main").scrollLeft();if(e.freezenhorizontaldata!=null&&e.freezenverticaldata!=null){let l=e.freezenhorizontaldata[0],a=e.freezenhorizontaldata[1],o=n-e.freezenhorizontaldata[2],s=e.freezenverticaldata[0],u=e.freezenverticaldata[1],f=t-e.freezenverticaldata[2];for(let A=0;A=a?b+k=a?b+k=u?R+D=u?R+D=l?O+q=l?O+q=s?j+G=s?j+G=a?p+v=a?p+v=l?I+x=l?I+x=a?p+v=a?p+v=l?I+x=l?I+xg+40+2?$(s).css("visibility","hidden"):($(s).css({top:l+n,height:g-y,visibility:"visible"}),$(s).find("canvas").css("top",-y))}else $(s).css({top:u.top-p+n,height:g,visibility:"visible"}),$(s).find("canvas").css("top",0);if(u.left-vh+20+2?$(s).css("visibility","hidden"):($(s).css({left:a+t,width:h-y,visibility:"visible"}),$(s).find("canvas").css("left",-y))}else $(s).css({left:u.left-v+t,width:h,visibility:"visible"}),$(s).find("canvas").css("left",0)})}else if(e.freezenhorizontaldata!=null){let l=e.freezenhorizontaldata[0]-e.freezenhorizontaldata[2];$("#luckysheet-cell-main .luckysheet-data-visualization-chart").each(function(a,o){let s=$(o).position(),u=$(o).height(),f=$(o).find("canvas").height(),A=f-u;if(s.top-Af+40+2?$(o).css("visibility","hidden"):($(o).css({top:l+n,height:f-h,visibility:"visible"}),$(o).find("canvas").css("top",-h))}else $(o).css({top:s.top-A+n,height:f,visibility:"visible"}),$(o).find("canvas").css("top",0)})}else if(e.freezenverticaldata!=null){let l=e.freezenverticaldata[0]-e.freezenverticaldata[2];$("#luckysheet-cell-main .luckysheet-data-visualization-chart").each(function(a,o){let s=$(o).position(),u=$(o).width(),f=$(o).find("canvas").width(),A=f-u;if(s.left-Af+20+2?$(o).css("visibility","hidden"):($(o).css({left:l+t,width:f-h,visibility:"visible"}),$(o).find("canvas").css("left",-h))}else $(o).css({left:s.left-A+t,width:f,visibility:"visible"}),$(o).find("canvas").css("left",0)})}else $("#luckysheet-cell-main .luckysheet-data-visualization-chart").each(function(l,a){let o=$(a).position(),s=$(a).width(),u=$(a).height(),f=$(a).find("canvas").width(),A=$(a).find("canvas").height(),h=A-u,g=f-s;$(a).css({top:o.top-h+n,height:A,left:o.left-g+t,width:f,visibility:"visible"}),$(a).find("canvas").css({top:0,left:0})})},scrollAdaptOfpostil:function(){let e=this,n=$("#luckysheet-cell-main").scrollTop(),t=$("#luckysheet-cell-main").scrollLeft();if(e.freezenhorizontaldata!=null&&e.freezenverticaldata!=null){let l=e.freezenhorizontaldata[0],a=e.freezenverticaldata[0],o=n-e.freezenhorizontaldata[2],s=t-e.freezenverticaldata[2];$("#luckysheet-postil-showBoxs .luckysheet-postil-show").each(function(u,f){let A=$(f).attr("id"),h=A.split("luckysheet-postil-show_")[1].split("_")[0],g=A.split("luckysheet-postil-show_")[1].split("_")[1],p=d.flowdata[h][g].ps,v=d.visibledatarow[h],y=h-1==-1?0:d.visibledatarow[h-1],b=d.visibledatacolumn[g],k=g-1==-1?0:d.visibledatacolumn[g-1],w=be.mergeborer(d.flowdata,h,g);w&&(v=w.row[1],y=w.row[0],b=w.column[1],k=w.column[0]);let C=b,I=y,x=p.left==null?C+18:p.left,T=p.top==null?I-18:p.top,R=p.width==null?He.defaultWidth:p.width,D=p.height==null?He.defaultHeight:p.height;T<0&&(T=2);let S=He.getArrowCanvasSize(x,T,C,I),B=!0,F=!0;h>=e.freezenhorizontaldata[1]?T+D=e.freezenverticaldata[1]?x+R=e.freezenhorizontaldata[1]?I+T=e.freezenverticaldata[1]?C+x=n&&a>=t?(o=l,s=a):(o=n,s=t),e.freezenhorizontaldata!=null&&e.freezenverticaldata!=null){let u=e.freezenhorizontaldata[1],f=nt(e.freezenhorizontaldata[3],$("#luckysheet-cell-main").scrollTop()-e.freezenhorizontaldata[2]),A=e.freezenverticaldata[1],h=nt(e.freezenverticaldata[3],$("#luckysheet-cell-main").scrollLeft()-e.freezenverticaldata[2]);if(o>=u&&s>=A)o=u)if(o=A)if(s=u)o=u)s=n&&A>=l)f=n)if(f=l)A=n?s=n)oa&&(a=o),a==-1&&(a=0);let s=d.visibledatarow[a]-2-l+d.columnHeaderHeight;t={horizontal:{freezenhorizontaldata:[d.visibledatarow[a],a+1,l,bn.cutVolumn(d.visibledatarow,a+1),s],top:s}}}else if(n.type==="rangeColumn"){let l=0,a=nt(d.visibledatacolumn,l),o=n.range.column_focus;o>a&&(a=o),a==-1&&(a=0);let s=d.visibledatacolumn[a]-2-l+d.rowHeaderWidth;t={vertical:{freezenverticaldata:[d.visibledatacolumn[a],a+1,l,bn.cutVolumn(d.visibledatacolumn,a+1),s],left:s}}}else if(n.type==="rangeBoth"){let l=0,a=nt(d.visibledatarow,l),o=n.range.row_focus;o>a&&(a=o),a==-1&&(a=0);let s=d.visibledatarow[a]-2-l+d.columnHeaderHeight,u=[d.visibledatarow[a],a+1,l,bn.cutVolumn(d.visibledatarow,a+1),s],f=0,A=nt(d.visibledatacolumn,f),h=n.range.column_focus;h>A&&(A=h),A==-1&&(A=0);let g=d.visibledatacolumn[A]-2-f+d.rowHeaderWidth,p=[d.visibledatacolumn[A],A+1,f,bn.cutVolumn(d.visibledatacolumn,A+1),g];t={horizontal:{freezenhorizontaldata:u,top:s},vertical:{freezenverticaldata:p,left:g}}}else n.type==="cancel"&&(t={horizontal:null,vertical:null});e.freezen=t}},Y=bn});function Lt(e=!0){Ae.showinfobar?($("#luckysheet_info_detail").show(),d.infobarHeight=document.querySelector("#luckysheet_info_detail").offsetHeight):(d.infobarHeight=0,$("#luckysheet_info_detail").hide()),!!d.toobarObject&&!!d.toobarObject.toobarElements&&d.toobarObject.toobarElements.length===0?($("#"+d.container).find(".luckysheet-wa-editor").hide(),d.toolbarHeight=0):($("#"+d.container).find(".luckysheet-wa-editor").show(),d.toolbarHeight=document.querySelector("#"+d.container+" .luckysheet-wa-editor").offsetHeight),xy(),Cy();let n=document.querySelector("#"+d.container+" .luckysheet-wa-calculate");Ae.sheetFormulaBar?(n.style.display="block",d.calculatebarHeight=n.offsetHeight):(n.style.display="none",d.calculatebarHeight=0),$("#"+d.container).find(".luckysheet-grid-container").css("top",d.toolbarHeight+d.infobarHeight+d.calculatebarHeight),fa=$("#"+d.container).width(),Ae.showConfigWindowResize&&($("#luckysheet-modal-dialog-slider-pivot").is(":visible")?fa-=$("#luckysheet-modal-dialog-slider-pivot").outerWidth():$(".chartSetting").is(":visible")?fa-=$(".chartSetting").outerWidth():$("#luckysheet-modal-dialog-slider-alternateformat").is(":visible")&&(fa-=$("#luckysheet-modal-dialog-slider-alternateformat").outerWidth()),$("#luckysheet-modal-dialog-slider-protection").is(":visible")&&(fa-=$("#luckysheet-modal-dialog-slider-protection").outerWidth()));let l=te().toolbar,a=!1,o=0,s=`
-
-
- -
- ${l.toolMore} -
-
-
- -
-
-
`,u='
';$("#luckysheet-icon-morebtn-div").length==0&&$("body").append(u),_e("#luckysheet-icon-morebtn-div").style.visibility="hidden",$("#luckysheet-icon-morebtn-div > div").each(function(){let g=$(this)[0],p=$("#luckysheet-wa-editor")[0];p.appendChild(document.createTextNode(" ")),p.appendChild(g)}),$("#luckysheet-icon-morebtn").remove();let f=d.toobarObject.toobarWidths,A=d.toobarObject.toobarElements,h=0;if(f!=null){for(let g=f.length-1;g>=0;g--)if(f[g]-1){let b=` -
- ${l.toolClose} -
-
-
- `;$(this).find(".luckysheet-toolbar-button-inner-box").html(b)}else{let b=` -
- ${l.toolMore} -
-
-
- `;$(this).find(".luckysheet-toolbar-button-inner-box").html(b)}}),$("#luckysheet-icon-morebtn-div .luckysheet-toolbar-button-split-left").off("hover").hover(function(){$(this).next(".luckysheet-toolbar-button-split-right").addClass("luckysheet-toolbar-button-split-right-hover")},function(){$(this).next(".luckysheet-toolbar-button-split-right").removeClass("luckysheet-toolbar-button-split-right-hover")}),$("#luckysheet-icon-morebtn-div .luckysheet-toolbar-button-split-right").off("hover").hover(function(){$(this).prev(".luckysheet-toolbar-button-split-left").addClass("luckysheet-toolbar-button-hover")},function(){$(this).prev(".luckysheet-toolbar-button-split-left").removeClass("luckysheet-toolbar-button-hover")}),U.createHoverTip("#luckysheet-icon-morebtn-div",".luckysheet-toolbar-menu-button, .luckysheet-toolbar-button, .luckysheet-toolbar-combo-button")),$("#"+d.container+" .luckysheet-wa-editor .luckysheet-toolbar-button-split-left").off("hover").hover(function(){$(this).next(".luckysheet-toolbar-button-split-right").addClass("luckysheet-toolbar-button-split-right-hover")},function(){$(this).next(".luckysheet-toolbar-button-split-right").removeClass("luckysheet-toolbar-button-split-right-hover")}),$("#"+d.container+" .luckysheet-wa-editor .luckysheet-toolbar-button-split-right").off("hover").hover(function(){$(this).prev(".luckysheet-toolbar-button-split-left").addClass("luckysheet-toolbar-button-hover")},function(){$(this).prev(".luckysheet-toolbar-button-split-left").removeClass("luckysheet-toolbar-button-hover")}),Jc=$("#"+d.container).height(),$("#"+d.container).find(".luckysheet").height(Jc-2).width(fa-2),$c(fa,Jc),e&&Ue($("#luckysheet-cell-main").scrollLeft(),$("#luckysheet-cell-main").scrollTop()),ve.sheetArrowShowAndHide(),ve.sheetBarShowAndHide()}}function $c(e,n){e==null&&(e=$("#"+d.container).width()),n==null&&(n=$("#"+d.container).height()),d.cellmainHeight=n-(d.infobarHeight+d.toolbarHeight+d.calculatebarHeight+d.columnHeaderHeight+d.sheetBarHeight+d.statisticBarHeight),d.cellmainWidth=e-d.rowHeaderWidth,$("#luckysheet-cols-h-c, #luckysheet-cell-main").width(d.cellmainWidth),$("#luckysheet-cell-main").height(d.cellmainHeight),$("#luckysheet-rows-h").height(d.cellmainHeight-d.cellMainSrollBarSize),$("#luckysheet-scrollbar-y").height(d.cellmainHeight+d.columnHeaderHeight-d.cellMainSrollBarSize-3),$("#luckysheet-scrollbar-x").height(d.cellMainSrollBarSize),$("#luckysheet-scrollbar-y").width(d.cellMainSrollBarSize),$("#luckysheet-scrollbar-x").width(d.cellmainWidth).css("left",d.rowHeaderWidth-2),d.luckysheetTableContentHW=[d.cellmainWidth+d.rowHeaderWidth-d.cellMainSrollBarSize,d.cellmainHeight+d.columnHeaderHeight-d.cellMainSrollBarSize],$("#luckysheetTableContent, #luckysheetTableContentF").attr({width:Math.ceil(d.luckysheetTableContentHW[0]*d.devicePixelRatio),height:Math.ceil(d.luckysheetTableContentHW[1]*d.devicePixelRatio)}).css({width:d.luckysheetTableContentHW[0],height:d.luckysheetTableContentHW[1]}),$("#"+d.container).find("#luckysheet-grid-window-1").css("bottom",d.sheetBarHeight),$("#"+d.container).find(".luckysheet-grid-window").css("bottom",d.statisticBarHeight);let t=$("#luckysheet-grid-window-1").width();$("#luckysheet-freezebar-horizontal").find(".luckysheet-freezebar-horizontal-handle").css({width:t-10}).end().find(".luckysheet-freezebar-horizontal-drop").css({width:t-10});let l=$("#luckysheet-grid-window-1").height();$("#luckysheet-freezebar-vertical").find(".luckysheet-freezebar-vertical-handle").css({height:l-10}).end().find(".luckysheet-freezebar-vertical-drop").css({height:l-10}),Y.createAssistCanvas()}function uh(){let e=Ae.showtoolbar,n=Ae.showtoolbarConfig,t=d.toobarObject.toobarWidths=[],l=d.toobarObject.toobarElements=[],a=d.toobarObject.toolbarConfig=o();function o(){let f={};function A(g){let p={},v,y,b=0;for(let k=0;kA.index?1:-1}l.forEach((f,A,h)=>{h[A]=f.ele,A!==l.length-1?f.ele instanceof Array?t.push($(f.ele[0]).offset().left):t.push($(f.ele).offset().left):f.ele instanceof Array?(t.push($(f.ele[0]).offset().left),t.push($(f.ele[0]).offset().left+$(f.ele[0]).outerWidth()+5)):(t.push($(f.ele).offset().left),t.push($(f.ele).offset().left+$(f.ele).outerWidth()+5))});let u=$("#"+d.container).offset().left;t.forEach((f,A)=>{t[A]-=u})}function xy(){if(!Ae.initShowsheetbarConfig){Ae.initShowsheetbarConfig=!0;let t={add:!0,menu:!0,sheet:!0};if(!Ae.showsheetbar)for(let l in t)t[l]=!1;JSON.stringify(Ae.showsheetbarConfig)!=="{}"&&Object.assign(t,Ae.showsheetbarConfig),Ae.showsheetbarConfig=t}let e=Ae.showsheetbarConfig,n=0;for(let t in e)if(!e[t])switch(t){case"add":$("#luckysheet-sheets-add").hide(),n++;break;case"menu":$("#luckysheet-sheets-m").hide(),n++;break;case"sheet":$("#luckysheet-sheet-container").hide(),$("#luckysheet-sheets-leftscroll").hide(),$("#luckysheet-sheets-rightscroll").hide(),n++;break;default:break}n===3?($("#"+d.container).find("#luckysheet-sheet-area").hide(),d.sheetBarHeight=0):($("#"+d.container).find("#luckysheet-sheet-area").show(),d.sheetBarHeight=31)}function Cy(){if(!Ae.initStatisticBarConfig){Ae.initStatisticBarConfig=!0;let t={count:!0,view:!0,zoom:!0};if(!Ae.showstatisticBar)for(let l in t)t[l]=!1;JSON.stringify(Ae.showstatisticBarConfig)!=="{}"&&Object.assign(t,Ae.showstatisticBarConfig),Ae.showstatisticBarConfig=t}let e=Ae.showstatisticBarConfig,n=0;for(let t in e)if(!e[t])switch(t){case"count":$("#luckysheet-sta-content").hide(),n++;break;case"view":$(".luckysheet-print-viewList").hide(),n++;break;case"zoom":$("#luckysheet-zoom-content").hide(),n++;break;default:break}n===3?($("#"+d.container).find(".luckysheet-stat-area").hide(),d.statisticBarHeight=0):($("#"+d.container).find(".luckysheet-stat-area").show(),d.statisticBarHeight=23)}var fa,Jc,Nr=De(()=>{yl();Kl();_t();je();yt();cl();rl();dt();ic();fa=0,Jc=0});function gh(e){let n=te(),t=n.protection,l=n.button,a=e.name,o=e.sqref,s=e.password,u="";s!=null&&s.length>0&&(u='');let f=` -
-
- -
-
- ${a}${u} -
-
- ${o} -
-
- -
-
- `;$("#luckysheet-protection-rangeItem-container").append(f)}function Ty(e){let n=te(),t=n.protection,l=n.button;$("#luckysheet-slider-protection-ok").click(function(){let a=$("#protection-password").val(),o=$("#protection-swichProtectionState").is(":checked"),s=$("#protection-hint").val(),u=hh,f={};u!=null&&u.config!=null&&u.config.authority!=null&&(f=u.config.authority);let A={},h="None";a!="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"?(A.password=a,A.algorithmName="None",A.saltValue=null):f!=null?(A.algorithmName=f.algorithmName,A.saltValue=f.saltValue,A.password=f.password):(A.algorithmName="None",A.saltValue=null,A.password=""),A.hintText=s,A.sheet=o==!0?1:0;for(let g=0;g div.luckysheet-protection-rangeItem").index(o),f=kn[u];Ah=u,$("#protection-allowRangeAdd-title").val(f.name),$("#protection-allowRangeAdd-range input").val(f.sqref),f.algorithmName=="None"?$("#protection-allowRangeAdd-password").val(f.password):$("#protection-allowRangeAdd-password").val("\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"),$("#protection-allowRangeAdd-hint").val(f.hintText)}),$(document).off("click.luckysheetProtection.rangeItemDelete").on("click.luckysheetProtection.rangeItemDelete","#luckysheet-protection-rangeItem-container .luckysheet-protection-rangeItem-del",function(a){let o=$(a.target).closest(".luckysheet-protection-rangeItem"),u=$("#luckysheet-protection-rangeItem-container").find("> div.luckysheet-protection-rangeItem").index(o),f=kn[u];kn.splice(u,1),o.remove()}),$(document).off("click.luckysheetProtection.rangeItemConfirm").on("click.luckysheetProtection.rangeItemConfirm","#luckysheet-protection-rangeItem-confirm",function(){let a=$("#protection-allowRangeAdd-title").val(),o=$("#protection-allowRangeAdd-range input").val(),s=$("#protection-allowRangeAdd-password").val(),u=$("#protection-allowRangeAdd-hint").val();if(a.length==0){alert(t.rangeItemErrorTitleNull);return}let f=Qe.getRangeByTxt(o);if(o.length==0){alert(t.rangeItemErrorRangeNull);return}if(f.length==0){alert(t.rangeItemErrorRange);return}if(o=bh(o),eu){let A={name:a,password:s,hintText:u,algorithmName:"None",saltValue:null,checkRangePasswordUrl:null,sqref:o};gh(A),kn.push(A)}else{let A=Ah,h=kn[A];h.name=a,h.sqref=o,h.hintText=u,s!="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"&&(h.password=s,h.algorithmName="None");let p=$("#luckysheet-protection-rangeItem-container").find("> div.luckysheet-protection-rangeItem").eq(A),v=p.find(".luckysheet-protection-rangeItem-name"),y="";s!=null&&s.length>0&&(y=''),v.html(a+y).attr("title",a),p.find(".luckysheet-protection-rangeItem-range").html(o).attr("title",o)}$("#luckysheet-protection-rangeItem-dialog").hide(),$("#luckysheet-modal-dialog-mask").hide()}),$(document).off("click.luckysheetProtection.validationConfirm").on("click.luckysheetProtection.validationConfirm","#luckysheet-protection-sheet-validation-confirm",function(a){let o=$("#luckysheet-protection-sheet-validation"),s=gs;if(s==null){ru(gs),o.hide(),$("#luckysheet-modal-dialog-mask").hide(),$("#luckysheet-modal-dialog-slider-protection").show(),Lt();return}let f=o.find("input").val();if(f==null||f.length==0){alert(t.checkPasswordNullalert);return}if(s.algorithmName!=null&&s.algorithmName!="None")if(s.saltValue!=null&&s.saltValue.length>0){var A=CryptoApi.getHasher(s.algorithmName);f=CryptoApi.hmac(s.saltValue,f,A)}else f=CryptoApi.hash(s.algorithmName,f);f==s.password?(ru(gs),o.hide(),$("#luckysheet-modal-dialog-mask").hide(),$("#luckysheet-modal-dialog-slider-protection").show(),Lt(),tu=!1):alert(t.checkPasswordWrongalert)}),$("#luckysheet-protection-check-selectLockedCells").change(function(){let a=$("#luckysheet-protection-check-selectLockedCells"),o=$("#luckysheet-protection-check-selectunLockedCells"),s=a.is(":checked"),u=o.is(":checked");s&&o.prop("checked",!0)}),$("#luckysheet-protection-check-selectunLockedCells").change(function(){let a=$("#luckysheet-protection-check-selectLockedCells"),o=$("#luckysheet-protection-check-selectunLockedCells"),s=a.is(":checked");o.is(":checked")||a.prop("checked",!1)}),$(document).off("click.luckysheetProtection.dvRange").on("click.luckysheetProtection.dvRange","#protection-allowRangeAdd-range .fa-table",function(a){$("#luckysheet-protection-rangeItem-dialog").hide();let o="0",s=$(this).siblings("input").val().trim();Qe.rangeDialog(o,s),Qe.selectRange=[];let u=Qe.getRangeByTxt(s);if(u.length>0)for(let f=0;f0)for(let f=0;f -
-
- ${t.allowRangeAddTitle} -
-
- -
-
-
-
- ${t.allowRangeAddSqrf} -
-
-
- - -
-
-
-
-
- ${t.allowRangeAddTitlePassword} -
-
- -
-
-
-
- ${t.allowRangeAddTitleHint} -
-
- -
-
-
- `,botton:` - `,style:"z-index:100003"}))}function Iy(e){let n=te(),t=n.protection,l=n.button,a="";for(let s=0;s -
- -
- - `}let o=` - - `;$("body").append(o),$("body").append(we(At,{id:"luckysheet-protection-sheet-validation",addclass:"luckysheet-protection-sheet-validation",title:t.validationTitle,content:` -
-
- ${t.validationTips} -
-
-
-
- -
-
- `,botton:` - `,style:"z-index:100003"}))}function ru(e){e==null&&(e={});for(let a=0;a0?e.algorithmName=="None"||e.algorithmName==null?$("#protection-password").val(e.password):$("#protection-password").val("\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"):$("#protection-password").val("");let n=e.sheet;e.sheet==null&&(n=0),$("#protection-swichProtectionState").prop("checked",n==1);let t=e.hintText;t==null&&(t=""),$("#protection-hint").val(t),kn=[],$("#luckysheet-protection-rangeItem-container").empty();let l=e.allowRangeList;if(l!=null&&l.length>0)for(let a=0;a0){gs=n,$("#luckysheet-protection-sheet-validation input").val(""),Oa("luckysheet-protection-sheet-validation");return}else ru(n)}else $("#luckysheet-protection-check-selectLockedCells").prop("checked",!0),$("#luckysheet-protection-check-selectunLockedCells").prop("checked",!0);$("#luckysheet-modal-dialog-slider-protection").show(),Lt()}function yh(){$("#luckysheet-protection-rangeItem-dialog").hide(),$("#luckysheet-modal-dialog-slider-protection").hide(),Lt()}function nu(e,n,t,l,a=!0,o=!0){let s=!1,u=t.allowRangeList;if(u!=null&&u.length>0){let f=!1;for(let A=0;A0)for(let v=0;v=y&&e<=b&&n>=k&&n<=w){f=!0;break}}if(f){let v=h.password;if(v!=null&&v.length>0&&!(g in lu))return a&&(Sy(h),$("#luckysheet-selection-copy .luckysheet-selection-copy").hide()),!1;s=!0;break}}}if(!s&&!o&&(s=!0),!s&&a){let f;t.hintText!=null&&t.hintText.length>0?f=t.hintText:f=l.defaultSheetHintText,U.info("",f),$("#luckysheet-selection-copy .luckysheet-selection-copy").hide()}return s}function Sy(e){let n=te(),t=n.protection,l=n.button;mh||$("body").append(we(At,{id:"luckysheet-protection-range-validation",addclass:"luckysheet-protection-sheet-validation",title:t.validationTitle,content:` -
-
- -
-
-
-
- -
-
- `,botton:` - `,style:"z-index:100003"})),mh=!0,Oa("luckysheet-protection-range-validation");let a=$("#luckysheet-protection-range-validation-hint");e.hintText!=null&&e.hintText.length>0?a.html(e.hintText):a.html(t.defaultRangeHintText);let o=$("#luckysheet-protection-range-validation"),s=o.find("input");s.val(""),$("#luckysheet-protection-range-validation-confirm").off("click").on("click",function(){let u=s.val();if(u==null||u.length==0){alert(t.checkPasswordNullalert);return}if(e.algorithmName!=null&&e.algorithmName!="None")if(e.saltValue!=null&&e.saltValue.length>0){var f=CryptoApi.getHasher(e.algorithmName);u=CryptoApi.hmac(e.saltValue,u,f)}else u=CryptoApi.hash(e.algorithmName,u);u==e.password?(lu[e.sqref]=1,o.hide(),$("#luckysheet-modal-dialog-mask").hide(),alert(t.checkPasswordSucceedalert)):alert(t.checkPasswordWrongalert)})}function tn(e){let n=ve.getSheetByIndex(e);if(n==null||n.config==null||n.config.authority==null)return!0;let t=n.config.authority;if(t==null||t.sheet==null||t.sheet==0)return!0;let a=te().protection,o;return t.hintText!=null&&t.hintText.length>0?o=t.hintText:o=a.defaultSheetHintText,U.info("",o),!1}function Fn(e,n,t,l=!0,a=!0){let o=ve.getSheetByIndex(t);if(o==null||o.config==null||o.config.authority==null)return!0;let s=o.data,u=s[e][n],f=o.config.authority;if(f==null||f.sheet==null||f.sheet==0||u&&u.lo===0)return!0;let h=te().protection;return nu(e,n,f,h,l,a)}function ps(e,n,t){let l=ve.getSheetByIndex(t);if(!l||l.data&&!l.data[e]||l.data&&!l.data[e][n]||l.config==null||l.config.authority==null)return!0;let a=l.data,o=a[e][n],s=l.config.authority;return s==null||s.sheet==null||s.sheet==0||o==null||o.hi==null||o.hi==0}function xr(e,n){let t=ve.getSheetByIndex(n);if(t==null||t.config==null||t.config.authority==null)return!0;let l=t.config.authority;if(l==null||l.sheet==null||l.sheet==0||e==null||e.length==0)return!0;let o=te().protection;for(let s=0;s0?o=t.hintText:o=a.defaultSheetHintText,U.info("",o),!1}function Rt(e,n="formatColumns",t=!0){let l=ve.getSheetByIndex(e);if(l==null||l.config==null||l.config.authority==null)return!0;let a=l.config.authority;if(a==null||a.sheet==null||a.sheet==0||a[n]==1||a[n]==null)return!0;if(t){let s=te().protection,u;a.hintText!=null&&a.hintText.length>0?u=a.hintText:u=s.defaultSheetHintText,U.info("",u)}return!1}var dh,fh,kn,eu,Ah,gs,hh,tu,lu,mh,hi,Bl=De(()=>{je();yt();Wt();wt();Wr();cl();Nr();mr();dt();el();rl();$r();dh=!1,fh=!1,kn=[],eu=!0,Ah=null,gs=null,hh=null,tu=!0,lu={},mh=!1,hi=["selectLockedCells","selectunLockedCells","formatCells","formatColumns","formatRows","insertColumns","insertRows","insertHyperlinks","deleteColumns","deleteRows","sort","filter","usePivotTablereports","editObjects","editScenarios"]});function Jr(e,n,t){t==null&&(t=!0);let l=function(o,s){let u=o[n],f=s[n];if(N(o[n])=="object"&&(u=o[n].v),N(s[n])=="object"&&(f=s[n].v),me(u))return 1;if(me(f))return-1;if(qt(u)&&qt(f))return Al(u,f);if(V(u)&&V(f))return(0,Ln.default)(u).value()-(0,Ln.default)(f).value();if(!V(u)&&!V(f))return u.localeCompare(f,"zh");if(V(u)){if(!V(f))return-1}else return 1},a=function(o,s){let u=o[n],f=s[n];if(N(o[n])=="object"&&(u=o[n].v),N(s[n])=="object"&&(f=s[n].v),me(u))return 1;if(me(f))return-1;if(qt(u)&&qt(f))return Al(f,u);if(V(u)&&V(f))return(0,Ln.default)(f).value()-(0,Ln.default)(u).value();if(!V(u)&&!V(f))return f.localeCompare(u,"zh");if(V(u)){if(!V(f))return 1}else return-1};return t?e.sort(l):e.sort(a)}function hs(e,n){n==null&&(n=!0);let t=function(a,o){let s=a,u=o;if(N(a)=="object"&&(s=a.v),N(o)=="object"&&(u=o.v),s==null&&(s=""),u==null&&(u=""),qt(s)&&qt(u))return Al(s,u);if(V(s)&&V(u))return(0,Ln.default)(s).value()-(0,Ln.default)(u).value();if(!V(s)&&!V(u))return s.localeCompare(u,"zh");if(V(s)){if(!V(u))return-1}else return 1},l=function(a,o){let s=a,u=o;if(N(a)=="object"&&(s=a.v),N(o)=="object"&&(u=o.v),s==null&&(s=""),u==null&&(u=""),qt(s)&&qt(u))return Al(u,s);if(V(s)&&V(u))return(0,Ln.default)(u).value()-(0,Ln.default)(s).value();if(!V(s)&&!V(u))return u.localeCompare(s,"zh");if(V(s)){if(!V(u))return 1}else return-1};return n?e.sort(t):e.sort(l)}function iu(e){if(!Rt(d.currentSheetIndex,"sort"))return;let t=te().sort;if(d.luckysheet_select_save.length>1){he()?alert(t.noRangeError):U.info(t.noRangeError,"");return}e==null&&(e=!0);let l=Te.deepCopyFlowData(d.flowdata),a=d.luckysheet_select_save[0].row[0],o=d.luckysheet_select_save[0].row[1],s=d.luckysheet_select_save[0].column[0],u=d.luckysheet_select_save[0].column[1],f,A;for(let v=a;v<=o;v++)if(l[v]!=null&&l[v][s]!=null){let y=l[v][s];if(y.mc!=null||me(y.v))continue;if(f==null&&/[\u4e00-\u9fa5]+/g.test(y.v)){f=v+1,A=v+1;continue}f==null&&(f=v),A=v}if(f==null||f>o)return;let h=!1,g=[];for(let v=f;v<=A;v++){let y=[];for(let b=s;b<=u;b++){if(l[v][b]!=null&&l[v][b].mc!=null){h=!0;break}y.push(l[v][b])}g.push(y)}if(h){he()?alert(t.mergeError):U.info(t.mergeError,"");return}g=Jr(g,0,e);for(let v=f;v<=A;v++)for(let y=s;y<=u;y++)l[v][y]=g[v-f][y-s];let p={};if(d.config.rowlen!=null){let v=$.extend(!0,{},d.config);v=vl(l,f,A,v),p={cfg:v,RowlChange:!0}}Je(l,[{row:[f,A],column:[s,u]}],p)}function ou(e,n){if(!Rt(d.currentSheetIndex,"sort"))return;n==null&&(n=!0);let l=te().sort,a=Te.deepCopyFlowData(d.flowdata),o=0,s=a.length-1,u=0,f=a[0].length-1,A,h;for(let y=o;y<=s;y++)if(!(a[y][e]!=null&&a[y][e].mc!=null)){if(a[y][e]!=null&&!me(a[y][e].v)&&/[\u4e00-\u9fa5]+/g.test(a[y][e].v)&&A==null){A=y+1,h=y+1;continue}A==null&&(A=y),a[y][e]!=null&&!me(a[y][e].v)&&(h=y)}if(A==null||A>s)return;let g=!1,p=[];for(let y=A;y<=h;y++){let b=[];for(let k=u;k<=f;k++){if(a[y][k]!=null&&a[y][k].mc!=null){g=!0;break}b.push(a[y][k])}p.push(b)}if(g){he()?alert(l.columnSortMergeError):U.info(l.columnSortMergeError,"");return}p=Jr(p,e,n);for(let y=A;y<=h;y++)for(let b=u;b<=f;b++)a[y][b]=p[y-A][b-u];let v={};if(d.config.rowlen!=null){let y=$.extend(!0,{},d.config);y=vl(a,A,h,y),v={cfg:y,RowlChange:!0}}Je(a,[{row:[A,h],column:[u,f]}],v)}var Ln,za=De(()=>{dt();Pt();Kr();rl();tl();wr();_t();Bl();je();yt();Ln=Ol(Ma())});var Dy,kh,wh=De(()=>{Qt();Dy={transpose:function(e,n=!0){let t=[];if(e.length==0)return[];if(e[0].length==0)return[];for(let l=0;le.row[1]||n.row[1]e.column[1]||n.column[1]e.column[0]&&n.column[1]e.row[0]&&n.row[1]e.row[0]&&n.row[0]e.column[0]&&n.column[1]e.column[0]&&n.column[0]{dt();Ry={STDEVP:function(e,n){let t=0;for(let l=0;l{wt();dt();Qt();Kr();Fl();Pt();za();_t();rl();tl();$r();wh();xh();el();Br();Wt();cl();Nr();$t();Bl();je();yt();co=Ol(Ma());Wt();Ch={pivotDatas:null,pivotSheetIndex:0,pivotDataSheetIndex:0,celldata:null,origindata:null,getCellData:function(e,n,t){let l=this,a;e!=null?a=e:a=d.currentSheetIndex;let o=Q(a);N(d.luckysheetfile[o].pivotTable)!="object"&&(d.luckysheetfile[o].pivotTable=new Function("return "+d.luckysheetfile[o].pivotTable)()),d.luckysheetfile[o].pivotTable!=null?(l.column=d.luckysheetfile[o].pivotTable.column,l.row=d.luckysheetfile[o].pivotTable.row,l.values=d.luckysheetfile[o].pivotTable.values,l.filter=d.luckysheetfile[o].pivotTable.filter,l.showType=d.luckysheetfile[o].pivotTable.showType,l.filterparm=d.luckysheetfile[o].pivotTable.filterparm,d.luckysheetfile[o].pivotTable.drawPivotTable!=null?l.drawPivotTable=d.luckysheetfile[o].pivotTable.drawPivotTable:l.drawPivotTable=!0,d.luckysheetfile[o].pivotTable.pivotTableBoundary!=null?l.pivotTableBoundary=d.luckysheetfile[o].pivotTable.pivotTableBoundary:l.pivotTableBoundary=[12,6],t!=null?l.pivot_select_save=t:l.pivot_select_save=d.luckysheetfile[o].pivotTable.pivot_select_save,n!=null?l.pivotDataSheetIndex=n:l.pivotDataSheetIndex=d.luckysheetfile[o].pivotTable.pivotDataSheetIndex):(l.column=null,l.row=null,l.values=null,l.filter=null,l.showType=null,l.filterparm=null,l.drawPivotTable=!0,l.pivotTableBoundary=[12,6],t!=null?l.pivot_select_save=t:l.pivot_select_save=d.luckysheet_select_save,n!=null?l.pivotDataSheetIndex=n:l.pivotDataSheetIndex=a);let s=Q(l.pivotDataSheetIndex),u=d.luckysheetfile[s];u.data==null&&(u.data=ve.buildGridData(u)),l.origindata=$i(u.data,l.pivot_select_save);let f={};if(l.filterparm!=null)for(let h in l.filterparm)for(let g in l.filterparm[h])g==="rowhidden"&&l.filterparm[h][g]!=null&&(f=$.extend(!0,f,l.filterparm[h][g]));l.rowhidden=f,l.pivotSheetIndex=a;let A=[];for(let h=0;h=2?($("#luckysheetpivottablevaluecolrowshow").show(),e.showType=="column"?($("#luckysheetpivottablevaluecolrow").prop("checked",!0),$("#luckysheetpivottablevaluecolrowshow label[for='luckysheetpivottablevaluecolrow']").addClass("ui-state-active"),$("#luckysheetpivottablevaluecolrow1").prop("checked",!1),$("#luckysheetpivottablevaluecolrowshow label[for='luckysheetpivottablevaluecolrow1']").removeClass("ui-state-active")):($("#luckysheetpivottablevaluecolrow1").prop("checked",!0),$("#luckysheetpivottablevaluecolrowshow label[for='luckysheetpivottablevaluecolrow1']").addClass("ui-state-active"),$("#luckysheetpivottablevaluecolrow").prop("checked",!1),$("#luckysheetpivottablevaluecolrowshow label[for='luckysheetpivottablevaluecolrow']").removeClass("ui-state-active"))):$("#luckysheetpivottablevaluecolrowshow").hide()},resetOrderby:function(e){let n=$("#luckysheet-modal-dialog-config-value .luckysheet-modal-dialog-slider-config-item").index(e);$("#luckysheet-modal-dialog-config-row, #luckysheet-modal-dialog-config-column").find(".luckysheet-modal-dialog-slider-config-item").each(function(){$(this).data("orderby")==n&&$(this).data("orderby","self")})},luckysheetsliderlistclearfilter:function(e){let n=this,t=e.parent(),l=t.data("index"),a={},o={},s=n.origindata,u={};t.data("rowhidden","").find(".luckysheet-slider-list-item-filtered").hide(),n.setDatatojsfile("selected",{},l),n.setDatatojsfile("rowhidden",null,l);let f=[];for(let A=0;A2)for(let T in b){let R=0,D="";for(let B in b[T]){let F=0,E="";for(let P in b[T][B]){let L=b[T][B][P];F+=L;let O;Number(B)<10?O="0"+Number(B):O=B;let q;Number(P)<10?q="0"+Number(P):q=P,T in k&&B in k&&P in k?E+='
( '+L+" )
":E+='
( '+L+" )
"}R+=F;let z;Number(B)<10?z="0"+Number(B):z=B,T in k&&B in k?D+='
( '+F+' )
'+E+"
":D+='
( '+F+' )
'+E+"
"}let S;T in k?S='
( '+R+' )
'+D+"
":S='
( '+R+' )
'+D+"
",I.unshift(S)}if(JSON.stringify(w).length>2){let T=Object.keys(w);T=hs(T,!0);for(let R=0;R( '+w[D][S]+" )":F='
( '+w[D][S]+" )
",I.push(F)}}}let x=u-o.top-350;x<0&&(x=100),$("#luckysheet-pivotTableFilter-byvalue-select").append("
"+I.join("")+"
"),p.close()},1),yn(s,o.left-250,o.top)},getSumTypeName:function(e){let n="",l=te().pivotTable;return e=="SUM"?n=l.valueStatisticsSUM:e=="COUNT"?n=l.valueStatisticsCOUNT:e=="COUNTA"?n=l.valueStatisticsCOUNTA:e=="COUNTUNIQUE"?n=l.valueStatisticsCOUNTUNIQUE:e=="AVERAGE"?n=l.valueStatisticsAVERAGE:e=="MAX"?n=l.valueStatisticsMAX:e=="MIN"?n=l.valueStatisticsMIN:e=="MEDIAN"?n=l.valueStatisticsMEDIAN:e=="PRODUCT"?n=l.valueStatisticsPRODUCT:e=="STDEV"?n=l.valueStatisticsSTDEV:e=="STDEVP"?n=l.valueStatisticsSTDEVP:e=="let"?n=l.valueStatisticslet:e=="VARP"&&(n=l.valueStatisticsVARP),n},setDatatojsfile:function(e,n,t){let l=this,a=Q(l.pivotSheetIndex);d.luckysheetfile[a].pivotTable==null&&(d.luckysheetfile[a].pivotTable={}),t==null?(d.luckysheetfile[a].pivotTable[e]=n,l[e]=n):(d.luckysheetfile[a].pivotTable.filterparm==null&&(d.luckysheetfile[a].pivotTable.filterparm={}),d.luckysheetfile[a].pivotTable.filterparm[t.toString()]==null&&(d.luckysheetfile[a].pivotTable.filterparm[t.toString()]={}),d.luckysheetfile[a].pivotTable.filterparm[t.toString()][e]=n,l.filterparm==null&&(l.filterparm={}),l.filterparm[t.toString()]==null&&(l.filterparm[t.toString()]={}),l.filterparm[t.toString()][e]=n)},createPivotTable:function(e){if(he()||d.allowEdit===!1)return;let n=this,t=d.currentSheetIndex,a=te().pivotTable;if(he()){alert(a.errorNotAllowEdit);return}if(d.luckysheet_select_save.length>1){U.info("",a.errorNotAllowMulti);return}if(d.luckysheet_select_save.length==0||d.luckysheet_select_save[0].row[0]==d.luckysheet_select_save[0].row[1]||d.luckysheet_select_save[0].column[0]==d.luckysheet_select_save[0].column[1]){U.info("",a.errorSelectRange);return}let o=$.extend(!0,{},d.luckysheet_select_save[0]);ve.addNewSheet(e,!0),n.getCellData(d.currentSheetIndex,t,o),n.setDatatojsfile("pivot_select_save",o),n.setDatatojsfile("pivotDataSheetIndex",t),n.initialPivotManage()},changePivotTable:function(e){let n=this,l=te().pivotTable,a=d.luckysheetfile[Q(e)].pivotTable.pivotDataSheetIndex;if(Q(a)==null){U.info(l.errorIsDamage,"");return}n.getCellData(e),n.initialPivotManage(!0),n.refreshPivotTable()},refreshPivotTable:function(e=!0){let n=this,t={};t.pivotTable=Ch,t.data=Te.deepCopyFlowData(d.flowdata),n.storePivotTableParam();let l=n.dataHandler(n.column,n.row,n.values,n.showType,n.celldata);n.setDatatojsfile("pivotDatas",l);let a=$.extend(!0,[],ve.nulldata),o=a,s=0,u=0;if(l.length==0)n.setDatatojsfile("drawPivotTable",!0),n.setDatatojsfile("pivotTableBoundary",[12,6]);else{n.setDatatojsfile("drawPivotTable",!1),n.setDatatojsfile("pivotTableBoundary",[l.length,l[0].length]);let f=l.length,A=l[0].length;s=f-a.length,u=A-a[0].length,o=fl(a,s+20,u+10,!0);for(let h=0;h0||u>0?rr(o[0].length,o.length,o,null,d.luckysheet_select_save,"datachangeAll",void 0,void 0,e):(Je(o,d.luckysheet_select_save,{},null,e),at()),d.clearjfundo=!0},drawPivotTable:!0,pivotTableBoundary:[12,6],pivotclick:function(e,n,t){t==null&&(t=d.currentSheetIndex);let l=d.luckysheetfile[Q(t)];if(!l.isPivotTable)return;let a=l.pivotTable.pivotDataSheetIndex;if(Q(a)==null)return;let s=$("#luckysheet-modal-dialog-slider-pivot"),u=this.isPivotRange(e,n);if(u&&s.is(":hidden")){if(!Rt(t,"usePivotTablereports",!1))return;s.show(),Lt(),$("#luckysheet-sta-content").css("padding-right",260)}else!u&&s.is(":visible")&&(s.hide(),Lt(),$("#luckysheet-sta-content").css("padding-right",10))},isPivotRange:function(e,n){let t=this;if(d.luckysheetcurrentisPivotTable)return e0){for(let o=0;o0)return e;for(let o=0;o',botton:'"})),$("body").append(we(qo(),{menuid:"pivotTableFilter"})),$("body").append(we(Ho(),{menuid:"pivotTableFilter"})),$("body").append(Nd()),$("body").append(Fd()),$("#luckysheet-pivotTableFilter-orderby-asc").remove(),$("#luckysheet-pivotTableFilter-orderby-desc").next().remove(),$("#luckysheet-pivotTableFilter-orderby-desc").remove(),$("#luckysheet-pivotTableFilter-orderby-color").next().remove(),$("#luckysheet-pivotTableFilter-orderby-color").remove(),$("#luckysheetpivottablevaluecolrow, #luckysheetpivottablevaluecolrow1").checkboxradio({icon:!1}).change(function(){n.refreshPivotTable()});let h=null;$("#luckysheet-pivotTableFilter-menu").mouseover(function(){clearTimeout(h),h=setTimeout(function(){$("#luckysheet-pivotTableFilter-submenu").hide()},500)}),$(document).off("click.ptFilterCheckbox1").on("click.ptFilterCheckbox1","#luckysheet-pivotTableFilter-byvalue-select .textBox",function(){$(this).attr("data-check")=="true"?($(this).attr("data-check","false"),$(this).find("input[type='checkbox']").removeAttr("checked")):($(this).attr("data-check","true"),$(this).find("input[type='checkbox']").prop("checked",!0))}),$(document).off("click.ptFilterCheckbox2").on("click.ptFilterCheckbox2","#luckysheet-pivotTableFilter-byvalue-select .year",function(){$(this).attr("data-check")=="true"?($(this).attr("data-check","false"),$(this).parents(".yearBox").find(".month").attr("data-check","false"),$(this).parents(".yearBox").find(".day").attr("data-check","false"),$(this).parents(".yearBox").find("input[type='checkbox']").removeAttr("checked")):($(this).attr("data-check","true"),$(this).parents(".yearBox").find(".month").attr("data-check","true"),$(this).parents(".yearBox").find(".day").attr("data-check","true"),$(this).parents(".yearBox").find("input[type='checkbox']").prop("checked",!0))}),$(document).off("click.ptFilterCheckbox3").on("click.ptFilterCheckbox3","#luckysheet-pivotTableFilter-byvalue-select .month",function(){$(this).attr("data-check")=="true"?($(this).attr("data-check","false"),$(this).parents(".monthBox").find(".day").attr("data-check","false"),$(this).parents(".monthBox").find("input[type='checkbox']").removeAttr("checked")):($(this).attr("data-check","true"),$(this).parents(".monthBox").find(".day").attr("data-check","true"),$(this).parents(".monthBox").find("input[type='checkbox']").prop("checked",!0));let g=!0;$(this).parents(".yearBox").find(".day").each(function(v,y){$(y).attr("data-check")=="true"||(g=!1)}),g?($(this).parents(".yearBox").find(".year").attr("data-check","true"),$(this).parents(".yearBox").find(".year input[type='checkbox']").prop("checked",!0)):($(this).parents(".yearBox").find(".year").attr("data-check","false"),$(this).parents(".yearBox").find(".year input[type='checkbox']").removeAttr("checked"))}),$(document).off("click.ptFilterCheckbox4").on("click.ptFilterCheckbox4","#luckysheet-pivotTableFilter-byvalue-select .day",function(){$(this).attr("data-check")=="true"?($(this).attr("data-check","false"),$(this).find("input[type='checkbox']").removeAttr("checked")):($(this).attr("data-check","true"),$(this).find("input[type='checkbox']").prop("checked",!0));let g=!0;$(this).parents(".monthBox").find(".day").each(function(b,k){$(k).attr("data-check")=="true"||(g=!1)}),g?($(this).parents(".monthBox").find(".month").attr("data-check","true"),$(this).parents(".monthBox").find(".month input[type='checkbox']").prop("checked",!0)):($(this).parents(".monthBox").find(".month").attr("data-check","false"),$(this).parents(".monthBox").find(".month input[type='checkbox']").removeAttr("checked"));let v=!0;$(this).parents(".yearBox").find(".day").each(function(b,k){$(k).attr("data-check")=="true"||(v=!1)}),v?($(this).parents(".yearBox").find(".year").attr("data-check","true"),$(this).parents(".yearBox").find(".year input[type='checkbox']").prop("checked",!0)):($(this).parents(".yearBox").find(".year").attr("data-check","false"),$(this).parents(".yearBox").find(".year input[type='checkbox']").removeAttr("checked"))}),$(document).off("click.ptFilterYearDropdown").on("click.ptFilterYearDropdown","#luckysheet-pivotTableFilter-byvalue-select .yearBox .fa-caret-right",function(){let g=$(this).parents(".luckysheet-mousedown-cancel");g.hasClass("year")&&$(this).parents(".yearBox").find(".monthList").slideToggle(),g.hasClass("month")&&$(this).parents(".monthBox").find(".dayList").slideToggle()}),$("#luckysheet-pivotTableFilter-byvalue-btn-all").click(function(){$("#luckysheet-pivotTableFilter-byvalue-select .ListBox input[type='checkbox']").prop("checked",!0),$("#luckysheet-pivotTableFilter-byvalue-select .ListBox input[type='checkbox']").parents(".luckysheet-mousedown-cancel").attr("data-check","true")}),$("#luckysheet-pivotTableFilter-byvalue-btn-contra").click(function(){$("#luckysheet-pivotTableFilter-byvalue-select .ListBox input[type='checkbox']").each(function(y,b){$(b).is(":checked")?($(b).removeAttr("checked"),$(b).parents(".luckysheet-mousedown-cancel").attr("data-check","false")):($(b).prop("checked",!0),$(b).parents(".luckysheet-mousedown-cancel").attr("data-check","true"))}),$("#luckysheet-pivotTableFilter-byvalue-select .ListBox .monthBox").each(function(y,b){let k=!0;$(b).find(".day input[type='checkbox']").each(function(C,I){$(I).is(":checked")||(k=!1)}),k?($(b).find(".month input[type='checkbox']").prop("checked",!0),$(b).attr("data-check","true")):($(b).find(".month input[type='checkbox']").removeAttr("checked"),$(b).attr("data-check","false"))}),$("#luckysheet-pivotTableFilter-byvalue-select .ListBox .yearBox").each(function(y,b){let k=!0;$(b).find(".day input[type='checkbox']").each(function(C,I){$(I).is(":checked")||(k=!1)}),k?($(b).find(".year input[type='checkbox']").prop("checked",!0),$(b).attr("data-check","true")):($(b).find(".year input[type='checkbox']").removeAttr("checked"),$(b).attr("data-check","false"))})}),$("#luckysheet-pivotTableFilter-byvalue-btn-clear").click(function(){$("#luckysheet-pivotTableFilter-byvalue-select .ListBox input[type='checkbox']").removeAttr("checked"),$("#luckysheet-pivotTableFilter-byvalue-select .ListBox input[type='checkbox']").parents(".luckysheet-mousedown-cancel").attr("data-check","false")}),$("#luckysheet-pivotTableFilter-byvalue-input").on("input propertychange",function(){let g=$(this).val().toString();$("#luckysheet-pivotTableFilter-byvalue-select .ListBox .luckysheet-mousedown-cancel").show(),g!=""&&$("#luckysheet-pivotTableFilter-byvalue-select .ListBox input[type='checkbox']").each(function(v,y){let b=$(y).parents(".luckysheet-mousedown-cancel");if(b.hasClass("day")){let k=$(y).siblings("label").text().toString(),w=$(y).parents(".monthBox").find(".month label").text().toString();($(y).parents(".yearBox").find(".year label").text().toString()+"-"+w+"-"+k).indexOf(g)==-1&&($(y).parents(".day").hide(),$(y).parents(".dayList").find(".day:visible").length==0&&$(y).parents(".monthBox").find(".month").hide(),$(y).parents(".monthList").find(".day:visible").length==0&&$(y).parents(".yearBox").find(".year").hide())}b.hasClass("textBox")&&$(y).siblings("label").text().toString().indexOf(g)==-1&&$(y).parents(".textBox").hide()})}),$("#luckysheet-pivotTableFilter-bycondition, #luckysheet-pivotTableFilter-byvalue").click(function(){let g=$(this);g.next().slideToggle(200),setTimeout(function(){g.attr("id")=="luckysheet-pivotTableFilter-bycondition"&&$("#luckysheet-pivotTableFilter-bycondition").next().is(":visible")&&$("#luckysheet-pivotTableFilter-selected span").text()!=o.filiterInputNone&&$("#luckysheet-pivotTableFilter-byvalue").next().slideUp(200),g.is($("#luckysheet-pivotTableFilter-bycondition"))&&$("#luckysheet-pivotTableFilter-bycondition").next().is(":hidden")&&$("#luckysheet-pivotTableFilter-byvalue").next().is(":hidden")&&$("#luckysheet-pivotTableFilter-byvalue").next().slideDown(200)},300)}),$("#luckysheet-pivotTableFilter-cancel").click(function(){$("#luckysheet-pivotTableFilter-menu, #luckysheet-pivotTableFilter-submenu").hide()}),$("#luckysheet-pivotTableFilter-selected").click(function(){let g=$(this),p=g.offset(),v=$("#luckysheet-pivotTableFilter-submenu");v.hide();let y=$(window).height(),b=$(window).width(),k=v.width(),w=v.height(),C=p.top,I=p.left,x=y-p.top-20;p.left+k>b&&(I=p.left-k),p.top>y/2&&(C=y-p.top,C<0&&(C=0),x=p.top-20),v.css({top:C,left:I,height:x}).show(),clearTimeout(h)}),$("#luckysheet-pivotTableFilter-submenu").mouseover(function(){clearTimeout(h)}).find(".luckysheet-cols-menuitem").click(function(g){$("#luckysheet-pivotTableFilter-selected span").html($(this).find(".luckysheet-cols-menuitem-content").text()).data("value",$(this).data("value")),$("#luckysheet-pivotTableFilter-menu .luckysheet-pivotTableFilter-selected-input").hide(),$(this).data("type")=="2"?($("#luckysheet-pivotTableFilter-selected span").data("type","2"),$("#luckysheet-pivotTableFilter-menu .luckysheet-pivotTableFilter-selected-input2").show()):$(this).data("type")=="0"?$("#luckysheet-pivotTableFilter-selected span").data("type","0"):($("#luckysheet-pivotTableFilter-selected span").data("type","1"),$("#luckysheet-pivotTableFilter-menu .luckysheet-pivotTableFilter-selected-input").eq(0).show(),$(this).attr("data-value")=="dateequal"||$(this).attr("data-value")=="datelessthan"||$(this).attr("data-value")=="datemorethan"?$("#luckysheet-pivotTableFilter-menu .luckysheet-pivotTableFilter-selected-input input").prop("type","date"):$("#luckysheet-pivotTableFilter-menu .luckysheet-pivotTableFilter-selected-input input").prop("type","text")),$("#luckysheet-pivotTableFilter-byvalue").next().slideUp(),$("#luckysheet-pivotTableFilter-submenu").hide()}),$("#luckysheet-modal-dialog-pivotTable-list").on("click"," .luckysheet-slider-list-item-filter",function(g){return n.luckysheetsliderlistitemfilter($(this)),g.stopPropagation(),!1}),$("#luckysheet-modal-dialog-pivotTable-list").on("click"," .luckysheet-slider-list-item-filtered",function(g){return n.luckysheetsliderlistclearfilter($(this).next()),g.stopPropagation(),!1}),$("#luckysheet-dialog-pivotTable-range-seleted").click(function(){$("#luckysheet-modal-dialog-slider-pivot").hide(),Lt();let g=$("#luckysheet-data-pivotTable-selection"),p=g.outerHeight(),v=g.outerWidth(),y=$(window).width(),b=$(window).height(),k=$(document).scrollLeft(),w=$(document).scrollTop();$("#luckysheet-data-pivotTable-selection").css({left:(y+k-v)/2,top:(b+w-p)/4}).show(),n.jgridCurrentPivotInput=$("#luckysheet-dialog-pivotTable-range").html(),$("#luckysheet-pivotTable-range-selection-input").val(n.jgridCurrentPivotInput),n.luckysheet_pivotTable_select_state=!0}),$("#luckysheet-pivotTableFilter-initial").click(function(){$("#luckysheet-modal-dialog-pivotTable-list .luckysheet-slider-list-item-filtered").hide(),$("#luckysheet-modal-dialog-pivotTable-list .luckysheet-modal-dialog-slider-list-item").data("rowhidden",""),$("#luckysheet-pivotTableFilter-menu, #luckysheet-pivotTableFilter-submenu").hide(),$("#luckysheet-pivotTableFilter-menu .luckysheet-pivotTableFilter-selected-input").hide().find("input").val(),$("#luckysheet-pivotTableFilter-selected span").data("type","0").data("type",null).text(o.filiterInputNone),n.setDatatojsfile("filterparm",null),n.celldata=n.origindata,n.refreshPivotTable()}),$("#luckysheet-modal-dialog-config-row, #luckysheet-modal-dialog-config-column").on("click",".luckysheet-modal-dialog-slider-config-item-icon",function(g){let p=$(g.target),v=p.closest(".luckysheet-modal-dialog-slider-config-item"),y=v.data("index"),b=v.offset(),k=v.data("order"),w=v.data("orderby"),C=v.data("stastic");k==null&&(k="default");let I='";return $("#luckysheet-modal-dialog-config-value .luckysheet-modal-dialog-slider-config-item").each(function(x){I+='"}),$("#luckysheet-pivotTable-config-option-orderby").empty().html(I),w==null&&(w="self"),C==null&&(C="1"),$("#luckysheet-pivotTable-config-option-order").val(k).data("index",y),$("#luckysheet-pivotTable-config-option-orderby").val(w).data("index",y),$("#luckysheet-pivotTable-config-option-stastic").val(C).data("index",y),sl($("#luckysheet-pivotTable-config-option"),b.left+v.outerWidth(),b.top-13,"rightbottom"),g.stopPropagation(),!1}),$("#luckysheet-pivotTable-config-option-order,#luckysheet-pivotTable-config-option-orderby,#luckysheet-pivotTable-config-option-stastic").change(function(){let g=$(this),p=g.data("index");$("#luckysheet-modal-dialog-config-row, #luckysheet-modal-dialog-config-column").find(".luckysheet-modal-dialog-slider-config-item").each(function(){$(this).data("index")==p&&$(this).data(g.attr("id").replace("luckysheet-pivotTable-config-option-",""),g.val())}),n.refreshPivotTable()}),$("#luckysheet-modal-dialog-config-value").on("click",".luckysheet-modal-dialog-slider-config-item-icon",function(g){let p=$(g.target),v=p.closest(".luckysheet-modal-dialog-slider-config-item"),y=v.data("index"),b=v.offset(),k=v.data("sumtype"),w=n.pivot_data_type[y.toString()];k==null&&(w=="num"?k="SUM":k="COUNTA");let C=$("#luckysheet-pivotTable-config-option-sumtype");return C.find(".luckysheet-submenu-arrow").hide(),C.find(".luckysheet-cols-menuitem[sumtype='"+k+"'] .luckysheet-submenu-arrow").css("display","inline"),C.data("item",v),sl(C,b.left+v.outerWidth(),b.top-13,"rightbottom"),g.stopPropagation(),!1}),$("#luckysheet-pivotTable-config-option-sumtype .luckysheet-cols-menuitem").click(function(){let g=$("#luckysheet-pivotTable-config-option-sumtype").data("item"),p=$(this).attr("sumtype");g.data("sumtype",$(this).attr("sumtype"));let v=n.getSumTypeName(p)+":"+g.data("name");g.attr("title",v).find(".luckysheet-modal-dialog-slider-config-item-txt").html(v),$("#luckysheet-pivotTable-config-option-sumtype").hide(),n.refreshPivotTable()}),$("#luckysheet-modal-dialog-config-filter").on("click",".luckysheet-modal-dialog-slider-config-item-icon",function(g){let p=$(g.target),v=p.closest(".luckysheet-modal-dialog-slider-config-item").data("index");return n.luckysheetsliderlistitemfilter($("#luckysheet-modal-dialog-pivotTable-list .luckysheet-modal-dialog-slider-list-item").eq(v).find(".luckysheet-slider-list-item-filter")),g.stopPropagation(),!1}),$("#luckysheet-pivotTableFilter-confirm").click(function(){let p=$("#luckysheet-pivotTableFilter-menu").data("index"),v={};$("#luckysheet-modal-dialog-pivotTable-list .luckysheet-modal-dialog-slider-list-item").each(function(){let x=$(this),T=x.data("rowhidden");if(x.data("index")!=p){if(T==null||T=="")return!0;N(T)=="string"&&(T=JSON.parse(T));for(let R in T)v[R]=0}});let y=n.origindata,b={},k={},w={};if($("#luckysheet-pivotTableFilter-bycondition").next().is(":visible")&&$("#luckysheet-pivotTableFilter-byvalue").next().is(":hidden")&&$("#luckysheet-pivotTableFilter-selected span").data("value")!="null"){let x=$("#luckysheet-pivotTableFilter-selected span"),T=x.data("type"),R=x.data("value");if(w.value=R,w.text=x.text(),T=="0")w.type="0";else if(T=="2"){let D=$("#luckysheet-pivotTableFilter-menu .luckysheet-pivotTableFilter-selected-input2 input");w.type="2",w.value1=D.eq(0).val(),w.value2=D.eq(1).val()}else w.type="1",w.value1=$("#luckysheet-pivotTableFilter-menu .luckysheet-pivotTableFilter-selected-input").eq(0).find("input").val();for(let D=1;D-1&&(k[D]=0)}else if(R=="textstart"){let B=w.value1,F=B.length;(S==null||me(S.v)||S.m.substr(0,F)!=B)&&(k[D]=0)}else if(R=="textend"){let B=w.value1,F=B.length;(S==null||me(S.v)||F>S.m.length||S.m.substr(S.m.length-F,F)!=B)&&(k[D]=0)}else if(R=="textequal"){let B=w.value1;(S==null||me(S.v)||S.m!=B)&&(k[D]=0)}else if(R=="dateequal"){let B=it(w.value1)[2];S==null||me(S.v)?k[D]=0:S.ct!=null&&S.ct.t=="d"?parseInt(S.v)!=B&&(k[D]=0):k[D]=0}else if(R=="datelessthan"){let B=it(w.value1)[2];S==null||me(S.v)?k[D]=0:S.ct!=null&&S.ct.t=="d"?parseInt(S.v)>=B&&(k[D]=0):k[D]=0}else if(R=="datemorethan"){let B=it(w.value1)[2];S==null||me(S.v)?k[D]=0:S.ct!=null&&S.ct.t=="d"?parseInt(S.v)<=B&&(k[D]=0):k[D]=0}else if(R=="morethan"){let B=parseFloat(w.value1);S==null||me(S.v)?k[D]=0:S.ct!=null&&S.ct.t=="n"?S.v<=B&&(k[D]=0):k[D]=0}else if(R=="moreequalthan"){let B=parseFloat(w.value1);S==null||me(S.v)?k[D]=0:S.ct!=null&&S.ct.t=="n"?S.v=B&&(k[D]=0):k[D]=0}else if(R=="lessequalthan"){let B=parseFloat(w.value1);S==null||me(S.v)?k[D]=0:S.ct!=null&&S.ct.t=="n"?S.v>B&&(k[D]=0):k[D]=0}else if(R=="equal"){let B=parseFloat(w.value1);S==null||me(S.v)?k[D]=0:S.ct!=null&&S.ct.t=="n"?S.v!=B&&(k[D]=0):k[D]=0}else if(R=="noequal"){let B=parseFloat(w.value1);S==null||me(S.v)?k[D]=0:S.ct!=null&&S.ct.t=="n"?S.v==B&&(k[D]=0):k[D]=0}else if(R=="include"){let B=parseFloat(w.value1),F=parseFloat(w.value2),E,z;Bz)&&(k[D]=0):k[D]=0}else if(R=="noinclude"){let B=parseFloat(w.value1),F=parseFloat(w.value2),E,z;B=E&&S.v<=z&&(k[D]=0):k[D]=0}}}else{$("#luckysheet-pivotTableFilter-byvalue-select .ListBox input[type='checkbox']").each(function(x,T){if($(T).is(":visible")&&$(T).is(":checked"))return!0;if($(T).closest(".day").length>0){let R=$(T).siblings("label").text();Number(R)<10&&(R="0"+R);let D=$(T).closest(".monthBox").find(".month label").text().replace(o.filiterMonthText,"");Number(D)<10&&(D="0"+D);let S=$(T).closest(".yearBox").find(".year label").text().replace(o.filiterYearText,""),B=o.filterDateFormatTip+"#$$$#"+S+"-"+D+"-"+R;b[B]="1"}if($(T).closest(".textBox").length>0){let R=$(T).closest(".textBox").data("filter");b[R]="1"}});for(let x=1;x0||$("#luckysheet-pivotTableFilter-bycondition").next().is(":visible")&&$("#luckysheet-pivotTableFilter-byvalue").next().is(":hidden")&&$("#luckysheet-pivotTableFilter-selected span").data("value")!="null"?(C.data("rowhidden",JSON.stringify(k)).find(".luckysheet-slider-list-item-filtered").show(),n.setDatatojsfile("rowhidden",k,p),w!=null&&(C.data("byconditionvalue",w.value).data("byconditiontype",w.type).data("byconditiontext",w.text),w.value1!=null&&C.data("byconditionvalue1",w.value1),w.value2!=null&&C.data("byconditionvalue2",w.value2),n.setDatatojsfile("caljs",w,p))):(C.data("rowhidden","").find(".luckysheet-slider-list-item-filtered").hide(),n.setDatatojsfile("rowhidden",null,p));let I=[];for(let x=0;x1){y=v[0],b=v[1];for(let I in d.luckysheetfile)if(y==d.luckysheetfile[I].name){k=d.luckysheetfile[I].index;break}k==-1&&(k=0)}else{let I=Q(d.currentSheetIndex);y=d.luckysheetfile[I].name,k=d.luckysheetfile[I].index,b=v[0]}if(d.luckysheetfile[Q(k)].isPivotTable){he()?alert(l.errorNotAllowPivotData):U.info("",l.errorNotAllowPivotData),g.val(n.jgridCurrentPivotInput);return}if(b.indexOf(":")==-1){he()?alert(l.errorSelectionRange):U.info("",l.errorSelectionRange),g.val(n.jgridCurrentPivotInput);return}b=b.split(":");let w=[],C=[];if(w[0]=parseInt(b[0].replace(/[^0-9]/g,""))-1,w[1]=parseInt(b[1].replace(/[^0-9]/g,""))-1,w[0]>w[1]){he()?alert(l.errorSelectionRange):U.info("",l.errorSelectionRange),g.val(n.jgridCurrentPivotInput);return}if(C[0]=hl(b[0].replace(/[^A-Za-z]/g,"")),C[1]=hl(b[1].replace(/[^A-Za-z]/g,"")),C[0]>C[1]){he()?alert(l.errorSelectionRange):U.info(l.errorSelectionRange),g.val(n.jgridCurrentPivotInput);return}ve.changeSheetExec(n.pivotSheetIndex),n.setDatatojsfile("pivot_select_save",{row:w,column:C}),n.setDatatojsfile("pivotDataSheetIndex",k),n.getCellData(n.pivotSheetIndex,k,{row:w,column:C}),n.initialPivotManage(),$("#luckysheet-dialog-pivotTable-range").html(p),$("#luckysheet-modal-dialog-slider-pivot").show(),$("#luckysheet-data-pivotTable-selection").hide(),n.luckysheet_pivotTable_select_state=!1,n.refreshPivotTable(),Lt(),al()}}),$("#luckysheet-modal-dialog-slider-pivot").on("mousedown",".luckysheet-slider-list-item-name, .luckysheet-modal-dialog-slider-config-item-txt",function(g){let p=$(g.target);n.movestate=!0,n.movesave.obj=p.parent(),n.movesave.name=p.data("name"),n.movesave.containerid=p.parent().parent().attr("id"),n.movesave.index=p.data("index"),$("#luckysheet-modal-dialog-slider-pivot-move").length==0&&$("body").append('
'+n.movesave.name+"
"),n.movesave.width=$("#luckysheet-modal-dialog-slider-pivot-move").outerWidth(),n.movesave.height=$("#luckysheet-modal-dialog-slider-pivot-move").outerHeight(),$("#luckysheet-modal-dialog-pivotTable-list, #luckysheet-modal-dialog-config-filter, #luckysheet-modal-dialog-config-row, #luckysheet-modal-dialog-config-column, #luckysheet-modal-dialog-config-value").css("cursor","default")}),$("#luckysheet-modal-dialog-config-filter, #luckysheet-modal-dialog-config-row, #luckysheet-modal-dialog-config-column, #luckysheet-modal-dialog-config-value").mousemove(function(g){if(n.movestate){n.moveitemposition.length==0&&(n.moveitemposition=[0],$(this).find(".luckysheet-modal-dialog-slider-config-item").each(function(C){let I=$(this),x=I.outerHeight();n.moveitemposition.push(n.moveitemposition[C]+x+2)}),$(this).append('
')),$("#luckysheet-modal-dialog-slider-pivot-move").css({background:"#FD8585",color:"#fff",border:"1px solid #FD7070"});let p=event.pageX,v=event.pageY,y=$(this),b=v-y.offset().top+y.scrollTop(),k=n.moveitemposition,w=nt(k,b);w==-1?$("#luckysheet-modal-dialog-config-order-help").css({top:k[k.length-1]}):b-k[w-1]>(k[w]-k[w-1])/2?$("#luckysheet-modal-dialog-config-order-help").css({top:k[w]}):$("#luckysheet-modal-dialog-config-order-help").css({top:k[w-1]})}}).mouseleave(function(){n.movestate&&($("#luckysheet-modal-dialog-slider-pivot-move").css({background:"#fff",color:"#000",border:"1px dotted #000"}),n.moveitemposition=[],$("#luckysheet-modal-dialog-config-order-help").remove())}).mouseup(function(g){if(n.movestate){let p=$(this),v;if(n.movesave.containerid==p.attr("id"))v=n.movesave.obj.clone();else{let x=n.movesave.name,T="",R="";p.attr("id")=="luckysheet-modal-dialog-config-value"&&(n.pivot_data_type[n.movesave.index.toString()]=="num"?(x=l.valueStatisticsSUM+":"+x,T="data-sumtype='SUM'",R="data-nameindex='0'"):(x=l.valueStatisticsCOUNTA+":"+x,T="data-sumtype='COUNTA'",R="data-nameindex='0'"),$("#luckysheet-modal-dialog-config-value").find(".luckysheet-modal-dialog-slider-config-item").each(function(){if($(this).find(".luckysheet-modal-dialog-slider-config-item-txt").text()==x){let S=parseFloat($(this).data("nameindex"))+1;return x=x+S.toString(),$(this).data("nameindex",S),!1}})),v='
'+x+'
'}let y=event.pageX,b=event.pageY,k=$(this),w=b-k.offset().top+k.scrollTop(),C=n.moveitemposition,I=nt(C,w);(n.movesave.containerid=="luckysheet-modal-dialog-pivotTable-list"||n.movesave.containerid=="luckysheet-modal-dialog-config-value"&&n.movesave.containerid!=p.attr("id"))&&$("#luckysheet-modal-dialog-config-filter, #luckysheet-modal-dialog-config-row, #luckysheet-modal-dialog-config-column").find(".luckysheet-modal-dialog-slider-config-item").each(function(){$(this).data("index")==n.movesave.index&&$(this).remove()}),I==-1?p.find(".luckysheet-modal-dialog-slider-config-item").length==0?p.append(v):p.find(".luckysheet-modal-dialog-slider-config-item").last().after(v):w-C[I-1]>(C[I]-C[I-1])/2?p.find(".luckysheet-modal-dialog-slider-config-item").eq(I-1).after(v):p.find(".luckysheet-modal-dialog-slider-config-item").eq(I-1).before(v),n.movesave.containerid=="luckysheet-modal-dialog-pivotTable-list"||n.movesave.containerid=="luckysheet-modal-dialog-config-value"&&n.movesave.containerid!=p.attr("id")||n.movesave.obj.remove(),$("#luckysheet-modal-dialog-pivotTable-list").find(".luckysheet-modal-dialog-slider-list-item").each(function(){let x=$(this).find(".luckysheet-slider-list-item-selected");$(this).data("index")==n.movesave.index&&x.find("i").length==0&&x.append('')}),n.refreshPivotTable(),$("#luckysheet-modal-dialog-slider-pivot-move").remove(),n.movestate=!1,$("#luckysheet-modal-dialog-pivotTable-list, #luckysheet-modal-dialog-config-filter, #luckysheet-modal-dialog-config-row, #luckysheet-modal-dialog-config-column, #luckysheet-modal-dialog-config-value").css("cursor","default"),n.moveitemposition=[],$("#luckysheet-modal-dialog-config-order-help").remove(),n.showvaluecolrow(),g.stopPropagation()}}),$("#luckysheet-modal-dialog-pivotTable-list").on("click",".luckysheet-slider-list-item-selected",function(){let g=$(this),p=g.parent(),v=p.data("index"),y=p.data("name");if(g.find("i").length==0){g.append('');let b=n.pivot_data_type[v.toString()],k;if(b=="num")k='
\u6C42\u548C:'+y+'
',$("#luckysheet-modal-dialog-config-value").append(k);else{k='
'+y+'
';let w=$("#luckysheet-modal-dialog-config-column"),C=$("#luckysheet-modal-dialog-config-row"),I=w.find(".luckysheet-modal-dialog-slider-config-item"),x=C.find(".luckysheet-modal-dialog-slider-config-item");I.length<2?w.append(k):x.length<2?C.append(k):w.append(k)}}else g.find("i").remove(),$("#luckysheet-modal-dialog-config-filter, #luckysheet-modal-dialog-config-row, #luckysheet-modal-dialog-config-column, #luckysheet-modal-dialog-config-value").find(".luckysheet-modal-dialog-slider-config-item").each(function(){$(this).data("index")==v&&($(this).parent().attr("id")=="luckysheet-modal-dialog-config-value"&&n.resetOrderby($(this)),$(this).remove())});n.refreshPivotTable(),n.showvaluecolrow()}),$("#luckysheet-dialog-pivotTable-clearitem").click(function(){$("#luckysheet-modal-dialog-config-filter, #luckysheet-modal-dialog-config-row, #luckysheet-modal-dialog-config-column, #luckysheet-modal-dialog-config-value").find(".luckysheet-modal-dialog-slider-config-item").each(function(){$(this).remove()}),$("#luckysheet-modal-dialog-pivotTable-list").find(".luckysheet-modal-dialog-slider-list-item").each(function(){$(this).find(".luckysheet-slider-list-item-selected").find("i").remove()}),n.refreshPivotTable(),n.showvaluecolrow()})}e==null&&(e=!1),n.celldata.length<=1&&n.celldata[0].length<=1&&(he()?alert(l.errorIncreaseRange):U.info("",l.errorIncreaseRange));let s="",u=1,f={},A=1;for(let h=0;h0&&(v="display:block;"),s+='
'+g+'
'}if($("#luckysheet-modal-dialog-pivotTable-list").html(s),$("#luckysheetpivottablevaluecolrowshow").hide(),$("#luckysheetpivottablevaluecolrow").prop("checked",!0),$("#luckysheetpivottablevaluecolrow1").prop("checked",!1),$("#luckysheet-modal-dialog-config-filter, #luckysheet-modal-dialog-config-row, #luckysheet-modal-dialog-config-column, #luckysheet-modal-dialog-config-value").empty(),e){if(n.filter!=null&&n.filter.length>0)for(let h=0;h
'+g.name+'
';$("#luckysheet-modal-dialog-config-filter").append(p);let v=$("#luckysheet-modal-dialog-pivotTable-list .luckysheet-modal-dialog-slider-list-item").eq(g.index).find(".luckysheet-slider-list-item-selected");v.find("i").length==0&&v.append('')}if(n.row!=null&&n.row.length>0)for(let h=0;h
'+g.name+'
';$("#luckysheet-modal-dialog-config-row").append(v);let y=$("#luckysheet-modal-dialog-pivotTable-list .luckysheet-modal-dialog-slider-list-item").eq(g.index).find(".luckysheet-slider-list-item-selected");y.find("i").length==0&&y.append('')}if(n.column!=null&&n.column.length>0)for(let h=0;h
'+g.name+'
';$("#luckysheet-modal-dialog-config-column").append(v);let y=$("#luckysheet-modal-dialog-pivotTable-list .luckysheet-modal-dialog-slider-list-item").eq(g.index).find(".luckysheet-slider-list-item-selected");y.find("i").length==0&&y.append('')}if(n.values!=null&&n.values.length>0){for(let h=0;h
'+n.getSumTypeName(g.sumtype)+":"+g.name+'
';$("#luckysheet-modal-dialog-config-value").append(v);let y=$("#luckysheet-modal-dialog-pivotTable-list .luckysheet-modal-dialog-slider-list-item").eq(g.index).find(".luckysheet-slider-list-item-selected");y.find("i").length==0&&y.append('')}n.values.length>=2&&($("#luckysheetpivottablevaluecolrowshow").show(),n.showType=="column"?($("#luckysheetpivottablevaluecolrow").prop("checked",!0),$("#luckysheetpivottablevaluecolrowshow label[for='luckysheetpivottablevaluecolrow']").addClass("ui-state-active"),$("#luckysheetpivottablevaluecolrow1").prop("checked",!1),$("#luckysheetpivottablevaluecolrowshow label[for='luckysheetpivottablevaluecolrow1']").removeClass("ui-state-active")):($("#luckysheetpivottablevaluecolrow1").prop("checked",!0),$("#luckysheetpivottablevaluecolrowshow label[for='luckysheetpivottablevaluecolrow1']").addClass("ui-state-active"),$("#luckysheetpivottablevaluecolrow").prop("checked",!1),$("#luckysheetpivottablevaluecolrowshow label[for='luckysheetpivottablevaluecolrow']").removeClass("ui-state-active")))}}$("#luckysheet-dialog-pivotTable-range").html(mt(n.pivotDataSheetIndex,n.pivot_select_save)),$("#luckysheet-modal-dialog-slider-pivot").show(),Lt(!1)},getComposeArray:function(e){if(e.length==0)return[];let n=[];for(let t=0;t0){o.children=l.orderbygroupchildren(o.children,n[o.index].orderby,n[o.index].order,t);for(let A=0;Ae[t].max&&(e[t].max=a),ae[t].acc&&(e[t].acc=o)}l!=""&&(e[t].data.push(l),e[t].counta+=1,l in e[t].countuniquedata||(e[t].countuniquedata[l]=1,e[t].countunique+=1))},dataHandler:function(e,n,t,l,a){let o=this,s=te(),u=s.filter,f=s.pivotTable;if(l==null&&(l="column"),e.length==0&&n.length==0&&t.length==0||a.length==0)return o.pivotDatas=[],[];let A={},h=a,g=[],p=[],v={},y=0,b=[],k=[],w={},C=0;for(let F=1;F0&&O.unshift(f.valueSum),G.length>0&&G.unshift(f.valueSum);let ee=v,re=p;for(let fe=0;fe0?(K.push(""),K=K.join(t[fe].fullname+"|||").split("|||").slice(0,K.length-1)):K.push(t[fe].fullname):W.length>0?(W.push(""),W=W.join(t[fe].fullname+"|||").split("|||").slice(0,W.length-1)):W.push(t[fe].fullname),K.length==0&&K.push(""),W.length==0&&W.push("");for(let J=0;JE.acc&&(E.acc=z),E.result=Pl(E.result,E.acc)}if(g=o.getTitleFromGroup(p,n,A),b=o.getTitleFromGroup(k,e,A),l=="column")if(b.length>0&&b[0].length>0)b=o.addValuesToTitle(b,t);else for(let F=0;F0&&g[0].length>0)g=o.addValuesToTitle(g,t);else for(let F=0;F=S)b[F]!=null?N(b[F][E-S])=="object"?B[F][E]=b[F][E-S].name+f.valueSum:B[F][E]=b[F][E-S]:B[F][E]="";else if(F>=D&&E0&&n.length>0)B[0][0]=t[0].fullname,B.splice(e.length,1);else if(t.length==1&&e.length>0){let F=B.splice(e.length,1),E=[];for(let z=0;z0)for(let g=0;g0)for(let g=0;g{je();Th=function(){let e,n;return e=function(){this.init.apply(this,arguments)},arguments.length>1?(arguments[0]?(e.prototype=$.extend(new arguments[0],arguments[arguments.length-1]),e._super=arguments[0].prototype):e.prototype=arguments[arguments.length-1],arguments.length>2&&(n=Array.prototype.slice.call(arguments,1,-1),n.unshift(e.prototype),$.extend.apply($,n))):e.prototype=arguments[0],e.prototype.cls=e,e},My=Th({fre:/\{\{([\w.]+?)(:(.+?))?\}\}/g,precre:/(\w+)\.(\d+)/,init:function(e,n){this.format=e,this.fclass=n},render:function(e,n,t){let l=this,a=e,o,s,u,f,A;return this.format.replace(this.fre,function(){let h;return s=arguments[1],u=arguments[3],o=l.precre.exec(s),o?(A=o[2],s=o[1]):A=!1,f=a[s],f===void 0?"":u&&n&&n[u]?(h=n[u],h.get?n[u].get(f)||f:n[u][f]||f):(Ey(f)&&(t.get("numberFormatter")?f=t.get("numberFormatter")(f):f=By(f,A,t.get("numberDigitGroupCount"),t.get("numberDigitGroupSep"),t.get("numberDecimalMark"))),f)})}});$.spformat=function(e,n){return new My(e,n)};uo=function(e,n,t){return et?t:e},cu=function(e,n){let t;return n===2?(t=Math.floor(e.length/2),e.length%2?e[t]:(e[t-1]+e[t])/2):e.length%2?(t=(e.length*n+n)/4,t%1?(e[Math.floor(t)]+e[Math.floor(t)-1])/2:e[t-1]):(t=(e.length*n+2)/4,t%1?(e[Math.floor(t)]+e[Math.floor(t)-1])/2:e[t-1])},uu=function(e){let n;switch(e){case"undefined":e=void 0;break;case"null":e=null;break;case"true":e=!0;break;case"false":e=!1;break;default:n=parseFloat(e),e==n&&(e=n)}return e},du=function(e){let n,t=[];for(n=e.length;n--;)t[n]=uu(e[n]);return t},vs=function(e,n,t){let l;for(l=n.length;l--;)if(!(t&&n[l]===null)&&n[l]!==e)return!1;return!0},Ih=function(e,n){let t,l,a=[];for(t=0,l=e.length;t0;s-=t)e.splice(s,0,l);return e.join("")},fo=Th({init:function(e){let n,t,l=[];for(n in e)e.hasOwnProperty(n)&&typeof n=="string"&&n.indexOf(":")>-1&&(t=n.split(":"),t[0]=t[0].length===0?-Infinity:parseFloat(t[0]),t[1]=t[1].length===0?Infinity:parseFloat(t[1]),t[2]=e[n],l.push(t));this.map=e,this.rangelist=l||!1},get:function(e){let n=this.rangelist,t,l,a;if((a=this.map[e])!==void 0)return a;if(n){for(t=n.length;t--;)if(l=n[t],l[0]<=e&&l[1]>=e)return l[2]}}});$.range_map=function(e){return new fo(e)};Vr={defaultOption:{common:{type:"line",lineColor:"#2ec7c9",fillColor:"#CCF3F4",defaultPixelsPerValue:3,width:"auto",height:"auto",composite:!1,tagValuesAttribute:"values",tagOptionsPrefix:"spark",enableTagOptions:!1,enableHighlight:!0,highlightLighten:1.4,tooltipSkipNull:!0,tooltipPrefix:"",tooltipSuffix:"",disableHiddenCheck:!1,numberFormatter:!1,numberDigitGroupCount:3,numberDigitGroupSep:",",numberDecimalMark:".",disableTooltips:!0,disableInteraction:!0,offsetX:0,offsetY:0},line:{spotColor:0,highlightSpotColor:"#5f5",highlightLineColor:"#f22",spotRadius:1.5,minSpotColor:0,maxSpotColor:0,lineWidth:1,normalRangeMin:void 0,normalRangeMax:void 0,normalRangeColor:"#ccc",drawNormalOnTop:!0,chartRangeMin:void 0,chartRangeMax:void 0,chartRangeMinX:void 0,chartRangeMaxX:void 0},bar:{barColor:"#fc5c5c",negBarColor:"#97b552",stackedBarColor:["#2ec7c9","#fc5c5c","#5ab1ef","#ffb980","#d87a80","#8d98b3","#e5cf0d","#97b552","#95706d","#dc69aa","#07a2a4","#9a7fd1","#588dd5","#f5994e","#c05050","#59678c","#c9ab00","#7eb00a","#6f5553","#c14089"],zeroColor:void 0,nullColor:void 0,zeroAxis:!0,barWidth:4,barSpacing:1,chartRangeMax:void 0,chartRangeMin:void 0,chartRangeClip:!1,colorMap:void 0},column:{barColor:"#fc5c5c",negBarColor:"#97b552",stackedBarColor:["#2ec7c9","#fc5c5c","#5ab1ef","#ffb980","#d87a80","#8d98b3","#e5cf0d","#97b552","#95706d","#dc69aa","#07a2a4","#9a7fd1","#588dd5","#f5994e","#c05050","#59678c","#c9ab00","#7eb00a","#6f5553","#c14089"],zeroColor:void 0,nullColor:void 0,zeroAxis:!0,barWidth:4,barSpacing:1,chartRangeMax:void 0,chartRangeMin:void 0,chartRangeClip:!1,colorMap:void 0},tristate:{barWidth:4,barSpacing:1,posBarColor:"#fc5c5c",negBarColor:"#97b552",zeroBarColor:"#999",colorMap:{}},discrete:{lineHeight:"auto",thresholdColor:"#fc5c5c",thresholdValue:0,chartRangeMax:void 0,chartRangeMin:void 0,chartRangeClip:!1},bullet:{targetColor:"#f33",targetWidth:3,performanceColor:"#33f",rangeColors:["#d3dafe","#a8b6ff","#7f94ff","#6D87FF","#5876FF","#4465FF","#2F54FF","#1A43FF","#0532FF"],base:void 0},pie:{offset:0,sliceColors:["#2ec7c9","#fc5c5c","#5ab1ef","#ffb980","#d87a80","#8d98b3","#e5cf0d","#97b552","#95706d","#dc69aa","#07a2a4","#9a7fd1","#588dd5","#f5994e","#c05050","#59678c","#c9ab00","#7eb00a","#6f5553","#c14089"],borderWidth:0,borderColor:"#000"},box:{raw:!1,boxLineColor:"#000",boxFillColor:"#cdf",whiskerColor:"#000",outlierLineColor:"#5E5E5E",outlierFillColor:"#fff",medianColor:"#f00",showOutliers:!0,outlierIQR:1.5,spotRadius:1.5,target:void 0,targetColor:"#4a2",chartRangeMax:void 0,chartRangeMin:void 0}},line:{type:"line",init:function(e,n,t,l,a){this.vertices=[],this.regionMap=[],this.xvalues=[],this.yvalues=[],this.yminmax=[],this.hightlightSpotId=null,this.lastShapeId=null},getRegion:function(e,n,t){let l,a=this.regionMap;for(l=a.length;l--;)if(a[l]!==null&&n>=a[l][0]&&n<=a[l][1])return a[l][2]},getCurrentRegionFields:function(){let e=this.currentRegion;return{isNull:this.yvalues[e]===null,x:this.xvalues[e],y:this.yvalues[e],color:this.options.get("lineColor"),fillColor:this.options.get("fillColor"),offset:e}},renderHighlight:function(){let e=this.currentRegion,n=this.target,t=this.vertices[e],l=this.options,a=l.get("spotRadius"),o=l.get("highlightSpotColor"),s=l.get("highlightLineColor"),u,f;!t||(a&&o&&(u=n.drawCircle(t[0],t[1],a,void 0,o),this.highlightSpotId=u.id,n.insertAfterShape(this.lastShapeId,u)),s&&(f=n.drawLine(t[0],this.canvasTop,t[0],this.canvasTop+this.canvasHeight,s),this.highlightLineId=f.id,n.insertAfterShape(this.lastShapeId,f)))},removeHighlight:function(){let e=this.target;this.highlightSpotId&&(e.removeShapeId(this.highlightSpotId),this.highlightSpotId=null),this.highlightLineId&&(e.removeShapeId(this.highlightLineId),this.highlightLineId=null)},scanValues:function(){let e=this.values,n=e.length,t=this.xvalues,l=this.yvalues,a=this.yminmax,o,s,u,f,A;for(o=0;othis.maxy&&(this.maxy=t)),e.get("chartRangeMin")!==void 0&&(e.get("chartRangeClip")||e.get("chartRangeMin")this.maxy)&&(this.maxy=e.get("chartRangeMax")),e.get("chartRangeMinX")!==void 0&&(e.get("chartRangeClipX")||e.get("chartRangeMinX")this.maxx)&&(this.maxx=e.get("chartRangeMaxX"))},drawNormalRange:function(e,n,t,l,a){let o=this.options.get("normalRangeMin"),s=this.options.get("normalRangeMax"),u=n+Math.round(t-t*((s-this.miny)/a)),f=Math.round(t*(s-o)/a);f==0&&o==s&&(f=1),this.target.drawRect(e,u,l,f,void 0,this.options.get("normalRangeColor")).append()},render:function(e,n){this.vertices=[],this.regionMap=[],this.xvalues=[],this.yvalues=[],this.yminmax=[],this.hightlightSpotId=null,this.lastShapeId=null,this.values=n;let t=this.options,l=this.target,a=e.mergedOptions.width,o=e.mergedOptions.height,s=this.vertices,u=t.get("spotRadius"),f=this.regionMap,A,h,g,p,v,y,b,k,w,C,I,x,T,R,D,S,B,F,E,z,P,L,O,q,j;if(this.scanValues(),this.processRangeOptions(),O=this.xvalues,q=this.yvalues,!(!this.yminmax.length||this.yvalues.length<2)){for(p=v=0,A=this.maxx-this.minx==0?1:this.maxx-this.minx,h=this.maxy-this.miny==0?1:this.maxy-this.miny,g=this.yvalues.length-1,u&&(athis.maxy&&(C=this.maxy),b.length||b.push([x,p+o]),y=[x,p+Math.round(o-o*((C-this.miny)/h))],b.push(y),s.push(y));for(B=[],F=[],E=k.length,j=0;j2&&(b[0]=[b[0][0],b[1][1]]),B.push(b));for(E=F.length,j=0;j-1,(p||$.isArray(O))&&(F=!0,p&&(O=n[w]=du(O.split(":"))),O=Ih(O,null),v=Math.min.apply(Math,O),y=Math.max.apply(Math,O),vg&&(g=y));this.stacked=F,this.regionShapes={},this.barWidth=Math.floor(l/n.length)-s,this.barSpacing=s,this.totalBarWidth=this.barWidth+s,this.width=l,A&&(S=u===void 0?-Infinity:u,B=f===void 0?Infinity:f),k=[],b=F?[]:k;let _=[],ee=[];for(w=0,C=n.length;w0&&(_[w]+=O),h<0&&g>0?O<0?ee[w]+=Math.abs(O):b[w]+=O:b[w]+=Math.abs(O),k.push(O));else O=A?uo(n[w],S,B):n[w],O=n[w]=uu(O),O!==null&&k.push(O);this.max=D=Math.max.apply(Math,k),this.min=R=Math.min.apply(Math,k),this.stackMax=g=F?Math.max.apply(Math,_):D,this.stackMin=h=F?Math.min.apply(Math,k):R,t.get("chartRangeMin")!==void 0&&(t.get("chartRangeClip")||t.get("chartRangeMin")D)&&(D=t.get("chartRangeMax")),this.zeroAxis=x=t.get("zeroAxis",!0),R<=0&&D>=0&&x?T=0:x==!1?T=R:R>0?T=0:T=D,this.xaxisOffset=T,I=F?Math.max.apply(Math,b)+Math.max.apply(Math,ee):D-T,this.canvasHeightEf=x&&R<0?this.canvasHeight-2:this.canvasHeight-1,this.isNeg=!1,R=this.values.length?void 0:l},getCurrentRegionFields:function(){let e=this.currentRegion,n=ensureArray(this.values[e]),t=[],l,a;for(a=n.length;a--;)l=n[a],t.push({isNull:l===null,value:l,color:this.calcColor(a,l,e),offset:e});return t},calcColor:function(e,n,t){let l=this.colorMapByIndex,a=this.colorMapByValue,o=this.options,s,u;return this.stacked?s=o.get("stackedBarColor"):s=n<0?o.get("negBarColor"):o.get("barColor"),n===0&&o.get("zeroColor")!==void 0&&(s=o.get("zeroColor")),a&&(u=a.get(n))?s=u:l&&l.length>t&&(s=l[t]),$.isArray(s)?s[e%s.length]:s},renderRegion:function(e,n){let t=this.values[e],l=this.options,a=this.xaxisOffset,o=[],s=this.range,u=this.stacked,f=this.target,A=e*this.totalBarWidth,h=this.canvasHeightEf,g=this.yoffset,p,v,y,b,k,w,C,I,x,T;if(t=$.isArray(t)?t:[t],C=t.length,I=t[0],b=vs(null,t),T=vs(a,t,!0),b)return l.get("nullColor")?(y=n?l.get("nullColor"):this.calcHighlightColor(l.get("nullColor"),l),p=g>0?g-1:g,f.drawRect(p,A,0,this.barWidth-1,y,y)):void 0;for(k=g,this.isNeg&&(h=Math.floor(h/2)),w=0;w0?v=Math.floor(h*(Math.abs(I-a)/s)):v=h,I-1,(p||$.isArray(O))&&(F=!0,p&&(O=n[w]=du(O.split(":"))),O=Ih(O,null),v=Math.min.apply(Math,O),y=Math.max.apply(Math,O),vg&&(g=y));this.stacked=F,this.regionShapes={},this.barWidth=Math.floor(l/n.length)-s,this.barSpacing=s,this.totalBarWidth=this.barWidth+s,this.width=l,A&&(S=u===void 0?-Infinity:u,B=f===void 0?Infinity:f),k=[],b=F?[]:k;let _=[],ee=[];for(w=0,C=n.length;w0&&(_[w]+=O),h<0&&g>0?O<0?ee[w]+=Math.abs(O):b[w]+=O:b[w]+=Math.abs(O),k.push(O));else O=A?uo(n[w],S,B):n[w],O=n[w]=uu(O),O!==null&&k.push(O);this.max=D=Math.max.apply(Math,k),this.min=R=Math.min.apply(Math,k),this.stackMax=g=F?Math.max.apply(Math,_):D,this.stackMin=h=F?Math.min.apply(Math,k):R,t.get("chartRangeMin")!==void 0&&(t.get("chartRangeClip")||t.get("chartRangeMin")D)&&(D=t.get("chartRangeMax")),this.zeroAxis=x=t.get("zeroAxis",!0),R<=0&&D>=0&&x?T=0:x==!1?T=R:R>0?T=0:T=D,this.xaxisOffset=T,I=F?Math.max.apply(Math,b)+Math.max.apply(Math,ee):D-T,this.canvasHeightEf=x&&R<0?this.canvasHeight-2:this.canvasHeight-1,this.isNeg=!1,R=this.values.length?void 0:l},getCurrentRegionFields:function(){let e=this.currentRegion,n=ensureArray(this.values[e]),t=[],l,a;for(a=n.length;a--;)l=n[a],t.push({isNull:l===null,value:l,color:this.calcColor(a,l,e),offset:e});return t},calcColor:function(e,n,t){let l=this.colorMapByIndex,a=this.colorMapByValue,o=this.options,s,u;return this.stacked?s=o.get("stackedBarColor"):s=n<0?o.get("negBarColor"):o.get("barColor"),n===0&&o.get("zeroColor")!==void 0&&(s=o.get("zeroColor")),a&&(u=a.get(n))?s=u:l&&l.length>t&&(s=l[t]),$.isArray(s)?s[e%s.length]:s},renderRegion:function(e,n){let t=this.values[e],l=this.options,a=this.xaxisOffset,o=[],s=this.range,u=this.stacked,f=this.target,A=e*this.totalBarWidth,h=this.canvasHeightEf,g=this.yoffset,p,v,y,b,k,w,C,I,x,T;if(t=$.isArray(t)?t:[t],C=t.length,I=t[0],b=vs(null,t),T=vs(a,t,!0),b)return l.get("nullColor")?(y=n?l.get("nullColor"):this.calcHighlightColor(l.get("nullColor"),l),p=g>0?g-1:g,f.drawRect(A,p,this.barWidth-1,0,y,y)):void 0;for(k=g,this.isNeg&&(h=Math.floor(h/2)),w=0;w0?v=Math.floor(h*(Math.abs(I-a)/s)):v=h,In?s=a[n]:t[n]<0?s=l.get("negBarColor"):t[n]>0?s=l.get("posBarColor"):s=l.get("zeroBarColor"),s},renderRegion:function(e,n){let t=this.values,l=this.options,a=this.target,o,s,u,f,A,h;if(o=this.canvasHeight,u=Math.round(o/2),f=e*this.totalBarWidth,t[e]<0?(A=u,s=u-1):t[e]>0?(A=0,s=u-1):(A=u-1,s=2),h=this.calcColor(t[e],e),h!==null)return n&&(h=this.calcHighlightColor(h,l)),a.drawRect(f,A,this.barWidth-1,s-1,h,h)}},discrete:{type:"discrete",init:function(e,n){let t=this.options,l=e.mergedOptions.width,a=e.mergedOptions.height;this.canvasWidth=e.mergedOptions.width,this.canvasHeight=e.mergedOptions.height,this.regionShapes={},this.values=n=$.map(n,Number),this.min=Math.min.apply(Math,n),this.max=Math.max.apply(Math,n),this.range=this.max-this.min,this.width=l,this.interval=Math.floor(l/n.length),this.itemWidth=l/n.length,t.get("chartRangeMin")!==void 0&&(t.get("chartRangeClip")||t.get("chartRangeMin")this.max)&&(this.max=t.get("chartRangeMax")),this.target&&(this.lineHeight=t.get("lineHeight")==="auto"?Math.round(this.canvasHeight*.3):t.get("lineHeight"))},getRegion:function(e,n,t){return Math.floor(n/this.itemWidth)},getCurrentRegionFields:function(){let e=this.currentRegion;return{isNull:this.values[e]===void 0,value:this.values[e],offset:e}},renderRegion:function(e,n){let t=this.values,l=this.options,a=this.min,o=this.max,s=this.range,u=this.interval,f=this.target,A=this.canvasHeight,h=this.lineHeight,g=A-h,p,v,y,b;return v=uo(t[e],a,o),b=e*u,p=Math.round(g-g*((v-a)/s)),y=l.get("thresholdColor")&&v0)for(s=n.length;s--;)o+=n[s];this.total=o,this.radius=Math.floor(Math.min(this.canvasWidth,this.canvasHeight)/2)},getRegion:function(e,n,t){let l=this.target.getShapeAt(e,n,t);return l!==void 0&&this.shapes[l]!==void 0?this.shapes[l]:void 0},getCurrentRegionFields:function(){let e=this.currentRegion;return{isNull:this.values[e]===void 0,value:this.values[e],percent:this.values[e]/this.total*100,color:this.options.get("sliceColors")[e%this.options.get("sliceColors").length],offset:e}},changeHighlight:function(e){let n=this.currentRegion,t=this.renderSlice(n,e),l=this.valueShapes[n];delete this.shapes[l],this.target.replaceWithShape(l,t),this.valueShapes[n]=t.id,this.shapes[t.id]=n},renderSlice:function(e,n){let t=this.target,l=this.options,a=this.radius,o=l.get("borderWidth"),s=l.get("offset"),u=2*Math.PI,f=this.values,A=this.total,h=s?2*Math.PI*(s/360):0,g,p,v,y,b;for(y=f.length,v=0;v0&&(p=h+u*(f[v]/A)),e===v)return b=l.get("sliceColors")[v%l.get("sliceColors").length],n&&(b=this.calcHighlightColor(b,l)),t.drawPieSlice(a,a,a-o,g,p,void 0,b);h=p}},render:function(e,n){this.init(e,n);let t=this.target,l=this.values,a=this.options,o=this.radius,s=a.get("borderWidth"),u,f;for(s&&t.drawCircle(o,o,Math.floor(o-s/2),a.get("borderColor"),void 0,s).append(),f=l.length;f--;)l[f]&&(u=this.renderSlice(f).append(),this.valueShapes[f]=u.id,this.shapes[u.id]=f)}},box:{type:"box",init:function(e,n){let t=this.options,l=e.mergedOptions.width,a=e.mergedOptions.height;this.canvasWidth=e.mergedOptions.width,this.canvasHeight=e.mergedOptions.height,this.values=$.map(n,Number),this.width=t.get("width")==="auto"?"4.0em":l,this.values.length||(this.disabled=1)},getRegion:function(){return 1},getCurrentRegionFields:function(){let e=[{field:"lq",value:this.quartiles[0]},{field:"med",value:this.quartiles[1]},{field:"uq",value:this.quartiles[2]}];return this.loutlier!==void 0&&e.push({field:"lo",value:this.loutlier}),this.routlier!==void 0&&e.push({field:"ro",value:this.routlier}),this.lwhisker!==void 0&&e.push({field:"lw",value:this.lwhisker}),this.rwhisker!==void 0&&e.push({field:"rw",value:this.rwhisker}),e},render:function(e,n){this.init(e,n);let t=this.target,l=this.values,a=l.length,o=this.options,s=this.canvasWidth,u=this.canvasHeight,f=o.get("chartRangeMin")===void 0?Math.min.apply(Math,l):o.get("chartRangeMin"),A=o.get("chartRangeMax")===void 0?Math.max.apply(Math,l):o.get("chartRangeMax"),h=0,g,p,v,y,b,k,w,C,I,x,T;if(o.get("raw"))o.get("showOutliers")&&l.length>5?(p=l[0],g=l[1],y=l[2],b=l[3],k=l[4],w=l[5],C=l[6]):(g=l[0],y=l[1],b=l[2],k=l[3],w=l[4]);else if(l.sort(function(R,D){return R-D}),y=cu(l,1),b=cu(l,2),k=cu(l,3),v=k-y,o.get("showOutliers")){for(g=w=void 0,I=0;Iy-v*o.get("outlierIQR")&&(g=l[I]),l[I]w&&t.drawCircle((C-f)*T+h,u/2,o.get("spotRadius"),o.get("outlierLineColor"),o.get("outlierFillColor")).append()),t.drawRect(Math.round((y-f)*T+h),Math.round(u*.1),Math.round((k-y)*T),Math.round(u*.8),o.get("boxLineColor"),o.get("boxFillColor")).append(),t.drawLine(Math.round((g-f)*T+h),Math.round(u/2),Math.round((y-f)*T+h),Math.round(u/2),o.get("lineColor")).append(),t.drawLine(Math.round((g-f)*T+h),Math.round(u/4),Math.round((g-f)*T+h),Math.round(u-u/4),o.get("whiskerColor")).append(),t.drawLine(Math.round((w-f)*T+h),Math.round(u/2),Math.round((k-f)*T+h),Math.round(u/2),o.get("lineColor")).append(),t.drawLine(Math.round((w-f)*T+h),Math.round(u/4),Math.round((w-f)*T+h),Math.round(u-u/4),o.get("whiskerColor")).append(),t.drawLine(Math.round((b-f)*T+h),Math.round(u*.1),Math.round((b-f)*T+h),Math.round(u*.9),o.get("medianColor")).append(),o.get("target")&&(x=Math.ceil(o.get("spotRadius")),t.drawLine(Math.round((o.get("target")-f)*T+h),Math.round(u/2-x),Math.round((o.get("target")-f)*T+h),Math.round(u/2+x),o.get("targetColor")).append(),t.drawLine(Math.round((o.get("target")-f)*T+h-x),Math.round(u/2),Math.round((o.get("target")-f)*T+h+x),Math.round(u/2),o.get("targetColor")).append())}},shapeCount:0,shapes:{},shapeseq:[],lastShapeId:null,mergedOptions:null,init:function(e,n){let t,l,a;n=n||{};let o=this;return l=this.defaultOption,a=l.common,t=l[n.type||a.type],o.shapeCount=0,o.shapes={},o.shapeseq=[],o.lastShapeId=null,o.mergedOptions=$.extend({},a,t,n),o.mergedOptions.width=o.mergedOptions.width,o.mergedOptions.height=o.mergedOptions.height,o[o.mergedOptions.type].render(o,e),{shapes:o.shapes,shapeseq:o.shapeseq,offsetX:o.mergedOptions.offsetX,offsetY:o.mergedOptions.offsetY,pixelWidth:o.mergedOptions.width,pixelHeight:o.mergedOptions.height}},_getContext:function(e,n,t){let l;return this.ctx!=null?l=this.ctx:l=$("#"+this._canvasID).get(0).getContext("2d"),e!==void 0&&(l.strokeStyle=e),l.lineWidth=t===void 0?1:t,n!==void 0&&(l.fillStyle=n),l},reset:function(){this._getContext().clearRect(0,0,this.pixelWidth,this.pixelHeight),this.shapes={},this.shapeseq=[],this.currentTargetShapeId=void 0},_drawShape:function(e,n,t,l,a){let o=this._getContext(t,l,a),s,u;for(o.beginPath(),o.moveTo(n[0][0]+.5+this.offsetX,n[0][1]+.5+this.offsetY),s=1,u=n.length;s=d.visibledatarow.length&&(k=d.visibledatarow.length-1),w=nt(d.visibledatacolumn,e),C=nt(d.visibledatacolumn,e+t),w==-1&&(w=0),w+=s,C==-1&&(C=d.visibledatacolumn.length-1),C+=s,C>=d.visibledatacolumn.length&&(C=d.visibledatacolumn.length-1);let I,x,T,R;b==0?I=0:I=d.visibledatarow[b-1],x=d.visibledatarow[k],w==0?T=0:T=d.visibledatacolumn[w-1],R=d.visibledatacolumn[C],y.fillStyle="#ffffff",y.fillRect(a-1,o-1,R-e,x-n),y.font=ya(),y.fillStyle=Dl.fillStyle;let D=[],S={},B={};$e.createHookFunction("cellAllRenderBefore",d.flowdata,p,y);for(let O=b;O<=k;O++){let q;O==0?q=-n-1:q=d.visibledatarow[O-1]-n-1;let j=d.visibledatarow[O]-n;if(!(d.config.rowhidden!=null&&d.config.rowhidden[O]!=null))for(let G=w;G<=C;G++){let _;G==0?_=-e:_=d.visibledatacolumn[G-1]-e;let ee=d.visibledatacolumn[G]-e;if(d.config.colhidden!=null&&d.config.colhidden[G]!=null)continue;let re=d.defaultcollen;if(d.config.columnlen!=null&&d.config.columnlen[G]!=null&&(re=d.config.columnlen[G]),d.flowdata[O]!=null&&d.flowdata[O][G]!=null){let oe=d.flowdata[O][G];if(N(oe)=="object"&&"mc"in oe)if(B[O+"_"+G]={start_r:q,start_c:_,end_r:j,end_c:ee},"rs"in oe.mc){let se="r"+O+"c"+G;S[se]=D.length}else{let se="r"+oe.mc.r+"c"+oe.mc.c,ue=D[S[se]];ue==null?(S[se]=D.length,D.push({r:O,c:G,start_c:_,start_r:q,end_r:j,end_c:ee,firstcolumnlen:re})):(ue.c==G&&(ue.end_r+=j-q-1),ue.r==O&&(ue.end_c+=ee-_,ue.firstcolumnlen+=re));continue}}D.push({r:O,c:G,start_r:q,start_c:_,end_r:j,end_c:ee,firstcolumnlen:re}),B[O+"_"+G]={start_r:q,start_c:_,end_r:j,end_c:ee}}}let F=Zi(d.luckysheetfile[Q(d.currentSheetIndex)].dynamicArray),E=Jt.getComputeMap(),z=Re.getComputeMap(),P=Ny(y,w,C,b,k),L=[];for(let O=0;O0){let O=function(oe,se,ue,fe,ce,K,W,J,de){let le=oe,ke=fe-2+h+W,ie=ue+J-1,Ie=fe-2+h+W,Oe=ce-2+h+J;de.save(),be.setLineDash(de,le,"v",ke,ie,Ie,Oe),de.strokeStyle=se,de.stroke(),de.closePath(),de.restore()},q=function(oe,se,ue,fe,ce,K,W,J,de){let le=oe,ke=K-2+h+W,ie=ue+J-1,Ie=K-2+h+W,Oe=ce-2+h+J;de.save(),be.setLineDash(de,le,"v",ke,ie,Ie,Oe),de.strokeStyle=se,de.stroke(),de.closePath(),de.restore()},j=function(oe,se,ue,fe,ce,K,W,J,de){let le=oe,ke=fe-2+h+W,ie=ce-2+h+J,Ie=K-2+h+W,Oe=ce-2+h+J;de.save(),be.setLineDash(de,le,"h",ke,ie,Ie,Oe),de.strokeStyle=se,de.stroke(),de.closePath(),de.restore()},G=function(oe,se,ue,fe,ce,K,W,J,de){let le=oe,ke=fe-2+h+W,ie=ue-1+h+J,Ie=K-2+h+W,Oe=ue-1+h+J;de.save(),be.setLineDash(de,le,"h",ke,ie,Ie,Oe),de.strokeStyle=se,de.stroke(),de.closePath(),de.restore()},_=function(oe,se,ue,fe,ce,K,W,J,de,le){let ke=oe,ie,Ie,Oe,xe;le=="rt_lb"?(ie=K-2+h+W,Ie=ue-1+h+J,Oe=fe-2+h+W,xe=ce-2+h+J):le=="lt_rb"&&(ie=fe-2+h+W,Ie=ue-1+h+J,Oe=K-2+h+W,xe=ce-2+h+J),de.save(),be.setLineDash(de,ke,"d",ie,Ie,Oe,xe),de.strokeStyle=se,de.stroke(),de.closePath(),de.restore()},ee=function(oe,se,ue,fe,ce,K){let W={sr:ue,sc:fe,er:ce,ec:K},J=$("#luckysheet-cell-main").scrollLeft(),de=$("#luckysheet-cell-main").scrollTop(),le=d.rowHeaderWidth,ke=d.columnHeaderHeight,ie=d.flowdata[oe]&&d.flowdata[oe][se];if(ie==null)return W;if(N(ie)=="object"&&"mc"in ie){let Ie=ie.mc,Oe=Ie.c==0?0:d.visibledatacolumn[Ie.c-1]-J,xe=Ie.r==0?-de-1:d.visibledatarow[Ie.r-1]-de-1,Be=d.visibledatacolumn[Ie.c+Ie.cs-1]-J,We=d.visibledatarow[Ie.r+Ie.rs-1]-de;return{sr:xe,sc:Oe,er:We,ec:Be}}else return W},re=io(b,k,w,C);for(let oe in re){let se=oe.substr(0,oe.indexOf("_")),ue=oe.substr(oe.indexOf("_")+1);if(B[se+"_"+ue]){let fe=B[se+"_"+ue].start_r,ce=B[se+"_"+ue].start_c,K=B[se+"_"+ue].end_r,W=B[se+"_"+ue].end_c,J=hu(P,se,ue,w,C),de=re[oe].l;de!=null&&(!J.colIn||J.stc==ue)&&O(de.style,de.color,fe,ce,K,W,a,o,y);let le=re[oe].r;le!=null&&(!J.colIn||J.colLast)&&q(le.style,le.color,fe,ce,K,W,a,o,y);let ke=re[oe].t;ke!=null&&G(ke.style,ke.color,fe,ce,K,W,a,o,y);let ie=re[oe].b;ie!=null&&j(ie.style,ie.color,fe,ce,K,W,a,o,y);let Ie=re[oe].rt_lb;if(Ie!=null){let{sr:xe,sc:Be,er:We,ec:ze}=ee(se,ue,fe,ce,K,W);_(Ie.style,Ie.color,xe,Be,We,ze,a,o,y,"rt_lb")}let Oe=re[oe].lt_rb;if(Oe!=null){let{sr:xe,sc:Be,er:We,ec:ze}=ee(se,ue,fe,ce,K,W);_(Oe.style,Oe.color,xe,Be,We,ze,a,o,y,"lt_rb")}}}}C==d.visibledatacolumn.length-1&&y.clearRect(R-e+a-1,o-1,d.ch_width-d.visibledatacolumn[C],x-n),y.restore(),d.measureTextCacheTimeOut=setTimeout(()=>{d.measureTextCache={},d.measureTextCellInfoCache={},d.cellOverflowMapCache={}},100)}function Ny(e,n,t,l,a){let o={},s=d.flowdata;for(let u=l;u<=a;u++){if(s[u]==null)continue;if(d.cellOverflowMapCache[u]!=null){o[u]=d.cellOverflowMapCache[u];continue}let f=!1;for(let A=0;A=n)&&ks[e].length-1)return{success:!1,r:e,c:t};let u=s[e][t];if(u!=null&&(!me(u.v)||u.mc!=null))return{success:!1,r:e,c:t};let f=n-1<0?0:d.visibledatacolumn[n-1],A=d.visibledatacolumn[n],h=o-(A-f);a=="0"?(f-=h/2,A+=h/2):a=="1"?A+=h:a=="2"&&(f-=h);let g=t-1<0?0:d.visibledatacolumn[t-1],p=d.visibledatacolumn[t];if(l=="forward")return fp?mi(e,n,t+1,l,a,o):A>g?{success:!0,r:e,c:t}:{success:!1,r:e,c:t}}function hu(e,n,t,l,a){let o=!1,s=!1,u,f,A,h;for(let g in e){for(let p in e[g]){u=g,f=p;let v=e[g][p];if(A=v.stc,h=v.edc,u==n&&t>=A&&t<=h&&(o=!0,t==h||t==a)){s=!0;break}}if(s)break}return{colIn:o,colLast:s,rowIndex:u,colIndex:f,stc:A,edc:h}}function Eh(e,n,t){if(e==null)return;let l=e.values,a=t.pos_x,o=t.pos_y;if(l!=null){e.rotate!=0&&e.type!="verticalWrap"&&(n.save(),n.translate((a+e.textLeftAll)/d.zoomRatio,(o+e.textTopAll)/d.zoomRatio),n.rotate(-e.rotate*Math.PI/180),n.translate(-(e.textLeftAll+a)/d.zoomRatio,-(o+e.textTopAll)/d.zoomRatio));for(let s=0;s{vn();Ar();Pa();fu();ll();mr();Wt();Br();is();Za();Pt();wr();Qt();La();wt();dt();Qr();Xl();je();yt();cl();ca();Dh=function(e,n,t,l,a,o){if(d.flowdata[e]==null||d.flowdata[e][n]==null)return;let s=d.flowdata[e][n].spl;if(s!=null){if(typeof s=="string"&&(s=new Function("return "+s)()),N(s)=="object"){let u=s,f=u.offsetX,A=u.offsetY;f=f==null?0:f,A=A==null?0:A,nr.render(u.shapeseq,u.shapes,t+f,l+A,u.pixelWidth,u.pixelHeight,a,o)}else if(N(s)=="array"&&N(s[0])=="object")for(let u=0;uue?D:ue,K=re+ce-B;u.textBaseline="bottom";let W=K-13*d.zoomRatio;E=="0"?(K=re+ce/2,u.textBaseline="middle",W=K-6*d.zoomRatio):E=="1"&&(K=re+B,u.textBaseline="top",W=K+1*d.zoomRatio),fe=fe/d.zoomRatio,K=K/d.zoomRatio,W=W/d.zoomRatio,u.lineWidth=1,u.strokeStyle="#000",u.strokeRect(fe,W,10,10),j[e+"_"+n].checked&&(u.beginPath(),u.lineTo(fe+1,W+6),u.lineTo(fe+4,W+9),u.lineTo(fe+9,W+2),u.stroke(),u.closePath()),u.fillStyle=be.checkstatus(d.flowdata,e,n,"fc"),u.fillText(s==null?"":s,fe+14,K),u.restore()}else{if(P!=null&&P.dataBar!=null&&P.dataBar.valueLen&&P.dataBar.valueLen.toString()!=="NaN"){let se=l+h+S,ue=t+g+B,fe=R-S*2,ce=D-B*2,K=P.dataBar.valueType,W=P.dataBar.valueLen,J=P.dataBar.format;if(K=="minus"){let de=P.dataBar.minusLen;if(J.length>1){let le=u.createLinearGradient(se+fe*de*(1-W),ue,se+fe*de,ue);le.addColorStop(0,"#ffffff"),le.addColorStop(1,"#ff0000"),u.fillStyle=le}else u.fillStyle="#ff0000";u.fillRect(se+fe*de*(1-W),ue,fe*de*W,ce),u.beginPath(),u.moveTo(se+fe*de*(1-W),ue),u.lineTo(se+fe*de*(1-W),ue+ce),u.lineTo(se+fe*de,ue+ce),u.lineTo(se+fe*de,ue),u.lineTo(se+fe*de*(1-W),ue),u.lineWidth=1,u.strokeStyle="#ff0000",u.stroke(),u.closePath()}else if(K=="plus"){let de=P.dataBar.plusLen;if(de==1){if(J.length>1){let le=u.createLinearGradient(se,ue,se+fe*W,ue);le.addColorStop(0,J[0]),le.addColorStop(1,J[1]),u.fillStyle=le}else u.fillStyle=J[0];u.fillRect(se,ue,fe*W,ce),u.beginPath(),u.moveTo(se,ue),u.lineTo(se,ue+ce),u.lineTo(se+fe*W,ue+ce),u.lineTo(se+fe*W,ue),u.lineTo(se,ue),u.lineWidth=1,u.strokeStyle=J[0],u.stroke(),u.closePath()}else{let le=P.dataBar.minusLen;if(J.length>1){let ke=u.createLinearGradient(se+fe*le,ue,se+fe*le+fe*de*W,ue);ke.addColorStop(0,J[0]),ke.addColorStop(1,J[1]),u.fillStyle=ke}else u.fillStyle=J[0];u.fillRect(se+fe*le,ue,fe*de*W,ce),u.beginPath(),u.moveTo(se+fe*le,ue),u.lineTo(se+fe*le,ue+ce),u.lineTo(se+fe*le+fe*de*W,ue+ce),u.lineTo(se+fe*le+fe*de*W,ue),u.lineTo(se+fe*le,ue),u.lineWidth=1,u.strokeStyle=J[0],u.stroke(),u.closePath()}}}let ee=l+h,re=t+g+1;u.save(),u.beginPath(),u.rect(ee,re,R,D),u.clip(),u.scale(d.zoomRatio,d.zoomRatio);let oe=Pr(T,u,{cellWidth:R,cellHeight:D,space_width:S,space_height:B,r:e,c:n});if(P!=null&&P.icons!=null&&oe.type=="plain"){let se=P.icons.left,ue=P.icons.top,fe=oe.values[0],ce=ee+fe.left,K=re+fe.top-oe.textHeightAll;E=="0"?K=re+D/2-oe.textHeightAll/2:E=="1"?K=re:E=="2"&&(K=K-oe.desc),K=K/d.zoomRatio,ce=ce/d.zoomRatio,u.drawImage(Ka,se*42,ue*32,32,32,ee/d.zoomRatio,K,oe.textHeightAll/d.zoomRatio,oe.textHeightAll/d.zoomRatio),F!="0"&&F!="2"&&(ce=ce+oe.textHeightAll/d.zoomRatio)}u.fillStyle=be.checkstatus(d.flowdata,e,n,"fc"),z!=null&&z[0]!=null&&(u.fillStyle=z[0]),P!=null&&P.textColor!=null&&(u.fillStyle=P.textColor),T.ct&&T.ct.fa&&T.ct.fa.indexOf("[Red]")>-1&&T.ct.t=="n"&&T.v<0&&(u.fillStyle="#ff0000"),Eh(oe,u,{pos_x:ee,pos_y:re}),u.restore()}G&&!d.luckysheetcurrentisPivotTable&&d.showGridLines&&x&&(u.beginPath(),u.moveTo(o+h-2+C,t+g),u.lineTo(o+h-2+C,a+g),u.lineWidth=1,u.strokeStyle=Dl.strokeStyle,u.stroke(),u.closePath()),!d.luckysheetcurrentisPivotTable&&d.showGridLines&&x&&(u.beginPath(),u.moveTo(l+h-1,a+g-2+C),u.lineTo(o+h-1,a+g-2+C),u.lineWidth=1,u.strokeStyle=Dl.strokeStyle,u.stroke(),u.closePath()),$e.createHookFunction("cellRenderAfter",d.flowdata[e][n],{r:e,c:n,start_r:q[1],start_c:q[0],end_r:q[3]+q[1],end_c:q[2]+q[0]},ve.getSheetByIndex(),u)},Mh=function(e,n,t,l,a,o,s,u,f,A,h){let g;e==0?g=-o-1:g=d.visibledatarow[e-1]-o-1;let p=d.visibledatarow[e]-o,v;t==0?v=-s:v=d.visibledatacolumn[t-1]-s;let y=d.visibledatacolumn[l]-s,b=d.flowdata[e][n],k=y-v-2,w=p-g-2,C=2,I=2,x=v+u,T=g+f+1,R=Ra(b);a.font=R,a.save(),a.beginPath(),a.rect(x,T,k,w),a.clip(),a.scale(d.zoomRatio,d.zoomRatio);let D=Pr(b,a,{cellWidth:k,cellHeight:w,space_width:C,space_height:I,r:e,c:n}),S=Jt.checksAF(e,n,A),B=Re.checksCF(e,n,h);a.fillStyle=be.checkstatus(d.flowdata,e,n,"fc"),S!=null&&S[0]!=null&&(a.fillStyle=S[0]),B!=null&&B.textColor!=null&&(a.fillStyle=B.textColor),Eh(D,a,{pos_x:x,pos_y:T}),a.restore()}});var Fy,Sl,gi=De(()=>{dt();Qt();_t();El();jt();rl();tl();Wt();el();$t();cl();Kl();ll();wt();yt();je();Fy={item:{linkType:"external",linkAddress:"",linkTooltip:""},hyperlink:null,createDialog:function(){let e=this,n=te(),t=n.insertLink,l=n.toolbar,a=n.button;$("#luckysheet-modal-dialog-mask").show(),$("#luckysheet-insertLink-dialog").remove();let o="";d.luckysheetfile.forEach(y=>{o+=``});let s=`
-
- - -
-
- - -
-
-
- - -
-
-
-
- - -
-
- - -
-
-
- - -
-
`;$("body").append(we(At,{id:"luckysheet-insertLink-dialog",addclass:"luckysheet-insertLink-dialog",title:l.insertLink,content:s,botton:` - `,style:"z-index:100003"}));let u=$("#luckysheet-insertLink-dialog").find(".luckysheet-modal-dialog-content").css("min-width",350).end(),f=u.outerHeight(),A=u.outerWidth(),h=$(window).width(),g=$(window).height(),p=$(document).scrollLeft(),v=$(document).scrollTop();$("#luckysheet-insertLink-dialog").css({left:(h+p-A)/2,top:(g+v-f)/3}).show(),e.dataAllocation()},init:function(){let e=this,t=te().insertLink;$(document).off("change.linkType").on("change.linkType","#luckysheet-insertLink-dialog-linkType",function(l){let a=this.value;$("#luckysheet-insertLink-dialog .show-box").hide(),$("#luckysheet-insertLink-dialog .show-box-"+a).show()}),$(document).off("click.confirm").on("click.confirm","#luckysheet-insertLink-dialog-confirm",function(l){let a=d.luckysheet_select_save[d.luckysheet_select_save.length-1],o=a.row_focus||a.row[0],s=a.column_focus||a.column[0],u=$("#luckysheet-insertLink-dialog-linkText").val(),f=$("#luckysheet-insertLink-dialog-linkType").val(),A=$("#luckysheet-insertLink-dialog-linkAddress").val(),h=$("#luckysheet-insertLink-dialog-linkSheet").val(),g=$("#luckysheet-insertLink-dialog-linkCell").val(),p=$("#luckysheet-insertLink-dialog-linkTooltip").val();if(f=="external"){if(/^http[s]?:\/\//.test(A)||(A="https://"+A),!/^http[s]?:\/\/([\w\-\.]+)+[\w-]*([\w\-\.\/\?%&=]+)?$/ig.test(A)){U.info('',t.tooltipInfo1);return}}else{if(!m.iscelldata(g)){U.info('',t.tooltipInfo2);return}A=h+"!"+g}(u==null||u.replace(/\s/g,"")=="")&&(u=A);let v={linkType:f,linkAddress:A,linkTooltip:p},y=$.extend(!0,{},e.hyperlink),b=$.extend(!0,{},e.hyperlink);b[o+"_"+s]=v;let k=Te.deepCopyFlowData(d.flowdata),w=k[o][s];w==null&&(w={}),w.fc="rgb(0, 0, 255)",w.un=1,w.v=w.m=u,k[o][s]=w,e.ref(y,b,d.currentSheetIndex,k,[{row:[o,o],column:[s,s]}]),$("#luckysheet-modal-dialog-mask").hide(),$("#luckysheet-insertLink-dialog").hide()})},dataAllocation:function(){let e=this,n=d.luckysheet_select_save[d.luckysheet_select_save.length-1],t=n.row_focus||n.row[0],l=n.column_focus||n.column[0],o=(e.hyperlink||{})[t+"_"+l]||{},s=Fe(t,l,null,"m");$("#luckysheet-insertLink-dialog-linkText").val(s);let u=o.linkType||"external";$("#luckysheet-insertLink-dialog-linkType").val(u),$("#luckysheet-insertLink-dialog .show-box").hide(),$("#luckysheet-insertLink-dialog .show-box-"+u).show();let f=o.linkAddress||"";if(u=="external")$("#luckysheet-insertLink-dialog-linkAddress").val(f);else if(m.iscelldata(f)){let h=f.split("!")[0],g=f.split("!")[1];$("#luckysheet-insertLink-dialog-linkSheet").val(h),$("#luckysheet-insertLink-dialog-linkCell").val(g)}let A=o.linkTooltip||"";$("#luckysheet-insertLink-dialog-linkTooltip").val(A)},cellFocus:function(e,n){let t=this;if(t.hyperlink==null||t.hyperlink[e+"_"+n]==null)return;let l=t.hyperlink[e+"_"+n];if(l.linkType=="external")window.open(l.linkAddress);else{let a=m.getcellrange(l.linkAddress),o=a.sheetIndex,s=[{row:a.row,column:a.column}];o!=d.currentSheetIndex&&($("#luckysheet-sheet-area div.luckysheet-sheets-item").removeClass("luckysheet-sheets-item-active"),$("#luckysheet-sheets-item"+o).addClass("luckysheet-sheets-item-active"),ve.changeSheet(o)),d.luckysheet_select_save=s,at(!0);let u=a.row[0]-1==-1?0:d.visibledatarow[a.row[0]-1],f=a.column[0]-1==-1?0:d.visibledatacolumn[a.column[0]-1];$("#luckysheet-scrollbar-x").scrollLeft(f),$("#luckysheet-scrollbar-y").scrollTop(u)}},overshow:function(e){let n=this;if($("#luckysheet-hyperlink-overshow").remove(),$(e.target).closest("#luckysheet-cell-main").length==0)return;let t=Ke(e.pageX,e.pageY),l=$("#luckysheet-cell-main").scrollLeft(),a=$("#luckysheet-cell-main").scrollTop(),o=t[0]+l,s=t[1]+a;if(Y.freezenverticaldata!=null&&t[0] -
${g}
-
\u5355\u51FB\u9F20\u6807\u53EF\u4EE5\u8FFD\u8E2A
- `;$(k).appendTo($("#luckysheet-cell-main"))},ref:function(e,n,t,l,a){let o=this;if(d.clearjfundo){d.jfundo.length=0;let s={};s.type="updateHyperlink",s.sheetIndex=t,s.historyHyperlink=e,s.currentHyperlink=n,s.data=d.flowdata,s.curData=l,s.range=a,d.jfredo.push(s)}o.hyperlink=n,d.luckysheetfile[Q(t)].hyperlink=n,d.flowdata=l,Te.webWorkerFlowDataCache(d.flowdata),d.luckysheetfile[Q(t)].data=d.flowdata,ae.allowUpdate&&(ae.saveParam("all",t,n,{k:"hyperlink"}),ae.historyParam(d.flowdata,t,a[0])),setTimeout(function(){Ue()},1)}},Sl=Fy});function Ha(e){e==null&&(e="chart");for(var n=window.navigator.userAgent.replace(/[^a-zA-Z0-9]/g,"").split(""),t="",l=0;l<12;l++)t+=n[Math.round(Math.random()*(n.length-1))];var a=new Date().getTime();return e+"_"+t+"_"+a}function Ul(e,n){return e.replace(/\$\{([\w]+)\}/g,function(t,l){var a=n[l];return typeof a!="undefined"?a:t})}function It(e){return typeof e=="number"||typeof e=="string"&&!e.endsWith("px")?e+"px":e}function ho(e){return typeof e=="number"?e:typeof e=="string"&&!e.endsWith("px")?parseFloat(e):typeof e=="string"&&e.endsWith("px")?parseFloat(e.substring(0,e.length-2)):e}function ja(e,n,t,l,a,o){console.log("\u56FE\u8868\u4FE1\u606F\u66F4\u65B0",n),d.luckysheetfile[Q(e)].chart.forEach(u=>{u.chart_id===n&&(t&&(u.left=t),l&&(u.top=l),a&&(u.width=a),o&&(u.height=o))})}var wn=De(()=>{wt();je()});var Cr,On,Ly,M,mo=De(()=>{dt();Pt();Fl();jt();Cr=Ol(En()),On={v:"#VALUE!",n:"#NAME?",na:"#N/A",r:"#REF!",d:"#DIV/0!",nm:"#NUM!",nl:"#NULL!",sp:"#SPILL!"},Ly={getCellDataArr:function(e,n,t){let l=[];if(e.data==null)t||(n==="number"?l.push(0):n==="text"&&l.push(""));else if(N(e.data)==="array")for(let a=0;a1){let t=e[0].length;for(let l=1;lh)return On.v;l=e.data[0][o-A]}else if(e.coll==1){if(af)return On.v;l=e.data[a-u][0]}l==null||me(l.v)||l.mc!=null?n=="number"?l=0:n=="text"&&(l=""):l=l.v}else return On.v}else l=e;return l},getCellBoolen:function(e){let t=this.getFirstValue(e);if(H(t))return t;if(N(t)!="boolean")if(N(t)=="string"&&(t.toLowerCase()=="true"||t.toLowerCase()=="false"))t.toLowerCase()=="true"?t=!0:t.toLowerCase()=="false"&&(t=!1);else if(V(t))t=parseFloat(t),t=t!=0;else return On.v;return t},getCellDate:function(e){let n=this,t;if(N(e)=="array")if(N(e[0])=="array"){if(!n.isDyadicArr(e))return On.v;t=e[0][0]}else t=e[0];else if(N(e)=="object"&&e.startCell!=null){if(e.data==null||N(e.data)=="array"||me(e.data.v))return On.v;t=e.data.v,e.data.ct!=null&&e.data.ct.t=="d"&&(t=ht("YYYY-MM-DD h:mm:ss",t))}else t=e;return t},getCellrangeDate:function(e){let n=this,t=[];if(N(e)=="array"){if(N(e[0])=="array"&&!n.isDyadicArr(e))return On.v;t=t.concat(n.getDataArr(e,!1))}else if(N(e)=="object"&&e.startCell!=null)if(e.data==null)t.push(0);else if(N(e.data)=="array")for(let l=0;l=(0,Cr.default)(a))return!0;let o=(0,Cr.default)(n).year(),s=(0,Cr.default)().set({year:o,month:2,date:1});return t.isLeapYear(o)&&(0,Cr.default)(n)>=(0,Cr.default)(s)&&(0,Cr.default)(e)<(0,Cr.default)(s)},findResultIndex:function(e,n){let t={};for(let o=1;ol&&(l=n[i].length);for(let o=1;o{Bh={},Oy=function(n){var t=function(){};t.precision=1e-6,t.create=function(a){var o=new t;return o.setElements(a)};var l=t.create;t.I=function(a){for(var o=[],s=a,u;s--;)for(u=a,o[s]=[];u--;)o[s][u]=s===u?1:0;return t.create(o)},t.prototype={e:function(a,o){return a<1||a>this.elements.length||o<1||o>this.elements[0].length?null:this.elements[a-1][o-1]},dimensions:function(){var a=this.elements.length===0?0:this.elements[0].length;return{rows:this.elements.length,cols:a}},rows:function(){return this.elements.length},cols:function(){return this.elements.length===0?0:this.elements[0].length},eql:function(a){var o=a.elements||a;if((!o[0]||typeof o[0][0]=="undefined")&&(o=t.create(o).elements),this.elements.length===0||o.length===0)return this.elements.length===o.length;if(this.elements.length!==o.length||this.elements[0].length!==o[0].length)return!1;for(var s=this.elements.length,u=this.elements[0].length,f;s--;)for(f=u;f--;)if(Math.abs(this.elements[s][f]-o[s][f])>t.precision)return!1;return!0},dup:function(){return t.create(this.elements)},map:function(a,o){if(this.elements.length===0)return t.create([]);for(var s=[],u=this.elements.length,f=this.elements[0].length,A;u--;)for(A=f,s[u]=[];A--;)s[u][A]=a.call(o,this.elements[u][A],u+1,A+1);return t.create(s)},isSameSizeAs:function(a){var o=a.elements||a;return typeof o[0][0]=="undefined"&&(o=t.create(o).elements),this.elements.length===0?o.length===0:this.elements.length===o.length&&this.elements[0].length===o[0].length},add:function(a){if(this.elements.length===0)return this.map(function(s){return s});var o=a.elements||a;return typeof o[0][0]=="undefined"&&(o=t.create(o).elements),this.isSameSizeAs(o)?this.map(function(s,u,f){return s+o[u-1][f-1]}):null},subtract:function(a){if(this.elements.length===0)return this.map(function(s){return s});var o=a.elements||a;return typeof o[0][0]=="undefined"&&(o=t.create(o).elements),this.isSameSizeAs(o)?this.map(function(s,u,f){return s-o[u-1][f-1]}):null},canMultiplyFromLeft:function(a){if(this.elements.length===0)return!1;var o=a.elements||a;return typeof o[0][0]=="undefined"&&(o=t.create(o).elements),this.elements[0].length===o.length},multiply:function(a){if(this.elements.length===0)return null;if(!a.elements)return this.map(function(y){return y*a});var o=!!a.modulus,s=a.elements||a;if(typeof s[0][0]=="undefined"&&(s=t.create(s).elements),!this.canMultiplyFromLeft(s))return null;for(var u=this.elements.length,f=s[0].length,A,h=this.elements[0].length,g,p=[],v;u--;)for(A=f,p[u]=[];A--;){for(g=h,v=0;g--;)v+=this.elements[u][g]*s[g][A];p[u][A]=v}var s=t.create(p);return o?s.col(1):s},minor:function(a,o,s,u){if(this.elements.length===0)return null;for(var f=[],A=s,h,g,p,v=this.elements.length,y=this.elements[0].length;A--;)for(h=s-A-1,f[h]=[],g=u;g--;)p=u-g-1,f[h][p]=this.elements[(a+h-1)%v][(o+p-1)%y];return t.create(f)},transpose:function(){if(this.elements.length===0)return t.create([]);for(var a=this.elements.length,o,s=this.elements[0].length,u,f=[],o=s;o--;)for(u=a,f[o]=[];u--;)f[o][u]=this.elements[u][o];return t.create(f)},isSquare:function(){var a=this.elements.length===0?0:this.elements[0].length;return this.elements.length===a},max:function(){if(this.elements.length===0)return null;for(var a=0,o=this.elements.length,s=this.elements[0].length,u;o--;)for(u=s;u--;)Math.abs(this.elements[o][u])>Math.abs(a)&&(a=this.elements[o][u]);return a},indexOf:function(a){if(this.elements.length===0)return null;var o=null,s=this.elements.length,u,f=this.elements[0].length,A;for(u=0;ut.precision){o++;break}return o},augment:function(a){if(this.elements.length===0)return this.dup();var o=a.elements||a;typeof o[0][0]=="undefined"&&(o=t.create(o).elements);var s=this.dup(),u=s.elements[0].length,f=s.elements.length,A=o[0].length,h;if(f!==o.length)return null;for(;f--;)for(h=A;h--;)s.elements[f][u+h]=o[f][h];return s},inverse:function(){if(this.elements.length===0||!this.isSquare()||this.isSingular())return null;for(var a=this.elements.length,o=a,s,u=this.augment(t.I(a)).toRightTriangular(),f=u.elements[0].length,A,h,g,p=[],v;o--;){for(h=[],p[o]=[],g=u.elements[o][o],A=0;A=a&&p[o].push(v);for(u.elements[o]=h,s=o;s--;){for(h=[],A=0;A"&&(e="!="),e=="="&&(e="=="),t==null&&a==null)return"#INVERSE!";if(t=="#INVERSE!")t=0,e=="-"?e="+":e=="+"&&(e="-");else if(e=="-"&&t==null)t=0;else if(e=="/"&&(a==0||a==null))return Ne.d;function o(u,f,A){if(V(u)&&(u=parseFloat(u)),V(A)&&(A=parseFloat(A)),f=="==")return u==A;if(f=="!=")return u!=A;if(f==">=")return u>=A;if(f=="<=")return u<=A;if(f==">")return u>A;if(f=="<")return u="||e=="<="||e==">"||e=="<")if(N(t)=="array"&&N(a)=="array"){let u=[];if(N(t[0])=="array"&&N(a[0])=="array"){if(t.length!=a.length&&t[0].length!=a[0].length)return Ne.na;for(let f=0;f-1){n=e.split(";");for(let t=0;t1){a=l[0].replace(/''/g,"'"),o=l[1],a.substr(0,1)=="'"&&a.substr(a.length-1,1)=="'"&&(a=a.substring(1,a.length-1));for(let f in t)if(a==t[f].name){s=t[f].index,u=t[f].data;break}s==-1&&(s=0)}else{let f=Q(d.calculateSheetIndex);a=t[f].name,s=t[f].index,u=t[f].data,o=l[0]}if(o.indexOf(":")==-1){let f=parseInt(o.replace(/[^0-9]/g,""))-1,A=hl(o.replace(/[^A-Za-z]/g,""));if(!isNaN(f)&&!isNaN(A)){let h=$i(u,{row:[f,f],column:[A,A]})[0][0];if(m.execFunctionGlobalData!=null){let y=m.execFunctionGlobalData[f+"_"+A+"_"+s];y!=null&&(h=y)}let v={sheetName:a,startCell:o,rowl:1,coll:1,data:h};return window.luckysheet_getcelldata_cache[e]=v,v}else return[]}else{o=o.split(":");let f=[],A=[];if(f[0]=parseInt(o[0].replace(/[^0-9]/g,""))-1,f[1]=parseInt(o[1].replace(/[^0-9]/g,""))-1,isNaN(f[0])&&(f[0]=0),isNaN(f[1])&&(f[1]=u.length-1),f[0]>f[1])return U.info("\u9009\u62E9\u5931\u8D25","\u8F93\u5165\u8303\u56F4\u9519\u8BEF\uFF01"),[];if(A[0]=hl(o[0].replace(/[^A-Za-z]/g,"")),A[1]=hl(o[1].replace(/[^A-Za-z]/g,"")),isNaN(A[0])&&(A[0]=0),isNaN(A[1])&&(A[1]=u[0].length-1),A[0]>A[1])return U.info("\u9009\u62E9\u5931\u8D25","\u8F93\u5165\u8303\u56F4\u9519\u8BEF\uFF01"),[];let h=$i(u,{row:f,column:A});if(m.execFunctionGlobalData!=null)for(let y=f[0];y<=f[1];y++)for(let b=A[0];b<=A[1];b++){let k=m.execFunctionGlobalData[y+"_"+b+"_"+s];k!=null&&(h[y-f[0]][b-A[0]]=k)}let g=f[1]-f[0]+1,p=A[1]-A[0]+1,v={sheetName:a,startCell:o[0],rowl:g,coll:p,data:h};return window.luckysheet_getcelldata_cache[e]=v,v}}function vi(e){if(typeof e=="object")return e==null?"":Array.isArray(e)?it(e[0])[2]:Array.isArray(e.data)?Ne.v:e.data.v===void 0?"":e.data.v;if(m.isCompareOperator(e).flag){if(typeof e=="string"||typeof e=="number")return e}else return it(e)[2];return Ne.v}function Aa(){let e=arguments[0];for(let n=0;n=4){if(l=M.getFirstValue(arguments[3]),H(l))return l;if(!V(l))return m.error.v;l=parseInt(l)}var a=arguments[0].coll;if(arguments.length==5){if(a=M.getFirstValue(arguments[4]),H(a))return a;if(!V(a))return m.error.v;a=parseInt(a)}if(l<1||a<1)return m.error.r;var o=m.getcellrange(e),s=o.row[0],u=o.column[0];s+=n,u+=t;var f=s+l-1,A=u+a-1;return s<0||f>=d.flowdata.length||u<0||A>=d.flowdata[0].length?m.error.r:mt(d.calculateSheetIndex,{row:[s,f],column:[u,A]})}function bu(e,n,t){let l,a;if(e?(a=n,l=t):(l=n,a=t),l.startCell.indexOf(":")>-1||a.indexOf(":")>-1)return Ne.v;if(e)return gr(a+":"+l.startCell);{let o=a,s="";if(a.indexOf("!")>-1){let u=a.split("!");s=u[0]+"!",o=u[1]}return gr(s+l.startCell+":"+o)}}var go,po=De(()=>{mo();jt();rl();Pt();Qt();Fl();mu();wt();dt();je();go=Ol(Ma())});function Nh(e,n,t,l){ga(l||qy),ma(t||Vy,null,function(){let a=new Vuex.Store;console.groupCollapsed("chartmix \u63D2\u4EF6\u52A0\u8F7D\u5B8C\u6210"),console.info(chartmix.default),console.groupEnd(),Vue.use(chartmix.default,{store:a});let o=document.getElementsByTagName("body")[0];chartmix.default.initChart(o,d.lang==="zh"?"ch":d.lang),$(".chartSetting").css({top:"1px",bottom:"1px",position:"absolute",right:"0px",width:"350px",background:"#fff",border:"1px solid #E5E5E5","z-index":1004,"box-shadow":"0px 2px 4px rgba(0,0,0,0.2)","-webkit-box-shadow":"0px 2px 4px rgba(0,0,0,0.2)","-moz-box-shadow":"0px 2px 4px rgba(0,0,0,0.2)","-moz-user-select":"none","-khtml-user-select":"none","-webkit-user-select":"none","-ms-user-select":"none","user-select":"none","padding-left":"30px",display:"none"}),d.createChart=chartmix.default.createChart,d.highlightChart=chartmix.default.highlightChart,d.deleteChart=chartmix.default.deleteChart,d.resizeChart=chartmix.default.resizeChart,d.changeChartRange=chartmix.default.changeChartRange,d.changeChartCellData=chartmix.default.changeChartCellData,d.getChartJson=chartmix.default.getChartJson,d.chart_selection=ku(),d.chartparam.jfrefreshchartall=vo,d.chartparam.changeChartCellData=chartmix.default.changeChartCellData,d.chartparam.renderChart=chartmix.default.renderChart,d.chartparam.getChartJson=chartmix.default.getChartJson,d.chartparam.insertToStore=chartmix.default.insertToStore;for(let s=0;sl.chartType==="chartmix");for(let l=0;l
${content}
',f=$(Ul(u,{id:s,addclass:"luckysheet-data-visualization-chart",title:"\u56FE\u8868\u751F\u6210",content:""})).appendTo($(".luckysheet-cell-main"));ha(f),$(`#${s}`).children(".luckysheet-modal-dialog-content")[0].id=o;let A=document.getElementById(s),h;h=d.chartparam.getChartJson(a.chart_id),d.chartparam.renderChart({chart_id:a.chart_id,chartOptions:h}),d.currentChart=h,Tr(o),$(`#${o}_c .luckysheet-modal-controll-del`).click(function(b){yo(o)}),$(`#${o}_c .luckysheet-modal-controll-update`).click(function(b){bo()}),f.children(".luckysheet-modal-dialog-content").mousedown(function(b){d.chartparam.luckysheetCurrentChartMaxState||Tr(o),b.stopPropagation()}),f.mousedown(function(b){if(!d.chartparam.luckysheetCurrentChartMaxState){Tr(o),Rl(!0),!$(b.target).is(".luckysheet-modal-dialog-controll")&&!$(b.target).is(".luckysheet-modal-controll-btn")&&!$(b.target).is("i")&&(d.chartparam.luckysheetCurrentChartMoveTimeout=setTimeout(function(){d.chartparam.luckysheetCurrentChartMove=!0},100));var k=d.chartparam.luckysheetCurrentChartMoveObj.offset(),w=d.chartparam.luckysheetCurrentChartMoveObj.position();d.chartparam.luckysheetCurrentChartMoveXy=[b.pageX-k.left,b.pageY-k.top,w.left,w.top,$("#luckysheet-scrollbar-x").scrollLeft(),$("#luckysheet-scrollbar-y").scrollTop()],d.chartparam.luckysheetCurrentChartMoveWinH=$("#luckysheet-cell-main")[0].scrollHeight,d.chartparam.luckysheetCurrentChartMoveWinW=$("#luckysheet-cell-main")[0].scrollWidth,!$(b.target).hasClass("luckysheet-mousedown-cancel")&&$(b.target).filter("[class*='sp-palette']").length==0&&$(b.target).filter("[class*='sp-thumb']").length==0&&$(b.target).filter("[class*='sp-']").length==0&&($("#luckysheet-rightclick-menu").hide(),$("#luckysheet-cols-h-hover").hide(),$("#luckysheet-cols-menu-btn").hide(),$("#luckysheet-rightclick-menu").hide(),$("#luckysheet-sheet-list, #luckysheet-rightclick-sheet-menu, #luckysheet-user-menu").hide(),$("body > .luckysheet-filter-menu, body > .luckysheet-filter-submenu, body > .luckysheet-cols-menu").hide()),console.log(" move chart",k,w),b.stopPropagation()}}).find(".luckysheet-modal-dialog-resize-item").mousedown(function(b){if(d.chartparam.luckysheetCurrentChartActive){d.chartparam.luckysheetCurrentChartResize=$(this).data("type");var k=Ke(b.pageX,b.pageY),w=$("#luckysheet-scrollbar-x").scrollLeft(),C=$("#luckysheet-scrollbar-y").scrollTop(),I=k[0]+w,x=k[1]+C,T=d.chartparam.luckysheetCurrentChartResizeObj.position();d.chartparam.luckysheetCurrentChartResizeXy=[I,x,f.width(),f.height(),T.left+w,T.top+C,w,C],d.chartparam.luckysheetCurrentChartResizeWinH=$("#luckysheet-cell-main")[0].scrollHeight,d.chartparam.luckysheetCurrentChartResizeWinW=$("#luckysheet-cell-main")[0].scrollWidth,d.chartparam.luckysheetCurrentChart=o,b.stopPropagation()}});let g=It(a.width),p=It(a.height),v=It(a.left),y=It(a.top);A.style.width=g,A.style.height=p,A.style.position="absolute",A.style.background="#fff",A.style.left=v,A.style.top=y,A.style.zIndex=d.zIndex?d.zIndex:15,d.zIndex++}}function vo(e,n,t,l,a,o){if(!chartmix.default)return;let s=d.currentChart;if(!!s&&s.rangeArray.length==1){var u=s.rangeArray[0].row,f=s.rangeArray[0].column;if(n>u[1]||tf[1]||a \u56FE\u8868\u534F\u540C : chartmix \u6570\u636E\u66F4\u65B0"),o)return;let v=d.luckysheetfile[Q(d.currentSheetIndex)];ae.saveParam("c",v.index,{r_st:n,r_ed:t,c_st:l,c_ed:a,chart_id:s.chart_id,chartOptions:s},{op:"update_data"})}}}function ku(){return{create:function(){var e=d.currentChart;if(e.rangeArray.length>1)return;$("#luckysheet-chart-rangeShow").empty(),$("#luckysheet-cell-selected-boxs").hide(),$("#luckysheet-cell-selected-focus").hide(),$("#luckysheet-rows-h-selected").empty(),$("#luckysheet-cols-h-selected").empty(),$("#luckysheet-row-count-show").hide(),$("#luckysheet-column-count-show").hide();var n=e.rangeArray[0].row[0],t=e.rangeArray[0].column[0],l=e.rangeSplitArray,a=e.rangeRowCheck;if(a.exits)var o=A("rowtitle",l.rowtitle.row[0]+n,l.rowtitle.row[1]+n,l.rowtitle.column[0]+t,l.rowtitle.column[1]+t);else var o="";var s=e.rangeColCheck;if(s.exits)var u=A("coltitle",l.coltitle.row[0]+n,l.coltitle.row[1]+n,l.coltitle.column[0]+t,l.coltitle.column[1]+t);else var u="";var f=A("content",l.content.row[0]+n,l.content.row[1]+n,l.content.column[0]+t,l.content.column[1]+t);$("#luckysheet-chart-rangeShow").append(o+u+f);function A(h,g,p,v,y){var b=jo(),k=Xo(),w=b[p],C=g-1==-1?0:b[g-1],I=k[y],x=v-1==-1?0:k[v-1];if(h=="rowtitle")var T="#C65151";if(h=="coltitle")var T="#9667C0";if(h=="content")var T="#4970D1";var R='
';return R}},rangeMove:!1,rangeMovexy:null,rangeMoveIndex:null,rangeMoveObj:null,rangeMoveDraging:function(e,n,t){var l=d.currentChart,a=l.rangeArray[0].row[0],o=l.rangeArray[0].column[0],s=l.rangeRowCheck,u=l.rangeColCheck,f=l.rangeSplitArray,A=Ke(e.pageX,e.pageY),h=$("#luckysheet-cell-main").scrollLeft(),g=$("#luckysheet-cell-main").scrollTop(),p=A[0]+h,v=A[1]+g,y=$(window).height()+g-n-t,b=$(window).width()+h,k=Ph(v),w=k[2],C=zh(p),I=C[2],x=jo(),T=Xo(),R=d.chart_selection.rangeMoveObj.attr("id");if(R=="luckysheet-chart-rangeShow-content"){var D=d.chart_selection.rangeMoveIndex[0]-d.chart_selection.rangeMovexy[0]+w;s.exits?(D=x.length-1||v>y)&&(D=x.length-1-f.content.row[1]+f.content.row[0],S=x.length-1);var B=d.chart_selection.rangeMoveIndex[1]-d.chart_selection.rangeMovexy[1]+I;u.exits?(B=T.length-1||p>b)&&(B=T.length-1-f.content.column[1]+f.content.column[0],F=T.length-1),s.exits&&u.exits?(l.rangeArray=[{row:[a,S],column:[o,F]}],l.rangeSplitArray.range={row:[a,S],column:[o,F]},l.rangeSplitArray.content={row:[D-a,S-a],column:[B-o,F-o]},l.rangeSplitArray.rowtitle={row:l.rangeSplitArray.rowtitle.row,column:[B-o,F-o]},l.rangeSplitArray.coltitle={row:[D-a,S-a],column:l.rangeSplitArray.coltitle.column}):s.exits?(l.rangeArray=[{row:[a,S],column:[B,F]}],l.rangeSplitArray.range={row:[a,S],column:[B,F]},l.rangeSplitArray.content={row:[D-a,S-a],column:l.rangeSplitArray.content.column}):u.exits?(l.rangeArray=[{row:[D,S],column:[o,F]}],l.rangeSplitArray.range={row:[D,S],column:[o,F]},l.rangeSplitArray.content={row:l.rangeSplitArray.content.row,column:[B-o,F-o]}):(l.rangeArray=[{row:[D,S],column:[B,F]}],l.rangeSplitArray.range={row:[D,S],column:[B,F]})}else if(R=="luckysheet-chart-rangeShow-rowtitle"){var B=d.chart_selection.rangeMoveIndex[1]-d.chart_selection.rangeMovexy[1]+I;u.exits?(B=T.length-1||p>b)&&(B=T.length-1-f.rowtitle.column[1]+f.rowtitle.column[0],F=T.length-1),u.exits?(l.rangeArray=[{row:l.rangeArray[0].row,column:[o,F]}],l.rangeSplitArray.range={row:l.rangeArray[0].row,column:[o,F]},l.rangeSplitArray.rowtitle={row:l.rangeSplitArray.rowtitle.row,column:[B-o,F-o]},l.rangeSplitArray.content={row:l.rangeSplitArray.content.row,column:[B-o,F-o]}):(l.rangeArray=[{row:l.rangeArray[0].row,column:[B,F]}],l.rangeSplitArray.range={row:l.rangeArray[0].row,column:[B,F]})}else if(R=="luckysheet-chart-rangeShow-coltitle"){var D=d.chart_selection.rangeMoveIndex[0]-d.chart_selection.rangeMovexy[0]+w;s.exits?(D=x.length-1||v>y)&&(D=x.length-1-f.coltitle.row[1]+f.coltitle.row[0],S=x.length-1),s.exits?(l.rangeArray=[{row:[a,S],column:l.rangeArray[0].column}],l.rangeSplitArray.range={row:[a,S],column:l.rangeArray[0].column},l.rangeSplitArray.coltitle={row:[D-a,S-a],column:l.rangeSplitArray.coltitle.column},l.rangeSplitArray.content={row:[D-a,S-a],column:l.rangeSplitArray.content.column}):(l.rangeArray=[{row:[D,S],column:l.rangeArray[0].column}],l.rangeSplitArray.range={row:[D,S],column:l.rangeArray[0].column})}d.chart_selection.create()},rangeMoveDragged:function(){d.chart_selection.rangeMove=!1;var e=d.currentChart;e.rangeTxt=mt(d.currentSheetIndex,e.rangeArray[0],d.currentSheetIndex),e.chartData=Ct(e.rangeArray[0],d.currentSheetIndex),d.changeChartRange(e.chart_id,e.chartData,e.rangeArray,e.rangeTxt)},rangeResize:!1,rangeResizexy:null,rangeResizeIndex:null,rangeResizeObj:null,rangeResizeDraging:function(e,n,t){var l=d.currentChart,a=l.rangeArray[0].row[0],o=l.rangeArray[0].column[0],s=l.rangeRowCheck,u=l.rangeColCheck,f=l.rangeSplitArray,A=Ke(e.pageX,e.pageY),h=$("#luckysheet-cell-main").scrollLeft(),g=$("#luckysheet-cell-main").scrollTop(),p=A[0]+h,v=A[1]+g,y=$(window).height()+g-n-t,b=$(window).width()+h,k=Ph(v),w=k[2],C=zh(p),I=C[2],x=jo(),T=Xo(),R=d.chart_selection.rangeResizeObj.attr("id");if(R=="luckysheet-chart-rangeShow-content"){var D,S,B,F;if(d.chart_selection.rangeResize=="lt"?(D=d.chart_selection.rangeResizeIndex.row[0],B=d.chart_selection.rangeResizeIndex.column[0],S=d.chart_selection.rangeResizeIndex.row[1],F=d.chart_selection.rangeResizeIndex.column[1]):d.chart_selection.rangeResize=="lb"?(D=d.chart_selection.rangeResizeIndex.row[1],B=d.chart_selection.rangeResizeIndex.column[0],S=d.chart_selection.rangeResizeIndex.row[0],F=d.chart_selection.rangeResizeIndex.column[1]):d.chart_selection.rangeResize=="rt"?(D=d.chart_selection.rangeResizeIndex.row[0],B=d.chart_selection.rangeResizeIndex.column[1],S=d.chart_selection.rangeResizeIndex.row[1],F=d.chart_selection.rangeResizeIndex.column[0]):d.chart_selection.rangeResize=="rb"&&(D=d.chart_selection.rangeResizeIndex.row[1],B=d.chart_selection.rangeResizeIndex.column[1],S=d.chart_selection.rangeResizeIndex.row[0],F=d.chart_selection.rangeResizeIndex.column[0]),s.exits){var E=D-d.chart_selection.rangeResizexy[0]+w;E=x.length-1||v>y)&&(E=x.length-1)}else{var E=a-d.chart_selection.rangeResizexy[0]+w;E<0||v<0?E=0:(E>=x.length-1||v>y)&&(E=x.length-1)}if(u.exits){var z=B-d.chart_selection.rangeResizexy[1]+I;z=T.length-1||p>b)&&(z=T.length-1)}else{var z=o-d.chart_selection.rangeResizexy[1]+I;z<0||p<0?z=0:(z>=T.length-1||p>b)&&(z=T.length-1)}var P,L,O,q;E>S?(P=S,L=E):(P=E,L=S),z>F?(O=F,q=z):(O=z,q=F),!s.exits&&!u.exits?(l.rangeArray=[{row:[P,L],column:[O,q]}],l.rangeSplitArray.range={row:[P,L],column:[O,q]}):(l.rangeArray=[{row:[a,L],column:[o,q]}],l.rangeSplitArray.range={row:[a,L],column:[o,q]},l.rangeSplitArray.content={row:[P-a,L-a],column:[O-o,q-o]},s.exits&&(l.rangeSplitArray.rowtitle={row:l.rangeSplitArray.rowtitle.row,column:[O-o,q-o]}),u.exits&&(l.rangeSplitArray.coltitle={row:[P-a,L-a],column:l.rangeSplitArray.coltitle.column}))}else if(R=="luckysheet-chart-rangeShow-rowtitle"){var B,F;if(d.chart_selection.rangeResize=="lt"||d.chart_selection.rangeResize=="lb"?(B=d.chart_selection.rangeResizeIndex.column[0],F=d.chart_selection.rangeResizeIndex.column[1]):(d.chart_selection.rangeResize=="rt"||d.chart_selection.rangeResize=="rb")&&(B=d.chart_selection.rangeResizeIndex.column[1],F=d.chart_selection.rangeResizeIndex.column[0]),u.exits){var z=B-d.chart_selection.rangeResizexy[1]+I;z=T.length-1||p>b)&&(z=T.length-1)}else{var z=o-d.chart_selection.rangeResizexy[1]+I;z<0||p<0?z=0:(z>=T.length-1||p>b)&&(z=T.length-1)}var O,q;z>F?(O=F,q=z):(O=z,q=F),u.exits?(l.rangeArray=[{row:l.rangeArray[0].row,column:[o,q]}],l.rangeSplitArray.range={row:l.rangeArray[0].row,column:[o,q]},l.rangeSplitArray.rowtitle={row:l.rangeSplitArray.rowtitle.row,column:[O-o,q-o]},l.rangeSplitArray.content={row:l.rangeSplitArray.content.row,column:[O-o,q-o]}):(l.rangeArray=[{row:l.rangeArray[0].row,column:[O,q]}],l.rangeSplitArray.range={row:l.rangeArray[0].row,column:[O,q]})}else if(R=="luckysheet-chart-rangeShow-coltitle"){var D,S;if(d.chart_selection.rangeResize=="lt"||d.chart_selection.rangeResize=="rt"?(D=d.chart_selection.rangeResizeIndex.row[0],S=d.chart_selection.rangeResizeIndex.row[1]):(d.chart_selection.rangeResize=="lb"||d.chart_selection.rangeResize=="rb")&&(D=d.chart_selection.rangeResizeIndex.row[1],S=d.chart_selection.rangeResizeIndex.row[0]),s.exits){var E=D-d.chart_selection.rangeResizexy[0]+w;E=x.length-1||v>y)&&(E=x.length-1)}else{var E=a-d.chart_selection.rangeResizexy[0]+w;E<0||v<0?E=0:(E>=x.length-1||v>y)&&(E=x.length-1)}var P,L;E>S?(P=S,L=E):(P=E,L=S),s.exits?(l.rangeArray=[{row:[a,L],column:l.rangeArray[0].column}],l.rangeSplitArray.range={row:[a,L],column:l.rangeArray[0].column},l.rangeSplitArray.coltitle={row:[P-a,L-a],column:l.rangeSplitArray.coltitle.column},l.rangeSplitArray.content={row:[P-a,L-a],column:l.rangeSplitArray.content.column}):(l.rangeArray=[{row:[P,L],column:l.rangeArray[0].column}],l.rangeSplitArray.range={row:[P,L],column:l.rangeArray[0].column})}d.chart_selection.create()},rangeResizeDragged:function(){d.chart_selection.rangeResize=null;var e=d.currentChart;e.rangeTxt=mt(d.currentSheetIndex,e.rangeArray[0],d.currentSheetIndex),e.chartData=Ct(e.rangeArray[0],d.currentSheetIndex),d.changeChartRange(e.chart_id,e.chartData,e.rangeArray,e.rangeTxt)}}}function bs(e,n,t,l){var a=luckysheet.getluckysheet_select_save();a.length==1&&a[0].row[0]==a[0].row[1]&&a[0].column[0]==a[0].column[1]&&(pn("right","rangeOfSelect"),pn("down","rangeOfSelect"),a=luckysheet.getluckysheet_select_save());for(var o=-1,s=a[0].row[1]-a[0].row[0],u=a[0].row[0];u<=a[0].row[1];u++){for(var f=a[0].column[0];f<=a[0].column[1];f++){var A=Fe(u,f,luckysheet.flowdata());if(A!=null&&A.toString().length>0){o=u;break}}if(o!==-1)break}o==-1&&(o=0),a[0].row=[o,o],a[0].row_focus=o,luckysheet.setluckysheet_select_save(a),d.luckysheet_shiftpositon=$.extend(!0,{},a[0]),Pn("down","range",!1,s),a=luckysheet.getluckysheet_select_save();for(var h=-1,g=a[0].column[1]-a[0].column[0],f=a[0].column[0];f<=a[0].column[1];f++){for(var u=a[0].row[0];u<=a[0].row[1];u++){var A=Fe(u,f,luckysheet.flowdata());if(A!=null&&A.toString().length>0){h=f;break}}if(h!==-1)break}h==-1&&(h=0),a[0].column=[h,h],a[0].column_focus=h,luckysheet.setluckysheet_select_save(a),d.luckysheet_shiftpositon=$.extend(!0,{},a[0]),Pn("right","range",!1,g),a=luckysheet.getluckysheet_select_save();var p=$.extend(!0,[],a),v=mt(d.currentSheetIndex,p[0],d.currentSheetIndex);console.log("==>rangeArray ",p);let y=Ct();console.dir(y);let b=Ha("chart"),k=b+"_c",w='',C=$(Ul(w,{id:k,addclass:"luckysheet-data-visualization-chart",title:"\u56FE\u8868\u751F\u6210",content:""})).appendTo($(".luckysheet-cell-main")),I=document.getElementById(k),{render:x,chart_json:T}=d.createChart($(`#${k}`).children(".luckysheet-modal-dialog-content")[0],y,b,p,v);e=It(e||400),n=It(n||250),t=It(t||0),l=It(l||0),I.style.width=e,I.style.height=n,I.style.position="absolute",I.style.background="#fff",I.style.left=t,I.style.top=l,x.style.width="100%",x.style.height="100%",I.style.zIndex=d.zIndex?d.zIndex:15,d.zIndex++;let R=d.luckysheetfile[Q(d.currentSheetIndex)];R.chart||(R.chart=[]),R.chart.push({chartType:"chartmix",chart_id:b,width:e,height:n,left:t,top:l,sheetIndex:R.index}),Tr(b),$(`#${b}_c .luckysheet-modal-controll-del`).click(function(S){yo(b)}),ha(C),$(`#${b}_c .luckysheet-modal-controll-update`).click(function(S){bo()}),C.children(".luckysheet-modal-dialog-content").mousedown(function(S){d.chartparam.luckysheetCurrentChartMaxState||Tr(b),S.stopPropagation()}),C.mousedown(function(S){if(!d.chartparam.luckysheetCurrentChartMaxState){Tr(b),Rl(!0),!$(S.target).is(".luckysheet-modal-dialog-controll")&&!$(S.target).is(".luckysheet-modal-controll-btn")&&!$(S.target).is("i")&&(d.chartparam.luckysheetCurrentChartMoveTimeout=setTimeout(function(){d.chartparam.luckysheetCurrentChartMove=!0},100));var B=d.chartparam.luckysheetCurrentChartMoveObj.offset(),F=d.chartparam.luckysheetCurrentChartMoveObj.position();d.chartparam.luckysheetCurrentChartMoveXy=[S.pageX-B.left,S.pageY-B.top,F.left,F.top,$("#luckysheet-scrollbar-x").scrollLeft(),$("#luckysheet-scrollbar-y").scrollTop()],d.chartparam.luckysheetCurrentChartMoveWinH=$("#luckysheet-cell-main")[0].scrollHeight,d.chartparam.luckysheetCurrentChartMoveWinW=$("#luckysheet-cell-main")[0].scrollWidth,!$(S.target).hasClass("luckysheet-mousedown-cancel")&&$(S.target).filter("[class*='sp-palette']").length==0&&$(S.target).filter("[class*='sp-thumb']").length==0&&$(S.target).filter("[class*='sp-']").length==0&&($("#luckysheet-rightclick-menu").hide(),$("#luckysheet-cols-h-hover").hide(),$("#luckysheet-cols-menu-btn").hide(),$("#luckysheet-rightclick-menu").hide(),$("#luckysheet-sheet-list, #luckysheet-rightclick-sheet-menu, #luckysheet-user-menu").hide(),$("body > .luckysheet-filter-menu, body > .luckysheet-filter-submenu, body > .luckysheet-cols-menu").hide()),S.stopPropagation()}}).find(".luckysheet-modal-dialog-resize-item").mousedown(function(S){if(d.chartparam.luckysheetCurrentChartActive){d.chartparam.luckysheetCurrentChartResize=$(this).data("type");var B=Ke(S.pageX,S.pageY),F=$("#luckysheet-scrollbar-x").scrollLeft(),E=$("#luckysheet-scrollbar-y").scrollTop(),z=B[0]+F,P=B[1]+E,L=d.chartparam.luckysheetCurrentChartResizeObj.position();d.chartparam.luckysheetCurrentChartResizeXy=[z,P,C.width(),C.height(),L.left+F,L.top+E,F,E],d.chartparam.luckysheetCurrentChartResizeWinH=$("#luckysheet-cell-main")[0].scrollHeight,d.chartparam.luckysheetCurrentChartResizeWinW=$("#luckysheet-cell-main")[0].scrollWidth,d.chartparam.luckysheetCurrentChart=b,S.stopPropagation()}});let D={chartType:"chartmix",chart_id:b,width:e,height:n,left:t,top:l,sheetIndex:R.index,needRangeShow:!1,chartOptions:T.chartOptions,chartData:y};console.log("==> \u56FE\u8868\u534F\u540C :\u65B0\u5EFA\u56FE\u8868",D),ae.saveParam("c",R.index,D,{op:"add",cid:b,chartType:"chartmix"})}function ha(e){e.find(".luckysheet-modal-dialog-content").hover(function(){e.removeClass("chart-moveable")},function(){e.addClass("chart-moveable")}),e.hover(function(){e.addClass("chart-moveable")},function(){e.removeClass("chart-moveable")})}function yo(e){$(`.luckysheet-cell-main #${e}_c`).remove(),Vn();let n=d.luckysheetfile[Q(d.currentSheetIndex)],t=n.chart.findIndex(l=>l.chart_id==e);n.chart.splice(t,1),console.log("==> \u56FE\u8868\u534F\u540C :\u5220\u9664\u56FE\u8868",e),ae.saveParam("c",n.index,{chart_id:e,chartType:"chartmix"},{op:"del",cid:e})}function Tr(e){let n=d.luckysheetfile[Q(d.currentSheetIndex)].chart;for(let t in n)n[t].needRangeShow=!1,n[t].chart_id==e&&(n[t].needRangeShow=!0,d.currentChart=d.getChartJson(e));Fh(e)}function Vn(){let e=d.luckysheetfile[Q(d.currentSheetIndex)].chart;for(let n in e)e[n].needRangeShow=!1;Lh()}function Fh(e){let n=$("#"+e+"_c");d.chart_selection.create(),d.chartparam.luckysheetCurrentChartActive=!0,d.chartparam.luckysheetCurrentChartMoveObj=n,d.chartparam.luckysheetCurrentChartResizeObj=n,d.chartparam.luckysheetCurrentChart=e,$("#luckysheet-cell-main").find(".luckysheet-modal-dialog-chart .luckysheet-modal-dialog-resize").hide(),$("#luckysheet-cell-main").find(".luckysheet-modal-dialog-chart .luckysheet-modal-dialog-controll").hide(),n.css("z-index",d.chartparam.luckysheetCurrentChartZIndexRank++),n.find(".luckysheet-modal-dialog-resize").show(),n.find(".luckysheet-modal-dialog-controll").show(),($(".chartSetting").is(":visible")||d.chartparam.luckysheet_chart_redo_click)&&e!=d.chartparam.luckysheetCurrentChart&&$("body .luckysheet-cols-menu").hide(),d.currentChart=d.highlightChart(e)}function Lh(e){if($("#luckysheet-cell-main .luckysheet-modal-dialog-chart .luckysheet-modal-dialog-resize, #luckysheet-cell-main .luckysheet-modal-dialog-chart .luckysheet-modal-dialog-controll").hide(),$("#luckysheet-cell-main").find(".luckysheet-datavisual-selection-set div").remove(),d.chartparam.luckysheetCurrentChartActive=!1,$("#luckysheet-chart-rangeShow").empty(),!e&&$(".chartSetting").is(":visible")&&!he()){let n=d.luckysheetfile[Q(d.currentSheetIndex)],t=d.chartparam.luckysheetCurrentChart,l=d.chartparam.getChartJson(t);console.log("==> \u56FE\u8868\u534F\u540C :\u66F4\u65B0\u914D\u7F6E"),ae.saveParam("c",n.index,{chart_id:t,chartOptions:l,chartType:"chartmix"},{op:"update",cid:t}),jy()}}function bo(e,n){$(".chartSetting").is(":visible")||($(".chartSetting").show(),$("#luckysheet-cell-main").find(".luckysheet-datavisual-selection-set div").show(),d.chartparam.luckysheetCurrentChartActive=!0,setTimeout(function(){Lt()},0))}function jy(e){$(".chartSetting").is(":visible")&&($(".chartSetting").hide(),$("#luckysheet-cell-main .luckysheet-modal-dialog-chart .luckysheet-modal-dialog-resize, #luckysheet-cell-main .luckysheet-modal-dialog-chart .luckysheet-modal-dialog-controll").hide(),$("#luckysheet-cell-main").find(".luckysheet-datavisual-selection-set div").remove(),d.chartparam.luckysheetCurrentChartActive=!1,!he()&&!e&&setTimeout(function(){Lt()},0))}function wu(e){Lh("true"),d.luckysheetfile.forEach(t=>{t.index==e?((t.chart||[]).filter(a=>a.chartType==="chartmix")||[]).forEach(a=>{a.isShow=!0,$("#"+a.chart_id+"_c").show(),d.resizeChart(a.chart_id),a.needRangeShow==!0&&(d.currentChart=d.getChartJson(a.chart_id),Fh(a.chart_id))}):(t.chart||[]).forEach(a=>{a.isShow=!1,$("#"+a.chart_id+"_c").hide()})})}var Ph,zh,Vy,qy,qr=De(()=>{dt();wn();Qt();je();jt();po();wt();El();Wr();Ba();Pt();Nr();$t();je();Ph=Tt,zh=xt,Vy=["expendPlugins/libs/vue@2.6.11.min.js","expendPlugins/libs/vuex.min.js","expendPlugins/libs/elementui.min.js","expendPlugins/libs/echarts.min.js","expendPlugins/libs/chartmix.umd.min.js"],qy=["expendPlugins/libs/element-ui.css","expendPlugins/libs/chartmix.css"]});function yi(e){return!(e===""||e==null||isNaN(e))}function qn(e){var n=Object.prototype.toString,t={"[object Boolean]":"boolean","[object Number]":"number","[object String]":"string","[object Function]":"function","[object Array]":"array","[object Date]":"date","[object RegExp]":"regExp","[object Undefined]":"undefined","[object Null]":"null","[object Object]":"object"};return t[n.call(e)]}function ko(e,n,t,l){var a={};return n.length>1&&(a={title:{row:[0,0],column:[0,0]},rowtitle:{row:[0,0],column:[1,e[0].length-1]},coltitle:{row:[1,e.length-1],column:[0,0]},content:{row:[1,e.length-1],column:[1,e[0].length-1]},type:"multi",range:n}),t.exits&&l.exits?a={title:{row:l.range,column:t.range},rowtitle:{row:l.range,column:[t.range[1]+1,e[0].length-1]},coltitle:{row:[l.range[1]+1,e.length-1],column:t.range},content:{row:[l.range[1]+1,e.length-1],column:[t.range[1]+1,e[0].length-1]},type:"normal",range:n[0]}:t.exits?a={title:null,rowtitle:null,coltitle:{row:[0,e.length-1],column:t.range},content:{row:[0,e.length-1],column:[t.range[1]+1,e[0].length-1]},type:"leftright",range:n[0]}:l.exits?a={title:null,rowtitle:{row:l.range,column:[0,e[0].length-1]},coltitle:null,content:{row:[l.range[1]+1,e.length-1],column:[0,e[0].length-1]},type:"topbottom",range:n[0]}:a={title:null,rowtitle:null,coltitle:null,content:{row:[0,e.length-1],column:[0,e[0].length-1]},type:"contentonly",range:n[0]},a}function wo(e){for(var n=e.length-1,t=e[0].length-1,l,a;n>=0&&t>=0;){var o=e[n][t];if(o===null||yi(o)||qn(o)=="object"&&yi(o.v)||qn(o)=="object"&&qn(o.v)=="undefined"||o===""||o.v==="")if(o&&o.ct&&o.ct.fa=="yyyy-MM-dd"){l=n+1,a=t+1;break}else l=n--,a=t--;else{n==e.length-1&&t==e[0].length-1?(l=n,a=t):(l=n+1,a=t+1);break}}var s={exits:!1,range:[0,0]};if(l>0)for(var u=l;u>=0;u--){var o=e[u][a];if(!(o===null||yi(o)||qn(o)=="object"&&yi(o.v)||qn(o)=="object"&&qn(o.v)=="undefined"||o===""||o.v==="")){s.exits=!0,s.range=[0,u];break}}var f={exits:!1,range:[0,0]};if(a>0)for(var u=a;u>=0;u--){var o=e[l][u];if(o===null||yi(o)||qn(o)=="object"&&yi(o.v)||qn(o)=="object"&&qn(o.v)=="undefined"||o===""||o.v===""){if(o&&o.ct&&o.ct.fa=="yyyy-MM-dd"){f.exits=!0,f.range=[0,u];break}}else{f.exits=!0,f.range=[0,u];break}}return s.range[1]+1==e.length&&(s={exits:!1,range:[0,0]}),f.range[1]+1==e[0].length&&(f={exits:!1,range:[0,0]}),[s,f]}function Oh(e){var n="string";return Xy(e)?n="date":!isNaN(parseFloat(e))&&!Uy(e)&&(n="num"),n}function Vh(e,n,t){var l=null,a=t[e][n];return a!=null&&(a.v!=null?l=a.v:l=a),l==null&&(l=""),l}function Xy(e){if(e==null||e.toString().length<5)return!1;return!!n(e);function n(t){var l=/^(\d{4})-(\d{1,2})-(\d{1,2})(\s(\d{1,2}):(\d{1,2})(:(\d{1,2}))?)?$/,a=/^(\d{4})\/(\d{1,2})\/(\d{1,2})(\s(\d{1,2}):(\d{1,2})(:(\d{1,2}))?)?$/;if(!l.test(t)&&!a.test(t))return!1;var o=RegExp.$1,s=RegExp.$2,u=RegExp.$3;if(o<1900||s>12||u>31)return!1;if(s==2){if(new Date(o,1,29).getDate()==29&&u>29)return!1;if(new Date(o,1,29).getDate()!=29&&u>28)return!1}return!0}}function Uy(e){var n=/[\u4E00-\u9FA5]|[\uFE30-\uFFA0]/gi;return!!n.exec(e)}var xu=De(()=>{});function qh(e,n,t,l){ga(l||Gy),ma(t||Wy,null,()=>{console.log("## vchart \u63D2\u4EF6\u76F8\u5173\u4F9D\u8D56\u52A0\u8F7D\u5B8C\u6210\uFF01"),d.chart_selection=ku();for(let a=0;at.chartType==="vchart");for(let t=0;t
${content}
',u=$(Ul(s,{id:o,addclass:"luckysheet-data-visualization-chart",title:"\u56FE\u8868\u751F\u6210",content:""})).appendTo($(".luckysheet-cell-main"));jh(u);let f=$(`#${o}`).children(".luckysheet-modal-dialog-content")[0];f.id=a;let A=document.getElementById(o),h=new VChart.default(l.chartOptions.spec,{dom:f});h.renderSync(),Cu(a,h),d.currentChart=l.chartOptions,nn(a),$(`#${a}_c .luckysheet-modal-controll-del`).click(function(g){xo(a)}),$(`#${a}_c .luckysheet-modal-controll-update`).click(function(g){Co(h,a)}),u.children(".luckysheet-modal-dialog-content").mousedown(function(g){d.chartparam.luckysheetCurrentChartMaxState||nn(a),g.stopPropagation()}),u.mousedown(function(g){if(!d.chartparam.luckysheetCurrentChartMaxState){nn(a),Rl(!0),!$(g.target).is(".luckysheet-modal-dialog-controll")&&!$(g.target).is(".luckysheet-modal-controll-btn")&&!$(g.target).is("i")&&(d.chartparam.luckysheetCurrentChartMoveTimeout=setTimeout(function(){d.chartparam.luckysheetCurrentChartMove=!0},100));var p=d.chartparam.luckysheetCurrentChartMoveObj.offset(),v=d.chartparam.luckysheetCurrentChartMoveObj.position();d.chartparam.luckysheetCurrentChartMoveXy=[g.pageX-p.left,g.pageY-p.top,v.left,v.top,$("#luckysheet-scrollbar-x").scrollLeft(),$("#luckysheet-scrollbar-y").scrollTop()],d.chartparam.luckysheetCurrentChartMoveWinH=$("#luckysheet-cell-main")[0].scrollHeight,d.chartparam.luckysheetCurrentChartMoveWinW=$("#luckysheet-cell-main")[0].scrollWidth,!$(g.target).hasClass("luckysheet-mousedown-cancel")&&$(g.target).filter("[class*='sp-palette']").length==0&&$(g.target).filter("[class*='sp-thumb']").length==0&&$(g.target).filter("[class*='sp-']").length==0&&($("#luckysheet-rightclick-menu").hide(),$("#luckysheet-cols-h-hover").hide(),$("#luckysheet-cols-menu-btn").hide(),$("#luckysheet-rightclick-menu").hide(),$("#luckysheet-sheet-list, #luckysheet-rightclick-sheet-menu, #luckysheet-user-menu").hide(),$("body > .luckysheet-filter-menu, body > .luckysheet-filter-submenu, body > .luckysheet-cols-menu").hide()),g.stopPropagation()}}).find(".luckysheet-modal-dialog-resize-item").mousedown(function(g){if(d.chartparam.luckysheetCurrentChartActive){d.chartparam.luckysheetCurrentChartResize=$(this).data("type");var p=Ke(g.pageX,g.pageY),v=$("#luckysheet-scrollbar-x").scrollLeft(),y=$("#luckysheet-scrollbar-y").scrollTop(),b=p[0]+v,k=p[1]+y,w=d.chartparam.luckysheetCurrentChartResizeObj.position();d.chartparam.luckysheetCurrentChartResizeXy=[b,k,u.width(),u.height(),w.left+v,w.top+y,v,y],d.chartparam.luckysheetCurrentChartResizeWinH=$("#luckysheet-cell-main")[0].scrollHeight,d.chartparam.luckysheetCurrentChartResizeWinW=$("#luckysheet-cell-main")[0].scrollWidth,d.chartparam.luckysheetCurrentChart=a,g.stopPropagation()}}),A.style.width=It(l.width),A.style.height=It(l.height),A.style.position="absolute",A.style.background="#fff",A.style.left=It(l.left),A.style.top=It(l.top),A.style.zIndex=d.zIndex?d.zIndex:15,d.zIndex++}}}function bi(){let e=Reflect.get(window,"VChart");return e||console.warn("VChart is not defined, register plugin for 'vchart' please."),e}function Cu(e,n){let t=Q(d.currentSheetIndex),a=d.luckysheetfile[t].chart;if(a){let o=a.find(s=>s.chart_id===e);o&&(o.vchart=n)}}function ws(e,n,t,l){if(!bi())return;var a=luckysheet.getluckysheet_select_save();if(a.length==1&&a[0].row[0]==a[0].row[1]&&a[0].column[0]==a[0].column[1])return;for(var o=-1,s=a[0].row[1]-a[0].row[0],u=a[0].row[0];u<=a[0].row[1];u++){for(var f=a[0].column[0];f<=a[0].column[1];f++){var A=Fe(u,f,luckysheet.flowdata());if(A!=null&&A.toString().length>0){o=u;break}}if(o!==-1)break}o==-1&&(o=0),a[0].row=[o,o],a[0].row_focus=o,luckysheet.setluckysheet_select_save(a),d.luckysheet_shiftpositon=$.extend(!0,{},a[0]),Pn("down","range",!1,s),a=luckysheet.getluckysheet_select_save();for(var h=-1,g=a[0].column[1]-a[0].column[0],f=a[0].column[0];f<=a[0].column[1];f++){for(var u=a[0].row[0];u<=a[0].row[1];u++){var A=Fe(u,f,luckysheet.flowdata());if(A!=null&&A.toString().length>0){h=f;break}}if(h!==-1)break}h==-1&&(h=0),a[0].column=[h,h],a[0].column_focus=h,luckysheet.setluckysheet_select_save(a),d.luckysheet_shiftpositon=$.extend(!0,{},a[0]),Pn("right","range",!1,g),a=luckysheet.getluckysheet_select_save();var p=$.extend(!0,[],a),v=mt(d.currentSheetIndex,p[0],d.currentSheetIndex);let y=Ct(),b=Ha("chart"),k=b+"_c",w='',C=$(Ul(w,{id:k,addclass:"luckysheet-data-visualization-chart",title:"\u56FE\u8868\u751F\u6210",content:""})).appendTo($(".luckysheet-cell-main")),I=$(`#${k}`).children(".luckysheet-modal-dialog-content")[0];I.id=b;let x=document.getElementById(k),T=wo(y),R=T[0],D=T[1],S=ko(y,p,D,R),B=Hh(S,y),F={spec:B,rangeColCheck:D,rangeRowCheck:R,chart_id:b,rangeSplitArray:S,rangeArray:p,rangeTxt:v},E=new VChart.default(B,{dom:I});E.renderSync(),d.currentChart=F,x.style.width=It(e||400),x.style.height=It(n||250),x.style.position="absolute",x.style.background="#fff",x.style.left=It(t||0),x.style.top=It(l||0),x.style.zIndex=d.zIndex?d.zIndex:15,d.zIndex++;let z=d.luckysheetfile[Q(d.currentSheetIndex)];z.chart||(z.chart=[]),z.chart.push({chartType:"vchart",chart_id:b,width:x.style.width,height:x.style.height,left:x.style.left,top:x.style.top,sheetIndex:z.index,chartOptions:F,vchart:E}),jh(C),$(`#${b}_c .luckysheet-modal-controll-del`).click(function(L){xo(b)}),$(`#${b}_c .luckysheet-modal-controll-update`).click(function(L){Co(E,b)}),C.children(".luckysheet-modal-dialog-content").mousedown(function(L){d.chartparam.luckysheetCurrentChartMaxState||nn(b),L.stopPropagation()}),C.mousedown(function(L){if(!d.chartparam.luckysheetCurrentChartMaxState){nn(b),Rl(!0),!$(L.target).is(".luckysheet-modal-dialog-controll")&&!$(L.target).is(".luckysheet-modal-controll-btn")&&!$(L.target).is("i")&&(d.chartparam.luckysheetCurrentChartMoveTimeout=setTimeout(function(){d.chartparam.luckysheetCurrentChartMove=!0},100));var O=d.chartparam.luckysheetCurrentChartMoveObj.offset(),q=d.chartparam.luckysheetCurrentChartMoveObj.position();d.chartparam.luckysheetCurrentChartMoveXy=[L.pageX-O.left,L.pageY-O.top,q.left,q.top,$("#luckysheet-scrollbar-x").scrollLeft(),$("#luckysheet-scrollbar-y").scrollTop()],d.chartparam.luckysheetCurrentChartMoveWinH=$("#luckysheet-cell-main")[0].scrollHeight,d.chartparam.luckysheetCurrentChartMoveWinW=$("#luckysheet-cell-main")[0].scrollWidth,!$(L.target).hasClass("luckysheet-mousedown-cancel")&&$(L.target).filter("[class*='sp-palette']").length==0&&$(L.target).filter("[class*='sp-thumb']").length==0&&$(L.target).filter("[class*='sp-']").length==0&&($("#luckysheet-rightclick-menu").hide(),$("#luckysheet-cols-h-hover").hide(),$("#luckysheet-cols-menu-btn").hide(),$("#luckysheet-rightclick-menu").hide(),$("#luckysheet-sheet-list, #luckysheet-rightclick-sheet-menu, #luckysheet-user-menu").hide(),$("body > .luckysheet-filter-menu, body > .luckysheet-filter-submenu, body > .luckysheet-cols-menu").hide()),L.stopPropagation()}}).find(".luckysheet-modal-dialog-resize-item").mousedown(function(L){if(d.chartparam.luckysheetCurrentChartActive){d.chartparam.luckysheetCurrentChartResize=$(this).data("type");var O=Ke(L.pageX,L.pageY),q=$("#luckysheet-scrollbar-x").scrollLeft(),j=$("#luckysheet-scrollbar-y").scrollTop(),G=O[0]+q,_=O[1]+j,ee=d.chartparam.luckysheetCurrentChartResizeObj.position();d.chartparam.luckysheetCurrentChartResizeXy=[G,_,C.width(),C.height(),ee.left+q,ee.top+j,q,j],d.chartparam.luckysheetCurrentChartResizeWinH=$("#luckysheet-cell-main")[0].scrollHeight,d.chartparam.luckysheetCurrentChartResizeWinW=$("#luckysheet-cell-main")[0].scrollWidth,d.chartparam.luckysheetCurrentChart=b,L.stopPropagation()}});let P={chartType:"vchart",chart_id:b,width:x.style.width,height:x.style.height,left:x.style.left,top:x.style.top,sheetIndex:z.index,needRangeShow:!1,chartOptions:F,chartData:y};console.log("==> \u56FE\u8868\u534F\u540C :vchart \u65B0\u5EFA\u56FE\u8868",P),ae.saveParam("c",z.index,P,{op:"add",cid:b,chartType:"vchart"})}function xo(e){$(`.luckysheet-cell-main #${e}_c`).remove(),Qy();let n=d.luckysheetfile[Q(d.currentSheetIndex)],t=n.chart.findIndex(l=>l.chart_id==e);n.chart.splice(t,1),console.log("==> \u56FE\u8868\u534F\u540C :\u5220\u9664\u56FE\u8868",e),ae.saveParam("c",n.index,{chart_id:e,chartType:"vchart"},{op:"del",cid:e})}function Tu(e){Uh("true"),d.luckysheetfile.forEach(t=>{t.index==e?((t.chart||[]).filter(a=>a.chartType==="vchart")||[]).forEach(a=>{a.isShow=!0,$("#"+a.chart_id+"_c").show(),a.needRangeShow==!0&&Xh(a.chart_id)}):(t.chart||[]).forEach(a=>{a.isShow=!1,$("#"+a.chart_id+"_c").hide()})})}function Hh(e,n,t){let{coltitle:l,rowtitle:a,content:o,range:s}=e,u={data:[{id:"data",values:[]}],type:t||["line","bar"][Math.floor(Math.random()*2)],xField:"key",yField:"value"};if(e.type==="contentonly"){var f=e;let b=f.content,k=[];if(b!=null){for(var A={},h=b.row[0];h<=b.row[1];h++){for(var g=[],p=0,v=b.column[0];v<=b.column[1];v++){var y=Vh(h,v,n);g.push(y),h==b.row[0]&&(A[p++]=Oh(y))}k.push(g)}k.forEach((w,C)=>{let I=w[0];for(let x=0;xa.chart_id===e);d.currentChart=l.chartOptions}Xh(e)}function xs(e,n,t,l,a,o){if(!Reflect.get(window,"VChart")||!e)return;let s=e.chartOptions;if(s.rangeArray.length==1){var u=s.rangeArray[0].row,f=s.rangeArray[0].column;if(n>u[1]||tf[1]||a \u56FE\u8868\u534F\u540C : vchart \u6570\u636E\u66F4\u65B0");let x=Q(d.currentSheetIndex),T=d.luckysheetfile[x],R=T.chart.find(D=>D.chart_id===s.chart_id);if(R.chartOptions=I,o)return;ae.saveParam("c",T.index,{r_st:n,r_ed:t,c_st:l,c_ed:a,chart_id:s.chart_id,chartOptions:I},{op:"update_data"})}}}function Xh(e){let n=$("#"+e+"_c");d.chart_selection.create(),d.chartparam.luckysheetCurrentChartActive=!0,d.chartparam.luckysheetCurrentChartMoveObj=n,d.chartparam.luckysheetCurrentChartResizeObj=n,d.chartparam.luckysheetCurrentChart=e,$("#luckysheet-cell-main").find(".luckysheet-modal-dialog-chart .luckysheet-modal-dialog-resize").hide(),$("#luckysheet-cell-main").find(".luckysheet-modal-dialog-chart .luckysheet-modal-dialog-controll").hide(),n.css("z-index",d.chartparam.luckysheetCurrentChartZIndexRank++),n.find(".luckysheet-modal-dialog-resize").show(),n.find(".luckysheet-modal-dialog-controll").show(),($(".chartSetting").is(":visible")||d.chartparam.luckysheet_chart_redo_click)&&e!=d.chartparam.luckysheetCurrentChart&&$("body .luckysheet-cols-menu").hide()}function Qy(){let e=d.luckysheetfile[Q(d.currentSheetIndex)].chart;for(let n in e)e[n].needRangeShow=!1;Uh()}function Uh(){$("#luckysheet-cell-main .luckysheet-modal-dialog-chart .luckysheet-modal-dialog-resize, #luckysheet-cell-main .luckysheet-modal-dialog-chart .luckysheet-modal-dialog-controll").hide(),$("#luckysheet-cell-main").find(".luckysheet-datavisual-selection-set div").remove(),d.chartparam.luckysheetCurrentChartActive=!1,$("#luckysheet-chart-rangeShow").empty()}function Co(e,n){if(!n)return console.warn("\u8BF7\u4F20\u5165\u56FE\u8868ID");let a=d.luckysheetfile[Q(d.currentSheetIndex)].chart.find(h=>h.chart_id==n).chartOptions.spec;console.log("baseSpec",a);let o=$("#luckysheet-modal-dialog-mask");o.show(),o.css("background","rgba(0, 0, 0, 0.15)");let s=$("#luckysheet-vchart-setting-dialog");s.length?$(s).show():s=$(Ul("
")).appendTo($("body"));let u='
${title}
${body}
',f='
${type}${style}
${typeContent}
';$(s).html(Ul(u,{title:te().vChart.title,close:te().vChart.close,cancel:te().vChart.cancel,confirm:te().vChart.confirm,body:Ul(f,{type:te().vChart.type,style:te().vChart.style,typeContent:Qd,styleContent:_d})})),$(".luckysheet-vchart-setting-dialog-body").on("click",".tab",function(){let h=$(this).index();$(".tab").removeClass("active"),$(this).addClass("active"),$(".luckysheet-vchart-setting-dialog-body-content").hide(),$(".luckysheet-vchart-setting-dialog-body-content").eq(h).show()});let A=null;$(".vchart-type-item").off("click").on("click",function(){let h=$(this).attr("data-type");switch(console.log("==> \u5F53\u524D\u70B9\u51FB\u7684\u56FE\u8868\u7C7B\u578B",h),h){case"basic-line":A=Object.assign(a,{type:"line",line:{style:{curveType:"none"}}});break;case"smoothed-line":A=Object.assign(a,{type:"line",line:{style:{curveType:"monotone"}}});break;case"step-line":A=Object.assign(a,{type:"line",line:{style:{curveType:"stepAfter"}}});break;case"basic-area":A=Object.assign(a,{type:"area",line:{style:{curveType:"none"}}});break;case"smoothed-area":A=Object.assign(a,{type:"area",line:{style:{curveType:"monotone"}}});break;case"step-area":A=Object.assign(a,{type:"area",line:{style:{curveType:"stepAfter"}}});break;case"basic-column":A=Object.assign(a,{type:"bar"});break;case"basic-pie":A=Object.assign(a,{type:"pie",outerRadius:.8,innerRadius:0,padAngle:0,valueField:"value",categoryField:"key",pie:{style:{cornerRadius:0},state:{hover:{outerRadius:.85,lineWidth:1},selected:{outerRadius:.85,lineWidth:1}}}});break;case"ring-pie":A=Object.assign(a,{type:"pie",valueField:"value",categoryField:"key",outerRadius:.8,innerRadius:.5,padAngle:.6,pie:{style:{cornerRadius:10},state:{hover:{outerRadius:.85,lineWidth:1},selected:{outerRadius:.85,lineWidth:1}}}}),console.log("==> spec",A);break}e.updateSpec(A)}),$("#vchart-setting-title").off("input").on("input",function(){let h=$(this)[0].value;A=Object.assign(a,{title:{visible:!0,text:h}}),e.updateSpec(A)}),$("#luckysheet-vchart-setting-dialog-close").off("click").on("click",()=>Cs(A)),$(".luckysheet-vchart-setting-dialog-footer .cancel").off("click").on("click",()=>Cs(A)),$(".luckysheet-vchart-setting-dialog-footer .confirm").off("click").on("click",()=>Cs(A)),$(o).off("click").on("click",()=>Cs(A))}function Cs(e){let n=$("#luckysheet-modal-dialog-mask"),t=$("#luckysheet-vchart-setting-dialog");n.css("background","#fff"),n.hide(),t.hide(),!!e&&(console.log("==> \u56FE\u8868\u534F\u540C :\u66F4\u65B0\u914D\u7F6E"),console.log("==> \u66F4\u65B0\u540E\u7684\u914D\u7F6E",e),console.groupEnd())}var Wy,Gy,Hn=De(()=>{dt();wn();Wt();je();wt();qr();Wr();Qt();Ba();El();xu();wn();yt();$t();jt();je();Wy=["expendPlugins/libs/vchart.min.js"],Gy=["expendPlugins/libs/vchart.css"]});function jn(e){d.flowdata==null||d.flowdata.length==0||(clearTimeout(Wh),Wh=setTimeout(()=>{d.clearjfundo&&d.jfredo.push({type:"zoomChange",zoomRatio:d.zoomRatio,curZoomRatio:e,sheetIndex:d.currentSheetIndex}),an=null,d.zoomRatio=e;let n=ve.getSheetByIndex();He.buildAllPs(n.data),Se.images=n.images,Se.allImagesShow(),Se.init(),n.config==null&&(n.config={}),n.config.sheetViewZoom==null&&(n.config.sheetViewZoom={});let t=n.config.curentsheetView;t==null&&(t="viewNormal"),n.config.sheetViewZoom[t+"ZoomScale"]=e,ae.saveParam("all",d.currentSheetIndex,d.zoomRatio,{k:"zoomRatio"}),ae.saveParam("cg",d.currentSheetIndex,n.config.sheetViewZoom,{k:"sheetViewZoom"}),ki()},100))}function ki(){Mt(d.flowdata.length,d.flowdata[0].length),$c()}function Gh(){let e=.02,n=.1,t=4,l=.1;$("#luckysheet-zoom-minus").click(function(){let a;d.zoomRatio==null?a=d.zoomRatio=1:a=Math.ceil(d.zoomRatio*10)/10,a=a-n,a==d.zoomRatio&&(a=a-n),a<=l&&(a=l),jn(a),pr(a)}),$("#luckysheet-zoom-plus").click(function(){let a;d.zoomRatio==null?a=d.zoomRatio=1:a=Math.floor(d.zoomRatio*10)/10,a=a+n,a==d.zoomRatio&&(a=a+n),a>=t&&(a=t),jn(a),pr(a)}),$("#luckysheet-zoom-slider").mousedown(function(a){let o=$(this).offset().left,s=a.pageX,u=Yh(s-o);jn(u),pr(u)}),$("#luckysheet-zoom-cursor").mousedown(function(a){let o=a.pageX,s=parseFloat($("#luckysheet-zoom-cursor").css("left"));$("#luckysheet-zoom-cursor").css("transition","none"),$(document).off("mousemove.zoomCursor").on("mousemove.zoomCursor",function(u){let A=u.pageX-o,h=s+A,g=Yh(h);g>t&&(g=t,h=100),g=t?an=t:an=t?s=t:s50&&(n=Math.round(((e-50)*6/100+1)*100)/100),n}function _y(e){let n=50;e<1?n=Math.round((e-.1)*100/.18)/10:e>1&&(n=Math.round((e-1)*100/.6)/10+50),$("#luckysheet-zoom-cursor").css("left",n-4)}function pr(e){let n=Math.round(e*100)+"%";$("#luckysheet-zoom-ratioText").html(n),_y(e)}var Wh,an,wi=De(()=>{je();yt();dt();cl();Nr();_t();$t();ln();hr();Wh=null;an=null});function Ky(e,n,t={}){let{includeMergedCells:l=!0,includeStyledCells:a=!0,includeBorderedCells:o=!0,includeConditionalFormatting:s=!0,includeAlternateFormatting:u=!0}=t;if(d.config.rowhidden!=null&&d.config.rowhidden[e]!=null||d.config.colhidden!=null&&d.config.colhidden[n]!=null)return!1;let f=d.flowdata[e]&&d.flowdata[e][n];if(f!=null&&!me(f.v)||f!=null&&bl(f))return!0;if(l&&f!=null&&N(f)=="object"&&"mc"in f){if("rs"in f.mc)return!0;if(f.mc.r!=null&&f.mc.c!=null&&d.flowdata[f.mc.r]&&d.flowdata[f.mc.r][f.mc.c]&&!me(eo(f.mc.r,f.mc.c)))return!0}if(a&&f!=null){let A=be.checkstatus(d.flowdata,e,n,"bg");if(A!=null&&A!=="#FFFFFF")return!0;let h=be.checkstatus(d.flowdata,e,n,"fc");if(h!=null&&h!=="#000000")return!0;let g=be.checkstatus(d.flowdata,e,n,"bl"),p=be.checkstatus(d.flowdata,e,n,"it");if(g=="1"||p=="1")return!0;let v=be.checkstatus(d.flowdata,e,n,"fs");if(v!=null&&v!=="10")return!0;let y=be.checkstatus(d.flowdata,e,n,"ff");if(y!=null&&y!=="0"||f.tc!=null)return!0}if(o){let A=io(e,e,n,n);if(A==null)return!1;let h=e+"_"+n;if(A[h]){let g=A[h];if(g.l||g.r||g.t||g.b)return!0}}if(s)try{let A=Re.getComputeMap(),h=Re.checksCF(e,n,A);if(h!=null&&(h.cellColor!=null||h.textColor!=null||h.dataBar!=null||h.icons!=null))return!0}catch(A){console.warn("\u6761\u4EF6\u683C\u5F0F\u68C0\u67E5\u5931\u8D25:",A)}if(u)try{let A=Jt.getComputeMap(),h=Jt.checksAF(e,n,A);if(h!=null&&(h[0]!=null||h[1]!=null))return!0}catch(A){console.warn("\u4EA4\u66FF\u989C\u8272\u68C0\u67E5\u5931\u8D25:",A)}return!1}function Zy(e,n){let t=d.flowdata[e]&&d.flowdata[e][n];if(t!=null&&N(t)=="object"&&"mc"in t){if("rs"in t.mc)return{startRow:t.mc.r,endRow:t.mc.r+t.mc.rs-1,startCol:t.mc.c,endCol:t.mc.c+t.mc.cs-1};{let l=t.mc.r,a=t.mc.c,o=d.flowdata[l]&&d.flowdata[l][a];if(o&&o.mc&&"rs"in o.mc)return{startRow:o.mc.r,endRow:o.mc.r+o.mc.rs-1,startCol:o.mc.c,endCol:o.mc.c+o.mc.cs-1}}}return{startRow:e,endRow:e,startCol:n,endCol:n}}function Jy(e={}){let{startRow:n=0,endRow:t=d.visibledatarow.length-1,startCol:l=0,endCol:a=d.visibledatacolumn.length-1,cellOptions:o={}}=e,s=[],u=[],f=new Set;for(let A=n;A<=t;A++)for(let h=l;h<=a;h++)if(Ky(A,h,o)){let g=Zy(A,h),p=`${g.startRow}_${g.startCol}`;if(!f.has(p)){f.add(p);for(let v=g.startRow;v<=g.endRow;v++)s.includes(v)||s.push(v);for(let v=g.startCol;v<=g.endCol;v++)u.includes(v)||u.push(v)}}return s.sort((A,h)=>A-h),u.sort((A,h)=>A-h),{nonEmptyRows:s,nonEmptyCols:u}}function Qh(e={}){let t=qe(qe({},{includeMergedCells:!0,includeStyledCells:!0,includeBorderedCells:!0,includeConditionalFormatting:!0,includeAlternateFormatting:!0}),e);return Jy({cellOptions:t})}var _h=De(()=>{je();dt();Pt();Qr();Qt();ll();Pa();Ar();La()});function Kh(){let e=document.createElement("div");e.style.width="1mm",document.querySelector("body").appendChild(e);let t=e.getBoundingClientRect().width;return $(e).remove(),t}function $y(e){let n=210*Kh()*d.zoomRatio,t=297*Kh()*d.zoomRatio;return{paperWidth:n,paperHeight:t}}function e0(e,n){let t=ve.getSheetByIndex();t.config==null&&(t.config={}),t.config.sheetViewZoom==null&&(t.config.sheetViewZoom={});let l=1,a="zoomScaleNormal";e=="viewNormal"?a="viewNormalZoomScale":e=="viewLayout"?a="viewLayoutZoomScale":e=="viewPage"&&(a="viewPageZoomScale",l=.6);let o=t.config.sheetViewZoom[a];o==null&&(o=l),t.config.curentsheetView=e,d.clearjfundo&&d.jfredo.push({type:"viewChange",curType:e,preType:n,sheetIndex:d.currentSheetIndex}),ae.saveParam("cg",d.currentSheetIndex,e,{k:"curentsheetView"}),d.currentSheetView=e,jn(o)}function Iu(e,n,t,l){let a=[],o=[],{paperWidth:s,paperHeight:u}=$y();for(let f=e;f0?d.visibledatarow[f-1]:0,g=A,p=h;if(a.length>0){let b=a[a.length-1].rowIndex,k=d.visibledatarow[b];g=A-k,p=h-k}let v=Math.floor(g/u),y=Math.floor(p/u);v>y&&a.push({rowIndex:f,y:A,h:A-(a[a.length-1]?a[a.length-1].y:0)})}for(let f=t;f0?d.visibledatacolumn[f-1]:0,g=A,p=h;if(o.length>0){let b=o[o.length-1].colIndex,k=d.visibledatacolumn[b];g=A-k,p=h-k}let v=Math.floor(g/s),y=Math.floor(p/s);v>y&&o.push({colIndex:f,x:A,w:A-(o[o.length-1]?o[o.length-1].x:0)})}if(!a.find(f=>f.rowIndex===n)){let f=d.visibledatarow[n-1];a.push({rowIndex:n,y:f,h:f-(a[a.length-1]?a[a.length-1].y:0)})}if(!o.find(f=>f.colIndex===l)){let f=d.visibledatacolumn[l-1];o.push({colIndex:l,x:f,w:f-(o[o.length-1]?o[o.length-1].x:0)})}return{rows:a,columns:o}}function Zh(){if(d.flowdata==null)return;let e=$("#luckysheet-cell-main").scrollLeft(),n=$("#luckysheet-cell-main").scrollTop(),t=d.luckysheetTableContentHW[0],l=d.luckysheetTableContentHW[1],a=d.rowHeaderWidth,o=d.columnHeaderHeight,s=$("#luckysheetTableContent").get(0).getContext("2d");s.save(),s.scale(d.devicePixelRatio,d.devicePixelRatio);let{fill_col_ed:u,fill_row_ed:f}=l0(e,n,t,l),A=u-e,h=f-n,g=d.visibledatarow.length,p=d.visibledatacolumn.length,{rows:v,columns:y}=Iu(0,g,0,p),b=y.length;for(let k=0;k0?v[k-1].y:0,x=C-(C-I)/2-n;Jh(s,a,w.y-n+o,A+a,w.y-n+o);for(let T=0;T0?y[T-1].x:0,B=D-(D-S)/2-e+a;Jh(s,R.x-e+a,o,R.x-e+a,h+o),t0(s,B,x,k*b+T+1)}}s.restore()}function t0(e,n,t,l,a="18px Arial",o="#55bb8a"){e.save(),e.font=a,e.fillStyle=o;let s=`\u7B2C${l}\u9875`,u=d0(e,s),f=f0(e,s);e.fillText(s,n-u/2,t+f/2),e.restore()}function Jh(e,n,t,l,a,o=1,s="#55bb8a",u=[10,6]){e.save(),e.beginPath(),e.strokeStyle=s,e.lineWidth=o,e.setLineDash(u),e.moveTo(n,t),e.lineTo(l,a),e.stroke(),e.closePath(),e.restore()}function l0(e,n,t,l){let a=0,o=0,s,u,f,A;s=nt(d.visibledatarow,n),u=nt(d.visibledatarow,n+l),s==-1&&(s=0),s+=o,u==-1&&(u=d.visibledatarow.length-1),u+=o,u>=d.visibledatarow.length&&(u=d.visibledatarow.length-1),f=nt(d.visibledatacolumn,e),A=nt(d.visibledatacolumn,e+t),f==-1&&(f=0),f+=a,A==-1&&(A=d.visibledatacolumn.length-1),A+=a,A>=d.visibledatacolumn.length&&(A=d.visibledatacolumn.length-1);let h,g,p,v;return s==0?h=0:h=d.visibledatarow[s-1],g=d.visibledatarow[u],f==0?p=0:p=d.visibledatacolumn[f-1],v=d.visibledatacolumn[A],{dataset_row_st:s,dataset_row_ed:u,dataset_col_st:f,dataset_col_ed:A,fill_col_ed:v,fill_row_ed:g}}function r0(e,n){return Tl(this,null,function*(){let t=[],l=[],a=[],o=0,s=0,u=0,f=0,A=Qh({includeMergedCells:!0,includeStyledCells:!0,includeBorderedCells:!0,includeConditionalFormatting:!0,includeAlternateFormatting:!0});t.push(...A.nonEmptyRows),l.push(...A.nonEmptyCols),t.sort((p,v)=>p-v),l.sort((p,v)=>p-v);let{rows:h,columns:g}=Iu(t[0],t[t.length-1]+1,l[0],l[l.length-1]+1);for(let p=0;p").attr({width:Math.ceil(t*o),height:Math.ceil(l*o)}).css({width:t,height:l});Zl(e,n,t,l,1,1,null,null,s,o,1,{printGridLine:a});let u=s.get(0).getContext("2d");return yield n0(u,e,n,t,l),yield a0(u,e,n,t,l),s.get(0).toDataURL("image/png")})}function n0(e,n,t,l,a,o){return Tl(this,null,function*(){var f;Vn();let s=((f=d.luckysheetfile[Q(d.currentSheetIndex)])==null?void 0:f.chart)||{};if(!s||!s.length)return;let u=[];s.forEach(A=>{let{chart_id:h}=A,g=ho(A.left),p=ho(A.top),v=ho(A.width),y=ho(A.height);if(!Du({left:g,top:p,width:v,height:y,scrollHeight:t,scrollWidth:n,rh_height:a,ch_width:l}))return;let b=new Promise((k,w)=>{let C=$(`#${h}_c`);if(!C)return;let I=$(C).find("canvas").get(0);I||k();let x=I.toDataURL("image/png"),T=new Image;T.src=x,T.onload=()=>{e.save(),e.scale(o,o);let R=[20,10],D="#fff",S=10;e.fillStyle=D,e.fillRect(g,p,v,y),e.drawImage(T,g+R[1],p+R[0],v-2*R[1],y-2*R[0]),e.restore(),k()},T.onerror=k});u.push(b)}),yield Promise.all(u)})}function a0(e,n,t,l,a,o){return Tl(this,null,function*(){var g;($("#luckysheet-modal-dialog-activeImage").is(":visible")||$("#luckysheet-modal-dialog-cropping").is(":visible"))&&Se.cancelActiveImgItem();let s=((g=d.luckysheetfile[Q(d.currentSheetIndex)])==null?void 0:g.images)||{};if(!s||!Object.keys(s).length)return Promise.resolve();let u=[],f=d.rowHeaderWidth,A=d.columnHeaderHeight;for(let p in s){let v=s[p],{border:y,crop:b,isFixedPos:k,fixedLeft:w,fixedTop:C,src:I}=v,x=v.default.left*d.zoomRatio,T=v.default.top*d.zoomRatio,R=v.default.width*d.zoomRatio,D=v.default.height*d.zoomRatio;if(k){let S=$("#luckysheetTableContent").get(0),{x:B,y:F}=S.getBoundingClientRect(),E=(w-B-f)*d.zoomRatio,z=(C-F-A)*d.zoomRatio;if(Du({left:E,top:z,width:R,height:D,scrollHeight:t,scrollWidth:n,rh_height:a,ch_width:l})){let P=new Promise((L,O)=>{let q=new Image;q.src=I,q.onload=()=>{if(e.save(),e.scale(o,o),tm(b,v.default)){let j=b.offsetLeft*d.zoomRatio,G=b.offsetTop*d.zoomRatio,_=b.width*d.zoomRatio,ee=b.height*d.zoomRatio,re=E,oe=z,se=b.width*d.zoomRatio,ue=b.height*d.zoomRatio;e.drawImage(q,j,G,_,ee,re,oe,se,ue)}else{let j=E,G=z,_=R,ee=D;e.drawImage(q,j,G,_,ee)}if(y.width){let j=E-n,G=z-t;h(j,G,R,D,y)}e.restore()},L()});u.push(P)}}else if(Du({left:x,top:T,width:R,height:D,scrollHeight:t,scrollWidth:n,rh_height:a,ch_width:l})){let S=new Promise(B=>{let F=new Image;F.src=I,F.onload=()=>{if(e.save(),e.scale(o,o),tm(b,v.default)){let E=b.offsetLeft*d.zoomRatio,z=b.offsetTop*d.zoomRatio,P=b.width*d.zoomRatio,L=b.height*d.zoomRatio,O=x,q=T,j=b.width*d.zoomRatio,G=b.height*d.zoomRatio;e.drawImage(F,E,z,P,L,O,q,j,G)}else{let E=x,z=T,P=R,L=D;e.drawImage(F,E,z,P,L)}if(y.width){let E=(v.default.left-n)*d.zoomRatio,z=(v.default.top-t)*d.zoomRatio;h(E,z,R,D,y)}e.restore(),B()}});u.push(S)}}let h=(p,v,y,b,k)=>{e.save();let{width:w,style:C,color:I}=k;e.strokeStyle=I,e.lineWidth=w,e.beginPath(),e.moveTo(p,v),e.lineTo(p+y,v),e.lineTo(p+y,v+b),e.lineTo(p,v+b),e.closePath(),e.stroke(),e.restore()};yield Promise.all(u)})}function tm(e,n){return!(e.width===n.width&&e.height===n.height&&e.offsetLeft===0&&e.offsetTop===0)}function Du({scrollWidth:e,scrollHeight:n,ch_width:t,rh_height:l,left:a,top:o,width:s,height:u}){let f=a>=e&&o>=n&&a<=e+t&&o<=n+l,A=a+s>=e&&o>=n&&a+s<=e+t&&o<=n+l,h=a>=e&&o+u>=n&&a<=e+t&&o+u<=n+l,g=a+s>=e&&o+u>=n&&a+s<=e+t&&o+u<=n+l;return f||A||h||g}function lm(e){let n=e.parent(),t=n.find("luckysheet-print-viewBtn-active").attr("type");if(e.attr("type")==t)return;let l=e.attr("type");if(l!=null)e0(l,t);else return;e.parent().find(".luckysheet-print-viewBtn").removeClass("luckysheet-print-viewBtn-active"),e.addClass("luckysheet-print-viewBtn-active")}function rm(){let e=Ae.container,n=this;$("#"+e).find(".luckysheet-print-viewBtn").click(function(){lm($(this))})}function Is(e){if(!e||!e.length)return;let n=document.createElement("iframe");n.classList="print-preview-iframe",document.querySelector("body").append(n);let t=n.contentDocument;if(t==null)return;i0(t);let l=document.createElement("div");l.className="print-box",l.style.cssText=` - position: absolute; - top: 0; - left: 0; - box-sizing: border-box; - background: white; - border: solid transparent 0px; - z-index: 9999; - padding: 1px; - `,o0(l,e),t.body.append(l),s0(t),setTimeout(()=>{n.contentWindow.print(),n.remove()},100)}function i0(e){let n=document.createElement("style");n.media="print",n.innerText="@page { size: auto; }",e.head.append(n)}function o0(e,n){e.innerHTML=n.map(t=>` -
- -
`).join("")}function s0(e){let n=e.querySelector(".print-box"),{clientWidth:t,clientHeight:l}=n,a=e.createElement("canvas");a.width=t,a.height=l,a.style.cssText=` - position: absolute; - top: 0; - left: 0; - z-index: 10000; - width: ${t}px; - height: ${l}px; - `,e.body.append(a);let o=a.getContext("2d");o.save(),o.fillStyle="rgba(0,0,0,0.04)",o.font="bold 20px Arial";let s="Luckysheet-crdt",u=o.measureText(s).width,f=20,A=Math.sqrt(t*t+l*l),h=Math.max(u,f)*1.5;o.translate(t/2,l/2),o.rotate(-Math.PI/4);for(let g=-A;g',botton:'',style:"z-index:100003"}));let n=$("#luckysheet-print-page-dialog").find(".luckysheet-modal-dialog-content").css("min-width",260).end(),t=n.outerHeight(),l=n.outerWidth(),a=$(window).width(),o=$(window).height(),s=$(document).scrollLeft(),u=$(document).scrollTop();$("#luckysheet-print-page-dialog").css({left:(a+s-l)/2,top:(o+u-t)/3}).show(),setTimeout(()=>{$("#luckysheet-print-page-dialog input").focus()},10);function f(){var g,p;(g=$("#luckysheet-modal-dialog-mask"))==null||g.hide(),(p=$("#luckysheet-print-page-dialog"))==null||p.remove()}$("#luckysheet-print-page-dialog .luckysheet-modal-dialog-buttons .luckysheet-model-confirm-btn").click(function(){let p=$("#luckysheet-print-page-dialog input").val().trim().split(",").map(v=>v.split("\uFF0C")).reduce((v,y)=>v.concat(y),[]).map(Number);e&&typeof e=="function"&&e(p),f()})}function u0(e,n){let t=Qe,l=Mu(),o=l[l.length-1];if(n!=null)if(N(n)=="string"){if(!m.iscelldata(n))return U.info("The range parameter is invalid.","");o=n}else if(N(n)=="object"&&n.row!==null&&n.column!==null)o=di(n);else return U.info("The range parameter is invalid.","");t.rangeDialog(null,o),t.selectRange=[];let s=t.getRangeByTxt(o);if(m.rangetosheet=d.currentSheetIndex,s[0].sheetIndex!=d.currentSheetIndex&&ve.changeSheetExec(s[0].sheetIndex),s.length>0)for(let u=0;u -
\u6253\u5370\u9884\u89C8
- -
-
-
-
-
- \u6253\u5370\u8BBE\u7F6E -
-
- - -
- -
- - -
- -
- - -
- -
- -
-
-
- `),t.appendTo($("body")));let l=t.find(".luckysheet-print-preview-images");l.html(e.map(f=>`
- -
`).join(""));let a=210,o=297,s=$(".luckysheet-print-preview-images").height(),u=a/o*s;l.find("div").css({width:u,height:s}),t.find(".luckysheet-print-preview-box-header button").click(function(){t.remove()}),t.find("#grid-line").off("change").change(function(){return Tl(this,null,function*(){let f=$(this).prop("checked");if(n=="sheet")nm({printGridLine:f});else if(n=="areas"){let A=Ts.printSelection||d.luckysheet_select_save[0],h=[yield em({range:A},f)];To(h,"areas")}else if(n=="pages"){let A=Ts.pageArray||[],h=[];for(let g=0;gTl(this,[t],function*({range:n}){Ts.printSelection=n;let l=[yield em({range:n})];if(!e)return Is(l);To(l,"areas")}))})}function h0(e){return Tl(this,null,function*(){let n=$(".luckysheet-print-viewBtn.luckysheet-print-viewPage");lm(n),setTimeout(()=>{c0(t=>Tl(this,null,function*(){Ts.pageArray=t;let l=[];for(let a=0;a{yl();wi();cl();$t();El();je();Br();ll();Ao();Wt();el();mr();jt();en();dt();Pt();rl();wt();hr();qr();wn();_h();Ts={pageArray:[],printSelection:null}});function Eu(e,n,t){m.execFunctionExist=[];for(let l=0;l{w.chartType==="chartmix"?(d.currentChart=d.getChartJson(w.chart_id),vo(d.flowdata,n[p].row[0],n[p].row[1],n[p].column[0],n[p].column[1])):w.chartType==="vchart"&&xs(w,n[p].row[0],n[p].row[1],n[p].column[0],n[p].column[1])})}l&&Eu(n,d.currentSheetIndex,e),a&&(Hr=setTimeout(function(){Ue()},1)),at(),window.luckysheet_getcelldata_cache=null}function rr(e,n,t,l,a,o,s,u,f=!0){let A={},h=!1;if(clearTimeout(Hr),o=="cellRowChange"){A.type="cellRowChange",A.config=$.extend(!0,{},d.config),A.curconfig=$.extend(!0,{},l),A.range=$.extend(!0,[],d.luckysheet_select_save),A.currange=a,A.ctrlType=o,A.ctrlValue=s;let g=l.rowlen;g==null&&(g={}),ae.saveParam("cg",d.currentSheetIndex,g,{k:"rowlen"})}else if(o=="resizeC"){A.type="resize",A.config=$.extend(!0,{},d.config),A.curconfig=$.extend(!0,{},l),A.range=$.extend(!0,[],d.luckysheet_select_save),A.currange=a,A.ctrlType=o,A.ctrlValue=s;let g=l.columnlen;g==null&&(g={}),ae.saveParam("cg",d.currentSheetIndex,g,{k:"columnlen"})}else if(o.indexOf("extend")>-1)A.type="extend",A.config=$.extend(!0,{},d.config),A.curconfig=$.extend(!0,{},l),A.range=$.extend(!0,[],d.luckysheet_select_save),A.currange=a,A.ctrlType=o,A.ctrlValue=s,ae.saveParam("arc",d.currentSheetIndex,{index:s.index,len:s.len,direction:s.direction,mc:l.merge},{rc:s.type});else if(o.indexOf("dele")>-1)A.type="dele",A.config=$.extend(!0,{},d.config),A.curconfig=$.extend(!0,{},l),A.range=$.extend(!0,[],d.luckysheet_select_save),A.currange=a,A.ctrlType=o,A.ctrlValue=s,ae.saveParam("drc",d.currentSheetIndex,{index:s.index,len:s.len,mc:l.merge,borderInfo:l.borderInfo},{rc:s.type});else{A.type="datachangeAll",A.range=$.extend(!0,[],d.luckysheet_select_save),A.currange=a,A.ctrlType=o,A.ctrlValue=s,h=!0;for(let g=0;g0&&at(),h&&Eu(a,d.currentSheetIndex,t),Mt(n,e),f&&(Hr=setTimeout(function(){Ue()},1)),ve.storeSheetParamALL(),window.luckysheet_getcelldata_cache=null}function si(e,n,t){clearTimeout(Hr),d.clearjfundo&&(d.jfundo.length=0,d.jfredo.push({type:"rangechange",data:d.flowdata,curdata:e,range:n,sheetIndex:d.currentSheetIndex,cdformat:$.extend(!0,[],d.luckysheetfile[Q(d.currentSheetIndex)].luckysheet_conditionformat_save),curCdformat:t})),d.flowdata=e,Te.webWorkerFlowDataCache(d.flowdata),d.luckysheetfile[Q(d.currentSheetIndex)].data=d.flowdata,t!=null&&(d.luckysheetfile[Q(d.currentSheetIndex)].luckysheet_conditionformat_save=t),Eu(n,d.currentSheetIndex,e),Hr=setTimeout(function(){Ue()},1);for(let l=0;l0)for(let w=0;w0)for(let g=0;g0&&at(),d.luckysheetfile[Q(e.sheetIndex)].luckysheet_conditionformat_save=e.curCdformat,d.luckysheetfile[Q(n.sheetIndex)].luckysheet_conditionformat_save=n.curCdformat,d.currentSheetIndex==e.sheetIndex?Qe.dataVerification=e.curDataVerification:d.currentSheetIndex==n.sheetIndex&&(Qe.dataVerification=n.curDataVerification),d.luckysheetfile[Q(e.sheetIndex)].dataVerification=e.curDataVerification,d.luckysheetfile[Q(n.sheetIndex)].dataVerification=n.curDataVerification,m.execFunctionExist.reverse(),m.execFunctionGroup(null,null,null,null,n.curData),m.execFunctionGlobalData=null;let o=Q(d.currentSheetIndex),s=d.luckysheetfile[o];s.scrollTop=$("#luckysheet-cell-main").scrollTop(),s.scrollLeft=$("#luckysheet-cell-main").scrollLeft(),ve.showSheet(),Hr=setTimeout(function(){Ue()},1),ve.storeSheetParamALL(),ae.saveParam("all",e.sheetIndex,e.curConfig,{k:"config"}),ae.saveParam("all",n.sheetIndex,n.curConfig,{k:"config"}),ae.historyParam(e.curData,e.sheetIndex,{row:e.range.row,column:e.range.column}),ae.historyParam(n.curData,n.sheetIndex,{row:n.range.row,column:n.range.column}),ae.saveParam("all",e.sheetIndex,e.curCdformat,{k:"luckysheet_conditionformat_save"}),ae.saveParam("all",n.sheetIndex,n.curCdformat,{k:"luckysheet_conditionformat_save"}),ae.saveParam("all",e.sheetIndex,e.curDataVerification,{k:"dataVerification"}),ae.saveParam("all",n.sheetIndex,n.curDataVerification,{k:"dataVerification"})}function Mt(e,n,t=!0){Mn(e,n),clearTimeout(Hr),ve.storeSheetParam();let l=d.luckysheetfile[Q(d.currentSheetIndex)].calcChain;if(l!=null&&l.length>0){d.config.rowlen==null&&(d.config.rowlen={}),d.config.columnlen==null&&(d.config.columnlen={});for(let a=0;a0&&$("#luckysheet-filter-options-sheet"+d.currentSheetIndex+" .luckysheet-filter-options").each(function(a,o){let s=$(o).data("str"),u=$(o).data("cindex"),f=d.visibledatacolumn[u]-20,A=s-1==-1?0:d.visibledatarow[s-1];$(o).css({left:f,top:A})});if($("#luckysheet-filter-selected-sheet"+d.currentSheetIndex).length>0){let a=d.luckysheetfile[Q(d.currentSheetIndex)].filter_select,o=a.row[0],s=a.row[1],u=a.column[0],f=a.column[1],A=d.visibledatarow[s],h=o-1==-1?0:d.visibledatarow[o-1],g=d.visibledatacolumn[f],p=u-1==-1?0:d.visibledatacolumn[u-1];$("#luckysheet-filter-selected-sheet"+d.currentSheetIndex).css({left:p,width:g-p-1,top:h,height:A-h-1})}ve.showSheet(),t&&(Hr=setTimeout(function(){Ue()},1))}function Ue(e,n){if(m.groupValuesRefresh(),e==null&&(e=$("#luckysheet-cell-main").scrollLeft()),n==null&&(n=$("#luckysheet-cell-main").scrollTop()),Y.freezenverticaldata!=null||Y.freezenhorizontaldata!=null){let t,l,a,o,s,u,f=d.luckysheetTableContentHW[0],A=d.luckysheetTableContentHW[1];Y.freezenverticaldata!=null&&Y.freezenhorizontaldata!=null?(t=Y.freezenhorizontaldata[0],l=Y.freezenhorizontaldata[1],a=Y.freezenhorizontaldata[2],o=Y.freezenverticaldata[0],s=Y.freezenverticaldata[1],u=Y.freezenverticaldata[2],Zl(u,a,o,t,1,1,null,null,"freezen_3"),Zl(e+o-u,a,f-o+u,t,1,1,null,null,"freezen_4"),Zl(u,n+t-a,o,A-t+a,1,1,null,null,"freezen_7"),Zl(e+o-u,n+t-a,f-o+u,A-t+a,o-u+d.rowHeaderWidth,t-a+d.columnHeaderHeight),qa(u,o,d.rowHeaderWidth),qa(e+o-u,f-o+u,o-u+d.rowHeaderWidth),Va(a,t,d.columnHeaderHeight),Va(n+t-a,A-t+a,t-a+d.columnHeaderHeight)):Y.freezenhorizontaldata!=null?(t=Y.freezenhorizontaldata[0],l=Y.freezenhorizontaldata[1],a=Y.freezenhorizontaldata[2],Zl(e,a,f,t,1,1,null,null,"freezen_h"),Zl(e,n+t-a,f,A-t+a,null,t-a+d.columnHeaderHeight),qa(e,f,null),Va(a,t,d.columnHeaderHeight),Va(n+t-a,A-t+a,t-a+d.columnHeaderHeight)):Y.freezenverticaldata!=null&&(o=Y.freezenverticaldata[0],s=Y.freezenverticaldata[1],u=Y.freezenverticaldata[2],Zl(u,n,o,A,1,1,null,null,"freezen_v"),Zl(e+o-u,n,f-o+u,A,o-u+d.rowHeaderWidth,null),Va(n,A,null),qa(u,o,d.rowHeaderWidth),qa(e+o-u,f-o+u,o-u+d.rowHeaderWidth))}else{if($("#luckysheetTableContent").length==0)return;let t=$("#luckysheetTableContent").get(0).getContext("2d");Zl(e,n),qa(e),Va(n),t.clearRect(0,0,d.rowHeaderWidth*d.devicePixelRatio-1,d.columnHeaderHeight*d.devicePixelRatio-1),d.currentSheetView==="viewPage"&&Zh()}}var Hr,_t=De(()=>{lo();jt();tl();zr();Qt();wr();Ao();Kl();$t();cl();ln();mr();gi();el();zn();wt();je();Hn();qr();Io();Hr=null});function am(e,n,t){return new Promise((l,a)=>{let o=new XMLHttpRequest||new ActiveXObject("Microsoft.XMLHTTP");o.open(e,n),o.send(JSON.stringify(t)),o.onreadystatechange=function(){o.readyState==4&&(o.status==200?l(o.responseText):a("error"))}})}var im=De(()=>{});function m0(e,n,t,l,a,o,s,u,f){let A=n+"_c",h='',g=$(Ul(h,{id:A,addclass:"luckysheet-data-visualization-chart",title:"\u56FE\u8868\u751F\u6210",content:""})).appendTo($(".luckysheet-cell-main")),p=document.getElementById(A),{render:v,chart_json:y}=d.createChart($(`#${A}`).children(".luckysheet-modal-dialog-content")[0],t,n,l,a);o=It(chart.width),s=It(chart.height),f=It(chart.left),u=It(chart.top),p.style.width=o,p.style.height=s,p.style.position="absolute",p.style.background="#fff",p.style.left=f,p.style.top=u,v.style.width="100%",v.style.height="100%",p.style.zIndex=d.zIndex?d.zIndex:15,d.zIndex++;let b=d.luckysheetfile[Q(d.currentSheetIndex)];return b.chart||(b.chart=[]),b.chart.push({chart_id:n,width:o,height:s,left:f,top:u,sheetIndex:b.index}),Tr(n),$(`#${n}_c .luckysheet-modal-controll-del`).click(function(k){yo(n)}),ha(g),$(`#${n}_c .luckysheet-modal-controll-update`).click(function(k){bo()}),g.children(".luckysheet-modal-dialog-content").mousedown(function(k){d.chartparam.luckysheetCurrentChartMaxState||Tr(n),k.stopPropagation()}),g.mousedown(function(k){if(!d.chartparam.luckysheetCurrentChartMaxState){Tr(n),Rl(!0),!$(k.target).is(".luckysheet-modal-dialog-controll")&&!$(k.target).is(".luckysheet-modal-controll-btn")&&!$(k.target).is("i")&&(d.chartparam.luckysheetCurrentChartMoveTimeout=setTimeout(function(){d.chartparam.luckysheetCurrentChartMove=!0},100));var w=d.chartparam.luckysheetCurrentChartMoveObj.offset(),C=d.chartparam.luckysheetCurrentChartMoveObj.position();d.chartparam.luckysheetCurrentChartMoveXy=[k.pageX-w.left,k.pageY-w.top,C.left,C.top,$("#luckysheet-scrollbar-x").scrollLeft(),$("#luckysheet-scrollbar-y").scrollTop()],d.chartparam.luckysheetCurrentChartMoveWinH=$("#luckysheet-cell-main")[0].scrollHeight,d.chartparam.luckysheetCurrentChartMoveWinW=$("#luckysheet-cell-main")[0].scrollWidth,!$(k.target).hasClass("luckysheet-mousedown-cancel")&&$(k.target).filter("[class*='sp-palette']").length==0&&$(k.target).filter("[class*='sp-thumb']").length==0&&$(k.target).filter("[class*='sp-']").length==0&&($("#luckysheet-rightclick-menu").hide(),$("#luckysheet-cols-h-hover").hide(),$("#luckysheet-cols-menu-btn").hide(),$("#luckysheet-rightclick-menu").hide(),$("#luckysheet-sheet-list, #luckysheet-rightclick-sheet-menu, #luckysheet-user-menu").hide(),$("body > .luckysheet-filter-menu, body > .luckysheet-filter-submenu, body > .luckysheet-cols-menu").hide()),k.stopPropagation()}}).find(".luckysheet-modal-dialog-resize-item").mousedown(function(k){if(d.chartparam.luckysheetCurrentChartActive){d.chartparam.luckysheetCurrentChartResize=$(this).data("type");var w=Ke(k.pageX,k.pageY),C=$("#luckysheet-scrollbar-x").scrollLeft(),I=$("#luckysheet-scrollbar-y").scrollTop(),x=w[0]+C,T=w[1]+I,R=d.chartparam.luckysheetCurrentChartResizeObj.position();d.chartparam.luckysheetCurrentChartResizeXy=[x,T,g.width(),g.height(),R.left+C,R.top+I,C,I],d.chartparam.luckysheetCurrentChartResizeWinH=$("#luckysheet-cell-main")[0].scrollHeight,d.chartparam.luckysheetCurrentChartResizeWinW=$("#luckysheet-cell-main")[0].scrollWidth,d.chartparam.luckysheetCurrentChart=n,k.stopPropagation()}}),{chart_json:y}}function g0(e){let{chart_id:n,chartData:t}=e,{width:l,height:a,left:o,top:s}=e,u=JSON.parse(JSON.stringify(e.chartOptions)),f=n+"_c",A='',h=$(Ul(A,{id:f,addclass:"luckysheet-data-visualization-chart",title:"\u56FE\u8868\u751F\u6210",content:""})).appendTo($(".luckysheet-cell-main"));ha(h),$(`#${f}`).children(".luckysheet-modal-dialog-content")[0].id=n;let g=document.getElementById(f);d.chartparam.renderChart({chart_id:n,chartOptions:u}),d.currentChart=u,chartmix.default.insertToStore({chart_id:n,chartOptions:u}),Tr(n),$(`#${n}_c .luckysheet-modal-controll-del`).click(function(p){yo(n)}),$(`#${n}_c .luckysheet-modal-controll-update`).click(function(p){bo()}),h.children(".luckysheet-modal-dialog-content").mousedown(function(p){d.chartparam.luckysheetCurrentChartMaxState||Tr(n),p.stopPropagation()}),h.mousedown(function(p){if(!d.chartparam.luckysheetCurrentChartMaxState){Tr(n),Rl(!0),!$(p.target).is(".luckysheet-modal-dialog-controll")&&!$(p.target).is(".luckysheet-modal-controll-btn")&&!$(p.target).is("i")&&(d.chartparam.luckysheetCurrentChartMoveTimeout=setTimeout(function(){d.chartparam.luckysheetCurrentChartMove=!0},100));var v=d.chartparam.luckysheetCurrentChartMoveObj.offset(),y=d.chartparam.luckysheetCurrentChartMoveObj.position();d.chartparam.luckysheetCurrentChartMoveXy=[p.pageX-v.left,p.pageY-v.top,y.left,y.top,$("#luckysheet-scrollbar-x").scrollLeft(),$("#luckysheet-scrollbar-y").scrollTop()],d.chartparam.luckysheetCurrentChartMoveWinH=$("#luckysheet-cell-main")[0].scrollHeight,d.chartparam.luckysheetCurrentChartMoveWinW=$("#luckysheet-cell-main")[0].scrollWidth,!$(p.target).hasClass("luckysheet-mousedown-cancel")&&$(p.target).filter("[class*='sp-palette']").length==0&&$(p.target).filter("[class*='sp-thumb']").length==0&&$(p.target).filter("[class*='sp-']").length==0&&($("#luckysheet-rightclick-menu").hide(),$("#luckysheet-cols-h-hover").hide(),$("#luckysheet-cols-menu-btn").hide(),$("#luckysheet-rightclick-menu").hide(),$("#luckysheet-sheet-list, #luckysheet-rightclick-sheet-menu, #luckysheet-user-menu").hide(),$("body > .luckysheet-filter-menu, body > .luckysheet-filter-submenu, body > .luckysheet-cols-menu").hide()),p.stopPropagation()}}).find(".luckysheet-modal-dialog-resize-item").mousedown(function(p){if(d.chartparam.luckysheetCurrentChartActive){d.chartparam.luckysheetCurrentChartResize=$(this).data("type");var v=Ke(p.pageX,p.pageY),y=$("#luckysheet-scrollbar-x").scrollLeft(),b=$("#luckysheet-scrollbar-y").scrollTop(),k=v[0]+y,w=v[1]+b,C=d.chartparam.luckysheetCurrentChartResizeObj.position();d.chartparam.luckysheetCurrentChartResizeXy=[k,w,h.width(),h.height(),C.left+y,C.top+b,y,b],d.chartparam.luckysheetCurrentChartResizeWinH=$("#luckysheet-cell-main")[0].scrollHeight,d.chartparam.luckysheetCurrentChartResizeWinW=$("#luckysheet-cell-main")[0].scrollWidth,d.chartparam.luckysheetCurrentChart=n,p.stopPropagation()}}),l=It(l),a=It(a),o=It(o),s=It(s),g.style.width=l,g.style.height=a,g.style.position="absolute",g.style.background="#fff",g.style.left=o,g.style.top=s,g.style.zIndex=d.zIndex?d.zIndex:15,d.zIndex++}function p0(e){let{chart_id:n,chartOptions:t}=e,{left:l,top:a,width:o,height:s}=e,u=n+"_c",f=document.getElementById(u);l&&(l=It(l),f.style.left=l),a&&(a=It(a),f.style.top=a),o&&(o=It(o),f.style.width=o),s&&(s=It(s),f.style.height=s),ja(d.currentSheetIndex,n,l,a,o,s),t&&d.chartparam.renderChart({chart_id:n,chartOptions:t}),d.resizeChart(n)}function v0(e){$(`.luckysheet-cell-main #${e}_c`).remove(),Vn();let n=d.luckysheetfile[Q(d.currentSheetIndex)],t=n.chart.findIndex(l=>l.chart_id==e);n.chart.splice(t,1)}var Un,Bu=De(()=>{je();wn();wt();Wr();qr();El();je();Un={createChart:m0,updateChart:p0,deleteChart:v0,renderChart:g0}});function y0(e,n,t,l,a,o,s,u,f){if(!bi())return;let A=n+"_c",h='',g=$(Ul(h,{id:A,addclass:"luckysheet-data-visualization-chart",title:"\u56FE\u8868\u751F\u6210",content:""})).appendTo($(".luckysheet-cell-main")),p=$(`#${A}`).children(".luckysheet-modal-dialog-content")[0];p.id=n;let v=document.getElementById(A),y=wo(t),b=y[0],k=y[1],w=ko(t,l,k,b),C=getVChartOption(w),I={spec:C,rangeColCheck:k,rangeRowCheck:b,chart_id:n,rangeSplitArray:w,rangeArray:l,rangeTxt:a},x=new VChart.default(C,{dom:p});x.renderSync(),o=It(o||400),s=It(s||250),f=It(f||0),u=It(u||0),v.style.width=o,v.style.height=s,v.style.position="absolute",v.style.background="#fff",v.style.left=f,v.style.top=u,v.style.zIndex=d.zIndex?d.zIndex:15,d.zIndex++;let T=d.luckysheetfile[Q(d.currentSheetIndex)];T.chart||(T.chart=[]),T.chart.push({chartType:"vchart",chart_id:n,width:o,height:s,left:f,top:u,sheetIndex:T.index,chartOptions:I}),ha(g),$(`#${n}_c .luckysheet-modal-controll-del`).click(function(R){xo(n)}),$(`#${n}_c .luckysheet-modal-controll-update`).click(function(R){Co(x,n)}),g.children(".luckysheet-modal-dialog-content").mousedown(function(R){d.chartparam.luckysheetCurrentChartMaxState||nn(n),R.stopPropagation()}),g.mousedown(function(R){if(!d.chartparam.luckysheetCurrentChartMaxState){nn(n),Rl(!0),!$(R.target).is(".luckysheet-modal-dialog-controll")&&!$(R.target).is(".luckysheet-modal-controll-btn")&&!$(R.target).is("i")&&(d.chartparam.luckysheetCurrentChartMoveTimeout=setTimeout(function(){d.chartparam.luckysheetCurrentChartMove=!0},100));var D=d.chartparam.luckysheetCurrentChartMoveObj.offset(),S=d.chartparam.luckysheetCurrentChartMoveObj.position();d.chartparam.luckysheetCurrentChartMoveXy=[R.pageX-D.left,R.pageY-D.top,S.left,S.top,$("#luckysheet-scrollbar-x").scrollLeft(),$("#luckysheet-scrollbar-y").scrollTop()],d.chartparam.luckysheetCurrentChartMoveWinH=$("#luckysheet-cell-main")[0].scrollHeight,d.chartparam.luckysheetCurrentChartMoveWinW=$("#luckysheet-cell-main")[0].scrollWidth,!$(R.target).hasClass("luckysheet-mousedown-cancel")&&$(R.target).filter("[class*='sp-palette']").length==0&&$(R.target).filter("[class*='sp-thumb']").length==0&&$(R.target).filter("[class*='sp-']").length==0&&($("#luckysheet-rightclick-menu").hide(),$("#luckysheet-cols-h-hover").hide(),$("#luckysheet-cols-menu-btn").hide(),$("#luckysheet-rightclick-menu").hide(),$("#luckysheet-sheet-list, #luckysheet-rightclick-sheet-menu, #luckysheet-user-menu").hide(),$("body > .luckysheet-filter-menu, body > .luckysheet-filter-submenu, body > .luckysheet-cols-menu").hide()),R.stopPropagation()}}).find(".luckysheet-modal-dialog-resize-item").mousedown(function(R){if(d.chartparam.luckysheetCurrentChartActive){d.chartparam.luckysheetCurrentChartResize=$(this).data("type");var D=Ke(R.pageX,R.pageY),S=$("#luckysheet-scrollbar-x").scrollLeft(),B=$("#luckysheet-scrollbar-y").scrollTop(),F=D[0]+S,E=D[1]+B,z=d.chartparam.luckysheetCurrentChartResizeObj.position();d.chartparam.luckysheetCurrentChartResizeXy=[F,E,g.width(),g.height(),z.left+S,z.top+B,S,B],d.chartparam.luckysheetCurrentChartResizeWinH=$("#luckysheet-cell-main")[0].scrollHeight,d.chartparam.luckysheetCurrentChartResizeWinW=$("#luckysheet-cell-main")[0].scrollWidth,d.chartparam.luckysheetCurrentChart=n,R.stopPropagation()}})}function b0(e){if(!bi())return;let{chart_id:n,chartData:t,chartOptions:l}=e,a=n+"_c",o='',s=$(Ul(o,{id:a,addclass:"luckysheet-data-visualization-chart",title:"\u56FE\u8868\u751F\u6210",content:""})).appendTo($(".luckysheet-cell-main"));ha(s);let u=$(`#${a}`).children(".luckysheet-modal-dialog-content")[0];u.id=n;let f=document.getElementById(a),A=new VChart.default(l.spec,{dom:u});A.renderSync(),d.currentChart=l,Cu(n,A),nn(n),$(`#${n}_c .luckysheet-modal-controll-del`).click(function(y){xo(n)}),$(`#${n}_c .luckysheet-modal-controll-update`).click(function(y){Co(A,n)}),s.children(".luckysheet-modal-dialog-content").mousedown(function(y){d.chartparam.luckysheetCurrentChartMaxState||nn(n),y.stopPropagation()}),s.mousedown(function(y){if(!d.chartparam.luckysheetCurrentChartMaxState){nn(n),Rl(!0),!$(y.target).is(".luckysheet-modal-dialog-controll")&&!$(y.target).is(".luckysheet-modal-controll-btn")&&!$(y.target).is("i")&&(d.chartparam.luckysheetCurrentChartMoveTimeout=setTimeout(function(){d.chartparam.luckysheetCurrentChartMove=!0},100));var b=d.chartparam.luckysheetCurrentChartMoveObj.offset(),k=d.chartparam.luckysheetCurrentChartMoveObj.position();d.chartparam.luckysheetCurrentChartMoveXy=[y.pageX-b.left,y.pageY-b.top,k.left,k.top,$("#luckysheet-scrollbar-x").scrollLeft(),$("#luckysheet-scrollbar-y").scrollTop()],d.chartparam.luckysheetCurrentChartMoveWinH=$("#luckysheet-cell-main")[0].scrollHeight,d.chartparam.luckysheetCurrentChartMoveWinW=$("#luckysheet-cell-main")[0].scrollWidth,!$(y.target).hasClass("luckysheet-mousedown-cancel")&&$(y.target).filter("[class*='sp-palette']").length==0&&$(y.target).filter("[class*='sp-thumb']").length==0&&$(y.target).filter("[class*='sp-']").length==0&&($("#luckysheet-rightclick-menu").hide(),$("#luckysheet-cols-h-hover").hide(),$("#luckysheet-cols-menu-btn").hide(),$("#luckysheet-rightclick-menu").hide(),$("#luckysheet-sheet-list, #luckysheet-rightclick-sheet-menu, #luckysheet-user-menu").hide(),$("body > .luckysheet-filter-menu, body > .luckysheet-filter-submenu, body > .luckysheet-cols-menu").hide()),y.stopPropagation()}}).find(".luckysheet-modal-dialog-resize-item").mousedown(function(y){if(d.chartparam.luckysheetCurrentChartActive){d.chartparam.luckysheetCurrentChartResize=$(this).data("type");var b=Ke(y.pageX,y.pageY),k=$("#luckysheet-scrollbar-x").scrollLeft(),w=$("#luckysheet-scrollbar-y").scrollTop(),C=b[0]+k,I=b[1]+w,x=d.chartparam.luckysheetCurrentChartResizeObj.position();d.chartparam.luckysheetCurrentChartResizeXy=[C,I,s.width(),s.height(),x.left+k,x.top+w,k,w],d.chartparam.luckysheetCurrentChartResizeWinH=$("#luckysheet-cell-main")[0].scrollHeight,d.chartparam.luckysheetCurrentChartResizeWinW=$("#luckysheet-cell-main")[0].scrollWidth,d.chartparam.luckysheetCurrentChart=n,y.stopPropagation()}});let h=It(e.width),g=It(e.height),p=It(e.left),v=It(e.top);f.style.width=h,f.style.height=g,f.style.position="absolute",f.style.background="#fff",f.style.left=p,f.style.top=v,f.style.zIndex=d.zIndex?d.zIndex:15,d.zIndex++}function k0(e){if(!bi())return;let{chart_id:n,chartOptions:t}=e,{left:l,top:a,width:o,height:s}=e,u=n+"_c",f=document.getElementById(u);l&&(l=It(l),f.style.left=l),a&&(a=It(a),f.style.top=a),o&&(o=It(o),f.style.width=o),s&&(s=It(s),f.style.height=s),ja(d.currentSheetIndex,n,l,a,o,s)}function w0(e){$(`.luckysheet-cell-main #${e}_c`).remove(),Vn();let n=d.luckysheetfile[Q(d.currentSheetIndex)],t=n.chart.findIndex(l=>l.chart_id==e);n.chart.splice(t,1)}var Ds,om=De(()=>{je();wn();wt();Wr();qr();Hn();El();xu();Hn();je();Ds={createVChart:y0,renderVChart:b0,updateVChart:k0,deleteVChart:w0}});var Pu,So,Xa,ae,$t=De(()=>{Pu=Ol(qA());jc();_t();tl();Wt();cl();ll();zn();Kl();ln();hr();mr();gi();dt();wt();je();el();yt();So=Ol(En());ui();yl();im();Xl();Bu();om();Hn();qr();fi();Xa={gridKey:null,loadUrl:null,updateUrl:null,updateImageUrl:null,title:null,loadSheetUrl:null,retryTimer:null,allowUpdate:!1,historyParam:function(e,n,t){let l=this,a=t.row[0],o=t.row[1],s=t.column[0],u=t.column[1];if(a==o&&s==u){let f=e[a][s];l.saveParam("v",n,f,{r:a,c:s})}else{let f=o-a+1,A=u-s+1,h=Math.floor(1e3/A),g=Math.ceil(f/h);for(let p=0;p{console.log(u)}).catch(u=>{console.log(u)});else{let u=Pu.default.gzip(encodeURIComponent(JSON.stringify(o)),{to:"string"});a.websocket!=null&&a.websocket.send(u)}},websocket:null,wxErrorCount:0,openWebSocket:function(){let e=this,n=new Date().getTime();if("WebSocket"in window){let t=e.updateUrl+"?t="+n+"&g="+encodeURIComponent(e.gridKey);e.updateUrl.indexOf("?")>-1&&(t=e.updateUrl+"&t="+n+"&g="+encodeURIComponent(e.gridKey)),e.websocket=new WebSocket(t),e.websocket.onopen=function(){console.info(te().websocket.success),Ki(),e.wxErrorCount=0,e.retryTimer=setInterval(function(){e.websocket.send("rub")},6e4)},e.websocket.onmessage=function(l){d.result=l;let a=new Function("return "+l.data)();$e.createHookFunction("cooperativeMessage",a);let o=a.type,{message:s,id:u}=a,f=u;if(s==="\u7528\u6237\u9000\u51FA"&&($("#luckysheet-multipleRange-show-"+u).hide(),d.cooperativeEdit.changeCollaborationSize=d.cooperativeEdit.changeCollaborationSize.filter(A=>A.id!=u),d.cooperativeEdit.checkoutData=d.cooperativeEdit.checkoutData.filter(A=>A.id!=u)),o==1){let A=a.data.v.index,h=d.luckysheetfile.filter(g=>g.index===A)[0];h!==null&&setTimeout(()=>{let g=a.data.i;h.index=g,d.currentSheetIndex=g,$(`#luckysheet-sheets-item${A}`).attr("data-index",g),$(`#luckysheet-sheets-item${A}`).prop("id",`luckysheet-sheets-item${g}`),$(`#luckysheet-datavisual-selection-set-${A}`).prop("id",`luckysheet-datavisual-selection-set-${g}`)},1)}else if(o==2){let A=JSON.parse(a.data);e.wsUpdateMsg(A,f);let h=JSON.parse(a.data);h.k=="columnlen"?Xn(h.v,null):h.k=="rowlen"&&Xn(null,h.v)}else if(o==3){let A=a.id,h=a.username,g=JSON.parse(a.data),p=g.t,v=g.i,y=g.v;d.cooperativeEdit.changeCollaborationSize.length===0&&d.cooperativeEdit.changeCollaborationSize.push({id:A,v:g.v[0],i:v}),d.cooperativeEdit.changeCollaborationSize.some(I=>I.id==A)?d.cooperativeEdit.changeCollaborationSize.forEach(I=>{I.id==A&&(I.v=g.v[0]||g.v.range[0]||g.range[0],I.i=v)}):d.cooperativeEdit.changeCollaborationSize.push({id:A,v:g.v[0],i:v}),N(y)!="array"&&N(y)!=="object"&&(y=JSON.parse(y));let k=0,w=0;if(v==d.currentSheetIndex?N(y)==="object"&&y.op==="enterEdit"?(k=y.range[y.range.length-1].row[0],w=y.range[y.range.length-1].column[0],e.multipleRangeShow(A,h,k,w,y.op)):(k=y[y.length-1].row[0],w=y[y.length-1].column[0],e.multipleRangeShow(A,h,k,w)):N(y)==="object"&&y.op==="enterEdit"?(k=y.range[y.range.length-1].row[0],w=y.range[y.range.length-1].column[0]):(k=y[y.length-1].row[0],w=y[y.length-1].column[0]),d.cooperativeEdit.checkoutData.length===0&&(y.op?d.cooperativeEdit.checkoutData.push({id:A,username:h,r:k,c:w,op:y.op,index:v}):d.cooperativeEdit.checkoutData.push({id:A,username:h,r:k,c:w,index:v})),d.cooperativeEdit.checkoutData.some(I=>I.id==A)?d.cooperativeEdit.checkoutData.forEach(I=>{I.id==A&&(I.username=h,I.r=k,I.c=w,I.index=v,y.op==="enterEdit"&&(I.op=y.op))}):y.op==="enterEdit"?d.cooperativeEdit.checkoutData.push({id:A,username:h,r:k,c:w,op:y.op,index:v}):d.cooperativeEdit.checkoutData.push({id:A,username:h,r:k,c:w,index:v}),d.cooperativeEdit.checkoutData.forEach(I=>{I.index!=d.currentSheetIndex&&($("#luckysheet-multipleRange-show-"+I.id).hide(),I.op=="")}),$("#luckysheet-multipleRange-show-"+A)[0]){let I=$("#luckysheet-multipleRange-show-"+A)[0].offsetHeight-1;$("#luckysheet-multipleRange-show-"+A+">.username").css({bottom:I+"px"})}d.cooperativeEdit.merge_range.length&&d.cooperativeEdit.merge_range.find(I=>I.userid===f)&&(d.cooperativeEdit.merge_range=d.cooperativeEdit.merge_range.filter(I=>I.userid!==f)),Xn()}else if(o==4){let A=a.data===""?a.data:JSON.parse(a.data);for(let h=0;h3)ra(te().websocket.refresh);else{if(ra(te().websocket.wait),this.websocket)return;e.openWebSocket()}},e.websocket.onclose=function(l){console.info(te().websocket.close),l.code===1e3?(clearInterval(e.retryTimer),e.retryTimer=null):(e.wxErrorCount++,e.wxErrorCount>3?console.error(te().websocket.contact):(console.info("\u8FDE\u63A5\u5173\u95ED\uFF0C\u6B63\u5728\u91CD\u8BD5..."),e.openWebSocket()))}}else console.error(te().websocket.support)},wsUpdateMsg:function(e,n){var s,u,f,A,h,g;let t=e.t,l=e.i,a=e.v,o=d.luckysheetfile[Q(l)];if(!(["v","rv","cg","all","fc","drc","arc","f","fsc","fsr","sh","c"].includes(t)&&o==null))if(t=="v"){if(o.data==null||o.data.length==0)return;let p=e.r,v=e.c;o.data[p][v]=a,l==d.currentSheetIndex&&(d.flowdata=o.data,Te.webWorkerFlowDataCache(d.flowdata),a!=null&&a.ps!=null?He.buildPs(p,v,a.ps):He.buildPs(p,v,null),setTimeout(function(){Ue()},1))}else if(t=="rv"){if(Object.keys(e.range).length>0&&((!d.cooperativeEdit.merge_range||!d.cooperativeEdit.merge_range.length)&&(d.cooperativeEdit.merge_range=[]),d.cooperativeEdit.merge_range.push({range:e.range,v:e.v,userid:n}),Xn()),o.data==null||o.data.length==0)return;let p=e.range.row[0],v=e.range.row[1],y=e.range.column[0],b=e.range.column[1];for(let k=p;k<=v;k++)for(let w=y;w<=b;w++)o.data[k][w]=a[k-p][w-y];if(l==d.currentSheetIndex){d.flowdata=o.data,Te.webWorkerFlowDataCache(d.flowdata);for(let k=p;k<=v;k++)for(let w=y;w<=b;w++)a[k-p][w-y]!=null&&a[k-p][w-y].ps!=null?He.buildPs(k,w,a[k-p][w-y].ps):He.buildPs(k,w,null);setTimeout(function(){Ue()},1)}}else if(t=="cg"){let p=e.k;p=="borderInfo"?o.config.borderInfo=a:(p in o.config||(o.config[p]={}),a&&typeof a=="object"&&(o.config[p]=a)),l==d.currentSheetIndex&&(d.config=o.config,(p=="rowlen"||p=="columnlen"||p=="rowhidden")&&Mt(d.flowdata.length,d.flowdata[0].length),setTimeout(function(){Ue()},1))}else if(t=="all"){let p=e.k;if(o[p]=a,p=="name")$("#luckysheet-sheet-container-c #luckysheet-sheets-item"+l).find("span.luckysheet-sheets-item-name").html(a);else if(p=="color"){let v=$("#luckysheet-sheet-container-c #luckysheet-sheets-item"+l);v.find(".luckysheet-sheets-item-color").remove(),(a!=null||a!="")&&v.append('
')}else if(p!="pivotTable")if(p=="frozen"){if(Y.frozenTofreezen(),l==d.currentSheetIndex){let y=te().freezen;o.freezen.horizontal==null?($("#luckysheet-freezen-btn-horizontal").html(' '+y.freezenRow),Y.freezenhorizontaldata=null,$("#luckysheet-freezebar-horizontal").hide()):Y.createFreezenHorizontal(o.freezen.horizontal.freezenhorizontaldata,o.freezen.horizontal.top),o.freezen.vertical==null?($("#luckysheet-freezen-btn-vertical").html(' '+y.freezenColumn),Y.freezenverticaldata=null,$("#luckysheet-freezebar-vertical").hide()):Y.createFreezenVertical(o.freezen.vertical.freezenverticaldata,o.freezen.vertical.left),Y.createAssistCanvas()}}else p=="filter_select"?l==d.currentSheetIndex&&Ll(a):p=="filter"?l==d.currentSheetIndex&&Ll(o.filter_select,a):p=="luckysheet_conditionformat_save"?l==d.currentSheetIndex&&setTimeout(function(){Ue()},1):p=="luckysheet_alternateformat_save"?l==d.currentSheetIndex&&setTimeout(function(){Ue()},1):p=="config"?l==d.currentSheetIndex&&(d.config=a,Mt(d.flowdata.length,d.flowdata[0].length)):p=="dynamicArray"?l==d.currentSheetIndex&&setTimeout(function(){Ue()},1):p=="images"?l==d.currentSheetIndex&&(Se.images=a,Se.allImagesShow(),Se.init()):p=="dataVerification"?l==d.currentSheetIndex&&(Qe.dataVerification=a,Qe.init()):p=="hyperlink"&&l==d.currentSheetIndex&&(Sl.hyperlink=a,Sl.init())}else if(t=="fc"){let p=e.op,v=e.pos;N(a)!="object"&&(a=new Function("return "+a)());let y=a.r,b=a.c,k=o.calcChain==null?[]:o.calcChain;if(p=="add")k.push(a);else if(p=="del")for(let w=0;w addData",b),p=="r"){o.row+=y;let x=[];for(let R=0;R'),$("#luckysheet-sheet-container-c").append(we(sr,{index:a.index,active:"",name:a.name,style:"",colorset:p})),$("#luckysheet-cell-main").append('
'),ve.locationSheet()}else if(t=="shc"){let p=a.copyindex,v=a.name,y=Q(p),b=$.extend(!0,{},d.luckysheetfile[y]);b.index=l,b.name=v,d.luckysheetfile.splice(y+1,0,b);let k=$("#luckysheet-sheets-item"+p);$("#luckysheet-sheet-container-c").append(we(sr,{index:b.index,active:"",name:b.name,style:"",colorset:""})),$("#luckysheet-sheets-item"+b.index).insertAfter(k),$("#luckysheet-cell-main").append('
')}else if(t=="shd"){for(let p=0;p0?b=b.eq(0).data("index"):b=y.prevAll(":visible").eq(0).data("index"),$("#luckysheet-sheets-item"+b).addClass("luckysheet-sheets-item-active"),ve.changeSheetExec(b)}Xa.sheetDeleSave.push(d.luckysheetfile[p]),d.luckysheetfile.splice(p,1);break}$("#luckysheet-sheets-item"+a.deleIndex).remove(),$("#luckysheet-datavisual-selection-set-"+a.deleIndex).remove(),ve.locationSheet()}else if(t=="shr"){for(let y in a)d.luckysheetfile[Q(y)].order=a[y];let p={};(d.luckysheetfile||[]).forEach(y=>{p[y.index.toString()]=y.order}),d.luckysheetfile.sort((y,b)=>{let k=p[y.index.toString()],w=p[b.index.toString()];return k!=null&&w!=null?k-w:k!=null?-1:(w!=null,1)});let v={};d.luckysheetfile.forEach((y,b,k)=>{if(k[b].order=b,v[y.index.toString()]=b,b>0){let w=k[b-1].index;$("#luckysheet-sheets-item"+y.index).insertAfter($("#luckysheet-sheets-item"+w))}})}else if(t=="shre"){for(let p=0;p'),$("#luckysheet-sheet-container-c").append(we(sr,{index:v.index,active:"",name:v.name,style:"",colorset:y})),$("#luckysheet-cell-main").append('
');break}}else if(t=="sh"){let p=e.op,v=e.cur;p=="hide"?(o.hide=1,$("#luckysheet-sheets-item"+l).hide(),l==d.currentSheetIndex&&($("#luckysheet-sheets-item"+v).addClass("luckysheet-sheets-item-active"),ve.changeSheetExec(v))):p=="show"&&(o.hide=0,$("#luckysheet-sheets-item"+l).show()),ve.locationSheet()}else if(t=="c"){let p=e.op,v=e.cid;if(p=="add")o.chart?o.chart.push(a):o.chart=[a],((s=e==null?void 0:e.v)==null?void 0:s.chartType)==="vchart"?Ds.renderVChart(a):((u=e==null?void 0:e.v)==null?void 0:u.chartType)==="chartmix"&&Un.renderChart(a);else if(p=="xy"||p=="wh"||p=="update"){((f=e==null?void 0:e.v)==null?void 0:f.chartType)==="vchart"?Ds.updateVChart(a):((A=e==null?void 0:e.v)==null?void 0:A.chartType)==="chartmix"&&Un.updateChart(a);for(let y=0;y{x.chartType==="chartmix"?(d.currentChart=d.getChartJson(x.chart_id),vo(d.flowdata,y,b,k,w,!0)):x.chartType==="vchart"&&xs(x,y,b,k,w,!0)})}}else t=="na"&&$("#luckysheet_info_detail_input").val(a).css("width",Ua(a)*10)},multipleIndex:0,multipleRangeShow:function(e,n,t,l,a){let o=this,s=n,u=d.visibledatarow[t],f=t-1==-1?0:d.visibledatarow[t-1],A=d.visibledatacolumn[l],h=l-1==-1?0:d.visibledatacolumn[l-1],g=be.mergeborer(d.flowdata,t,l);if(g&&(u=g.row[1],f=g.row[0],A=g.column[1],h=g.column[0]),h<=0&&(h+=1),f<=0&&(f+=1),Ua(n)>16&&(n=Ua(n,16)+"..."),a==="enterEdit"&&(n+=" "+te().edit.typing),$("#luckysheet-multipleRange-show-"+e).length>0)$("#luckysheet-multipleRange-show-"+e).css({position:"absolute",left:h-1,width:A-h-1,top:f-1,height:u-f-1}),$("#luckysheet-multipleRange-show-"+e+" .username").text(n),$("#luckysheet-multipleRange-show-"+e+" .username").show(),d.cooperativeEdit.usernameTimeout["user"+e]!=null&&clearTimeout(d.cooperativeEdit.usernameTimeout["user"+e]),d.cooperativeEdit.usernameTimeout["user"+e]=setTimeout(()=>{clearTimeout(d.cooperativeEdit.usernameTimeout["user"+e]),d.cooperativeEdit.usernameTimeout["user"+e]=null},10*1e3);else{let p=`
- -
- ${n} -
- -
-
- -
`;$(p).appendTo($("#luckysheet-cell-main #luckysheet-multipleRange-show")),o.multipleIndex++,d.cooperativeEdit.usernameTimeout["user"+e]!=null&&clearTimeout(d.cooperativeEdit.usernameTimeout["user"+e]),d.cooperativeEdit.usernameTimeout["user"+e]=setTimeout(()=>{clearTimeout(d.cooperativeEdit.usernameTimeout["user"+e]),d.cooperativeEdit.usernameTimeout["user"+e]=null},10*1e3)}},sheetDeleSave:[],submitInterval:1e3,imagesubmitInterval:5e3,submitdatalimit:50,submitcompresslimit:1e3,checksubmit:function(e){let n=this;n.submitTimeout(),clearTimeout(n.imageRequestTimeout),n.imageRequestTimeout=setTimeout(function(){n.imageRequest()},n.imagesubmitInterval)},submitTimeout:function(){let e=this;clearTimeout(e.requestTimeOut),!e.requestLock&&e.requestlast!=null&&e.requestlast.clone().add(1,"seconds").isBefore((0,So.default)())&&e.request(),e.requestTimeOut=setTimeout(function(){e.submitTimeout()},e.submitInterval)},requestLock:!1,requestlast:null,firstchange:!0,requestTimeOut:null,request:function(){let e=this,t=this.gridKey+"__qkcache";e.cachelocaldata(function(l,a){if(a.length==0)return;a=encodeURIComponent(JSON.stringify(a));let o=a.length,s=!1;e.requestLock=!0,e.updateUrl!=""&&$.post(e.updateUrl,{compress:s,gridKey:e.gridKey,data:a},function(u){new Function("return "+u)().status?($("#luckysheet_info_detail_update").html("\u6700\u8FD1\u5B58\u6863\u65F6\u95F4:"+(0,So.default)().format("M-D H:m:s")),$("#luckysheet_info_detail_save").html("\u540C\u6B65\u6210\u529F"),e.clearcachelocaldata()):($("#luckysheet_info_detail_save").html("\u540C\u6B65\u5931\u8D25"),e.restorecachelocaldata()),e.requestlast=(0,So.default)(),e.requestLock=!1})})},imageRequestLast:null,imageRequestLock:!1,imageRequestTimeout:null,imageRequest:function(){let e=this;html2canvas($("#"+container).find(".luckysheet-grid-window").get(0),{onrendered:function(n){let t=$(n).appendTo("body");t.hide();let l=t.width(),a=t.height(),o=t.get(0).getContext("2d").getImageData(0,0,l,a),s=l,u=a;s*.54>u?s=u/.54:u=s*.54;let f=$("").attr("width",s).attr("height",u)[0];f.getContext("2d").putImageData(o,0,0),t.attr("width",350),t.attr("height",189),t.get(0).getContext("2d").drawImage(f,0,0,350,189);let A=t.get(0).toDataURL("image/jpeg",.9),h=luckysheet.sheetmanage.getCurSheetnoset();e.imageRequestLock=!0;let g=encodeURIComponent(JSON.stringify({t:"thumb",img:A,curindex:h}));t.remove(),e.updateImageUrl!=""&&Lr({url:e.updateImageUrl,headers:Ae.requestHeaders,data:{compress:!0,gridKey:e.gridKey,data:g},success:function(p){new Function("return "+p)().status?imageRequestLast=(0,So.default)():$("#luckysheet_info_detail_save").html("\u7F51\u7EDC\u4E0D\u7A33\u5B9A"),e.imageRequestLock=!0}})}})},localdata:[],matchOpt:function(e,n){for(let t in e){if(t=="t"&&e.t in{drc:1,arc:1,sha:1,shc:1,shd:1})return!1;if(t!="v"&&(!(t in n)||n[t]!=e[t]))return!1}return!0},deleteRepeatOpt:function(e,n){let t=e,l=this;if(n instanceof Array)for(let o=0;o1){let s=[];s[0]=a[0];for(let u=1;u{e.index==d.currentSheetIndex&&(e.op==="enterEdit"?Xa.multipleRangeShow(e.id,e.username,e.r,e.c,e.op):Xa.multipleRangeShow(e.id,e.username,e.r,e.c))})},closeWebSocket:function(){let e=this;e.websocket!=null&&e.websocket.close(1e3)}},ae=Xa});var x0,U,rl=De(()=>{Wt();Za();dt();yt();$t();x0={info:function(e,n){$("#luckysheet-modal-dialog-mask").show(),$("#luckysheet-info").remove();let l=te().button;$("body").append(we(At,{id:"luckysheet-info",addclass:"",title:e,content:n,botton:'",style:"z-index:100003"}));let a=$("#luckysheet-info").find(".luckysheet-modal-dialog-content").css("min-width",300).end(),o=a.outerHeight(),s=a.outerWidth(),u=$(window).width(),f=$(window).height(),A=$(document).scrollLeft(),h=$(document).scrollTop();$("#luckysheet-info").css({left:(u+A-s)/2,top:(f+h-o)/3}).show()},confirm:function(e,n,t,l,a,o){$("#luckysheet-modal-dialog-mask").show(),$("#luckysheet-confirm").remove();let u=te().button;a==null&&(a=u.confirm),o==null&&(o=u.cancel),$("body").append(we(At,{id:"luckysheet-confirm",addclass:"",style:"z-index:100003",title:e,content:n,botton:'"}));let f=$("#luckysheet-confirm").find(".luckysheet-modal-dialog-content").css("min-width",300).end(),A=f.outerHeight(),h=f.outerWidth(),g=$(window).width(),p=$(window).height(),v=$(document).scrollLeft(),y=$(document).scrollTop();$("#luckysheet-confirm").css({left:(g+v-h)/2,top:(p+y-A)/3}).show(),f.find(".luckysheet-model-conform-btn").click(function(){typeof t=="function"&&t(),ae.keepHighLightBox(),$("#luckysheet-confirm").hide(),$("#luckysheet-modal-dialog-mask").hide()}),f.find(".luckysheet-model-cancel-btn").click(function(){typeof l=="function"&&l(),$("#luckysheet-confirm").hide(),$("#luckysheet-modal-dialog-mask").hide()})},screenshot:function(e,n,t){let a=te().screenshot;$("#luckysheet-modal-dialog-mask").show(),$("#luckysheet-confirm").remove(),$("body").append(we(At,{id:"luckysheet-confirm",addclass:"",style:"z-index:100003",title:e,content:n,botton:'  '+a.downLoadBtn+'    "}));let o=$("#luckysheet-confirm").find(".luckysheet-modal-dialog-content").css("min-width",300).end(),s=o.outerHeight(),u=o.outerWidth(),f=$(window).width(),A=$(window).height(),h=$(document).scrollLeft(),g=$(document).scrollTop();$("#luckysheet-confirm").css({left:(f+h-u)/2,top:(A+g-s)/3}).show(),o.find(".luckysheet-model-conform-btn").click(function(){br.isIE()=="1"?alert(a.browserNotTip):(!!window.ActiveXObject||"ActiveXObject"in window)&&($("#IframeReportImg").length===0&&$('').appendTo("body"),$("#IframeReportImg").attr("src")!=t?$("#IframeReportImg").attr("src",t):$("#IframeReportImg").src!="about:blank"&&window.frames.IframeReportImg.document.execCommand("SaveAs"))}),o.find(".luckysheet-model-cancel-btn").click(function(){$("#luckysheet-confirm").hide(),$("#luckysheet-modal-dialog-mask").hide()}),$("#luckysheet-confirm .luckysheet-model-copy-btn").click(function(){let p=new clipboard.DT;p.setData("text/html",""),br.isIE()=="1"?alert(a.rightclickTip):(clipboard.write(p),alert(a.successTip))})},chartPointConfig:function(e,n,t){$("body").append(we(At,{id:e,addclass:"luckysheet-chart-point-config-c",title:"\u6570\u636E\u70B9\u6279\u91CF\u8BBE\u7F6E",content:Wd,botton:'',style:"z-index:100003;height:80%;width:80%;top:10%;left:10%;"})),$("#luckysheet-modal-dialog-mask").show();let l=$(window).width(),a=$(window).height();$("#"+e).find(".luckysheet-chart-point-config").css("height",a-160),$("#"+e).css({height:a-90,width:l-100,left:7,top:14}).show().find(".luckysheet-model-save-btn").click(function(){typeof n=="function"&&n(),$("#"+e).hide(),$("#luckysheet-modal-dialog-mask").hide()}),$("#"+e).find(".luckysheet-model-save-btn").click(function(){typeof t=="function"&&t(),$("#"+e).hide(),$("#luckysheet-modal-dialog-mask").hide()})},sheetConfig:function(){},hoverTipshowState:!1,hoverTipshowTimeOut:null,createHoverTip:function(e,n){let t=this;$(e).on("mouseover",n,function(l){t.hoverTipshowState||(clearTimeout(t.hoverTipshowTimeOut),t.hoverTipshowTimeOut=setTimeout(function(){let a=$(l.currentTarget),o=a.offset(),s=$("#luckysheet-tooltip-up"),u=a.data("tips");if((u==null||u.length==0)&&(u=a.prev().data("tips"),u==null||u.length==0))return;s.length==0&&($("body").append(Gd),s=$("#luckysheet-tooltip-up")),s.removeClass("jfk-tooltip-hide").find("div.jfk-tooltip-contentId").html(u);let f=s.outerWidth();s.find("div.jfk-tooltip-arrow").css("left",f/2);let A=o.left+(a.outerWidth()-f)/2;A<2&&(A=2,s.find("div.jfk-tooltip-arrow").css("left",a.outerWidth()/2)),s.css({top:o.top+a.outerHeight()+1,left:A})},300))}).on("mouseout",n,function(l){t.hoverTipshowState=!1,clearTimeout(t.hoverTipshowTimeOut),$("#luckysheet-tooltip-up").addClass("jfk-tooltip-hide")}).on("click",n,function(l){t.hoverTipshowState=!0,clearTimeout(t.hoverTipshowTimeOut),$("#luckysheet-tooltip-up").addClass("jfk-tooltip-hide")})},popover:function(e,n,t,l,a,o){let s=te(),u=s.button,f=s.paint;a==null&&(a=u.close);let A='
'+f.start+'
'+a+"
";$("#luckysheetpopover").remove(),$("body").append(A),$("#luckysheetpopover .luckysheetpopover-content").html(e);let h=$("#luckysheetpopover").outerWidth(),g=$("#luckysheetpopover").outerHeight(),p={};n=="topLeft"?(p.top="20px",p.left="20px"):n=="topCenter"?(p.top="20px",p.left="50%",p["margin-left"]=-h/2):n=="topRight"?(p.top="20px",p.right="20px"):n=="midLeft"?(p.top="50%",p["margin-top"]=-g/2,p.left="20px"):n=="center"?(p.top="50%",p["margin-top"]=-g/2,p.left="50%",p["margin-left"]=-h/2):n=="midRight"?(p.top="50%",p["margin-top"]=-g/2,p.right="20px"):n=="bottomLeft"?(p.bottom="20px",p.left="20px"):n=="bottomCenter"?(p.bottom="20px",p.left="50%",p["margin-left"]=-h/2):n=="bottomRight"?(p.bottom="20px",p.right="20px"):(p.top="20px",p.left="50%",p["margin-left"]=-h/2),l=="white"&&(p.background="rgba(255, 255, 255, 0.65)",p.color="#000",$("#luckysheetpopover .luckysheetpopover-btn").css({border:"1px solid #000"})),setTimeout(function(){$("#luckysheetpopover .luckysheetpopover-content").css({"margin-left":-$("#luckysheetpopover .luckysheetpopover-btn").outerWidth()/2})},1),$("#luckysheetpopover").css(p).fadeIn(),$("#luckysheetpopover .luckysheetpopover-btn").click(function(){typeof o=="function"&&o()}),t!=null&&typeof t=="number"&&setTimeout(function(){$("#luckysheetpopover").fadeOut().remove(),typeof o=="function"&&o()},t)}},U=x0});var Rs,C0,Re,Ar=De(()=>{wt();dt();jt();Pt();rl();_t();Qt();Fl();Wt();$t();el();cl();yt();Bl();je();Rs=Ol(En()),C0={fileClone:[],editorRule:null,ruleTypeHtml:function(){let e=te().conditionformat;return`
-
- - ${e.ruleTypeItem1} -
-
- - ${e.ruleTypeItem2} -
-
- - ${e.ruleTypeItem3} -
-
- - ${e.ruleTypeItem4} -
-
- - ${e.ruleTypeItem5} -
-
- - ${e.ruleTypeItem6} -
-
`},textCellColorHtml:function(){let e=te().conditionformat;return`
-
- - - -
-
- - - -
-
`},selectRange:[],selectStatus:!1,dataBarList:[{format:["#638ec6","#ffffff"]},{format:["#63c384","#ffffff"]},{format:["#ff555a","#ffffff"]},{format:["#ffb628","#ffffff"]},{format:["#008aef","#ffffff"]},{format:["#d6007b","#ffffff"]},{format:["#638ec6"]},{format:["#63c384"]},{format:["#ff555a"]},{format:["#ffb628"]},{format:["#008aef"]},{format:["#d6007b"]}],colorGradationList:[{format:["rgb(99, 190, 123)","rgb(255, 235, 132)","rgb(248, 105, 107)"]},{format:["rgb(248, 105, 107)","rgb(255, 235, 132)","rgb(99, 190, 123)"]},{format:["rgb(99, 190, 123)","rgb(252, 252, 255)","rgb(248, 105, 107)"]},{format:["rgb(248, 105, 107)","rgb(252, 252, 255)","rgb(99, 190, 123)"]},{format:["rgb(90, 138, 198)","rgb(252, 252, 255)","rgb(248, 105, 107)"]},{format:["rgb(248, 105, 107)","rgb(252, 252, 255)","rgb(90, 138, 198)"]},{format:["rgb(252, 252, 255)","rgb(248, 105, 107)"]},{format:["rgb(248, 105, 107)","rgb(252, 252, 255)"]},{format:["rgb(99, 190, 123)","rgb(252, 252, 255)"]},{format:["rgb(252, 252, 255)","rgb(99, 190, 123)"]},{format:["rgb(99, 190, 123)","rgb(255, 235, 132)"]},{format:["rgb(255, 235, 132)","rgb(99, 190, 123)"]}],init:function(){let e=this,n=te().conditionformat;$(document).off("change.CFchooseSheet").on("change.CFchooseSheet","#luckysheet-administerRule-dialog .chooseSheet",function(){let t=$("#luckysheet-administerRule-dialog .chooseSheet option:selected").val();e.getConditionRuleList(t)}),$(document).off("click.CFadministerRuleItem").on("click.CFadministerRuleItem","#luckysheet-administerRule-dialog .ruleList .listBox .item",function(){$(this).addClass("on").siblings().removeClass("on")}),$(document).off("click.CFadministerRuleConfirm").on("click.CFadministerRuleConfirm","#luckysheet-administerRule-dialog-confirm",function(){if(!Or(d.currentSheetIndex))return;let t=$.extend(!0,[],d.luckysheetfile),l=e.getHistoryRules(t),a=$.extend(!0,[],e.fileClone);for(let u=0;u0)for(let s=0;s1){e.infoDialog(n.onlySingleCell,"");return}else if(b.length==1){let w=b[0].row[0],C=b[0].row[1],I=b[0].column[0],x=b[0].column[1];if(w==C&&I==x)v=Fe(w,I,d.flowdata),A.push({row:b[0].row,column:b[0].column}),h.push(v);else{e.infoDialog(n.onlySingleCell,"");return}}else if(b.length==0)if(isNaN(v)||v==""){e.infoDialog(n.conditionValueCanOnly,"");return}else h.push(v);let k=e.getRangeByTxt(y);if(k.length>1){e.infoDialog(n.onlySingleCell,"");return}else if(k.length==1){let w=k[0].row[0],C=k[0].row[1],I=k[0].column[0],x=k[0].column[1];if(w==C&&I==x)y=Fe(w,I,d.flowdata),A.push({row:k[0].row,column:k[0].column}),h.push(y);else{e.infoDialog(n.onlySingleCell,"");return}}else if(k.length==0)if(isNaN(y)||y==""){e.infoDialog(n.conditionValueCanOnly,"");return}else h.push(y)}else{let v=$("#luckysheet-newConditionRule-dialog #conditionVal input").val().trim(),y=e.getRangeByTxt(v);if(y.length>1){e.infoDialog(n.onlySingleCell,"");return}else if(y.length==1){let b=y[0].row[0],k=y[0].row[1],w=y[0].column[0],C=y[0].column[1];if(b==k&&w==C)v=Fe(b,w,d.flowdata),A.push({row:y[0].row,column:y[0].column}),h.push(v);else{e.infoDialog(n.onlySingleCell,"");return}}else if(y.length==0)if(isNaN(v)||v==""){e.infoDialog(n.conditionValueCanOnly,"");return}else h.push(v)}else if(l=="text"){f="textContains";let v=$("#luckysheet-newConditionRule-dialog #conditionVal input").val().trim(),y=e.getRangeByTxt(v);if(y.length>1){e.infoDialog(n.onlySingleCell,"");return}else if(y.length==1){let b=y[0].row[0],k=y[0].row[1],w=y[0].column[0],C=y[0].column[1];if(b==k&&w==C)v=Fe(b,w,d.flowdata),A.push({row:y[0].row,column:y[0].column}),h.push(v);else{e.infoDialog(n.onlySingleCell,"");return}}else if(y.length==0)if(v==""){e.infoDialog(n.conditionValueCanOnly,"");return}else h.push(v)}else if(l=="date"){f="occurrenceDate";let v=$("#luckysheet-newConditionRule-dialog #daterange-btn").val();if(v==""||v==null){e.infoDialog(n.pleaseSelectADate,"");return}h.push(v)}}else if(t==2){l=="top"?$("#luckysheet-newConditionRule-dialog #isPercent").is(":selected")?f="top10%":f="top10":l=="last"&&($("#luckysheet-newConditionRule-dialog #isPercent").is(":selected")?f="last10%":f="last10");let v=$("#luckysheet-newConditionRule-dialog #conditionVal input").val().trim();if(parseInt(v)!=v||parseInt(v)<1||parseInt(v)>1e3){e.infoDialog(n.pleaseEnterInteger,"");return}h.push(parseInt(v))}else if(t==3)l=="AboveAverage"?(f="AboveAverage",h.push("AboveAverage")):l=="SubAverage"&&(f="SubAverage",h.push("SubAverage"));else if(t==4)f="duplicateValue",h.push(l);else if(t==5){f="formula";let v=$("#luckysheet-newConditionRule-dialog #formulaConditionVal input").val().trim();if(v==""){e.infoDialog("Condition value cannot be empty!","");return}h.push(v)}let g;$("#luckysheet-newConditionRule-dialog #checkTextColor").is(":checked")?g=$("#luckysheet-newConditionRule-dialog #textcolorshow").spectrum("get").toHexString():g=null;let p;$("#luckysheet-newConditionRule-dialog #checkCellColor").is(":checked")?p=$("#luckysheet-newConditionRule-dialog #cellcolorshow").spectrum("get").toHexString():p=null,o={textColor:g,cellColor:p},s={type:"default",cellrange:$.extend(!0,[],d.luckysheet_select_save),format:o,conditionName:f,conditionRange:A,conditionValue:h}}$("#luckysheet-newConditionRule-dialog").hide();let u=$(this).attr("data-source");if(u==0){$("#luckysheet-modal-dialog-mask").hide();let f=$.extend(!0,[],d.luckysheetfile),A=e.getHistoryRules(f),h=d.luckysheetfile[Q(d.currentSheetIndex)].luckysheet_conditionformat_save==null?[]:d.luckysheetfile[Q(d.currentSheetIndex)].luckysheet_conditionformat_save;h.push(s),d.luckysheetfile[Q(d.currentSheetIndex)].luckysheet_conditionformat_save=h;let g=$.extend(!0,[],d.luckysheetfile),p=e.getCurrentRules(g);e.ref(A,p),ae.allowUpdate&&ae.saveParam("all",d.currentSheetIndex,h,{k:"luckysheet_conditionformat_save"})}else if(u==1){let f=e.fileClone[Q(d.currentSheetIndex)].luckysheet_conditionformat_save?e.fileClone[Q(d.currentSheetIndex)].luckysheet_conditionformat_save:[];f.push(s),e.fileClone[Q(d.currentSheetIndex)].luckysheet_conditionformat_save=f,e.administerRuleDialog()}}),$(document).off("click.CFnewConditionRuleClose").on("click.CFnewConditionRuleClose","#luckysheet-newConditionRule-dialog-close",function(){let t=$(this).attr("data-source");t==0&&$("#luckysheet-modal-dialog-mask").hide(),t==1&&$("#luckysheet-administerRule-dialog").show(),$("#luckysheet-newConditionRule-dialog").hide(),$("#luckysheet-formula-functionrange-select").hide(),$("#luckysheet-row-count-show").hide(),$("#luckysheet-column-count-show").hide()}),$(document).off("click.CFeditorConditionRule").on("click.CFeditorConditionRule","#editorConditionRule",function(){let t=$("#luckysheet-administerRule-dialog .chooseSheet option:selected").val();if(!Or(t))return;let l=$("#luckysheet-administerRule-dialog .ruleList .listBox .item.on").attr("data-item"),a={sheetIndex:t,itemIndex:l,data:e.fileClone[Q(t)].luckysheet_conditionformat_save[l]};e.editorRule=a,e.editorConditionRuleDialog()}),$(document).off("click.CFeditorConditionRuleConfirm").on("click.CFeditorConditionRuleConfirm","#luckysheet-editorConditionRule-dialog-confirm",function(){let t=$("#luckysheet-editorConditionRule-dialog .ruleTypeItem.on").index(),l=$("#luckysheet-editorConditionRule-dialog #type1 option:selected").val(),a=$("#luckysheet-editorConditionRule-dialog ."+l+"Box #type2 option:selected").val(),o=e.editorRule.data.cellrange,s,u;if(t==0){if(l=="dataBar"){let h=$(this).parents("#luckysheet-editorConditionRule-dialog").find(".dataBarBox .luckysheet-conditionformat-config-color").spectrum("get").toHexString();a=="gradient"?s=[h,"#ffffff"]:a=="solid"&&(s=[h]),u={type:"dataBar",cellrange:o,format:s}}else if(l=="colorGradation"){let h=$(this).parents("#luckysheet-editorConditionRule-dialog").find(".colorGradationBox .maxVal .luckysheet-conditionformat-config-color").spectrum("get").toRgbString(),g=$(this).parents("#luckysheet-editorConditionRule-dialog").find(".colorGradationBox .midVal .luckysheet-conditionformat-config-color").spectrum("get").toRgbString(),p=$(this).parents("#luckysheet-editorConditionRule-dialog").find(".colorGradationBox .minVal .luckysheet-conditionformat-config-color").spectrum("get").toRgbString();a=="threeColor"?s=[h,g,p]:a=="twoColor"&&(s=[h,p]),u={type:"colorGradation",cellrange:o,format:s}}else if(l=="icons"){let h=$(this).parents("#luckysheet-editorConditionRule-dialog").find(".iconsBox .model").attr("data-len"),g=$(this).parents("#luckysheet-editorConditionRule-dialog").find(".iconsBox .model").attr("data-leftmin"),p=$(this).parents("#luckysheet-editorConditionRule-dialog").find(".iconsBox .model").attr("data-top");s={len:h,leftMin:g,top:p},u={type:"icons",cellrange:o,format:s}}}else{let h="",g=[],p=[];if(t==1){if(l=="number")if(h=a,a=="betweenness"){let b=$("#luckysheet-editorConditionRule-dialog #conditionVal input").val().trim(),k=$("#luckysheet-editorConditionRule-dialog #conditionVal2 input").val().trim(),w=e.getRangeByTxt(b);if(w.length>1){e.infoDialog(n.onlySingleCell,"");return}else if(w.length==1){let I=w[0].row[0],x=w[0].row[1],T=w[0].column[0],R=w[0].column[1];if(I==x&&T==R)b=Fe(I,T,d.flowdata),g.push({row:w[0].row,column:w[0].column}),p.push(b);else{e.infoDialog(n.onlySingleCell,"");return}}else if(w.length==0)if(isNaN(b)||b==""){e.infoDialog(n.conditionValueCanOnly,"");return}else p.push(b);let C=e.getRangeByTxt(k);if(C.length>1){e.infoDialog(n.onlySingleCell,"");return}else if(C.length==1){let I=C[0].row[0],x=C[0].row[1],T=C[0].column[0],R=C[0].column[1];if(I==x&&T==R)k=Fe(I,T,d.flowdata),g.push({row:C[0].row,column:C[0].column}),p.push(k);else{e.infoDialog(n.onlySingleCell,"");return}}else if(C.length==0)if(isNaN(k)||k==""){e.infoDialog(n.conditionValueCanOnly,"");return}else p.push(k)}else{let b=$("#luckysheet-editorConditionRule-dialog #conditionVal input").val().trim(),k=e.getRangeByTxt(b);if(k.length>1){e.infoDialog(n.onlySingleCell,"");return}else if(k.length==1){let w=k[0].row[0],C=k[0].row[1],I=k[0].column[0],x=k[0].column[1];if(w==C&&I==x)b=Fe(w,I,d.flowdata),g.push({row:k[0].row,column:k[0].column}),p.push(b);else{e.infoDialog(n.onlySingleCell,"");return}}else if(k.length==0)if(isNaN(b)||b==""){e.infoDialog(n.conditionValueCanOnly,"");return}else p.push(b)}else if(l=="text"){h="textContains";let b=$("#luckysheet-editorConditionRule-dialog #conditionVal input").val().trim(),k=e.getRangeByTxt(b);if(k.length>1){e.infoDialog(n.onlySingleCell,"");return}else if(k.length==1){let w=k[0].row[0],C=k[0].row[1],I=k[0].column[0],x=k[0].column[1];if(w==C&&I==x)b=Fe(w,I,d.flowdata),g.push({row:k[0].row,column:k[0].column}),p.push(b);else{e.infoDialog(n.onlySingleCell,"");return}}else if(k.length==0)if(isNaN(b)||b==""){e.infoDialog(n.conditionValueCanOnly,"");return}else p.push(b)}else if(l=="date"){h="occurrenceDate";let b=$("#luckysheet-editorConditionRule-dialog #daterange-btn").val();if(b==""||b==null){e.infoDialog(n.pleaseSelectADate,"");return}p.push(b)}}else if(t==2){l=="top"?$("#luckysheet-editorConditionRule-dialog #isPercent").is(":selected")?h="top10%":h="top10":l=="last"&&($("#luckysheet-editorConditionRule-dialog #isPercent").is(":selected")?h="last10%":h="last10");let b=$("#luckysheet-editorConditionRule-dialog #conditionVal input").val().trim();if(parseInt(b)!=b||parseInt(b)<1||parseInt(b)>1e3){e.infoDialog(n.pleaseEnterInteger,"");return}p.push(b)}else if(t==3)l=="AboveAverage"?(h="AboveAverage",p.push("AboveAverage")):l=="SubAverage"&&(h="SubAverage",p.push("SubAverage"));else if(t==4)h="duplicateValue",p.push(l);else if(t==5){h="formula";let b=$("#luckysheet-editorConditionRule-dialog #formulaConditionVal input").val().trim();if(console.log(b),b==""){e.infoDialog("Condition value cannot be empty!","");return}p.push(b)}let v;$("#luckysheet-editorConditionRule-dialog #checkTextColor").is(":checked")?v=$("#luckysheet-editorConditionRule-dialog #textcolorshow").spectrum("get").toHexString():v=null;let y;$("#luckysheet-editorConditionRule-dialog #checkCellColor").is(":checked")?y=$("#luckysheet-editorConditionRule-dialog #cellcolorshow").spectrum("get").toHexString():y=null,s={textColor:v,cellColor:y},u={type:"default",cellrange:o,format:s,conditionName:h,conditionRange:g,conditionValue:p}}let f=e.editorRule.sheetIndex,A=e.editorRule.itemIndex;e.fileClone[Q(f)].luckysheet_conditionformat_save[A]=u,$("#luckysheet-editorConditionRule-dialog").hide(),e.administerRuleDialog()}),$(document).off("click.CFeditorConditionRuleClose").on("click.CFeditorConditionRuleClose","#luckysheet-editorConditionRule-dialog-close",function(){$("#luckysheet-editorConditionRule-dialog").hide(),$("#luckysheet-administerRule-dialog").show(),$("#luckysheet-formula-functionrange-select").hide(),$("#luckysheet-row-count-show").hide(),$("#luckysheet-column-count-show").hide()}),$(document).off("click.CFnewEditorRuleItem").on("click.CFnewEditorRuleItem",".luckysheet-newEditorRule-dialog .ruleTypeItem",function(){$(this).addClass("on").siblings().removeClass("on");let t=$(this).index();$(this).parents(".luckysheet-newEditorRule-dialog").find(".ruleExplainBox").html(e.getRuleExplain(t)),e.colorSelectInit()}),$(document).off("change.CFnewEditorRuleType1").on("change.CFnewEditorRuleType1",".luckysheet-newEditorRule-dialog #type1",function(){let t=$(this).find("option:selected").val();(t=="dataBar"||t=="colorGradation"||t=="icons"||t=="number"||t=="text"||t=="date")&&$(this).parents(".luckysheet-newEditorRule-dialog").find("."+t+"Box").show().siblings().hide(),t=="date"&&e.daterangeInit($(this).parents(".luckysheet-newEditorRule-dialog").attr("id"))}),$(document).off("change.CFnewEditorRuleType2").on("change.CFnewEditorRuleType2",".luckysheet-newEditorRule-dialog #type2",function(){let t=$(this).parents(".luckysheet-newEditorRule-dialog").find("#type1 option:selected").val();t=="colorGradation"?$(this).find("option:selected").val()=="threeColor"?$(this).parents(".luckysheet-newEditorRule-dialog").find(".midVal").show():$(this).parents(".luckysheet-newEditorRule-dialog").find(".midVal").hide():t=="number"&&($(this).find("option:selected").val()=="betweenness"?($(this).parents(".luckysheet-newEditorRule-dialog").find(".txt").show(),$(this).parents(".luckysheet-newEditorRule-dialog").find("#conditionVal2").show()):($(this).parents(".luckysheet-newEditorRule-dialog").find(".txt").hide(),$(this).parents(".luckysheet-newEditorRule-dialog").find("#conditionVal2").hide()))}),$(document).off("click.CFiconsShowbox").on("click.CFiconsShowbox",".luckysheet-newEditorRule-dialog .iconsBox .showbox",function(){$(this).parents(".iconsBox").find("ul").toggle()}),$(document).off("click.CFiconsLi").on("click.CFiconsLi",".luckysheet-newEditorRule-dialog .iconsBox li",function(){let t=$(this).find("div").attr("data-len"),l=$(this).find("div").attr("data-leftmin"),a=$(this).find("div").attr("data-top"),o=$(this).find("div").attr("title"),s=$(this).find("div").css("background-position");$(this).parents(".iconsBox").find(".showbox .model").css("background-position",s),$(this).parents(".iconsBox").find(".showbox .model").attr("data-len",t),$(this).parents(".iconsBox").find(".showbox .model").attr("data-leftmin",l),$(this).parents(".iconsBox").find(".showbox .model").attr("data-top",a),$(this).parents(".iconsBox").find(".showbox .model").attr("title",o),$(this).parents("ul").hide()}),$(document).off("click.CFdeleteConditionRule").on("click.CFdeleteConditionRule","#deleteConditionRule",function(){let t=$("#luckysheet-administerRule-dialog .chooseSheet option:selected").val();if(!Or(t))return;let l=$("#luckysheet-administerRule-dialog .ruleList .listBox .item.on").attr("data-item");e.fileClone[Q(t)].luckysheet_conditionformat_save.splice(l,1),e.administerRuleDialog()}),$(document).off("click.CFdefault").on("click.CFdefault","#luckysheet-conditionformat-dialog-confirm",function(){if(!Or(d.currentSheetIndex))return;let t=$("#luckysheet-conditionformat-dialog .box").attr("data-itemvalue"),l=[],a=[];if(t=="greaterThan"||t=="lessThan"||t=="equal"||t=="textContains"){let v=$("#luckysheet-conditionformat-dialog #conditionVal").val().trim(),y=e.getRangeByTxt(v);if(y.length>1){e.infoDialog(n.onlySingleCell,"");return}else if(y.length==1){let b=y[0].row[0],k=y[0].row[1],w=y[0].column[0],C=y[0].column[1];if(b==k&&w==C)v=Fe(b,w,d.flowdata),l.push({row:y[0].row,column:y[0].column}),a.push(v);else{e.infoDialog(n.onlySingleCell,"");return}}else if(y.length==0)if(isNaN(v)||v==""){e.infoDialog(n.conditionValueCanOnly,"");return}else a.push(v)}else if(t=="betweenness"){let v=$("#luckysheet-conditionformat-dialog #conditionVal").val().trim(),y=$("#luckysheet-conditionformat-dialog #conditionVal2").val().trim(),b=e.getRangeByTxt(v);if(b.length>1){e.infoDialog(n.onlySingleCell,"");return}else if(b.length==1){let w=b[0].row[0],C=b[0].row[1],I=b[0].column[0],x=b[0].column[1];if(w==C&&I==x)v=Fe(w,I,d.flowdata),l.push({row:b[0].row,column:b[0].column}),a.push(v);else{e.infoDialog(n.onlySingleCell,"");return}}else if(b.length==0)if(isNaN(v)||v==""){e.infoDialog(n.conditionValueCanOnly,"");return}else a.push(v);let k=e.getRangeByTxt(y);if(k.length>1){e.infoDialog(n.onlySingleCell,"");return}else if(k.length==1){let w=k[0].row[0],C=k[0].row[1],I=k[0].column[0],x=k[0].column[1];if(w==C&&I==x)y=Fe(w,I,d.flowdata),l.push({row:k[0].row,column:k[0].column}),a.push(y);else{e.infoDialog(n.onlySingleCell,"");return}}else if(k.length==0)if(isNaN(y)||y==""){e.infoDialog(n.conditionValueCanOnly,"");return}else a.push(y)}else if(t=="occurrenceDate"){let v=$("#luckysheet-conditionformat-dialog #daterange-btn").val();if(v==""||v==null){e.infoDialog(n.pleaseSelectADate,"");return}a.push(v)}else if(t=="duplicateValue")a.push($("#luckysheet-conditionformat-dialog #conditionVal option:selected").val());else if(t=="top10"||t=="top10%"||t=="last10"||t=="last10%"){let v=$("#luckysheet-conditionformat-dialog #conditionVal").val().trim();if(parseInt(v)!=v||parseInt(v)<1||parseInt(v)>1e3){e.infoDialog(n.pleaseEnterInteger,"");return}a.push(v)}else t=="AboveAverage"?a.push("AboveAverage"):t=="SubAverage"&&a.push("SubAverage");let o;$("#checkTextColor").is(":checked")?o=$("#textcolorshow").spectrum("get").toHexString():o=null;let s;$("#checkCellColor").is(":checked")?s=$("#cellcolorshow").spectrum("get").toHexString():s=null;let u=$.extend(!0,[],d.luckysheetfile),f=e.getHistoryRules(u),A={type:"default",cellrange:$.extend(!0,[],d.luckysheet_select_save),format:{textColor:o,cellColor:s},conditionName:t,conditionRange:l,conditionValue:a},h=d.luckysheetfile[Q(d.currentSheetIndex)].luckysheet_conditionformat_save==null?[]:d.luckysheetfile[Q(d.currentSheetIndex)].luckysheet_conditionformat_save;h.push(A),d.luckysheetfile[Q(d.currentSheetIndex)].luckysheet_conditionformat_save=h;let g=$.extend(!0,[],d.luckysheetfile),p=e.getCurrentRules(g);e.ref(f,p),$("#luckysheet-modal-dialog-mask").hide(),$("#luckysheet-conditionformat-dialog").hide(),ae.allowUpdate&&ae.saveParam("all",d.currentSheetIndex,h,{k:"luckysheet_conditionformat_save"})}),$(document).off("click.CFicons").on("click.CFicons","#luckysheet-CFicons-dialog .item",function(){if($("#luckysheet-modal-dialog-mask").hide(),$("#luckysheet-CFicons-dialog").hide(),d.luckysheet_select_save.length>0){let t=$.extend(!0,[],d.luckysheet_select_save),l={len:$(this).attr("data-len"),leftMin:$(this).attr("data-leftMin"),top:$(this).attr("data-top")};e.updateItem("icons",t,l)}}),$(document).on("click",".range .fa-table",function(){let t=$(this).parents(".luckysheet-modal-dialog").attr("id");$("#"+t).hide();let l;if(t=="luckysheet-conditionformat-dialog")$(this).siblings("input").attr("id")=="conditionVal"?l="0_1":l="0_2";else if(t=="luckysheet-newConditionRule-dialog"){let o=$(this).parents(".range").attr("id");o=="formulaConditionVal"?l="1_0":o=="conditionVal"?l="1_1":l="1_2"}else if(t=="luckysheet-editorConditionRule-dialog"){let o=$(this).parents(".range").attr("id");o=="formulaConditionVal"?l="2_0":o=="conditionVal"?l="2_1":l="2_2"}let a=$(this).siblings("input").val();e.singleRangeDialog(l,a),Et(e.getRangeByTxt(a))}),$(document).on("click","#luckysheet-singleRange-dialog-confirm",function(){$("#luckysheet-modal-dialog-mask").show(),$(this).parents("#luckysheet-singleRange-dialog").hide();let t=$(this).attr("data-source"),l=$(this).parents("#luckysheet-singleRange-dialog").find("input").val();t=="0_1"?($("#luckysheet-conditionformat-dialog").show(),$("#luckysheet-conditionformat-dialog #conditionVal").val(l)):t=="0_2"?($("#luckysheet-conditionformat-dialog").show(),$("#luckysheet-conditionformat-dialog #conditionVal2").val(l)):t=="1_0"?($("#luckysheet-newConditionRule-dialog").show(),$("#luckysheet-newConditionRule-dialog #formulaConditionVal input").val(l)):t=="1_1"?($("#luckysheet-newConditionRule-dialog").show(),$("#luckysheet-newConditionRule-dialog #conditionVal input").val(l)):t=="1_2"?($("#luckysheet-newConditionRule-dialog").show(),$("#luckysheet-newConditionRule-dialog #conditionVal2 input").val(l)):t=="2_0"?($("#luckysheet-editorConditionRule-dialog").show(),$("#luckysheet-editorConditionRule-dialog #formulaConditionVal input").val(l)):t=="2_1"?($("#luckysheet-editorConditionRule-dialog").show(),$("#luckysheet-editorConditionRule-dialog #conditionVal input").val(l)):t=="2_2"&&($("#luckysheet-editorConditionRule-dialog").show(),$("#luckysheet-editorConditionRule-dialog #conditionVal2 input").val(l)),Et([])}),$(document).on("click","#luckysheet-singleRange-dialog-close",function(){$("#luckysheet-modal-dialog-mask").show(),$(this).parents("#luckysheet-singleRange-dialog").hide();let t=$(this).attr("data-source");t=="0_1"||t=="0_2"?$("#luckysheet-conditionformat-dialog").show():t=="1_0"||t=="1_1"||t=="1_2"?$("#luckysheet-newConditionRule-dialog").show():(t=="2_0"||t=="2_1"||t=="2_2")&&$("#luckysheet-editorConditionRule-dialog").show(),Et([])}),$(document).on("click",".luckysheet-modal-dialog-title-close",function(){let t=$(this).parents(".luckysheet-modal-dialog").attr("id");if(t=="luckysheet-newConditionRule-dialog"&&$("#"+t).find("#luckysheet-newConditionRule-dialog-close").attr("data-source")==1&&$("#luckysheet-administerRule-dialog").show(),t=="luckysheet-editorConditionRule-dialog"&&$("#luckysheet-administerRule-dialog").show(),t=="luckysheet-singleRange-dialog"){$("#luckysheet-modal-dialog-mask").show();let l=$(this).parents("#luckysheet-singleRange-dialog").find("#luckysheet-singleRange-dialog-confirm").attr("data-source");l=="0_1"||l=="0_2"?$("#luckysheet-conditionformat-dialog").show():l=="1_1"||l=="1_2"?$("#luckysheet-newConditionRule-dialog").show():(l=="2_1"||l=="2_2")&&$("#luckysheet-editorConditionRule-dialog").show(),Et([])}t=="luckysheet-multiRange-dialog"&&($("#luckysheet-modal-dialog-mask").show(),$("#luckysheet-administerRule-dialog").show(),Et([])),t=="luckysheet-conditionformat-info-dialog"&&$("#luckysheet-modal-dialog-mask").show()}),$(document).on("click","#luckysheet-conditionformat-info-dialog-close",function(){$(this).parents("#luckysheet-conditionformat-info-dialog").hide()})},singleRangeDialog:function(e,n){$("#luckysheet-modal-dialog-mask").hide(),$("#luckysheet-singleRange-dialog").remove();let t=te().conditionformat;$("body").append(we(At,{id:"luckysheet-singleRange-dialog",addclass:"luckysheet-singleRange-dialog",title:t.selectCell,content:``,botton:` - `,style:"z-index:100003"}));let l=$("#luckysheet-singleRange-dialog").find(".luckysheet-modal-dialog-content").css("min-width",300).end(),a=l.outerHeight(),o=l.outerWidth(),s=$(window).width(),u=$(window).height(),f=$(document).scrollLeft(),A=$(document).scrollTop();$("#luckysheet-singleRange-dialog").css({left:(s+f-o)/2,top:(u+A-a)/3}).show()},multiRangeDialog:function(e,n){let t=this;$("#luckysheet-modal-dialog-mask").hide(),$("#luckysheet-multiRange-dialog").remove();let l=te().conditionformat;$("body").append(we(At,{id:"luckysheet-multiRange-dialog",addclass:"luckysheet-multiRange-dialog",title:l.selectRange,content:``,botton:` - `,style:"z-index:100003"}));let a=$("#luckysheet-multiRange-dialog").find(".luckysheet-modal-dialog-content").css("min-width",300).end(),o=a.outerHeight(),s=a.outerWidth(),u=$(window).width(),f=$(window).height(),A=$(document).scrollLeft(),h=$(document).scrollTop();$("#luckysheet-multiRange-dialog").css({left:(u+A-s)/2,top:(f+h-o)/3}).show(),Et(t.getRangeByTxt(n))},getTxtByRange:function(e){if(e.length>0){let n=[];for(let t=0;t${l.confirm} - `,style:"z-index:9999"}));let a=$("#luckysheet-conditionformat-dialog").find(".luckysheet-modal-dialog-content").css("min-width",300).end(),o=a.outerHeight(),s=a.outerWidth(),u=$(window).width(),f=$(window).height(),A=$(document).scrollLeft(),h=$(document).scrollTop();$("#luckysheet-conditionformat-dialog").css({left:(u+A-s)/2,top:(f+h-o)/3}).show(),t.init(),t.colorSelectInit(),e==te().conditionformat.conditionformat_occurrenceDate&&t.daterangeInit("luckysheet-conditionformat-dialog")},CFiconsDialog:function(){$("#luckysheet-modal-dialog-mask").show(),$("#luckysheet-CFicons-dialog").remove();let e=te().conditionformat,n=`
-
${e.pleaseSelectIcon}
-
${e.direction}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
${e.shape}
-
-
-
-
-
-
-
-
-
-
-
-
-
${e.mark}
-
-
-
-
-
-
-
-
-
-
-
${e.grade}
-
-
-
-
-
-
-
-
-
-
-
-
-
`;$("body").append(we(At,{id:"luckysheet-CFicons-dialog",addclass:"luckysheet-CFicons-dialog",title:e.icons,content:n,botton:``,style:"z-index:100003"}));let t=$("#luckysheet-CFicons-dialog").find(".luckysheet-modal-dialog-content").css("min-width",400).end(),l=t.outerHeight(),a=t.outerWidth(),o=$(window).width(),s=$(window).height(),u=$(document).scrollLeft(),f=$(document).scrollTop();$("#luckysheet-CFicons-dialog").css({left:(o+u-a)/2,top:(s+f-l)/3}).show()},administerRuleDialog:function(){$("#luckysheet-modal-dialog-mask").show(),$("#luckysheet-administerRule-dialog").remove();let e=te().conditionformat,n="";for(let g=0;g - ${e.currentSheet}\uFF1A${d.luckysheetfile[g].name} - `:n+=``;let t=`
- - -
-
-
- - - -
-
-
- ${e.rule} - ${e.format} - ${e.applyRange} -
-
-
-
`;$("body").append(we(At,{id:"luckysheet-administerRule-dialog",addclass:"luckysheet-administerRule-dialog",title:e.conditionformatManageRules,content:t,botton:` - `,style:"z-index:100003"}));let l=$("#luckysheet-administerRule-dialog").find(".luckysheet-modal-dialog-content").css("min-width",400).end(),a=l.outerHeight(),o=l.outerWidth(),s=$(window).width(),u=$(window).height(),f=$(document).scrollLeft(),A=$(document).scrollTop();$("#luckysheet-administerRule-dialog").css({left:(s+f-o)/2,top:(u+A-a)/3}).show();let h=$("#luckysheet-administerRule-dialog .chooseSheet option:selected").val();this.getConditionRuleList(h)},getConditionRuleList:function(e){let n=this;$("#luckysheet-administerRule-dialog .ruleList .listBox").empty();let t=n.fileClone[Q(e)].luckysheet_conditionformat_save;if(t!=null&&t.length>0){let l=te().conditionformat;for(let a=0;a
'):o=="colorGradation"?(f=l.colorGradation,A=''):o=="icons"?(f=l.icons,A=''):(f=n.getConditionRuleName(t[a].conditionName,t[a].conditionRange,t[a].conditionValue),s.textColor!=null&&(A+=''),s.cellColor!=null&&(A+=''));let h=[];for(let p=0;p
'+f+'
'+A+'
';$("#luckysheet-administerRule-dialog .ruleList .listBox").prepend(g)}$("#luckysheet-administerRule-dialog .ruleList .listBox .item canvas").each(function(a){let o=$(this).closest(".item").attr("data-item"),s=t[o].type,u=t[o].format,f=$(this).get(0).getContext("2d");if(s=="dataBar")if(u.length==2){let A=f.createLinearGradient(0,0,46,0);A.addColorStop(0,u[0]),A.addColorStop(1,u[1]),f.fillStyle=A,f.fillRect(0,0,46,18),f.beginPath(),f.moveTo(0,0),f.lineTo(0,18),f.lineTo(46,18),f.lineTo(46,0),f.lineTo(0,0),f.lineWidth=d.devicePixelRatio,f.strokeStyle=u[0],f.stroke(),f.closePath()}else u.length==1&&(f.fillStyle=u[0],f.fillRect(0,0,46,18),f.beginPath(),f.moveTo(0,0),f.lineTo(0,18),f.lineTo(46,18),f.lineTo(46,0),f.lineTo(0,0),f.lineWidth=d.devicePixelRatio,f.strokeStyle=u[0],f.stroke(),f.closePath());else if(s=="colorGradation"){let A=f.createLinearGradient(0,0,46,0);u.length==3?(A.addColorStop(0,u[0]),A.addColorStop(.5,u[1]),A.addColorStop(1,u[2])):u.length==2&&(A.addColorStop(0,u[0]),A.addColorStop(1,u[1])),f.fillStyle=A,f.fillRect(0,0,46,18)}else if(s=="icons"){let A=u.len,h=u.leftMin,g=u.top,p=32*A+10*(A-1),v=32,y=46,b=46*32/p;h=="0"?f.drawImage(Ka,0,g*32,p,v,0,(18-b)/2,y,b):h=="5"&&f.drawImage(Ka,210,g*32,p,v,0,(18-b)/2,y,b)}}),$("#luckysheet-administerRule-dialog .ruleList .listBox .item").eq(0).addClass("on")}},getConditionRuleName:function(e,n,t){let l;n[0]!=null?l=rt(n[0].column[0])+(n[0].row[0]+1):l=t[0];let a=te().conditionformat;if(e=="greaterThan")return a.cellValue+" > "+l;if(e=="lessThan")return a.cellValue+" < "+l;if(e=="betweenness"){let o;return n[1]!=null?o=rt(n[1].column[0])+(n[1].row[0]+1):o=t[1],a.cellValue+" "+a.between+" "+l+" "+a.in+" "+o+" "+a.between2}else{if(e=="equal")return a.cellValue+" = "+l;if(e=="textContains")return a.cellValue+a.contain+" ="+l;if(e=="occurrenceDate")return t;if(e=="duplicateValue"){if(t=="0")return a.duplicateValue;if(t=="1")return a.uniqueValue}else{if(e=="top10")return a.top+" "+l+" "+a.oneself;if(e=="top10%")return a.top+" "+l+"% "+a.oneself;if(e=="last10")return a.last+" "+l+" "+a.oneself;if(e=="last10%")return a.last+" "+l+"% "+a.oneself;if(e=="AboveAverage")return a.aboveAverage;if(e=="SubAverage")return a.belowAverage;if(e=="formula")return l.slice(0,1)!="="&&(l="="+l),a.formula+": "+l}}},newConditionRuleDialog:function(e){let n=this,t=te().conditionformat,l=n.getRuleExplain(0);$("#luckysheet-modal-dialog-mask").show(),$("#luckysheet-administerRule-dialog").hide(),$("#luckysheet-newConditionRule-dialog").remove();let a='
'+t.chooseRuleType+"\uFF1A
"+n.ruleTypeHtml()+'
'+t.editRuleDescription+'\uFF1A
'+l+"
";$("body").append(we(At,{id:"luckysheet-newConditionRule-dialog",addclass:"luckysheet-newEditorRule-dialog",title:t.newFormatRule,content:a,botton:` - `,style:"z-index:100003"}));let o=$("#luckysheet-newConditionRule-dialog").find(".luckysheet-modal-dialog-content").css("min-width",400).end(),s=o.outerHeight(),u=o.outerWidth(),f=$(window).width(),A=$(window).height(),h=$(document).scrollLeft(),g=$(document).scrollTop();$("#luckysheet-newConditionRule-dialog").css({left:(f+h-u)/2,top:(A+g-s)/3}).show(),$("#luckysheet-newConditionRule-dialog .ruleTypeBox .ruleTypeItem:eq(0)").addClass("on").siblings().removeClass("on"),n.colorSelectInit()},editorConditionRuleDialog:function(){let e=this,n=te().conditionformat,t=e.editorRule.data;if(t==null)return;let l=t.type,a=t.format,o=t.conditionName,s,u;l=="dataBar"||l=="colorGradation"||l=="icons"?(s=0,u=l):o=="greaterThan"||o=="lessThan"||o=="betweenness"||o=="equal"||o=="textContains"||o=="occurrenceDate"?(s=1,o=="greaterThan"||o=="lessThan"||o=="betweenness"||o=="equal"?u="number":o=="textContains"?u="text":o=="occurrenceDate"&&(u="date")):o=="top10"||o=="top10%"||o=="last10"||o=="last10%"?(s=2,o=="top10"||o=="top10%"?u="top":(o=="last10"||o=="last10%")&&(u="last")):o=="AboveAverage"||o=="SubAverage"?(s=3,u=o):o=="duplicateValue"?(s=4,u=t.conditionValue):o=="formula"&&(s=5);let f=e.getRuleExplain(s);$("#luckysheet-modal-dialog-mask").show(),$("#luckysheet-administerRule-dialog").hide(),$("#luckysheet-editorConditionRule-dialog").remove();let A='
'+n.chooseRuleType+"\uFF1A
"+e.ruleTypeHtml()+'
'+n.editRuleDescription+'\uFF1A
'+f+"
";$("body").append(we(At,{id:"luckysheet-editorConditionRule-dialog",addclass:"luckysheet-newEditorRule-dialog",title:n.editFormatRule,content:A,botton:` - `,style:"z-index:100003"}));let h=$("#luckysheet-editorConditionRule-dialog").find(".luckysheet-modal-dialog-content").css("min-width",400).end(),g=h.outerHeight(),p=h.outerWidth(),v=$(window).width(),y=$(window).height(),b=$(document).scrollLeft(),k=$(document).scrollTop();if($("#luckysheet-editorConditionRule-dialog").css({left:(v+b-p)/2,top:(y+k-g)/3}).show(),e.colorSelectInit(),$("#luckysheet-editorConditionRule-dialog .ruleTypeBox .ruleTypeItem:eq("+s+")").addClass("on").siblings().removeClass("on"),$("#luckysheet-editorConditionRule-dialog #type1").val(u),(u=="dataBar"||u=="colorGradation"||u=="icons"||u=="number"||u=="text"||u=="date")&&($("#luckysheet-editorConditionRule-dialog ."+u+"Box").show(),$("#luckysheet-editorConditionRule-dialog ."+u+"Box").siblings().hide()),u=="date"&&e.daterangeInit("luckysheet-editorConditionRule-dialog"),l=="dataBar"||l=="colorGradation"||l=="icons"){if(u=="dataBar")a.length==2?$("#luckysheet-editorConditionRule-dialog .dataBarBox #type2").val("gradient"):a.length==1&&$("#luckysheet-editorConditionRule-dialog .dataBarBox #type2").val("solid"),$("#luckysheet-editorConditionRule-dialog .dataBarBox .luckysheet-conditionformat-config-color").spectrum("set",a[0]);else if(u=="colorGradation")a.length==3?($("#luckysheet-editorConditionRule-dialog .colorGradationBox #type2").val("threeColor"),$("#luckysheet-editorConditionRule-dialog .colorGradationBox .midVal").show(),$("#luckysheet-editorConditionRule-dialog .colorGradationBox .maxVal .luckysheet-conditionformat-config-color").spectrum("set",a[0]),$("#luckysheet-editorConditionRule-dialog .colorGradationBox .midVal .luckysheet-conditionformat-config-color").spectrum("set",a[1]),$("#luckysheet-editorConditionRule-dialog .colorGradationBox .minVal .luckysheet-conditionformat-config-color").spectrum("set",a[2])):a.length==2&&($("#luckysheet-editorConditionRule-dialog .colorGradationBox #type2").val("twoColor"),$("#luckysheet-editorConditionRule-dialog .colorGradationBox .midVal").hide(),$("#luckysheet-editorConditionRule-dialog .colorGradationBox .maxVal .luckysheet-conditionformat-config-color").spectrum("set",a[0]),$("#luckysheet-editorConditionRule-dialog .colorGradationBox .minVal .luckysheet-conditionformat-config-color").spectrum("set",a[1]));else if(u=="icons"){let w=a.len,C=a.leftMin,I=a.top;$("#luckysheet-editorConditionRule-dialog .iconsBox li").each(function(x,T){if($(T).find("div").attr("data-len")==w&&$(T).find("div").attr("data-leftmin")==C&&$(T).find("div").attr("data-top")==I)return $("#luckysheet-editorConditionRule-dialog .iconsBox .showbox .model").css("background-position",$(T).find("div").css("background-position")),$("#luckysheet-editorConditionRule-dialog .iconsBox .showbox .model").attr("data-len",$(T).find("div").attr("data-len")),$("#luckysheet-editorConditionRule-dialog .iconsBox .showbox .model").attr("data-leftmin",$(T).find("div").attr("data-leftmin")),$("#luckysheet-editorConditionRule-dialog .iconsBox .showbox .model").attr("data-top",$(T).find("div").attr("data-leftmin")),$("#luckysheet-editorConditionRule-dialog .iconsBox .showbox .model").attr("title",$(T).find("div").attr("title")),!0})}}else{if(u=="number"){$("#luckysheet-editorConditionRule-dialog .numberBox #type2").val(o);let w;if(t.conditionRange[0]!=null?w=mt(d.currentSheetIndex,{row:t.conditionRange[0].row,column:t.conditionRange[0].column},d.currentSheetIndex):w=t.conditionValue[0],$("#luckysheet-editorConditionRule-dialog .numberBox #conditionVal input").val(w),o=="betweenness"){$("#luckysheet-editorConditionRule-dialog .numberBox .txt").show(),$("#luckysheet-editorConditionRule-dialog .numberBox #conditionVal2").show();let C;t.conditionRange[1]!=null?C=mt(d.currentSheetIndex,{row:t.conditionRange[1].row,column:t.conditionRange[1].column},d.currentSheetIndex):C=t.conditionValue[1],$("#luckysheet-editorConditionRule-dialog .numberBox #conditionVal2 input").val(C)}else $("#luckysheet-editorConditionRule-dialog .numberBox .txt").hide(),$("#luckysheet-editorConditionRule-dialog .numberBox #conditionVal2").hide()}else if(u=="text"){let w;t.conditionRange[0]!=null?w=mt(d.currentSheetIndex,{row:t.conditionRange[0].row,column:t.conditionRange[0].column},d.currentSheetIndex):w=t.conditionValue[0],$("#luckysheet-editorConditionRule-dialog .textBox #conditionVal input").val(w)}else if(u=="date"){e.daterangeInit("luckysheet-editorConditionRule-dialog");let w=t.conditionValue[0];$("#luckysheet-editorConditionRule-dialog .dateBox #daterange-btn").val(w)}else if(u=="top"||u=="last"){let w=t.conditionValue[0];(o=="top10%"||o=="last10%")&&$("#luckysheet-editorConditionRule-dialog #isPercent").attr("checked","checked")}else if(o=="formula"){let w=t.conditionValue[0];$("#luckysheet-editorConditionRule-dialog #formulaConditionVal input").val(w)}$("#luckysheet-editorConditionRule-dialog #textcolorshow").spectrum("set",a.textColor),$("#luckysheet-editorConditionRule-dialog #cellcolorshow").spectrum("set",a.cellColor)}},infoDialog:function(e,n){$("#luckysheet-modal-dialog-mask").show(),$("#luckysheet-conditionformat-info-dialog").remove(),$("body").append(we(At,{id:"luckysheet-conditionformat-info-dialog",addclass:"",title:e,content:n,botton:``,style:"z-index:100003"}));let t=$("#luckysheet-conditionformat-info-dialog").find(".luckysheet-modal-dialog-content").css("min-width",300).end(),l=t.outerHeight(),a=t.outerWidth(),o=$(window).width(),s=$(window).height(),u=$(document).scrollLeft(),f=$(document).scrollTop();$("#luckysheet-conditionformat-info-dialog").css({left:(o+u-a)/2,top:(s+f-l)/3}).show()},getRuleExplain:function(e){let n=te().conditionformat,t=this.textCellColorHtml(),l;switch(e){case 0:l=`
${n.ruleTypeItem1}\uFF1A
-
- - -
-
-
-
- - -
-
- - -
-
- - -
`;break;case 1:l=`
${n.ruleTypeItem2_title}\uFF1A
-
- -
-
- -
- - -
- - -
- - -
-
-
${n.setFormat}:
${t}`;break;case 2:l=`
${n.ruleTypeItem3_title}\uFF1A
-
- -
- -
- - -
-
${n.setFormat}\uFF1A
${t}`;break;case 3:l=`
${n.ruleTypeItem4_title}\uFF1A
-
- - ${n.selectRange_average} -
-
${n.setFormat}\uFF1A
${t}`;break;case 4:l=`
${n.all}\uFF1A
-
- - ${n.selectRange_value} -
-
${n.setFormat}\uFF1A
${t}`;break;case 5:l=`
${n.ruleTypeItem2_title}\uFF1A
-
-
- - -
-
-
${n.setFormat}:
${t}`;break}return l},daterangeInit:function(e){let n=te().conditionformat;$(".ranges_1 ul").remove(),$("#"+e).find("#daterange-btn").flatpickr({mode:"range",onChange:function(t,l){let[a,o]=t,s=[n.yesterday,n.today],u=[n.lastWeek,n.thisWeek,n.lastMonth,n.thisMonth,n.lastYear,n.thisYear,n.last7days,n.last30days];l==n.all?$("#daterange-btn").val(""):s.indexOf(l)>-1?$("#daterange-btn").val((0,Rs.default)(a).format("YYYY/MM/DD")):u.indexOf(l)>-1&&$("#daterange-btn").val((0,Rs.default)(a).format("YYYY/MM/DD")+"-"+(0,Rs.default)(o).format("YYYY/MM/DD"))}})},CFSplitRange:function(e,n,t,l){let a=[],o=t.row[0]-n.row[0],s=t.column[0]-n.column[0],u=e.row[0],f=e.row[1],A=e.column[0],h=e.column[1];return u>=n.row[0]&&f<=n.row[1]&&A>=n.column[0]&&h<=n.column[1]?l=="allPart"?a=[{row:[u+o,f+o],column:[A+s,h+s]}]:l=="restPart"?a=[]:l=="operatePart"&&(a=[{row:[u+o,f+o],column:[A+s,h+s]}]):u>=n.row[0]&&u<=n.row[1]&&A>=n.column[0]&&h<=n.column[1]?l=="allPart"?a=[{row:[n.row[1]+1,f],column:[A,h]},{row:[u+o,n.row[1]+o],column:[A+s,h+s]}]:l=="restPart"?a=[{row:[n.row[1]+1,f],column:[A,h]}]:l=="operatePart"&&(a=[{row:[u+o,n.row[1]+o],column:[A+s,h+s]}]):f>=n.row[0]&&f<=n.row[1]&&A>=n.column[0]&&h<=n.column[1]?l=="allPart"?a=[{row:[u,n.row[0]-1],column:[A,h]},{row:[n.row[0]+o,f+o],column:[A+s,h+s]}]:l=="restPart"?a=[{row:[u,n.row[0]-1],column:[A,h]}]:l=="operatePart"&&(a=[{row:[n.row[0]+o,f+o],column:[A+s,h+s]}]):un.row[1]&&A>=n.column[0]&&h<=n.column[1]?l=="allPart"?a=[{row:[u,n.row[0]-1],column:[A,h]},{row:[n.row[1]+1,f],column:[A,h]},{row:[n.row[0]+o,n.row[1]+o],column:[A+s,h+s]}]:l=="restPart"?a=[{row:[u,n.row[0]-1],column:[A,h]},{row:[n.row[1]+1,f],column:[A,h]}]:l=="operatePart"&&(a=[{row:[n.row[0]+o,n.row[1]+o],column:[A+s,h+s]}]):A>=n.column[0]&&A<=n.column[1]&&u>=n.row[0]&&f<=n.row[1]?l=="allPart"?a=[{row:[u,f],column:[n.column[1]+1,h]},{row:[u+o,f+o],column:[A+s,n.column[1]+s]}]:l=="restPart"?a=[{row:[u,f],column:[n.column[1]+1,h]}]:l=="operatePart"&&(a=[{row:[u+o,f+o],column:[A+s,n.column[1]+s]}]):h>=n.column[0]&&h<=n.column[1]&&u>=n.row[0]&&f<=n.row[1]?l=="allPart"?a=[{row:[u,f],column:[A,n.column[0]-1]},{row:[u+o,f+o],column:[n.column[0]+s,h+s]}]:l=="restPart"?a=[{row:[u,f],column:[A,n.column[0]-1]}]:l=="operatePart"&&(a=[{row:[u+o,f+o],column:[n.column[0]+s,h+s]}]):An.column[1]&&u>=n.row[0]&&f<=n.row[1]?l=="allPart"?a=[{row:[u,f],column:[A,n.column[0]-1]},{row:[u,f],column:[n.column[1]+1,h]},{row:[u+o,f+o],column:[n.column[0]+s,n.column[1]+s]}]:l=="restPart"?a=[{row:[u,f],column:[A,n.column[0]-1]},{row:[u,f],column:[n.column[1]+1,h]}]:l=="operatePart"&&(a=[{row:[u+o,f+o],column:[n.column[0]+s,n.column[1]+s]}]):u>=n.row[0]&&u<=n.row[1]&&A>=n.column[0]&&A<=n.column[1]?l=="allPart"?a=[{row:[u,n.row[1]],column:[n.column[1]+1,h]},{row:[n.row[1]+1,f],column:[A,h]},{row:[u+o,n.row[1]+o],column:[A+s,n.column[1]+s]}]:l=="restPart"?a=[{row:[u,n.row[1]],column:[n.column[1]+1,h]},{row:[n.row[1]+1,f],column:[A,h]}]:l=="operatePart"&&(a=[{row:[u+o,n.row[1]+o],column:[A+s,n.column[1]+s]}]):u>=n.row[0]&&u<=n.row[1]&&h>=n.column[0]&&h<=n.column[1]?l=="allPart"?a=[{row:[u,n.row[1]],column:[A,n.column[0]-1]},{row:[n.row[1]+1,f],column:[A,h]},{row:[u+o,n.row[1]+o],column:[n.column[0]+s,h+s]}]:l=="restPart"?a=[{row:[u,n.row[1]],column:[A,n.column[0]-1]},{row:[n.row[1]+1,f],column:[A,h]}]:l=="operatePart"&&(a=[{row:[u+o,n.row[1]+o],column:[n.column[0]+s,h+s]}]):f>=n.row[0]&&f<=n.row[1]&&A>=n.column[0]&&A<=n.column[1]?l=="allPart"?a=[{row:[u,n.row[0]-1],column:[A,h]},{row:[n.row[0],f],column:[n.column[1]+1,h]},{row:[n.row[0]+o,f+o],column:[A+s,n.column[1]+s]}]:l=="restPart"?a=[{row:[u,n.row[0]-1],column:[A,h]},{row:[n.row[0],f],column:[n.column[1]+1,h]}]:l=="operatePart"&&(a=[{row:[n.row[0]+o,f+o],column:[A+s,n.column[1]+s]}]):f>=n.row[0]&&f<=n.row[1]&&h>=n.column[0]&&h<=n.column[1]?l=="allPart"?a=[{row:[u,n.row[0]-1],column:[A,h]},{row:[n.row[0],f],column:[A,n.column[0]-1]},{row:[n.row[0]+o,f+o],column:[n.column[0]+s,h+s]}]:l=="restPart"?a=[{row:[u,n.row[0]-1],column:[A,h]},{row:[n.row[0],f],column:[A,n.column[0]-1]}]:l=="operatePart"&&(a=[{row:[n.row[0]+o,f+o],column:[n.column[0]+s,h+s]}]):un.row[1]&&A>=n.column[0]&&A<=n.column[1]?l=="allPart"?a=[{row:[u,n.row[0]-1],column:[A,h]},{row:[n.row[0],n.row[1]],column:[n.column[1]+1,h]},{row:[n.row[1]+1,f],column:[A,h]},{row:[n.row[0]+o,n.row[1]+o],column:[A+s,n.column[1]+s]}]:l=="restPart"?a=[{row:[u,n.row[0]-1],column:[A,h]},{row:[n.row[0],n.row[1]],column:[n.column[1]+1,h]},{row:[n.row[1]+1,f],column:[A,h]}]:l=="operatePart"&&(a=[{row:[n.row[0]+o,n.row[1]+o],column:[A+s,n.column[1]+s]}]):un.row[1]&&h>=n.column[0]&&h<=n.column[1]?l=="allPart"?a=[{row:[u,n.row[0]-1],column:[A,h]},{row:[n.row[0],n.row[1]],column:[A,n.column[0]-1]},{row:[n.row[1]+1,f],column:[A,h]},{row:[n.row[0]+o,n.row[1]+o],column:[n.column[0]+s,h+s]}]:l=="restPart"?a=[{row:[u,n.row[0]-1],column:[A,h]},{row:[n.row[0],n.row[1]],column:[A,n.column[0]-1]},{row:[n.row[1]+1,f],column:[A,h]}]:l=="operatePart"&&(a=[{row:[n.row[0]+o,n.row[1]+o],column:[n.column[0]+s,h+s]}]):An.column[1]&&u>=n.row[0]&&u<=n.row[1]?l=="allPart"?a=[{row:[u,n.row[1]],column:[A,n.column[0]-1]},{row:[u,n.row[1]],column:[n.column[1]+1,h]},{row:[n.row[1]+1,f],column:[A,h]},{row:[u+o,n.row[1]+o],column:[n.column[0]+s,n.column[1]+s]}]:l=="restPart"?a=[{row:[u,n.row[1]],column:[A,n.column[0]-1]},{row:[u,n.row[1]],column:[n.column[1]+1,h]},{row:[n.row[1]+1,f],column:[A,h]}]:l=="operatePart"&&(a=[{row:[u+o,n.row[1]+o],column:[n.column[0]+s,n.column[1]+s]}]):An.column[1]&&f>=n.row[0]&&f<=n.row[1]?l=="allPart"?a=[{row:[u,n.row[0]-1],column:[A,h]},{row:[n.row[0],f],column:[A,n.column[0]-1]},{row:[n.row[0],f],column:[n.column[1]+1,h]},{row:[n.row[0]+o,f+o],column:[n.column[0]+s,n.column[1]+s]}]:l=="restPart"?a=[{row:[u,n.row[0]-1],column:[A,h]},{row:[n.row[0],f],column:[A,n.column[0]-1]},{row:[n.row[0],f],column:[n.column[1]+1,h]}]:l=="operatePart"&&(a=[{row:[n.row[0]+o,f+o],column:[n.column[0]+s,n.column[1]+s]}]):un.row[1]&&An.column[1]?l=="allPart"?a=[{row:[u,n.row[0]-1],column:[A,h]},{row:[n.row[0],n.row[1]],column:[A,n.column[0]-1]},{row:[n.row[0],n.row[1]],column:[n.column[1]+1,h]},{row:[n.row[1]+1,f],column:[A,h]},{row:[n.row[0]+o,n.row[1]+o],column:[n.column[0]+s,n.column[1]+s]}]:l=="restPart"?a=[{row:[u,n.row[0]-1],column:[A,h]},{row:[n.row[0],n.row[1]],column:[A,n.column[0]-1]},{row:[n.row[0],n.row[1]],column:[n.column[1]+1,h]},{row:[n.row[1]+1,f],column:[A,h]}]:l=="operatePart"&&(a=[{row:[n.row[0]+o,n.row[1]+o],column:[n.column[0]+s,n.column[1]+s]}]):l=="allPart"?a=[{row:[u,f],column:[A,h]}]:l=="restPart"?a=[{row:[u,f],column:[A,h]}]:l=="operatePart"&&(a=[]),a},getcolorGradation:function(e,n,t,l,a){let o=e.split(","),s=parseInt(o[0].split("(")[1]),u=parseInt(o[1]),f=parseInt(o[2].split(")")[0]),A=n.split(","),h=parseInt(A[0].split("(")[1]),g=parseInt(A[1]),p=parseInt(A[2].split(")")[0]),v=Math.round(s-(s-h)/(t-l)*(t-a)),y=Math.round(u-(u-g)/(t-l)*(t-a)),b=Math.round(f-(f-p)/(t-l)*(t-a));return"rgb("+v+", "+y+", "+b+")"},getCFPartRange:function(e,n,t){let l=[],a=d.luckysheetfile[Q(e)].luckysheet_conditionformat_save;if(a!=null&&a.length>0){e:for(let o=0;o=f&&range[p].row[0]<=A||range[p].row[1]>=f&&range[p].row[1]<=A||range[p].column[0]>=h&&range[p].column[0]<=g||range[p].column[1]>=h&&range[p].column[1]<=g){l.push(a[o]);continue e}}}}return l},checksCF:function(e,n,t){return t!=null&&e+"_"+n in t?t[e+"_"+n]:null},getComputeMap:function(e){let n=Q(d.currentSheetIndex);e!=null&&(n=Q(e));let t=d.luckysheetfile[n].luckysheet_conditionformat_save,l=d.luckysheetfile[n].data;return l==null?null:this.compute(t,l)},compute:function(e,n){let t=this;e==null&&(e=[]);let l={};if(e.length>0)for(let a=0;af)&&(f=parseInt(v.v)),(A==null||parseInt(v.v)0){let k=Math.round(parseInt(b.v)/f*100)/100;v+"_"+y in l?l[v+"_"+y].dataBar={valueType:"plus",plusLen:h,minusLen:g,valueLen:k,format:u}:l[v+"_"+y]={dataBar:{valueType:"plus",plusLen:h,minusLen:g,valueLen:k,format:u}}}}}}else{let h=1;for(let g=0;gf)&&(f=parseInt(b.v)),(A==null||parseInt(b.v)A&&parseInt(k.v)p&&parseInt(k.v)A&&parseInt(b.v)g)&&(g=parseInt(k.v)),(p==null||parseInt(k.v)=b[0]&&parseInt(T.v)<=b[1]?I+"_"+x in l?l[I+"_"+x].icons={left:A+2,top:h}:l[I+"_"+x]={icons:{left:A+2,top:h}}:parseInt(T.v)>=k[0]&&parseInt(T.v)<=k[1]?I+"_"+x in l?l[I+"_"+x].icons={left:A+1,top:h}:l[I+"_"+x]={icons:{left:A+1,top:h}}:parseInt(T.v)>=w[0]&&parseInt(T.v)<=w[1]&&(I+"_"+x in l?l[I+"_"+x].icons={left:A,top:h}:l[I+"_"+x]={icons:{left:A,top:h}}))}}else if(f==4){let b,k,w,C;y==2?(b=[p,p+v],k=[p+v+1,p+v*2],w=[p+v*2+1,p+v*3],C=[p+v*3+1,g]):y==3?(b=[p,p+v],k=[p+v+1,p+v*2],w=[p+v*2+1,p+v*3+1],C=[p+v*3+2,g]):(b=[p,p+v-1],k=[p+v,p+v*2-1],w=[p+v*2,p+v*3-1],C=[p+v*3,g]);for(let I=0;I=b[0]&&parseInt(R.v)<=b[1]?x+"_"+T in l?l[x+"_"+T].icons={left:A+3,top:h}:l[x+"_"+T]={icons:{left:A+3,top:h}}:parseInt(R.v)>=k[0]&&parseInt(R.v)<=k[1]?x+"_"+T in l?l[x+"_"+T].icons={left:A+2,top:h}:l[x+"_"+T]={icons:{left:A+2,top:h}}:parseInt(R.v)>=w[0]&&parseInt(R.v)<=w[1]?x+"_"+T in l?l[x+"_"+T].icons={left:A+1,top:h}:l[x+"_"+T]={icons:{left:A+1,top:h}}:parseInt(R.v)>=C[0]&&parseInt(R.v)<=C[1]&&(x+"_"+T in l?l[x+"_"+T].icons={left:A,top:h}:l[x+"_"+T]={icons:{left:A,top:h}}))}}else if(f==5){let b,k,w,C,I;y==2?(b=[p,p+v],k=[p+v+1,p+v*2],w=[p+v*2+1,p+v*3],C=[p+v*3+1,p+v*4],I=[p+v*4+1,g]):y==3?(b=[p,p+v],k=[p+v+1,p+v*2],w=[p+v*2+1,p+v*3+1],C=[p+v*3+2,p+v*4+1],I=[p+v*4+2,g]):y==4?(b=[p,p+v],k=[p+v+1,p+v*2+1],w=[p+v*2+2,p+v*3+1],C=[p+v*3+2,p+v*4+2],I=[p+v*4+3,g]):(b=[p,p+v-1],k=[p+v,p+v*2-1],w=[p+v*2,p+v*3-1],C=[p+v*3,p+v*4-1],I=[p+v*4,g]);for(let x=0;x=b[0]&&parseInt(D.v)<=b[1]?T+"_"+R in l?l[T+"_"+R].icons={left:A+4,top:h}:l[T+"_"+R]={icons:{left:A+4,top:h}}:parseInt(D.v)>=k[0]&&parseInt(D.v)<=k[1]?T+"_"+R in l?l[T+"_"+R].icons={left:A+3,top:h}:l[T+"_"+R]={icons:{left:A+3,top:h}}:parseInt(D.v)>=w[0]&&parseInt(D.v)<=w[1]?T+"_"+R in l?l[T+"_"+R].icons={left:A+2,top:h}:l[T+"_"+R]={icons:{left:A+2,top:h}}:parseInt(D.v)>=C[0]&&parseInt(D.v)<=C[1]?T+"_"+R in l?l[T+"_"+R].icons={left:A+1,top:h}:l[T+"_"+R]={icons:{left:A+1,top:h}}:parseInt(D.v)>=I[0]&&parseInt(D.v)<=I[1]&&(T+"_"+R in l?l[T+"_"+R].icons={left:A,top:h}:l[T+"_"+R]={icons:{left:A,top:h}}))}}}}else{let f=e[a].conditionName,A=e[a].conditionValue[0],h=e[a].conditionValue[1],g=u.textColor,p=u.cellColor;for(let v=0;vA?y+"_"+b in l?(l[y+"_"+b].textColor=g,l[y+"_"+b].cellColor=p):l[y+"_"+b]={textColor:g,cellColor:p}:f=="lessThan"&&k.vh?(y=A,b=h):(y=h,b=A);for(let k=s[v].row[0];k<=s[v].row[1];k++)for(let w=s[v].column[0];w<=s[v].column[1];w++){if(n[k]==null||n[k][w]==null)continue;let C=n[k][w];N(C)!="object"||me(C.v)||C.v>=b&&C.v<=y&&(k+"_"+w in l?(l[k+"_"+w].textColor=g,l[k+"_"+w].cellColor=p):l[k+"_"+w]={textColor:g,cellColor:p})}}else if(f=="occurrenceDate"){let y,b;if(A.toString().indexOf("-")==-1)y=it(A)[2],b=it(A)[2];else{let k=A.toString().split("-");y=it(k[1].trim())[2],b=it(k[0].trim())[2]}for(let k=s[v].row[0];k<=s[v].row[1];k++)for(let w=s[v].column[0];w<=s[v].column[1];w++)if(!(n[k]==null||n[k][w]==null)&&n[k][w].ct!=null&&n[k][w].ct.t=="d"){let C=Fe(k,w,n);C>=b&&C<=y&&(k+"_"+w in l?(l[k+"_"+w].textColor=g,l[k+"_"+w].cellColor=p):l[k+"_"+w]={textColor:g,cellColor:p})}}else if(f=="duplicateValue"){let y={};for(let b=s[v].row[0];b<=s[v].row[1];b++)for(let k=s[v].column[0];k<=s[v].column[1];k++){let w=Fe(b,k,n);w in y||(y[w]=[]),y[w].push({r:b,c:k})}if(A=="0"){for(let b in y)if(b!="null"&&b!="undefined"&&y[b].length>1)for(let k=0;kk&&(w+"_"+C in l?(l[w+"_"+C].textColor=g,l[w+"_"+C].cellColor=p):l[w+"_"+C]={textColor:g,cellColor:p})}else if(f=="SubAverage")for(let w=s[v].row[0];w<=s[v].row[1];w++)for(let C=s[v].column[0];C<=s[v].column[1];C++){if(n[w]==null||n[w][C]==null)continue;Fe(w,C,n)0&&(T="="+m.functionCopy(T,"down",R)),D>0&&(T="="+m.functionCopy(T,"right",D));let B=m.execfunction(T)[1];typeof B!="boolean"&&(B=!!Number(B)),!!B&&(I+"_"+x in l?(l[I+"_"+x].textColor=g,l[I+"_"+x].cellColor=p):l[I+"_"+x]={textColor:g,cellColor:p})}}}}return l},updateItem:function(e,n,t){if(!Or(d.currentSheetIndex))return;let l=this,a=Q(d.currentSheetIndex),o=$.extend(!0,[],d.luckysheetfile),s=l.getHistoryRules(o),u;if(e=="delSheet")u=[];else{let h={type:e,cellrange:n,format:t};u=d.luckysheetfile[a].luckysheet_conditionformat_save==null?[]:d.luckysheetfile[a].luckysheet_conditionformat_save,u.push(h)}d.luckysheetfile[a].luckysheet_conditionformat_save=u;let f=$.extend(!0,[],d.luckysheetfile),A=l.getCurrentRules(f);l.ref(s,A),ae.allowUpdate&&ae.saveParam("all",d.currentSheetIndex,u,{k:"luckysheet_conditionformat_save"})},getHistoryRules:function(e){let n=[];for(let t=0;t{el();ll();Ar();Bl();tl();rl();jt();La();Qt();wr();Pt();_t();Fl();wt();dt();je();yt();hr();ca();T0={clearcopy:function(e){let n=window.clipboardData;n||e&&(n=e.originalEvent.clipboardData);let t=" ";if(d.luckysheet_selection_range=[],Et(),n)return n.setData("Text",t),!1;{let l=$("#luckysheet-copy-content").css("visibility","hidden");l.val(t),l.focus(),l.select(),setTimeout(function(){l.blur().css("visibility","visible")},10)}},getHtmlBorderStyle:function(e,n){let t="";return e={0:"none",1:"Thin",2:"Hair",3:"Dotted",4:"Dashed",5:"DashDot",6:"DashDotDot",7:"Double",8:"Medium",9:"MediumDashed",10:"MediumDashDot",11:"MediumDashDotDot",12:"SlantedDashDot",13:"Thick"}[e.toString()],e.indexOf("Medium")>-1?t+="1pt ":e=="Thick"?t+="1.5pt ":t+="0.5pt ",e=="Hair"?t+="double ":e.indexOf("DashDotDot")>-1?t+="dotted ":e.indexOf("DashDot")>-1?t+="dashed ":e.indexOf("Dotted")>-1?t+="dotted ":e.indexOf("Dashed")>-1?t+="dashed ":t+="solid ",t+n+";"},copy:function(e){let n=window.clipboardData;n||(n=e.originalEvent.clipboardData),d.luckysheet_selection_range=[];let t=[],l=[],a=[],o=!1,s=!1;for(let p=0;p0&&(f=rn());let A="",h=Te.deepCopyFlowData(d.flowdata),g="";for(let p=0;p':A+=``;for(let y=0;y':g+=''),d.config.colhidden!=null&&d.config.colhidden[b]!=null)continue;let k='';if(h[v]!=null&&h[v][b]!=null){let w="",C="",I=/^(w|W)((0?)|(0\.0+))$/,x;if(h[v][b].ct!=null&&h[v][b].ct.fa!=null&&h[v][b].ct.fa.match(I)?x=Fe(v,b,h):x=Fe(v,b,h,"m"),w+=be.getStyleByCell(h,v,b),N(h[v][b])=="object"&&"mc"in h[v][b])if("rs"in h[v][b].mc){if(C='rowspan="'+h[v][b].mc.rs+'" colspan="'+h[v][b].mc.cs+'"',f&&f[v+"_"+b]){let T={color:{},style:{}},R={color:{},style:{}},D={color:{},style:{}},S={color:{},style:{}};for(let E=v;E23){let E=null,z=null;for(let P in T.color)T.color[P]>=B/2&&(E=P);for(let P in T.style)T.style[P]>=B/2&&(z=P);E!=null&&z!=null&&(w+="border-left:"+u.getHtmlBorderStyle(z,E))}if(JSON.stringify(R).length>23){let E=null,z=null;for(let P in R.color)R.color[P]>=B/2&&(E=P);for(let P in R.style)R.style[P]>=B/2&&(z=P);E!=null&&z!=null&&(w+="border-right:"+u.getHtmlBorderStyle(z,E))}if(JSON.stringify(D).length>23){let E=null,z=null;for(let P in D.color)D.color[P]>=F/2&&(E=P);for(let P in D.style)D.style[P]>=F/2&&(z=P);E!=null&&z!=null&&(w+="border-top:"+u.getHtmlBorderStyle(z,E))}if(JSON.stringify(S).length>23){let E=null,z=null;for(let P in S.color)S.color[P]>=F/2&&(E=P);for(let P in S.style)S.style[P]>=F/2&&(z=P);E!=null&&z!=null&&(w+="border-bottom:"+u.getHtmlBorderStyle(z,E))}}}else continue;else if(f&&f[v+"_"+b]){if(f[v+"_"+b].l){let T=f[v+"_"+b].l.style,R=f[v+"_"+b].l.color;w+="border-left:"+u.getHtmlBorderStyle(T,R)}if(f[v+"_"+b].r){let T=f[v+"_"+b].r.style,R=f[v+"_"+b].r.color;w+="border-right:"+u.getHtmlBorderStyle(T,R)}if(f[v+"_"+b].b){let T=f[v+"_"+b].b.style,R=f[v+"_"+b].b.color;w+="border-bottom:"+u.getHtmlBorderStyle(T,R)}if(f[v+"_"+b].t){let T=f[v+"_"+b].t.style,R=f[v+"_"+b].t.color;w+="border-top:"+u.getHtmlBorderStyle(T,R)}}k=we(k,{style:w,span:C}),x==null&&(x=Fe(v,b,h)),x==null&&h[v][b]&&h[v][b].ct&&h[v][b].ct.t=="inlineStr"&&(x=h[v][b].ct.s.map(T=>{let R=$('
');return T.v.split(`\r -`).map(S=>{if(!S)return"";let B=$("");return T.fs&&B.css("font-size",`${T.fs}pt`),T.bl&&B.css("font-weight","bold"),T.it&&B.css("font-style","italic"),T.un&&B.css("text-decoration","underline"),T.fc&&B.css("color",T.fc),T.cl?B.append(`${S}`):B.text(S),B[0].outerHTML}).join(R[0].outerHTML)}).join("")),x==null&&(x=""),k+=x}else{let w="";if(f&&f[v+"_"+b]){if(f[v+"_"+b].l){let C=f[v+"_"+b].l.style,I=f[v+"_"+b].l.color;w+="border-left:"+u.getHtmlBorderStyle(C,I)}if(f[v+"_"+b].r){let C=f[v+"_"+b].r.style,I=f[v+"_"+b].r.color;w+="border-right:"+u.getHtmlBorderStyle(C,I)}if(f[v+"_"+b].b){let C=f[v+"_"+b].b.style,I=f[v+"_"+b].b.color;w+="border-bottom:"+u.getHtmlBorderStyle(C,I)}if(f[v+"_"+b].t){let C=f[v+"_"+b].t.style,I=f[v+"_"+b].t.color;w+="border-top:"+u.getHtmlBorderStyle(C,I)}}k+="",k=we(k,{style:w,span:""}),k+=""}k+="",A+=k}A+=""}}if(A=`${g}`+A+"
",d.iscopyself=!0,n)return n.setData("Text",A),!1;{let p=$("#luckysheet-copy-content");p.html(A),p.focus(),p.select(),document.execCommand("selectAll"),document.execCommand("Copy"),setTimeout(function(){$("#luckysheet-copy-content").blur()},10)}},copybyformat:function(e,n){let t=window.clipboardData;t||(t=e.originalEvent&&e.originalEvent.clipboardData),d.luckysheet_selection_range=[{row:d.luckysheet_select_save[0].row,column:d.luckysheet_select_save[0].column}],Et();let l=n;if(d.iscopyself=!0,t)return t.setData("Text",l),!1;{let a=$("#luckysheet-copy-content");a.text(l),a.focus(),a.select(),document.execCommand("selectAll"),document.execCommand("Copy"),setTimeout(function(){a.blur()},10)}},isPasteAction:!1,paste:function(e,n){console.log("==> paste\u4E8B\u4EF6");let t=this;if(d.allowEdit===!1)return;let a=te().drag,o=$("#luckysheet-copy-content");o.focus(),o.select(),setTimeout(function(){let s=o.html();s.indexOf("luckysheet_copy_action_table")>-1&&d.luckysheet_copy_save.copyRange!=null&&d.luckysheet_copy_save.copyRange.length>0?d.luckysheet_paste_iscut?(d.luckysheet_paste_iscut=!1,t.pasteHandlerOfCutPaste(d.luckysheet_copy_save),t.clearcopy(e)):t.pasteHandlerOfCopyPaste(d.luckysheet_copy_save):s.indexOf("luckysheet_copy_action_image")>-1?Se.pasteImgItem():n!="btn"?t.pasteHandler(s):he()?alert(a.pasteMustKeybordAlert):U.info(a.pasteMustKeybordAlertHTMLTitle,a.pasteMustKeybordAlertHTML)},10)},pasteHandler:function(e,n){if(!xr(d.luckysheet_select_save,d.currentSheetIndex)||d.allowEdit===!1)return;let l=te().paste;if(d.luckysheet_select_save.length>1&&(he()?alert(l.errorNotAllowMulti):U.info(`${l.warning}`,l.errorNotAllowMulti)),typeof e=="object"){if(e.length==0)return;let a=$.extend(!0,{},d.config);a.merge==null&&(a.merge={}),JSON.stringify(n).length>2&&a.borderInfo==null&&(a.borderInfo=[]);let o=e.length,s=e[0].length,u=d.luckysheet_select_save[0].row[0],f=u+o-1,A=d.luckysheet_select_save[0].column[0],h=A+s-1,g=!1;if(a.merge!=null&&(g=Bt(a,u,f,A,h)),g){he()?alert(l.errorNotAllowMerged):U.info(`${l.warning}`,l.errorNotAllowMerged);return}let p=Te.deepCopyFlowData(d.flowdata),v=p.length,y=p[0].length,b=f-v+1,k=h-y+1;(b>0||k>0)&&(p=fl([].concat(p),b,k,!0)),a.rowlen==null&&(a.rowlen={});let w=!1,C={};for(let I=u;I<=f;I++){let x=[].concat(p[I]),T=d.defaultrowlen;a.rowlen[I]!=null&&(T=a.rowlen[I]);for(let R=A;R<=h;R++){N(x[R])=="object"&&"mc"in x[R]&&("rs"in x[R].mc&&delete a.merge[x[R].mc.r+"_"+x[R].mc.c],delete x[R].mc);let D=null;if(e[I-u]!=null&&e[I-u][R-A]!=null&&(D=e[I-u][R-A]),x[R]=$.extend(!0,{},D),D!=null&&"mc"in x[R]&&(x[R].mc.rs!=null?(x[R].mc.r=I,x[R].mc.c=R,a.merge[x[R].mc.r+"_"+x[R].mc.c]=x[R].mc,C[D.mc.r+"_"+D.mc.c]=[x[R].mc.r,x[R].mc.c]):x[R]={mc:{r:C[D.mc.r+"_"+D.mc.c][0],c:C[D.mc.r+"_"+D.mc.c][1]}}),n[I-u+"_"+(R-A)]){let F={rangeType:"cell",value:{row_index:I,col_index:R,l:n[I-u+"_"+(R-A)].l,r:n[I-u+"_"+(R-A)].r,t:n[I-u+"_"+(R-A)].t,b:n[I-u+"_"+(R-A)].b},timestamp:new Date().getTime()};a.borderInfo.push(F)}let S=Ra(x[R]),B=be.getTextSize("\u7530",S)[1];B>T&&(T=B,w=!0)}p[I]=x,T!=d.defaultrowlen&&(a.rowlen[I]=T)}if(d.luckysheet_select_save=[{row:[u,f],column:[A,h]}],b>0||k>0||w){let I={cfg:a,RowlChange:!0};Je(p,d.luckysheet_select_save,I)}else{let I={cfg:a};Je(p,d.luckysheet_select_save,I),at()}}else{e=e.replace(/\r/g,"");let a=[],o=e.split(` -`),s=o[0].split(" ").length;for(let k=0;k${l.warning}`,l.errorNotAllowMerged);return}let y=A+g-u.length,b=h+p-u[0].length;(y>0||b>0)&&(u=fl([].concat(u),y,b,!0));for(let k=0;k0&&(I.f="",m.delFunctionGroup(k+A,C+h,d.currentSheetIndex));else{let T={},R=it(x);T.v=R[2],T.ct=R[1],T.m=R[0],w[C+h]=T}}u[k+A]=w}if(f.row=[A,A+g-1],f.column=[h,h+p-1],y>0||b>0){let k={RowlChange:!0};Je(u,d.luckysheet_select_save,k)}else Je(u,d.luckysheet_select_save),at()}},pasteHandlerOfCutPaste:function(e){if(!xr(d.luckysheet_select_save,d.currentSheetIndex)||d.allowEdit===!1)return;let t=te().paste,l=$.extend(!0,{},d.config);l.merge==null&&(l.merge={});let a=e.HasMC,o=e.RowlChange,s=e.dataSheetIndex,u=e.copyRange[0].row[0],f=e.copyRange[0].row[1],A=e.copyRange[0].column[0],h=e.copyRange[0].column[1],g=$.extend(!0,[],Ct({row:[u,f],column:[A,h]},s)),p=g.length,v=g[0].length,y=d.luckysheet_select_save[d.luckysheet_select_save.length-1],b=y.row_focus,k=b+p-1,w=y.column_focus,C=w+v-1,I=!1;if(l.merge!=null&&(I=Bt(l,b,k,w,C)),I){he()?alert(t.errorNotAllowMerged):U.info(`${t.warning}`,t.errorNotAllowMerged);return}let x=Te.deepCopyFlowData(d.flowdata),T=x.length,R=x[0].length,D=p+b-T,S=v+w-R;(D>0||S>0)&&(x=fl([].concat(x),D,S,!0));let B=rn(s),F=$.extend(!0,{},d.luckysheetfile[Q(s)].dataVerification),E=$.extend(!0,{},d.luckysheetfile[Q(d.currentSheetIndex)].dataVerification);if(d.currentSheetIndex==s){for(let O=u;O<=f;O++)for(let q=A;q<=h;q++){let j=x[O][q];N(j)=="object"&&"mc"in j&&("rs"in j.mc&&delete l.merge[j.mc.r+"_"+j.mc.c],delete j.mc),x[O][q]=null,delete E[O+"_"+q]}if(l.borderInfo&&l.borderInfo.length>0){let O=[];for(let q=0;q=u&&G<=f&&_>=A&&_<=h||O.push(l.borderInfo[q])}}l.borderInfo=O}}let z={};for(let O=b;O<=k;O++){let q=[].concat(x[O]);for(let j=w;j<=C;j++){if(B[u+O-b+"_"+(A+j-w)]){let _={rangeType:"cell",value:{row_index:O,col_index:j,l:B[u+O-b+"_"+(A+j-w)].l,r:B[u+O-b+"_"+(A+j-w)].r,t:B[u+O-b+"_"+(A+j-w)].t,b:B[u+O-b+"_"+(A+j-w)].b},timestamp:new Date().getTime()};l.borderInfo==null&&(l.borderInfo=[]),l.borderInfo.push(_)}else if(B[O+"_"+j]){let _={rangeType:"cell",value:{row_index:O,col_index:j,l:null,r:null,t:null,b:null},timestamp:new Date().getTime()};l.borderInfo==null&&(l.borderInfo=[]),l.borderInfo.push(_)}F[u+O-b+"_"+(A+j-w)]&&(E[O+"_"+j]=F[u+O-b+"_"+(A+j-w)]),N(q[j])=="object"&&"mc"in q[j]&&("rs"in q[j].mc&&delete l.merge[q[j].mc.r+"_"+q[j].mc.c],delete q[j].mc);let G=null;g[O-b]!=null&&g[O-b][j-w]!=null&&(G=g[O-b][j-w]),q[j]=$.extend(!0,{},G),G!=null&&a&&"mc"in q[j]&&(q[j].mc.rs!=null?(q[j].mc.r=O,q[j].mc.c=j,l.merge[q[j].mc.r+"_"+q[j].mc.c]=q[j].mc,z[G.mc.r+"_"+G.mc.c]=[q[j].mc.r,q[j].mc.c]):q[j]={mc:{r:z[G.mc.r+"_"+G.mc.c][0],c:z[G.mc.r+"_"+G.mc.c][1]}})}x[O]=q}y.row=[b,k],y.column=[w,C],o&&(d.currentSheetIndex!=s?l=vl(x,b,k,l):(l=vl(x,u,f,l),l=vl(x,b,k,l)));let P,L;if(d.currentSheetIndex!=s){let O=$.extend(!0,[],d.luckysheetfile[Q(s)].data),q=$.extend(!0,{},d.luckysheetfile[Q(s)].config),j=$.extend(!0,[],O),G=$.extend(!0,{},q);G.merge==null&&(G.merge={});for(let ue=u;ue<=f;ue++)for(let fe=A;fe<=h;fe++){let ce=j[ue][fe];N(ce)=="object"&&"mc"in ce&&("rs"in ce.mc&&delete G.merge[ce.mc.r+"_"+ce.mc.c],delete ce.mc),j[ue][fe]=null}if(o&&(G=vl(j,u,f,G)),G.borderInfo&&G.borderInfo.length>0){let ue=[];for(let fe=0;fe=u&&K<=f&&W>=A&&W<=h||ue.push(G.borderInfo[fe])}}G.borderInfo=ue}let _=$.extend(!0,[],d.luckysheetfile[Q(s)].luckysheet_conditionformat_save),ee=$.extend(!0,[],_),re=[];if(ee!=null&&ee.length>0)for(let ue=0;ue0&&(K=K.concat(de))}if(ee[ue].cellrange=ce,K.length>0){let W=$.extend(!0,{},ee[ue]);W.cellrange=K,re.push(W)}}let oe=$.extend(!0,[],d.luckysheetfile[Q(d.currentSheetIndex)].luckysheet_conditionformat_save),se=$.extend(!0,[],oe);re.length>0&&(se=se.concat(re));for(let ue=u;ue<=f;ue++)for(let fe=A;fe<=h;fe++)delete F[ue+"_"+fe];P={sheetIndex:s,data:O,curData:j,config:q,curConfig:G,cdformat:_,curCdformat:ee,dataVerification:$.extend(!0,{},d.luckysheetfile[Q(s)].dataVerification),curDataVerification:F,range:{row:[u,f],column:[A,h]}},L={sheetIndex:d.currentSheetIndex,data:d.flowdata,curData:x,config:$.extend(!0,{},d.config),curConfig:l,cdformat:oe,curCdformat:se,dataVerification:$.extend(!0,{},d.luckysheetfile[Q(d.currentSheetIndex)].dataVerification),curDataVerification:E,range:{row:[b,k],column:[w,C]}}}else{let O=$.extend(!0,[],d.luckysheetfile[Q(d.currentSheetIndex)].luckysheet_conditionformat_save),q=$.extend(!0,[],O);if(q!=null&&q.length>0)for(let j=0;j0||S>0?xi(P,L,!0):xi(P,L,o)},pasteHandlerOfCopyPaste:function(e){if(!xr(d.luckysheet_select_save,d.currentSheetIndex))return;let t=te().paste,l=$.extend(!0,{},d.config);l.merge==null&&(l.merge={});let a=e.HasMC,o=e.RowlChange,s=e.dataSheetIndex,u=e.copyRange[0].row[0],f=e.copyRange[0].row[1],A=e.copyRange[0].column[0],h=e.copyRange[0].column[1],g=[],p=!1;for(let se=0;se1?u==e.copyRange[1].row[0]&&f==e.copyRange[1].row[1]?(ue=ue[0].map(function(fe,ce){return ue.map(function(K){return K[ce]})}),g=g.concat(ue),p=!0):A==e.copyRange[1].column[0]&&h==e.copyRange[1].column[1]&&(g=g.concat(ue)):g=ue}p&&(g=g[0].map(function(se,ue){return g.map(function(fe){return fe[ue]})}));let v=$.extend(!0,[],g);if(e.copyRange.length>1)for(let se=0;se${t.warning}`,t.errorNotAllowMerged);return}let S=(C-w+1)/y,B=(x-I+1)/b,F=Te.deepCopyFlowData(d.flowdata),E=F.length,z=F[0].length,P=y+w-E,L=b+I-z;(P>0||L>0)&&(F=fl([].concat(F),P,L,!0));let O=rn(s),q=$.extend(!0,{},d.luckysheetfile[Q(s)].dataVerification),j=null,G=0,_=0,ee=0,re=0;for(let se=1;se<=S;se++)for(let ue=1;ue<=B;ue++){G=w+(se-1)*y,_=I+(ue-1)*b,re=w+se*y,ee=I+ue*b;let fe=G-u,ce=_-A,K={};for(let W=G;W0&&(ke="="+m.functionCopy(ke,"down",fe)),fe<0&&(ke="="+m.functionCopy(ke,"up",Math.abs(fe))),ce>0&&(ke="="+m.functionCopy(ke,"right",ce)),ce<0&&(ke="="+m.functionCopy(ke,"left",Math.abs(ce)));let ie=m.execfunction(ke,W,de,void 0,!0);le.spl!=null?(le.f=ie[2],le.v=ie[1],le.spl=ie[3].data):(le.f=ie[2],le.v=ie[1],le.ct!=null&&le.ct.fa!=null&&(le.m=ht(le.ct.fa,ie[1])))}J[de]=$.extend(!0,{},le),le!=null&&a&&"mc"in J[de]&&(J[de].mc.rs!=null?(J[de].mc.r=W,J[de].mc.c=de,l.merge[J[de].mc.r+"_"+J[de].mc.c]=J[de].mc,K[le.mc.r+"_"+le.mc.c]=[J[de].mc.r,J[de].mc.c]):J[de]={mc:{r:K[le.mc.r+"_"+le.mc.c][0],c:K[le.mc.r+"_"+le.mc.c][1]}})}F[W]=J}}let oe=null;if(e.copyRange.length==1){let se=d.luckysheetfile[Q(s)],ue=d.luckysheetfile[Q(d.currentSheetIndex)],fe=$.extend(!0,[],se.luckysheet_conditionformat_save);if(fe!=null&&fe.length>0){oe=$.extend(!0,[],ue.luckysheet_conditionformat_save);for(let ce=0;ce0&&(W=W.concat(ke))}}W.length>0&&(fe[ce].cellrange=W,oe.push(fe[ce]))}}}if(k.row=[w,C],k.column=[I,x],o||P>0||L>0){l=vl(F,w,C,l);let se={cfg:l,RowlChange:!0,cdformat:oe,dataVerification:j};Je(F,d.luckysheet_select_save,se)}else{let se={cfg:l,cdformat:oe,dataVerification:j};Je(F,d.luckysheet_select_save,se),at()}},pasteHandlerOfPaintModel:function(e){if(!xr(d.luckysheet_select_save,d.currentSheetIndex))return;let t=te().paste,l=$.extend(!0,{},d.config);l.merge==null&&(l.merge={});let a=e.HasMC,o=e.RowlChange,s=e.dataSheetIndex,u=e.copyRange[0].row[0],f=e.copyRange[0].row[1],A=e.copyRange[0].column[0],h=e.copyRange[0].column[1],g=$.extend(!0,[],Ct({row:[u,f],column:[A,h]},s)),p=d.luckysheet_select_save[d.luckysheet_select_save.length-1],v=p.row[0],y=p.row[1],b=p.column[0],k=p.column[1],w=g.length,C=g[0].length;if(v==y&&b==k){let j=!1;if(l.merge!=null&&(j=Bt(l,v,v+w-1,b,b+C-1)),j){he()?alert(t.errorNotAllowMerged):U.info(`${t.warning}`,t.errorNotAllowMerged);return}y=v+w-1,k=b+C-1}let I=Math.ceil((y-v+1)/w),x=Math.ceil((k-b+1)/C),T=Te.deepCopyFlowData(d.flowdata),R=T[0].length,D=T.length,S=rn(s),B=$.extend(!0,{},d.luckysheetfile[Q(s)].dataVerification),F=null,E=0,z=0,P=0,L=0;for(let j=1;j<=I;j++)for(let G=1;G<=x;G++){E=v+(j-1)*w,z=b+(G-1)*C,L=v+j*w>D?D:v+j*w,L>y+1&&(L=y+1),P=b+G*C>R?R:b+G*C,P>k+1&&(P=k+1);let _={};for(let ee=E;ee{Reflect.deleteProperty(re[oe],fe)}):re[oe]={v:re[oe]},re[oe]=$.extend(!0,re[oe],se),re[oe].ct&&re[oe].ct.t==="inlineStr"&&re[oe].ct.s.forEach(ue=>ue=$.extend(!0,ue,se)),a&&"mc"in re[oe]&&(re[oe].mc.rs!=null?(re[oe].mc.r=ee,re[oe].mc.rs+ee>=L&&(re[oe].mc.rs=L-ee),re[oe].mc.c=oe,re[oe].mc.cs+oe>=P&&(re[oe].mc.cs=P-oe),l.merge[re[oe].mc.r+"_"+re[oe].mc.c]=re[oe].mc,_[se.mc.r+"_"+se.mc.c]=[re[oe].mc.r,re[oe].mc.c]):re[oe]={mc:{r:_[se.mc.r+"_"+se.mc.c][0],c:_[se.mc.r+"_"+se.mc.c][1]}}),re[oe].v!=null&&se.ct!=null&&se.ct.fa!=null)){let ue=ht(se.ct.fa,re[oe].v);re[oe].m=ue}}T[ee]=re}}let O=null,q=$.extend(!0,[],d.luckysheetfile[Q(s)].luckysheet_conditionformat_save);if(q!=null&&q.length>0){O=$.extend(!0,[],d.luckysheetfile[Q(d.currentSheetIndex)].luckysheet_conditionformat_save);for(let j=0;j0&&(_=_.concat(re))}_.length>0&&(q[j].cellrange=[{row:[v,y],column:[b,k]}],O.push(q[j]))}}if(p.row=[v,y],p.column=[b,k],o){l=vl(T,v,y,l);let j={cfg:l,RowlChange:!0,cdformat:O,dataVerification:F};Je(T,d.luckysheet_select_save,j)}else{y>=T.length&&(y=T.length-1),l=vl(T,v,y,l);let j={cfg:l,RowlChange:!0,cdformat:O,dataVerification:F};Je(T,d.luckysheet_select_save,j),at()}},matchcopy:function(e,n){let t=[],l=[],a,o;if(typeof e=="object")t=e;else{t=e.split(` -`);for(let s=0;s{Ci();ll()});function zu(e,n,t,l,a){let o=Ed();o=we(o,{logotitle:a}),o=we(o,{menu:Yd()});let s=Bd;d.config==null&&(d.config={}),Mn(n,e);let f=te().info,A=''+f.row+'('+f.addLast+")",h=' ",g=we(f.pageInfo,{total:Ae.total?Ae.total:"",totalPage:Ae.pageInfo.totalPage?Ae.pageInfo.totalPage:"",currentPage:Ae.pageInfo.currentPage?Ae.pageInfo.currentPage:""}),p=' '+g+' ',v=' '+g+"",y="";Ae.enableAddRow&&(y+=A),Ae.enablePage&&(parseInt(Ae.pageInfo.totalPage)==1?y+=v:y+=p),Ae.enableAddBackTop&&(y+=h);let b=we('
'+y+"
",{height:d.rh_height,width:d.ch_width-1}),k=we(Ld,{width:d.ch_width,index:0,column:""});s=we(s,{width:d.ch_width,flow:b,index:0}),o=we(o,{flow:s,rowHeader:"
",columnHeader:k,functionButton:Ae.functionButton}),$("#"+d.container).append(o),$("#luckysheet-scrollbar-x div").width(d.ch_width),$("#luckysheet-scrollbar-y div").height(d.rh_height+d.columnHeaderHeight-d.cellMainSrollBarSize-3),$("body").append(Hd),$("body").append(Pd),$("body").append(zd()),$("body").append(qd),$("body").append(we(qo(),{menuid:"filter"})),$("body").append(we(Ho(),{menuid:"filter"})),$("body").append(jd()),$("#luckysheet-rows-h").width(d.rowHeaderWidth-1.5),$("#luckysheet-cols-h-c").height(d.columnHeaderHeight-1.5),$("#luckysheet-left-top").css({width:d.rowHeaderWidth-1.5,height:d.columnHeaderHeight-1.5}),$("#luckysheet_info_detail_input").val(Ae.title)}var sm=De(()=>{Wt();yl();ln();Qt();tl();lo();dt();je();yt()});var I0,ve,cl=De(()=>{Pt();$r();Qt();zr();sm();rl();jt();_t();lo();tl();Fa();Pt();dt();Wt();$t();yl();vn();Nr();ln();hr();mr();gi();Kl();zn();el();je();yt();qr();Nr();wi();ll();Xl();Hn();fi();I0={generateRandomSheetIndex:function(e){e==null&&(e="Sheet");let n=window.navigator.userAgent.replace(/[^a-zA-Z0-9]/g,"").split(""),t="";for(let a=0;a<12;a++)t+=n[Math.round(Math.random()*(n.length-1))];let l=new Date().getTime();return e+"_"+t+"_"+l},generateRandomSheetName:function(e,n){let t=e.length,a=te().pivotTable.title;for(let o=0;o-1||e[o].name.indexOf(a)>-1){let s=parseFloat(e[o].name.replace("Sheet","").replace(a,""));s!="NaN"&&Math.ceil(s)>t&&(t=Math.ceil(s))}return n?a+(t+1):"Sheet"+(t+1)},generateCopySheetName:function(e,n){let l=te().info,a="("+l.copy,o=n.toString().indexOf(a);~o&&(n=n.toString().substring(0,o));let s="",u=n+a,f=[];for(let h=0;h=s&&(s=parseInt(y)+1)}}let A;do{let h=a+s+")",g=31-h.length;A=n,A.length>g&&(A=A.slice(0,g-1)+"\u2026"),A=A+h}while(~f.indexOf(A)&&(s=(s||1)+1));return A},getSheetByIndex:function(e){let n=this;e==null&&(e=d.currentSheetIndex);let t=n.getSheetIndex(e);return d.luckysheetfile[t]},getSheetByName:function(e){let n=this;if(e==null)return null;for(let t=0;t{typeof t.index=="undefined"&&(t.index=this.generateRandomSheetIndex()),n.includes(t.index)?t.index=this.generateRandomSheetIndex():n.push(t.index),typeof t.status=="undefined"&&(t.status=0),t.status==1&&(e?t.status=0:e=!0)}),e||(d.luckysheetfile[0].status=1)}d.currentSheetIndex=d.luckysheetfile[0].index;for(let e=0;e'),al(e),ae.saveParam("sha",null,$.extend(!0,{},s)),d.clearjfundo){d.jfundo.length=0;let u={};u.type="addSheet",u.sheetconfig=$.extend(!0,{},s),u.index=a,u.currentSheetIndex=d.currentSheetIndex,d.jfredo.push(u)}t.changeSheetExec(a,n,!0)},setSheetHide:function(e,n){let t=this,l=t.getSheetIndex(e);if(!n&&!$e.createHookFunction("sheetHideBefore",{sheet:d.luckysheetfile[l]}))return;d.luckysheetfile[l].hide=1;let a=$("#luckysheet-sheets-item"+e);a.hide(),$("#luckysheet-sheet-area div.luckysheet-sheets-item").removeClass("luckysheet-sheets-item-active");let o;if(Ae.showsheetbarConfig.sheet)o=a.nextAll(":visible"),a.nextAll(":visible").length>0?o=o.eq(0).data("index"):o=a.prevAll(":visible").eq(0).data("index");else{let s,u=[];d.luckysheetfile.forEach((A,h)=>{A.hide!==1&&u.push(h)});let f=u.length;f===1?s=u[0]:s=u[f-1]>l?u.find(A=>A>l):u[f-1],o=d.luckysheetfile[s].index}$("#luckysheet-sheets-item"+o).addClass("luckysheet-sheets-item-active"),t.changeSheetExec(o),t.locationSheet(),ae.saveParam("sh",a.data("index"),1,{op:"hide",cur:o}),n||$e.createHookFunction("sheetHideAfter",{sheet:d.luckysheetfile[l]})},setSheetShow:function(e){let n=this,t=d.luckysheetfile[n.getSheetIndex(e)];!$e.createHookFunction("sheetShowBefore",{sheet:t})||(t.hide=0,n.changeSheetExec(e),ae.saveParam("sh",e,0,{op:"show",cur:null}),$e.createHookFunction("sheetShowAfter",{sheet:t}))},sheetMaxIndex:0,ordersheet:function(e){return function(n,t){let l=n[e],a=t[e];return l-a}},getCurrentOrder:function(){let e={};return $("#luckysheet-sheet-area div.luckysheet-sheets-item").each(function(n){let t=$(this).data("index");for(let l=0;l{let l=n.order,a=t.order;return l!=null&&a!=null?l-a:l!=null?-1:(a!=null,1)})},createSheet:function(){let e=this,n=[];d.luckysheetfile.sort(e.ordersheet("order"));for(let t=0;t'),d.currentSheetIndex==a?n.push(we(sr,{index:a,active:"luckysheet-sheets-item-active",name:d.luckysheetfile[t].name,style:"",colorset:o})):(d.luckysheetfile[t].hide==1?n.push(we(sr,{index:a,active:"",name:d.luckysheetfile[t].name,style:"display:none;",colorset:o})):n.push(we(sr,{index:a,active:"",name:d.luckysheetfile[t].name,style:"",colorset:o})),l="style='display:none;'"),$("#luckysheet-cell-main").append("
')}$("#luckysheet-sheet-container-c").append(n.join("")),e.locationSheet()},locationSheet:function(){let e=$("#luckysheet-sheet-container-c"),n=$("#"+d.container).width(),t=$("#luckysheet-sheet-container-c > div.luckysheet-sheets-item-active").eq(0),l=0,a=0;$("#luckysheet-sheet-container-c > div.luckysheet-sheets-item:visible").each(function(){$(this).hasClass("luckysheet-sheets-item-active")&&(l=a),a+=$(this).outerWidth()}),setTimeout(function(){e.scrollLeft(l-10),Ae.showsheetbarConfig.sheet&&(a>=n*.7?($("#luckysheet-sheet-area .luckysheet-sheets-scroll").css("display","inline-block"),$("#luckysheet-sheet-container .docs-sheet-fade-left").show()):($("#luckysheet-sheet-area .luckysheet-sheets-scroll").css("display","none"),$("#luckysheet-sheet-container .docs-sheet-fade-left").hide()))},1)},copySheet:function(e,n){if(he()||d.allowEdit===!1)return;let t=this,l=d.luckysheetfile.length,a=t.generateRandomSheetIndex(),o=t.getSheetIndex(e),s=$.extend(!0,{},d.luckysheetfile[o]);if(s.order=l,s.index=a,s.name=t.generateCopySheetName(d.luckysheetfile,s.name),!$e.createHookFunction("sheetCopyBefore",{targetSheet:d.luckysheetfile[o],copySheet:s}))return;let u="";s.color!=null&&(u='
');let f=$("#luckysheet-sheets-item"+e);if($("#luckysheet-sheet-container-c").append(we(sr,{index:s.index,active:"",name:s.name,order:s.order,style:"",colorset:u})),$("#luckysheet-sheets-item"+s.index).insertAfter(f),d.luckysheetfile.splice(o+1,0,s),$("#luckysheet-sheet-area div.luckysheet-sheets-item").removeClass("luckysheet-sheets-item-active"),$("#luckysheet-sheets-item"+a).addClass("luckysheet-sheets-item-active"),$("#luckysheet-cell-main").append('
'),al(n),ae.saveParam("shc",a,{copyindex:e,name:s.name}),t.changeSheetExec(a,void 0,void 0,!0),t.reOrderAllSheet(),d.clearjfundo)d.jfredo.push({type:"copySheet",copyindex:e,index:s.index,sheetIndex:s.index});else if(d.jfredo.length>0){let A=d.jfredo[d.jfredo.length-1];A.type=="copySheet"&&(A.index=s.index,A.sheetIndex=s.index)}},hasSheet:function(e){return!(e==null||(e=this.getSheetIndex(e),e==null))},createSheetbydata:function(e,n,t=!0){let l=this,a="";if(e.color!=null&&(a='
'),$("#luckysheet-sheet-container-c").append(we(sr,{index:e.index,active:"",name:e.name,order:e.order,style:"",colorset:a})),t){let o=e.order;o>=d.luckysheetfile.length?(o=d.luckysheetfile.length-1,$("#luckysheet-sheets-item"+e.index).insertAfter($("#luckysheet-sheets-item"+d.luckysheetfile[o].index))):$("#luckysheet-sheets-item"+e.index).insertBefore($("#luckysheet-sheets-item"+d.luckysheetfile[o].index))}d.luckysheetfile.push(e),$("#luckysheet-sheet-area div.luckysheet-sheets-item").removeClass("luckysheet-sheets-item-active"),$("#luckysheet-sheets-item"+e.index).addClass("luckysheet-sheets-item-active"),$("#luckysheet-cell-main").append('
'),al(),n!=null?(ae.saveParam("shre",null,{reIndex:e.index}),e.hide=0,ae.saveParam("sh",e.index,0,{op:"show",cur:null})):ae.saveParam("sha",null,e),l.changeSheetExec(e.index,e.isPivotTable,!0),l.reOrderAllSheet()},deleteSheet:function(e){let n=this;if(d.allowEdit===!1)return;let t=n.getSheetIndex(e),l=d.luckysheetfile[t];if(!$e.createHookFunction("sheetDeleteBefore",{sheet:l}))return;n.setSheetHide(e,!0),$("#luckysheet-sheets-item"+e).remove(),$("#luckysheet-datavisual-selection-set-"+e).remove();let a=d.luckysheetfile.splice(t,1);n.reOrderAllSheet(),ae.saveParam("shd",null,{deleIndex:e}),d.clearjfundo&&(a[0].type="deleteSheet",d.jfredo.push(a[0])),$e.createHookFunction("sheetDeleteAfter",{sheet:l})},nulldata:null,getGridData:function(e){let n=[];for(let t=0;t0?e.data:fl([],n,t),a=e.celldata;if(e.data&&e.data.length>0)for(let o=0;o0)for(let o=0;o=l.length&&(l=fl(l,u-l.length+1,0)),f>=l[0].length&&(l=fl(l,0,f-l[0].length+1)),Xt(u,f,l,A)}return Ae.autoFormatw=!1,Ae.accuracy=void 0,l},cutGridData:function(e){let n=0;for(let t=e.length-1;t>=0;t--){let l=!0;for(let a=0;a0){l=!1;break}}if(l)n=t;else break}return e.slice(0,n)},addGridData:function(e,n,t){let l=fl([],n,t);if(e!=null)for(let a=0;a=l.length&&(l=fl(l,s-l.length+1,0)),u>=l[0].length&&(l=fl(l,0,u-l[0].length+1)),Xt(s,u,l,f)}return l},sheetParamRestore:function(e,n){if(d.luckysheet_select_save=e.luckysheet_select_save,(d.luckysheet_select_save==null||d.luckysheet_select_save.length==0)&&(n[0]!=null&&n[0][0]!=null&&n[0][0].mc!=null?d.luckysheet_select_save=[{row:[0,n[0][0].mc.rs-1],column:[0,n[0][0].mc.cs-1]}]:d.luckysheet_select_save=[{row:[0,0],column:[0,0]}]),d.luckysheet_selection_range=e.luckysheet_selection_range==null?[]:e.luckysheet_selection_range,d.config=e.config==null?{}:e.config,d.zoomRatio=e.zoomRatio==null?1:e.zoomRatio,e.defaultRowHeight!=null?d.defaultrowlen=parseFloat(e.defaultRowHeight):d.defaultrowlen=Ae.defaultRowHeight,e.defaultColWidth!=null?d.defaultcollen=parseFloat(e.defaultColWidth):d.defaultcollen=Ae.defaultColWidth,e.showGridLines!=null){let t=e.showGridLines;t==0||t==!1?d.showGridLines=!1:d.showGridLines=!0}else d.showGridLines=!0},initialjfFile:function(e,n){let t=this;t.getCurSheet();let l=d.luckysheetfile[t.getSheetIndex(d.currentSheetIndex)];t.nulldata=fl([],d.defaultrowNum,d.defaultcolumnNum);let a=t.buildGridData(l),o=[];l.jfgird_select_save=l.jfgird_select_save||[],l.jfgird_select_save.forEach(h=>o.push({row:h.row,column:h.column})),l.luckysheet_select_save=o,this.sheetParamRestore(l,a);let s=d.luckysheet_select_save[0].row[1],u=d.luckysheet_select_save[0].column[1];if(d.luckysheet_select_save.length>1)for(let h=0;hs&&(s=d.luckysheet_select_save[h].row[1]),d.luckysheet_select_save[h].column[1]>u&&(u=d.luckysheet_select_save[h].column[1]);be.fontInitial(d.fontList),l.data=a;let f=a.length;s>f-1&&(f=s+1);let A=a[0].length;u>A-1&&(A=u+1),typeof Ae.beforeCreateDom=="function"&&Ae.beforeCreateDom(luckysheet),typeof Ae.workbookCreateBefore=="function"&&Ae.workbookCreateBefore(luckysheet),zu(A,f,a,e,n),setTimeout(function(){U.createHoverTip("#luckysheet_info_detail",".luckysheet-icon-menu-btn, .luckysheet_info_detail_input, .luckysheet_info_detail_update"),U.createHoverTip("#luckysheet-wa-editor",".luckysheet-toolbar-menu-button, .luckysheet-toolbar-button, .luckysheet-toolbar-combo-button"),d.luckysheetTableContentHW=[$("#luckysheet-cell-main").width()+d.rowHeaderWidth-d.cellMainSrollBarSize,$("#luckysheet-cell-main").height()+d.columnHeaderHeight-d.cellMainSrollBarSize],$("#luckysheetTableContent, #luckysheetTableContentF").attr({width:Math.ceil(d.luckysheetTableContentHW[0]*d.devicePixelRatio),height:Math.ceil(d.luckysheetTableContentHW[1]*d.devicePixelRatio)}).css({width:d.luckysheetTableContentHW[0],height:d.luckysheetTableContentHW[1]}).get(0).getContext("2d");let h=te().info,p=ae.gridKey+"__qkcache",v=function(){l.load="1",t.createSheet();let y=function(){t.mergeCalculation(l.index),t.setSheetParam(!1),t.storeSheetParam(),t.restoreselect(),t.CacheNotLoadControll=[],t.restoreCache(),m.execFunctionGroupForce(Ae.forceCalculation),t.restoreSheetAll(d.currentSheetIndex),$("#luckysheet_info_detail_save").html(h.detailSave),l.isPivotTable?d.luckysheetcurrentisPivotTable=!0:(d.luckysheetcurrentisPivotTable=!1,$("#luckysheet-modal-dialog-slider-pivot").hide()),uh(),Lt(),l.scrollLeft!=null&&l.scrollLeft>0?$("#luckysheet-scrollbar-x").scrollLeft(l.scrollLeft):$("#luckysheet-scrollbar-x").scrollLeft(0),l.scrollTop!=null&&l.scrollTop>0?$("#luckysheet-scrollbar-y").scrollTop(l.scrollTop):$("#luckysheet-scrollbar-y").scrollTop(0),ks(d.asyncLoad,"core"),Ae.pointEdit?setTimeout(function(){d.loadingObj.close()},0):setTimeout(function(){d.loadingObj.close()},500)},b=ae.loadSheetUrl;if(b=="")t.loadOtherFile(l),y();else{let k=t.checkLoadSheetIndex(l),w=[];for(let C=0;C$("#luckysheet-scrollbar-x")[0].offsetWidth&&(n.scrollLeft=$("#luckysheet-scrollbar-x").scrollLeft()),$("#luckysheet-scrollbar-y")[0].scrollHeight>$("#luckysheet-scrollbar-y")[0].offsetHeight&&(n.scrollTop=$("#luckysheet-scrollbar-y").scrollTop()),n.zoomRatio=d.zoomRatio},setSheetParam:function(e=!0){let n=this.getSheetIndex(d.currentSheetIndex),t=d.luckysheetfile[n];d.flowdata=t.data,Te.webWorkerFlowDataCache(d.flowdata),m.execFunctionGlobalData=null,window.luckysheet_getcelldata_cache=null,this.sheetParamRestore(t,d.flowdata),t.freezen==null?(Y.freezenhorizontaldata=null,Y.freezenverticaldata=null):(Y.freezenhorizontaldata=t.freezen.horizontal==null?null:t.freezen.horizontal.freezenhorizontaldata,Y.freezenverticaldata=t.freezen.vertical==null?null:t.freezen.vertical.freezenverticaldata),e&&Mn(d.flowdata.length,d.flowdata[0].length),He.buildAllPs(d.flowdata),Se.currentImgId=null,Se.images=t.images,Se.allImagesShow(),Se.init(),Qe.dataVerification=t.dataVerification,Qe.init(),Sl.hyperlink=t.hyperlink,Sl.init(),Ll(t.filter_select,t.filter)},restoreselect:function(){let e=this.getSheetIndex(d.currentSheetIndex),n=d.luckysheetfile[e];at(!0),Et(),n.scrollLeft!=null&&n.scrollLeft>0?$("#luckysheet-scrollbar-x").scrollLeft(n.scrollLeft):$("#luckysheet-scrollbar-x").scrollLeft(0),n.scrollTop!=null&&n.scrollTop>0?$("#luckysheet-scrollbar-y").scrollTop(n.scrollTop):$("#luckysheet-scrollbar-y").scrollTop(0)},storeSheetParamALL:function(){let e=this;e.storeSheetParam();let n=e.getSheetIndex(d.currentSheetIndex);d.luckysheetfile[n].data=d.flowdata,d.luckysheetfile[n].config=$.extend(!0,{},d.config)},mergeCalculationSheet:{},mergeCalculation:function(e){let n=d.luckysheetfile[this.getSheetIndex(e)],t=n.config,l=n.data;if(t==null)return;let a=t.merge;if(!(a==null||e in this.mergeCalculationSheet||n.autoCalculationMerge===!1)){this.mergeCalculationSheet[e]=1;for(let o in a){let s=parseInt(o.substr(0,o.indexOf("_"))),u=parseInt(o.substr(o.indexOf("_")+1)),f=a[o];l[s][u]==null&&(l[s][u]={}),l[s][u].mc={r:s,c:u,rs:f.rs,cs:f.cs};for(let A=s;A{if(m.addToCellList(h,g),g.indexOf("!")>-1){let p=g.substr(0,g.indexOf("!")),v=this.getSheetByName(p);if(v!=null){let y=v.index;s[y]=1,m.addToSheetIndexList(h,y)}}}),m.formulaContainSheetList[h]==null&&m.addToSheetIndexList(h,A);A!=null}for(let u in s){let f=u;o[f.toString()]==null&&(a.push(f),o[f.toString()]=1,this.checkLoadSheetIndexToDataIndex[f]=1)}}if(t!=null)for(let s=0;s0?$("#luckysheet-scrollbar-x").scrollLeft(n.scrollLeft*d.zoomRatio):$("#luckysheet-scrollbar-x").scrollLeft(0),n.scrollTop!=null&&n.scrollTop>0?$("#luckysheet-scrollbar-y").scrollTop(n.scrollTop*d.zoomRatio):$("#luckysheet-scrollbar-y").scrollTop(0),setTimeout(()=>{d.scrollRefreshSwitch=!0},0),pr(d.zoomRatio)},setCurSheet:function(e){for(let n=0;nt?Ae.showsheetbarConfig.sheet&&($("#luckysheet-sheet-area .luckysheet-sheets-scroll").css("display","inline-block"),$("#luckysheet-sheet-container .docs-sheet-fade-left").show()):($("#luckysheet-sheet-area .luckysheet-sheets-scroll").css("display","none"),$("#luckysheet-sheet-container .docs-sheet-fade-left").hide())}},sheetBarShowAndHide(e){let n=$("#luckysheet-sheet-container-c");if(e!=null){let o=$("#luckysheet-sheets-item"+e);n.scrollLeft(o.offset().left)}let t=n.width(),l=n[0].scrollWidth,a=n.scrollLeft();a<=0?$("#luckysheet-sheet-container .docs-sheet-fade-left").hide():$("#luckysheet-sheet-container .docs-sheet-fade-left").show(),t+a>=l?$("#luckysheet-sheet-container .docs-sheet-fade-right").hide():$("#luckysheet-sheet-container .docs-sheet-fade-right").show()},delChart:function(e,n){let t=this.getSheetIndex(n),l=d.luckysheetfile[t];if(l.chart==null)l.chart=[];else for(let a=0;a sheetmanage",a),m.insertUpdateFunctionGroup(f,A,l))}else if(t=="cg"){let s=a,u=e.k,f=n.getSheetConfig(l);u in f||(f[u]={});for(let A in s)f[u][A]=s[A];d.config=f}else if(t=="f"){let s=a,u=e.op,f=e.pos,A=o.filter;A==null&&(A={}),u=="upOrAdd"?A[f]=s:u=="del"&&delete A[f]}else if(t=="fsc")o.filter=null,o.filter_select=null;else if(t=="fsr"){let s=a;o.filter=s.filter,o.filter_select=s.filter_select}else if(t=="sh"){let s=e.op,u=e.cur,f=a;if(s=="hide")o.status=0,d.luckysheetfile[n.getSheetIndex(u)].status=1;else if(s=="show"){for(let A=0;A=u&&b.r=u+f&&(b.r-=f)}o.row-=f}else{for(let y=0;A.length==0;y++){let b=A[y];b.c>=u&&b.c=u+f&&(b.c-=f)}o.column-=f}let h=[];for(let y=0;yu&&(p.r+=f)}o.row+=f}else{for(let g=0;gu&&(p.c+=f)}o.column+=f}let h;s=="r"?h="row":h="column",Fr(h,u,f,!0)}else t=="na"?ae.saveParam("na",null,a):t=="thumb"&&setTimeout(function(){n.imageRequest()},2e3)}},ve=I0});function cm(e){return e==null||e==null||e==""}var um=De(()=>{});var S0,m,jt=De(()=>{dt();wt();Wr();Wt();cl();ll();$t();Kl();Bl();mr();el();Pt();Kr();wr();Qt();zr();Fl();tl();rl();El();Dn();_t();Qr();po();je();yt();ui();Xl();um();ca();hr();S0={error:{v:"#VALUE!",n:"#NAME?",na:"#N/A",r:"#REF!",d:"#DIV/0!",nm:"#NUM!",nl:"#NULL!",sp:"#SPILL!"},errorInfo:function(e){return e},errorParamCheck:function(e,n,t){let l,a,s=te().formulaMore;return t-1?[!0,s.tipSuccessText]:l.indexOf("range")>-1&&(N(n)=="object"||N(n)=="array")?[!0,s.tipSuccessText]:l.indexOf("number")>-1&&(V(n)||N(n)=="boolean")?[!0,s.tipSuccessText]:l.indexOf("string")>-1&&N(n)=="string"?[!0,s.tipSuccessText]:l.indexOf("date")>-1&&qt(n)?[!0,s.tipSuccessText]:[!1,s.tipParamErrorText]},getPureValueByData:function(e){if(e.length==0)return[];let n=[];if(N(e)=="array")if(N(e[0])=="array")for(let t=0;tn)for(let o=n;o-1){l||(l={});let h=A.split(":");h.length==2?l[h[0]]=h[1]:h.length>1&&(l[h[0]+":"+h[1]]=h[2])}else l||(l=[]),l.push(A)}a++}return l},colorList:["#2ec7c9","#fc5c5c","#5ab1ef","#ffb980","#d87a80","#8d98b3","#e5cf0d","#97b552","#95706d","#dc69aa","#07a2a4","#9a7fd1","#588dd5","#f5994e","#c05050","#59678c","#c9ab00","#7eb00a","#6f5553","#c14089"],classlist:{province:{11:"\u5317\u4EAC",12:"\u5929\u6D25",13:"\u6CB3\u5317",14:"\u5C71\u897F",15:"\u5185\u8499\u53E4",21:"\u8FBD\u5B81",22:"\u5409\u6797",23:"\u9ED1\u9F99\u6C5F",31:"\u4E0A\u6D77",32:"\u6C5F\u82CF",33:"\u6D59\u6C5F",34:"\u5B89\u5FBD",35:"\u798F\u5EFA",36:"\u6C5F\u897F",37:"\u5C71\u4E1C",41:"\u6CB3\u5357",42:"\u6E56\u5317",43:"\u6E56\u5357",44:"\u5E7F\u4E1C",45:"\u5E7F\u897F",46:"\u6D77\u5357",50:"\u91CD\u5E86",51:"\u56DB\u5DDD",52:"\u8D35\u5DDE",53:"\u4E91\u5357",54:"\u897F\u85CF",61:"\u9655\u897F",62:"\u7518\u8083",63:"\u9752\u6D77",64:"\u5B81\u590F",65:"\u65B0\u7586",71:"\u53F0\u6E7E",81:"\u9999\u6E2F",82:"\u6FB3\u95E8",91:"\u56FD\u5916"}},oldvalue:null,dontupdate:function(){let e=this;d.luckysheetCellUpdate.length=0,$("#luckysheet-functionbox-cell, #luckysheet-rich-text-editor").html(e.oldvalue),e.cancelNormalSelected(),e.rangetosheet!=d.currentSheetIndex&&ve.changeSheetExec(e.rangetosheet)},xssDeal:function(e){return typeof e!="string"?e:e.replace(/ - - - diff --git a/wolai-frontend/public/onlyoffice/plugins/agent-tools/plugin.js b/wolai-frontend/public/onlyoffice/plugins/agent-tools/plugin.js deleted file mode 100644 index b9816230..00000000 --- a/wolai-frontend/public/onlyoffice/plugins/agent-tools/plugin.js +++ /dev/null @@ -1,149 +0,0 @@ -// 说明:该插件用于把 ONLYOFFICE 编辑器“选区读/写”能力暴露给宿主页面(MNOTE)。 -// 宿主页面通过 postMessage 下发 oo_* 工具调用;插件执行后再 postMessage 回传结果。 -(function () { - const CHANNEL = "mnote_onlyoffice_agent_tools_v1"; - let readyPulseTimer = null; - let readyPulseDeadline = 0; - - const safePostToTop = (payload) => { - try { - if (typeof window === "undefined") return; - if (!window.top) return; - window.top.postMessage({ channel: CHANNEL, ...payload }, "*"); - } catch { - // ignore - } - }; - - const isPluginApiReady = () => - Boolean(window.Asc && window.Asc.plugin && typeof window.Asc.plugin.executeMethod === "function"); - - const stopReadyPulse = () => { - if (readyPulseTimer) { - window.clearInterval(readyPulseTimer); - readyPulseTimer = null; - } - readyPulseDeadline = 0; - }; - - const startReadyPulse = () => { - if (readyPulseTimer) return; - readyPulseDeadline = Date.now() + 20_000; - safePostToTop({ type: "ready" }); - readyPulseTimer = window.setInterval(() => { - if (Date.now() > readyPulseDeadline) { - stopReadyPulse(); - return; - } - safePostToTop({ type: "ready" }); - }, 1000); - }; - - const waitForPluginApiReady = () => { - if (isPluginApiReady()) { - startReadyPulse(); - return; - } - - const deadline = Date.now() + 60_000; - const timer = window.setInterval(() => { - if (isPluginApiReady()) { - window.clearInterval(timer); - startReadyPulse(); - return; - } - if (Date.now() > deadline) { - window.clearInterval(timer); - safePostToTop({ type: "result", callId: "bridge_bootstrap", ok: false, error: "ONLYOFFICE 插件 API 长时间未就绪" }); - } - }, 500); - }; - - const execMethod = (method, args) => - new Promise((resolve, reject) => { - try { - if (!window.Asc || !window.Asc.plugin) throw new Error("ONLYOFFICE 插件 API 未就绪"); - window.Asc.plugin.executeMethod(method, Array.isArray(args) ? args : [], (res) => resolve(res)); - } catch (e) { - reject(e instanceof Error ? e : new Error(String(e))); - } - }); - - const handleTool = async (tool, args) => { - const a = args && typeof args === "object" ? args : {}; - if (tool === "oo_get_selection") { - // 说明:先做最基础的“纯文本选区读取”,HTML 可在后续升级(需要更复杂的导出方式)。 - const text = await execMethod("GetSelectedText", []); - return { format: "text", text: String(text ?? "") }; - } - - if (tool === "oo_replace_selection") { - const format = String(a.format ?? "text"); - const text = String(a.text ?? ""); - if (format === "html") { - await execMethod("PasteHtml", [text]); - return { ok: true, format: "html", length: text.length }; - } - await execMethod("PasteText", [text]); - return { ok: true, format: "text", length: text.length }; - } - - if (tool === "oo_insert_text") { - const text = String(a.text ?? ""); - await execMethod("PasteText", [text]); - return { ok: true, length: text.length }; - } - - if (tool === "oo_insert_html") { - const html = String(a.html ?? ""); - await execMethod("PasteHtml", [html]); - return { ok: true, length: html.length }; - } - - if (tool === "oo_insert_image") { - // 说明:优先用 HTML 插入 ,避免依赖 PutImageDataToSelection 的参数差异。 - const src = String(a.src ?? a.imageData ?? ""); - if (!src) throw new Error("缺少图片数据(src/imageData)"); - const w = Number(a.width ?? 0); - const h = Number(a.height ?? 0); - const widthAttr = Number.isFinite(w) && w > 0 ? ` width=\"${Math.floor(w)}\"` : ""; - const heightAttr = Number.isFinite(h) && h > 0 ? ` height=\"${Math.floor(h)}\"` : ""; - const html = ``; - await execMethod("PasteHtml", [html]); - return { ok: true }; - } - - throw new Error(`未知工具:${tool}`); - }; - - window.Asc = window.Asc || {}; - window.Asc.plugin = window.Asc.plugin || {}; - - window.Asc.plugin.init = function () { - startReadyPulse(); - }; - - waitForPluginApiReady(); - - window.addEventListener("message", async (ev) => { - const msg = ev && ev.data ? ev.data : null; - if (!msg || typeof msg !== "object") return; - if (msg.channel !== CHANNEL) return; - if (msg.type !== "call") return; - - stopReadyPulse(); - - const callId = String(msg.callId ?? "").trim(); - const tool = String(msg.tool ?? "").trim(); - const args = msg.args && typeof msg.args === "object" ? msg.args : {}; - if (!callId || !tool) return; - - try { - const result = await handleTool(tool, args); - safePostToTop({ type: "result", callId, ok: true, result }); - } catch (e) { - const err = e instanceof Error ? e.message : String(e); - safePostToTop({ type: "result", callId, ok: false, error: err }); - } - }); -})(); diff --git a/wolai-frontend/public/vercel.svg b/wolai-frontend/public/vercel.svg deleted file mode 100644 index 77053960..00000000 --- a/wolai-frontend/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/wolai-frontend/public/window.svg b/wolai-frontend/public/window.svg deleted file mode 100644 index b2b2a44f..00000000 --- a/wolai-frontend/public/window.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/wolai-frontend/scripts/build-leptos-tiptap-island.js b/wolai-frontend/scripts/build-leptos-tiptap-island.js deleted file mode 100644 index 4c72a55e..00000000 --- a/wolai-frontend/scripts/build-leptos-tiptap-island.js +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env node -"use strict"; - -const fs = require("fs/promises"); -const path = require("path"); -const { spawnSync } = require("child_process"); - -const REPO_ROOT = path.resolve(__dirname, "..", ".."); -const SPIKE_ROOT = path.join(REPO_ROOT, "rust", "spikes", "leptos-tiptap-spike"); -const GENERATED_ROOT = path.join(SPIKE_ROOT, "generated", "island"); -const TOOLCHAIN = "1.89.0-x86_64-unknown-linux-gnu"; -const TARGET = "wasm32-unknown-unknown"; -const OUT_NAME = "mnote-leptos-tiptap-spike-island"; -const MANIFEST_PATH = path.join(SPIKE_ROOT, "Cargo.toml"); -const WASM_PATH = path.join( - SPIKE_ROOT, - "target", - TARGET, - "debug", - "mnote_leptos_tiptap_spike.wasm", -); -const ENTRY_PATH = path.join(GENERATED_ROOT, `${OUT_NAME}.js`); - -function run(command, args, options = {}) { - const result = spawnSync(command, args, { - cwd: options.cwd ?? REPO_ROOT, - env: process.env, - encoding: "utf8", - stdio: "pipe", - }); - - if (result.status === 0) { - return result; - } - - const stdout = (result.stdout || "").trim(); - const stderr = (result.stderr || "").trim(); - const details = [stdout, stderr].filter(Boolean).join("\n"); - throw new Error( - [`命令失败:${command} ${args.join(" ")}`, details].filter(Boolean).join("\n"), - ); -} - -async function ensureGeneratedDir() { - await fs.rm(GENERATED_ROOT, { recursive: true, force: true }); - await fs.mkdir(GENERATED_ROOT, { recursive: true }); -} - -async function verifyEntry() { - const source = await fs.readFile(ENTRY_PATH, "utf8"); - if (!source.includes("export function mount(container, options)")) { - throw new Error("island 入口缺少 mount 导出"); - } - if (!source.includes("export function unmount(mount_id)")) { - throw new Error("island 入口缺少 unmount 导出"); - } - if (!source.includes("export function mount_mindmap_shell(container, options)")) { - throw new Error("island 入口缺少 mount_mindmap_shell 导出"); - } - if (!source.includes("export function unmount_mindmap_shell(mount_id)")) { - throw new Error("island 入口缺少 unmount_mindmap_shell 导出"); - } -} - -async function buildLeptosTiptapIsland() { - // 说明:正式主链直接消费 lib.rs 的 wasm-bindgen 产物,不再把 trunk 演示页产物当作页面内 island 入口。 - run("rustup", ["target", "add", TARGET, "--toolchain", TOOLCHAIN]); - run( - "cargo", - [ - `+${TOOLCHAIN}`, - "build", - "--manifest-path", - MANIFEST_PATH, - "--lib", - "--target", - TARGET, - ], - { cwd: SPIKE_ROOT }, - ); - - await ensureGeneratedDir(); - - run( - "wasm-bindgen", - [ - WASM_PATH, - "--target", - "web", - "--out-dir", - GENERATED_ROOT, - "--out-name", - OUT_NAME, - ], - { cwd: SPIKE_ROOT }, - ); - - await verifyEntry(); - return { - generatedRoot: GENERATED_ROOT, - entryPath: ENTRY_PATH, - wasmPath: path.join(GENERATED_ROOT, `${OUT_NAME}_bg.wasm`), - }; -} - -module.exports = { - buildLeptosTiptapIsland, - GENERATED_ROOT, - OUT_NAME, -}; - -if (require.main === module) { - buildLeptosTiptapIsland() - .then(({ generatedRoot }) => { - process.stdout.write(`leptos-tiptap island 已生成:${generatedRoot}\n`); - }) - .catch((error) => { - process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`); - process.exit(1); - }); -} diff --git a/wolai-frontend/scripts/build-tree-shell-runtime.js b/wolai-frontend/scripts/build-tree-shell-runtime.js deleted file mode 100644 index 2097e62c..00000000 --- a/wolai-frontend/scripts/build-tree-shell-runtime.js +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env node -"use strict"; - -const fs = require("fs/promises"); -const path = require("path"); -const { spawnSync } = require("child_process"); - -const REPO_ROOT = path.resolve(__dirname, "..", ".."); -const RUST_ROOT = path.join(REPO_ROOT, "rust"); -const CRATE_ROOT = path.join(RUST_ROOT, "crates", "tree-shell-runtime-wasm"); -const GENERATED_ROOT = path.join(CRATE_ROOT, "generated"); -const TOOLCHAIN = "1.89.0-x86_64-unknown-linux-gnu"; -const TARGET = "wasm32-unknown-unknown"; -const OUT_NAME = "mnote-tree-shell-runtime"; -const MANIFEST_PATH = path.join(CRATE_ROOT, "Cargo.toml"); -const WASM_PATH = path.join( - RUST_ROOT, - "target", - TARGET, - "debug", - "tree_shell_runtime_wasm.wasm", -); -const ENTRY_PATH = path.join(GENERATED_ROOT, `${OUT_NAME}.js`); -const OUTPUT_WASM_PATH = path.join(GENERATED_ROOT, `${OUT_NAME}_bg.wasm`); -const GITIGNORE_PATH = path.join(GENERATED_ROOT, ".gitignore"); - -function run(command, args, options = {}) { - const result = spawnSync(command, args, { - cwd: options.cwd ?? REPO_ROOT, - env: process.env, - encoding: "utf8", - stdio: "pipe", - }); - - if (result.status === 0) { - return result; - } - - const stdout = (result.stdout || "").trim(); - const stderr = (result.stderr || "").trim(); - const details = [stdout, stderr].filter(Boolean).join("\n"); - throw new Error( - [`命令失败:${command} ${args.join(" ")}`, details].filter(Boolean).join("\n"), - ); -} - -async function ensureGeneratedDir() { - await fs.rm(GENERATED_ROOT, { recursive: true, force: true }); - await fs.mkdir(GENERATED_ROOT, { recursive: true }); - await fs.writeFile(GITIGNORE_PATH, "*\n!.gitignore\n", "utf8"); -} - -async function verifyOutput() { - const entrySource = await fs.readFile(ENTRY_PATH, "utf8"); - await fs.access(OUTPUT_WASM_PATH); - - if (!entrySource.includes("export function reduceTreeShellRuntime")) { - throw new Error("tree shell runtime js glue 缺少 reduceTreeShellRuntime 导出"); - } -} - -async function buildTreeShellRuntime() { - // 说明:tree shell iframe 主链直接加载固定 wasm/js 产物,因此在 Next 启动前先生成正式 artifact。 - run("rustup", ["target", "add", TARGET, "--toolchain", TOOLCHAIN], { cwd: RUST_ROOT }); - run( - "cargo", - [ - `+${TOOLCHAIN}`, - "build", - "--manifest-path", - MANIFEST_PATH, - "--package", - "tree-shell-runtime-wasm", - "--lib", - "--target", - TARGET, - "--locked", - ], - { cwd: RUST_ROOT }, - ); - - await ensureGeneratedDir(); - - run( - "wasm-bindgen", - [ - WASM_PATH, - "--target", - "web", - "--out-dir", - GENERATED_ROOT, - "--out-name", - OUT_NAME, - ], - { cwd: RUST_ROOT }, - ); - - await verifyOutput(); - - return { - generatedRoot: GENERATED_ROOT, - entryPath: ENTRY_PATH, - wasmPath: OUTPUT_WASM_PATH, - }; -} - -module.exports = { - buildTreeShellRuntime, - GENERATED_ROOT, - OUT_NAME, -}; - -if (require.main === module) { - buildTreeShellRuntime() - .then(({ generatedRoot }) => { - process.stdout.write(`tree shell runtime 已生成:${generatedRoot}\n`); - }) - .catch((error) => { - process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`); - process.exit(1); - }); -} diff --git a/wolai-frontend/scripts/dev-server.js b/wolai-frontend/scripts/dev-server.js deleted file mode 100644 index 898e5745..00000000 --- a/wolai-frontend/scripts/dev-server.js +++ /dev/null @@ -1,496 +0,0 @@ -#!/usr/bin/env node -/** - * 自定义 Next dev server: - * - 解决 ONLYOFFICE 在 `/onlyoffice-server/*` 下的 WebSocket Upgrade 需求(socket.io / coauthoring)。 - * - Next App Router 的 Route Handler 无法处理 Upgrade,因此必须在 Node http server 层做透传。 - * - * 用法(保持与 next dev 类似): - * - pnpm dev -p 3000 - * - node scripts/dev-server.js -p 3000 - * - * 依赖环境变量: - * - ONLYOFFICE_INTERNAL_URL:可显式指定;未指定或失效时优先探测 8082,再回退 8081 - */ - -const http = require("http"); -const net = require("net"); -const path = require("path"); - -// 说明:Next 当前 vendored 的 browserslist/baseline 提示会在进程启动早期输出, -// 即使项目侧依赖已经升级也未必消失。这里默认静音这类“当前不可操作”的噪音; -// 如需排查真实数据新鲜度,可在启动前显式传入 `false` 恢复原始警告。 -if (process.env.BROWSERSLIST_IGNORE_OLD_DATA === "false") { - delete process.env.BROWSERSLIST_IGNORE_OLD_DATA; -} else if (process.env.BROWSERSLIST_IGNORE_OLD_DATA === undefined) { - process.env.BROWSERSLIST_IGNORE_OLD_DATA = "true"; -} -if (process.env.BASELINE_BROWSER_MAPPING_IGNORE_OLD_DATA === "false") { - delete process.env.BASELINE_BROWSER_MAPPING_IGNORE_OLD_DATA; -} else if (process.env.BASELINE_BROWSER_MAPPING_IGNORE_OLD_DATA === undefined) { - process.env.BASELINE_BROWSER_MAPPING_IGNORE_OLD_DATA = "true"; -} - -const next = require("next"); -const { parse: parseUrl } = require("url"); -const { buildTreeShellRuntime } = require("./build-tree-shell-runtime"); -const { buildLeptosTiptapIsland } = require("./build-leptos-tiptap-island"); - -const ONLYOFFICE_PREFIX = "/onlyoffice-server"; -const CONVEX_PREFIX = "/convex"; -const DEFAULT_ONLYOFFICE_INTERNAL_URL = "http://127.0.0.1:8082"; -const ONLYOFFICE_PROBE_PATH = "/web-apps/apps/api/documents/api.js"; -const ONLYOFFICE_RESOLVE_CACHE_TTL_MS = 30_000; - -let cachedOnlyOfficeInternalUrl = ""; -let cachedOnlyOfficeInternalUrlAt = 0; -let pendingOnlyOfficeInternalUrl = null; - -function readArgValue(flag) { - const idx = process.argv.findIndex((x) => x === flag); - if (idx === -1) return null; - const v = process.argv[idx + 1]; - if (!v || v.startsWith("-")) return null; - return v; -} - -function resolvePort() { - const fromArg = readArgValue("-p") || readArgValue("--port"); - const raw = fromArg || process.env.PORT || "3000"; - const n = Number(raw); - return Number.isFinite(n) ? Math.max(1, Math.min(65535, Math.floor(n))) : 3000; -} - -function resolveHostname() { - return readArgValue("-H") || readArgValue("--hostname") || process.env.HOSTNAME || "0.0.0.0"; -} - -function isOnlyOfficePath(urlString) { - try { - const u = new URL(urlString, "http://localhost"); - return u.pathname === ONLYOFFICE_PREFIX || u.pathname.startsWith(`${ONLYOFFICE_PREFIX}/`); - } catch { - return false; - } -} - -function isConvexPath(urlString) { - try { - const u = new URL(urlString, "http://localhost"); - return u.pathname === CONVEX_PREFIX || u.pathname.startsWith(`${CONVEX_PREFIX}/`); - } catch { - return false; - } -} - -function normalizeOnlyOfficeInternalUrl(raw) { - const value = String(raw || "").trim().replace(/\/+$/, ""); - if (!value) return ""; - try { - const url = new URL(value); - if (url.protocol !== "http:" && url.protocol !== "https:") return ""; - return url.toString().replace(/\/+$/, ""); - } catch { - return ""; - } -} - -function listOnlyOfficeInternalUrlCandidates() { - const candidates = []; - const push = (value) => { - const normalized = normalizeOnlyOfficeInternalUrl(value); - if (!normalized) return; - if (!candidates.includes(normalized)) candidates.push(normalized); - }; - - push(process.env.ONLYOFFICE_INTERNAL_URL); - - for (const raw of String(process.env.ONLYOFFICE_INTERNAL_URL_CANDIDATES || "").split(",")) { - push(raw); - } - - push(DEFAULT_ONLYOFFICE_INTERNAL_URL); - push("http://127.0.0.1:8081"); - push("http://localhost:8082"); - push("http://localhost:8081"); - - return candidates.length > 0 ? candidates : [DEFAULT_ONLYOFFICE_INTERNAL_URL]; -} - -async function probeOnlyOfficeInternalUrl(candidate) { - if (typeof fetch !== "function") return false; - try { - const probeUrl = new URL(ONLYOFFICE_PROBE_PATH, `${candidate}/`); - const response = await fetch(probeUrl, { - method: "HEAD", - redirect: "follow", - cache: "no-store", - signal: AbortSignal.timeout(2500), - }); - return response.ok; - } catch { - return false; - } -} - -async function resolveOnlyOfficeInternalUrl() { - const now = Date.now(); - if (cachedOnlyOfficeInternalUrl && now - cachedOnlyOfficeInternalUrlAt < ONLYOFFICE_RESOLVE_CACHE_TTL_MS) { - return new URL(`${cachedOnlyOfficeInternalUrl}/`); - } - - if (!pendingOnlyOfficeInternalUrl) { - pendingOnlyOfficeInternalUrl = (async () => { - const candidates = listOnlyOfficeInternalUrlCandidates(); - for (const candidate of candidates) { - if (await probeOnlyOfficeInternalUrl(candidate)) { - return candidate; - } - } - return candidates[0] || DEFAULT_ONLYOFFICE_INTERNAL_URL; - })(); - } - - try { - const resolved = await pendingOnlyOfficeInternalUrl; - cachedOnlyOfficeInternalUrl = resolved; - cachedOnlyOfficeInternalUrlAt = Date.now(); - return new URL(`${resolved}/`); - } finally { - pendingOnlyOfficeInternalUrl = null; - } -} - -function buildUpstreamRequestHead(req, targetUrl, prefix) { - const incoming = new URL(req.url || "/", "http://localhost"); - const rawPath = incoming.pathname || "/"; - const stripped = rawPath === prefix ? "/" : rawPath.slice(prefix.length) || "/"; - - const basePath = String(targetUrl.pathname || "/").replace(/\/+$/, "") || ""; - const upstreamPath = `${basePath}${stripped}`.replace(/\/{2,}/g, "/") + (incoming.search || ""); - - const lines = []; - lines.push(`${req.method || "GET"} ${upstreamPath} HTTP/1.1`); - - const headers = req.headers || {}; - const headerValue = (name) => { - const v = headers[name]; - if (!v) return ""; - return Array.isArray(v) ? v[0] : String(v); - }; - - // 说明:ONLYOFFICE 在反向代理下会依赖 X-Forwarded-* 推导“对外地址”,用于拼出 ws/wss 与缓存资源 URL。 - // 这里尽量沿用上游(frp/nginx)注入的 x-forwarded-proto/host;若缺失,则回退到本机 http。 - const originLike = headerValue("origin") || headerValue("referer") || ""; - const originUrl = (() => { - try { - if (!originLike) return null; - return new URL(originLike); - } catch { - return null; - } - })(); - - const forwardedHostRaw = - headerValue("x-forwarded-host") || (originUrl ? originUrl.host : "") || headerValue("host") || ""; - const forwardedProto = - (headerValue("x-forwarded-proto") || "").split(",")[0].trim() || - (originUrl ? originUrl.protocol.replace(":", "") : "") || - "http"; - const forwardedPort = (() => { - const fromHeader = (headerValue("x-forwarded-port") || "").split(",")[0].trim(); - if (fromHeader) return fromHeader; - const hostHasPort = forwardedHostRaw.includes(":") ? forwardedHostRaw.split(":").pop() : ""; - if (hostHasPort && /^\d+$/.test(hostHasPort)) return hostHasPort; - return forwardedProto === "https" ? "443" : "80"; - })(); - const forwardedHost = forwardedHostRaw.split(",")[0].trim() || "localhost"; - - for (const [k, v] of Object.entries(headers)) { - if (!v) continue; - const key = String(k); - if (key.toLowerCase() === "host") continue; - if (Array.isArray(v)) { - lines.push(`${key}: ${v.join(", ")}`); - } else { - lines.push(`${key}: ${String(v)}`); - } - } - - // 说明:补齐/覆盖 forward 信息,避免 ONLYOFFICE 返回指向内部端口的绝对 URL。 - lines.push(`x-forwarded-host: ${forwardedHost}`); - lines.push(`x-forwarded-proto: ${forwardedProto}`); - lines.push(`x-forwarded-port: ${forwardedPort}`); - lines.push(`x-forwarded-prefix: ${prefix}`); - - // 说明:Host 必须指向 ONLYOFFICE_INTERNAL_URL,否则上游可能拒绝 Upgrade。 - lines.push(`Host: ${targetUrl.host}`); - lines.push(""); - lines.push(""); - return lines.join("\r\n"); -} - -async function proxyOnlyOfficeUpgrade(req, socket, head) { - let target; - try { - target = await resolveOnlyOfficeInternalUrl(); - } catch (err) { - try { - const msg = err && err.message ? String(err.message) : String(err || ""); - console.log("[dev-server][onlyoffice-ws] resolve error", msg); - } catch {} - try { - socket.destroy(); - } catch {} - return; - } - const port = Number(target.port) || (target.protocol === "https:" ? 443 : 80); - - const upstream = net.connect({ host: target.hostname, port }, () => { - try { - const reqHead = buildUpstreamRequestHead(req, target, ONLYOFFICE_PREFIX); - upstream.write(reqHead); - if (head && head.length > 0) upstream.write(head); - socket.pipe(upstream); - upstream.pipe(socket); - } catch (e) { - try { - socket.destroy(); - } catch {} - try { - upstream.destroy(); - } catch {} - } - }); - - const onError = (err) => { - try { - const msg = err && err.message ? String(err.message) : String(err || ""); - - console.log("[dev-server][onlyoffice-ws] proxy error", msg); - } catch {} - try { - socket.destroy(); - } catch {} - try { - upstream.destroy(); - } catch {} - }; - - upstream.on("error", onError); - socket.on("error", onError); -} - -function resolveConvexInternalUrl() { - // 说明:Convex 本地 dev server 默认 3210;对外访问(如 frp https)时,浏览器需要 wss, - // 因此这里通过同源反代把 Upgrade 转发到本机 3210。 - const raw = (process.env.CONVEX_INTERNAL_URL || "http://127.0.0.1:3210").trim(); - try { - return new URL(raw.replace(/\/+$/, "") + "/"); - } catch { - return new URL("http://127.0.0.1:3210/"); - } -} - -function proxyConvexUpgrade(req, socket, head) { - const target = resolveConvexInternalUrl(); - const port = Number(target.port) || (target.protocol === "https:" ? 443 : 80); - - const upstream = net.connect({ host: target.hostname, port }, () => { - try { - const reqHead = buildUpstreamRequestHead(req, target, CONVEX_PREFIX); - upstream.write(reqHead); - if (head && head.length > 0) upstream.write(head); - socket.pipe(upstream); - upstream.pipe(socket); - } catch { - try { - socket.destroy(); - } catch {} - try { - upstream.destroy(); - } catch {} - } - }); - - const onError = (err) => { - try { - const msg = err && err.message ? String(err.message) : String(err || ""); - - console.log("[dev-server][convex-ws] proxy error", msg); - } catch {} - try { - socket.destroy(); - } catch {} - try { - upstream.destroy(); - } catch {} - }; - - upstream.on("error", onError); - socket.on("error", onError); -} - -function proxyConvexHttp(req, res) { - const target = resolveConvexInternalUrl(); - const incoming = new URL(req.url || "/", "http://localhost"); - const rawPath = incoming.pathname || "/"; - const stripped = rawPath === CONVEX_PREFIX ? "/" : rawPath.slice(CONVEX_PREFIX.length) || "/"; - const upstreamPath = stripped + (incoming.search || ""); - - const isHttps = target.protocol === "https:"; - const mod = isHttps ? require("https") : require("http"); - - const headers = { ...(req.headers || {}) }; - headers.host = target.host; - // 说明:让上游能感知对外协议/域名(主要用于调试;Convex 本身通常不依赖这些头)。 - const forwardedHostRaw = String(headers["x-forwarded-host"] || req.headers.host || ""); - const forwardedHost = forwardedHostRaw.split(",")[0].trim() || "localhost"; - const forwardedProto = - String(headers["x-forwarded-proto"] || "").split(",")[0].trim() || - (String(req.headers.origin || "").startsWith("https") ? "https" : "http"); - const forwardedPortRaw = String(headers["x-forwarded-port"] || "").split(",")[0].trim(); - const forwardedPort = (() => { - if (forwardedPortRaw) return forwardedPortRaw; - const hostHasPort = forwardedHost.includes(":") ? forwardedHost.split(":").pop() : ""; - if (hostHasPort && /^\d+$/.test(hostHasPort)) return hostHasPort; - return forwardedProto === "https" ? "443" : "80"; - })(); - - headers["x-forwarded-host"] = forwardedHost; - headers["x-forwarded-proto"] = forwardedProto; - headers["x-forwarded-port"] = forwardedPort; - headers["x-forwarded-prefix"] = CONVEX_PREFIX; - - const upstreamReq = mod.request( - { - protocol: target.protocol, - hostname: target.hostname, - port: target.port || (isHttps ? 443 : 80), - method: req.method, - path: upstreamPath, - headers, - }, - (upstreamRes) => { - res.writeHead(upstreamRes.statusCode || 502, upstreamRes.headers || {}); - upstreamRes.pipe(res); - }, - ); - - upstreamReq.on("error", (err) => { - try { - - console.log("[dev-server][convex-http] proxy error", err && err.message ? err.message : String(err || "")); - } catch {} - try { - res.statusCode = 502; - res.end("Bad Gateway"); - } catch {} - }); - - const method = String(req.method || "GET").toUpperCase(); - const hasBody = - !["GET", "HEAD"].includes(method) && - ((typeof req.headers["content-length"] === "string" && req.headers["content-length"] !== "0") || - String(req.headers["transfer-encoding"] || "").trim().length > 0); - - if (!hasBody) { - upstreamReq.end(); - return; - } - - req.pipe(upstreamReq); -} - -async function main() { - const port = resolvePort(); - const hostname = resolveHostname(); - const dev = true; - - // 说明:tree shell iframe 通过 3000 同源 js glue + wasm 直接调用 reducer,因此 dev 启动前先生成正式 artifact。 - await buildTreeShellRuntime(); - - // 说明:3000 主链需要直接挂载正式 Leptos island,因此在 Next dev 启动前先生成 lib.rs 的 wasm-bindgen 产物。 - await buildLeptosTiptapIsland(); - - const app = next({ dev, dir: path.join(__dirname, ".."), hostname, port }); - const handle = app.getRequestHandler(); - - await app.prepare(); - - const server = http.createServer((req, res) => { - try { - res.setHeader("x-mnote-dev-server", "1"); - res.setHeader("x-mnote-onlyoffice-ws-proxy", "1"); - res.setHeader("x-mnote-convex-ws-proxy", "1"); - } catch { - // ignore - } - - if (isConvexPath(req.url || "/")) { - proxyConvexHttp(req, res); - return; - } - - const parsed = parseUrl(req.url || "/", true); - handle(req, res, parsed); - }); - - server.on("upgrade", (req, socket, head) => { - if (isConvexPath(req.url || "/")) { - try { - - console.log( - "[dev-server][convex-ws] upgrade", - req.url, - "host=", - req.headers.host, - "xfp=", - req.headers["x-forwarded-proto"], - "xfh=", - req.headers["x-forwarded-host"], - ); - } catch {} - proxyConvexUpgrade(req, socket, head); - return; - } - if (isOnlyOfficePath(req.url || "/")) { - try { - - console.log( - "[dev-server][onlyoffice-ws] upgrade", - req.url, - "host=", - req.headers.host, - "xfp=", - req.headers["x-forwarded-proto"], - "xfh=", - req.headers["x-forwarded-host"], - ); - } catch {} - proxyOnlyOfficeUpgrade(req, socket, head); - return; - } - // 说明:Next custom server 会在首个 HTTP 请求时自动给同一个 http.Server 绑定 HMR upgrade listener。 - // 非 Convex / ONLYOFFICE 的 Upgrade 交给 Next 自己的 listener,避免同一 socket 被处理两次。 - }); - - server.listen(port, hostname, () => { - resolveOnlyOfficeInternalUrl() - .then((onlyofficeTarget) => { - console.log( - `[dev-server] ready http://${hostname}:${port} (ONLYOFFICE ws via ${ONLYOFFICE_PREFIX} -> ${onlyofficeTarget.toString().replace(/\/$/, "")}; Convex ws via ${CONVEX_PREFIX} -> ${process.env.CONVEX_INTERNAL_URL || "http://127.0.0.1:3210"})`, - ); - }) - .catch(() => { - console.log( - `[dev-server] ready http://${hostname}:${port} (ONLYOFFICE ws via ${ONLYOFFICE_PREFIX} -> ${process.env.ONLYOFFICE_INTERNAL_URL || DEFAULT_ONLYOFFICE_INTERNAL_URL}; Convex ws via ${CONVEX_PREFIX} -> ${process.env.CONVEX_INTERNAL_URL || "http://127.0.0.1:3210"})`, - ); - }); - }); -} - -main().catch((err) => { - - console.error(err instanceof Error ? err.stack : String(err)); - process.exit(1); -}); diff --git a/wolai-frontend/scripts/fix-auth-env.js b/wolai-frontend/scripts/fix-auth-env.js deleted file mode 100644 index b12e66f1..00000000 --- a/wolai-frontend/scripts/fix-auth-env.js +++ /dev/null @@ -1,79 +0,0 @@ -const { execSync } = require('child_process'); -const fs = require('fs'); -const path = require('path'); - -function setConvexEnv(name, value) { - // 说明:使用 stdin 传值,避免以 `-----BEGIN ...` 开头的内容被 CLI 误当作参数/选项。 - execSync(`npx convex env set ${name}`, { - input: String(value ?? ""), - stdio: ['pipe', 'inherit', 'inherit'], - cwd: process.cwd(), - }); -} - -const privateKey = `-----BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCPcrWDwjh74nu1 -gfYo9ywVIRMedfPwbNiDjHnovzHAIeToc1+Lw3ju/io9OME3KLWzIibUHX39LilK -sSQKigJ3+i7J13IquJD++ozY9C8Dp/P34txVY+ECSQRdIBbETLJxYBWXOx6Ysw/d -/CUuDOdEw2IhNvN97sTh5jUZe6f/H6mfCpJ3X5SZrqrIUTTL6r4Lj9ZS3PUX5ivU -6sd2rXzpDsUQgrNgmvGN3w9nAHudxAUv/zOGa4802Z+z14LCBIX6v6PIK79YQ4s9 -/5hH73MesA6vgzWShJcyVy3guyde+duhpECo1q3d6dIZeqrazQwqAW3vcpmEFJdX -jkpociDfAgMBAAECggEAAYtRE+mH1SGThlkvTrKWeWXBQG8xoJFzZTsiZtSEExbq -UWxIh4cjqqL2znDpd5ALILIJ6/ejTxHrpN+yTSC+NQ9u6IJWusoA2ZXV5VH/nZD1 -yeHZ0FuCZRVnJB9/zz4qH5lSsi2TPz6SOagIuG2wIafeyw+94EmtOedSBAO2Q8NN -3jHoINBCyRu6hU3ml0h7daoIhUw9ONI7MZUlYvuV7Ti3yf+czzpqwYx3gZA39sa8 -qUbJPFk6ts+CVjqSAdYVSfU3TC1Us1usOM3+04mACp3vc6ZUKxnB4Dsk/sSHj71n -EcZ3fOR7EgjmXf6wBiq24T2+0UzoHW2yDaiAowr6iQKBgQDDs2eJTopFejt47EMm -zY//e8fyUDKx07CWwUZKP78IMTZcp8BwHmKTSY+VbQLvVYnNxADkckYapokjU3a7 -GMOXxvKVLqfgU/oUBIkRFDRo+nasFCd5cPpnYjQ6lGalBkDghVlyvq7kKF/MPtIK -dLqsV3tZDXoHX2mjTga94ygTZwKBgQC7pa+eZdWiz275mpYql9Gs6N3G3HhcpRxb -oWYWekCtZQ9gtecpvA9e0tW3ShoF19ksWYGpM4vxAOXul5Ei9IPVVNHc7RvGF5CO -0Zryx3qaVk7E6XQc3BQVVAenT2fNiv3+fkCMeAvcZBZOGiPPziCCT3wU3bd3tsn8 -EsMiAPvTyQKBgEs0iWhBr29NrsckfBXQTzMN/WOIIEMoJ6d3dKyZ3K6oQszOhmxP -sPALB8uTjdotk/xoAzPHGlupfe/+ZhU2Sgvsn1JnEIprmyHQMGBI1G83OR2dzSGl -IgVSvuF4IA3w3kOp2xr2Xj09qrrRtWPhQc9y+urY+/kTWIQyOvMD9WWnAoGADdUN -2BBLqj++P3oMvcEJPMTBrGoOGU42g+6m1ttWLzH26zsdei8ZtvS1ulglCO87XBCR -BUb+dtqJGIhls3zwxuYEvlNgK78K8ewzjtfzirL4BX3sCECU3mmeUtAAp98qD/uA -iJpEzY83MbStlSDtto1jaSpa3uFDjGhZqAUIizkCgYABf/HSMwWsnvRgo0JMmDru -3L/xScJiFVdFV9vNiSckjUYhx7lgFqEjsDEN1mZiRIa2UhLOqpo5S/cIXfMkeEPh -9Rusf9STwNwOOyhFRrHCxqhTH6Ivnj6oZSM/UrS8GvCVMPWs1z8k45FWKI/IRXQq -AT5PRKihrWbg63/rXaSCxw== ------END PRIVATE KEY-----`; - -const jwks = '{"keys":[{"use":"sig","kty":"RSA","n":"j3K1g8I4e-J7tYH2KPcsFSETHnXz8GzYg4x56L8xwCHk6HNfi8N47v4qPTjBNyi1syIm1B19_S4pSrEkCooCd_ouyddyKriQ_vqM2PQvA6fz9-LcVWPhAkkEXSAWxEyycWAVlzsemLMP3fwlLgznRMNiITbzfe7E4eY1GXun_x-pnwqSd1-Uma6qyFE0y-q-C4_WUtz1F-Yr1OrHdq186Q7FEIKzYJrxjd8PZwB7ncQFL_8zhmuPNNmfs9eCwgSF-r-jyCu_WEOLPf-YR-9zHrAOr4M1koSXMlct4LsnXvnboaRAqNat3enSGXqq2s0MKgFt73KZhBSXV45KaHIg3w","e":"AQAB"}]}'; - -// 使用临时目录(跨平台兼容) -const tmpDir = process.env.TMPDIR || process.env.TEMP || '/tmp'; -const keyFile = path.join(tmpDir, 'convex_jwt_key.txt'); -const jwksFile = path.join(tmpDir, 'convex_jwks.txt'); - -// 写入临时文件 -fs.writeFileSync(keyFile, privateKey); -fs.writeFileSync(jwksFile, jwks); - -console.log('Setting JWT_PRIVATE_KEY from file...'); -const keyContent = fs.readFileSync(keyFile, 'utf8'); -try { - setConvexEnv("JWT_PRIVATE_KEY", keyContent); - console.log('✓ JWT_PRIVATE_KEY set'); -} catch (e) { - console.error('✗ Failed to set JWT_PRIVATE_KEY:', e.message); -} - -console.log('Setting JWKS...'); -try { - setConvexEnv("JWKS", jwks); - console.log('✓ JWKS set'); -} catch (e) { - console.error('✗ Failed to set JWKS:', e.message); -} - -// 清理临时文件 -try { - fs.unlinkSync(keyFile); - fs.unlinkSync(jwksFile); -} catch (e) { - // 忽略清理错误 -} - -console.log('Done! You can now test registration.'); diff --git a/wolai-frontend/scripts/generateKeys.mjs b/wolai-frontend/scripts/generateKeys.mjs deleted file mode 100644 index 2be16e13..00000000 --- a/wolai-frontend/scripts/generateKeys.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import { exportJWK, exportPKCS8, generateKeyPair } from "jose"; - -const keys = await generateKeyPair("RS256", { extractable: true }); -const privateKey = await exportPKCS8(keys.privateKey); -const publicKey = await exportJWK(keys.publicKey); -const jwks = JSON.stringify({ keys: [{ use: "sig", ...publicKey }] }); - -process.stdout.write( - `JWT_PRIVATE_KEY="${privateKey.trimEnd().replace(/\n/g, " ")}"`, -); -process.stdout.write("\n"); -process.stdout.write(`JWKS=${jwks}`); -process.stdout.write("\n"); diff --git a/wolai-frontend/scripts/mindmap_ai_pdf_extract.py b/wolai-frontend/scripts/mindmap_ai_pdf_extract.py deleted file mode 100644 index ae2afab5..00000000 --- a/wolai-frontend/scripts/mindmap_ai_pdf_extract.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -用于 mindmap-ai v2:从 PDF 中提取分页文本。 - -注意: -- 本脚本仅做“文本抽取”,不做大纲推断(大纲交给 TS 侧 + LLM/规则)。 -- 输出为 JSON,便于 Next.js API 通过 child_process 调用并解析。 -""" - -from __future__ import annotations - -import argparse -import json -import sys -from typing import Any, Dict, List, Optional - - -def _load_reader(path: str): - try: - # pypdf (推荐) - from pypdf import PdfReader # type: ignore - return PdfReader(path) - except Exception: - # PyPDF2 兜底 - from PyPDF2 import PdfReader # type: ignore - return PdfReader(path) - - -def _extract_pages(reader, max_pages: Optional[int] = None) -> List[Dict[str, Any]]: - pages: List[Dict[str, Any]] = [] - total = len(getattr(reader, "pages", []) or []) - limit = total if max_pages is None else min(total, max_pages) - - for i in range(limit): - try: - page = reader.pages[i] - text = page.extract_text() or "" - except Exception: - text = "" - # 清理一些不可见字符,避免 JSON/解析异常 - text = text.replace("\x00", "").strip() - pages.append({"page": i + 1, "text": text}) - return pages - - -def main(argv: List[str]) -> int: - parser = argparse.ArgumentParser() - parser.add_argument("--input", required=True, help="PDF 文件路径") - parser.add_argument("--max-pages", type=int, default=50, help="最多提取页数") - args = parser.parse_args(argv) - - reader = _load_reader(args.input) - meta_title = None - try: - info = getattr(reader, "metadata", None) - meta_title = getattr(info, "title", None) if info else None - except Exception: - meta_title = None - - pages = _extract_pages(reader, max_pages=args.max_pages) - payload: Dict[str, Any] = { - "meta": {"title": meta_title}, - "pages": pages, - "totalPages": len(getattr(reader, "pages", []) or []), - } - # Windows 控制台默认编码可能是 gbk,直接写入会导致 UnicodeEncodeError - try: - sys.stdout.reconfigure(encoding="utf-8") # py3.7+ - except Exception: - pass - sys.stdout.write(json.dumps(payload, ensure_ascii=False)) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) diff --git a/wolai-frontend/scripts/prod-server.js b/wolai-frontend/scripts/prod-server.js deleted file mode 100644 index 88454d04..00000000 --- a/wolai-frontend/scripts/prod-server.js +++ /dev/null @@ -1,450 +0,0 @@ -#!/usr/bin/env node -/** - * 自定义 Next 生产 server: - * - 解决 ONLYOFFICE 在 `/onlyoffice-server/*` 下的 WebSocket Upgrade 需求(socket.io / coauthoring)。 - * - 解决 Convex 在 HTTPS(frp/nginx)场景下浏览器不能连接 ws:// 的问题:通过同源 `/convex/*` 反代到本机 Convex。 - * - * 用法: - * - pnpm build - * - pnpm start (默认会执行本脚本) - * - * 依赖环境变量: - * - ONLYOFFICE_INTERNAL_URL:可显式指定;未指定或失效时优先探测 8082,再回退 8081 - * - CONVEX_INTERNAL_URL:默认 http://127.0.0.1:3210 - */ - -const http = require("http"); -const net = require("net"); -const path = require("path"); - -// 说明:Next 当前 vendored 的 browserslist/baseline 提示会在进程启动早期输出, -// 即使项目侧依赖已经升级也未必消失。这里默认静音这类“当前不可操作”的噪音; -// 如需排查真实数据新鲜度,可在启动前显式传入 `false` 恢复原始警告。 -if (process.env.BROWSERSLIST_IGNORE_OLD_DATA === "false") { - delete process.env.BROWSERSLIST_IGNORE_OLD_DATA; -} else if (process.env.BROWSERSLIST_IGNORE_OLD_DATA === undefined) { - process.env.BROWSERSLIST_IGNORE_OLD_DATA = "true"; -} -if (process.env.BASELINE_BROWSER_MAPPING_IGNORE_OLD_DATA === "false") { - delete process.env.BASELINE_BROWSER_MAPPING_IGNORE_OLD_DATA; -} else if (process.env.BASELINE_BROWSER_MAPPING_IGNORE_OLD_DATA === undefined) { - process.env.BASELINE_BROWSER_MAPPING_IGNORE_OLD_DATA = "true"; -} - -const next = require("next"); -const { parse: parseUrl } = require("url"); - -const ONLYOFFICE_PREFIX = "/onlyoffice-server"; -const CONVEX_PREFIX = "/convex"; -const DEFAULT_ONLYOFFICE_INTERNAL_URL = "http://127.0.0.1:8082"; -const ONLYOFFICE_PROBE_PATH = "/web-apps/apps/api/documents/api.js"; -const ONLYOFFICE_RESOLVE_CACHE_TTL_MS = 30_000; - -let cachedOnlyOfficeInternalUrl = ""; -let cachedOnlyOfficeInternalUrlAt = 0; -let pendingOnlyOfficeInternalUrl = null; - -function readArgValue(flag) { - const idx = process.argv.findIndex((x) => x === flag); - if (idx === -1) return null; - const v = process.argv[idx + 1]; - if (!v || v.startsWith("-")) return null; - return v; -} - -function resolvePort() { - const fromArg = readArgValue("-p") || readArgValue("--port"); - const raw = fromArg || process.env.PORT || "3000"; - const n = Number(raw); - return Number.isFinite(n) ? Math.max(1, Math.min(65535, Math.floor(n))) : 3000; -} - -function resolveHostname() { - return readArgValue("-H") || readArgValue("--hostname") || process.env.HOSTNAME || "0.0.0.0"; -} - -function isOnlyOfficePath(urlString) { - try { - const u = new URL(urlString, "http://localhost"); - return u.pathname === ONLYOFFICE_PREFIX || u.pathname.startsWith(`${ONLYOFFICE_PREFIX}/`); - } catch { - return false; - } -} - -function isConvexPath(urlString) { - try { - const u = new URL(urlString, "http://localhost"); - return u.pathname === CONVEX_PREFIX || u.pathname.startsWith(`${CONVEX_PREFIX}/`); - } catch { - return false; - } -} - -function normalizeOnlyOfficeInternalUrl(raw) { - const value = String(raw || "").trim().replace(/\/+$/, ""); - if (!value) return ""; - try { - const url = new URL(value); - if (url.protocol !== "http:" && url.protocol !== "https:") return ""; - return url.toString().replace(/\/+$/, ""); - } catch { - return ""; - } -} - -function listOnlyOfficeInternalUrlCandidates() { - const candidates = []; - const push = (value) => { - const normalized = normalizeOnlyOfficeInternalUrl(value); - if (!normalized) return; - if (!candidates.includes(normalized)) candidates.push(normalized); - }; - - push(process.env.ONLYOFFICE_INTERNAL_URL); - - for (const raw of String(process.env.ONLYOFFICE_INTERNAL_URL_CANDIDATES || "").split(",")) { - push(raw); - } - - push(DEFAULT_ONLYOFFICE_INTERNAL_URL); - push("http://127.0.0.1:8081"); - push("http://localhost:8082"); - push("http://localhost:8081"); - - return candidates.length > 0 ? candidates : [DEFAULT_ONLYOFFICE_INTERNAL_URL]; -} - -async function probeOnlyOfficeInternalUrl(candidate) { - if (typeof fetch !== "function") return false; - try { - const probeUrl = new URL(ONLYOFFICE_PROBE_PATH, `${candidate}/`); - const response = await fetch(probeUrl, { - method: "HEAD", - redirect: "follow", - cache: "no-store", - signal: AbortSignal.timeout(2500), - }); - return response.ok; - } catch { - return false; - } -} - -async function resolveOnlyOfficeInternalUrl() { - const now = Date.now(); - if (cachedOnlyOfficeInternalUrl && now - cachedOnlyOfficeInternalUrlAt < ONLYOFFICE_RESOLVE_CACHE_TTL_MS) { - return new URL(`${cachedOnlyOfficeInternalUrl}/`); - } - - if (!pendingOnlyOfficeInternalUrl) { - pendingOnlyOfficeInternalUrl = (async () => { - const candidates = listOnlyOfficeInternalUrlCandidates(); - for (const candidate of candidates) { - if (await probeOnlyOfficeInternalUrl(candidate)) { - return candidate; - } - } - return candidates[0] || DEFAULT_ONLYOFFICE_INTERNAL_URL; - })(); - } - - try { - const resolved = await pendingOnlyOfficeInternalUrl; - cachedOnlyOfficeInternalUrl = resolved; - cachedOnlyOfficeInternalUrlAt = Date.now(); - return new URL(`${resolved}/`); - } finally { - pendingOnlyOfficeInternalUrl = null; - } -} - -function buildUpstreamRequestHead(req, targetUrl, prefix) { - const incoming = new URL(req.url || "/", "http://localhost"); - const rawPath = incoming.pathname || "/"; - const stripped = rawPath === prefix ? "/" : rawPath.slice(prefix.length) || "/"; - - const basePath = String(targetUrl.pathname || "/").replace(/\/+$/, "") || ""; - const upstreamPath = `${basePath}${stripped}`.replace(/\/{2,}/g, "/") + (incoming.search || ""); - - const lines = []; - lines.push(`${req.method || "GET"} ${upstreamPath} HTTP/1.1`); - - const headers = req.headers || {}; - const headerValue = (name) => { - const v = headers[name]; - if (!v) return ""; - return Array.isArray(v) ? v[0] : String(v); - }; - - const originLike = headerValue("origin") || headerValue("referer") || ""; - const originUrl = (() => { - try { - if (!originLike) return null; - return new URL(originLike); - } catch { - return null; - } - })(); - - const forwardedHostRaw = - headerValue("x-forwarded-host") || (originUrl ? originUrl.host : "") || headerValue("host") || ""; - const forwardedProto = - (headerValue("x-forwarded-proto") || "").split(",")[0].trim() || - (originUrl ? originUrl.protocol.replace(":", "") : "") || - "http"; - const forwardedPort = (() => { - const fromHeader = (headerValue("x-forwarded-port") || "").split(",")[0].trim(); - if (fromHeader) return fromHeader; - const hostHasPort = forwardedHostRaw.includes(":") ? forwardedHostRaw.split(":").pop() : ""; - if (hostHasPort && /^\d+$/.test(hostHasPort)) return hostHasPort; - return forwardedProto === "https" ? "443" : "80"; - })(); - const forwardedHost = forwardedHostRaw.split(",")[0].trim() || "localhost"; - - for (const [k, v] of Object.entries(headers)) { - if (!v) continue; - const key = String(k); - if (key.toLowerCase() === "host") continue; - if (Array.isArray(v)) { - lines.push(`${key}: ${v.join(", ")}`); - } else { - lines.push(`${key}: ${String(v)}`); - } - } - - lines.push(`x-forwarded-host: ${forwardedHost}`); - lines.push(`x-forwarded-proto: ${forwardedProto}`); - lines.push(`x-forwarded-port: ${forwardedPort}`); - lines.push(`x-forwarded-prefix: ${prefix}`); - lines.push(`Host: ${targetUrl.host}`); - lines.push(""); - lines.push(""); - return lines.join("\r\n"); -} - -async function proxyOnlyOfficeUpgrade(req, socket, head) { - let target; - try { - target = await resolveOnlyOfficeInternalUrl(); - } catch (err) { - try { - const msg = err && err.message ? String(err.message) : String(err || ""); - console.log("[prod-server][onlyoffice-ws] resolve error", msg); - } catch {} - try { - socket.destroy(); - } catch {} - return; - } - const port = Number(target.port) || (target.protocol === "https:" ? 443 : 80); - - const upstream = net.connect({ host: target.hostname, port }, () => { - try { - const reqHead = buildUpstreamRequestHead(req, target, ONLYOFFICE_PREFIX); - upstream.write(reqHead); - if (head && head.length > 0) upstream.write(head); - socket.pipe(upstream); - upstream.pipe(socket); - } catch { - try { - socket.destroy(); - } catch {} - try { - upstream.destroy(); - } catch {} - } - }); - - const onError = (err) => { - try { - const msg = err && err.message ? String(err.message) : String(err || ""); - console.log("[prod-server][onlyoffice-ws] proxy error", msg); - } catch {} - try { - socket.destroy(); - } catch {} - try { - upstream.destroy(); - } catch {} - }; - - upstream.on("error", onError); - socket.on("error", onError); -} - -function resolveConvexInternalUrl() { - const raw = (process.env.CONVEX_INTERNAL_URL || "http://127.0.0.1:3210").trim(); - try { - return new URL(raw.replace(/\/+$/, "") + "/"); - } catch { - return new URL("http://127.0.0.1:3210/"); - } -} - -function proxyConvexUpgrade(req, socket, head) { - const target = resolveConvexInternalUrl(); - const port = Number(target.port) || (target.protocol === "https:" ? 443 : 80); - - const upstream = net.connect({ host: target.hostname, port }, () => { - try { - const reqHead = buildUpstreamRequestHead(req, target, CONVEX_PREFIX); - upstream.write(reqHead); - if (head && head.length > 0) upstream.write(head); - socket.pipe(upstream); - upstream.pipe(socket); - } catch { - try { - socket.destroy(); - } catch {} - try { - upstream.destroy(); - } catch {} - } - }); - - const onError = (err) => { - try { - const msg = err && err.message ? String(err.message) : String(err || ""); - console.log("[prod-server][convex-ws] proxy error", msg); - } catch {} - try { - socket.destroy(); - } catch {} - try { - upstream.destroy(); - } catch {} - }; - - upstream.on("error", onError); - socket.on("error", onError); -} - -function proxyConvexHttp(req, res) { - const target = resolveConvexInternalUrl(); - const incoming = new URL(req.url || "/", "http://localhost"); - const rawPath = incoming.pathname || "/"; - const stripped = rawPath === CONVEX_PREFIX ? "/" : rawPath.slice(CONVEX_PREFIX.length) || "/"; - const upstreamPath = stripped + (incoming.search || ""); - - const isHttps = target.protocol === "https:"; - const mod = isHttps ? require("https") : require("http"); - - const headers = { ...(req.headers || {}) }; - headers.host = target.host; - - const forwardedHostRaw = String(headers["x-forwarded-host"] || req.headers.host || ""); - const forwardedHost = forwardedHostRaw.split(",")[0].trim() || "localhost"; - const forwardedProto = - String(headers["x-forwarded-proto"] || "").split(",")[0].trim() || - (String(req.headers.origin || "").startsWith("https") ? "https" : "http"); - const forwardedPortRaw = String(headers["x-forwarded-port"] || "").split(",")[0].trim(); - const forwardedPort = (() => { - if (forwardedPortRaw) return forwardedPortRaw; - const hostHasPort = forwardedHost.includes(":") ? forwardedHost.split(":").pop() : ""; - if (hostHasPort && /^\d+$/.test(hostHasPort)) return hostHasPort; - return forwardedProto === "https" ? "443" : "80"; - })(); - - headers["x-forwarded-host"] = forwardedHost; - headers["x-forwarded-proto"] = forwardedProto; - headers["x-forwarded-port"] = forwardedPort; - headers["x-forwarded-prefix"] = CONVEX_PREFIX; - - const upstreamReq = mod.request( - { - protocol: target.protocol, - hostname: target.hostname, - port: target.port || (isHttps ? 443 : 80), - method: req.method, - path: upstreamPath, - headers, - }, - (upstreamRes) => { - res.writeHead(upstreamRes.statusCode || 502, upstreamRes.headers || {}); - upstreamRes.pipe(res); - }, - ); - - upstreamReq.on("error", (err) => { - try { - console.log("[prod-server][convex-http] proxy error", err && err.message ? err.message : String(err || "")); - } catch {} - try { - res.statusCode = 502; - res.end("Bad Gateway"); - } catch {} - }); - - req.pipe(upstreamReq); -} - -async function main() { - const port = resolvePort(); - const hostname = resolveHostname(); - const dev = false; - - const app = next({ dev, dir: path.join(__dirname, "..") }); - const handle = app.getRequestHandler(); - - await app.prepare(); - const handleUpgrade = typeof app.getUpgradeHandler === "function" ? app.getUpgradeHandler() : null; - - const server = http.createServer((req, res) => { - try { - res.setHeader("x-mnote-prod-server", "1"); - res.setHeader("x-mnote-onlyoffice-ws-proxy", "1"); - res.setHeader("x-mnote-convex-ws-proxy", "1"); - } catch { - // ignore - } - - if (isConvexPath(req.url || "/")) { - proxyConvexHttp(req, res); - return; - } - - const parsed = parseUrl(req.url || "/", true); - handle(req, res, parsed); - }); - - server.on("upgrade", (req, socket, head) => { - if (isConvexPath(req.url || "/")) { - proxyConvexUpgrade(req, socket, head); - return; - } - if (isOnlyOfficePath(req.url || "/")) { - proxyOnlyOfficeUpgrade(req, socket, head); - return; - } - if (handleUpgrade) { - handleUpgrade(req, socket, head); - return; - } - try { - socket.destroy(); - } catch { - // ignore - } - }); - - server.listen(port, hostname, () => { - resolveOnlyOfficeInternalUrl() - .then((onlyofficeTarget) => { - console.log( - `[prod-server] ready http://${hostname}:${port} (ONLYOFFICE ws via ${ONLYOFFICE_PREFIX} -> ${onlyofficeTarget.toString().replace(/\/$/, "")}; Convex ws via ${CONVEX_PREFIX} -> ${process.env.CONVEX_INTERNAL_URL || "http://127.0.0.1:3210"})`, - ); - }) - .catch(() => { - console.log( - `[prod-server] ready http://${hostname}:${port} (ONLYOFFICE ws via ${ONLYOFFICE_PREFIX} -> ${process.env.ONLYOFFICE_INTERNAL_URL || DEFAULT_ONLYOFFICE_INTERNAL_URL}; Convex ws via ${CONVEX_PREFIX} -> ${process.env.CONVEX_INTERNAL_URL || "http://127.0.0.1:3210"})`, - ); - }); - }); -} - -main().catch((err) => { - console.error(err instanceof Error ? err.stack : String(err)); - process.exit(1); -}); - diff --git a/wolai-frontend/scripts/register-test-user.js b/wolai-frontend/scripts/register-test-user.js deleted file mode 100644 index 3be33733..00000000 --- a/wolai-frontend/scripts/register-test-user.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * 注册测试账号脚本 - * - * 用途:快速注册测试账号,方便开发和测试 - * 运行:node scripts/register-test-user.js - */ - -const TEST_CREDENTIALS = { - email: "mnote.e2e@example.com", - password: "MnoteE2E123!", - name: "mnote-e2e", -}; - -async function registerTestUser() { - const baseUrl = "http://localhost:3000"; - - console.log("正在注册测试账号..."); - console.log(`邮箱: ${TEST_CREDENTIALS.email}`); - console.log(`密码: ${TEST_CREDENTIALS.password}`); - - try { - const response = await fetch(`${baseUrl}/api/auth`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - action: "auth:signIn", - args: { - provider: "password", - params: { - email: TEST_CREDENTIALS.email, - password: TEST_CREDENTIALS.password, - name: TEST_CREDENTIALS.name, - flow: "signUp", - }, - }, - }), - }); - - const result = await response.json(); - - if (response.ok) { - console.log("✓ 测试账号注册成功!"); - console.log(`\n您现在可以使用以下凭据登录:`); - console.log(` 邮箱: ${TEST_CREDENTIALS.email}`); - console.log(` 密码: ${TEST_CREDENTIALS.password}`); - console.log(`\n或在登录页面点击"测试账号快速登录"按钮。`); - } else { - const message = result?.error || result?.message || ""; - if (/already exists|已存在|已被使用|已被占用/i.test(message)) { - console.log("ℹ 测试账号已存在,可直接使用:"); - console.log(` 邮箱: ${TEST_CREDENTIALS.email}`); - console.log(` 密码: ${TEST_CREDENTIALS.password}`); - return; - } - console.log("ℹ 自动注册失败,请通过浏览器手动注册:"); - console.log(` 1. 访问 http://localhost:3000/auth`); - console.log(` 2. 点击"测试账号快速登录"或"还没有账户?立即注册"`); - console.log(` 3. 填写:`); - console.log(` 邮箱: ${TEST_CREDENTIALS.email}`); - console.log(` 密码: ${TEST_CREDENTIALS.password}`); - console.log(` 姓名: ${TEST_CREDENTIALS.name}`); - } - } catch (error) { - console.error("✗ 请求失败:", error.message); - console.log("\n请确保开发服务器正在运行 (pnpm dev)"); - } -} - -registerTestUser(); diff --git a/wolai-frontend/scripts/set-auth-env.js b/wolai-frontend/scripts/set-auth-env.js deleted file mode 100644 index a12dfac3..00000000 --- a/wolai-frontend/scripts/set-auth-env.js +++ /dev/null @@ -1,57 +0,0 @@ -const { execSync } = require('child_process'); - -function setConvexEnv(name, value) { - // 说明:使用 stdin 传值,避免以 `-----BEGIN ...` 开头的内容被 CLI 误当作参数/选项。 - execSync(`npx convex env set ${name}`, { - input: String(value ?? ""), - stdio: ['pipe', 'inherit', 'inherit'], - cwd: process.cwd(), - }); -} - -const privateKey = `-----BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCPcrWDwjh74nu1 -gfYo9ywVIRMedfPwbNiDjHnovzHAIeToc1+Lw3ju/io9OME3KLWzIibUHX39LilK -sSQKigJ3+i7J13IquJD++ozY9C8Dp/P34txVY+ECSQRdIBbETLJxYBWXOx6Ysw/d -/CUuDOdEw2IhNvN97sTh5jUZe6f/H6mfCpJ3X5SZrqrIUTTL6r4Lj9ZS3PUX5ivU -6sd2rXzpDsUQgrNgmvGN3w9nAHudxAUv/zOGa4802Z+z14LCBIX6v6PIK79YQ4s9 -/5hH73MesA6vgzWShJcyVy3guyde+duhpECo1q3d6dIZeqrazQwqAW3vcpmEFJdX -jkpociDfAgMBAAECggEAAYtRE+mH1SGThlkvTrKWeWXBQG8xoJFzZTsiZtSEExbq -UWxIh4cjqqL2znDpd5ALILIJ6/ejTxHrpN+yTSC+NQ9u6IJWusoA2ZXV5VH/nZD1 -yeHZ0FuCZRVnJB9/zz4qH5lSsi2TPz6SOagIuG2wIafeyw+94EmtOedSBAO2Q8NN -3jHoINBCyRu6hU3ml0h7daoIhUw9ONI7MZUlYvuV7Ti3yf+czzpqwYx3gZA39sa8 -qUbJPFk6ts+CVjqSAdYVSfU3TC1Us1usOM3+04mACp3vc6ZUKxnB4Dsk/sSHj71n -EcZ3fOR7EgjmXf6wBiq24T2+0UzoHW2yDaiAowr6iQKBgQDDs2eJTopFejt47EMm -zY//e8fyUDKx07CWwUZKP78IMTZcp8BwHmKTSY+VbQLvVYnNxADkckYapokjU3a7 -GMOXxvKVLqfgU/oUBIkRFDRo+nasFCd5cPpnYjQ6lGalBkDghVlyvq7kKF/MPtIK -dLqsV3tZDXoHX2mjTga94ygTZwKBgQC7pa+eZdWiz275mpYql9Gs6N3G3HhcpRxb -oWYWekCtZQ9gtecpvA9e0tW3ShoF19ksWYGpM4vxAOXul5Ei9IPVVNHc7RvGF5CO -0Zryx3qaVk7E6XQc3BQVVAenT2fNiv3+fkCMeAvcZBZOGiPPziCCT3wU3bd3tsn8 -EsMiAPvTyQKBgEs0iWhBr29NrsckfBXQTzMN/WOIIEMoJ6d3dKyZ3K6oQszOhmxP -sPALB8uTjdotk/xoAzPHGlupfe/+ZhU2Sgvsn1JnEIprmyHQMGBI1G83OR2dzSGl -IgVSvuF4IA3w3kOp2xr2Xj09qrrRtWPhQc9y+urY+/kTWIQyOvMD9WWnAoGADdUN -2BBLqj++P3oMvcEJPMTBrGoOGU42g+6m1ttWLzH26zsdei8ZtvS1ulglCO87XBCR -BUb+dtqJGIhls3zwxuYEvlNgK78K8ewzjtfzirL4BX3sCECU3mmeUtAAp98qD/uA -iJpEzY83MbStlSDtto1jaSpa3uFDjGhZqAUIizkCgYABf/HSMwWsnvRgo0JMmDru -3L/xScJiFVdFV9vNiSckjUYhx7lgFqEjsDEN1mZiRIa2UhLOqpo5S/cIXfMkeEPh -9Rusf9STwNwOOyhFRrHCxqhTH6Ivnj6oZSM/UrS8GvCVMPWs1z8k45FWKI/IRXQq -AT5PRKihrWbg63/rXaSCxw== ------END PRIVATE KEY-----`; - -const jwks = '{"keys":[{"use":"sig","kty":"RSA","n":"j3K1g8I4e-J7tYH2KPcsFSETHnXz8GzYg4x56L8xwCHk6HNfi8N47v4qPTjBNyi1syIm1B19_S4pSrEkCooCd_ouyddyKriQ_vqM2PQvA6fz9-LcVWPhAkkEXSAWxEyycWAVlzsemLMP3fwlLgznRMNiITbzfe7E4eY1GXun_x-pnwqSd1-Uma6qyFE0y-q-C4_WUtz1F-Yr1OrHdq186Q7FEIKzYJrxjd8PZwB7ncQFL_8zhmuPNNmfs9eCwgSF-r-jyCu_WEOLPf-YR-9zHrAOr4M1koSXMlct4LsnXvnboaRAqNat3enSGXqq2s0MKgFt73KZhBSXV45KaHIg3w","e":"AQAB"}]}'; - -console.log('Setting JWT_PRIVATE_KEY...'); -try { - setConvexEnv("JWT_PRIVATE_KEY", privateKey); -} catch (e) { - console.error('Failed to set JWT_PRIVATE_KEY:', e.message); -} - -console.log('Setting JWKS...'); -try { - setConvexEnv("JWKS", jwks); -} catch (e) { - console.error('Failed to set JWKS:', e.message); -} - -console.log('Done!'); diff --git a/wolai-frontend/src/app/(app)/documents/[id]/[...filePath]/route.ts b/wolai-frontend/src/app/(app)/documents/[id]/[...filePath]/route.ts deleted file mode 100644 index fa037674..00000000 --- a/wolai-frontend/src/app/(app)/documents/[id]/[...filePath]/route.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { promises as fs } from "fs"; -import path from "path"; - -import { getDocumentsBaseDir } from "@/lib/server/local-paths"; - -export const dynamic = "force-dynamic"; - -function contentTypeByExt(file: string): string { - const ext = path.extname(file).toLowerCase(); - if (ext === ".md") return "text/markdown; charset=utf-8"; - if (ext === ".json") return "application/json; charset=utf-8"; - if (ext === ".txt") return "text/plain; charset=utf-8"; - return "application/octet-stream"; -} - -export async function GET( - _req: Request, - { params }: { params: Promise<{ id: string; filePath: string[] }> }, -) { - const { id, filePath } = await params; - if (!id || !Array.isArray(filePath) || filePath.length === 0) { - return new Response("Not Found", { status: 404 }); - } - - const baseDir = path.resolve(getDocumentsBaseDir(), id); - const resolved = path.resolve(baseDir, ...filePath); - if (!resolved.startsWith(baseDir + path.sep)) { - return new Response("Bad Request", { status: 400 }); - } - - try { - const buf = await fs.readFile(resolved); - return new Response(buf, { - headers: { - "Content-Type": contentTypeByExt(resolved), - "Cache-Control": "no-store", - }, - }); - } catch { - return new Response("Not Found", { status: 404 }); - } -} - diff --git a/wolai-frontend/src/app/(app)/documents/[id]/loading.tsx b/wolai-frontend/src/app/(app)/documents/[id]/loading.tsx deleted file mode 100644 index c1020817..00000000 --- a/wolai-frontend/src/app/(app)/documents/[id]/loading.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Skeleton } from "@/components/ui/skeleton"; - -export default function LoadingDocument() { - return ( -
- - - -
- ); -} diff --git a/wolai-frontend/src/app/(app)/documents/[id]/page.tsx b/wolai-frontend/src/app/(app)/documents/[id]/page.tsx deleted file mode 100644 index 181d5d89..00000000 --- a/wolai-frontend/src/app/(app)/documents/[id]/page.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { notFound, redirect } from "next/navigation"; -import { DocumentShell } from "@/components/editor/document-shell"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { - DEFAULT_EDITOR_HOST_KIND, - normalizeEditorHostKind, - resolveEditorHostKind, - type EditorHostKind, -} from "@/components/editor/editor-host-config"; -import { getMnoteRuntimeConfig } from "@/lib/runtime-config"; -import { loadPageAggregateFromNextHeaders } from "@/lib/documents/page-aggregate-loader"; - -interface DocumentPageProps { - params: Promise<{ id: string }>; - searchParams?: Promise>; -} - -export default async function DocumentPage({ params, searchParams }: DocumentPageProps) { - const { id } = await params; - const resolvedSearch = (await searchParams) ?? {}; - const openTableIdRaw = resolvedSearch?.openTableId; - const openTableId = typeof openTableIdRaw === "string" ? openTableIdRaw : null; - const runtimeConfig = getMnoteRuntimeConfig(); - const queryEditorHostRaw = resolvedSearch?.editorHost; - const queryHostAliasRaw = resolvedSearch?.host; - const queryHostRaw = - typeof queryEditorHostRaw === "string" - ? queryEditorHostRaw - : typeof queryHostAliasRaw === "string" - ? queryHostAliasRaw - : undefined; - const runtimeHostRaw = runtimeConfig.documentEditorHost; - - // 说明:优先级保持稳定且可预期: - // 1) query(兼容 editorHost,并支持 host 别名); - // 2) 运行时配置 documentEditorHost; - // 3) 默认正式主链 leptos_tiptap_island。 - const editorHostKind: EditorHostKind = (() => { - if (typeof queryHostRaw === "string") { - return resolveEditorHostKind({ - override: queryHostRaw, - runtimeDefault: runtimeHostRaw, - }); - } - return normalizeEditorHostKind(runtimeHostRaw, DEFAULT_EDITOR_HOST_KIND); - })(); - - if (isConvexEnabled()) { - const workspaceIdRaw = resolvedSearch?.workspaceId; - const workspaceId = typeof workspaceIdRaw === "string" ? workspaceIdRaw : null; - const loaded = await loadPageAggregateFromNextHeaders({ - documentId: id, - workspaceId, - }); - if (!loaded) { - notFound(); - } - - return ( -
-
- -
-
- ); - } - - redirect("/auth"); -} diff --git a/wolai-frontend/src/app/(app)/layout.tsx b/wolai-frontend/src/app/(app)/layout.tsx deleted file mode 100644 index fd670aef..00000000 --- a/wolai-frontend/src/app/(app)/layout.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { redirect } from "next/navigation"; -import type { ReactNode } from "react"; -import { AppLayoutShell } from "@/components/app-layout-shell"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { getAuthedConvexClient } from "@/lib/convex/route"; -import { loadSidebarDataFromConvex } from "@/lib/server/sidebar-data"; - -export default async function AppLayout({ children }: { children: ReactNode }) { - if (isConvexEnabled()) { - const { auth, client } = await getAuthedConvexClient(); - const { - sidebarInitialData, - } = await loadSidebarDataFromConvex({ - client, - auth, - fallbackName: auth.name ?? auth.email ?? "我的空间", - }); - - if (!sidebarInitialData) { - return ( -
-
-
{children}
-
-
- ); - } - - return ( - - {children} - - ); - } - - redirect("/login"); -} diff --git a/wolai-frontend/src/app/(app)/wolai-import/page.tsx b/wolai-frontend/src/app/(app)/wolai-import/page.tsx deleted file mode 100644 index 1b21f86c..00000000 --- a/wolai-frontend/src/app/(app)/wolai-import/page.tsx +++ /dev/null @@ -1,169 +0,0 @@ -"use client"; - -import { useEffect, useMemo, useState } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { cn } from "@/lib/utils"; - -export default function WolaiImportPage() { - const router = useRouter(); - const searchParams = useSearchParams(); - const parentId = useMemo(() => searchParams.get("parentId") ?? "", [searchParams]); - - // 说明:该页面是工具页,避免开发环境下偶发的 hydration mismatch(SSR 与客户端首屏不一致)。 - // 首屏统一渲染一个稳定的占位内容,挂载后再渲染真实表单。 - const [mounted, setMounted] = useState(false); - useEffect(() => setMounted(true), []); - - const [file, setFile] = useState(null); - const [zipPath, setZipPath] = useState(""); - const [rootMdPath, setRootMdPath] = useState(""); - const [busy, setBusy] = useState(false); - const [error, setError] = useState(""); - const [warnings, setWarnings] = useState([]); - - const handleSubmit = async () => { - if (!zipPath.trim() && !file) { - setError("请填写本地 ZIP 路径,或选择 ZIP 文件"); - return; - } - setBusy(true); - setError(""); - setWarnings([]); - try { - const useLocalPath = Boolean(zipPath.trim()); - let res: Response; - if (useLocalPath) { - res = await fetch("/api/wolai-import", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - zipPath: zipPath.trim(), - parentId: parentId || undefined, - rootMdPath: rootMdPath.trim() || undefined, - }), - }); - } else { - if (!file) { - setError("请选择 ZIP 文件"); - return; - } - const formData = new FormData(); - formData.set("file", file); - if (parentId) formData.set("parentId", parentId); - if (rootMdPath.trim()) formData.set("rootMdPath", rootMdPath.trim()); - res = await fetch("/api/wolai-import", { method: "POST", body: formData }); - } - const payload = (await res.json().catch(() => null)) as any; - if (!res.ok) { - const msg = payload?.error ? String(payload.error) : `导入失败:${res.status}`; - setError(msg); - if (res.status === 409 && Array.isArray(payload?.candidates)) { - setWarnings([`无法自动判断入口页面,请在“入口 rootMdPath”填入其中一个:`, ...payload.candidates.map(String)]); - } - return; - } - - const ws = Array.isArray(payload?.warnings) ? payload.warnings.map(String) : []; - setWarnings(ws); - - const rootDocumentId = String(payload?.rootDocumentId ?? ""); - if (rootDocumentId) { - router.push(`/documents/${encodeURIComponent(rootDocumentId)}`); - return; - } - setError("导入完成但缺少 rootDocumentId"); - } catch (err) { - const msg = err instanceof Error ? err.message : "导入失败"; - setError(msg); - } finally { - setBusy(false); - } - }; - - if (!mounted) { - return ( -
- - - 导入 Wolai(Markdown ZIP) - - 加载中... - -
- ); - } - - return ( -
- - - 导入 Wolai(Markdown ZIP) - - -
-
本地 ZIP 路径(大文件推荐)
- setZipPath(e.target.value)} - placeholder="例如:C:\\Users\\liaib\\Downloads\\个人.zip" - disabled={busy} - /> -
- 说明:你的 个人.zip 约 1.26GB,浏览器上传很容易 500。 - 建议填本地路径并清空 ZIP 文件选择(或不选择文件)。 -
-
- -
-
ZIP 文件
- setFile(e.target.files?.[0] ?? null)} - disabled={busy} - /> -
说明:请使用 Wolai 导出的“Markdown(含附件/图片)”ZIP。
-
- -
-
入口 rootMdPath(可选)
- setRootMdPath(e.target.value)} - placeholder="例如:dQeAax/个人.md(留空则自动判断)" - disabled={busy} - /> -
- 自动判断失败时会返回候选列表,你可以把其中一个填进来重试。 -
-
- - {parentId ? ( -
- 将作为子页面导入到:{parentId} -
- ) : null} - - {error ?
{error}
: null} - {warnings.length > 0 ? ( -
- {warnings.join("\n")} -
- ) : null} - -
- - -
-
-
-
- ); -} diff --git a/wolai-frontend/src/app/(auth)/auth/page.tsx b/wolai-frontend/src/app/(auth)/auth/page.tsx deleted file mode 100644 index 83461ad9..00000000 --- a/wolai-frontend/src/app/(auth)/auth/page.tsx +++ /dev/null @@ -1,368 +0,0 @@ -"use client"; - -import { useConvexAuth, useMutation, useQuery } from "convex/react"; -import { useAuthActions } from "@convex-dev/auth/react"; -import { useState, useCallback, useEffect } from "react"; -import { useRouter } from "next/navigation"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { api } from "@/lib/convex/api"; -import { getUserFacingErrorMessage } from "@/lib/auth/errors"; - -type AuthStep = "signIn" | "signUp"; - -// 测试账号凭据常量 -const TEST_CREDENTIALS = { - email: "mnote.e2e@example.com", - password: "MnoteE2E123!", -} as const; - -/** - * Convex Auth 登录/注册页面 - * - * 支持功能: - * - 邮箱密码登录 - * - 邮箱密码注册 - */ -export default function AuthPage() { - const { isLoading, isAuthenticated } = useConvexAuth(); - const { signIn } = useAuthActions(); - const setMyUsername = useMutation(api.users.setMyUsername); - const router = useRouter(); - - const currentUser = useQuery(api.users.currentUser, isAuthenticated ? {} : "skip"); - - const navigateToHome = useCallback(() => { - if (typeof window !== "undefined") { - window.location.assign("/"); - return; - } - router.replace("/"); - }, [router]); - - useEffect(() => { - if (isLoading) return; - if (!isAuthenticated) return; - if (currentUser === undefined) return; - if (currentUser && currentUser.name) { - navigateToHome(); - } - }, [currentUser, isAuthenticated, isLoading, navigateToHome]); - - const [flow, setFlow] = useState("signIn"); - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [username, setUsername] = useState(""); - const [pendingUsernameToSave, setPendingUsernameToSave] = useState(null); - const [isSavingUsername, setIsSavingUsername] = useState(false); - const [message, setMessage] = useState<{ - type: "success" | "error" | "info"; - text: string; - } | null>(null); - - // 注册完成后,等待登录态同步完成再写入用户名,避免出现“未登录”的竞态 - useEffect(() => { - if (!pendingUsernameToSave) return; - if (!isAuthenticated) return; - if (currentUser === undefined || currentUser === null) return; - if (currentUser.name) { - setPendingUsernameToSave(null); - return; - } - - let cancelled = false; - setIsSavingUsername(true); - - (async () => { - try { - await setMyUsername({ username: pendingUsernameToSave }); - if (cancelled) return; - setPendingUsernameToSave(null); - setMessage({ type: "success", text: "注册成功!" }); - navigateToHome(); - } catch (err: unknown) { - if (cancelled) return; - setPendingUsernameToSave(null); - setMessage({ type: "error", text: getUserFacingErrorMessage(err, "用户名设置失败,请重试") }); - } finally { - if (!cancelled) setIsSavingUsername(false); - } - })(); - - return () => { - cancelled = true; - }; - }, [currentUser, isAuthenticated, navigateToHome, pendingUsernameToSave, setMyUsername]); - - // 执行登录的核心逻辑 - const performSignIn = useCallback(async (email: string, password: string, flow: AuthStep) => { - setMessage(null); - - try { - if (flow === "signUp" && !username.trim()) { - setMessage({ type: "error", text: "用户名不能为空" }); - return; - } - - const formData = new FormData(); - formData.append("email", email); - formData.append("password", password); - formData.append("flow", flow); - - const result = await signIn("password", formData); - - if (result.signingIn) { - if (flow === "signUp") { - // 说明:注册已成功,但此刻登录态可能尚未同步到 Convex functions。 - // 这里先进入“待写入用户名”状态,等 currentUser 可用后再写,避免出现“未登录”的竞态。 - setMessage({ type: "info", text: "注册成功,正在保存用户名..." }); - setPendingUsernameToSave(username.trim()); - return; - } - // 说明:登录成功后不要立刻强推 `/`。 - // Convex Auth cookie 与 currentUser 查询存在短暂同步窗口,过早跳转会把页面推进到根路由空白态。 - // 这里仅提示“正在同步”,真正跳转交给上面的 useEffect 在登录态稳定后执行。 - setMessage({ type: "info", text: "登录成功,正在同步登录状态..." }); - return; - } - - // 说明:部分认证方式会返回 redirect(例如 OAuth);密码模式一般不会走到这里。 - if (result.redirect) { - setMessage({ type: "info", text: "正在跳转..." }); - router.push(result.redirect.toString()); - return; - } - - setMessage({ type: "error", text: "操作失败,请重试" }); - } catch (error: any) { - setMessage({ type: "error", text: getUserFacingErrorMessage(error, "操作失败,请重试") }); - } - }, [router, signIn, username]); - - const handleSubmit = useCallback(async (event: React.FormEvent) => { - event.preventDefault(); - await performSignIn(email, password, flow); - }, [email, password, flow, performSignIn]); - - // 检查是否启用了 Convex - const isConvex = isConvexEnabled(); - if (!isConvex) { - return ( -
-
-

认证功能不可用

-

请启用 Convex 后端后再试

-
-
- ); - } - - if (isLoading) { - return ( -
-
-
-

加载中...

-
-
- ); - } - - if (isAuthenticated && currentUser && currentUser.name) { - return ( -
-
-
-

正在跳转...

-
-
- ); - } - - if (isAuthenticated) { - if (currentUser === undefined || currentUser === null) { - return ( -
-
-
-

正在同步登录状态...

-
-
- ); - } - - const saveUsername = async () => { - setMessage(null); - try { - if (isSavingUsername) return; - setIsSavingUsername(true); - await setMyUsername({ username: username.trim() }); - setMessage({ type: "success", text: "用户名已保存!" }); - navigateToHome(); - } catch (error: any) { - setMessage({ type: "error", text: getUserFacingErrorMessage(error, "保存失败,请重试") }); - } finally { - setIsSavingUsername(false); - } - }; - - return ( -
-
-
-

设置用户名

-

用于分享功能的用户查找(全局唯一)

-
- - {message && ( -
-

{message.text}

-
- )} - -
-
- - setUsername(e.target.value)} - disabled={isSavingUsername} - className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" - placeholder="用户名(2-32 位,不含空格)" - /> -
- -
-
-
- ); - } - - return ( -
-
-
-

- {flow === "signIn" ? "登录账户" : "创建账户"} -

-

- 使用 Convex Auth -

-
- - {message && ( -
-

{message.text}

-
- )} - -
-
-
- - setEmail(e.target.value)} - className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm" - placeholder="邮箱地址" - /> -
- {flow === "signUp" && ( -
- - setUsername(e.target.value)} - className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm" - placeholder="用户名(2-32 位,不含空格)" - /> -
- )} -
- - setPassword(e.target.value)} - className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm" - placeholder="密码(至少 8 位)" - /> -
-
- -
- -
- - {flow === "signIn" && ( -
- -
- )} - -
- -
-
-
-
- ); -} diff --git a/wolai-frontend/src/app/(auth)/login/page.tsx b/wolai-frontend/src/app/(auth)/login/page.tsx deleted file mode 100644 index 696a8eba..00000000 --- a/wolai-frontend/src/app/(auth)/login/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -"use client"; - -import { useEffect } from "react"; -import { useRouter } from "next/navigation"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; - -export default function LoginPage() { - const router = useRouter(); - - useEffect(() => { - // 已移除 Supabase 登录,统一走 Convex Auth 页面。 - router.replace("/auth"); - }, [router]); - - return ( -
- - - 正在跳转 -

该页面已迁移到 Convex Auth(/auth)。

-
- -

如果没有自动跳转,请手动打开 /auth。

-
-
-
- ); -} diff --git a/wolai-frontend/src/app/api/.gitkeep b/wolai-frontend/src/app/api/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/wolai-frontend/src/app/api/ai-agent/client-tool-result/route.ts b/wolai-frontend/src/app/api/ai-agent/client-tool-result/route.ts deleted file mode 100644 index 6dfc54c4..00000000 --- a/wolai-frontend/src/app/api/ai-agent/client-tool-result/route.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { requireAuthContext } from "@/lib/auth/authContext"; -import { - buildClientToolKey, - resolveClientToolCall, - type ClientToolResult, -} from "@/lib/ai-agent/runtime/clientToolBridge"; - -export const dynamic = "force-dynamic"; - -type Payload = { - requestId: string; - callId: string; - ok: boolean; - result?: unknown; - error?: string; -}; - -export async function POST(request: Request) { - const payload = (await request.json().catch(() => null)) as Payload | null; - if (!payload) return NextResponse.json({ error: "缺少请求体" }, { status: 400 }); - - const requestId = String(payload.requestId ?? "").trim(); - const callId = String(payload.callId ?? "").trim(); - if (!requestId || !callId) { - return NextResponse.json({ error: "缺少 requestId/callId" }, { status: 400 }); - } - - let userId: string | null = null; - if (isConvexEnabled()) { - const auth = await requireAuthContext(); - userId = auth.userId; - } - - const resolvedUserId = async () => { - if (userId) return userId; - return null; - }; - - const finalUserId = await resolvedUserId(); - if (!finalUserId) return NextResponse.json({ error: "未登录" }, { status: 401 }); - - const result: ClientToolResult = payload.ok - ? { ok: true, result: "result" in payload ? payload.result : null } - : { ok: false, error: String(payload.error ?? "客户端工具执行失败") }; - - const key = buildClientToolKey(requestId, callId); - const resolved = resolveClientToolCall({ - key, - userId: finalUserId, - result, - }); - if (!resolved.ok) { - return NextResponse.json({ error: resolved.error }, { status: 404 }); - } - - return NextResponse.json({ ok: true }); -} diff --git a/wolai-frontend/src/app/api/ai-agent/document/config/route.test.ts b/wolai-frontend/src/app/api/ai-agent/document/config/route.test.ts deleted file mode 100644 index 7d0e1ff9..00000000 --- a/wolai-frontend/src/app/api/ai-agent/document/config/route.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const mockIsConvexEnabled = vi.fn(); -const mockGetAuthedConvexClient = vi.fn(); -const mockFetchDocumentAiOrchestratorConfig = vi.fn(); - -vi.mock("@/lib/convex/enabled", () => ({ - isConvexEnabled: mockIsConvexEnabled, -})); - -vi.mock("@/lib/convex/route", () => ({ - getAuthedConvexClient: mockGetAuthedConvexClient, -})); - -vi.mock("@/lib/server/document-ai-orchestrator", () => ({ - fetchDocumentAiOrchestratorConfig: mockFetchDocumentAiOrchestratorConfig, -})); - -describe("/api/ai-agent/document/config route", () => { - beforeEach(() => { - mockIsConvexEnabled.mockReset(); - mockGetAuthedConvexClient.mockReset(); - mockFetchDocumentAiOrchestratorConfig.mockReset(); - }); - - it("应返回 orchestrator config 能力面", async () => { - mockIsConvexEnabled.mockReturnValue(true); - mockGetAuthedConvexClient.mockResolvedValue({ - auth: { - userId: "user-1", - }, - }); - mockFetchDocumentAiOrchestratorConfig.mockResolvedValue({ - provider: "online", - defaultModelKey: "gpt-5.4", - defaultProfileId: "page_writer_ai_first", - sessionEnabled: true, - models: [{ key: "gpt-5.4", title: "GPT-5.4" }], - profiles: [{ id: "page_writer_ai_first", title: "AI 主写" }], - tools: [{ name: "doc_get", description: "读取当前页摘要" }], - }); - - const { GET } = await import("./route"); - const response = await GET(new Request("http://127.0.0.1:3000/api/ai-agent/document/config")); - const payload = await response.json(); - - expect(response.status).toBe(200); - expect(mockFetchDocumentAiOrchestratorConfig).toHaveBeenCalledTimes(1); - expect(payload.defaultModelKey).toBe("gpt-5.4"); - expect(payload.defaultProfileId).toBe("page_writer_ai_first"); - expect(payload.tools[0]?.name).toBe("doc_get"); - }); -}); diff --git a/wolai-frontend/src/app/api/ai-agent/document/config/route.ts b/wolai-frontend/src/app/api/ai-agent/document/config/route.ts deleted file mode 100644 index 5f027352..00000000 --- a/wolai-frontend/src/app/api/ai-agent/document/config/route.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { getAuthedConvexClient } from "@/lib/convex/route"; -import { fetchDocumentAiOrchestratorConfig } from "@/lib/server/document-ai-orchestrator"; - -export const dynamic = "force-dynamic"; - -export async function GET(request: Request) { - let userId = ""; - if (isConvexEnabled()) { - const { auth } = await getAuthedConvexClient(); - userId = auth.userId ?? ""; - } - if (!userId) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - try { - const payload = await fetchDocumentAiOrchestratorConfig({ request }); - return NextResponse.json(payload, { - headers: { - "Cache-Control": "no-store", - }, - }); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - return NextResponse.json({ error: message }, { status: 500 }); - } -} diff --git a/wolai-frontend/src/app/api/ai-agent/run/route.test.ts b/wolai-frontend/src/app/api/ai-agent/run/route.test.ts deleted file mode 100644 index 5767dfe1..00000000 --- a/wolai-frontend/src/app/api/ai-agent/run/route.test.ts +++ /dev/null @@ -1,282 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const mockSafeGetJsonBody = vi.fn(); -const mockValidateRequestBody = vi.fn(); -const mockIsConvexEnabled = vi.fn(); -const mockGetAuthedConvexClient = vi.fn(); -const mockStartMnoteCliAgentHostRun = vi.fn(); -const mockStartCodexJsonRun = vi.fn(); -const mockCodexMessagesToPrompt = vi.fn(); -const mockFindWorkspaceRoot = vi.fn(); -const mockStartHermesRun = vi.fn(); -const mockStreamHermesRunEvents = vi.fn(); -const mockRequireAuthContext = vi.fn(); -const mockGetConvexAuthedHttpClient = vi.fn(); - -vi.mock("next/server", () => ({ - NextResponse: class NextResponse extends Response { - static json(body: unknown, init?: ResponseInit) { - return new Response(JSON.stringify(body), { - ...init, - headers: { - "Content-Type": "application/json", - ...(init?.headers ?? {}), - }, - }); - } - }, -})); - -vi.mock("@/lib/api-utils", async () => { - const actual = await vi.importActual("@/lib/api-utils"); - return { - ...actual, - safeGetJsonBody: mockSafeGetJsonBody, - validateRequestBody: mockValidateRequestBody, - }; -}); - -vi.mock("@/lib/convex/enabled", () => ({ - isConvexEnabled: mockIsConvexEnabled, -})); - -vi.mock("@/lib/convex/route", () => ({ - getAuthedConvexClient: mockGetAuthedConvexClient, -})); - -vi.mock("@/lib/auth/authContext", () => ({ - requireAuthContext: mockRequireAuthContext, -})); - -vi.mock("@/lib/convex/server", () => ({ - getConvexAuthedHttpClient: mockGetConvexAuthedHttpClient, -})); - -vi.mock("@/lib/server/mnote-cli-agent-host", () => ({ - startMnoteCliAgentHostRun: mockStartMnoteCliAgentHostRun, -})); - -vi.mock("@/lib/ai/codex/codexExec", () => ({ - codexMessagesToPrompt: mockCodexMessagesToPrompt, - findWorkspaceRoot: mockFindWorkspaceRoot, - startCodexJsonRun: mockStartCodexJsonRun, -})); - -vi.mock("@/lib/ai-agent/hermes/bridge", () => ({ - startHermesRun: mockStartHermesRun, - streamHermesRunEvents: mockStreamHermesRunEvents, -})); - -describe("/api/ai-agent/run route", () => { - beforeEach(() => { - mockSafeGetJsonBody.mockReset(); - mockValidateRequestBody.mockReset(); - mockIsConvexEnabled.mockReset(); - mockGetAuthedConvexClient.mockReset(); - mockStartMnoteCliAgentHostRun.mockReset(); - mockStartCodexJsonRun.mockReset(); - mockCodexMessagesToPrompt.mockReset(); - mockFindWorkspaceRoot.mockReset(); - mockStartHermesRun.mockReset(); - mockStreamHermesRunEvents.mockReset(); - mockRequireAuthContext.mockReset(); - mockGetConvexAuthedHttpClient.mockReset(); - mockCodexMessagesToPrompt.mockImplementation((messages) => messages.map((m: any) => m.content).join("\n")); - mockFindWorkspaceRoot.mockResolvedValue("/mnt/Data1T/mnote"); - }); - - it("文档页在线请求应只进入 mnote-cli host", async () => { - mockSafeGetJsonBody.mockResolvedValue({ - stream: true, - scope: "document", - messages: [{ role: "user", content: "总结当前页面" }], - context: { - documentId: "doc-1", - documentBlocks: [], - pageOptions: { - wideLayout: true, - }, - }, - options: { - ai: { - provider: "online", - sessionId: "doc_ai:doc-1:session-1", - modelKey: "gpt-5.3-codex", - profileId: "page_writer_polish", - }, - }, - }); - mockValidateRequestBody.mockReturnValue(null); - mockIsConvexEnabled.mockReturnValue(true); - mockGetAuthedConvexClient.mockResolvedValue({ - auth: { - userId: "user-1", - }, - }); - mockRequireAuthContext.mockResolvedValue({ - userId: "user-1", - }); - mockGetConvexAuthedHttpClient.mockResolvedValue({}); - mockStartMnoteCliAgentHostRun.mockResolvedValue( - new Response('event: completion\ndata: {"ok":true,"text":"mnote-cli"}\n\n', { - headers: { - "Content-Type": "text/event-stream; charset=utf-8", - "x-mnote-ai-execution-owner": "mnote-cli", - }, - }), - ); - - const { POST } = await import("./route"); - const response = await POST( - new Request("http://127.0.0.1:3000/api/ai-agent/run", { - method: "POST", - }), - ); - const text = await response.text(); - - expect(response.status).toBe(200); - expect(mockStartMnoteCliAgentHostRun).toHaveBeenCalledWith( - expect.objectContaining({ - userId: "user-1", - payload: expect.objectContaining({ - scope: "document", - context: expect.objectContaining({ - documentId: "doc-1", - pageOptions: expect.objectContaining({ - wideLayout: true, - }), - }), - options: expect.objectContaining({ - ai: expect.objectContaining({ - provider: "online", - sessionId: "doc_ai:doc-1:session-1", - modelKey: "gpt-5.3-codex", - profileId: "page_writer_polish", - }), - }), - }), - }), - ); - expect(text).toContain("mnote-cli"); - }); - - it.each(["local", "ollama"] as const)( - "provider=%s 继续进入 mnote-cli host", - async (provider) => { - mockSafeGetJsonBody.mockResolvedValue({ - stream: true, - scope: "document", - messages: [{ role: "user", content: "处理当前页面" }], - context: { - documentId: "doc-1", - }, - options: { - ai: { - provider, - }, - }, - }); - mockValidateRequestBody.mockReturnValue(null); - mockIsConvexEnabled.mockReturnValue(true); - mockGetAuthedConvexClient.mockResolvedValue({ - auth: { - userId: "user-1", - }, - }); - mockRequireAuthContext.mockResolvedValue({ - userId: "user-1", - }); - mockGetConvexAuthedHttpClient.mockResolvedValue({}); - mockStartMnoteCliAgentHostRun.mockResolvedValue( - new Response('event: completion\ndata: {"ok":true,"text":"mnote-cli"}\n\n', { - headers: { - "Content-Type": "text/event-stream; charset=utf-8", - "x-mnote-ai-execution-owner": "mnote-cli", - }, - }), - ); - - const { POST } = await import("./route"); - const response = await POST( - new Request("http://127.0.0.1:3000/api/ai-agent/run", { - method: "POST", - }), - ); - - expect(response.status).toBe(200); - expect(mockStartMnoteCliAgentHostRun).toHaveBeenCalledTimes(1); - expect(mockStartMnoteCliAgentHostRun).toHaveBeenCalledWith( - expect.objectContaining({ - payload: expect.objectContaining({ - options: expect.objectContaining({ - ai: expect.objectContaining({ - provider, - }), - }), - }), - }), - ); - }, - ); - - it.each(["codex", "hermes", "claudecode"] as const)( - "provider=%s 已退场,必须明确失败且不能静默进入 mnote-cli", - async (provider) => { - mockSafeGetJsonBody.mockResolvedValue({ - stream: true, - scope: "document", - messages: [{ role: "user", content: "继续检查" }], - context: { - documentId: "doc-1", - }, - options: { - ai: { - provider, - }, - }, - }); - mockValidateRequestBody.mockReturnValue(null); - mockIsConvexEnabled.mockReturnValue(true); - mockGetAuthedConvexClient.mockResolvedValue({ - auth: { - userId: "user-1", - }, - }); - const { POST } = await import("./route"); - const response = await POST(new Request("http://127.0.0.1:3000/api/ai-agent/run", { method: "POST" })); - const text = await response.text(); - - expect(response.status).toBe(410); - expect(mockStartMnoteCliAgentHostRun).not.toHaveBeenCalled(); - expect(response.headers.get("x-mnote-ai-execution-owner")).toBe(`${provider}-retired`); - expect(mockStartCodexJsonRun).not.toHaveBeenCalled(); - expect(mockStartHermesRun).not.toHaveBeenCalled(); - expect(text).toContain(provider); - }, - ); - - it("未登录时不应启动 mnote-cli host", async () => { - mockSafeGetJsonBody.mockResolvedValue({ - stream: true, - messages: [{ role: "user", content: "总结当前页面" }], - }); - mockValidateRequestBody.mockReturnValue(null); - mockIsConvexEnabled.mockReturnValue(true); - mockGetAuthedConvexClient.mockResolvedValue({ - auth: { - userId: "", - }, - }); - mockRequireAuthContext.mockRejectedValue(new Error("未登录,请先登录")); - - const { POST } = await import("./route"); - const response = await POST( - new Request("http://127.0.0.1:3000/api/ai-agent/run", { - method: "POST", - }), - ); - - expect(response.status).toBe(401); - expect(mockStartMnoteCliAgentHostRun).not.toHaveBeenCalled(); - }); -}); diff --git a/wolai-frontend/src/app/api/ai-agent/run/route.ts b/wolai-frontend/src/app/api/ai-agent/run/route.ts deleted file mode 100644 index 38093e81..00000000 --- a/wolai-frontend/src/app/api/ai-agent/run/route.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { errorResponses, safeGetJsonBody, validateRequestBody } from "@/lib/api-utils"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { getAuthedConvexClient } from "@/lib/convex/route"; -import { - startMnoteCliAgentHostRun, - type MnoteCliAgentRunPayload, -} from "@/lib/server/mnote-cli-agent-host"; -import { NextResponse } from "next/server"; - -export const dynamic = "force-dynamic"; - -export async function POST(request: Request) { - const payload = await safeGetJsonBody(request); - if (!payload) { - return errorResponses.badRequest("请求体不能为空"); - } - const validationError = validateRequestBody(payload, ["messages"] as const); - if (validationError) return validationError; - if (!Array.isArray(payload.messages) || payload.messages.length === 0) { - return errorResponses.badRequest("缺少 messages"); - } - - let userId = ""; - let userEmail: string | undefined; - let userName: string | undefined; - if (isConvexEnabled()) { - const { auth } = await getAuthedConvexClient(); - userId = auth.userId ?? ""; - userEmail = auth.email; - userName = auth.name; - } - if (!userId) { - return errorResponses.unauthorized(); - } - - const provider = String(payload.options?.ai?.provider ?? "").trim().toLowerCase(); - if (provider === "codex" || provider === "hermes" || provider === "claudecode") { - return NextResponse.json( - { - error: `${provider} 已退出默认系统组件,当前只保留 mnote-cli host 主执行入口。`, - }, - { - status: 410, - headers: { - "x-mnote-ai-execution-owner": `${provider}-retired`, - }, - }, - ); - } - - return startMnoteCliAgentHostRun({ - request, - userId, - userEmail, - userName, - payload, - }); -} diff --git a/wolai-frontend/src/app/api/auth/callback/route.ts b/wolai-frontend/src/app/api/auth/callback/route.ts deleted file mode 100644 index b79ddf50..00000000 --- a/wolai-frontend/src/app/api/auth/callback/route.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { NextResponse } from "next/server"; -import type { NextRequest } from "next/server"; - -export async function POST(request: NextRequest) { - // 说明:历史兼容接口(Supabase Auth callback)。当前项目迁移到 Convex 后,这里保持空实现。 - await request.json().catch(() => null); - return NextResponse.json({ success: true }); -} diff --git a/wolai-frontend/src/app/api/auth/mnote-web-token/route.test.ts b/wolai-frontend/src/app/api/auth/mnote-web-token/route.test.ts deleted file mode 100644 index 9f48b21a..00000000 --- a/wolai-frontend/src/app/api/auth/mnote-web-token/route.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const mockConvexAuthNextjsToken = vi.fn(); - -vi.mock("@convex-dev/auth/nextjs/server", () => ({ - convexAuthNextjsToken: mockConvexAuthNextjsToken, -})); - -describe("/api/auth/mnote-web-token compat route", () => { - beforeEach(() => { - mockConvexAuthNextjsToken.mockReset(); - }); - - it("有 token 时仅作为 next-auth-token-debug compat 边界写入 mnote-web cookie", async () => { - mockConvexAuthNextjsToken.mockResolvedValue("token-demo"); - - const { GET } = await import("./route"); - const response = await GET(); - const payload = await response.json(); - - expect(response.status).toBe(200); - expect(response.headers.get("x-mnote-web-owner")).toBe("mnote-web"); - expect(response.headers.get("x-mnote-compat-boundary")).toBe("next-auth-token-debug"); - expect(payload).toEqual({ - ok: true, - owner: "mnote-web", - compatibility: "next-auth-token-debug", - }); - expect(response.headers.get("set-cookie")).toContain("mnote_web_convex_token=token-demo"); - }); - - it("无 token 时清空兼容 cookie 并保留 mnote-web owner header", async () => { - mockConvexAuthNextjsToken.mockResolvedValue(""); - - const { GET } = await import("./route"); - const response = await GET(); - const payload = await response.json(); - - expect(response.status).toBe(401); - expect(response.headers.get("x-mnote-web-owner")).toBe("mnote-web"); - expect(response.headers.get("x-mnote-compat-boundary")).toBe("next-auth-token-debug"); - expect(payload).toEqual({ error: "未登录" }); - expect(response.headers.get("set-cookie")).toContain("mnote_web_convex_token="); - expect(response.headers.get("set-cookie")).toContain("Max-Age=0"); - }); -}); diff --git a/wolai-frontend/src/app/api/auth/mnote-web-token/route.ts b/wolai-frontend/src/app/api/auth/mnote-web-token/route.ts deleted file mode 100644 index 524bfd16..00000000 --- a/wolai-frontend/src/app/api/auth/mnote-web-token/route.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { NextResponse } from "next/server"; -import { convexAuthNextjsToken } from "@convex-dev/auth/nextjs/server"; - -const COOKIE_NAME = "mnote_web_convex_token"; -const COMPAT_HEADERS = { - "cache-control": "no-store", - "x-mnote-web-owner": "mnote-web", - "x-mnote-compat-boundary": "next-auth-token-debug", -}; - -export const dynamic = "force-dynamic"; - -export async function GET() { - const token = (await convexAuthNextjsToken())?.trim() ?? ""; - - if (!token) { - const response = NextResponse.json({ error: "未登录" }, { status: 401 }); - response.cookies.set({ - name: COOKIE_NAME, - value: "", - httpOnly: true, - sameSite: "lax", - path: "/", - maxAge: 0, - }); - for (const [name, value] of Object.entries(COMPAT_HEADERS)) { - response.headers.set(name, value); - } - return response; - } - - const response = NextResponse.json({ - ok: true, - owner: "mnote-web", - compatibility: "next-auth-token-debug", - }); - response.cookies.set({ - name: COOKIE_NAME, - value: token, - httpOnly: true, - sameSite: "lax", - path: "/", - }); - for (const [name, value] of Object.entries(COMPAT_HEADERS)) { - response.headers.set(name, value); - } - return response; -} diff --git a/wolai-frontend/src/app/api/auth/signin/route.ts b/wolai-frontend/src/app/api/auth/signin/route.ts deleted file mode 100644 index 1e07101e..00000000 --- a/wolai-frontend/src/app/api/auth/signin/route.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { isDevAuthEnabled } from "@/lib/auth/devUser"; - -interface SignInRequest { - email: string; - password: string; - name?: string; - flow: "signIn" | "signUp"; -} - -/** - * POST /api/auth/signin - * - * 处理登录/注册请求 - * - 在 Convex 模式下调用 Convex mutation - * - 在 Supabase 模式下调用 Supabase Auth - */ -export async function POST(request: Request) { - // Convex 模式 - if (isConvexEnabled()) { - try { - await request.json().catch(() => null); - - // 说明:当前项目默认使用开发用户鉴权(USE_CONVEX=1),无需走登录/注册。 - if (isDevAuthEnabled()) { - return NextResponse.json({ ok: true, mode: "dev_user" }); - } - - return NextResponse.json({ error: "Convex Auth 登录/注册尚未接入" }, { status: 501 }); - } catch (error) { - console.error("Convex Auth error:", error); - return NextResponse.json( - { error: "服务器错误" }, - { status: 500 } - ); - } - } - - return NextResponse.json({ error: "仅支持 Convex 模式" }, { status: 501 }); -} diff --git a/wolai-frontend/src/app/api/auth/whoami/route.ts b/wolai-frontend/src/app/api/auth/whoami/route.ts deleted file mode 100644 index c6c319a8..00000000 --- a/wolai-frontend/src/app/api/auth/whoami/route.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { NextResponse } from "next/server"; -import { HttpError } from "@/lib/auth/authContext"; -import { getAuthedConvexClient } from "@/lib/convex/route"; - -export const dynamic = "force-dynamic"; - -export async function GET() { - try { - const { auth } = await getAuthedConvexClient(); - return NextResponse.json({ - userId: auth.userId, - email: auth.email ?? null, - name: auth.name ?? null, - }); - } catch (err) { - if (err instanceof HttpError) { - return NextResponse.json({ error: "未登录" }, { status: err.status }); - } - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } -} - diff --git a/wolai-frontend/src/app/api/backend/health/route.ts b/wolai-frontend/src/app/api/backend/health/route.ts deleted file mode 100644 index e8f0d244..00000000 --- a/wolai-frontend/src/app/api/backend/health/route.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { NextResponse } from "next/server"; -import { getMnoteRuntimeConfig } from "@/lib/runtime-config"; - -export const dynamic = "force-dynamic"; - -export async function GET() { - // 说明:浏览器端需要走公网(FRP),但 Next 服务端在本机时应优先走内网/本机(HTTP), - // 否则会遇到 FRP Auto HTTPS 的自签证书导致 Node fetch 失败。 - const cfg = getMnoteRuntimeConfig(); - const backendUrl = - process.env.BACKEND_INTERNAL_URL || process.env.BACKEND_URL || cfg.backendUrl; - if (!backendUrl) { - return NextResponse.json({ status: "disabled" }, { status: 200 }); - } - - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), 2500); - try { - const response = await fetch(`${backendUrl}/health`, { - method: "GET", - signal: controller.signal, - }); - return NextResponse.json({ status: response.ok ? "ok" : "error" }, { status: 200 }); - } catch { - return NextResponse.json({ status: "error" }, { status: 200 }); - } finally { - clearTimeout(timer); - } -} diff --git a/wolai-frontend/src/app/api/blocks/embed/route.ts b/wolai-frontend/src/app/api/blocks/embed/route.ts deleted file mode 100644 index e4bbd899..00000000 --- a/wolai-frontend/src/app/api/blocks/embed/route.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { - documentBridgeErrorResponse, - executeBlockEmbedCommand, -} from "@/lib/blocks/block-command-adapter"; - -type EmbedBlockPayload = { - sourceDocumentId: string; - blockId: string; - targetDocumentId: string; - position?: "end"; -}; - -export async function POST(request: Request) { - const { sourceDocumentId, blockId, targetDocumentId }: EmbedBlockPayload = await request.json(); - - if (!sourceDocumentId || !blockId || !targetDocumentId) { - return NextResponse.json({ error: "缺少参数" }, { status: 400 }); - } - - if (sourceDocumentId === targetDocumentId) { - return NextResponse.json({ error: "禁止嵌入到当前页面" }, { status: 400 }); - } - - if (isConvexEnabled()) { - try { - const result = await executeBlockEmbedCommand({ - request, - sourceDocumentId, - blockId, - targetDocumentId, - }); - return NextResponse.json({ - ok: true, - ...(result.result && typeof result.result === "object" ? result.result : {}), - meta: { - requestId: result.requestId, - traceId: result.traceId, - commandId: result.commandId, - commandName: result.commandName, - }, - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - - /* - const supabase = await createSupabaseRouteClient(); - const { - data: { session }, - } = await supabase.auth.getSession(); - - if (!session) { - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - - const { data: sourceDoc } = await supabase - .from("documents") - .select("id,content,user_id") - .eq("id", sourceDocumentId) - .eq("user_id", session.user.id) - .single(); - if (!sourceDoc) return NextResponse.json({ error: "源页面不存在或无权限" }, { status: 404 }); - - const sourceBlocks = getBlocksFromDocumentContent(sourceDoc.content); - if (!findBlockInTree(sourceBlocks, blockId)) { - return NextResponse.json({ error: "源块不存在或无权限" }, { status: 404 }); - } - - const { data: targetDoc } = await supabase - .from("documents") - .select("id,content,user_id") - .eq("id", targetDocumentId) - .eq("user_id", session.user.id) - .single(); - if (!targetDoc) return NextResponse.json({ error: "目标页面不存在或无权限" }, { status: 404 }); - - const targetBlocks = getBlocksFromDocumentContent(targetDoc.content); - const referenceBlock = { - id: randomUUID(), - type: "blockReference", - props: { - sourceDocumentId, - targetBlockId: blockId, - display: "embed", - }, - content: [], - children: [], - }; - - const nextBlocks = [...targetBlocks, referenceBlock]; - const payload = withBlocksWrittenBack(targetDoc.content, nextBlocks); - - const { error } = await supabase - .from("documents") - .update({ content: payload }) - .eq("id", targetDocumentId) - .eq("user_id", session.user.id); - - if (error) return NextResponse.json({ error: error.message }, { status: 500 }); - return NextResponse.json({ ok: true }); - */ -} - -export const runtime = "nodejs"; diff --git a/wolai-frontend/src/app/api/blocks/get/route.ts b/wolai-frontend/src/app/api/blocks/get/route.ts deleted file mode 100644 index 4e296601..00000000 --- a/wolai-frontend/src/app/api/blocks/get/route.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { - documentBridgeErrorResponse, - executeBlockGetQuery, -} from "@/lib/blocks/block-command-adapter"; - -export async function GET(request: Request) { - const url = new URL(request.url); - const sourceDocumentId = url.searchParams.get("sourceDocumentId") || ""; - const blockId = url.searchParams.get("blockId") || ""; - - if (!sourceDocumentId || !blockId) { - return NextResponse.json({ error: "缺少参数" }, { status: 400 }); - } - - if (isConvexEnabled()) { - try { - const result = await executeBlockGetQuery({ - request, - sourceDocumentId, - blockId, - }); - return NextResponse.json({ ok: true, block: result.block, meta: result.meta }); - } catch (error) { - return documentBridgeErrorResponse(error); - } - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - - /* - const supabase = await createSupabaseRouteClient(); - const { - data: { session }, - } = await supabase.auth.getSession(); - if (!session) return NextResponse.json({ error: "未登录" }, { status: 401 }); - - const { data: doc } = await supabase - .from("documents") - .select("id,content,user_id") - .eq("id", sourceDocumentId) - .eq("user_id", session.user.id) - .single(); - if (!doc) return NextResponse.json({ error: "页面不存在或无权限" }, { status: 404 }); - - const blocks = getBlocksFromDocumentContent(doc.content); - const hit = findBlockInTree(blocks, blockId); - if (!hit) return NextResponse.json({ error: "块不存在或无权限" }, { status: 404 }); - - return NextResponse.json({ ok: true, block: hit.block }); - */ -} - -export const runtime = "nodejs"; diff --git a/wolai-frontend/src/app/api/blocks/move/route.ts b/wolai-frontend/src/app/api/blocks/move/route.ts deleted file mode 100644 index 1397b80e..00000000 --- a/wolai-frontend/src/app/api/blocks/move/route.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { - documentBridgeErrorResponse, - executeBlockMoveCommand, -} from "@/lib/blocks/block-command-adapter"; - -type MoveBlockPayload = { - sourceDocumentId: string; - blockId: string; - targetDocumentId: string; - position?: "end"; -}; - -export async function POST(request: Request) { - const { sourceDocumentId, blockId, targetDocumentId }: MoveBlockPayload = await request.json(); - - if (!sourceDocumentId || !blockId || !targetDocumentId) { - return NextResponse.json({ error: "缺少参数" }, { status: 400 }); - } - - if (sourceDocumentId === targetDocumentId) { - // 说明:同页移动先不做“定位插入”,视为 no-op。 - return NextResponse.json({ ok: true, noop: true }); - } - - if (isConvexEnabled()) { - try { - const result = await executeBlockMoveCommand({ - request, - sourceDocumentId, - blockId, - targetDocumentId, - }); - const payload = - result.result && typeof result.result === "object" - ? (result.result as Record) - : {}; - return NextResponse.json({ - ...payload, - ok: typeof payload.ok === "boolean" ? payload.ok : true, - meta: { - requestId: result.requestId, - traceId: result.traceId, - commandId: result.commandId, - commandName: result.commandName, - }, - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - - /* - const supabase = await createSupabaseRouteClient(); - const { - data: { session }, - } = await supabase.auth.getSession(); - - if (!session) { - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - - const { data: sourceDoc } = await supabase - .from("documents") - .select("id,content,user_id") - .eq("id", sourceDocumentId) - .eq("user_id", session.user.id) - .single(); - if (!sourceDoc) return NextResponse.json({ error: "源页面不存在或无权限" }, { status: 404 }); - - const sourceBlocks = getBlocksFromDocumentContent(sourceDoc.content); - const removedRes = removeBlockSubtree(sourceBlocks, blockId); - if (!removedRes.removed) return NextResponse.json({ error: "源块不存在或无权限" }, { status: 404 }); - - const { data: targetDoc } = await supabase - .from("documents") - .select("id,content,user_id") - .eq("id", targetDocumentId) - .eq("user_id", session.user.id) - .single(); - if (!targetDoc) return NextResponse.json({ error: "目标页面不存在或无权限" }, { status: 404 }); - - const targetBlocks = getBlocksFromDocumentContent(targetDoc.content); - const nextSourcePayload = withBlocksWrittenBack(sourceDoc.content, removedRes.nextBlocks); - const nextTargetPayload = withBlocksWrittenBack(targetDoc.content, [...targetBlocks, removedRes.removed]); - - const { error: srcErr } = await supabase - .from("documents") - .update({ content: nextSourcePayload }) - .eq("id", sourceDocumentId) - .eq("user_id", session.user.id); - if (srcErr) return NextResponse.json({ error: srcErr.message }, { status: 500 }); - - const { error: tgtErr } = await supabase - .from("documents") - .update({ content: nextTargetPayload }) - .eq("id", targetDocumentId) - .eq("user_id", session.user.id); - if (tgtErr) return NextResponse.json({ error: tgtErr.message }, { status: 500 }); - - return NextResponse.json({ ok: true }); - */ -} - -export const runtime = "nodejs"; diff --git a/wolai-frontend/src/app/api/blocks/patch/route.ts b/wolai-frontend/src/app/api/blocks/patch/route.ts deleted file mode 100644 index 20d32185..00000000 --- a/wolai-frontend/src/app/api/blocks/patch/route.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { - documentBridgeErrorResponse, - executeBlockPatchCommand, -} from "@/lib/blocks/block-command-adapter"; - -type PatchPayload = { - sourceDocumentId: string; - workspaceId?: string | null; - blockId: string; - nextBlock: unknown; -}; - -export async function POST(request: Request) { - if (isConvexEnabled()) { - try { - const { sourceDocumentId, workspaceId, blockId, nextBlock }: PatchPayload = await request.json(); - const result = await executeBlockPatchCommand({ - request, - sourceDocumentId, - workspaceId, - blockId, - nextBlock, - }); - return NextResponse.json({ - ok: true, - meta: { - requestId: result.requestId, - traceId: result.traceId, - commandId: result.commandId, - commandName: result.commandName, - }, - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - - /* - const supabase = await createSupabaseRouteClient(); - const { - data: { session }, - } = await supabase.auth.getSession(); - if (!session) return NextResponse.json({ error: "未登录" }, { status: 401 }); - - const { data: doc } = await supabase - .from("documents") - .select("id,content,user_id") - .eq("id", sourceDocumentId) - .eq("user_id", session.user.id) - .single(); - if (!doc) return NextResponse.json({ error: "页面不存在或无权限" }, { status: 404 }); - - const blocks = getBlocksFromDocumentContent(doc.content); - const replaced = replaceBlockInTree(blocks, blockId, nextBlock as any); - if (!replaced.ok) return NextResponse.json({ error: "块不存在或无权限" }, { status: 404 }); - - const payload = withBlocksWrittenBack(doc.content, replaced.nextBlocks); - const { error } = await supabase - .from("documents") - .update({ content: payload }) - .eq("id", sourceDocumentId) - .eq("user_id", session.user.id); - if (error) return NextResponse.json({ error: error.message }, { status: 500 }); - - return NextResponse.json({ ok: true }); - */ -} - -export const runtime = "nodejs"; diff --git a/wolai-frontend/src/app/api/bridge/request/route.ts b/wolai-frontend/src/app/api/bridge/request/route.ts deleted file mode 100644 index 646cda33..00000000 --- a/wolai-frontend/src/app/api/bridge/request/route.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { NextResponse } from "next/server"; -import { getAuthedConvexClient } from "@/lib/convex/route"; -import { - buildDocumentBridgeContext, - buildDocumentQueryEnvelope, - documentBridgeErrorResponse, -} from "@/lib/documents/bridge"; -import { - executeRustBridgeQueryTransport, - resolveRustBridgeQueryPlan, -} from "@/lib/documents/rust-runtime"; - -function sortByNewest>(rows: T[]) { - return [...rows].sort((left, right) => { - const leftTime = - (typeof left.created_at === "string" && left.created_at) || - (typeof left.finished_at === "string" && left.finished_at) || - ""; - const rightTime = - (typeof right.created_at === "string" && right.created_at) || - (typeof right.finished_at === "string" && right.finished_at) || - ""; - return rightTime.localeCompare(leftTime); - }); -} - -function filterByCommandId>(rows: T[], commandId: string | null) { - if (!commandId) return rows; - return rows.filter((row) => row.command_id === commandId); -} - -export async function GET(request: Request) { - try { - const url = new URL(request.url); - const workspaceId = url.searchParams.get("workspaceId")?.trim() ?? ""; - const requestId = url.searchParams.get("requestId")?.trim() ?? ""; - const commandId = url.searchParams.get("commandId")?.trim() ?? ""; - if (!workspaceId || !requestId) { - return NextResponse.json({ error: "缺少 workspaceId 或 requestId" }, { status: 400 }); - } - - const { client } = await getAuthedConvexClient(); - const context = await buildDocumentBridgeContext({ - request, - workspaceId, - }); - const envelope = buildDocumentQueryEnvelope({ - name: "bridge.request.get", - payload: { - workspaceId, - requestId, - commandId: commandId || null, - }, - }); - const plan = await resolveRustBridgeQueryPlan({ - context, - envelope, - }); - const result = await executeRustBridgeQueryTransport<{ - request_id: string; - command_logs: Array>; - domain_events: Array>; - generated_at: string; - }>({ - client, - plan, - }); - const commandLogs = sortByNewest(filterByCommandId(result.command_logs ?? [], commandId || null)); - const domainEvents = sortByNewest(filterByCommandId(result.domain_events ?? [], commandId || null)); - - return NextResponse.json({ - request_id: result.request_id, - command_logs: commandLogs, - domain_events: domainEvents, - generated_at: result.generated_at, - counts: { - command_logs: commandLogs.length, - domain_events: domainEvents.length, - }, - meta: { - requestId: context.requestId, - traceId: context.traceId, - queryName: envelope.name, - commandId: commandId || null, - }, - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } -} diff --git a/wolai-frontend/src/app/api/bridge/trace/route.ts b/wolai-frontend/src/app/api/bridge/trace/route.ts deleted file mode 100644 index 411a8771..00000000 --- a/wolai-frontend/src/app/api/bridge/trace/route.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { NextResponse } from "next/server"; -import { getAuthedConvexClient } from "@/lib/convex/route"; -import { - buildDocumentBridgeContext, - buildDocumentQueryEnvelope, - documentBridgeErrorResponse, -} from "@/lib/documents/bridge"; -import { - executeRustBridgeQueryTransport, - resolveRustBridgeQueryPlan, -} from "@/lib/documents/rust-runtime"; - -function sortByNewest>(rows: T[]) { - return [...rows].sort((left, right) => { - const leftTime = - (typeof left.created_at === "string" && left.created_at) || - (typeof left.finished_at === "string" && left.finished_at) || - ""; - const rightTime = - (typeof right.created_at === "string" && right.created_at) || - (typeof right.finished_at === "string" && right.finished_at) || - ""; - return rightTime.localeCompare(leftTime); - }); -} - -function filterByCommandId>(rows: T[], commandId: string | null) { - if (!commandId) return rows; - return rows.filter((row) => row.command_id === commandId); -} - -export async function GET(request: Request) { - try { - const url = new URL(request.url); - const workspaceId = url.searchParams.get("workspaceId")?.trim() ?? ""; - const traceId = url.searchParams.get("traceId")?.trim() ?? ""; - const commandId = url.searchParams.get("commandId")?.trim() ?? ""; - if (!workspaceId || !traceId) { - return NextResponse.json({ error: "缺少 workspaceId 或 traceId" }, { status: 400 }); - } - - const { client } = await getAuthedConvexClient(); - const context = await buildDocumentBridgeContext({ - request, - workspaceId, - }); - const envelope = buildDocumentQueryEnvelope({ - name: "bridge.trace.get", - payload: { - workspaceId, - traceId, - commandId: commandId || null, - }, - }); - const plan = await resolveRustBridgeQueryPlan({ - context, - envelope, - }); - const result = await executeRustBridgeQueryTransport<{ - trace_id: string; - command_logs: Array>; - domain_events: Array>; - generated_at: string; - }>({ - client, - plan, - }); - const commandLogs = sortByNewest(filterByCommandId(result.command_logs ?? [], commandId || null)); - const domainEvents = sortByNewest(filterByCommandId(result.domain_events ?? [], commandId || null)); - - return NextResponse.json({ - trace_id: result.trace_id, - command_logs: commandLogs, - domain_events: domainEvents, - generated_at: result.generated_at, - counts: { - command_logs: commandLogs.length, - domain_events: domainEvents.length, - }, - meta: { - requestId: context.requestId, - traceId: context.traceId, - queryName: envelope.name, - commandId: commandId || null, - }, - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } -} diff --git a/wolai-frontend/src/app/api/bridge/workspace/route.ts b/wolai-frontend/src/app/api/bridge/workspace/route.ts deleted file mode 100644 index d21ec72b..00000000 --- a/wolai-frontend/src/app/api/bridge/workspace/route.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { NextResponse } from "next/server"; -import { getAuthedConvexClient } from "@/lib/convex/route"; -import { - buildDocumentBridgeContext, - buildDocumentQueryEnvelope, - documentBridgeErrorResponse, -} from "@/lib/documents/bridge"; -import { - executeRustBridgeQueryTransport, - resolveRustBridgeQueryPlan, -} from "@/lib/documents/rust-runtime"; - -export async function GET(request: Request) { - try { - const url = new URL(request.url); - const workspaceId = url.searchParams.get("workspaceId")?.trim() ?? ""; - if (!workspaceId) { - return NextResponse.json({ error: "缺少 workspaceId" }, { status: 400 }); - } - - const limit = Number(url.searchParams.get("limit") ?? "50"); - const cursor = url.searchParams.get("cursor")?.trim() ?? ""; - const commandStatus = url.searchParams.get("commandStatus")?.trim() ?? ""; - const eventStatus = url.searchParams.get("eventStatus")?.trim() ?? ""; - const targetPageId = url.searchParams.get("targetPageId")?.trim() ?? ""; - const targetBlockId = url.searchParams.get("targetBlockId")?.trim() ?? ""; - const aggregateType = url.searchParams.get("aggregateType")?.trim() ?? ""; - const aggregateId = url.searchParams.get("aggregateId")?.trim() ?? ""; - - const { client } = await getAuthedConvexClient(); - const context = await buildDocumentBridgeContext({ - request, - workspaceId, - }); - const envelope = buildDocumentQueryEnvelope({ - name: "bridge.workspace.overview", - payload: { - workspaceId, - limit: Number.isFinite(limit) ? limit : 50, - cursor: cursor || null, - commandStatus: commandStatus || null, - eventStatus: eventStatus || null, - targetPageId: targetPageId || null, - targetBlockId: targetBlockId || null, - aggregateType: aggregateType || null, - aggregateId: aggregateId || null, - }, - }); - const plan = await resolveRustBridgeQueryPlan({ - context, - envelope, - }); - const result = await executeRustBridgeQueryTransport<{ - workspace_id: string; - command_logs: Array>; - domain_events: Array>; - counts: { - command_logs: number; - domain_events: number; - }; - filters: Record | null; - next_cursor: string | null; - has_more: boolean; - generated_at: string; - }>({ - client, - plan, - }); - - return NextResponse.json({ - workspace_id: result.workspace_id, - command_logs: result.command_logs ?? [], - domain_events: result.domain_events ?? [], - counts: result.counts ?? { - command_logs: Array.isArray(result.command_logs) ? result.command_logs.length : 0, - domain_events: Array.isArray(result.domain_events) ? result.domain_events.length : 0, - }, - filters: result.filters ?? null, - next_cursor: result.next_cursor ?? null, - has_more: Boolean(result.has_more), - generated_at: result.generated_at, - meta: { - requestId: context.requestId, - traceId: context.traceId, - queryName: envelope.name, - }, - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } -} diff --git a/wolai-frontend/src/app/api/dev/ingest/enqueue/route.ts b/wolai-frontend/src/app/api/dev/ingest/enqueue/route.ts deleted file mode 100644 index 61842db8..00000000 --- a/wolai-frontend/src/app/api/dev/ingest/enqueue/route.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { NextResponse } from "next/server"; -import { requireAuthContext } from "@/lib/auth/authContext"; -import { getConvexAuthedHttpClient } from "@/lib/convex/server"; -import { api } from "@/lib/convex/api"; - -export const dynamic = "force-dynamic"; - -type Payload = - | { type: "document"; documentId: string } - | { type: "mindmap"; docId: string; mindmapId: string } - | { type: "media"; assetId: string }; - -export async function POST(request: Request) { - const auth = await requireAuthContext(); - const client = await getConvexAuthedHttpClient(); - - const body = (await request.json().catch(() => null)) as Payload | null; - if (!body) { - return NextResponse.json({ error: "缺少请求体" }, { status: 400 }); - } - - if (body.type === "document") { - const documentId = String(body.documentId ?? "").trim(); - if (!documentId) return NextResponse.json({ error: "缺少 documentId" }, { status: 400 }); - const result = await client.mutation(api.jobs.enqueueRagIndexDocument, { userId: auth.userId, documentId }); - return NextResponse.json(result); - } - - if (body.type === "mindmap") { - const docId = String(body.docId ?? "").trim(); - const mindmapId = String(body.mindmapId ?? "").trim(); - if (!docId) return NextResponse.json({ error: "缺少 docId" }, { status: 400 }); - if (!mindmapId) return NextResponse.json({ error: "缺少 mindmapId" }, { status: 400 }); - const result = await client.mutation(api.jobs.enqueueRagIndexMindmap, { userId: auth.userId, docId, mindmapId }); - return NextResponse.json(result); - } - - if (body.type === "media") { - const assetId = String(body.assetId ?? "").trim(); - if (!assetId) return NextResponse.json({ error: "缺少 assetId" }, { status: 400 }); - const result = await client.mutation(api.jobs.enqueueRagIndexMediaAsset, { userId: auth.userId, assetId }); - return NextResponse.json(result); - } - - return NextResponse.json({ error: "不支持的 type" }, { status: 400 }); -} diff --git a/wolai-frontend/src/app/api/dev/jobs/demo/route.ts b/wolai-frontend/src/app/api/dev/jobs/demo/route.ts deleted file mode 100644 index 2b3ea0eb..00000000 --- a/wolai-frontend/src/app/api/dev/jobs/demo/route.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { NextResponse } from "next/server"; -import { randomUUID } from "crypto"; -import { requireAuthContext } from "@/lib/auth/authContext"; -import { getConvexAuthedHttpClient } from "@/lib/convex/server"; -import { api } from "@/lib/convex/api"; - -export const dynamic = "force-dynamic"; - -export async function POST(request: Request) { - const auth = await requireAuthContext(); - const client = await getConvexAuthedHttpClient(); - - const { ms }: { ms?: number } = await request.json().catch(() => ({})); - const id = randomUUID(); - - const result = await client.mutation(api.jobs.enqueueDemo, { - userId: auth.userId, - id, - ms, - }); - - return NextResponse.json(result); -} - -export async function GET(request: Request) { - const auth = await requireAuthContext(); - const client = await getConvexAuthedHttpClient(); - - const url = new URL(request.url); - const id = url.searchParams.get("id") ?? ""; - if (!id) { - return NextResponse.json({ error: "缺少 id" }, { status: 400 }); - } - - const job = await client.query(api.jobs.get, { userId: auth.userId, id }); - if (!job) { - return NextResponse.json({ error: "任务不存在" }, { status: 404 }); - } - - return NextResponse.json(job); -} diff --git a/wolai-frontend/src/app/api/dev/whoami/route.ts b/wolai-frontend/src/app/api/dev/whoami/route.ts deleted file mode 100644 index 76bee864..00000000 --- a/wolai-frontend/src/app/api/dev/whoami/route.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NextResponse } from "next/server"; - -import { HttpError, requireAuthContext } from "@/lib/auth/authContext"; - -export const dynamic = "force-dynamic"; - -export async function GET() { - try { - const auth = await requireAuthContext(); - return NextResponse.json({ ok: true, auth }, { status: 200 }); - } catch (err) { - const status = err instanceof HttpError ? err.status : 500; - const message = err instanceof Error ? err.message : "Unknown error"; - return NextResponse.json({ ok: false, error: message }, { status }); - } -} diff --git a/wolai-frontend/src/app/api/documents/content/route.ts b/wolai-frontend/src/app/api/documents/content/route.ts deleted file mode 100644 index 58b02c80..00000000 --- a/wolai-frontend/src/app/api/documents/content/route.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { getAuthedConvexClient } from "@/lib/convex/route"; -import { - assertDocumentId, - buildDocumentBridgeContext, - buildDocumentQueryEnvelope, - documentBridgeErrorResponse, -} from "@/lib/documents/bridge"; -import type { PageSubtreeProjection } from "@/lib/documents/page-subtree"; -import { normalizeDocumentContentResponse } from "@/lib/documents/page-subtree-response"; -import { - executeRustBridgeQueryTransport, - resolveRustBridgeQueryPlan, -} from "@/lib/documents/rust-runtime"; - -export const dynamic = "force-dynamic"; -export const runtime = "nodejs"; - -export async function GET(request: Request) { - if (isConvexEnabled()) { - try { - const url = new URL(request.url); - const documentId = assertDocumentId(url.searchParams.get("documentId")); - const workspaceId = url.searchParams.get("workspaceId")?.trim() || null; - const { client } = await getAuthedConvexClient(); - const bridgeContext = await buildDocumentBridgeContext({ request, workspaceId }); - const envelope = buildDocumentQueryEnvelope({ - name: "documents.content.get", - payload: { documentId, workspaceId }, - }); - const plan = await resolveRustBridgeQueryPlan({ - context: bridgeContext, - envelope, - }); - const result = await executeRustBridgeQueryTransport<{ - content?: unknown; - revision?: number | null; - conflict_detection_key?: string | null; - title?: string | null; - page_subtree?: PageSubtreeProjection | null; - } | null>({ - client, - plan, - }); - - if (!result) { - return NextResponse.json( - { - error: "页面不存在", - meta: { - requestId: bridgeContext.requestId, - traceId: bridgeContext.traceId, - queryName: envelope.name, - }, - }, - { status: 404 }, - ); - } - - const normalized = normalizeDocumentContentResponse({ - documentId, - title: typeof result.title === "string" && result.title.trim() ? result.title : null, - payload: result, - }); - - return NextResponse.json({ - content: normalized.content, - revision: normalized.revision, - conflictDetectionKey: normalized.conflictDetectionKey, - pageSubtree: normalized.pageSubtree, - meta: { - requestId: bridgeContext.requestId, - traceId: bridgeContext.traceId, - queryName: envelope.name, - }, - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); -} diff --git a/wolai-frontend/src/app/api/documents/duplicate/route.ts b/wolai-frontend/src/app/api/documents/duplicate/route.ts deleted file mode 100644 index 1d8aeeff..00000000 --- a/wolai-frontend/src/app/api/documents/duplicate/route.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { getAuthedConvexClient } from "@/lib/convex/route"; -import { api } from "@/lib/convex/api"; -import { - assertDocumentId, - buildDocumentBridgeContext, - buildDocumentCommandEnvelope, - documentBridgeErrorResponse, -} from "@/lib/documents/bridge"; -import { - executePageLifecycleBridgeCommand, - type DocumentDuplicatePayload, -} from "@/lib/documents/page-command-adapter"; -import { - copyMindmapFilesIfExists, - ensureDocumentScaffold, -} from "@/lib/documents/page-lifecycle-side-effects"; - -interface DuplicatePayload { - documentId: string; -} - -export async function POST(request: Request) { - if (isConvexEnabled()) { - try { - const { client } = await getAuthedConvexClient(); - const { documentId }: DuplicatePayload = await request.json(); - const normalizedDocumentId = assertDocumentId(documentId); - - const sourceDoc = await client.query(api.documents.getMeta, { id: normalizedDocumentId }); - if (!sourceDoc) { - return NextResponse.json({ error: "页面不存在或无权访问" }, { status: 404 }); - } - - const fallbackTitle = - sourceDoc.title?.trim() && sourceDoc.title.trim().length > 0 ? sourceDoc.title.trim() : "无标题"; - const duplicatedTitle = `${fallbackTitle} 副本`; - const newId = - typeof crypto.randomUUID === "function" - ? crypto.randomUUID() - : `${Date.now()}_${Math.random().toString(16).slice(2)}`; - const normalizedWorkspaceId = sourceDoc.workspace_id?.trim() || null; - const bridgeContext = await buildDocumentBridgeContext({ - request, - workspaceId: normalizedWorkspaceId, - }); - const result = await executePageLifecycleBridgeCommand< - DocumentDuplicatePayload, - { - id: string; - title?: string | null; - parent_id?: string | null; - sort_order?: number | null; - } - >({ - context: bridgeContext, - envelope: buildDocumentCommandEnvelope({ - name: "documents.duplicate", - payload: { - sourceDocumentId: normalizedDocumentId, - newDocumentId: newId, - workspaceId: normalizedWorkspaceId, - title: duplicatedTitle, - }, - context: bridgeContext, - target: { - workspaceId: normalizedWorkspaceId, - pageId: newId, - }, - }), - }); - - await ensureDocumentScaffold(result.result.id, result.result.title ?? duplicatedTitle); - await copyMindmapFilesIfExists(normalizedDocumentId, result.result.id); - - return NextResponse.json({ - id: result.result.id, - title: result.result.title ?? duplicatedTitle, - parent_id: result.result.parent_id ?? null, - sort_order: result.result.sort_order ?? null, - meta: { - requestId: result.requestId, - traceId: result.traceId, - commandId: result.commandId, - commandName: result.commandName, - }, - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); -} - -export const runtime = "nodejs"; diff --git a/wolai-frontend/src/app/api/documents/empty-trash/route.ts b/wolai-frontend/src/app/api/documents/empty-trash/route.ts deleted file mode 100644 index 98efb3bc..00000000 --- a/wolai-frontend/src/app/api/documents/empty-trash/route.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { executeDocumentEmptyTrashBridgeCommand } from "@/lib/documents/page-command-adapter"; - -export const dynamic = "force-dynamic"; - -export async function POST(request: Request) { - if (isConvexEnabled()) { - return executeDocumentEmptyTrashBridgeCommand(request); - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); -} diff --git a/wolai-frontend/src/app/api/documents/meta/route.ts b/wolai-frontend/src/app/api/documents/meta/route.ts deleted file mode 100644 index ba843f2f..00000000 --- a/wolai-frontend/src/app/api/documents/meta/route.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { getAuthedConvexClient } from "@/lib/convex/route"; -import { api } from "@/lib/convex/api"; -import { - assertDocumentId, - buildDocumentBridgeContext, - buildDocumentQueryEnvelope, - documentBridgeErrorResponse, -} from "@/lib/documents/bridge"; - -export const dynamic = "force-dynamic"; - -export async function GET(request: Request) { - if (isConvexEnabled()) { - try { - const url = new URL(request.url); - const documentId = assertDocumentId(url.searchParams.get("documentId")); - const workspaceId = url.searchParams.get("workspaceId")?.trim() || null; - const { client } = await getAuthedConvexClient(); - const bridgeContext = await buildDocumentBridgeContext({ request, workspaceId }); - const envelope = buildDocumentQueryEnvelope({ - name: "documents.meta.get", - payload: { documentId, workspaceId }, - }); - - const doc = await client.query(api.documents.getMeta, { id: documentId }); - - if (!doc) { - return NextResponse.json( - { - error: "页面不存在", - meta: { - requestId: bridgeContext.requestId, - traceId: bridgeContext.traceId, - queryName: envelope.name, - }, - }, - { status: 404 }, - ); - } - - return NextResponse.json({ - doc, - meta: { - requestId: bridgeContext.requestId, - traceId: bridgeContext.traceId, - queryName: envelope.name, - }, - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); -} diff --git a/wolai-frontend/src/app/api/documents/options/route.ts b/wolai-frontend/src/app/api/documents/options/route.ts deleted file mode 100644 index 4905080c..00000000 --- a/wolai-frontend/src/app/api/documents/options/route.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import type { PageOptionsState } from "@/types/page-options"; -import { - assertDocumentId, - assertOptionsPatch, - buildDocumentBridgeContext, - buildDocumentCommandEnvelope, - documentBridgeErrorResponse, -} from "@/lib/documents/bridge"; -import { - type DocumentOptionsUpdatePayload, -} from "@/lib/documents/metadata-command-adapter"; -import { executePageWriteBridgeCommand } from "@/lib/documents/page-write-command-adapter"; -import { PAGE_COMMAND_NAMES } from "@/lib/documents/page-command-contract"; - -type OptionsPayload = { - documentId: string; - workspaceId?: string | null; - options: Partial; -}; - -export async function POST(request: Request) { - if (isConvexEnabled()) { - try { - const { documentId, workspaceId, options }: OptionsPayload = await request.json(); - const normalizedDocumentId = assertDocumentId(documentId); - assertOptionsPatch(options); - const normalizedWorkspaceId = workspaceId?.trim() || null; - const bridgeContext = await buildDocumentBridgeContext({ - request, - workspaceId: normalizedWorkspaceId, - }); - const envelope = buildDocumentCommandEnvelope({ - name: PAGE_COMMAND_NAMES.updateLayout, - payload: { - documentId: normalizedDocumentId, - workspaceId: normalizedWorkspaceId, - options, - } satisfies DocumentOptionsUpdatePayload, - context: bridgeContext, - target: { - workspaceId: normalizedWorkspaceId, - pageId: normalizedDocumentId, - }, - }); - const result = await executePageWriteBridgeCommand({ - context: bridgeContext, - envelope, - }); - - return NextResponse.json({ - ok: true, - meta: { - requestId: result.requestId, - traceId: result.traceId, - commandId: result.commandId, - commandName: result.commandName, - }, - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); -} diff --git a/wolai-frontend/src/app/api/documents/page/route.test.ts b/wolai-frontend/src/app/api/documents/page/route.test.ts deleted file mode 100644 index 0a8ec027..00000000 --- a/wolai-frontend/src/app/api/documents/page/route.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { GET } from "@/app/api/documents/page/route"; - -describe("documents/page route", () => { - it("compat 读链应明确返回 410,并指向 page aggregate 正式路由", async () => { - const response = await GET( - new Request("http://localhost/api/documents/page?documentId=doc_1&workspaceId=ws_1"), - ); - const payload = await response.json() as { - error: string; - redirectTo: string; - }; - - expect(response.status).toBe(410); - expect(payload.error).toContain("/api/page-aggregate/:documentId"); - expect(payload.redirectTo).toBe("/api/page-aggregate/doc_1?workspaceId=ws_1"); - }); -}); diff --git a/wolai-frontend/src/app/api/documents/page/route.ts b/wolai-frontend/src/app/api/documents/page/route.ts deleted file mode 100644 index 322ce041..00000000 --- a/wolai-frontend/src/app/api/documents/page/route.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { NextResponse } from "next/server"; - -export const dynamic = "force-dynamic"; -export const runtime = "nodejs"; - -export async function GET(request: Request) { - const url = new URL(request.url); - const documentId = url.searchParams.get("documentId")?.trim() || ""; - const workspaceId = url.searchParams.get("workspaceId")?.trim() || ""; - const redirectTo = new URL( - `/api/page-aggregate/${encodeURIComponent(documentId || ":documentId")}`, - request.url, - ); - if (workspaceId) { - redirectTo.searchParams.set("workspaceId", workspaceId); - } - return NextResponse.json( - { - error: "Next /api/documents/page compat 读链已退场,请直接使用 /api/page-aggregate/:documentId。", - redirectTo: redirectTo.pathname + redirectTo.search, - }, - { status: 410 }, - ); -} diff --git a/wolai-frontend/src/app/api/documents/route-adapters.test.ts b/wolai-frontend/src/app/api/documents/route-adapters.test.ts deleted file mode 100644 index 23d9e7da..00000000 --- a/wolai-frontend/src/app/api/documents/route-adapters.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; - -vi.mock("@/lib/convex/enabled", () => ({ - isConvexEnabled: vi.fn(() => true), -})); - -vi.mock("@/lib/auth/authContext", () => ({ - HttpError: class HttpError extends Error { - status: number; - - constructor(status: number, message: string) { - super(message); - this.status = status; - } - }, - requireAuthContext: vi.fn(async () => ({ - userId: "user_1", - })), -})); - -vi.mock("@/lib/api-utils", () => ({ - apiErrorResponse: vi.fn((message: string, status = 500, details?: unknown) => ({ - message, - status, - details, - })), -})); - -vi.mock("@/lib/documents/page-command-adapter", () => ({ - executeDocumentCreateChildBridgeCommand: vi.fn(async () => new Response(JSON.stringify({ ok: true }))), - executeDocumentTemplateBridgeCommand: vi.fn(async () => new Response(JSON.stringify({ ok: true }))), - executeDocumentEmptyTrashBridgeCommand: vi.fn(async () => new Response(JSON.stringify({ ok: true }))), -})); - -vi.mock("@/lib/documents/page-write-command-adapter", () => ({ - executePageWriteBridgeCommand: vi.fn(async () => ({ - requestId: "req_1", - traceId: "trace_1", - commandId: "cmd_1", - commandName: "page.command", - revision: 1, - conflictDetectionKey: "doc_1:1", - })), -})); - -import { POST as postCreateChild } from "@/app/api/documents/create-child/route"; -import { POST as postTemplate } from "@/app/api/documents/template/route"; -import { POST as postEmptyTrash } from "@/app/api/documents/empty-trash/route"; -import { POST as postTitle } from "@/app/api/documents/title/route"; -import { POST as postOptions } from "@/app/api/documents/options/route"; -import { POST as postSave } from "@/app/api/documents/save/route"; -import { - executeDocumentCreateChildBridgeCommand, - executeDocumentTemplateBridgeCommand, - executeDocumentEmptyTrashBridgeCommand, -} from "@/lib/documents/page-command-adapter"; -import { executePageWriteBridgeCommand } from "@/lib/documents/page-write-command-adapter"; - -afterEach(() => { - vi.restoreAllMocks(); - vi.clearAllMocks(); -}); - -describe("documents route adapters", () => { - it("title route 在缺少 page head commandName 时返回校验错误", async () => { - const response = await postTitle(new Request("http://localhost/api/documents/title", { - method: "POST", - headers: { - "content-type": "application/json", - }, - body: JSON.stringify({ documentId: "doc_1", workspaceId: "ws_1", title: "新标题" }), - })) as { - message: string; - status: number; - details?: { code?: string; details?: Array<{ field: string; reason: string }> }; - }; - - expect(response.status).toBe(400); - expect(response.message).toBe("标题保存仅支持 page.head.updateTitle"); - expect(response.details).toEqual({ - code: "VALIDATION_ERROR", - details: [{ field: "commandName", reason: "expected page.head.updateTitle" }], - }); - expect(executePageWriteBridgeCommand).not.toHaveBeenCalled(); - }); - - it("creates child route delegates to unified adapter", async () => { - await postCreateChild(new Request("http://localhost/api/documents/create-child", { - method: "POST", - body: JSON.stringify({ parentId: null }), - })); - expect(executeDocumentCreateChildBridgeCommand).toHaveBeenCalled(); - }); - - it("template route delegates to unified adapter", async () => { - await postTemplate(new Request("http://localhost/api/documents/template", { - method: "POST", - body: JSON.stringify({ documentId: "doc_1", isTemplate: true }), - })); - expect(executeDocumentTemplateBridgeCommand).toHaveBeenCalled(); - }); - - it("empty trash route delegates to unified adapter", async () => { - await postEmptyTrash(new Request("http://localhost/api/documents/empty-trash", { - method: "POST", - body: JSON.stringify({ workspaceId: "ws_1" }), - })); - expect(executeDocumentEmptyTrashBridgeCommand).toHaveBeenCalled(); - }); - - it("title route 在 page head 请求下仍委托 unified page write adapter", async () => { - await postTitle(new Request("http://localhost/api/documents/title", { - method: "POST", - body: JSON.stringify({ - documentId: "doc_1", - workspaceId: "ws_1", - title: "新标题", - commandName: "page.head.updateTitle", - }), - })); - - expect(executePageWriteBridgeCommand).toHaveBeenCalled(); - }); - - it("options route delegates to unified page write adapter", async () => { - await postOptions(new Request("http://localhost/api/documents/options", { - method: "POST", - body: JSON.stringify({ documentId: "doc_1", workspaceId: "ws_1", options: { wideLayout: true } }), - })); - - expect(executePageWriteBridgeCommand).toHaveBeenCalled(); - }); - - it("save route delegates to unified page write adapter", async () => { - await postSave(new Request("http://localhost/api/documents/save", { - method: "POST", - body: JSON.stringify({ documentId: "doc_1", workspaceId: "ws_1", content: [] }), - })); - - expect(executePageWriteBridgeCommand).toHaveBeenCalled(); - }); -}); diff --git a/wolai-frontend/src/app/api/documents/save/route.ts b/wolai-frontend/src/app/api/documents/save/route.ts deleted file mode 100644 index ba2ce5ef..00000000 --- a/wolai-frontend/src/app/api/documents/save/route.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { - assertDocumentId, - buildDocumentBridgeContext, - buildDocumentCommandEnvelope, - documentBridgeErrorResponse, -} from "@/lib/documents/bridge"; -import { executePageWriteBridgeCommand } from "@/lib/documents/page-write-command-adapter"; -import { - buildDocumentSavePayload, - type DocumentSavePayload, -} from "@/lib/documents/save-contract"; -import { PAGE_COMMAND_NAMES } from "@/lib/documents/page-command-contract"; - -export async function POST(request: Request) { - if (isConvexEnabled()) { - try { - const body = await request.json() as Partial & { content?: unknown }; - const normalizedDocumentId = assertDocumentId(body.documentId); - const payload = buildDocumentSavePayload({ - documentId: normalizedDocumentId, - workspaceId: body.workspaceId, - revision: body.revision, - editorDocument: body.editorDocument, - content: body.content as DocumentSavePayload["content"] | undefined, - tiptapDocument: body.tiptapDocument, - conflictDetectionKey: body.conflictDetectionKey, - snapshotCapturedAt: body.snapshotCapturedAt, - blockCount: body.blockCount, - }); - const normalizedWorkspaceId = payload.workspaceId; - const bridgeContext = await buildDocumentBridgeContext({ request, workspaceId: normalizedWorkspaceId }); - const envelope = buildDocumentCommandEnvelope({ - name: PAGE_COMMAND_NAMES.saveBody, - payload: payload satisfies DocumentSavePayload, - context: bridgeContext, - target: { - workspaceId: normalizedWorkspaceId, - pageId: normalizedDocumentId, - }, - }); - const result = await executePageWriteBridgeCommand({ - context: bridgeContext, - envelope, - }); - - return NextResponse.json({ - ok: true, - revision: result.revision, - conflictDetectionKey: result.conflictDetectionKey, - meta: { - requestId: result.requestId, - traceId: result.traceId, - commandId: result.commandId, - commandName: result.commandName, - }, - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); -} - -export const runtime = "nodejs"; diff --git a/wolai-frontend/src/app/api/documents/stats/route.ts b/wolai-frontend/src/app/api/documents/stats/route.ts deleted file mode 100644 index d3490e7d..00000000 --- a/wolai-frontend/src/app/api/documents/stats/route.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import type { DocumentStats } from "@/types/page-options"; -import { - assertDocumentId, - assertStats, - buildDocumentBridgeContext, - buildDocumentCommandEnvelope, - documentBridgeErrorResponse, -} from "@/lib/documents/bridge"; -import { - executeMetadataBridgeCommand, - type DocumentStatsUpdatePayload, -} from "@/lib/documents/metadata-command-adapter"; - -interface StatsPayload { - documentId: string; - workspaceId?: string | null; - stats: DocumentStats; -} - -export async function POST(request: Request) { - if (isConvexEnabled()) { - try { - const { documentId, workspaceId, stats }: StatsPayload = await request.json(); - const normalizedDocumentId = assertDocumentId(documentId); - assertStats(stats); - const normalizedWorkspaceId = workspaceId?.trim() || null; - const bridgeContext = await buildDocumentBridgeContext({ - request, - workspaceId: normalizedWorkspaceId, - }); - const envelope = buildDocumentCommandEnvelope({ - name: "documents.stats.update", - payload: { - documentId: normalizedDocumentId, - workspaceId: normalizedWorkspaceId, - stats, - } satisfies DocumentStatsUpdatePayload, - context: bridgeContext, - target: { - workspaceId: normalizedWorkspaceId, - pageId: normalizedDocumentId, - }, - }); - const result = await executeMetadataBridgeCommand({ - context: bridgeContext, - envelope, - }); - - return NextResponse.json({ - ok: true, - meta: { - requestId: result.requestId, - traceId: result.traceId, - commandId: result.commandId, - commandName: result.commandName, - }, - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); -} diff --git a/wolai-frontend/src/app/api/documents/template/route.ts b/wolai-frontend/src/app/api/documents/template/route.ts deleted file mode 100644 index 9e1a947f..00000000 --- a/wolai-frontend/src/app/api/documents/template/route.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { executeDocumentTemplateBridgeCommand } from "@/lib/documents/page-command-adapter"; - -export async function POST(request: Request) { - if (isConvexEnabled()) { - return executeDocumentTemplateBridgeCommand(request); - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); -} diff --git a/wolai-frontend/src/app/api/documents/title/route.ts b/wolai-frontend/src/app/api/documents/title/route.ts deleted file mode 100644 index 562744fe..00000000 --- a/wolai-frontend/src/app/api/documents/title/route.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { - DocumentBridgeError, - assertDocumentId, - assertTitle, - buildDocumentBridgeContext, - buildDocumentCommandEnvelope, - documentBridgeErrorResponse, -} from "@/lib/documents/bridge"; -import { - type DocumentTitleUpdatePayload, -} from "@/lib/documents/metadata-command-adapter"; -import { executePageWriteBridgeCommand } from "@/lib/documents/page-write-command-adapter"; -import { PAGE_COMMAND_NAMES } from "@/lib/documents/page-command-contract"; - -interface RenamePayload { - documentId: string; - workspaceId?: string | null; - title: string; - commandName?: string | null; -} - -function assertPageHeadCommandName(commandName: string | null | undefined) { - if (commandName !== PAGE_COMMAND_NAMES.updateTitle) { - throw new DocumentBridgeError("标题保存仅支持 page.head.updateTitle", 400, "VALIDATION_ERROR", [ - { - field: "commandName", - reason: `expected ${PAGE_COMMAND_NAMES.updateTitle}`, - }, - ]); - } -} - -export async function POST(request: Request) { - if (isConvexEnabled()) { - try { - const { documentId, workspaceId, title, commandName }: RenamePayload = await request.json(); - const normalizedDocumentId = assertDocumentId(documentId); - const normalizedTitle = assertTitle(title); - const normalizedWorkspaceId = workspaceId?.trim() || null; - assertPageHeadCommandName(commandName); - - const bridgeContext = await buildDocumentBridgeContext({ request, workspaceId: normalizedWorkspaceId }); - const envelope = buildDocumentCommandEnvelope({ - name: PAGE_COMMAND_NAMES.updateTitle, - payload: { - documentId: normalizedDocumentId, - workspaceId: normalizedWorkspaceId, - title: normalizedTitle, - } satisfies DocumentTitleUpdatePayload, - context: bridgeContext, - target: { - workspaceId: normalizedWorkspaceId, - pageId: normalizedDocumentId, - }, - }); - const result = await executePageWriteBridgeCommand({ - context: bridgeContext, - envelope, - }); - - return NextResponse.json({ - ok: true, - meta: { - requestId: result.requestId, - traceId: result.traceId, - commandId: result.commandId, - commandName: result.commandName, - }, - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); -} - -export const runtime = "nodejs"; diff --git a/wolai-frontend/src/app/api/leptos-tiptap-runtime/[...asset]/route.ts b/wolai-frontend/src/app/api/leptos-tiptap-runtime/[...asset]/route.ts deleted file mode 100644 index 434b03ab..00000000 --- a/wolai-frontend/src/app/api/leptos-tiptap-runtime/[...asset]/route.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { promises as fs } from "node:fs"; -import path from "node:path"; -import { NextResponse } from "next/server"; - -export const runtime = "nodejs"; -export const dynamic = "force-dynamic"; - -type IslandManifest = { - entryAssetPath: string | null; - wasmAssetPath: string | null; - assetPaths: string[]; - generatedRootPath: string | null; -}; - -const GENERATED_ROOT = path.resolve( - process.cwd(), - "..", - "rust", - "spikes", - "leptos-tiptap-spike", - "generated", - "island", -); -const ENTRY_ASSET_PATTERN = /^mnote-leptos-tiptap-spike-island\.js$/; -const WASM_PATTERN = /^mnote-leptos-tiptap-spike-island_bg\.wasm$/; - -function toPosixPath(value: string): string { - return value.split(path.sep).join("/"); -} - -function sanitizeRelativePath(asset: string[]): string | null { - if (!Array.isArray(asset) || asset.length === 0) { - return null; - } - const decoded = asset.map((segment) => decodeURIComponent(segment)); - const joined = decoded.join("/"); - if (!joined || joined.includes("\0")) { - return null; - } - const normalized = path.posix.normalize(joined); - if (normalized === "." || normalized.startsWith("../") || normalized.includes("/../")) { - return null; - } - return normalized; -} - -async function walkFiles(rootDir: string): Promise { - const output: string[] = []; - const queue: string[] = [rootDir]; - - while (queue.length > 0) { - const current = queue.shift(); - if (!current) { - continue; - } - const entries = await fs.readdir(current, { withFileTypes: true }); - for (const entry of entries) { - const absolute = path.join(current, entry.name); - if (entry.isDirectory()) { - queue.push(absolute); - continue; - } - if (!entry.isFile()) { - continue; - } - const relative = path.relative(rootDir, absolute); - output.push(toPosixPath(relative)); - } - } - - return output.sort((a, b) => a.localeCompare(b)); -} - -async function buildIslandManifest(): Promise { - const files = await walkFiles(GENERATED_ROOT); - const entryAssetPath = files.find((item) => ENTRY_ASSET_PATTERN.test(path.posix.basename(item))) ?? null; - const wasmAssetPath = files.find((item) => WASM_PATTERN.test(path.posix.basename(item))) ?? null; - const generatedRootPath = entryAssetPath ? path.posix.dirname(entryAssetPath) : null; - const assetPaths = files.filter((item) => { - const basename = path.posix.basename(item); - return ( - basename.endsWith(".js") || - basename.endsWith(".wasm") || - basename.endsWith(".css") || - basename.endsWith(".json") - ); - }); - - return { - entryAssetPath, - wasmAssetPath, - assetPaths, - generatedRootPath, - }; -} - -function guessContentType(absolutePath: string): string { - const extension = path.extname(absolutePath).toLowerCase(); - if (extension === ".js" || extension === ".mjs") { - return "application/javascript; charset=utf-8"; - } - if (extension === ".wasm") { - return "application/wasm"; - } - if (extension === ".json") { - return "application/json; charset=utf-8"; - } - if (extension === ".html") { - return "text/html; charset=utf-8"; - } - if (extension === ".css") { - return "text/css; charset=utf-8"; - } - return "application/octet-stream"; -} - -export async function GET( - _request: Request, - context: { params: Promise<{ asset?: string[] }> }, -) { - const params = await context.params; - const asset = params.asset ?? []; - - if (asset.length === 1 && asset[0] === "manifest.json") { - try { - const manifest = await buildIslandManifest(); - return NextResponse.json(manifest, { - headers: { "Cache-Control": "no-store" }, - }); - } catch (error) { - return NextResponse.json( - { - error: "无法生成 leptos-tiptap island 清单", - detail: error instanceof Error ? error.message : "unknown", - }, - { status: 500 }, - ); - } - } - - const relativePath = sanitizeRelativePath(asset); - if (!relativePath) { - return NextResponse.json({ error: "非法 island 资源路径" }, { status: 400 }); - } - - const absolutePath = path.resolve(GENERATED_ROOT, relativePath); - if (!absolutePath.startsWith(GENERATED_ROOT + path.sep)) { - return NextResponse.json({ error: "越界访问 island 资源被拒绝" }, { status: 403 }); - } - - try { - const fileContent = await fs.readFile(absolutePath); - return new NextResponse(fileContent, { - status: 200, - headers: { - "Content-Type": guessContentType(absolutePath), - "Cache-Control": "no-store", - }, - }); - } catch (error) { - if ((error as NodeJS.ErrnoException)?.code === "ENOENT") { - return NextResponse.json({ error: "island 资源不存在" }, { status: 404 }); - } - return NextResponse.json( - { - error: "读取 island 资源失败", - detail: error instanceof Error ? error.message : "unknown", - }, - { status: 500 }, - ); - } -} diff --git a/wolai-frontend/src/app/api/luckysheet/get-workerbook/route.ts b/wolai-frontend/src/app/api/luckysheet/get-workerbook/route.ts deleted file mode 100644 index 080c465b..00000000 --- a/wolai-frontend/src/app/api/luckysheet/get-workerbook/route.ts +++ /dev/null @@ -1,62 +0,0 @@ -"use server"; - -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { getConvexAuthedHttpClient } from "@/lib/convex/server"; -import { api } from "@/lib/convex/api"; - -export async function GET(request: Request) { - const url = new URL(request.url); - const gridKey = url.searchParams.get("gridKey"); - - if (!gridKey) { - return NextResponse.json({ code: 400, msg: "gridKey 参数缺失" }, { status: 400 }); - } - - // Convex 模式 - if (isConvexEnabled()) { - try { - const client = await getConvexAuthedHttpClient(); - const result = await client.query(api.tables.getByGridKey, { gridKey }); - - if (!result) { - return NextResponse.json({ code: 404, msg: "未查询到相关数据" }, { status: 404 }); - } - - return NextResponse.json({ - code: 200, - msg: "ok", - data: result, - }); - } catch (error) { - console.error("Convex API error:", error); - return NextResponse.json({ code: 500, msg: "服务器错误" }, { status: 500 }); - } - } - - // Supabase 模式 - return NextResponse.json({ code: 501, msg: "当前仅支持 Convex 模式" }, { status: 501 }); - - /* - const supabase = await createSupabaseRouteClient(); - const { data, error } = await supabase - .from("document_tables") - .select("id,title,grid_key") - .eq("grid_key", gridKey) - .single(); - - if (error || !data) { - return NextResponse.json({ code: 404, msg: "未查询到相关数据" }, { status: 404 }); - } - - return NextResponse.json({ - code: 200, - msg: "ok", - data: { - title: data.title ?? "未命名工作簿", - lang: "zh", - gridKey: data.grid_key, - }, - }); - */ -} diff --git a/wolai-frontend/src/app/api/luckysheet/load/route.ts b/wolai-frontend/src/app/api/luckysheet/load/route.ts deleted file mode 100644 index 5b04db3c..00000000 --- a/wolai-frontend/src/app/api/luckysheet/load/route.ts +++ /dev/null @@ -1,86 +0,0 @@ -"use server"; - -import { NextResponse } from "next/server"; -import { createDefaultTableSnapshot } from "@/lib/online-table"; -import type { DocumentTableSnapshot, TableSchema } from "@/types/online-table"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { getConvexAuthedHttpClient } from "@/lib/convex/server"; -import { api } from "@/lib/convex/api"; - -const fallbackSchema: TableSchema = { - columns: [], - frozenRowCount: 0, - frozenColCount: 0, -}; - -export async function POST(request: Request) { - const url = new URL(request.url); - const gridKey = url.searchParams.get("gridKey"); - - if (!gridKey) { - return NextResponse.json({ code: 400, msg: "gridKey 参数缺失" }, { status: 400 }); - } - - // Convex 模式 - if (isConvexEnabled()) { - try { - const client = await getConvexAuthedHttpClient(); - const tableData = await client.query(api.tables.getByGridKeyFull, { gridKey }); - - if (!tableData) { - return NextResponse.json({ code: 404, msg: "未查询到相关数据" }, { status: 404 }); - } - - const snapshot = (tableData.snapshot as DocumentTableSnapshot | null) ?? null; - const schema = (tableData.schema as TableSchema | null) ?? fallbackSchema; - - let payload: unknown[] = []; - if (snapshot?.luckysheet && Array.isArray(snapshot.luckysheet)) { - payload = snapshot.luckysheet; - } else { - const defaultSnapshot = createDefaultTableSnapshot(schema); - payload = defaultSnapshot.luckysheet ?? []; - } - - const body = JSON.stringify(payload); - return new NextResponse(body, { - headers: { "Content-Type": "text/plain; charset=utf-8" }, - }); - } catch (error) { - console.error("Convex API error:", error); - return NextResponse.json({ code: 500, msg: "服务器错误" }, { status: 500 }); - } - } - - // Supabase 模式 - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - - /* - const supabase = await createSupabaseRouteClient(); - const { data, error } = await supabase - .from("document_tables") - .select("schema,snapshot") - .eq("grid_key", gridKey) - .single(); - - if (error || !data) { - return NextResponse.json({ code: 404, msg: "未查询到相关数据" }, { status: 404 }); - } - - const snapshot = (data.snapshot as DocumentTableSnapshot | null) ?? null; - const schema = (data.schema as TableSchema | null) ?? fallbackSchema; - - let payload: unknown[] = []; - if (snapshot?.luckysheet && Array.isArray(snapshot.luckysheet)) { - payload = snapshot.luckysheet; - } else { - const defaultSnapshot = createDefaultTableSnapshot(schema); - payload = defaultSnapshot.luckysheet ?? []; - } - - const body = JSON.stringify(payload); - return new NextResponse(body, { - headers: { "Content-Type": "text/plain; charset=utf-8" }, - }); - */ -} diff --git a/wolai-frontend/src/app/api/luckysheet/upload-image/route.ts b/wolai-frontend/src/app/api/luckysheet/upload-image/route.ts deleted file mode 100644 index fc5ef265..00000000 --- a/wolai-frontend/src/app/api/luckysheet/upload-image/route.ts +++ /dev/null @@ -1,45 +0,0 @@ -"use server"; - -import { Buffer } from "node:buffer"; -import { randomUUID } from "node:crypto"; -import { NextResponse } from "next/server"; - -const BUCKET = "media"; -const DIRECTORY = "luckysheet"; - -export async function POST(request: Request) { - return NextResponse.json({ code: 501, msg: "当前仅支持 Convex 模式" }, { status: 501 }); - - /* - const supabase = await createSupabaseRouteClient(); - const formData = await request.formData(); - const file = formData.get("image"); - - if (!(file instanceof File)) { - return NextResponse.json({ code: 400, msg: "缺少 image 文件" }, { status: 400 }); - } - - const arrayBuffer = await file.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - const extension = file.name?.split(".").pop() || "bin"; - const objectPath = `${DIRECTORY}/${randomUUID()}.${extension}`; - - const { error: uploadError } = await supabase.storage - .from(BUCKET) - .upload(objectPath, buffer, { contentType: file.type || "application/octet-stream", upsert: false }); - - if (uploadError) { - return NextResponse.json({ code: 500, msg: "上传失败", detail: uploadError.message }, { status: 500 }); - } - - const { - data: { publicUrl }, - } = supabase.storage.from(BUCKET).getPublicUrl(objectPath); - - return NextResponse.json({ - code: 200, - msg: "ok", - url: publicUrl, - }); - */ -} diff --git a/wolai-frontend/src/app/api/media/assets/route.ts b/wolai-frontend/src/app/api/media/assets/route.ts deleted file mode 100644 index 57dcd441..00000000 --- a/wolai-frontend/src/app/api/media/assets/route.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { NextResponse } from "next/server"; -import type { MediaAsset } from "@/types/media"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { HttpError, requireAuthContext } from "@/lib/auth/authContext"; -import { api } from "@/lib/convex/api"; -import { getConvexAuthedHttpClient } from "@/lib/convex/server"; -import { maybeProxyForBrowserUrl } from "@/lib/url/proxyForBrowser"; - -export const dynamic = "force-dynamic"; - -export async function GET(request: Request) { - if (isConvexEnabled()) { - let auth; - try { - auth = await requireAuthContext(); - } catch (err) { - if (err instanceof HttpError) { - return NextResponse.json({ error: "未登录" }, { status: err.status }); - } - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - - const { searchParams } = new URL(request.url); - const workspaceId = searchParams.get("workspaceId"); - const limit = Number.parseInt(searchParams.get("limit") ?? "12", 10); - const assetType = searchParams.get("assetType") ?? undefined; - - if (!workspaceId) { - return NextResponse.json({ error: "缺少 workspaceId" }, { status: 400 }); - } - - const client = await getConvexAuthedHttpClient(); - const items = await client.query(api.mediaAssets.listByWorkspace, { - userId: auth.userId, - workspaceId, - assetType, - includeDeleted: false, - limit: Number.isNaN(limit) ? 12 : limit, - }); - - const safeItems = (items ?? []).map((a: any) => ({ - ...a, - file_url: maybeProxyForBrowserUrl(request, String(a?.file_url ?? "")), - thumbnail_url: maybeProxyForBrowserUrl(request, String(a?.thumbnail_url ?? a?.file_url ?? "")), - })) as MediaAsset[]; - - return NextResponse.json({ items: safeItems }); - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - - const supabase = null as any; - const { - data: { user }, - error: authError, - } = await supabase.auth.getUser(); - - if (authError || !user) { - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - - const { searchParams } = new URL(request.url); - const workspaceId = searchParams.get("workspaceId"); - const limit = Number.parseInt(searchParams.get("limit") ?? "12", 10); - - if (!workspaceId) { - return NextResponse.json({ error: "缺少 workspaceId" }, { status: 400 }); - } - - let query = supabase - .from("media_assets") - .select("*") - .eq("workspace_id", workspaceId) - .is("deleted_at", null) - .order("created_at", { ascending: false }) - .limit(Number.isNaN(limit) ? 12 : limit); - - const assetType = searchParams.get("assetType"); - if (assetType) { - query = query.eq("asset_type", assetType); - } - - const { data, error } = await query; - - if (error) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } - - return NextResponse.json({ items: (data ?? []) as MediaAsset[] }); -} - -export async function POST(request: Request) { - if (isConvexEnabled()) { - let auth; - try { - auth = await requireAuthContext(); - } catch (err) { - if (err instanceof HttpError) { - return NextResponse.json({ error: "未登录" }, { status: err.status }); - } - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - - const payload = (await request.json()) as { - workspaceId: string; - documentId: string; - fileUrl: string; - thumbnailUrl?: string; - assetType?: string; - fileName?: string; - fileSize?: number; - mimeType?: string; - }; - - if (!payload.workspaceId || !payload.documentId || !payload.fileUrl) { - return NextResponse.json({ error: "参数不完整" }, { status: 400 }); - } - - const assetId = - typeof crypto.randomUUID === "function" - ? crypto.randomUUID() - : `${Date.now()}_${Math.random().toString(16).slice(2)}`; - - const now = new Date().toISOString(); - const asset: MediaAsset = { - id: assetId, - workspace_id: payload.workspaceId, - document_id: payload.documentId, - file_url: payload.fileUrl, - thumbnail_url: payload.thumbnailUrl ?? payload.fileUrl, - asset_type: payload.assetType ?? "image", - file_name: payload.fileName ?? null, - file_size: payload.fileSize ?? null, - mime_type: payload.mimeType ?? null, - ocr_text: null, - ocr_status: null, - created_at: now, - updated_at: now, - }; - - const client = await getConvexAuthedHttpClient(); - await client.mutation(api.mediaAssets.create, { - userId: auth.userId, - asset: { - id: asset.id, - workspace_id: asset.workspace_id, - document_id: asset.document_id, - asset_type: asset.asset_type, - file_url: asset.file_url, - thumbnail_url: asset.thumbnail_url, - bucket: null, - storage_path: null, - file_name: asset.file_name, - file_size: asset.file_size, - mime_type: asset.mime_type, - }, - }); - - const safeAsset = { - ...asset, - file_url: maybeProxyForBrowserUrl(request, String(asset?.file_url ?? "")), - thumbnail_url: maybeProxyForBrowserUrl(request, String(asset?.thumbnail_url ?? asset?.file_url ?? "")), - } as MediaAsset; - - return NextResponse.json({ asset: safeAsset }); - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - - const supabase = null as any; - const { - data: { user }, - error: authError, - } = await supabase.auth.getUser(); - - if (authError || !user) { - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - - const payload = (await request.json()) as { - workspaceId: string; - documentId: string; - fileUrl: string; - thumbnailUrl?: string; - assetType?: string; - fileName?: string; - fileSize?: number; - mimeType?: string; - }; - - if (!payload.workspaceId || !payload.documentId || !payload.fileUrl) { - return NextResponse.json({ error: "参数不完整" }, { status: 400 }); - } - - const { data, error } = await supabase - .from("media_assets") - .insert({ - workspace_id: payload.workspaceId, - document_id: payload.documentId, - file_url: payload.fileUrl, - thumbnail_url: payload.thumbnailUrl ?? payload.fileUrl, - asset_type: payload.assetType ?? "image", - file_name: payload.fileName, - file_size: payload.fileSize ?? null, - mime_type: payload.mimeType ?? null, - created_by: user.id, - }) - .select("*") - .single(); - - if (error) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } - - return NextResponse.json({ asset: data as MediaAsset }); -} diff --git a/wolai-frontend/src/app/api/media/batch/route.test.ts b/wolai-frontend/src/app/api/media/batch/route.test.ts deleted file mode 100644 index c2278029..00000000 --- a/wolai-frontend/src/app/api/media/batch/route.test.ts +++ /dev/null @@ -1,338 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const mockIsConvexEnabled = vi.fn(() => true); -const mockRequireAuthContext = vi.fn(); -const mockGetConvexAuthedHttpClient = vi.fn(); -const mockBuildDocumentBridgeContext = vi.fn(); -const mockBuildDocumentCommandEnvelope = vi.fn(); -const mockResolveRustBridgeCommandPlan = vi.fn(); -const mockExecuteRustBridgeMutationTransport = vi.fn(); -const mockRecordRustBridgeCommandArtifacts = vi.fn(); -const mockMaterializeRustTreeStreamDelta = vi.fn(); -const mockMaterializeRustTreeDomainEventPlan = vi.fn(); -const mockReadRustTreeDomainEventType = vi.fn(); -const mockRecordBridgeCommandArtifacts = vi.fn(); - -vi.mock("@/lib/convex/enabled", () => ({ - isConvexEnabled: () => mockIsConvexEnabled(), -})); - -vi.mock("@/lib/auth/authContext", () => ({ - HttpError: class HttpError extends Error { - status: number; - - constructor(status: number, message: string) { - super(message); - this.status = status; - } - }, - requireAuthContext: () => mockRequireAuthContext(), -})); - -vi.mock("@/lib/convex/api", () => ({ - api: { - mediaAssets: { - listByIds: "mediaAssets:listByIds", - patchById: "mediaAssets:patchById", - }, - documents: { - getMeta: "documents:getMeta", - }, - }, -})); - -vi.mock("@/lib/convex/server", () => ({ - getConvexAuthedHttpClient: () => mockGetConvexAuthedHttpClient(), -})); - -vi.mock("@/lib/documents/bridge", () => ({ - buildDocumentBridgeContext: (...args: unknown[]) => mockBuildDocumentBridgeContext(...args), - buildDocumentCommandEnvelope: (...args: unknown[]) => mockBuildDocumentCommandEnvelope(...args), -})); - -vi.mock("@/lib/documents/rust-runtime", () => ({ - resolveRustBridgeCommandPlan: (...args: unknown[]) => mockResolveRustBridgeCommandPlan(...args), - executeRustBridgeMutationTransport: (...args: unknown[]) => - mockExecuteRustBridgeMutationTransport(...args), - recordRustBridgeCommandArtifacts: (...args: unknown[]) => - mockRecordRustBridgeCommandArtifacts(...args), - materializeRustTreeStreamDelta: (...args: unknown[]) => - mockMaterializeRustTreeStreamDelta(...args), - materializeRustTreeDomainEventPlan: (...args: unknown[]) => - mockMaterializeRustTreeDomainEventPlan(...args), - readRustTreeDomainEventType: (...args: unknown[]) => mockReadRustTreeDomainEventType(...args), -})); - -vi.mock("@/lib/documents/bridge-log", () => ({ - recordBridgeCommandArtifacts: (...args: unknown[]) => mockRecordBridgeCommandArtifacts(...args), -})); - -vi.mock("@/lib/url/proxyForBrowser", () => ({ - maybeProxyForBrowserUrl: (_request: Request, url: string) => url, -})); - -describe("/api/media/batch route", () => { - beforeEach(() => { - vi.resetModules(); - mockIsConvexEnabled.mockReset().mockReturnValue(true); - mockRequireAuthContext.mockReset().mockResolvedValue({ - userId: "user_1", - }); - mockBuildDocumentBridgeContext.mockReset().mockResolvedValue({ - requestId: "req_1", - traceId: "trace_1", - workspaceId: "ws_1", - actor: { actorType: "user", actorId: "user_1", sessionId: null }, - source: { channel: "next-route", client: "vitest" }, - tenantId: null, - deploymentId: null, - projectId: null, - authToken: null, - idempotencyKey: null, - validateOnly: false, - dryRun: false, - }); - mockBuildDocumentCommandEnvelope.mockReset().mockImplementation((input: unknown) => ({ - ...(input as Record), - commandId: "cmd_asset_1", - idempotencyKey: null, - })); - mockResolveRustBridgeCommandPlan.mockReset().mockResolvedValue({ - kind: "command", - commandName: "tree.resource.move", - commandId: "cmd_asset_1", - functionName: "mediaAssets:batchMove", - argsJson: { - domainEventPlan: { - family: "tree", - schema: "mnote.tree.domain_event", - schemaVersion: 1, - eventType: "tree.resource.moved", - }, - domainEventHint: { - family: "tree", - eventType: "tree.resource.moved", - }, - }, - }); - mockExecuteRustBridgeMutationTransport.mockReset().mockResolvedValue({ - items: [ - { - id: "asset_1", - workspace_id: "ws_1", - document_id: "doc_target", - asset_type: "file", - file_url: "/file.pdf", - thumbnail_url: "/file.pdf", - file_name: "file.pdf", - file_size: 1024, - mime_type: "application/pdf", - ocr_text: null, - ocr_status: null, - created_at: "2026-04-26T00:00:00Z", - updated_at: "2026-04-26T00:00:00Z", - }, - ], - }); - mockRecordRustBridgeCommandArtifacts.mockReset().mockResolvedValue(null); - mockMaterializeRustTreeStreamDelta.mockReset().mockReturnValue({ - op: "upsert_assets", - upsertAssets: [{ id: "asset_1" }], - }); - mockMaterializeRustTreeDomainEventPlan.mockReset().mockReturnValue({ - family: "tree", - schema: "mnote.tree.domain_event", - schemaVersion: 1, - eventType: "tree.resource.moved", - streamDelta: { - op: "upsert_assets", - upsertAssets: [{ id: "asset_1" }], - }, - }); - mockReadRustTreeDomainEventType.mockReset().mockReturnValue("tree.resource.moved"); - mockRecordBridgeCommandArtifacts.mockReset().mockResolvedValue(undefined); - }); - - it("copy/move 应进入 Rust resource command plan 并记录资源 delta artifact", async () => { - const client = { - query: vi.fn(async (name: string, args: Record) => { - if (name === "mediaAssets:listByIds") { - expect(args).toEqual({ userId: "user_1", ids: ["asset_1"] }); - return [{ id: "asset_1", workspace_id: "ws_1" }]; - } - if (name === "documents:getMeta") { - expect(args).toEqual({ id: "doc_target" }); - return { id: "doc_target", workspace_id: "ws_1" }; - } - return null; - }), - mutation: vi.fn(), - }; - mockGetConvexAuthedHttpClient.mockResolvedValue(client); - - const { POST } = await import("./route"); - const response = await POST( - new Request("http://localhost/api/media/batch", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - action: "move", - assetIds: ["asset_1"], - targetDocumentId: "doc_target", - targetSubPath: "mindmaps/mind_1", - }), - }), - ); - - expect(response.status).toBe(200); - expect(await response.json()).toMatchObject({ - items: [ - { - id: "asset_1", - document_id: "doc_target", - }, - ], - }); - expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( - expect.objectContaining({ - name: "tree.resource.move", - payload: { - assetIds: ["asset_1"], - targetDocumentId: "doc_target", - targetSubPath: "mindmaps/mind_1", - }, - target: { - workspaceId: "ws_1", - pageId: "doc_target", - }, - }), - ); - expect(mockExecuteRustBridgeMutationTransport).toHaveBeenCalledWith( - expect.objectContaining({ - client, - plan: expect.objectContaining({ - functionName: "mediaAssets:batchMove", - }), - }), - ); - expect(mockRecordRustBridgeCommandArtifacts).toHaveBeenCalledWith( - expect.objectContaining({ - envelope: expect.objectContaining({ - name: "tree.resource.move", - }), - plan: expect.objectContaining({ - commandName: "tree.resource.move", - functionName: "mediaAssets:batchMove", - }), - result: expect.objectContaining({ - items: [expect.objectContaining({ id: "asset_1" })], - }), - }), - ); - expect(mockRecordBridgeCommandArtifacts).not.toHaveBeenCalled(); - }); - - it("delete/restore/rename 应通过正式 tree.resource 生命周期命令执行", async () => { - const client = { - query: vi.fn(async (name: string, args: Record) => { - if (name === "mediaAssets:listByIds") { - expect(args).toEqual({ userId: "user_1", ids: ["asset_1"] }); - return [ - { - id: "asset_1", - workspace_id: "ws_1", - document_id: "doc_1", - file_name: "old.pdf", - }, - ]; - } - return null; - }), - mutation: vi.fn(), - }; - mockGetConvexAuthedHttpClient.mockResolvedValue(client); - mockResolveRustBridgeCommandPlan.mockResolvedValueOnce({ - kind: "command", - commandName: "tree.resource.archive", - commandId: "cmd_asset_archive", - functionName: "mediaAssets:patchById", - argsJson: { - domainEventPlan: { eventType: "tree.resource.archived" }, - }, - }); - mockResolveRustBridgeCommandPlan.mockResolvedValueOnce({ - kind: "command", - commandName: "tree.resource.restore", - commandId: "cmd_asset_restore", - functionName: "mediaAssets:patchById", - argsJson: { - domainEventPlan: { eventType: "tree.resource.restored" }, - }, - }); - mockResolveRustBridgeCommandPlan.mockResolvedValueOnce({ - kind: "command", - commandName: "tree.resource.rename", - commandId: "cmd_asset_rename", - functionName: "mediaAssets:patchById", - argsJson: { - domainEventPlan: { eventType: "tree.resource.renamed" }, - }, - }); - mockExecuteRustBridgeMutationTransport.mockResolvedValue({ ok: true }); - - const { POST } = await import("./route"); - for (const body of [ - { action: "delete", assetIds: ["asset_1"] }, - { action: "restore", assetIds: ["asset_1"] }, - { action: "rename", assetIds: ["asset_1"], newName: "new" }, - ]) { - const response = await POST( - new Request("http://localhost/api/media/batch", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify(body), - }), - ); - expect(response.status).toBe(200); - } - - expect(mockBuildDocumentCommandEnvelope).toHaveBeenNthCalledWith( - 1, - expect.objectContaining({ - name: "tree.resource.archive", - payload: { - resourceKind: "file", - assetId: "asset_1", - }, - target: { - workspaceId: "ws_1", - pageId: "doc_1", - }, - }), - ); - expect(mockBuildDocumentCommandEnvelope).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - name: "tree.resource.restore", - payload: { - resourceKind: "file", - assetId: "asset_1", - }, - }), - ); - expect(mockBuildDocumentCommandEnvelope).toHaveBeenNthCalledWith( - 3, - expect.objectContaining({ - name: "tree.resource.rename", - payload: { - resourceKind: "file", - assetId: "asset_1", - newName: "new.pdf", - }, - }), - ); - expect(mockExecuteRustBridgeMutationTransport).toHaveBeenCalledTimes(3); - expect(mockRecordRustBridgeCommandArtifacts).toHaveBeenCalledTimes(3); - expect(client.mutation).not.toHaveBeenCalled(); - }); -}); diff --git a/wolai-frontend/src/app/api/media/batch/route.ts b/wolai-frontend/src/app/api/media/batch/route.ts deleted file mode 100644 index 65da7582..00000000 --- a/wolai-frontend/src/app/api/media/batch/route.ts +++ /dev/null @@ -1,447 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { HttpError, requireAuthContext } from "@/lib/auth/authContext"; -import { api } from "@/lib/convex/api"; -import { getConvexAuthedHttpClient } from "@/lib/convex/server"; -import { maybeProxyForBrowserUrl } from "@/lib/url/proxyForBrowser"; -import { - buildDocumentBridgeContext, - buildDocumentCommandEnvelope, -} from "@/lib/documents/bridge"; -import { - recordRustBridgeCommandArtifacts, - resolveRustBridgeCommandPlan, - executeRustBridgeMutationTransport, -} from "@/lib/documents/rust-runtime"; - -export const dynamic = "force-dynamic"; - -type Action = "copy" | "move" | "delete" | "rename" | "restore"; - -interface BatchPayload { - action: Action; - assetIds: string[]; - targetDocumentId?: string; - targetSubPath?: string; - newName?: string; -} - -function sanitizeSubPath(input: string | undefined): string { - const raw = typeof input === "string" ? input : ""; - const cleaned = raw - .replace(/\\/g, "/") - .split("/") - .map((seg) => seg.trim()) - .filter((seg) => Boolean(seg) && seg !== "." && seg !== "..") - .map((seg) => seg.replace(/[\\/]/g, "_")) - .slice(0, 8); - return cleaned.join("/"); -} - -function sanitizeTransferredAssetForBrowser(request: Request, asset: any) { - return { - ...asset, - file_url: maybeProxyForBrowserUrl(request, String(asset?.file_url ?? "")), - thumbnail_url: maybeProxyForBrowserUrl(request, String(asset?.thumbnail_url ?? asset?.file_url ?? "")), - }; -} - -async function executeResourceLifecycleCommand(input: { - request: Request; - client: never; - asset: any; - authUserId: string; - commandName: "tree.resource.archive" | "tree.resource.restore" | "tree.resource.rename"; - payload: Record; -}) { - const workspaceId = String(input.asset?.workspace_id ?? "").trim(); - if (!workspaceId) { - throw new Error("缺少资源工作空间"); - } - const documentId = String(input.asset?.document_id ?? "").trim(); - const context = await buildDocumentBridgeContext({ - request: input.request, - workspaceId, - }); - const envelope = buildDocumentCommandEnvelope({ - name: input.commandName, - payload: input.payload, - context, - target: { - workspaceId, - pageId: documentId || undefined, - }, - reason: `media-batch ${input.commandName}`, - refs: ["file-tree-resource-command", "media-batch-compat-alias"], - }); - const plan = await resolveRustBridgeCommandPlan({ - context, - envelope, - }); - const result = await executeRustBridgeMutationTransport({ - client: input.client, - plan, - }); - try { - await recordRustBridgeCommandArtifacts({ - context, - envelope, - client: input.client, - plan, - result, - }); - } catch (error) { - console.warn("[media.batch] Rust lifecycle bridge artifacts skipped:", error); - } - return result; -} - -export async function POST(request: Request) { - if (isConvexEnabled()) { - let auth; - try { - auth = await requireAuthContext(); - } catch (err) { - if (err instanceof HttpError) { - return NextResponse.json({ error: "未登录" }, { status: err.status }); - } - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - - const payload = (await request.json()) as BatchPayload; - if (!payload?.action || !payload.assetIds?.length) { - return NextResponse.json({ error: "缺少参数" }, { status: 400 }); - } - - const client = await getConvexAuthedHttpClient(); - const assets = (await client.query(api.mediaAssets.listByIds, { - userId: auth.userId, - ids: payload.assetIds, - })) as any[]; - - if (!assets?.length) { - return NextResponse.json({ error: "未找到附件" }, { status: 404 }); - } - - try { - switch (payload.action) { - case "rename": { - if (payload.assetIds.length !== 1 || !payload.newName) { - return NextResponse.json({ error: "重命名需要单个文件与新名称" }, { status: 400 }); - } - const asset = assets[0]; - const currentName = String(asset.file_name ?? ""); - const ext = currentName.includes(".") ? `.${currentName.split(".").pop()}` : ""; - const newFileName = payload.newName.includes(".") || !ext ? payload.newName : `${payload.newName}${ext}`; - const safeName = newFileName.replace(/[\\/]/g, "_"); - - await executeResourceLifecycleCommand({ - request, - client: client as never, - asset, - authUserId: auth.userId, - commandName: "tree.resource.rename", - payload: { - resourceKind: "file", - assetId: String(asset.id), - newName: safeName, - }, - }); - return NextResponse.json({ ok: true }); - } - case "delete": - case "restore": { - const commandName = - payload.action === "delete" ? "tree.resource.archive" : "tree.resource.restore"; - for (const asset of assets) { - await executeResourceLifecycleCommand({ - request, - client: client as never, - asset, - authUserId: auth.userId, - commandName, - payload: { - resourceKind: "file", - assetId: String(asset.id), - }, - }); - } - return NextResponse.json({ ok: true }); - } - case "copy": - case "move": { - if (!payload.targetDocumentId) { - return NextResponse.json({ error: "缺少目标页面" }, { status: 400 }); - } - - const targetDoc = await client.query(api.documents.getMeta, { - id: payload.targetDocumentId, - }); - - if (!targetDoc) { - return NextResponse.json({ error: "目标页面不存在" }, { status: 404 }); - } - - const workspaceId = String(targetDoc.workspace_id ?? "").trim(); - if (!workspaceId) { - return NextResponse.json({ error: "缺少目标工作空间" }, { status: 400 }); - } - - const context = await buildDocumentBridgeContext({ - request, - workspaceId, - }); - const commandName = - payload.action === "copy" ? "tree.resource.copy" : "tree.resource.move"; - const envelope = buildDocumentCommandEnvelope({ - name: commandName, - payload: { - assetIds: payload.assetIds, - targetDocumentId: payload.targetDocumentId, - targetSubPath: sanitizeSubPath(payload.targetSubPath), - }, - context, - target: { - workspaceId, - pageId: payload.targetDocumentId, - }, - reason: `media-batch ${commandName}`, - refs: ["file-tree-resource-command"], - }); - const plan = await resolveRustBridgeCommandPlan({ - context, - envelope, - }); - const result = await executeRustBridgeMutationTransport<{ items?: any[] }>({ - client: client as never, - plan, - }); - try { - await recordRustBridgeCommandArtifacts({ - context, - envelope, - client: client as never, - plan, - result, - }); - } catch (error) { - console.warn("[media.batch] Rust bridge artifacts skipped:", error); - } - - const safeItems = (result.items ?? []).map((item: any) => - sanitizeTransferredAssetForBrowser(request, item), - ); - - return NextResponse.json({ items: safeItems }); - } - default: - return NextResponse.json({ error: "不支持的操作" }, { status: 400 }); - } - } catch (err) { - const message = err instanceof Error ? err.message : "操作失败"; - return NextResponse.json({ error: message }, { status: 500 }); - } - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - - /* - const supabase = await createSupabaseRouteClient(); - const { - data: { session }, - } = await supabase.auth.getSession(); - - if (!session) { - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - - const payload = (await request.json()) as BatchPayload; - if (!payload?.action || !payload.assetIds?.length) { - return NextResponse.json({ error: "缺少参数" }, { status: 400 }); - } - - const { data: assets, error: fetchError } = await supabase - .from("media_assets") - .select("*") - .in("id", payload.assetIds); - - if (fetchError) { - return NextResponse.json({ error: fetchError.message }, { status: 500 }); - } - - if (!assets?.length) { - return NextResponse.json({ error: "未找到附件" }, { status: 404 }); - } - - try { - switch (payload.action) { - case "delete": { - // 可撤销删除:仅标记 deleted_at,真正清理(OCR/Storage/LightRAG)由后台宽限期任务处理 - const { error } = await supabase - .from("media_assets") - .update({ - deleted_at: new Date().toISOString(), - deleted_by: session.user.id, - }) - .in("id", payload.assetIds); - if (error) throw error; - return NextResponse.json({ ok: true }); - } - case "restore": { - const { error } = await supabase - .from("media_assets") - .update({ - deleted_at: null, - deleted_by: null, - purged_at: null, - }) - .in("id", payload.assetIds); - if (error) throw error; - return NextResponse.json({ ok: true }); - } - case "rename": { - if (payload.assetIds.length !== 1 || !payload.newName) { - return NextResponse.json({ error: "重命名需要单个文件与新名称" }, { status: 400 }); - } - const asset = assets[0]; - const ext = asset.file_name?.includes(".") ? `.${asset.file_name.split(".").pop()}` : ""; - const newFileName = payload.newName.includes(".") || !ext ? payload.newName : `${payload.newName}${ext}`; - const location = resolveAssetLocation(asset); - const safeName = newFileName.replace(/[\\/]/g, "_"); - if (location) { - const uniqueId = - typeof crypto.randomUUID === "function" - ? crypto.randomUUID() - : `${Date.now()}_${Math.random().toString(16).slice(2)}`; - const baseDir = (() => { - const current = location.path; - const idx = current.lastIndexOf("/"); - if (idx > 0) return current.slice(0, idx); - return `${asset.workspace_id}/${asset.document_id}`; - })(); - const targetPath = `${baseDir}/${Date.now()}-${uniqueId}-${safeName}`; - const bucket = location.bucket || asset.bucket || DEFAULT_DOC_BUCKET || BUCKET; - const moveResult = await supabase.storage.from(bucket).move(location.path, targetPath); - if (moveResult.error) throw moveResult.error; - const { data: signed } = await supabase.storage.from(bucket).createSignedUrl(targetPath, 60 * 60 * 24 * 7); - let signedUrl = signed?.signedUrl ?? null; - if (signedUrl) { - signedUrl = rewriteToPublicOrigin(signedUrl, getMnoteRuntimeConfig().supabaseUrl); - } - const { error } = await supabase - .from("media_assets") - .update({ - file_name: safeName, - bucket, - storage_path: targetPath, - file_url: signedUrl, - thumbnail_url: signedUrl, - }) - .eq("id", asset.id); - if (error) throw error; - return NextResponse.json({ ok: true }); - } - const { error } = await supabase - .from("media_assets") - .update({ - file_name: safeName, - }) - .eq("id", asset.id); - if (error) throw error; - return NextResponse.json({ ok: true }); - } - case "copy": - case "move": { - if (!payload.targetDocumentId) { - return NextResponse.json({ error: "缺少目标页面" }, { status: 400 }); - } - const { data: targetDoc, error: docErr } = await supabase - .from("documents") - .select("workspace_id") - .eq("id", payload.targetDocumentId) - .single(); - if (docErr || !targetDoc) { - return NextResponse.json({ error: "目标页面不存在" }, { status: 404 }); - } - const { data: existingRows } = await supabase - .from("media_assets") - .select("file_name") - .eq("document_id", payload.targetDocumentId); - const existingNames = new Set((existingRows ?? []).map((r) => (r.file_name ?? "").toString()).filter(Boolean)); - const results = []; - for (const asset of assets) { - const location = resolveAssetLocation(asset); - if (!location) continue; - const fileName = makeUniqueFileName(asset.file_name ?? "附件", existingNames).replace(/[\\/]/g, "_"); - const uniqueId = - typeof crypto.randomUUID === "function" - ? crypto.randomUUID() - : `${Date.now()}_${Math.random().toString(16).slice(2)}`; - const subdir = sanitizeSubPath(payload.targetSubPath); - const prefix = `${targetDoc.workspace_id}/${payload.targetDocumentId}${subdir ? `/${subdir}` : ""}`; - const targetPath = `${prefix}/${Date.now()}-${uniqueId}-${fileName}`; - const bucket = location.bucket || asset.bucket || DEFAULT_DOC_BUCKET || BUCKET; - if (payload.action === "copy") { - const copyRes = await supabase.storage.from(bucket).copy(location.path, targetPath); - if (copyRes.error) throw copyRes.error; - const { data: signed } = await supabase.storage.from(bucket).createSignedUrl(targetPath, 60 * 60 * 24 * 7); - let signedUrl = signed?.signedUrl ?? null; - if (signedUrl) { - signedUrl = rewriteToPublicOrigin(signedUrl, getMnoteRuntimeConfig().supabaseUrl); - } - const { data: inserted, error } = await supabase - .from("media_assets") - .insert({ - workspace_id: targetDoc.workspace_id, - document_id: payload.targetDocumentId, - asset_type: asset.asset_type, - file_name: fileName, - file_size: asset.file_size, - mime_type: asset.mime_type, - bucket, - storage_path: targetPath, - file_url: signedUrl, - thumbnail_url: signedUrl, - created_by: session.user.id, - }) - .select("*") - .single(); - if (error) throw error; - results.push(inserted); - } else { - const moveRes = await supabase.storage.from(bucket).move(location.path, targetPath); - if (moveRes.error) throw moveRes.error; - const { data: signed } = await supabase.storage.from(bucket).createSignedUrl(targetPath, 60 * 60 * 24 * 7); - let signedUrl = signed?.signedUrl ?? null; - if (signedUrl) { - signedUrl = rewriteToPublicOrigin(signedUrl, getMnoteRuntimeConfig().supabaseUrl); - } - const { data: updated, error } = await supabase - .from("media_assets") - .update({ - workspace_id: targetDoc.workspace_id, - document_id: payload.targetDocumentId, - storage_path: targetPath, - file_name: fileName, - bucket, - file_url: signedUrl, - thumbnail_url: signedUrl, - }) - .eq("id", asset.id) - .select("*") - .single(); - if (error) throw error; - results.push(updated); - } - } - return NextResponse.json({ items: results }); - } - default: - return NextResponse.json({ error: "不支持的操作" }, { status: 400 }); - } - } catch (err) { - const message = err instanceof Error ? err.message : "操作失败"; - return NextResponse.json({ error: message }, { status: 500 }); - } - */ -} diff --git a/wolai-frontend/src/app/api/media/by-document/route.ts b/wolai-frontend/src/app/api/media/by-document/route.ts deleted file mode 100644 index ec36da76..00000000 --- a/wolai-frontend/src/app/api/media/by-document/route.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { NextResponse } from "next/server"; -import type { MediaAsset } from "@/types/media"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { HttpError, requireAuthContext } from "@/lib/auth/authContext"; -import { api } from "@/lib/convex/api"; -import { getConvexAuthedHttpClient } from "@/lib/convex/server"; -import { maybeProxyForBrowserUrl } from "@/lib/url/proxyForBrowser"; - -export const dynamic = "force-dynamic"; - -export async function GET(request: Request) { - if (isConvexEnabled()) { - let auth; - try { - auth = await requireAuthContext(); - } catch (err) { - if (err instanceof HttpError) { - return NextResponse.json({ error: "未登录" }, { status: err.status }); - } - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - - const { searchParams } = new URL(request.url); - const documentId = searchParams.get("documentId") ?? ""; - const limit = Number.parseInt(searchParams.get("limit") ?? "200", 10); - - if (!documentId) { - return NextResponse.json({ error: "缺少 documentId" }, { status: 400 }); - } - - const client = await getConvexAuthedHttpClient(); - const items = await client.query(api.mediaAssets.listByDocument, { - userId: auth.userId, - documentId, - limit: Number.isNaN(limit) ? 200 : limit, - }); - - const safeItems = (items ?? []).map((a: any) => ({ - ...a, - file_url: maybeProxyForBrowserUrl(request, String(a?.file_url ?? "")), - thumbnail_url: maybeProxyForBrowserUrl(request, String(a?.thumbnail_url ?? a?.file_url ?? "")), - })) as MediaAsset[]; - - return NextResponse.json({ items: safeItems }); - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); -} diff --git a/wolai-frontend/src/app/api/media/empty-trash/route.ts b/wolai-frontend/src/app/api/media/empty-trash/route.ts deleted file mode 100644 index 8a0b70e9..00000000 --- a/wolai-frontend/src/app/api/media/empty-trash/route.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { HttpError, requireAuthContext } from "@/lib/auth/authContext"; -import { api } from "@/lib/convex/api"; -import { getConvexAuthedHttpClient } from "@/lib/convex/server"; - -export const dynamic = "force-dynamic"; - -interface EmptyTrashPayload { - workspaceId?: string; -} - -function makeExpiredDeletedAtForForceEmpty(): string { - // “清空附件垃圾桶”是一个显式的强制操作:直接清空所有垃圾桶内的附件,不受 10 分钟宽限期限制。 - // Convex 侧按 `deleted_at <= expiredDeletedAt` 做过滤,因此这里给一个很靠后的时间上界即可。 - return new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 100).toISOString(); -} - -export async function POST(request: Request) { - if (isConvexEnabled()) { - let auth; - try { - auth = await requireAuthContext(); - } catch (err) { - if (err instanceof HttpError) { - return NextResponse.json({ error: "未登录" }, { status: err.status }); - } - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - - const { workspaceId }: EmptyTrashPayload = await request.json().catch(() => ({})); - if (!workspaceId) { - return NextResponse.json({ error: "缺少 workspaceId" }, { status: 400 }); - } - - const client = await getConvexAuthedHttpClient(); - const res = await client.mutation(api.mediaAssets.emptyTrashByWorkspace, { - userId: auth.userId, - workspaceId, - expiredDeletedAt: makeExpiredDeletedAtForForceEmpty(), - }); - - return NextResponse.json({ success: true, ...(res as any) }); - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - - /* - const supabase = await createSupabaseRouteClient(); - const { - data: { session }, - } = await supabase.auth.getSession(); - - if (!session) { - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - - const { workspaceId }: EmptyTrashPayload = await request.json().catch(() => ({})); - if (!workspaceId) { - return NextResponse.json({ error: "缺少 workspaceId" }, { status: 400 }); - } - - const { data: membership, error: membershipError } = await supabase - .from("workspace_members") - .select("workspace_id") - .eq("workspace_id", workspaceId) - .eq("user_id", session.user.id) - .limit(1); - - if (membershipError) { - return NextResponse.json({ error: membershipError.message }, { status: 500 }); - } - - if (!membership || membership.length === 0) { - return NextResponse.json({ error: "无权操作该工作空间" }, { status: 403 }); - } - - const { data: assets, error: fetchError } = await supabase - .from("media_assets") - .select("id") - .eq("workspace_id", workspaceId) - .not("deleted_at", "is", null) - .is("purged_at", null) - .limit(2000); - - if (fetchError) { - return NextResponse.json({ error: fetchError.message }, { status: 500 }); - } - - const assetIds = (assets ?? []).map((row) => row.id).filter(Boolean); - if (assetIds.length === 0) { - return NextResponse.json({ success: true, updated: 0 }); - } - - const { error: updateError } = await supabase - .from("media_assets") - .update({ - deleted_at: makeExpiredDeletedAt(), - deleted_by: session.user.id, - }) - .in("id", assetIds); - - if (updateError) { - return NextResponse.json({ error: updateError.message }, { status: 500 }); - } - - return NextResponse.json({ success: true, updated: assetIds.length }); - */ -} diff --git a/wolai-frontend/src/app/api/media/ocr/route.ts b/wolai-frontend/src/app/api/media/ocr/route.ts deleted file mode 100644 index 9c81acf8..00000000 --- a/wolai-frontend/src/app/api/media/ocr/route.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { NextResponse } from "next/server"; -import { getMnoteRuntimeConfig } from "@/lib/runtime-config"; -import { isConvexEnabled } from "@/lib/convex/enabled"; - -export async function POST(request: Request) { - if (isConvexEnabled()) { - // 说明:OCR 链路目前仍依赖 Supabase JWT/表结构,迁移阶段先显式禁用,避免 UI/接口误用。 - return NextResponse.json({ error: "Convex 模式暂不支持 OCR" }, { status: 501 }); - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - - /* - const supabase = await createSupabaseRouteClient(); - const { - data: { session }, - } = await supabase.auth.getSession(); - - if (!session) { - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - - const { assetId } = (await request.json()) as { assetId?: string }; - if (!assetId) { - return NextResponse.json({ error: "缺少 assetId" }, { status: 400 }); - } - - const { error } = await supabase - .from("media_assets") - .update({ ocr_status: "processing" }) - .eq("id", assetId) - .is("deleted_at", null) - .limit(1); - - if (error) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } - - // 说明:同 /api/backend/health,优先走内网/本机后端,避免 Node fetch 被 FRP 自签证书拦截。 - const cfg = getMnoteRuntimeConfig(); - const backendUrl = - process.env.BACKEND_INTERNAL_URL || process.env.BACKEND_URL || cfg.backendUrl; - if (backendUrl) { - void fetch(`${backendUrl}/api/v1/tasks/media-ocr`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ asset_id: assetId }), - }).catch((err) => { - console.warn("触发后端 OCR 失败", err); - }); - } - - return NextResponse.json({ ok: true }); - */ -} diff --git a/wolai-frontend/src/app/api/media/purge/route.test.ts b/wolai-frontend/src/app/api/media/purge/route.test.ts deleted file mode 100644 index b71dbfe5..00000000 --- a/wolai-frontend/src/app/api/media/purge/route.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const mockIsConvexEnabled = vi.fn(() => true); -const mockRequireAuthContext = vi.fn(); -const mockGetConvexAuthedHttpClient = vi.fn(); -const mockBuildDocumentBridgeContext = vi.fn(); -const mockBuildDocumentCommandEnvelope = vi.fn(); -const mockResolveRustBridgeCommandPlan = vi.fn(); -const mockExecuteRustBridgeMutationTransport = vi.fn(); -const mockRecordRustBridgeCommandArtifacts = vi.fn(); - -vi.mock("@/lib/convex/enabled", () => ({ - isConvexEnabled: () => mockIsConvexEnabled(), -})); - -vi.mock("@/lib/auth/authContext", () => ({ - HttpError: class HttpError extends Error { - status: number; - - constructor(status: number, message: string) { - super(message); - this.status = status; - } - }, - requireAuthContext: () => mockRequireAuthContext(), -})); - -vi.mock("@/lib/convex/api", () => ({ - api: { - mediaAssets: { - listByIds: "mediaAssets:listByIds", - purgeById: "mediaAssets:purgeById", - }, - }, -})); - -vi.mock("@/lib/convex/server", () => ({ - getConvexAuthedHttpClient: () => mockGetConvexAuthedHttpClient(), -})); - -vi.mock("@/lib/documents/bridge", () => ({ - buildDocumentBridgeContext: (...args: unknown[]) => mockBuildDocumentBridgeContext(...args), - buildDocumentCommandEnvelope: (...args: unknown[]) => mockBuildDocumentCommandEnvelope(...args), -})); - -vi.mock("@/lib/documents/rust-runtime", () => ({ - resolveRustBridgeCommandPlan: (...args: unknown[]) => mockResolveRustBridgeCommandPlan(...args), - executeRustBridgeMutationTransport: (...args: unknown[]) => - mockExecuteRustBridgeMutationTransport(...args), - recordRustBridgeCommandArtifacts: (...args: unknown[]) => - mockRecordRustBridgeCommandArtifacts(...args), -})); - -describe("/api/media/purge route", () => { - beforeEach(() => { - vi.resetModules(); - mockIsConvexEnabled.mockReset().mockReturnValue(true); - mockRequireAuthContext.mockReset().mockResolvedValue({ userId: "user_1" }); - mockBuildDocumentBridgeContext.mockReset().mockResolvedValue({ - requestId: "req_1", - traceId: "trace_1", - workspaceId: "ws_1", - actor: { actorType: "user", actorId: "user_1", sessionId: null }, - source: { channel: "next-route", client: "vitest" }, - tenantId: null, - deploymentId: null, - projectId: null, - authToken: null, - idempotencyKey: null, - validateOnly: false, - dryRun: false, - }); - mockBuildDocumentCommandEnvelope.mockReset().mockImplementation((input: unknown) => ({ - ...(input as Record), - commandId: "cmd_asset_purge", - idempotencyKey: null, - })); - mockResolveRustBridgeCommandPlan.mockReset().mockResolvedValue({ - kind: "command", - commandName: "tree.resource.purge", - commandId: "cmd_asset_purge", - functionName: "mediaAssets:purgeById", - argsJson: { - domainEventPlan: { eventType: "tree.resource.purged" }, - }, - }); - mockExecuteRustBridgeMutationTransport.mockReset().mockResolvedValue({ ok: true, deleted: 1 }); - mockRecordRustBridgeCommandArtifacts.mockReset().mockResolvedValue(undefined); - }); - - it("应通过正式 tree.resource.purge 命令执行附件永久删除", async () => { - const client = { - query: vi.fn(async (name: string, args: Record) => { - expect(name).toBe("mediaAssets:listByIds"); - expect(args).toEqual({ userId: "user_1", ids: ["asset_1"] }); - return [{ id: "asset_1", workspace_id: "ws_1", document_id: "doc_1" }]; - }), - mutation: vi.fn(), - }; - mockGetConvexAuthedHttpClient.mockResolvedValue(client); - - const { POST } = await import("./route"); - const response = await POST( - new Request("http://localhost/api/media/purge", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ assetId: "asset_1" }), - }), - ); - - expect(response.status).toBe(200); - expect(await response.json()).toMatchObject({ success: true, ok: true, deleted: 1 }); - expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( - expect.objectContaining({ - name: "tree.resource.purge", - payload: { - resourceKind: "file", - assetId: "asset_1", - }, - target: { - workspaceId: "ws_1", - pageId: "doc_1", - }, - }), - ); - expect(mockExecuteRustBridgeMutationTransport).toHaveBeenCalledWith( - expect.objectContaining({ - client, - plan: expect.objectContaining({ - commandName: "tree.resource.purge", - functionName: "mediaAssets:purgeById", - }), - }), - ); - expect(mockRecordRustBridgeCommandArtifacts).toHaveBeenCalledTimes(1); - expect(client.mutation).not.toHaveBeenCalled(); - }); -}); diff --git a/wolai-frontend/src/app/api/media/purge/route.ts b/wolai-frontend/src/app/api/media/purge/route.ts deleted file mode 100644 index a4c19ee7..00000000 --- a/wolai-frontend/src/app/api/media/purge/route.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { HttpError, requireAuthContext } from "@/lib/auth/authContext"; -import { api } from "@/lib/convex/api"; -import { getConvexAuthedHttpClient } from "@/lib/convex/server"; -import { - buildDocumentBridgeContext, - buildDocumentCommandEnvelope, -} from "@/lib/documents/bridge"; -import { - executeRustBridgeMutationTransport, - recordRustBridgeCommandArtifacts, - resolveRustBridgeCommandPlan, -} from "@/lib/documents/rust-runtime"; - -export const dynamic = "force-dynamic"; - -interface PurgePayload { - assetId?: string; -} - -export async function POST(request: Request) { - if (isConvexEnabled()) { - let auth; - try { - auth = await requireAuthContext(); - } catch (err) { - if (err instanceof HttpError) { - return NextResponse.json({ error: "未登录" }, { status: err.status }); - } - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - - const { assetId }: PurgePayload = await request.json().catch(() => ({})); - if (!assetId) { - return NextResponse.json({ error: "缺少 assetId" }, { status: 400 }); - } - - const client = await getConvexAuthedHttpClient(); - const assets = (await client.query(api.mediaAssets.listByIds, { - userId: auth.userId, - ids: [assetId], - })) as any[]; - const asset = assets?.[0]; - if (!asset) { - return NextResponse.json({ error: "未找到附件" }, { status: 404 }); - } - const workspaceId = String(asset.workspace_id ?? "").trim(); - if (!workspaceId) { - return NextResponse.json({ error: "缺少资源工作空间" }, { status: 400 }); - } - const documentId = String(asset.document_id ?? "").trim(); - const context = await buildDocumentBridgeContext({ - request, - workspaceId, - }); - const envelope = buildDocumentCommandEnvelope({ - name: "tree.resource.purge", - payload: { - resourceKind: "file", - assetId, - }, - context, - target: { - workspaceId, - pageId: documentId || undefined, - }, - reason: "media-purge tree.resource.purge", - refs: ["file-tree-resource-command", "media-purge-compat-alias"], - }); - const plan = await resolveRustBridgeCommandPlan({ - context, - envelope, - }); - const res = await executeRustBridgeMutationTransport({ - client: client as never, - plan, - }); - try { - await recordRustBridgeCommandArtifacts({ - context, - envelope, - client: client as never, - plan, - result: res, - }); - } catch (error) { - console.warn("[media.purge] Rust lifecycle bridge artifacts skipped:", error); - } - - return NextResponse.json({ success: true, ...(res as any) }); - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - - /* - const supabase = await createSupabaseRouteClient(); - const { - data: { session }, - } = await supabase.auth.getSession(); - - if (!session) { - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - - const { assetId }: PurgePayload = await request.json().catch(() => ({})); - if (!assetId) { - return NextResponse.json({ error: "缺少 assetId" }, { status: 400 }); - } - - const { data: asset, error: fetchError } = await supabase - .from("media_assets") - .select("id,workspace_id,deleted_at,purged_at") - .eq("id", assetId) - .single(); - - if (fetchError) { - return NextResponse.json({ error: fetchError.message }, { status: 500 }); - } - - if (!asset) { - return NextResponse.json({ error: "未找到附件" }, { status: 404 }); - } - - const { data: membership, error: membershipError } = await supabase - .from("workspace_members") - .select("workspace_id") - .eq("workspace_id", asset.workspace_id) - .eq("user_id", session.user.id) - .limit(1); - - if (membershipError) { - return NextResponse.json({ error: membershipError.message }, { status: 500 }); - } - - if (!membership || membership.length === 0) { - return NextResponse.json({ error: "无权操作该附件" }, { status: 403 }); - } - - if (asset.purged_at) { - return NextResponse.json({ success: true, alreadyPurged: true }); - } - - const { error: updateError } = await supabase - .from("media_assets") - .update({ - deleted_at: makeExpiredDeletedAt(), - deleted_by: session.user.id, - }) - .eq("id", assetId); - - if (updateError) { - return NextResponse.json({ error: updateError.message }, { status: 500 }); - } - - return NextResponse.json({ success: true }); - */ -} diff --git a/wolai-frontend/src/app/api/media/sign/route.ts b/wolai-frontend/src/app/api/media/sign/route.ts deleted file mode 100644 index 047ebe6a..00000000 --- a/wolai-frontend/src/app/api/media/sign/route.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { api } from "@/lib/convex/api"; -import { HttpError } from "@/lib/auth/authContext"; -import { getAuthedConvexClient } from "@/lib/convex/route"; -import { maybeProxyForBrowserUrl } from "@/lib/url/proxyForBrowser"; - -export const dynamic = "force-dynamic"; - -export async function GET(request: Request) { - if (isConvexEnabled()) { - let auth; - let client; - try { - const authed = await getAuthedConvexClient(); - auth = authed.auth; - client = authed.client; - } catch (err) { - if (err instanceof HttpError) { - return NextResponse.json({ error: "未登录" }, { status: err.status }); - } - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - - const { searchParams } = new URL(request.url); - const assetId = searchParams.get("assetId"); - - if (!assetId) { - return NextResponse.json({ error: "缺少 assetId" }, { status: 400 }); - } - - const asset = await client.query(api.mediaAssets.getById, { userId: auth.userId, id: assetId }); - if (!asset) { - return NextResponse.json({ error: "资源不存在" }, { status: 404 }); - } - - const refreshed = await client.mutation(api.mediaAssets.refreshUrl, { userId: auth.userId, id: assetId }); - const signedUrl = (refreshed as { signedUrl?: string | null } | null)?.signedUrl ?? null; - if (!signedUrl) { - return NextResponse.json({ error: "生成签名链接失败" }, { status: 500 }); - } - - return NextResponse.json({ - signedUrl: maybeProxyForBrowserUrl(request, signedUrl), - asset: { - id: asset.id, - file_name: asset.file_name, - mime_type: asset.mime_type, - file_size: asset.file_size, - storage_id: (asset as any)?.storage_id ?? null, - updated_at: (asset as any)?.updated_at ?? null, - }, - }); - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); -} diff --git a/wolai-frontend/src/app/api/media/signed-url/route.ts b/wolai-frontend/src/app/api/media/signed-url/route.ts deleted file mode 100644 index 282e6aa1..00000000 --- a/wolai-frontend/src/app/api/media/signed-url/route.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { HttpError, requireAuthContext } from "@/lib/auth/authContext"; -import { maybeProxyForBrowserUrl } from "@/lib/url/proxyForBrowser"; - -export const dynamic = "force-dynamic"; - -export async function GET(request: Request) { - if (isConvexEnabled()) { - try { - await requireAuthContext(); - } catch (err) { - if (err instanceof HttpError) { - return NextResponse.json({ error: "未登录" }, { status: err.status }); - } - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - - const { searchParams } = new URL(request.url); - const fileUrl = searchParams.get("fileUrl"); - if (!fileUrl) { - return NextResponse.json({ error: "缺少 fileUrl" }, { status: 400 }); - } - - // 说明:Convex + MinIO 模式下,这个接口目前主要用于“外链文件”走 ONLYOFFICE 的场景。 - // 由于外链本身已经是可访问的 URL,这里只做最小透传。 - return NextResponse.json({ signedUrl: maybeProxyForBrowserUrl(request, fileUrl) }); - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); -} diff --git a/wolai-frontend/src/app/api/media/upload/route.test.ts b/wolai-frontend/src/app/api/media/upload/route.test.ts deleted file mode 100644 index 9e74caef..00000000 --- a/wolai-frontend/src/app/api/media/upload/route.test.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const mockIsConvexEnabled = vi.fn(() => true); -const mockRequireAuthContext = vi.fn(); -const mockGetConvexAuthedHttpClient = vi.fn(); -const mockBuildDocumentBridgeContext = vi.fn(); -const mockBuildDocumentCommandEnvelope = vi.fn(); -const mockResolveRustBridgeCommandPlan = vi.fn(); -const mockRecordRustBridgeCommandArtifacts = vi.fn(); -const mockMaterializeRustTreeStreamDelta = vi.fn(); -const mockMaterializeRustTreeDomainEventPlan = vi.fn(); -const mockReadRustTreeDomainEventType = vi.fn(); -const mockRecordBridgeCommandArtifacts = vi.fn(); - -vi.mock("@/lib/convex/enabled", () => ({ - isConvexEnabled: () => mockIsConvexEnabled(), -})); - -vi.mock("@/lib/auth/authContext", () => ({ - HttpError: class HttpError extends Error { - status: number; - - constructor(status: number, message: string) { - super(message); - this.status = status; - } - }, - requireAuthContext: () => mockRequireAuthContext(), -})); - -vi.mock("@/lib/convex/api", () => ({ - api: { - mediaAssets: { - generateUploadUrl: "mediaAssets:generateUploadUrl", - createWithStorage: "mediaAssets:createWithStorage", - }, - }, -})); - -vi.mock("@/lib/convex/server", () => ({ - getConvexAuthedHttpClient: () => mockGetConvexAuthedHttpClient(), -})); - -vi.mock("@/lib/documents/bridge", () => ({ - buildDocumentBridgeContext: (...args: unknown[]) => mockBuildDocumentBridgeContext(...args), - buildDocumentCommandEnvelope: (...args: unknown[]) => mockBuildDocumentCommandEnvelope(...args), -})); - -vi.mock("@/lib/documents/rust-runtime", () => ({ - resolveRustBridgeCommandPlan: (...args: unknown[]) => mockResolveRustBridgeCommandPlan(...args), - recordRustBridgeCommandArtifacts: (...args: unknown[]) => - mockRecordRustBridgeCommandArtifacts(...args), - materializeRustTreeStreamDelta: (...args: unknown[]) => - mockMaterializeRustTreeStreamDelta(...args), - materializeRustTreeDomainEventPlan: (...args: unknown[]) => - mockMaterializeRustTreeDomainEventPlan(...args), - readRustTreeDomainEventType: (...args: unknown[]) => mockReadRustTreeDomainEventType(...args), -})); - -vi.mock("@/lib/documents/bridge-log", () => ({ - recordBridgeCommandArtifacts: (...args: unknown[]) => mockRecordBridgeCommandArtifacts(...args), -})); - -vi.mock("@/lib/url/proxyForBrowser", () => ({ - maybeProxyForBrowserUrl: (_request: Request, url: string) => url, -})); - -describe("/api/media/upload route", () => { - beforeEach(() => { - vi.resetModules(); - vi.restoreAllMocks(); - mockIsConvexEnabled.mockReset().mockReturnValue(true); - mockRequireAuthContext.mockReset().mockResolvedValue({ - userId: "user_1", - }); - mockBuildDocumentBridgeContext.mockReset().mockResolvedValue({ - requestId: "req_1", - traceId: "trace_1", - workspaceId: "ws_1", - actor: { actorType: "user", actorId: "user_1", sessionId: null }, - source: { channel: "next-route", client: "vitest" }, - tenantId: null, - deploymentId: null, - projectId: null, - authToken: null, - idempotencyKey: null, - validateOnly: false, - dryRun: false, - }); - mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => ({ - ...(input as Record), - commandId: "cmd_upload_asset", - idempotencyKey: null, - })); - mockResolveRustBridgeCommandPlan.mockReset().mockResolvedValue({ - kind: "command", - commandName: "tree.resource.upload", - commandId: "cmd_upload_asset", - functionName: "mediaAssets:createWithStorage", - workspaceId: "ws_1", - requestId: "req_1", - traceId: "trace_1", - actorId: "user_1", - idempotencyKey: null, - payloadJson: "{}", - argsJson: { - domainEventPlan: { - family: "tree", - schema: "mnote.tree.domain_event", - schemaVersion: 1, - eventType: "tree.resource.uploaded", - }, - assetId: "asset_upload_1", - workspaceId: "ws_1", - targetDocumentId: "doc_target", - targetSubPath: "mindmaps/mind_1", - fileName: "demo.pdf", - fileSize: 7, - mimeType: "application/pdf", - assetType: "file", - resourceUploadPlan: { - action: "upload", - assetId: "asset_upload_1", - workspaceId: "ws_1", - targetDocumentId: "doc_target", - targetSubPath: "mindmaps/mind_1", - fileName: "demo.pdf", - fileSize: 7, - mimeType: "application/pdf", - assetType: "file", - }, - }, - }); - mockMaterializeRustTreeStreamDelta.mockReset().mockReturnValue({ - op: "upsert_assets", - upsertAssets: [{ id: "asset_upload_1" }], - }); - mockMaterializeRustTreeDomainEventPlan.mockReset().mockReturnValue({ - family: "tree", - schema: "mnote.tree.domain_event", - schemaVersion: 1, - eventType: "tree.resource.uploaded", - streamDelta: { - op: "upsert_assets", - upsertAssets: [{ id: "asset_upload_1" }], - }, - }); - mockReadRustTreeDomainEventType.mockReset().mockReturnValue("tree.resource.uploaded"); - mockRecordRustBridgeCommandArtifacts.mockReset().mockResolvedValue(null); - mockRecordBridgeCommandArtifacts.mockReset().mockResolvedValue(undefined); - }); - - it("上传应通过 tree.resource.upload plan 写入资源元数据并记录资源 delta artifact", async () => { - const createdAsset = { - id: "asset_upload_1", - workspace_id: "ws_1", - document_id: "doc_target", - asset_type: "file", - file_url: "/files/demo.pdf", - thumbnail_url: "/files/demo.pdf", - file_name: "demo.pdf", - file_size: 7, - mime_type: "application/pdf", - created_at: "2026-04-26T00:00:00Z", - updated_at: "2026-04-26T00:00:00Z", - }; - const client = { - mutation: vi.fn(async (name: string, args: Record) => { - if (name === "mediaAssets:generateUploadUrl") { - expect(args).toEqual({ userId: "user_1" }); - return "https://convex.test/upload"; - } - if (name === "mediaAssets:createWithStorage") { - expect(args).toMatchObject({ - userId: "user_1", - storageId: "storage_1", - targetSubPath: "mindmaps/mind_1", - resourceUploadPlan: { - action: "upload", - assetId: "asset_upload_1", - targetDocumentId: "doc_target", - }, - asset: { - id: "asset_upload_1", - workspace_id: "ws_1", - document_id: "doc_target", - file_name: "demo.pdf", - }, - }); - return createdAsset; - } - return null; - }), - }; - mockGetConvexAuthedHttpClient.mockResolvedValue(client); - vi.spyOn(globalThis, "fetch").mockResolvedValue( - new Response(JSON.stringify({ storageId: "storage_1" }), { - status: 200, - headers: { "content-type": "application/json" }, - }), - ); - - const { POST } = await import("./route"); - const formData = new FormData(); - formData.append("file", new Blob(["content"], { type: "application/pdf" }), "demo.pdf"); - formData.set("workspaceId", "ws_1"); - formData.set("documentId", "doc_target"); - formData.set("mindmapId", "mind_1"); - - const response = await POST( - new Request("http://localhost/api/media/upload", { - method: "POST", - body: formData, - }), - ); - - expect(response.status).toBe(200); - expect(await response.json()).toMatchObject({ - asset: { - id: "asset_upload_1", - document_id: "doc_target", - }, - mindmapUrl: "asset:asset_upload_1", - }); - expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( - expect.objectContaining({ - name: "tree.resource.upload", - payload: expect.objectContaining({ - workspaceId: "ws_1", - targetDocumentId: "doc_target", - targetSubPath: "mindmaps/mind_1", - fileName: expect.any(String), - }), - }), - ); - expect(mockRecordRustBridgeCommandArtifacts).toHaveBeenCalledWith( - expect.objectContaining({ - envelope: expect.objectContaining({ - name: "tree.resource.upload", - }), - plan: expect.objectContaining({ - commandName: "tree.resource.upload", - functionName: "mediaAssets:createWithStorage", - }), - result: expect.objectContaining({ - items: [expect.objectContaining({ id: "asset_upload_1" })], - }), - }), - ); - expect(mockRecordBridgeCommandArtifacts).not.toHaveBeenCalled(); - }); -}); diff --git a/wolai-frontend/src/app/api/media/upload/route.ts b/wolai-frontend/src/app/api/media/upload/route.ts deleted file mode 100644 index 906ee616..00000000 --- a/wolai-frontend/src/app/api/media/upload/route.ts +++ /dev/null @@ -1,270 +0,0 @@ -import { NextResponse } from "next/server"; -import type { MediaAsset } from "@/types/media"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { HttpError, requireAuthContext } from "@/lib/auth/authContext"; -import { api } from "@/lib/convex/api"; -import { getConvexAuthedHttpClient } from "@/lib/convex/server"; -import { maybeProxyForBrowserUrl } from "@/lib/url/proxyForBrowser"; -import { - buildDocumentBridgeContext, - buildDocumentCommandEnvelope, -} from "@/lib/documents/bridge"; -import { - recordRustBridgeCommandArtifacts, - resolveRustBridgeCommandPlan, -} from "@/lib/documents/rust-runtime"; - -export const dynamic = "force-dynamic"; - -const resolveAssetType = (mime: string): "image" | "video" | "audio" | "file" => { - if (mime.startsWith("image/")) return "image"; - if (mime.startsWith("video/")) return "video"; - if (mime.startsWith("audio/")) return "audio"; - return "file"; -}; - -function isUploadFile(value: FormDataEntryValue | null): value is File { - return Boolean( - value && - typeof value === "object" && - (typeof File === "undefined" || value instanceof File || "arrayBuffer" in value) && - typeof (value as File).arrayBuffer === "function" && - typeof (value as File).name === "string", - ); -} - -function readOptionalStringArg(args: Record, key: string): string | null { - const value = args[key]; - return typeof value === "string" && value.trim() ? value.trim() : null; -} - -function readNumberArg(args: Record, key: string): number | null { - const value = args[key]; - return typeof value === "number" && Number.isFinite(value) ? value : null; -} - -export async function POST(request: Request) { - if (isConvexEnabled()) { - let auth; - try { - auth = await requireAuthContext(); - } catch (err) { - if (err instanceof HttpError) { - return NextResponse.json({ error: "未登录" }, { status: err.status }); - } - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - - const formData = await request.formData(); - const file = formData.get("file"); - const workspaceId = String(formData.get("workspaceId") ?? ""); - const documentId = String(formData.get("documentId") ?? ""); - const mindmapIdRaw = String(formData.get("mindmapId") ?? "").trim(); - - if (!isUploadFile(file) || !workspaceId || !documentId) { - return NextResponse.json({ error: "缺少必要参数" }, { status: 400 }); - } - - try { - const arrayBuffer = await file.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - const assetId = - typeof crypto.randomUUID === "function" - ? crypto.randomUUID() - : `${Date.now()}_${Math.random().toString(16).slice(2)}`; - const assetType = resolveAssetType(file.type || ""); - const client = await getConvexAuthedHttpClient(); - const targetSubPath = mindmapIdRaw ? `mindmaps/${mindmapIdRaw}` : undefined; - const context = await buildDocumentBridgeContext({ - request, - workspaceId, - }); - const envelope = buildDocumentCommandEnvelope({ - name: "tree.resource.upload", - payload: { - assetId, - workspaceId, - targetDocumentId: documentId, - targetSubPath, - fileName: file.name || null, - fileSize: file.size, - mimeType: file.type || null, - assetType, - }, - context, - target: { - workspaceId, - pageId: documentId, - }, - reason: "media-upload tree.resource.upload", - refs: ["file-tree-resource-upload"], - }); - const plan = await resolveRustBridgeCommandPlan({ - context, - envelope, - }); - - // 1) 获取 Convex 的上传 URL(短时有效) - const uploadUrl = await client.mutation(api.mediaAssets.generateUploadUrl, { userId: auth.userId }); - if (!uploadUrl || typeof uploadUrl !== "string") { - return NextResponse.json({ error: "获取上传地址失败" }, { status: 500 }); - } - - // 2) 上传文件到 Convex Files - const uploadRes = await fetch(uploadUrl, { - method: "POST", - headers: { "Content-Type": file.type || "application/octet-stream" }, - body: buffer, - }); - if (!uploadRes.ok) { - const text = await uploadRes.text().catch(() => ""); - return NextResponse.json({ error: `上传到 Convex 失败:${uploadRes.status} ${text}` }, { status: 500 }); - } - const uploadJson = (await uploadRes.json().catch(() => null)) as { storageId?: string } | null; - const storageId = uploadJson?.storageId ?? ""; - if (!storageId) { - return NextResponse.json({ error: "上传到 Convex 失败:缺少 storageId" }, { status: 500 }); - } - - // 3) 写入 Convex 的 media_assets 元数据(并记录 storageId) - const created = await client.mutation(api.mediaAssets.createWithStorage, { - userId: auth.userId, - storageId: storageId as any, - targetSubPath: readOptionalStringArg(plan.argsJson, "targetSubPath"), - resourceUploadPlan: - plan.argsJson.resourceUploadPlan && typeof plan.argsJson.resourceUploadPlan === "object" - ? plan.argsJson.resourceUploadPlan - : undefined, - asset: { - id: readOptionalStringArg(plan.argsJson, "assetId") ?? assetId, - workspace_id: readOptionalStringArg(plan.argsJson, "workspaceId") ?? workspaceId, - document_id: readOptionalStringArg(plan.argsJson, "targetDocumentId") ?? documentId, - asset_type: readOptionalStringArg(plan.argsJson, "assetType") ?? assetType, - file_name: readOptionalStringArg(plan.argsJson, "fileName"), - file_size: readNumberArg(plan.argsJson, "fileSize"), - mime_type: readOptionalStringArg(plan.argsJson, "mimeType"), - }, - }); - try { - await recordRustBridgeCommandArtifacts({ - context, - envelope, - client: client as never, - plan, - result: { items: [created] }, - }); - } catch (error) { - console.warn("[media.upload] Rust bridge artifacts skipped:", error); - } - - const asset = created as unknown as MediaAsset; - const safeAsset = { - ...(asset as any), - file_url: maybeProxyForBrowserUrl(request, String((asset as any)?.file_url ?? "")), - thumbnail_url: maybeProxyForBrowserUrl(request, String((asset as any)?.thumbnail_url ?? (asset as any)?.file_url ?? "")), - } as MediaAsset; - - return NextResponse.json({ - asset: safeAsset, - mindmapUrl: `asset:${asset.id}`, - }); - } catch (error) { - console.error(error); - return NextResponse.json({ error: "上传失败" }, { status: 500 }); - } - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - - /* - const supabase = await createSupabaseRouteClient(); - const { - data: { session }, - } = await supabase.auth.getSession(); - - if (!session) { - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - - const formData = await request.formData(); - const file = formData.get("file"); - const workspaceId = String(formData.get("workspaceId") ?? ""); - const documentId = String(formData.get("documentId") ?? ""); - const mindmapIdRaw = String(formData.get("mindmapId") ?? "").trim(); - - if (!(file instanceof File) || !workspaceId || !documentId) { - return NextResponse.json({ error: "缺少必要参数" }, { status: 400 }); - } - - try { - const arrayBuffer = await file.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - const extension = extname(file.name || "").replace(/\s+/g, ""); - const uniqueId = typeof crypto.randomUUID === "function" ? crypto.randomUUID() : Math.random().toString(36).slice(2); - // 与 /api/media/batch 的 move/rename 规则对齐:放到 workspaceId/documentId 下 - const mindmapId = - mindmapIdRaw && /^[a-zA-Z0-9_-]{1,128}$/.test(mindmapIdRaw) ? mindmapIdRaw : ""; - const subdir = mindmapId ? `mindmaps/${mindmapId}` : ""; - const path = `${workspaceId}/${documentId}${subdir ? `/${subdir}` : ""}/${Date.now()}-${uniqueId}${extension}`; - const assetType = resolveAssetType(file.type || ""); - - const { error: uploadError } = await supabase.storage.from(DOC_BUCKET).upload(path, buffer, { - contentType: file.type, - upsert: false, - }); - - if (uploadError) { - return NextResponse.json({ error: uploadError.message }, { status: 500 }); - } - - // 为私有桶生成临时访问链接(7 天);前端可在需要时通过 /api/media/signed-url 刷新 - // 对于图片,使用高质量参数以获得更好的显示效果 - let signedUrl = ""; - if (assetType === "image") { - const { data: signed } = await supabase.storage.from(DOC_BUCKET).createSignedUrl(path, 60 * 60 * 24 * 7, { - transform: { - quality: 95, // 高质量 - format: "origin", // 保持原始格式 - }, - }); - signedUrl = signed?.signedUrl ?? ""; - } else { - const { data: signed } = await supabase.storage.from(DOC_BUCKET).createSignedUrl(path, 60 * 60 * 24 * 7); - signedUrl = signed?.signedUrl ?? ""; - } - - signedUrl = rewriteToPublicOrigin(signedUrl, getMnoteRuntimeConfig().supabaseUrl); - - const { data: asset, error } = await supabase - .from("media_assets") - .insert({ - workspace_id: workspaceId, - document_id: documentId, - file_url: signedUrl, - thumbnail_url: signedUrl, - bucket: DOC_BUCKET, - storage_path: path, - asset_type: assetType, - file_name: file.name, - file_size: file.size, - mime_type: file.type, - created_by: session.user.id, - }) - .select("*") - .single(); - - if (error) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } - - // 返回 asset 和一个特殊的 asset:id 格式用于存储在思维导图中 - return NextResponse.json({ - asset: asset as MediaAsset, - mindmapUrl: `asset:${asset.id}`, - }); - } catch (error) { - console.error(error); - return NextResponse.json({ error: "上传失败" }, { status: 500 }); - } - */ -} diff --git a/wolai-frontend/src/app/api/mindmap-ai/agent/route.ts b/wolai-frontend/src/app/api/mindmap-ai/agent/route.ts deleted file mode 100644 index 2afebbae..00000000 --- a/wolai-frontend/src/app/api/mindmap-ai/agent/route.ts +++ /dev/null @@ -1,840 +0,0 @@ -import { NextResponse } from "next/server"; -import { loadOnlineAiConfig } from "@/lib/ai/onlineAiConfig"; -import { loadLocalAiConfig } from "@/lib/ai/localAiConfig"; -import { openAiCompatibleChat, tryExtractJsonObject } from "@/lib/ai/openaiCompatibleChat"; -import { buildDocumentBridgeContextWithActor } from "@/lib/documents/bridge"; -import { executeRustBridgeTool } from "@/lib/documents/rust-runtime"; -import { readMindmapLocal, writeMindmapLocal } from "@/lib/mindmap/mindmapLocalStore"; - -export const dynamic = "force-dynamic"; - -type AgentMessage = { role: "user" | "assistant"; content: string }; - -type AgentAttachment = { - id: string; - title: string; - fileUrl: string; - mimeType?: string | null; -}; - -type RequestPayload = { - documentId: string; - mindmapId: string; - selectedUids?: string[]; - messages: AgentMessage[]; - attachments?: AgentAttachment[]; - toolChoice?: { mode: "auto" | "manual"; tools?: string[] }; - options?: { searxng?: boolean; ai?: { provider?: "online" | "local"; model?: string } }; -}; - -type ToolName = - | "mindmap_get" - | "mindmap_get_subtree" - | "search_web" - | "mindmap_apply_ops" - | "pdf_replace_mindmap"; - -type ToolCall = - | { type: "tool"; tool: ToolName; args: Record } - | { type: "final"; message: string; summary?: string }; - -type SearxResult = { title: string; url: string; snippet?: string; engine?: string }; - -const defaultMindmapData: MindmapTreeNode = { - data: { text: "中心主题" }, - children: [], -}; - -const safeUrlOrNull = (value: unknown) => { - const s = typeof value === "string" ? value.trim() : ""; - if (!s) return null; - try { - const u = new URL(s); - if (u.protocol !== "http:" && u.protocol !== "https:") return null; - return u.toString(); - } catch { - return null; - } -}; - -const searchSearxng = async (q: string, count = 6): Promise => { - const base = (process.env.SEARXNG_BASE_URL ?? "http://127.0.0.1:8889").replace(/\/+$/, ""); - const token = (process.env.SEARXNG_API_TOKEN ?? "").trim(); - const url = `${base}/search?q=${encodeURIComponent(q)}&format=json&language=zh-CN&categories=general&safesearch=1`; - - const tryFetch = async (headers: Record) => { - const res = await fetch(url, { headers, method: "GET" }); - if (!res.ok) return null; - return (await res.json().catch(() => null)) as any; - }; - - let json: any = null; - if (token) { - json = (await tryFetch({ Authorization: `Bearer ${token}` })) ?? (await tryFetch({ "X-API-Key": token })) ?? null; - } - if (!json) json = await tryFetch({}); - - const results = Array.isArray(json?.results) ? json.results : []; - return results - .map((r: any) => ({ - title: String(r?.title ?? "").trim(), - url: String(r?.url ?? "").trim(), - snippet: String(r?.content ?? r?.snippet ?? "").trim(), - engine: String(r?.engine ?? "").trim(), - })) - .filter((r: SearxResult) => r.title && safeUrlOrNull(r.url)) - .slice(0, Math.max(1, Math.min(10, count))); -}; - -const walkSummaries = (root: MindmapTreeNode, maxNodes = 120) => { - const list: Array<{ uid: string; text: string; parentUid: string | null; depth: number; childCount: number }> = []; - const queue: Array<{ node: MindmapTreeNode; parentUid: string | null; depth: number }> = [ - { node: root, parentUid: null, depth: 0 }, - ]; - - while (queue.length && list.length < maxNodes) { - const { node, parentUid, depth } = queue.shift()!; - const uid = String(node?.data?.uid || ""); - const text = String(node?.data?.text ?? "").replace(/<[^>]+>/g, "").trim(); - const children = Array.isArray(node?.children) ? node.children : []; - if (uid) { - list.push({ uid, text, parentUid, depth, childCount: children.length }); - } - children.forEach((c) => queue.push({ node: c, parentUid: uid || parentUid, depth: depth + 1 })); - } - return list; -}; - -const findNodeByUid = (root: MindmapTreeNode, uid: string): MindmapTreeNode | null => { - const target = String(uid || ""); - if (!target) return null; - const walk = (n: MindmapTreeNode): MindmapTreeNode | null => { - if (String(n?.data?.uid || "") === target) return n; - const children = Array.isArray(n.children) ? n.children : []; - for (const c of children) { - const hit = walk(c); - if (hit) return hit; - } - return null; - }; - return walk(root); -}; - -const summarizeSubtree = (node: MindmapTreeNode, depthLimit = 2, maxNodes = 60) => { - const list: Array<{ uid: string; text: string; depth: number; childCount: number }> = []; - const queue: Array<{ node: MindmapTreeNode; depth: number }> = [{ node, depth: 0 }]; - while (queue.length && list.length < maxNodes) { - const { node: n, depth } = queue.shift()!; - const uid = String(n?.data?.uid || ""); - const text = String(n?.data?.text ?? "").replace(/<[^>]+>/g, "").trim(); - const children = Array.isArray(n.children) ? n.children : []; - if (uid) list.push({ uid, text, depth, childCount: children.length }); - if (depth < depthLimit) children.forEach((c) => queue.push({ node: c, depth: depth + 1 })); - } - return list; -}; - -const coerceToolCall = (raw: Record): ToolCall | null => { - const type = String((raw as any)?.type ?? ""); - if (type === "final") { - return { type: "final", message: String((raw as any)?.message ?? ""), summary: String((raw as any)?.summary ?? "") || undefined }; - } - if (type === "tool") { - const tool = String((raw as any)?.tool ?? "") as ToolName; - const args = ((raw as any)?.args ?? {}) as Record; - return { type: "tool", tool, args }; - } - return null; -}; - -const normalizeAllowedTools = (payload: RequestPayload): Set => { - const mode = payload.toolChoice?.mode ?? "auto"; - if (mode !== "manual") { - return new Set(["mindmap_get", "mindmap_get_subtree", "search_web", "mindmap_apply_ops", "pdf_replace_mindmap"]); - } - const list = Array.isArray(payload.toolChoice?.tools) ? payload.toolChoice!.tools! : []; - const allowed = new Set(); - for (const x of list) { - const t = String(x || "") as ToolName; - if (t === "mindmap_get" || t === "mindmap_get_subtree" || t === "search_web" || t === "mindmap_apply_ops" || t === "pdf_replace_mindmap") { - allowed.add(t); - } - } - // 手动模式但未选择:默认仍允许读导图(避免完全不可用) - if (allowed.size === 0) allowed.add("mindmap_get"); - return allowed; -}; - -const refsFromSearx = (items: SearxResult[]): NodeRef[] => - items - .map((r) => ({ - kind: "url" as const, - fileUrl: r.url, - title: r.title, - snippet: r.snippet ? r.snippet.slice(0, 320) : undefined, - })) - .filter((x) => safeUrlOrNull(x.fileUrl)); - -const stripHtml = (value: unknown) => String(value ?? "").replace(/<[^>]+>/g, "").trim(); - -const stableStringify = (value: unknown): string => { - if (value === null) return "null"; - const t = typeof value; - if (t === "string") return JSON.stringify(value); - if (t === "number" || t === "boolean") return String(value); - if (Array.isArray(value)) return `[${value.map((x) => stableStringify(x)).join(",")}]`; - if (t === "object") { - const obj = value as Record; - const keys = Object.keys(obj).sort(); - return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`; - } - return JSON.stringify(String(value)); -}; - -const formatSearchResults = (items: SearxResult[]) => { - const lines: string[] = []; - for (let i = 0; i < Math.min(8, items.length); i += 1) { - const r = items[i]; - const title = String(r.title || "").trim(); - const url = String(r.url || "").trim(); - const snippet = String(r.snippet || "").replace(/\s+/g, " ").trim(); - lines.push(`${i + 1}. ${title || "(无标题)"}\n${url}${snippet ? `\n${snippet.slice(0, 240)}` : ""}`); - } - return lines.join("\n\n").trim(); -}; - -const answerFromSearchResults = async (args: { - cfg: { baseUrl: string; apiKey: string; model: string }; - modelOverride?: string | null; - question: string; - results: SearxResult[]; -}) => { - const q = String(args.question || "").trim(); - const resultsText = formatSearchResults(args.results); - if (!q) return resultsText || "(无问题)"; - - let aiError: string | null = null; - try { - const { text } = await openAiCompatibleChat( - [ - { - role: "system", - content: [ - "你是一个严谨的中文助手。", - "用户提出一个问题,你将基于提供的检索结果回答。", - "要求:", - "- 直接回答问题,给出结论;信息不确定时明确说明需要以官方为准。", - "- 尽量在回答末尾附上 1-3 个最相关链接(原样 URL)。", - "- 不要输出 JSON/不要输出 Markdown 代码块。", - ].join("\n"), - }, - { role: "user", content: `问题:${q}\n\n检索结果:\n${resultsText || "(无结果)"}` }, - ], - { - baseUrl: args.cfg.baseUrl, - apiKey: args.cfg.apiKey, - model: (args.modelOverride ?? "").trim() || args.cfg.model, - timeoutMs: 55_000, - maxTokens: 1200, - }, - ); - const out = String(text ?? "").trim(); - if (out) return out; - } catch (e) { - aiError = e instanceof Error ? e.message : String(e); - } - - const prefix = aiError ? `(在线 AI 总结失败:${aiError})\n\n` : ""; - return `${prefix}${resultsText || "(未检索到结果)"}`.trim(); -}; - -const buildFallbackExpandOps = async (args: { - mindmap: MindmapTreeNode; - targetUid: string; - instruction: string; - useSearx: boolean; -}): Promise => { - const targetUid = String(args.targetUid || "").trim(); - const instruction = String(args.instruction || "").trim(); - const ops: MindmapOp[] = []; - const seen = new Set(); - - const results = args.useSearx && instruction ? await searchSearxng(instruction, 6).catch(() => []) : []; - for (const r of results.slice(0, 6)) { - const title = String(r.title || "").trim(); - const url = safeUrlOrNull(r.url); - const snippet = String(r.snippet || "").replace(/\s+/g, " ").trim(); - if (!title || !url) continue; - const key = `${title}|${url}`; - if (seen.has(key)) continue; - seen.add(key); - - const explain = snippet ? snippet.slice(0, 46) : "相关资料(请核验)。"; - const text = `${title.slice(0, 20)}:${explain}`; - ops.push({ - op: "addChild", - parentUid: targetUid, - node: { - text, - hyperlink: url, - refs: [ - { - kind: "url", - fileUrl: url, - title, - snippet: snippet ? snippet.slice(0, 300) : undefined, - }, - ], - }, - }); - if (ops.length >= 6) break; - } - - // 兜底:确保至少 3 条可见的“内容型”节点(不是纯链接) - const base = instruction ? instruction.slice(0, 10) : stripHtml(args.mindmap?.data?.text) || "要点"; - let i = 1; - while (ops.length < 3) { - ops.push({ - op: "addChild", - parentUid: targetUid, - node: { - text: `${base}(要点)${i}:请补充定义、条件、例子与注意事项。`, - }, - }); - i += 1; - } - - return ops; -}; - -export async function POST(request: Request) { - const payload = (await request.json().catch(() => null)) as RequestPayload | null; - if (!payload?.documentId || !payload?.mindmapId || !Array.isArray(payload.messages) || payload.messages.length === 0) { - return NextResponse.json({ error: "缺少 documentId/mindmapId/messages" }, { status: 400 }); - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - - /* - const supabase = await createSupabaseRouteClient(); - const { - data: { session }, - } = await supabase.auth.getSession(); - if (!session) { - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - - const { data: doc, error: docError } = await supabase - .from("documents") - .select("id,title,workspace_id,mindmap_data") - .eq("id", payload.documentId) - .eq("user_id", session.user.id) - .maybeSingle(); - if (docError) { - return NextResponse.json({ error: docError.message }, { status: 400 }); - } - if (!doc) { - return NextResponse.json({ error: "页面不存在" }, { status: 404 }); - } - - const local = await readMindmapLocal(payload.documentId, payload.mindmapId); - let mindmap = (local.ok ? local.data : (doc.mindmap_data ?? defaultMindmapData)) as MindmapTreeNode; - // 补齐 uid,避免后续 addChild 找不到 root uid - mindmap = applyMindmapOps(mindmap, []).data; - - const provider = payload.options?.ai?.provider === "local" ? "local" : "online"; - const modelOverride = String(payload.options?.ai?.model ?? "").trim() || null; - const cfg = - provider === "local" - ? await loadLocalAiConfig().catch(() => null) - : await loadOnlineAiConfig().catch(() => null); - if (!cfg) { - const tip = - provider === "local" - ? "未找到本地 AI 配置(LOCAL_AI_BASE_URL/LOCAL_AI_MODEL 或 ai.local.md / ai-local.md)" - : "未找到在线 AI 配置(ai.md 或 ONLINE_AI_* 环境变量)"; - return NextResponse.json({ error: tip }, { status: 500 }); - } - - const allowed = normalizeAllowedTools(payload); - const useSearx = payload.options?.searxng !== false; - if (!useSearx) { - allowed.delete("search_web"); - } - - const selected = Array.isArray(payload.selectedUids) ? payload.selectedUids.map((x) => String(x)).filter(Boolean).slice(0, 6) : []; - const attachments = Array.isArray(payload.attachments) ? payload.attachments.slice(0, 10) : []; - const attachmentLines = attachments - .map((a, idx) => `${idx + 1}. id=${a.id} title=${a.title} mime=${a.mimeType ?? ""} url=${a.fileUrl}`) - .join("\n"); - - const summary = walkSummaries(mindmap, 120); - const selectedSummaries = selected - .map((uid) => { - const hit = findNodeByUid(mindmap, uid); - if (!hit) return `- uid=${uid} (not found)`; - const t = String(hit.data?.text ?? "").replace(/<[^>]+>/g, "").trim(); - return `- uid=${uid} text=${t}`; - }) - .join("\n"); - - const toolSpec = [ - { - name: "mindmap_get", - desc: "读取整张导图的精简结构(uid+text+父子关系+子数量)。", - args: { maxNodes: "number? (默认120)" }, - }, - { - name: "mindmap_get_subtree", - desc: "读取指定 uid 的子树(用于定位/精确改写)。", - args: { uid: "string", depth: "number? (默认2)", maxNodes: "number? (默认60)" }, - }, - { - name: "search_web", - desc: "使用 SearxNG 搜索,返回标题/URL/摘要;用于提供可追溯来源。", - args: { query: "string", count: "number? (默认6)" }, - }, - { - name: "mindmap_apply_ops", - desc: "对导图应用增量 ops(支持新增/删/改/加链接/加refs/加note),并自动落盘保存。", - args: { ops: "MindmapOp[]", reason: "string? (可选)" }, - }, - { - name: "pdf_replace_mindmap", - desc: "从 PDF 生成“章->节->要点”的导图并替换当前导图(会保存)。fileRef 需匹配 attachments 里的 id 或标题。", - args: { fileRef: "string", maxPages: "number? (默认9)" }, - }, - ] as const; - - const toolsText = toolSpec - .filter((t) => allowed.has(t.name as ToolName)) - .map((t) => `- ${t.name}: ${t.desc} args=${JSON.stringify(t.args)}`) - .join("\n"); - - const system = [ - "你是“Mindmap AI Agent”。你可以像 CLI 工具一样,通过工具(API)完成任务:读导图、写导图、检索、从 PDF 生成导图。", - "", - "输出格式要求(硬性):", - "你必须输出严格 JSON(不要 Markdown/不要代码块/不要解释)。", - "你只能输出以下两种之一:", - '1) {"type":"tool","tool":ToolName,"args":{...}}', - '2) {"type":"final","message":"...","summary":"...可选"}', - "", - "行为要求(硬性):", - "- 当用户明确要求“修改/生成/补完/写入导图”时,你必须至少调用一次 mindmap_apply_ops 或 pdf_replace_mindmap 来产生实际变更;不能只聊天。", - "- 当用户只是“问问题/咨询”(未要求写入导图)时:你可以不改导图;若需要检索,最多调用 search_web 1 次,然后必须输出 final 直接回答;不要反复调用工具。", - "- 新增节点优先写“内容”(可用 note 或子节点表达),并尽量带 refs(引用来源 URL 或 PDF 页码)。只有纯链接会显得呆板,应避免。", - "- 若调用 search_web,必须把搜索结果转为 refs(NodeRef.kind=url),并用于新节点或 note。", - "- ops 中的 uid 必须来自导图数据;不允许凭空捏造现有 uid。", - "- 禁止重复调用同一个工具且参数完全相同。", - "", - "可用工具列表:", - toolsText || "(无)", - ].join("\n"); - - const userIntro = [ - `documentId=${payload.documentId}`, - `mindmapId=${payload.mindmapId}`, - selected.length ? `selectedUids=${selected.join(",")}` : "selectedUids=(none)", - "", - "当前导图摘要(最多 120 个节点):", - JSON.stringify(summary, null, 2), - "", - selected.length ? `当前选中节点:\n${selectedSummaries}` : "", - attachments.length ? `附件(可用 fileRef 引用):\n${attachmentLines}` : "附件:(无)", - ] - .filter(Boolean) - .join("\n"); - - const conversation = payload.messages - .slice(-10) - .map((m) => ({ role: m.role, content: String(m.content ?? "") })); - - const maxSteps = 6; - const trace: Array<{ step: number; call: ToolCall; toolResult?: unknown }> = []; - let didMutate = false; - const toolCounts = new Map(); - - const origin = new URL(request.url).origin; - const cookie = request.headers.get("cookie") ?? ""; - - const runPdfReplace = async (fileRef: string, maxPages: number) => { - const ref = String(fileRef || "").trim(); - if (!ref) throw new Error("pdf_replace_mindmap: 缺少 fileRef"); - - const attachment = - attachments.find((a) => a.id === ref) || - attachments.find((a) => a.title === ref) || - attachments.find((a) => a.title?.includes(ref)) || - null; - if (!attachment) throw new Error(`pdf_replace_mindmap: 未找到匹配附件:${ref}`); - - const fileUrl = String(attachment.fileUrl || ""); - const testName = (() => { - try { - const u = new URL(fileUrl, "http://local"); - if (u.pathname !== "/api/mindmap-ai/test-pdf") return null; - const name = u.searchParams.get("name"); - return name ? decodeURIComponent(name) : null; - } catch { - return null; - } - })(); - const body = - testName - ? { - source: { kind: "test", name: testName }, - options: { preferProvider: provider === "local" ? "ollama" : "online", maxPages }, - } - : { - source: { kind: "url", fileUrl }, - options: { preferProvider: provider === "local" ? "ollama" : "online", maxPages }, - }; - - const res = await fetch(`${origin}/api/mindmap-ai/outline-to-mindmap`, { - method: "POST", - headers: { "Content-Type": "application/json", cookie }, - body: JSON.stringify(body), - }); - const json = (await res.json().catch(() => null)) as any; - if (!res.ok) { - throw new Error(String(json?.error ?? `outline-to-mindmap 失败:${res.status}`)); - } - if (!json?.mindmapData) { - throw new Error("outline-to-mindmap 返回缺少 mindmapData"); - } - mindmap = json.mindmapData as MindmapTreeNode; - await writeMindmapLocal(payload.documentId, payload.mindmapId, mindmap, doc.title ?? "无标题"); - didMutate = true; - return { ok: true, providerUsed: json?.meta?.providerUsed ?? "unknown", title: json?.meta?.title ?? "" }; - }; - - const lastUser = [...payload.messages].reverse().find((m) => m.role === "user")?.content ?? ""; - const userIntentModify = /补完|写入|保存|生成|替换|总结|导图|节点/i.test(String(lastUser)); - const rootUid = - String(mindmap?.data?.uid || "") || - String(walkSummaries(mindmap, 1)?.[0]?.uid || ""); - const defaultTargetUid = (selected[0] || rootUid || "").trim(); - - // 纯问答模式:不要求改导图时,直接“检索 → 总结”返回,避免模型反复输出工具 JSON 导致卡步数 - if (!userIntentModify) { - const question = String(lastUser || "").trim(); - const qaTrace: Array<{ step: number; call: ToolCall; toolResult?: unknown }> = []; - - if (question && allowed.has("search_web")) { - const results = await searchSearxng(question, 6).catch(() => []); - qaTrace.push({ - step: 1, - call: { type: "tool", tool: "search_web", args: { query: question, count: 6 } }, - toolResult: results, - }); - const answer = results.length - ? await answerFromSearchResults({ cfg, modelOverride, question, results }) - : "(未检索到结果)"; - qaTrace.push({ step: 2, call: { type: "final", message: answer } }); - return NextResponse.json({ ok: true, message: answer, data: mindmap, trace: qaTrace }); - } - - // 未开启检索或无问题:直接用 AI 回答(不改导图) - const { text } = await openAiCompatibleChat( - [ - { - role: "system", - content: [ - "你是一个严谨的中文助手。", - "用户提出问题,请直接回答。", - "如果你不确定,明确说明并建议以官方为准。", - "不要输出 JSON。", - ].join("\n"), - }, - { role: "user", content: question || "(空)" }, - ], - { - baseUrl: cfg.baseUrl, - apiKey: cfg.apiKey, - model: modelOverride || cfg.model, - timeoutMs: 55_000, - maxTokens: 1200, - }, - ); - const answer = String(text || "").trim() || "(无输出)"; - qaTrace.push({ step: 1, call: { type: "final", message: answer } }); - return NextResponse.json({ ok: true, message: answer, data: mindmap, trace: qaTrace }); - } - - for (let step = 1; step <= maxSteps; step += 1) { - const { text } = await openAiCompatibleChat( - [ - { role: "system", content: system }, - { role: "user", content: userIntro }, - ...conversation, - ...(trace.length - ? [ - { - role: "assistant" as const, - content: `(工具执行历史摘要)\n${trace - .map((t) => `#${t.step} ${t.call.type === "tool" ? `tool=${t.call.tool}` : "final"} `) - .join("\n")}`, - }, - ] - : []), - ], - { - baseUrl: cfg.baseUrl, - apiKey: cfg.apiKey, - model: modelOverride || cfg.model, - timeoutMs: 55_000, - maxTokens: 2200, - maxCompletionTokens: 2200, - responseFormat: "json_object", - }, - ); - - let json = tryExtractJsonObject(text); - let call = json ? coerceToolCall(json) : null; - // 若解析失败,再用更小上下文重试一次;仍失败则走兜底(保证可用性,不返回 500) - if (!call) { - try { - const minimalSystem = [ - "你是 Mindmap AI Agent。", - "你必须只输出一个 JSON:", - '1) {"type":"tool","tool":ToolName,"args":{...}} 或 2) {"type":"final","message":"..."}', - "", - "可用工具:", - toolsText || "(无)", - ].join("\n"); - const minimalUser = [ - `documentId=${payload.documentId}`, - `mindmapId=${payload.mindmapId}`, - defaultTargetUid ? `targetUid=${defaultTargetUid}` : "", - `用户请求:${String(lastUser).slice(0, 500)}`, - "", - "导图根节点:", - JSON.stringify({ uid: rootUid, text: stripHtml(mindmap?.data?.text) }, null, 2), - ] - .filter(Boolean) - .join("\n"); - const retry = await openAiCompatibleChat( - [ - { role: "system", content: minimalSystem }, - { role: "user", content: minimalUser }, - ], - { - baseUrl: cfg.baseUrl, - apiKey: cfg.apiKey, - model: modelOverride || cfg.model, - timeoutMs: 35_000, - maxTokens: 900, - maxCompletionTokens: 900, - responseFormat: "json_object", - }, - ); - json = tryExtractJsonObject(retry.text); - call = json ? coerceToolCall(json) : null; - } catch { - call = null; - } - } - - if (!call) { - // 兜底:若用户只是问问题,则不要擅自写入导图;优先给出基于检索的回答 - if (!userIntentModify) { - const q = String(lastUser || "").trim(); - const results = allowed.has("search_web") && q ? await searchSearxng(q, 6).catch(() => []) : []; - if (results.length) { - trace.push({ - step, - call: { type: "tool", tool: "search_web", args: { query: q, count: 6 } }, - toolResult: results, - }); - } - const answer = results.length - ? await answerFromSearchResults({ cfg, modelOverride, question: q, results }) - : "AI 输出未能解析(未执行任何导图写入)。你可以:1) 开启联网检索;2) 换一个模型;3) 更明确说明要写入导图还是仅回答问题。"; - trace.push({ step, call: { type: "final", message: answer } }); - return NextResponse.json({ ok: true, message: answer, data: mindmap, trace }); - } - - // 用户明确要改导图:直接补完(写入并保存),避免用户看到“没做任何事” - const ops = await buildFallbackExpandOps({ - mindmap, - targetUid: defaultTargetUid || rootUid, - instruction: String(lastUser || "补完选中节点"), - useSearx: allowed.has("search_web"), - }); - const { data: next, applied, errors } = applyMindmapOps(mindmap, ops); - mindmap = next; - await writeMindmapLocal(payload.documentId, payload.mindmapId, mindmap, doc.title ?? "无标题"); - didMutate = applied > 0 || didMutate; - trace.push({ - step, - call: { type: "tool", tool: "mindmap_apply_ops", args: { ops, reason: "fallback: ai-json-parse-failed" } }, - toolResult: { applied, errors, fallback: true }, - }); - return NextResponse.json({ - ok: true, - message: "已补完并保存(AI 输出未能解析,已自动使用兜底策略确保可用)。", - data: mindmap, - trace, - }); - } - - if (call.type === "final") { - trace.push({ step, call }); - return NextResponse.json({ - ok: true, - message: call.message, - summary: call.summary ?? null, - data: mindmap, - trace, - }); - } - - // 避免模型在同一个工具上打转(导致“已达最大执行步数”) - const toolKey = `${call.tool}:${stableStringify(call.args)}`; - const prevCount = toolCounts.get(toolKey) ?? 0; - toolCounts.set(toolKey, prevCount + 1); - if (prevCount >= 1) { - trace.push({ step, call, toolResult: { warning: "检测到重复工具调用,已提前停止并返回结果。" } }); - const lastSearch = [...trace] - .reverse() - .find((t) => t.call.type === "tool" && t.call.tool === "search_web")?.toolResult as SearxResult[] | undefined; - const answer = - !userIntentModify && Array.isArray(lastSearch) && lastSearch.length - ? await answerFromSearchResults({ cfg, modelOverride, question: String(lastUser || ""), results: lastSearch }) - : "检测到重复工具调用,已停止。你可以换一个模型或提供更明确的目标/限制。"; - trace.push({ step, call: { type: "final", message: answer } }); - return NextResponse.json({ ok: true, message: answer, data: mindmap, trace }); - } - - if (!allowed.has(call.tool)) { - trace.push({ step, call, toolResult: { error: `工具未被允许:${call.tool}` } }); - continue; - } - - try { - if (call.tool === "mindmap_get") { - const maxNodes = Number(call.args.maxNodes ?? 120) || 120; - const result = walkSummaries(mindmap, Math.max(20, Math.min(300, maxNodes))); - trace.push({ step, call, toolResult: result }); - continue; - } - - if (call.tool === "mindmap_get_subtree") { - const uid = String(call.args.uid ?? ""); - const depth = Number(call.args.depth ?? 2) || 2; - const maxNodes = Number(call.args.maxNodes ?? 60) || 60; - const hit = findNodeByUid(mindmap, uid); - const result = hit ? summarizeSubtree(hit, Math.max(1, Math.min(6, depth)), Math.max(10, Math.min(200, maxNodes))) : null; - trace.push({ step, call, toolResult: result }); - continue; - } - - if (call.tool === "search_web") { - const query = String(call.args.query ?? "").trim(); - const count = Number(call.args.count ?? 6) || 6; - const result = query ? await searchSearxng(query, count) : []; - trace.push({ step, call, toolResult: result }); - // 若用户只是问问题(非写导图),拿到检索结果后直接回答,避免继续 tool loop - if (!userIntentModify && query && Array.isArray(result) && result.length) { - const answer = await answerFromSearchResults({ - cfg, - modelOverride, - question: String(lastUser || query), - results: result, - }); - trace.push({ step, call: { type: "final", message: answer } }); - return NextResponse.json({ ok: true, message: answer, data: mindmap, trace }); - } - continue; - } - - if (call.tool === "mindmap_apply_ops") { - const opsRaw = (call.args as any)?.ops; - const ops = Array.isArray(opsRaw) ? (opsRaw as MindmapOp[]) : []; - const safeOps = ops.filter((op) => op && typeof op === "object" && typeof (op as any).op === "string").slice(0, 80); - - // 若用户开启 searxng 且 ops 中缺 refs,可自动把最近一次 search_web 的结果补到 note/refs(避免纯链接/无来源) - const lastSearch = [...trace] - .reverse() - .find((t) => t.call.type === "tool" && t.call.tool === "search_web")?.toolResult as SearxResult[] | undefined; - const inferredRefs = Array.isArray(lastSearch) ? refsFromSearx(lastSearch).slice(0, 6) : []; - const patchedOps: MindmapOp[] = safeOps.map((op) => { - if ((op as any)?.op !== "addChild" && (op as any)?.op !== "addSiblingAfter") return op; - const node = (op as any).node ?? {}; - const hasRefs = Array.isArray(node.refs) && node.refs.length > 0; - if (hasRefs || inferredRefs.length === 0) return op; - return { - ...op, - node: { - ...node, - refs: inferredRefs, - }, - } as MindmapOp; - }); - - const { data: next, applied, errors } = applyMindmapOps(mindmap, patchedOps); - mindmap = next; - await writeMindmapLocal(payload.documentId, payload.mindmapId, mindmap, doc.title ?? "无标题"); - if (applied > 0) didMutate = true; - trace.push({ step, call, toolResult: { applied, errors } }); - continue; - } - - if (call.tool === "pdf_replace_mindmap") { - const fileRef = String(call.args.fileRef ?? "").trim(); - const maxPages = Number(call.args.maxPages ?? 9) || 9; - const result = await runPdfReplace(fileRef, Math.max(1, Math.min(40, maxPages))); - trace.push({ step, call, toolResult: result }); - continue; - } - } catch (e) { - trace.push({ step, call, toolResult: { error: e instanceof Error ? e.message : String(e) } }); - continue; - } - } - - if (!didMutate && userIntentModify && allowed.has("mindmap_apply_ops") && defaultTargetUid) { - const ops = await buildFallbackExpandOps({ - mindmap, - targetUid: defaultTargetUid, - instruction: String(lastUser || "补完选中节点"), - useSearx: allowed.has("search_web"), - }); - const { data: next, applied, errors } = applyMindmapOps(mindmap, ops); - mindmap = next; - await writeMindmapLocal(payload.documentId, payload.mindmapId, mindmap, doc.title ?? "无标题"); - trace.push({ - step: maxSteps + 1, - call: { type: "tool", tool: "mindmap_apply_ops", args: { ops, reason: "fallback: maxSteps-reached" } }, - toolResult: { applied, errors, fallback: true }, - }); - return NextResponse.json({ - ok: true, - message: "已补完并保存(AI 未在限定步数内完成工具调用,已自动使用兜底策略)。", - data: mindmap, - trace, - }); - } - - // 若用户只是问问题(不写导图),但模型卡在 tool loop:尽量基于最后一次检索结果给出回答 - const lastSearch = [...trace] - .reverse() - .find((t) => t.call.type === "tool" && t.call.tool === "search_web")?.toolResult as SearxResult[] | undefined; - if (!userIntentModify && Array.isArray(lastSearch) && lastSearch.length) { - const answer = await answerFromSearchResults({ cfg, modelOverride, question: String(lastUser || ""), results: lastSearch }); - trace.push({ step: maxSteps + 1, call: { type: "final", message: answer } }); - return NextResponse.json({ ok: true, message: answer, data: mindmap, trace }, { status: 200 }); - } - - return NextResponse.json( - { - ok: true, - message: "已达到最大执行步数,已停止。你可以补充更明确的目标或限制。", - data: mindmap, - trace, - }, - { status: 200 }, - ); - */ -} diff --git a/wolai-frontend/src/app/api/mindmap-ai/assets/route.ts b/wolai-frontend/src/app/api/mindmap-ai/assets/route.ts deleted file mode 100644 index 86c46f31..00000000 --- a/wolai-frontend/src/app/api/mindmap-ai/assets/route.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { NextResponse } from "next/server"; -import { detectLocalMindmapFiles } from "@/lib/server/mindmap-files"; -import type { MediaAsset } from "@/types/media"; -import path from "path"; -import { promises as fs } from "fs"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { getAuthedConvexClient } from "@/lib/convex/route"; -import { api } from "@/lib/convex/api"; -import { maybeProxyForBrowserUrl } from "@/lib/url/proxyForBrowser"; -import { HttpError } from "@/lib/auth/authContext"; - -export const dynamic = "force-dynamic"; - -type AgentAssetItem = { - kind: "media" | "local-mindmap" | "test-pdf"; - id: string; - title: string; - fileUrl: string; - mimeType?: string | null; - assetType?: string | null; - fileName?: string | null; -}; - -const TEST_DIR = path.join(process.cwd(), "test"); - -async function listTestPdfs(): Promise { - try { - const entries = await fs.readdir(TEST_DIR); - return entries - .filter((name) => name.toLowerCase().endsWith(".pdf")) - .slice(0, 200) - .map((name) => ({ - kind: "test-pdf" as const, - id: `test-pdf:${name}`, - title: name, - fileUrl: `/api/mindmap-ai/test-pdf?name=${encodeURIComponent(name)}`, - mimeType: "application/pdf", - assetType: "file", - fileName: name, - })); - } catch { - return []; - } -} - -export async function GET(request: Request) { - if (!isConvexEnabled()) { - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - } - - const { searchParams } = new URL(request.url); - const documentId = String(searchParams.get("documentId") ?? "").trim(); - const q = String(searchParams.get("q") ?? "").trim().toLowerCase(); - const workspaceOnly = String(searchParams.get("workspaceOnly") ?? "").trim() === "1"; - if (!documentId) { - return NextResponse.json({ error: "缺少 documentId" }, { status: 400 }); - } - - let auth: { userId: string }; - let client: any; - try { - const res = await getAuthedConvexClient(); - auth = res.auth; - client = res.client; - } catch (err) { - if (err instanceof HttpError) { - return NextResponse.json({ error: "未登录" }, { status: err.status }); - } - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - const doc = await client.query(api.documents.getMeta, { id: documentId }); - if (!doc) { - return NextResponse.json({ error: "页面不存在" }, { status: 404 }); - } - - if (workspaceOnly) { - return NextResponse.json({ - ok: true, - workspaceId: (doc as any).workspace_id ?? null, - documentId, - items: [], - }); - } - - // 说明:这里的附件列表主要用于 Mindmap AI Agent 选择引用素材。 - const mediaRows = (await client.query(api.mediaAssets.listByDocument, { - userId: auth.userId, - documentId, - limit: 200, - })) as unknown as MediaAsset[]; - - const mediaAssets: AgentAssetItem[] = (mediaRows ?? []).map((row) => ({ - kind: "media" as const, - id: String((row as any).id ?? ""), - title: String((row as any).file_name ?? (row as any).id ?? "附件"), - fileUrl: maybeProxyForBrowserUrl(request, String((row as any).file_url ?? "")), - mimeType: (row as any).mime_type ?? null, - assetType: (row as any).asset_type ?? null, - fileName: (row as any).file_name ?? null, - })); - - const localMindmaps = (await detectLocalMindmapFiles([documentId])) - .filter((x) => x.documentId === documentId) - .map((x) => ({ - kind: "local-mindmap" as const, - id: `mindmap:${x.mindmapId}`, - title: x.fileName, - fileUrl: `/documents/${documentId}/${x.fileName}`, - mimeType: "application/json", - assetType: "mindmap", - fileName: x.fileName, - })); - - const testPdfs = await listTestPdfs(); - - let items: AgentAssetItem[] = [...localMindmaps, ...mediaAssets, ...testPdfs]; - if (q) { - items = items.filter((it) => { - const hay = `${it.title} ${it.fileName ?? ""}`.toLowerCase(); - return hay.includes(q); - }); - } - - return NextResponse.json({ - ok: true, - workspaceId: (doc as any).workspace_id ?? null, - documentId, - items, - }); - - /* - const supabase = await createSupabaseRouteClient(); - const { - data: { session }, - } = await supabase.auth.getSession(); - if (!session) { - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - - const { searchParams } = new URL(request.url); - const documentId = String(searchParams.get("documentId") ?? "").trim(); - const q = String(searchParams.get("q") ?? "").trim().toLowerCase(); - if (!documentId) { - return NextResponse.json({ error: "缺少 documentId" }, { status: 400 }); - } - - const { data: doc, error: docError } = await supabase - .from("documents") - .select("id,workspace_id,title") - .eq("id", documentId) - .eq("user_id", session.user.id) - .maybeSingle(); - if (docError) { - return NextResponse.json({ error: docError.message }, { status: 400 }); - } - if (!doc) { - return NextResponse.json({ error: "页面不存在" }, { status: 404 }); - } - - const { data: mediaRows } = await supabase - .from("media_assets") - .select("id,asset_type,file_url,file_name,mime_type,document_id,workspace_id,deleted_at") - .eq("document_id", documentId) - .eq("workspace_id", doc.workspace_id) - .is("deleted_at", null) - .order("created_at", { ascending: false }) - .limit(200); - - const mediaAssets = ((mediaRows ?? []) as MediaAsset[]).map((row) => ({ - kind: "media" as const, - id: String(row.id), - title: String(row.file_name ?? row.id ?? "附件"), - fileUrl: String(row.file_url ?? ""), - mimeType: (row as any).mime_type ?? null, - assetType: (row as any).asset_type ?? null, - fileName: (row as any).file_name ?? null, - })); - - const localMindmaps = (await detectLocalMindmapFiles([documentId])) - .filter((x) => x.documentId === documentId) - .map((x) => ({ - kind: "local-mindmap" as const, - id: `mindmap:${x.mindmapId}`, - title: x.fileName, - fileUrl: `/documents/${documentId}/${x.fileName}`, - mimeType: "application/json", - assetType: "mindmap", - fileName: x.fileName, - })); - - const testPdfs = await listTestPdfs(); - - let items: AgentAssetItem[] = [...localMindmaps, ...mediaAssets, ...testPdfs]; - if (q) { - items = items.filter((it) => { - const hay = `${it.title} ${it.fileName ?? ""}`.toLowerCase(); - return hay.includes(q); - }); - } - - return NextResponse.json({ - ok: true, - workspaceId: doc.workspace_id, - documentId, - items, - }); - */ -} diff --git a/wolai-frontend/src/app/api/mindmap-ai/expand-node/route.test.ts b/wolai-frontend/src/app/api/mindmap-ai/expand-node/route.test.ts deleted file mode 100644 index 34f026c7..00000000 --- a/wolai-frontend/src/app/api/mindmap-ai/expand-node/route.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { POST } from "@/app/api/mindmap-ai/expand-node/route"; - -describe("/api/mindmap-ai/expand-node route", () => { - it("显式退役旧 Next route,并指向 AI Agent 内置工具", async () => { - const response = await POST(); - const payload = await response.json() as { - error: string; - code: string; - replacement: { - kind: string; - toolName: string; - route: string; - }; - }; - - expect(response.status).toBe(410); - expect(response.headers.get("x-mnote-compat-boundary")).toBe("mindmap-expand-node-route-retired"); - expect(payload).toMatchObject({ - code: "mindmap-expand-node-route-retired", - replacement: { - kind: "ai-agent-tool", - toolName: "mindmap_expand_node", - route: "/api/ai-agent/run", - }, - }); - expect(payload.error).toContain("mindmap_expand_node"); - }); -}); diff --git a/wolai-frontend/src/app/api/mindmap-ai/expand-node/route.ts b/wolai-frontend/src/app/api/mindmap-ai/expand-node/route.ts deleted file mode 100644 index c8a32d9b..00000000 --- a/wolai-frontend/src/app/api/mindmap-ai/expand-node/route.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { NextResponse } from "next/server"; - -export const dynamic = "force-dynamic"; - -export async function POST() { - return NextResponse.json( - { - error: "Next /api/mindmap-ai/expand-node 已退场,请改用 AI Agent 内置 mindmap_expand_node 工具。", - code: "mindmap-expand-node-route-retired", - replacement: { - kind: "ai-agent-tool", - toolName: "mindmap_expand_node", - route: "/api/ai-agent/run", - }, - }, - { - status: 410, - headers: { - "x-mnote-compat-boundary": "mindmap-expand-node-route-retired", - }, - }, - ); -} diff --git a/wolai-frontend/src/app/api/mindmap-ai/outline-to-mindmap/route.ts b/wolai-frontend/src/app/api/mindmap-ai/outline-to-mindmap/route.ts deleted file mode 100644 index 329acae0..00000000 --- a/wolai-frontend/src/app/api/mindmap-ai/outline-to-mindmap/route.ts +++ /dev/null @@ -1,1199 +0,0 @@ -import { NextResponse } from "next/server"; -import { promises as fs } from "fs"; -import path from "path"; -import os from "os"; -import { execFile } from "child_process"; -import { promisify } from "util"; -import { buildDocumentBridgeContextWithActor } from "@/lib/documents/bridge"; -import { executeRustBridgeTool } from "@/lib/documents/rust-runtime"; -import { loadOnlineAiConfig } from "@/lib/ai/onlineAiConfig"; -import { openAiCompatibleChat, tryExtractJsonObject } from "@/lib/ai/openaiCompatibleChat"; -import { getMnoteRuntimeConfig } from "@/lib/runtime-config"; - -export const dynamic = "force-dynamic"; - -const execFileAsync = promisify(execFile); - -type OutlineItem = { - title: string; - level: number; // 1-based - page: number; // 1-based -}; - -type RequestPayload = { - source: - | { kind: "test"; name: string } - | { kind: "url"; fileUrl: string; title?: string }; - ollama?: { - // 兼容 MindmapSidebar 目前的默认值:完整 /api/chat 地址 - baseUrl?: string; - model?: string; - }; - options?: { - maxPages?: number; - maxCandidates?: number; - preferProvider?: "online" | "ollama" | "heuristic"; - forceHeuristic?: boolean; - ollamaTimeoutMs?: number; - onlineTimeoutMs?: number; - }; -}; - -const TEST_PDF_DIR = path.join(process.cwd(), "test"); - -const safeResolveTestPdf = (name: string) => { - const trimmed = (name ?? "").trim(); - if (!trimmed) return null; - if (trimmed.includes("/") || trimmed.includes("\\") || trimmed.includes("..")) return null; - if (!trimmed.toLowerCase().endsWith(".pdf")) return null; - return path.join(TEST_PDF_DIR, trimmed); -}; - -const cleanExtractedText = (value: string) => { - const v = String(value ?? ""); - // 1) 移除 NUL 与不可见控制字符 - // 2) 清理超长英文串(常见于扫描/坏编码提取产生的乱码) - // 3) 统一空白(保留换行,便于标题/段落检测) - return v - .replace(/\u0000/g, "") - .replace(/[\u0001-\u0008\u000B\u000C\u000E-\u001F]/g, " ") - .replace(/\uFFFD/g, "") // � 替换字符(坏编码/抽取失败常见) - .replace(/[\u200B-\u200D\uFEFF]/g, "") // 零宽字符 - .replace(/[A-Za-z]{8,}/g, "") - .replace(/[ \t\f\v]+/g, " ") - .replace(/\r\n/g, "\n") - .replace(/\n{3,}/g, "\n\n") - .trim(); -}; - -const normalizeLine = (value: string) => cleanExtractedText(value); - -const extractHeadingCandidates = (pages: { page: number; text: string }[], maxCandidates: number) => { - const candidates: { page: number; line: string }[] = []; - const seen = new Set(); - - const isMostlyGarbled = (line: string) => { - const s = line.replace(/\s+/g, ""); - if (!s) return true; - const len = s.length; - if (len < 2) return true; - const cjk = (s.match(/[\u4e00-\u9fff]/g) ?? []).length; - const digits = (s.match(/\d/g) ?? []).length; - const letters = (s.match(/[A-Za-z]/g) ?? []).length; - // 太像“乱码/噪声”:基本没有中文且长度不短 - if (cjk === 0 && len >= 10) return true; - // 非中文占比极高且长度较长 - if (len >= 14 && cjk / len < 0.12 && (letters + digits) / len > 0.55) return true; - // 连续符号堆叠 - if (/^[\W_]{8,}$/.test(s)) return true; - return false; - }; - - const isJunk = (line: string) => { - const l = line.trim(); - if (!l) return true; - if (l.length < 2) return true; - if (l.length > 80) return true; - if (/^第?\s*\d+\s*页$/i.test(l)) return true; - if (/^(http|https):\/\//i.test(l)) return true; - if (isMostlyGarbled(l)) return true; - return false; - }; - - const patterns: { re: RegExp; weight: number }[] = [ - { re: /^第[一二三四五六七八九十]+[章节篇]\s*\S+/, weight: 5 }, - { re: /^\d+(\.\d+){0,4}\s+\S+/, weight: 5 }, - { re: /^[一二三四五六七八九十]+[、..]\s*\S+/, weight: 4 }, - { re: /^[((][一二三四五六七八九十]+[))]\s*\S+/, weight: 3 }, - { re: /^(?\d+)\s*\S+/, weight: 2 }, - ]; - - const add = (page: number, line: string) => { - const key = `${page}:${line}`; - if (seen.has(key)) return; - seen.add(key); - candidates.push({ page, line }); - }; - - for (const p of pages) { - const lines = (p.text || "") - .split(/\r?\n/) - .map((l) => normalizeLine(l)) - .filter((l) => !isJunk(l)); - - // 先按规则挑“像标题”的行 - for (const line of lines) { - let score = 0; - for (const pat of patterns) { - if (pat.re.test(line)) score += pat.weight; - } - // 中文文档常见:标题行标点少、较短 - if (line.length <= 30) score += 1; - if (!/[。?!;,]/.test(line)) score += 1; - - if (score >= 5) { - add(p.page, line); - } - if (candidates.length >= maxCandidates) break; - } - if (candidates.length >= maxCandidates) break; - - // 若这一页没有候选,则取前若干行兜底(避免完全无结构) - const hasCandidateThisPage = candidates.some((c) => c.page === p.page); - if (!hasCandidateThisPage) { - for (const line of lines.slice(0, 3)) { - add(p.page, line); - if (candidates.length >= maxCandidates) break; - } - } - if (candidates.length >= maxCandidates) break; - } - - return candidates.slice(0, maxCandidates); -}; - -type TocEntry = { title: string; page: number; sourcePage: number }; - -const extractTocEntries = (pages: { page: number; text: string }[], maxPage: number, limit = 120): TocEntry[] => { - const linesForPage = (text: string) => - String(text || "") - .split(/\r?\n/) - .map((l) => cleanExtractedText(l)) - .map((l) => l.replace(/[·•●]+/g, "·").trim()) - .filter(Boolean); - - const tocLineToEntry = (line: string, maxP: number): { title: string; page: number } | null => { - const l = line.replace(/\s+/g, " ").trim(); - if (!l) return null; - if (l.length < 3 || l.length > 120) return null; - if (/^目录\s*$/.test(l) || /^目\s*录\s*$/.test(l)) return null; - if (/^第?\s*\d+\s*页$/i.test(l)) return null; - if (/(copyright|all rights|isbn)/i.test(l)) return null; - if (/[�]/.test(l)) return null; - - // 常见目录行:标题 + 引导点线 + 页码 - const m1 = l.match(/^(.+?)\s*(?:\.{2,}|…{2,}|·{2,}|—{2,}|-{2,}|_{2,})\s*(\d{1,3})\s*$/); - if (m1?.[1] && m1[2]) { - const title = m1[1].replace(/\s+/g, " ").trim(); - const page = Number(m1[2]); - if (!title) return null; - if (!Number.isFinite(page) || page < 1 || page > maxP) return null; - return { title, page: Math.floor(page) }; - } - - // 备选:末尾直接跟页码(无引导线) - const m2 = l.match(/^(.+?)\s+(\d{1,3})\s*$/); - if (m2?.[1] && m2[2]) { - const title = m2[1].replace(/\s+/g, " ").trim(); - const page = Number(m2[2]); - if (!title) return null; - if (!Number.isFinite(page) || page < 1 || page > maxP) return null; - // 过滤像“正文句子末尾刚好有数字”的情况:要求 title 看起来像标题 - const looksLikeHeading = - /^第[一二三四五六七八九十]+[章节篇]\b/.test(title) || - /^\d+(\.\d+){0,4}\s+/.test(title) || - /^[一二三四五六七八九十]+[、..]\s*/.test(title) || - /^[((][一二三四五六七八九十]+[))]\s*/.test(title); - if (!looksLikeHeading) return null; - return { title, page: Math.floor(page) }; - } - - return null; - }; - - const tocScore = (text: string) => { - const lines = linesForPage(text); - if (!lines.length) return 0; - let score = 0; - if (lines.some((l) => /^(目录|目\s*录)\s*$/.test(l))) score += 8; - // 目录页通常含有大量“点线+页码”形式 - const leaderHits = lines.filter((l) => /(\.{2,}|…{2,}|·{2,}|—{2,}|-{2,}|_{2,})\s*\d{1,3}\s*$/.test(l)).length; - score += Math.min(12, leaderHits); - const numberedHits = lines.filter((l) => /^第[一二三四五六七八九十]+[章节篇]/.test(l) || /^\d+(\.\d+){0,3}\s+/.test(l)).length; - score += Math.min(6, Math.floor(numberedHits / 2)); - return score; - }; - - const tocPages = [...pages] - .map((p) => ({ page: p.page, text: p.text, score: tocScore(p.text) })) - .filter((p) => p.score >= 10) - .sort((a, b) => b.score - a.score || a.page - b.page) - .slice(0, 3); - - const out: TocEntry[] = []; - const seen = new Set(); - for (const p of tocPages) { - const lines = linesForPage(p.text); - for (const line of lines) { - const entry = tocLineToEntry(line, maxPage); - if (!entry) continue; - const title = entry.title.replace(/\s+/g, " ").trim(); - if (!title) continue; - const key = `${entry.page}:${title}`; - if (seen.has(key)) continue; - seen.add(key); - out.push({ title, page: entry.page, sourcePage: p.page }); - if (out.length >= limit) break; - } - if (out.length >= limit) break; - } - - out.sort((a, b) => (a.page - b.page) || a.title.localeCompare(b.title, "zh")); - return out.slice(0, limit); -}; - -const inferLevelFromTitle = (title: string) => { - const t = title.trim(); - const m = t.match(/^(\d+(?:\.\d+){0,4})\s+/); - if (m?.[1]) { - return Math.max(1, m[1].split(".").length); - } - if (/^第[一二三四五六七八九十]+[章节篇]/.test(t)) return 1; - if (/^[一二三四五六七八九十]+[、..]/.test(t)) return 1; - if (/^[((][一二三四五六七八九十]+[))]/.test(t)) return 2; - if (/^(?\d+)/.test(t)) return 2; - return 2; -}; - -const buildOutlineHeuristic = (candidates: { page: number; line: string }[]): OutlineItem[] => { - const out: OutlineItem[] = []; - const seen = new Set(); - for (const c of candidates) { - const title = c.line.trim(); - if (!title) continue; - const key = `${c.page}:${title}`; - if (seen.has(key)) continue; - seen.add(key); - out.push({ - title, - level: Math.min(4, Math.max(1, inferLevelFromTitle(title))), - page: Math.max(1, c.page), - }); - } - return out; -}; - -const extractJsonArray = (value: string): unknown[] | null => { - const s = value ?? ""; - const start = s.indexOf("["); - const end = s.lastIndexOf("]"); - if (start === -1 || end === -1 || end <= start) return null; - try { - const parsed = JSON.parse(s.slice(start, end + 1)); - return Array.isArray(parsed) ? parsed : null; - } catch { - return null; - } -}; - -const normalizeOutlineItems = (raw: unknown, maxPage: number): OutlineItem[] => { - if (!Array.isArray(raw)) return []; - const items: OutlineItem[] = []; - const seen = new Set(); - for (const it of raw) { - if (!it || typeof it !== "object") continue; - const obj = it as Record; - const title = typeof obj.title === "string" ? obj.title.trim() : ""; - const levelNum = typeof obj.level === "number" ? obj.level : Number(obj.level); - const pageNum = typeof obj.page === "number" ? obj.page : Number(obj.page); - if (!title) continue; - const level = Number.isFinite(levelNum) ? Math.max(1, Math.min(6, Math.floor(levelNum))) : 1; - const page = Number.isFinite(pageNum) ? Math.max(1, Math.min(maxPage, Math.floor(pageNum))) : 1; - const key = `${page}:${level}:${title}`; - if (seen.has(key)) continue; - seen.add(key); - items.push({ title, level, page }); - } - // 尽量保证输出按页码递增,避免树构建乱序 - items.sort((a, b) => (a.page - b.page) || (a.level - b.level) || a.title.localeCompare(b.title, "zh")); - return items; -}; - -const buildMindmapTree = (rootTitle: string, outline: OutlineItem[], linkForPage: (p: number) => string) => { - const cryptoAny = crypto as unknown as { randomUUID?: () => string }; - const uid = () => (typeof cryptoAny.randomUUID === "function" ? cryptoAny.randomUUID() : Math.random().toString(36).slice(2)); - - const root = { data: { text: rootTitle, uid: uid() }, children: [] as any[] }; - const stack: any[] = [root]; - - const pushNode = (item: OutlineItem) => { - const level = Math.max(1, Math.min(6, item.level)); - while (stack.length > level) stack.pop(); - while (stack.length < level) stack.push(stack[stack.length - 1]); - const parent = stack[level - 1] ?? root; - const href = linkForPage(item.page); - const node = { - data: { - text: item.title, - uid: uid(), - hyperlink: href, - // 预留:后续可用于“引用详情/跳转信息” - refs: [{ kind: "pdf", page: item.page, title: item.title }], - }, - children: [] as any[], - }; - parent.children.push(node); - stack[level] = node; - }; - - outline.forEach(pushNode); - return root; -}; - -type AiPoint = { text: string; page?: number }; -type AiSection = { title: string; page: number; points?: AiPoint[] | string[] }; -type AiChapter = { title: string; page: number; sections?: AiSection[] }; -type AiPlan = { title?: string; chapters?: AiChapter[] }; - -const isChapterTitle = (line: string) => /^第[一二三四五六七八九十]+章/.test(line.trim()); -const isSectionTitle = (line: string) => /^第[一二三四五六七八九十]+节/.test(line.trim()); -const isPointTitle = (line: string) => { - const t = line.trim(); - if (!t) return false; - if (isChapterTitle(t) || isSectionTitle(t)) return false; - if (/^\d+(\.\d+){2,}\s+/.test(t)) return true; // 1.1.1 - if (/^[一二三四五六七八九十]+[、..]\s*\S+/.test(t)) return true; // 一、 - if (/^[((][一二三四五六七八九十]+[))]\s*\S+/.test(t)) return true; // (一) - if (/^(?\d+)\s*\S+/.test(t)) return true; // (1)/1) - return false; -}; - -const enrichPlanPointsFromCandidates = ( - plan: AiPlan, - candidates: { page: number; line: string }[], -) => { - if (!plan.chapters?.length) return plan; - - const cleaned = candidates - .map((c) => ({ page: c.page, line: cleanExtractedText(c.line) })) - .map((c) => ({ ...c, line: c.line.trim() })) - .filter((c) => c.line && c.line.length <= 80) - .filter((c) => c.line.length >= 6) - .filter((c) => !/^(目录|目\s*录)$/u.test(c.line)) - .filter((c) => !/[《〈『【((]\s*$/u.test(c.line)) - .filter((c) => !/^[0-9\s.·•\-–—_]{6,}$/.test(c.line)); - - const findPointsInRange = (fromPage: number, toPage: number, exclude: Set) => { - const points: AiPoint[] = []; - const seen = new Set(); - for (const c of cleaned) { - if (c.page < fromPage || c.page > toPage) continue; - const t = c.line.trim(); - if (!t) continue; - if (exclude.has(t)) continue; - if (/[《〈『【((]\s*$/u.test(t)) continue; - if (!isPointTitle(t)) continue; - if (seen.has(t)) continue; - seen.add(t); - points.push({ text: t, page: c.page }); - if (points.length >= 6) break; - } - return points; - }; - - for (let ci = 0; ci < plan.chapters.length; ci++) { - const ch = plan.chapters[ci]; - const nextChapterPage = plan.chapters[ci + 1]?.page ?? Number.POSITIVE_INFINITY; - const sections = ch.sections ?? []; - for (let si = 0; si < sections.length; si++) { - const sec = sections[si]; - const nextSectionPage = sections[si + 1]?.page ?? nextChapterPage; - const fromPage = Math.max(1, sec.page); - // 控制范围:最多跨 2 页,避免引入别的章节噪声 - const toPage = Math.max(fromPage, Math.min(Number.isFinite(nextSectionPage) ? nextSectionPage : fromPage, fromPage + 2)); - const exclude = new Set([ch.title, sec.title]); - const points = findPointsInRange(fromPage, toPage, exclude); - if (points.length) { - sec.points = points; - continue; - } - - // 兜底:同页取 2~3 个候选行做要点 - const fallback = cleaned - .filter((c) => c.page == fromPage) - .map((c) => c.line.trim()) - .filter((t) => t && !exclude.has(t)) - .filter((t) => t.length >= 6) - .filter((t) => !/[《〈『【((]\s*$/u.test(t)) - .filter((t) => !/^[0-9\s.·•\-–—_]{6,}$/.test(t)) - .slice(0, 3) - .map((t) => ({ text: t, page: fromPage })); - if (fallback.length) sec.points = fallback; - } - } - - return plan; -}; - -const enrichPlanPointsFromPages = (plan: AiPlan, pages: { page: number; text: string }[]) => { - if (!plan.chapters?.length) return plan; - - const getLinesForRange = (fromPage: number, toPage: number) => { - const out: { page: number; line: string }[] = []; - for (const p of pages) { - if (p.page < fromPage || p.page > toPage) continue; - const lines = String(p.text || "") - .split(/\r?\n/) - .map((l) => cleanExtractedText(l)) - .map((l) => l.replace(/[ \t]+/g, " ").trim()) - .filter(Boolean); - for (const line of lines) out.push({ page: p.page, line }); - } - return out; - }; - - const isNoiseLine = (line: string) => { - const t = line.trim(); - if (!t) return true; - if (t.length < 6) return true; - if (t.length > 90) return true; - if (/^(目录|目\s*录)\s*$/.test(t)) return true; - if (/^第?\s*\d+\s*页$/.test(t)) return true; - if (/^(http|https):\/\//i.test(t)) return true; - if (/^[\W_]{8,}$/.test(t)) return true; - // 孤立的引号/括号开头结尾(常见于页眉/装饰符) - if (/[《〈『【((]\s*$/.test(t)) return true; - // 极低中文占比的长句:更像乱码或页眉/脚注 - const s = t.replace(/\s+/g, ""); - const cjk = (s.match(/[\u4e00-\u9fff]/g) ?? []).length; - if (s.length >= 18 && cjk / s.length < 0.12) return true; - return false; - }; - - const scoreLine = (line: string) => { - const t = line.trim(); - let score = 0; - if (/[。;:]/.test(t)) score += 2; - if (/[,、]/.test(t)) score += 1; - if (isPointTitle(t)) score += 2; - if (/^(?:-|\*|•|·)\s*\S+/.test(t)) score += 2; - if (/^[((][一二三四五六七八九十]+[))]/.test(t)) score += 2; - if (/^\d+(\.\d+){1,}\s+/.test(t)) score += 2; - if (t.length >= 10 && t.length <= 45) score += 1; - return score; - }; - - for (let ci = 0; ci < plan.chapters.length; ci++) { - const ch = plan.chapters[ci]; - const nextChapterPage = plan.chapters[ci + 1]?.page ?? Number.POSITIVE_INFINITY; - const sections = ch.sections ?? []; - for (let si = 0; si < sections.length; si++) { - const sec = sections[si]; - const nextSectionPage = sections[si + 1]?.page ?? nextChapterPage; - const fromPage = Math.max(1, sec.page); - // 内容抽取范围:最多跨 3 页(更接近“内容”而不只标题) - const toPage = Math.max( - fromPage, - Math.min(Number.isFinite(nextSectionPage) ? nextSectionPage : fromPage, fromPage + 3), - ); - const exclude = new Set([ch.title, sec.title]); - - const pool = getLinesForRange(fromPage, toPage) - .map((x) => ({ ...x, line: x.line.trim() })) - .filter((x) => x.line && !exclude.has(x.line)) - .filter((x) => !isNoiseLine(x.line)) - .filter((x) => !isChapterTitle(x.line) && !isSectionTitle(x.line)); - - const seen = new Set(); - const scored = pool - .filter((x) => { - if (seen.has(x.line)) return false; - seen.add(x.line); - return true; - }) - .map((x) => ({ ...x, score: scoreLine(x.line) })) - .sort((a, b) => b.score - a.score || a.page - b.page); - - const picked = scored.slice(0, 6).map((x) => ({ text: x.line, page: x.page })); - if (picked.length) { - sec.points = picked; - } - } - } - - return plan; -}; - -const normalizeAiPlan = (raw: unknown, maxPage: number): AiPlan | null => { - if (!raw || typeof raw !== "object") return null; - const obj = raw as Record; - const title = typeof obj.title === "string" ? obj.title.trim() : undefined; - const chaptersRaw = obj.chapters; - if (!Array.isArray(chaptersRaw)) return null; - - const clampPage = (p: unknown) => { - const n = typeof p === "number" ? p : Number(p); - if (!Number.isFinite(n)) return 1; - return Math.max(1, Math.min(maxPage, Math.floor(n))); - }; - - const chapters: AiChapter[] = []; - for (const ch of chaptersRaw) { - if (!ch || typeof ch !== "object") continue; - const c = ch as Record; - const chTitle = typeof c.title === "string" ? c.title.trim() : ""; - if (!chTitle) continue; - const chPage = clampPage(c.page); - const sectionsRaw = c.sections; - const sections: AiSection[] = []; - if (Array.isArray(sectionsRaw)) { - for (const sec of sectionsRaw) { - if (!sec || typeof sec !== "object") continue; - const s = sec as Record; - const secTitle = typeof s.title === "string" ? s.title.trim() : ""; - if (!secTitle) continue; - const secPage = clampPage(s.page ?? chPage); - let points: AiPoint[] | undefined; - if (Array.isArray(s.points)) { - points = s.points - .map((p) => { - if (typeof p === "string") return { text: p.trim(), page: secPage }; - if (p && typeof p === "object") { - const po = p as Record; - const text = typeof po.text === "string" ? po.text.trim() : ""; - if (!text) return null; - return { text, page: clampPage(po.page ?? secPage) }; - } - return null; - }) - .filter(Boolean) as AiPoint[]; - } - sections.push({ title: secTitle, page: secPage, points }); - } - } - chapters.push({ title: chTitle, page: chPage, sections }); - } - - if (!chapters.length) return null; - return { title, chapters }; -}; - -const stripLeadingNumbering = (title: string) => { - const t = String(title || "").trim(); - if (!t) return ""; - return ( - t - // 常见:二、xxx / 2. xxx / 2.xxx - .replace(/^[一二三四五六七八九十]+[、..]\s*/u, "") - .replace(/^\d+(\.\d+){0,3}\s*[、..]?\s*/u, "") - // (一)xxx / (1) xxx / (1)xxx - .replace(/^[((][一二三四五六七八九十]+[))]\s*/u, "") - .replace(/^(?\d+)\s*/u, "") - .trim() - ); -}; - -const normalizePlanTitle = (planTitle: string | undefined, fallbackTitle: string) => { - const t = String(planTitle || "").trim(); - if (!t) return fallbackTitle; - if (/^(目录|目\s*录)$/u.test(t)) return fallbackTitle; - if (/^(文档结构|文档大纲|文档目录|大纲|结构)$/u.test(t)) return fallbackTitle; - if (/document\s+outline/i.test(t)) return fallbackTitle; - // 纯英文/数字标题通常是网关默认值或无意义占位 - if (/^[A-Za-z0-9 _.-]{6,}$/u.test(t) && !/[\u4e00-\u9fff]/u.test(t)) return fallbackTitle; - // 太短/过于泛化的标题,直接用文件名/提取标题 - if (t.length <= 2) return fallbackTitle; - return t; -}; - -const ensureChapterSectionLabels = (plan: AiPlan) => { - const chapters = plan.chapters ?? []; - for (let ci = 0; ci < chapters.length; ci++) { - const ch = chapters[ci]; - const rawChTitle = String(ch.title || "").trim(); - const chTitleBase = stripLeadingNumbering(rawChTitle) || rawChTitle; - const hasChapterWord = /章/u.test(rawChTitle) || /^第[一二三四五六七八九十]+章/u.test(rawChTitle); - ch.title = hasChapterWord ? rawChTitle : `第${ci + 1}章 ${chTitleBase}`.trim(); - - const sections = ch.sections ?? []; - for (let si = 0; si < sections.length; si++) { - const sec = sections[si]; - const rawSecTitle = String(sec.title || "").trim(); - const secTitleBase = stripLeadingNumbering(rawSecTitle) || rawSecTitle; - const hasSectionWord = /节/u.test(rawSecTitle) || /^第[一二三四五六七八九十]+节/u.test(rawSecTitle); - sec.title = hasSectionWord ? rawSecTitle : `第${si + 1}节 ${secTitleBase}`.trim(); - } - } - return plan; -}; - -const postProcessAiPlan = (plan: AiPlan, fallbackTitle: string) => { - plan.title = normalizePlanTitle(plan.title, fallbackTitle); - return ensureChapterSectionLabels(plan); -}; - -const ensurePlanHasChapterAndSections = ( - plan: AiPlan, - pdfTitle: string, - tocEntries: TocEntry[], - candidates: { page: number; line: string }[], - maxPage: number, -) => { - const chapters = plan.chapters ?? []; - const hasAnySections = chapters.some((c) => Array.isArray(c.sections) && c.sections.length > 0); - if (hasAnySections) return plan; - - const buildSectionsFromEntries = (entries: { title: string; page: number }[]) => { - const out: AiSection[] = []; - const seen = new Set(); - for (const e of entries) { - const raw = String(e.title || "").trim(); - // 目录项/AI 可能会自带“第X章/第X节”,这里统一剥离后再由 ensureChapterSectionLabels 重新加前缀 - const noChapterPrefix = raw.replace(/^第[一二三四五六七八九十0-9]+章\s*/u, "").trim(); - const noSectionPrefix = noChapterPrefix.replace(/^第[一二三四五六七八九十0-9]+节\s*/u, "").trim(); - const title = stripLeadingNumbering(noSectionPrefix) || noSectionPrefix || raw; - if (!title) continue; - const page = Math.max(1, Math.min(maxPage, Math.floor(e.page || 1))); - const key = `${page}:${title}`; - if (seen.has(key)) continue; - seen.add(key); - out.push({ title, page }); - if (out.length >= 12) break; - } - return out; - }; - - // 1) 若 AI 把所有条目都当成“chapter”,则降级:合并成一个 chapter,其它变 section - if (chapters.length >= 2) { - const asSections = buildSectionsFromEntries( - chapters.map((c) => ({ title: String(c.title || ""), page: Math.max(1, Math.min(maxPage, c.page || 1)) })), - ); - const firstPage = asSections[0]?.page ?? Math.max(1, Math.min(maxPage, chapters[0]?.page ?? 1)); - const merged: AiPlan = { - title: plan.title, - chapters: [ - { - title: `第1章 ${pdfTitle}`.trim(), - page: firstPage, - sections: asSections, - }, - ], - }; - return ensureChapterSectionLabels(merged); - } - - // 2) 优先使用目录识别结果作为 sections - const tocSections = buildSectionsFromEntries(tocEntries); - if (tocSections.length) { - const firstPage = tocSections[0]?.page ?? 1; - const merged: AiPlan = { - title: plan.title, - chapters: [ - { - title: `第1章 ${pdfTitle}`.trim(), - page: firstPage, - sections: tocSections, - }, - ], - }; - return ensureChapterSectionLabels(merged); - } - - // 3) 最后用候选标题补一个 section 列表 - const candidateSections = buildSectionsFromEntries( - candidates - .map((c) => ({ title: c.line, page: c.page })) - .filter((c) => c.title && c.title.length <= 80) - .filter((c) => !/^(目录|目\s*录)$/u.test(c.title.trim())), - ); - const firstPage = candidateSections[0]?.page ?? 1; - const merged: AiPlan = { - title: plan.title, - chapters: [ - { - title: `第1章 ${pdfTitle}`.trim(), - page: firstPage, - sections: candidateSections, - }, - ], - }; - return ensureChapterSectionLabels(merged); -}; - -const buildMindmapFromPlan = ( - rootTitle: string, - plan: AiPlan, - linkForPage: (p: number) => string, -) => { - const cryptoAny = crypto as unknown as { randomUUID?: () => string }; - const uid = () => - typeof cryptoAny.randomUUID === "function" - ? cryptoAny.randomUUID() - : Math.random().toString(36).slice(2); - - const title = normalizePlanTitle(plan.title, rootTitle); - const root = { data: { text: title, uid: uid() }, children: [] as any[] }; - - const chapters = plan.chapters ?? []; - for (const ch of chapters) { - const chNode = { - data: { - text: ch.title, - uid: uid(), - hyperlink: linkForPage(ch.page), - refs: [{ kind: "pdf", page: ch.page, title: ch.title }], - }, - children: [] as any[], - }; - - const sections = ch.sections ?? []; - for (const sec of sections) { - const secNode = { - data: { - text: sec.title, - uid: uid(), - hyperlink: linkForPage(sec.page), - refs: [{ kind: "pdf", page: sec.page, title: sec.title }], - }, - children: [] as any[], - }; - - const points = Array.isArray(sec.points) ? sec.points : []; - for (const p of points.slice(0, 10)) { - const rawText = typeof p === "string" ? p : p.text; - const text = cleanExtractedText(rawText || ""); - if (!text) continue; - const page = Math.max(1, (typeof p === "string" ? sec.page : (p.page ?? sec.page))); - secNode.children.push({ - data: { - text, - uid: uid(), - hyperlink: linkForPage(page), - refs: [{ kind: "pdf", page, title: text }], - }, - children: [] as any[], - }); - } - - chNode.children.push(secNode); - } - - root.children.push(chNode); - } - - return root; -}; - -const callPdfExtractor = async (pdfFile: string, maxPages: number) => { - const script = path.join(process.cwd(), "scripts", "mindmap_ai_pdf_extract.py"); - const run = async (cmd: string, args: string[]) => { - const { stdout } = await execFileAsync(cmd, args, { windowsHide: true, maxBuffer: 1024 * 1024 * 20 }); - return stdout; - }; - try { - return await run("python", [script, "--input", pdfFile, "--max-pages", String(maxPages)]); - } catch { - return await run("py", ["-3", script, "--input", pdfFile, "--max-pages", String(maxPages)]); - } -}; - -const downloadToTemp = async (fileUrl: string) => { - const res = await fetch(fileUrl); - if (!res.ok) throw new Error(`下载 PDF 失败:${res.status}`); - const arrayBuffer = await res.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "mindmap-ai-")); - const file = path.join(dir, `source-${Date.now()}.pdf`); - await fs.writeFile(file, buffer); - return { dir, file }; -}; - -const safeFetchableUrl = (raw: string) => { - const value = (raw ?? "").trim(); - if (!value) return null; - try { - const u = new URL(value); - const host = u.hostname; - // 基础防护:只允许本机或 supabase(同域/存储域名变化时可扩展) - if (host === "127.0.0.1" || host === "localhost") return u.toString(); - const supabaseUrl = getMnoteRuntimeConfig().supabaseUrl ?? ""; - if (supabaseUrl) { - const supaHost = new URL(supabaseUrl).hostname; - if (host === supaHost) return u.toString(); - } - // 若没有配置 supabase URL,则仍允许 https(但会有 SSRF 风险,默认不放开) - return null; - } catch { - return null; - } -}; - -const buildOutlineWithOllama = async ( - baseUrl: string, - model: string, - candidates: { page: number; line: string }[], - maxPage: number, - timeoutMs: number, -): Promise => { - const prompt = [ - "你是一个“文档结构抽取器”。", - "我将给你一组来自 PDF 的候选标题行(每行都带页码)。请输出一个 JSON 数组,每个元素格式为:", - '{ "title": string, "level": number, "page": number }', - "", - "要求:", - "- level 从 1 开始,越小越靠近中心主题;最多 4。", - "- title 要尽量保留原文标题含义,但去掉重复空格。", - "- page 必须是 1-based 页码。", - "- 只输出 JSON 数组本身,不要 Markdown,不要解释,不要额外字段。", - "", - "候选标题:", - ...candidates.map((c, idx) => `${idx + 1}. [p${c.page}] ${c.line}`), - ].join("\n"); - - const controller = new AbortController(); - const t = setTimeout(() => controller.abort(), Math.max(500, timeoutMs || 0)); - const res = await fetch(baseUrl, { - method: "POST", - signal: controller.signal, - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - model, - stream: false, - messages: [ - { role: "system", content: "只输出严格 JSON,不要代码块。" }, - { role: "user", content: prompt }, - ], - }), - }).finally(() => clearTimeout(t)); - if (!res.ok) return null; - const json = (await res.json().catch(() => null)) as any; - const content = String(json?.message?.content ?? ""); - const arr = extractJsonArray(content); - if (!arr) return null; - const normalized = normalizeOutlineItems(arr, maxPage); - return normalized.length ? normalized : null; -}; - -export async function POST(request: Request) { - const payload = (await request.json().catch(() => null)) as RequestPayload | null; - if (!payload?.source) { - return NextResponse.json({ error: "缺少 source" }, { status: 400 }); - } - - const maxPages = Math.max(1, Math.min(200, payload.options?.maxPages ?? 30)); - const maxCandidates = Math.max(10, Math.min(200, payload.options?.maxCandidates ?? 80)); - const forceHeuristic = Boolean(payload.options?.forceHeuristic); - const ollamaTimeoutMs = Math.max(500, Math.min(60_000, payload.options?.ollamaTimeoutMs ?? 8000)); - const onlineTimeoutMs = Math.max(500, Math.min(120_000, payload.options?.onlineTimeoutMs ?? 25_000)); - const preferProvider = payload.options?.preferProvider ?? "online"; - - let pdfTitle = "文档"; - let pdfUrlForLink: ((p: number) => string) | null = null; - let pageLinkPattern = "#page={page}"; - let cleanupDir: string | null = null; - let pdfFile: string | null = null; - - try { - if (payload.source.kind === "test") { - if (process.env.NODE_ENV === "production") { - return NextResponse.json({ error: "生产环境不支持 test 源" }, { status: 404 }); - } - const testName = payload.source.name; - const file = safeResolveTestPdf(testName); - if (!file) { - return NextResponse.json({ error: "test 文件名非法" }, { status: 400 }); - } - pdfFile = file; - pdfTitle = testName.replace(/\.pdf$/i, ""); - pdfUrlForLink = (p) => `/api/mindmap-ai/test-pdf?name=${encodeURIComponent(testName)}#page=${p}`; - pageLinkPattern = `/api/mindmap-ai/test-pdf?name=${encodeURIComponent(testName)}#page={page}`; - } else if (payload.source.kind === "url") { - const safe = safeFetchableUrl(payload.source.fileUrl); - if (!safe) { - return NextResponse.json({ error: "fileUrl 不允许(仅允许本机或 supabase 域名)" }, { status: 400 }); - } - const downloaded = await downloadToTemp(safe); - cleanupDir = downloaded.dir; - pdfFile = downloaded.file; - pdfTitle = payload.source.title?.trim() || "文档"; - pdfUrlForLink = (p) => `${safe}#page=${p}`; - pageLinkPattern = `${safe}#page={page}`; - } else { - return NextResponse.json({ error: "不支持的 source.kind" }, { status: 400 }); - } - - const extractedRaw = await callPdfExtractor(pdfFile, maxPages); - const extracted = JSON.parse(extractedRaw) as { - meta?: { title?: string | null }; - pages: { page: number; text: string }[]; - totalPages: number; - }; - - const maxPage = Math.max(1, Math.min(extracted.totalPages || maxPages, maxPages)); - if (extracted.meta?.title && String(extracted.meta.title).trim()) { - pdfTitle = String(extracted.meta.title).trim(); - } - - const pages = (extracted.pages ?? []).slice(0, maxPage); - // 先对分页文本做清洗,降低乱码对候选抽取的影响 - const cleanedPages = pages.map((p) => ({ page: p.page, text: cleanExtractedText(p.text || "") })); - const candidates = extractHeadingCandidates(cleanedPages, maxCandidates); - const tocEntries = extractTocEntries(cleanedPages, maxPage); - - let providerUsed: "online" | "ollama" | "heuristic" = "heuristic"; - let plan: AiPlan | null = null; - let outline: OutlineItem[] | null = null; - let onlineAvailable = false; - let onlineAttempted = false; - let onlineSucceeded = false; - let onlineError: string | null = null; - const ollamaBaseUrl = payload.ollama?.baseUrl?.trim() || "http://localhost:11434/api/chat"; - const ollamaModel = payload.ollama?.model?.trim() || "qwen3:30b-a3b-instruct-2507-q4_K_M"; - - const linkForPage = pdfUrlForLink ?? ((p) => `#page=${p}`); - - // 1) 在线 AI 优先:输出“章→节→要点”结构 - if (!forceHeuristic && preferProvider === "online") { - const cfg = await loadOnlineAiConfig().catch(() => null); - if (cfg) { - try { - onlineAvailable = true; - onlineAttempted = true; - const system = [ - "你是一个“PDF 思维导图结构化生成器”。", - "你必须输出严格 JSON(不要 Markdown/不要代码块/不要解释)。", - "目标结构必须是:章(第一层)→ 节(第二层)。", - "你要忽略无意义乱码/随机英文串;输出标题必须是可读中文(或常见编号标题,如 2.1 / (一))。", - "只输出章/节骨架:不要输出 points 字段,不要输出正文。", - ].join("\n"); - - const inputLines = - tocEntries.length > 0 - ? tocEntries.slice(0, Math.min(60, tocEntries.length)).map((e) => `[p${e.page}] ${e.title}`) - : candidates.slice(0, Math.min(60, candidates.length)).map((c) => `[p${c.page}] ${c.line}`); - - const user = [ - "请根据下方“目录/标题行(带页码)”生成思维导图结构 JSON。", - "", - "输出 JSON Schema:", - "{", - ' "title": string,', - ' "chapters": [', - " {", - ' "title": string,', - ' "page": number,', - ' "sections": [', - " {", - ' "title": string,', - ' "page": number', - " }", - " ]", - " }", - " ]", - "}", - "", - "硬性要求:", - "- 只允许输出上述字段;不要输出其它字段。", - "- chapters 必须按页码递增;sections 也按页码递增。", - "- page 为 1-based 页码(必须是整数)。", - "- title 不能包含无意义乱码(例如连续英文乱序、无意义符号堆叠)。", - "- 若候选行里存在“第X章/第X节”,优先按该结构组织。", - "- 输出规模控制:最多 10 个 chapter;每个 chapter 最多 10 个 section。", - "", - `输入行来源:${tocEntries.length > 0 ? "目录识别" : "标题候选"}`, - "输入行(格式:[p页码] 文本):", - ...inputLines, - ].join("\n"); - - const callOnce = async (userText: string, maxOutTokens: number) => { - const { text, raw } = await openAiCompatibleChat( - [ - { role: "system", content: system }, - { role: "user", content: userText }, - ], - { - baseUrl: cfg.baseUrl, - apiKey: cfg.apiKey, - model: cfg.model, - timeoutMs: onlineTimeoutMs, - maxTokens: maxOutTokens, - maxCompletionTokens: maxOutTokens, - responseFormat: "json_object", - }, - ); - const parsed = tryExtractJsonObject(text); - if (!parsed) { - const finishReason = String((raw as any)?.choices?.[0]?.finish_reason ?? ""); - const preview = String(text || "").slice(0, 220).replace(/\s+/g, " ").trim(); - throw new Error( - `在线 AI 未返回可解析 JSON(finish_reason=${finishReason || "unknown"}):${preview || "(empty)"}`, - ); - } - return parsed; - }; - - // attempt 1:优先使用目录识别(更少噪声),并加大输出 token 限制减少截断概率 - let json = await callOnce(user, 2600); - plan = normalizeAiPlan(json, maxPage); - - // attempt 2:更严格限制输入行与输出规模(应对网关硬性 token 上限) - if (!plan) { - const inputLinesSmall = - tocEntries.length > 0 - ? tocEntries.slice(0, Math.min(40, tocEntries.length)).map((e) => `[p${e.page}] ${e.title}`) - : candidates.slice(0, Math.min(40, candidates.length)).map((c) => `[p${c.page}] ${c.line}`); - const userSmall = [ - "请根据下方“目录/标题行(带页码)”生成思维导图结构 JSON。", - "", - "输出 JSON Schema:", - "{", - ' "title": string,', - ' "chapters": [', - " {", - ' "title": string,', - ' "page": number,', - ' "sections": [', - " {", - ' "title": string,', - ' "page": number', - " }", - " ]", - " }", - " ]", - "}", - "", - "硬性要求:", - "- 只允许输出上述字段;不要输出其它字段。", - "- chapters 必须按 page 递增;sections 也按 page 递增。", - "- page 为 1-based 页码(必须是整数)。", - "- title 不能包含无意义乱码。", - "- 输出规模控制:最多 8 个 chapter;每个 chapter 最多 10 个 section。", - "", - `输入行来源:${tocEntries.length > 0 ? "目录识别" : "标题候选"}`, - "输入行(格式:[p页码] 文本):", - ...inputLinesSmall, - ].join("\n"); - json = await callOnce(userSmall, 2600); - plan = normalizeAiPlan(json, maxPage); - } - - if (plan) { - plan = postProcessAiPlan(plan, pdfTitle); - plan = ensurePlanHasChapterAndSections(plan, pdfTitle, tocEntries, candidates, maxPage); - // 第三层“内容/要点”用规则从正文页抽取,避免在线输出膨胀导致截断/乱码 - plan = enrichPlanPointsFromPages(plan, cleanedPages); - // 兜底:用候选行补点(保持至少有内容) - plan = enrichPlanPointsFromCandidates(plan, candidates); - providerUsed = "online"; - onlineSucceeded = true; - } - } catch (e) { - onlineError = e instanceof Error ? e.message : String(e); - plan = null; - } - } - } - - // 2) 兜底:本地 Ollama(只做标题分级,不生成要点) - if (!plan && !forceHeuristic && (preferProvider === "ollama" || preferProvider === "online")) { - outline = await buildOutlineWithOllama(ollamaBaseUrl, ollamaModel, candidates, maxPage, ollamaTimeoutMs).catch(() => null); - if (outline?.length) providerUsed = "ollama"; - } - - // 3) 最后兜底:纯规则 - if (!plan && !outline) { - outline = buildOutlineHeuristic(candidates); - providerUsed = "heuristic"; - } - - // 若仍然极少结构,则兜底按页生成二级结构 - if (!plan && outline && outline.length < 3) { - outline = cleanedPages.flatMap((p) => [ - { title: `第 ${p.page} 页`, level: 1, page: p.page }, - ...((p.text || "") - .split(/\r?\n/) - .map((l) => normalizeLine(l)) - .filter(Boolean) - .slice(0, 3) - .map((l) => ({ title: l, level: 2, page: p.page }))), - ]); - } - - const finalOutline = plan - ? plan.chapters.flatMap((chapter) => [ - { - title: chapter.title, - level: 1, - page: chapter.page, - }, - ...((chapter.sections ?? []).map((section) => ({ - title: section.title, - level: 2, - page: section.page, - }))), - ]) - : (outline ?? []); - - const context = buildDocumentBridgeContextWithActor({ - request, - actor: { - actorType: "service", - actorId: "mindmap-outline-to-mindmap", - sessionId: null, - }, - workspaceId: null, - source: { - channel: "mindmap-outline-route", - client: "wolai-frontend", - }, - }); - const payload = { - documentId: "mindmap-outline-to-mindmap", - mindmapId: "mindmap-outline-to-mindmap", - rootTitle: pdfTitle, - pageLinkPattern, - outline: finalOutline, - }; - // 说明:导图树构建已经交给 Rust runtime,这里只保留 PDF 提取与 transport 壳。 - const rustResult = await executeRustBridgeTool<{ - ok: boolean; - data?: unknown; - meta?: { title?: string | null; outlineCount?: number | null } | null; - }>({ - context, - toolName: "mindmap_outline_to_mindmap", - invocationKind: "command", - args: payload, - data: payload, - mode: "result", - }); - const mindmapData = - rustResult.result && typeof rustResult.result === "object" && !Array.isArray(rustResult.result) && "data" in rustResult.result - ? ((rustResult.result as { data?: unknown }).data ?? { - data: { text: pdfTitle }, - children: [], - }) - : { - data: { text: pdfTitle }, - children: [], - }; - - return NextResponse.json({ - mindmapData, - outline: null, - plan, - candidates, - tocEntries, - meta: { - title: pdfTitle, - totalPages: extracted.totalPages ?? null, - providerUsed, - onlineAvailable, - onlineAttempted, - onlineSucceeded, - onlineError, - outlineCount: Array.isArray(finalOutline) ? finalOutline.length : 0, - }, - }); - } catch (error) { - return NextResponse.json({ error: String(error) }, { status: 500 }); - } finally { - if (cleanupDir) { - try { - await fs.rm(cleanupDir, { recursive: true, force: true }); - } catch { - // ignore - } - } - } -} diff --git a/wolai-frontend/src/app/api/mindmap-ai/test-pdf/route.ts b/wolai-frontend/src/app/api/mindmap-ai/test-pdf/route.ts deleted file mode 100644 index 7279eff4..00000000 --- a/wolai-frontend/src/app/api/mindmap-ai/test-pdf/route.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { NextResponse } from "next/server"; -import { promises as fs } from "fs"; -import path from "path"; - -export const dynamic = "force-dynamic"; - -const TEST_PDF_DIR = path.join(process.cwd(), "test"); - -const safeResolveTestPdf = (name: string) => { - const trimmed = (name ?? "").trim(); - if (!trimmed) return null; - // 防止目录穿越:只允许同目录文件名 - if (trimmed.includes("/") || trimmed.includes("\\") || trimmed.includes("..")) { - return null; - } - if (!trimmed.toLowerCase().endsWith(".pdf")) { - return null; - } - return path.join(TEST_PDF_DIR, trimmed); -}; - -export async function GET(request: Request) { - // 仅用于本地测试文件的“真跳页链接” - // 生产环境不应该暴露本地磁盘文件,因此默认只允许非生产环境访问 - if (process.env.NODE_ENV === "production") { - return NextResponse.json({ error: "生产环境不支持 test-pdf" }, { status: 404 }); - } - - const url = new URL(request.url); - const name = url.searchParams.get("name") ?? ""; - const file = safeResolveTestPdf(name); - if (!file) { - return NextResponse.json({ error: "文件名非法" }, { status: 400 }); - } - - try { - const buffer = await fs.readFile(file); - return new NextResponse(buffer, { - headers: { - "Content-Type": "application/pdf", - // 内嵌预览,浏览器 PDF 查看器可识别 #page= - "Content-Disposition": `inline; filename*=UTF-8''${encodeURIComponent(name)}`, - "Cache-Control": "no-store", - }, - }); - } catch (error) { - return NextResponse.json({ error: `读取 PDF 失败:${String(error)}` }, { status: 404 }); - } -} - diff --git a/wolai-frontend/src/app/api/mindmap-trash/empty/route.ts b/wolai-frontend/src/app/api/mindmap-trash/empty/route.ts deleted file mode 100644 index 43ad859a..00000000 --- a/wolai-frontend/src/app/api/mindmap-trash/empty/route.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { getAuthedConvexClient } from "@/lib/convex/route"; -import { - buildDocumentBridgeContextWithActor, - buildDocumentCommandEnvelope, - documentBridgeErrorResponse, -} from "@/lib/documents/bridge"; -import { - executeRustBridgeMutationTransport, - resolveRustBridgeCommandPlan, -} from "@/lib/documents/rust-runtime"; - -export const dynamic = "force-dynamic"; - -interface EmptyTrashPayload { - workspaceId?: string; -} - -export async function POST(request: Request) { - if (isConvexEnabled()) { - const { auth, client } = await getAuthedConvexClient(); - const { workspaceId }: EmptyTrashPayload = await request.json().catch(() => ({})); - if (!workspaceId) { - return NextResponse.json({ error: "缺少 workspaceId" }, { status: 400 }); - } - - try { - const context = buildDocumentBridgeContextWithActor({ - request, - actor: { - actorType: "user", - actorId: auth.userId, - sessionId: null, - }, - workspaceId, - source: { - channel: "mindmap-trash-route", - client: "wolai-frontend", - }, - }); - const envelope = buildDocumentCommandEnvelope({ - name: "mindmaps.emptyTrashByWorkspace", - payload: { workspaceId }, - context, - target: { - workspaceId, - }, - reason: "mindmap-trash:empty", - refs: ["task-047", "mindmap-trash"], - }); - const plan = await resolveRustBridgeCommandPlan({ context, envelope }); - const result = await executeRustBridgeMutationTransport<{ - ok?: boolean; - deletedCount?: number; - }>({ - client, - plan, - }); - return NextResponse.json({ ok: true, removed: result?.deletedCount ?? 0 }); - } catch (error) { - return documentBridgeErrorResponse(error); - } - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); -} diff --git a/wolai-frontend/src/app/api/mindmap/[docId]/[mindmapId]/route.ts b/wolai-frontend/src/app/api/mindmap/[docId]/[mindmapId]/route.ts deleted file mode 100644 index 4af47b8d..00000000 --- a/wolai-frontend/src/app/api/mindmap/[docId]/[mindmapId]/route.ts +++ /dev/null @@ -1,439 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { getAuthedConvexClient } from "@/lib/convex/route"; -import { buildMindmapRouteMeta } from "@/lib/mindmap/mindmapRouteMeta"; -import { - buildDocumentBridgeContextWithActor, - buildDocumentCommandEnvelope, - buildDocumentQueryEnvelope, - documentBridgeErrorResponse, -} from "@/lib/documents/bridge"; -import { - executeRustBridgeMutationTransport, - executeRustBridgeQueryTransport, - recordRustBridgeCommandArtifacts, - resolveRustBridgeCommandPlan, - resolveRustBridgeQueryPlan, -} from "@/lib/documents/rust-runtime"; -import { - applyMetadataOnlyMindmapCommands, - isMetadataOnlyMindmapCommandSet, -} from "@/lib/mindmap/mindmap-command-apply-local"; - -const defaultMindmapData = { - data: { text: "中心主题" }, - children: [], -}; - -async function recordMindmapCommandSuccess(args: { - context: ReturnType; - envelope: ReturnType; - client: Awaited>["client"]; - plan: Awaited>; - result: unknown; -}) { - try { - await recordRustBridgeCommandArtifacts(args); - } catch (error) { - console.warn("[mindmap-route] Rust bridge success artifacts skipped:", error); - } -} - -export async function GET( - request: Request, - { params }: { params: Promise<{ docId: string; mindmapId: string }> }, -) { - const { docId, mindmapId } = await params; - const url = new URL(request.url); - const view = url.searchParams.get("view"); - const queryName = - view === "simple_mind_map_scene" || url.searchParams.get("queryName") === "mindmap.simple_mind_map_scene.get" - ? "mindmap.simple_mind_map_scene.get" - : view === "editor_scene" - ? "mindmap.editor_scene.get" - : "mindmaps.get"; - - if (isConvexEnabled()) { - const { auth, client } = await getAuthedConvexClient(); - try { - const context = buildDocumentBridgeContextWithActor({ - request, - actor: { - actorType: "user", - actorId: auth.userId, - sessionId: null, - }, - workspaceId: null, - source: { - channel: "mindmap-route", - client: "wolai-frontend", - }, - }); - const envelope = buildDocumentQueryEnvelope({ - name: queryName, - payload: { - documentId: docId, - mindmapId, - workspaceId: null, - rootNodeId: url.searchParams.get("rootNodeId"), - }, - }); - const plan = await resolveRustBridgeQueryPlan({ context, envelope }); - const res = await executeRustBridgeQueryTransport<{ - ok?: boolean; - data?: unknown; - meta?: { - workspace_id?: string | null; - exists?: boolean; - deleted_at?: string | null; - created_at?: string | null; - updated_at?: string | null; - } | null; - }>({ - client, - plan, - }); - if (queryName === "mindmap.simple_mind_map_scene.get" || queryName === "mindmap.editor_scene.get") { - return NextResponse.json(res); - } - return NextResponse.json({ - data: res?.data ?? defaultMindmapData, - source: "convex", - meta: { - ...buildMindmapRouteMeta(request, { - workspaceId: res?.meta?.workspace_id ?? null, - documentId: docId, - mindmapId, - ownerUserId: auth.userId, - }), - exists: Boolean(res?.meta?.exists), - deletedAt: res?.meta?.deleted_at ?? null, - createdAt: res?.meta?.created_at ?? null, - updatedAt: res?.meta?.updated_at ?? null, - }, - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); -} - -export async function POST( - request: Request, - { params }: { params: Promise<{ docId: string; mindmapId: string }> }, -) { - const { docId, mindmapId } = await params; - - if (isConvexEnabled()) { - const { auth, client } = await getAuthedConvexClient(); - const { data, createOnly, commandName, commands, projectionRevision } = (await request.json().catch(() => ({ data: null }))) as { - data?: unknown; - createOnly?: boolean; - commandName?: string; - commands?: unknown[]; - projectionRevision?: number | null; - }; - - try { - const context = buildDocumentBridgeContextWithActor({ - request, - actor: { - actorType: "user", - actorId: auth.userId, - sessionId: null, - }, - workspaceId: null, - source: { - channel: "mindmap-route", - client: "wolai-frontend", - }, - }); - const isCommandApply = commandName === "mindmap.command.apply"; - const isMetadataOnlyCommandApply = isCommandApply && isMetadataOnlyMindmapCommandSet(Array.isArray(commands) ? commands : []); - if (isMetadataOnlyCommandApply) { - const currentEnvelope = buildDocumentQueryEnvelope({ - name: "mindmaps.get", - payload: { - documentId: docId, - mindmapId, - workspaceId: null, - }, - }); - const currentPlan = await resolveRustBridgeQueryPlan({ context, envelope: currentEnvelope }); - const currentResult = await executeRustBridgeQueryTransport<{ data?: unknown }>({ - client, - plan: currentPlan, - }); - const applied = applyMetadataOnlyMindmapCommands(currentResult?.data ?? defaultMindmapData, commands); - if (applied.errors.length > 0) { - return NextResponse.json( - { - error: "mindmap_command_apply_failed", - details: applied.errors, - }, - { status: 400 }, - ); - } - const putEnvelope = buildDocumentCommandEnvelope({ - name: "mindmaps.put", - payload: { - documentId: docId, - mindmapId, - data: applied.data, - createOnly: false, - }, - context, - target: { - workspaceId: null, - pageId: docId, - blockId: mindmapId, - }, - reason: "mindmap-route:command-apply-metadata-fallback", - refs: ["task-166", "mindmap-command-bridge", "mindmap-metadata-fallback"], - }); - const putPlan = await resolveRustBridgeCommandPlan({ context, envelope: putEnvelope }); - const result = await executeRustBridgeMutationTransport<{ - ok?: boolean; - workspace_id?: string | null; - updated_at?: string | null; - }>({ - client, - plan: putPlan, - }); - await recordMindmapCommandSuccess({ - context, - envelope: putEnvelope, - client, - plan: putPlan, - result, - }); - return NextResponse.json({ - ...(result ?? { ok: true }), - commandName: "mindmap.command.apply", - applied: applied.applied, - errors: applied.errors, - projectionRevision: typeof projectionRevision === "number" ? projectionRevision : null, - meta: { - ...buildMindmapRouteMeta(request, { - workspaceId: result?.workspace_id ?? null, - documentId: docId, - mindmapId, - ownerUserId: auth.userId, - }), - updatedAt: result?.updated_at ?? null, - }, - }); - } - const envelope = buildDocumentCommandEnvelope({ - name: isCommandApply ? "mindmap.command.apply" : "mindmaps.put", - payload: isCommandApply - ? { - documentId: docId, - mindmapId, - workspaceId: null, - commands: Array.isArray(commands) ? commands : [], - projectionRevision: typeof projectionRevision === "number" ? projectionRevision : null, - } - : { - documentId: docId, - mindmapId, - data: data ?? defaultMindmapData, - createOnly: typeof createOnly === "boolean" ? createOnly : false, - }, - context, - target: { - workspaceId: null, - pageId: docId, - blockId: mindmapId, - }, - reason: isCommandApply ? "mindmap-route:command-apply" : "mindmap-route:put", - refs: isCommandApply ? ["task-166", "mindmap-command-bridge"] : ["task-032", "mindmap-route"], - }); - const plan = await resolveRustBridgeCommandPlan({ context, envelope }); - const result = await executeRustBridgeMutationTransport<{ - ok?: boolean; - workspace_id?: string | null; - updated_at?: string | null; - }>({ - client, - plan, - }); - await recordMindmapCommandSuccess({ - context, - envelope, - client, - plan, - result, - }); - return NextResponse.json({ - ...(result ?? { ok: true }), - meta: { - ...buildMindmapRouteMeta(request, { - workspaceId: result?.workspace_id ?? null, - documentId: docId, - mindmapId, - ownerUserId: auth.userId, - }), - updatedAt: result?.updated_at ?? null, - }, - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); -} - -export async function DELETE( - request: Request, - { params }: { params: Promise<{ docId: string; mindmapId: string }> }, -) { - const { docId, mindmapId } = await params; - - if (isConvexEnabled()) { - const { auth, client } = await getAuthedConvexClient(); - try { - const context = buildDocumentBridgeContextWithActor({ - request, - actor: { - actorType: "user", - actorId: auth.userId, - sessionId: null, - }, - workspaceId: null, - source: { - channel: "mindmap-route", - client: "wolai-frontend", - }, - }); - const envelope = buildDocumentCommandEnvelope({ - name: "mindmaps.delete", - payload: { - documentId: docId, - mindmapId, - }, - context, - target: { - workspaceId: null, - pageId: docId, - blockId: mindmapId, - }, - reason: "mindmap-route:delete", - refs: ["task-047", "mindmap-route"], - }); - const plan = await resolveRustBridgeCommandPlan({ context, envelope }); - const result = await executeRustBridgeMutationTransport<{ - ok?: boolean; - workspace_id?: string | null; - deleted_at?: string | null; - }>({ - client, - plan, - }); - await recordMindmapCommandSuccess({ - context, - envelope, - client, - plan, - result, - }); - return NextResponse.json({ - ...(result ?? { ok: true }), - meta: { - ...buildMindmapRouteMeta(request, { - workspaceId: result?.workspace_id ?? null, - documentId: docId, - mindmapId, - ownerUserId: auth.userId, - }), - deletedAt: result?.deleted_at ?? null, - }, - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); -} - -export async function PATCH( - request: Request, - { params }: { params: Promise<{ docId: string; mindmapId: string }> }, -) { - const { docId, mindmapId } = await params; - - if (isConvexEnabled()) { - const { auth, client } = await getAuthedConvexClient(); - const { action } = (await request.json().catch(() => ({}))) as { action?: string }; - if (action !== "restore" && action !== "purge") { - return NextResponse.json({ error: "不支持的操作" }, { status: 400 }); - } - - try { - const context = buildDocumentBridgeContextWithActor({ - request, - actor: { - actorType: "user", - actorId: auth.userId, - sessionId: null, - }, - workspaceId: null, - source: { - channel: "mindmap-route", - client: "wolai-frontend", - }, - }); - const envelope = buildDocumentCommandEnvelope({ - name: action === "purge" ? "mindmaps.purge" : "mindmaps.restore", - payload: { - documentId: docId, - mindmapId, - }, - context, - target: { - workspaceId: null, - pageId: docId, - blockId: mindmapId, - }, - reason: `mindmap-route:${action}`, - refs: ["task-047", "mindmap-route"], - }); - const plan = await resolveRustBridgeCommandPlan({ context, envelope }); - const result = await executeRustBridgeMutationTransport<{ - ok?: boolean; - workspace_id?: string | null; - updated_at?: string | null; - }>({ - client, - plan, - }); - await recordMindmapCommandSuccess({ - context, - envelope, - client, - plan, - result, - }); - return NextResponse.json({ - ...(result ?? { ok: true }), - meta: { - ...buildMindmapRouteMeta(request, { - workspaceId: result?.workspace_id ?? null, - documentId: docId, - mindmapId, - ownerUserId: auth.userId, - }), - updatedAt: action === "restore" ? result?.updated_at ?? null : null, - }, - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); -} diff --git a/wolai-frontend/src/app/api/mindmap/[docId]/route.ts b/wolai-frontend/src/app/api/mindmap/[docId]/route.ts deleted file mode 100644 index 3ceed1c1..00000000 --- a/wolai-frontend/src/app/api/mindmap/[docId]/route.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { getAuthedConvexClient } from "@/lib/convex/route"; -import { - buildDocumentBridgeContextWithActor, - buildDocumentCommandEnvelope, - buildDocumentQueryEnvelope, - documentBridgeErrorResponse, -} from "@/lib/documents/bridge"; -import { - executeRustBridgeMutationTransport, - executeRustBridgeQueryTransport, - resolveRustBridgeCommandPlan, - resolveRustBridgeQueryPlan, -} from "@/lib/documents/rust-runtime"; -import { buildMindmapRouteMeta } from "@/lib/mindmap/mindmapRouteMeta"; - -const defaultMindmapData = { - data: { text: "中心主题" }, - children: [], -}; - -function getLegacyMindmapId(docId: string) { - return `legacy-${docId}`; -} - -export async function GET( - request: Request, - { params }: { params: Promise<{ docId: string }> }, -) { - const { docId } = await params; - - if (isConvexEnabled()) { - const { auth, client } = await getAuthedConvexClient(); - const mindmapId = getLegacyMindmapId(docId); - - try { - const context = buildDocumentBridgeContextWithActor({ - request, - actor: { - actorType: "user", - actorId: auth.userId, - sessionId: null, - }, - workspaceId: null, - source: { - channel: "mindmap-route", - client: "wolai-frontend", - }, - }); - const envelope = buildDocumentQueryEnvelope({ - name: "mindmaps.get", - payload: { - documentId: docId, - mindmapId, - workspaceId: null, - }, - }); - const plan = await resolveRustBridgeQueryPlan({ context, envelope }); - const res = await executeRustBridgeQueryTransport<{ - ok?: boolean; - data?: unknown; - meta?: { - workspace_id?: string | null; - exists?: boolean; - deleted_at?: string | null; - created_at?: string | null; - updated_at?: string | null; - } | null; - }>({ - client, - plan, - }); - return NextResponse.json({ - data: res?.data ?? defaultMindmapData, - source: "convex", - meta: { - ...buildMindmapRouteMeta(request, { - workspaceId: res?.meta?.workspace_id ?? null, - documentId: docId, - mindmapId, - ownerUserId: auth.userId, - }), - exists: Boolean(res?.meta?.exists), - deletedAt: res?.meta?.deleted_at ?? null, - createdAt: res?.meta?.created_at ?? null, - updatedAt: res?.meta?.updated_at ?? null, - }, - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); -} - -export async function POST( - request: Request, - { params }: { params: Promise<{ docId: string }> }, -) { - const { docId } = await params; - - if (isConvexEnabled()) { - const { auth, client } = await getAuthedConvexClient(); - const mindmapId = getLegacyMindmapId(docId); - const { data, createOnly } = (await request.json().catch(() => ({ data: null }))) as { - data?: unknown; - createOnly?: boolean; - }; - - try { - const context = buildDocumentBridgeContextWithActor({ - request, - actor: { - actorType: "user", - actorId: auth.userId, - sessionId: null, - }, - workspaceId: null, - source: { - channel: "mindmap-route", - client: "wolai-frontend", - }, - }); - const envelope = buildDocumentCommandEnvelope({ - name: "mindmaps.put", - payload: { - documentId: docId, - mindmapId, - data: data ?? defaultMindmapData, - createOnly: typeof createOnly === "boolean" ? createOnly : false, - }, - context, - target: { - workspaceId: null, - pageId: docId, - blockId: mindmapId, - }, - reason: "mindmap-route:put", - refs: ["task-032", "mindmap-route"], - }); - const plan = await resolveRustBridgeCommandPlan({ context, envelope }); - const result = await executeRustBridgeMutationTransport<{ - ok?: boolean; - workspace_id?: string | null; - updated_at?: string | null; - }>({ - client, - plan, - }); - return NextResponse.json({ - ...(result ?? { ok: true }), - meta: { - ...buildMindmapRouteMeta(request, { - workspaceId: result?.workspace_id ?? null, - documentId: docId, - mindmapId, - ownerUserId: auth.userId, - }), - updatedAt: result?.updated_at ?? null, - }, - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); -} - -export async function DELETE( - request: Request, - { params }: { params: Promise<{ docId: string }> }, -) { - const { docId } = await params; - - if (isConvexEnabled()) { - const { auth, client } = await getAuthedConvexClient(); - const mindmapId = `legacy-${docId}`; - try { - const context = buildDocumentBridgeContextWithActor({ - request, - actor: { - actorType: "user", - actorId: auth.userId, - sessionId: null, - }, - workspaceId: null, - source: { - channel: "mindmap-route", - client: "wolai-frontend", - }, - }); - const envelope = buildDocumentCommandEnvelope({ - name: "mindmaps.delete", - payload: { - documentId: docId, - mindmapId, - }, - context, - target: { - workspaceId: null, - pageId: docId, - blockId: mindmapId, - }, - reason: "mindmap-route:delete", - refs: ["task-047", "mindmap-route"], - }); - const plan = await resolveRustBridgeCommandPlan({ context, envelope }); - const result = await executeRustBridgeMutationTransport<{ - ok?: boolean; - workspace_id?: string | null; - deleted_at?: string | null; - }>({ - client, - plan, - }); - return NextResponse.json({ - ...(result ?? { ok: true }), - meta: { - ...buildMindmapRouteMeta(request, { - workspaceId: result?.workspace_id ?? null, - documentId: docId, - mindmapId, - ownerUserId: auth.userId, - }), - deletedAt: result?.deleted_at ?? null, - }, - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); -} diff --git a/wolai-frontend/src/app/api/mnote-web/stream/route.test.ts b/wolai-frontend/src/app/api/mnote-web/stream/route.test.ts deleted file mode 100644 index b77d10a3..00000000 --- a/wolai-frontend/src/app/api/mnote-web/stream/route.test.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const mockIsConvexEnabled = vi.fn(); -const mockGetAuthedConvexClient = vi.fn(); -const mockBuildDocumentBridgeContextWithActor = vi.fn(); -const mockBuildDocumentQueryEnvelope = vi.fn(); -const mockExecuteRustBridgeQuery = vi.fn(); -const mockExecuteRustBridgeQueryTransport = vi.fn(); -const mockResolveRustBridgeQueryPlan = vi.fn(); -const mockResolveKernelFileTreeProjection = vi.fn(); -const mockStreamTreeFrames = vi.fn(); - -vi.mock("@/lib/convex/enabled", () => ({ - isConvexEnabled: () => mockIsConvexEnabled(), -})); - -vi.mock("@/lib/convex/route", () => ({ - getAuthedConvexClient: () => mockGetAuthedConvexClient(), -})); - -vi.mock("@/lib/convex/api", () => ({ - api: {}, -})); - -vi.mock("@/lib/documents/bridge", () => ({ - buildDocumentBridgeContextWithActor: (...args: unknown[]) => - mockBuildDocumentBridgeContextWithActor(...args), - buildDocumentQueryEnvelope: (...args: unknown[]) => mockBuildDocumentQueryEnvelope(...args), -})); - -vi.mock("@/lib/documents/rust-runtime", () => ({ - executeRustBridgeQuery: (...args: unknown[]) => mockExecuteRustBridgeQuery(...args), - executeRustBridgeQueryTransport: (...args: unknown[]) => mockExecuteRustBridgeQueryTransport(...args), - resolveRustBridgeQueryPlan: (...args: unknown[]) => mockResolveRustBridgeQueryPlan(...args), -})); - -vi.mock("@/lib/server/kernel-file-tree", async () => { - const actual = await vi.importActual( - "@/lib/server/kernel-file-tree", - ); - return { - ...actual, - resolveKernelFileTreeProjection: (...args: unknown[]) => - mockResolveKernelFileTreeProjection(...args), - }; -}); - -vi.mock("@/lib/tree-stream/server", () => ({ - streamTreeFrames: (...args: unknown[]) => mockStreamTreeFrames(...args), -})); - -async function* makeFrames() { - yield { - event: "snapshot", - payload: { - kind: "snapshot", - stream: "workspace", - workspaceId: "ws_1", - rootNodeId: null, - cursor: "cursor_1", - projection: "sidebar_tree", - requestId: "req_1", - traceId: "trace_1", - data: { activeWorkspaceId: "ws_1", documents: [] }, - snapshot: { dataset: { active_workspace_id: "ws_1", documents: [] } }, - overview: { command_logs: [], domain_events: [] }, - }, - }; -} - -describe("/api/mnote-web/stream route", () => { - beforeEach(() => { - vi.resetModules(); - mockIsConvexEnabled.mockReset().mockReturnValue(true); - mockGetAuthedConvexClient.mockReset().mockResolvedValue({ - auth: { userId: "user_1" }, - client: { query: vi.fn(), mutation: vi.fn() }, - }); - mockBuildDocumentBridgeContextWithActor.mockReset().mockReturnValue({ - requestId: "req_1", - traceId: "trace_1", - actor: { actorType: "user", actorId: "user_1", sessionId: null }, - }); - mockBuildDocumentQueryEnvelope.mockReset().mockImplementation((input) => input); - mockResolveRustBridgeQueryPlan.mockReset().mockResolvedValue({ - argsJson: { workspaceId: "ws_1" }, - functionName: "bridgeLogs:listWorkspaceOverview", - }); - mockExecuteRustBridgeQuery.mockReset(); - mockExecuteRustBridgeQueryTransport.mockImplementation(async ({ plan }) => { - if (plan?.functionName === "bridgeLogs:listWorkspaceOverview") { - return { - command_logs: [], - domain_events: [], - }; - } - return { - active_workspace_id: "ws_1", - workspaces: [], - documents: [], - trashed_documents: [], - media_assets: [], - trashed_media_assets: [], - mindmap_assets: [], - trashed_mindmap_assets: [], - table_assets: [], - trashed_table_assets: [], - mindmap_docs: [], - mindmap_asset_children: {}, - }; - }); - mockResolveKernelFileTreeProjection.mockReset().mockResolvedValue({ - projectionId: "kernel_projection:file_tree:workspace_root", - projection: "file_tree", - rootNodeId: null, - items: [], - edges: [], - }); - mockStreamTreeFrames.mockReset().mockReturnValue(makeFrames()); - }); - - it("应返回 410,明确要求改用 /api/tree/events,不再代理 mnote-web:3104", async () => { - const fetchSpy = vi.spyOn(globalThis, "fetch"); - - const { GET } = await import("./route"); - const response = await GET( - new Request( - "http://127.0.0.1:3000/api/mnote-web/stream?workspaceId=ws_1&cursor=evt_9&rootNodeId=page_root&pollMs=500&maxPolls=0", - { method: "GET" }, - ), - ); - - expect(response.status).toBe(410); - expect(response.headers.get("x-mnote-compat-boundary")).toBe( - "mnote-web-stream-alias-retired", - ); - expect(await response.json()).toMatchObject({ - error: "Next /api/mnote-web/stream compat alias 已退场,请直接使用 /api/tree/events。", - redirectTo: - "http://127.0.0.1:3000/api/tree/events?workspaceId=ws_1&cursor=evt_9&rootNodeId=page_root&pollMs=500&maxPolls=0", - }); - expect(fetchSpy).not.toHaveBeenCalled(); - expect(mockExecuteRustBridgeQuery).not.toHaveBeenCalled(); - expect(mockStreamTreeFrames).not.toHaveBeenCalled(); - }); - - it("Convex 未启用时仍返回 retired alias,不探测 3104", async () => { - mockIsConvexEnabled.mockReturnValue(false); - const fetchSpy = vi.spyOn(globalThis, "fetch"); - - const { GET } = await import("./route"); - const response = await GET( - new Request("http://127.0.0.1:3000/api/mnote-web/stream?workspaceId=ws_1", { - method: "GET", - }), - ); - - expect(response.status).toBe(410); - expect(fetchSpy).not.toHaveBeenCalled(); - expect(await response.json()).toMatchObject({ - error: "Next /api/mnote-web/stream compat alias 已退场,请直接使用 /api/tree/events。", - redirectTo: "http://127.0.0.1:3000/api/tree/events?workspaceId=ws_1", - }); - }); -}); diff --git a/wolai-frontend/src/app/api/mnote-web/stream/route.ts b/wolai-frontend/src/app/api/mnote-web/stream/route.ts deleted file mode 100644 index c31a2237..00000000 --- a/wolai-frontend/src/app/api/mnote-web/stream/route.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { NextResponse } from "next/server"; - -export const dynamic = "force-dynamic"; -export const runtime = "nodejs"; - -export async function GET(request: Request) { - const directUrl = new URL("/api/tree/events", request.url); - directUrl.search = new URL(request.url).search; - return NextResponse.json( - { - error: "Next /api/mnote-web/stream compat alias 已退场,请直接使用 /api/tree/events。", - redirectTo: directUrl.toString(), - }, - { - status: 410, - headers: { - "x-mnote-compat-boundary": "mnote-web-stream-alias-retired", - }, - }, - ); -} diff --git a/wolai-frontend/src/app/api/onlyoffice/callback/route.test.ts b/wolai-frontend/src/app/api/onlyoffice/callback/route.test.ts deleted file mode 100644 index c13c340f..00000000 --- a/wolai-frontend/src/app/api/onlyoffice/callback/route.test.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const mockQuery = vi.fn(); -const mockMutation = vi.fn(); -const mockExecuteMediaAssetWritebackBridgeCommand = vi.fn(); -const mockPrepareOnlyOfficeCallback = vi.fn(); - -vi.mock("@/lib/auth/authContext", () => ({ - HttpError: class HttpError extends Error { - status: number; - - constructor(status: number, message: string) { - super(message); - this.status = status; - } - }, - requireAuthContext: vi.fn(), -})); - -vi.mock("@/lib/api-utils", () => ({ - apiErrorResponse: vi.fn((message: string, status = 500, details?: unknown) => ({ - message, - status, - details, - })), -})); - -vi.mock("@/lib/convex/enabled", () => ({ - isConvexEnabled: () => true, -})); - -vi.mock("@/lib/convex/server", () => ({ - getConvexHttpClient: () => ({ - query: mockQuery, - mutation: mockMutation, - }), -})); - -vi.mock("@/lib/convex/api", () => ({ - api: { - mediaAssets: { - getById: { name: "mediaAssets.getById" }, - generateUploadUrl: { name: "mediaAssets.generateUploadUrl" }, - replaceStorageFromUpload: { name: "mediaAssets.replaceStorageFromUpload" }, - }, - documents: { - getPermissionForUser: { name: "documents.getPermissionForUser" }, - }, - }, -})); - -vi.mock("@/lib/onlyoffice/internal-url", () => ({ - resolveOnlyOfficeInternalUrl: vi.fn().mockResolvedValue("http://127.0.0.1:8082"), -})); - -vi.mock("@/lib/documents/bridge", async () => { - const actual = await vi.importActual("@/lib/documents/bridge"); - return { - ...actual, - buildDocumentBridgeContextWithActor: vi.fn((input) => ({ - requestId: "req_test_onlyoffice_callback", - traceId: "trace_test_onlyoffice_callback", - deploymentId: null, - projectId: null, - workspaceId: input.workspaceId ?? null, - actor: input.actor, - source: input.source ?? { channel: "onlyoffice-callback", client: "vitest" }, - tenantId: null, - authToken: null, - idempotencyKey: input.idempotencyKey ?? null, - validateOnly: false, - dryRun: false, - })), - }; -}); - -vi.mock("@/lib/documents/media-asset-command-adapter", () => ({ - executeMediaAssetWritebackBridgeCommand: mockExecuteMediaAssetWritebackBridgeCommand, -})); - -vi.mock("@/lib/onlyoffice/rust-adapter", () => ({ - prepareOnlyOfficeCallback: mockPrepareOnlyOfficeCallback, -})); - -describe("/api/onlyoffice/callback route", () => { - beforeEach(() => { - mockQuery.mockReset(); - mockMutation.mockReset(); - mockExecuteMediaAssetWritebackBridgeCommand.mockReset(); - mockPrepareOnlyOfficeCallback.mockReset(); - vi.unstubAllGlobals(); - }); - - it("uses media asset bridge adapter for callback writeback", async () => { - const { POST } = await import("./route"); - - mockQuery - .mockResolvedValueOnce({ - id: "asset_1", - document_id: "doc_1", - workspace_id: "ws_1", - mime_type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - }) - .mockResolvedValueOnce({ - permission: "edit", - }); - mockMutation.mockResolvedValueOnce("http://127.0.0.1:3210/api/upload"); - mockExecuteMediaAssetWritebackBridgeCommand.mockResolvedValue({ - requestId: "req_test_onlyoffice_callback", - traceId: "trace_test_onlyoffice_callback", - commandId: "cmd_test_onlyoffice_callback", - commandName: "media.assets.replace_storage", - }); - mockPrepareOnlyOfficeCallback.mockResolvedValue({ - shouldWrite: true, - downloadUrl: "http://127.0.0.1:8082/cache/files/output.docx", - idempotencyKey: "doc_key_1", - locator: { - assetId: "asset_1", - workspaceId: "ws_1", - documentId: "doc_1", - }, - session: { - sessionId: "onlyoffice-callback", - assetId: "asset_1", - workspaceId: "ws_1", - documentId: "doc_1", - userId: "user_1", - }, - }); - - const fetchMock = vi.fn(async (input: RequestInfo | URL) => { - const url = String(input); - if (url === "http://127.0.0.1:8082/cache/files/output.docx") { - return new Response(Buffer.from("docx-binary"), { - status: 200, - headers: { "Content-Type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }, - }); - } - if (url === "http://127.0.0.1:3210/api/upload") { - return Response.json({ storageId: "storage_new_1" }, { status: 200 }); - } - throw new Error(`unexpected fetch: ${url}`); - }); - vi.stubGlobal("fetch", fetchMock); - - const request = new Request( - "http://127.0.0.1:3001/api/onlyoffice/callback?assetId=asset_1&userId=user_1", - { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - status: 2, - key: "doc_key_1", - url: "http://127.0.0.1:8082/cache/files/output.docx", - }), - }, - ); - - const response = await POST(request); - const payload = await response.json(); - - expect(payload).toEqual({ error: 0 }); - expect(mockMutation).toHaveBeenCalledTimes(1); - expect(mockMutation.mock.calls[0]?.[1]).toEqual({ userId: "user_1" }); - expect(mockExecuteMediaAssetWritebackBridgeCommand).toHaveBeenCalledTimes(1); - expect(mockExecuteMediaAssetWritebackBridgeCommand).toHaveBeenCalledWith({ - client: expect.any(Object), - context: expect.objectContaining({ - workspaceId: "ws_1", - actor: { - actorType: "service", - actorId: "user_1", - sessionId: "onlyoffice-callback", - }, - source: { - channel: "onlyoffice-callback", - client: "onlyoffice-document-server", - }, - idempotencyKey: "doc_key_1", - }), - envelope: expect.objectContaining({ - name: "media.assets.replace_storage", - payload: { - assetId: "asset_1", - documentId: "doc_1", - workspaceId: "ws_1", - storageId: "storage_new_1", - userId: "user_1", - }, - target: { - workspaceId: "ws_1", - pageId: "doc_1", - }, - }), - }); - }); -}); diff --git a/wolai-frontend/src/app/api/onlyoffice/callback/route.ts b/wolai-frontend/src/app/api/onlyoffice/callback/route.ts deleted file mode 100644 index 3194342d..00000000 --- a/wolai-frontend/src/app/api/onlyoffice/callback/route.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { getConvexHttpClient } from "@/lib/convex/server"; -import { api } from "@/lib/convex/api"; -import { resolveOnlyOfficeInternalUrl } from "@/lib/onlyoffice/internal-url"; -import { prepareOnlyOfficeCallback } from "@/lib/onlyoffice/rust-adapter"; -import { - buildDocumentBridgeContextWithActor, - buildDocumentCommandEnvelope, -} from "@/lib/documents/bridge"; -import { - executeMediaAssetWritebackBridgeCommand, - type MediaAssetReplaceStoragePayload, -} from "@/lib/documents/media-asset-command-adapter"; - -export const dynamic = "force-dynamic"; - -const ONLYOFFICE_CALLBACK_SECRET = String(process.env.ONLYOFFICE_CALLBACK_SECRET || "").trim(); - -type OnlyOfficeCallbackBody = { - status?: number; - url?: string; - key?: string; -}; - -type OnlyOfficeAssetRecord = { - document_id?: string | null; - workspace_id?: string | null; - mime_type?: string | null; -}; - -type OnlyOfficePermission = { - permission?: string | null; -}; - -const normalizeSecret = (raw: string) => { - const trimmed = String(raw || "").trim(); - if (!trimmed) return ""; - // 兼容用户把 .env 的值写成 "xxx" / 'xxx' - if ( - (trimmed.startsWith("\"") && trimmed.endsWith("\"") && trimmed.length >= 2) || - (trimmed.startsWith("'") && trimmed.endsWith("'") && trimmed.length >= 2) - ) { - return trimmed.slice(1, -1).trim(); - } - return trimmed; -}; - -export async function POST(request: Request) { - try { - const onlyofficeInternalUrl = await resolveOnlyOfficeInternalUrl(); - const { searchParams } = new URL(request.url); - const assetId = searchParams.get("assetId") || ""; - - // 说明:ONLYOFFICE 回调由文档服务器触发,不一定携带用户态;这里提供一个可选的共享密钥校验。 - // 若未配置 ONLYOFFICE_CALLBACK_SECRET,则保持兼容不校验。 - if (ONLYOFFICE_CALLBACK_SECRET) { - const got = normalizeSecret(searchParams.get("token") || ""); - const expected = normalizeSecret(ONLYOFFICE_CALLBACK_SECRET); - if (!got || got !== expected) { - return NextResponse.json({ error: 1 }); - } - } - - const body = (await request.json().catch(() => null)) as OnlyOfficeCallbackBody | null; - if (!body) { - return NextResponse.json({ error: 0 }); - } - - if (!assetId) { - return NextResponse.json({ error: 1 }); - } - - if (isConvexEnabled()) { - // 说明:Convex 模式下,保存写回 Convex Files,并更新 media_assets.storage_id/file_url。 - // 优先使用前端在 /onlyoffice 页面透传的 userId;避免固定 dev-user 导致 workspace membership 校验失败。 - const userId = - String(searchParams.get("userId") || "").trim() || - String(process.env.DEV_USER_ID || "").trim() || - "dev-user"; - const client = getConvexHttpClient(); - - const asset = (await client.query(api.mediaAssets.getById, { userId, id: assetId })) as - | OnlyOfficeAssetRecord - | null; - if (!asset) { - return NextResponse.json({ error: 1 }); - } - const documentId = String(asset.document_id || "").trim(); - const workspaceId = String(asset.workspace_id || "").trim() || null; - const mimeType = String(asset.mime_type || "").trim() || "application/octet-stream"; - - const prepared = await prepareOnlyOfficeCallback({ - request, - assetId, - documentId, - workspaceId, - userId, - sessionId: "onlyoffice-callback", - status: Number(body.status ?? -1), - url: body.url ?? null, - key: body.key ?? null, - onlyofficeInternalUrl, - }); - if (!prepared.shouldWrite) { - return NextResponse.json({ error: 0 }); - } - - // 只读权限:不允许通过 ONLYOFFICE 回调写回 - try { - const perm = (await client.query(api.documents.getPermissionForUser, { - userId, - id: documentId, - })) as OnlyOfficePermission | null; - if (!perm || perm.permission !== "edit") { - return NextResponse.json({ error: 1 }); - } - } catch { - return NextResponse.json({ error: 1 }); - } - - const downloadUrl = String(prepared.downloadUrl || "").trim(); - if (!downloadUrl) { - return NextResponse.json({ error: 1 }); - } - const upstream = await fetch(downloadUrl, { method: "GET", redirect: "follow" }); - if (!upstream.ok) { - return NextResponse.json({ error: 1 }); - } - - const buf = Buffer.from(await upstream.arrayBuffer()); - - const uploadUrl = await client.mutation(api.mediaAssets.generateUploadUrl, { userId }); - if (!uploadUrl || typeof uploadUrl !== "string") { - return NextResponse.json({ error: 1 }); - } - - const uploadRes = await fetch(uploadUrl, { - method: "POST", - headers: { "Content-Type": mimeType }, - body: buf, - }); - - if (!uploadRes.ok) { - return NextResponse.json({ error: 1 }); - } - - const uploadJson = (await uploadRes.json().catch(() => null)) as { storageId?: string } | null; - const storageId = String(uploadJson?.storageId || ""); - if (!storageId) { - return NextResponse.json({ error: 1 }); - } - - const bridgeContext = buildDocumentBridgeContextWithActor({ - request, - workspaceId, - actor: { - actorType: "service", - actorId: userId, - sessionId: "onlyoffice-callback", - }, - source: { - channel: "onlyoffice-callback", - client: "onlyoffice-document-server", - }, - idempotencyKey: String(prepared.idempotencyKey || assetId || "").trim() || null, - }); - const envelope = buildDocumentCommandEnvelope({ - name: "media.assets.replace_storage", - payload: { - assetId, - documentId, - workspaceId, - storageId, - userId, - } satisfies MediaAssetReplaceStoragePayload, - context: bridgeContext, - target: { - workspaceId, - pageId: documentId || null, - }, - reason: "ONLYOFFICE 回调写回附件存储", - refs: [`onlyoffice:asset:${assetId}`], - }); - - await executeMediaAssetWritebackBridgeCommand({ - client, - context: bridgeContext, - envelope, - }); - - return NextResponse.json({ error: 0 }); - } - - return NextResponse.json({ error: 1 }); - } catch (error) { - // 说明:避免异常导致 ONLYOFFICE 重试/阻塞(例如 Convex 未部署新 mutation)。 - console.error("[onlyoffice/callback] convex writeback failed:", error); - return NextResponse.json({ error: 1 }); - } -} diff --git a/wolai-frontend/src/app/api/onlyoffice/forcesave/route.ts b/wolai-frontend/src/app/api/onlyoffice/forcesave/route.ts deleted file mode 100644 index 80e634a7..00000000 --- a/wolai-frontend/src/app/api/onlyoffice/forcesave/route.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { NextResponse } from "next/server"; -import { api } from "@/lib/convex/api"; -import { HttpError } from "@/lib/auth/authContext"; -import { getAuthedConvexClient } from "@/lib/convex/route"; -import { resolveOnlyOfficeInternalUrl } from "@/lib/onlyoffice/internal-url"; -import { documentBridgeErrorResponse } from "@/lib/documents/bridge"; -import { prepareOnlyOfficeForcesave } from "@/lib/onlyoffice/rust-adapter"; - -export const dynamic = "force-dynamic"; - -export async function POST(request: Request) { - const onlyofficeInternalUrl = await resolveOnlyOfficeInternalUrl(); - let auth; - let client; - try { - const authed = await getAuthedConvexClient(); - auth = authed.auth; - client = authed.client; - } catch (err) { - if (err instanceof HttpError) { - return NextResponse.json({ error: "未登录" }, { status: err.status }); - } - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - - const { searchParams } = new URL(request.url); - const assetId = String(searchParams.get("assetId") || "").trim(); - const key = String(searchParams.get("key") || "").trim(); - if (!assetId) return NextResponse.json({ error: "缺少 assetId" }, { status: 400 }); - if (!key) return NextResponse.json({ error: "缺少 key" }, { status: 400 }); - - const asset = await client.query(api.mediaAssets.getById, { userId: auth.userId, id: assetId }); - if (!asset) { - return NextResponse.json({ error: "资源不存在" }, { status: 404 }); - } - - // 只读权限:不允许触发 forcesave(避免只读用户间接写回存储) - const docId = String((asset as any).document_id || "").trim(); - if (!docId) { - return NextResponse.json({ error: "缺少 documentId" }, { status: 400 }); - } - const doc = await client.query(api.documents.getMeta, { id: docId }); - if (!doc || (doc as any).can_edit === false) { - return NextResponse.json({ error: "无权修改(只读共享的文件)" }, { status: 403 }); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const tryJson = async (res: Response) => (await res.json().catch(() => null)) as any; - - try { - const prepared = await prepareOnlyOfficeForcesave({ - request, - assetId, - key, - onlyofficeInternalUrl, - secret: process.env.ONLYOFFICE_JWT_SECRET || "", - actorId: auth.userId, - documentId: docId, - workspaceId: String((asset as any).workspace_id || "").trim() || null, - }); - - const errors: Record = {}; - for (const outbound of prepared.requests) { - const response = await fetch(outbound.url, { - method: outbound.method, - headers: Object.fromEntries(outbound.headers.map((item) => [item.name, item.value])), - body: outbound.bodyJson, - }); - const payload = await tryJson(response); - if (response.ok && Number(payload?.error ?? 0) === 0) { - return NextResponse.json({ ok: true, via: outbound.via, result: payload }); - } - errors[outbound.via] = payload; - } - - return NextResponse.json( - { error: "触发 forcesave 失败", detail: errors }, - { status: 502 }, - ); - } catch (error) { - if (error instanceof Error && error.name === "DocumentBridgeError") { - return documentBridgeErrorResponse(error); - } - console.error("[onlyoffice/forcesave] failed:", error); - return NextResponse.json({ error: "触发 forcesave 失败" }, { status: 502 }); - } -} diff --git a/wolai-frontend/src/app/api/onlyoffice/proxy/route.ts b/wolai-frontend/src/app/api/onlyoffice/proxy/route.ts deleted file mode 100644 index 7f81115a..00000000 --- a/wolai-frontend/src/app/api/onlyoffice/proxy/route.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { NextResponse } from "next/server"; -import { documentBridgeErrorResponse } from "@/lib/documents/bridge"; -import { prepareOnlyOfficeProxyRequest } from "@/lib/onlyoffice/rust-adapter"; -import { getMnoteRuntimeConfig } from "@/lib/runtime-config"; - -export const dynamic = "force-dynamic"; - -const toHeaders = (items: Array<{ name: string; value: string }>) => { - const headers = new Headers(); - for (const item of items) { - if (!item.name || !item.value) continue; - headers.set(item.name, item.value); - } - return headers; -}; - -const handle = async (request: Request, method: "GET" | "HEAD") => { - try { - const { searchParams } = new URL(request.url); - const encodedUrl = String(searchParams.get("u") || "").trim(); - if (!encodedUrl) { - return NextResponse.json({ error: "缺少 u" }, { status: 400 }); - } - - const prepared = await prepareOnlyOfficeProxyRequest({ - request, - encodedUrl, - method, - range: request.headers.get("range"), - runtimeConfig: getMnoteRuntimeConfig(), - }); - - const upstream = await fetch(prepared.targetUrl, { - method, - headers: toHeaders(prepared.forwardHeaders), - redirect: "follow", - }); - - const headers = new Headers(upstream.headers); - headers.delete("set-cookie"); - - if (method === "HEAD") { - return new Response(null, { - status: upstream.status, - statusText: upstream.statusText, - headers, - }); - } - - return new Response(upstream.body, { - status: upstream.status, - statusText: upstream.statusText, - headers, - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } -}; - -export async function GET(request: Request) { - return await handle(request, "GET"); -} - -export async function HEAD(request: Request) { - return await handle(request, "HEAD"); -} diff --git a/wolai-frontend/src/app/api/onlyoffice/sign/route.test.ts b/wolai-frontend/src/app/api/onlyoffice/sign/route.test.ts deleted file mode 100644 index d506549f..00000000 --- a/wolai-frontend/src/app/api/onlyoffice/sign/route.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const mockSignOnlyOfficeConfig = vi.fn(); - -vi.mock("@/lib/api-utils", () => ({ - apiErrorResponse: vi.fn((message: string, status = 500, details?: unknown) => ({ - message, - status, - details, - })), -})); - -vi.mock("@/lib/onlyoffice/rust-adapter", () => ({ - signOnlyOfficeConfig: mockSignOnlyOfficeConfig, -})); - -describe("/api/onlyoffice/sign route", () => { - beforeEach(() => { - mockSignOnlyOfficeConfig.mockReset(); - }); - - it("delegates signing to rust adapter", async () => { - const { POST } = await import("./route"); - mockSignOnlyOfficeConfig.mockResolvedValue({ - token: "token_all", - documentToken: "token_doc", - editorConfigToken: "token_editor", - }); - - const response = await POST( - new Request("http://127.0.0.1:3001/api/onlyoffice/sign", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - config: { - document: { title: "A" }, - editorConfig: { mode: "edit" }, - }, - }), - }), - ); - const payload = await response.json(); - - expect(payload).toEqual({ - token: "token_all", - documentToken: "token_doc", - editorConfigToken: "token_editor", - }); - expect(mockSignOnlyOfficeConfig).toHaveBeenCalledTimes(1); - }); -}); diff --git a/wolai-frontend/src/app/api/onlyoffice/sign/route.ts b/wolai-frontend/src/app/api/onlyoffice/sign/route.ts deleted file mode 100644 index 5bc54c42..00000000 --- a/wolai-frontend/src/app/api/onlyoffice/sign/route.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { NextResponse } from "next/server"; -import { signOnlyOfficeConfig } from "@/lib/onlyoffice/rust-adapter"; - -export const dynamic = "force-dynamic"; - -export async function POST(request: Request) { - try { - const body = (await request.json().catch(() => null)) as { config?: Record } | null; - if (!body?.config) { - return NextResponse.json({ error: "缺少 config" }, { status: 400 }); - } - - const result = await signOnlyOfficeConfig({ - request, - config: body.config, - secret: process.env.ONLYOFFICE_JWT_SECRET || "", - }); - - return NextResponse.json(result); - } catch (error) { - return NextResponse.json( - { error: error instanceof Error ? error.message : "OnlyOffice 签名失败" }, - { status: 500 }, - ); - } -} diff --git a/wolai-frontend/src/app/api/references/backlinks/route.ts b/wolai-frontend/src/app/api/references/backlinks/route.ts deleted file mode 100644 index bb18a849..00000000 --- a/wolai-frontend/src/app/api/references/backlinks/route.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { getAuthedConvexClient } from "@/lib/convex/route"; -import { api } from "@/lib/convex/api"; - -export async function GET(request: Request) { - if (isConvexEnabled()) { - const { auth, client } = await getAuthedConvexClient(); - const { searchParams } = new URL(request.url); - const workspaceId = searchParams.get("workspaceId"); - const pageId = searchParams.get("pageId"); - if (!workspaceId || !pageId) { - return NextResponse.json({ error: "缺少 workspaceId 或 pageId" }, { status: 400 }); - } - - const limit = Number(searchParams.get("limit") ?? "50"); - const offset = Number(searchParams.get("offset") ?? "0"); - - const backlinks = await client.query(api.references.listBacklinks, { - userId: auth.userId, - workspaceId, - pageId, - limit: Number.isFinite(limit) ? limit : 50, - offset: Number.isFinite(offset) ? offset : 0, - }); - - return NextResponse.json({ backlinks }); - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - - /* - const supabase = await createSupabaseRouteClient(); - const { - data: { session }, - } = await supabase.auth.getSession(); - - if (!session) { - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - - const { searchParams } = new URL(request.url); - const workspaceId = searchParams.get("workspaceId"); - const pageId = searchParams.get("pageId"); - const limit = Number(searchParams.get("limit") ?? "50"); - const offset = Number(searchParams.get("offset") ?? "0"); - - if (!workspaceId || !pageId) { - return NextResponse.json({ error: "缺少 workspaceId 或 pageId" }, { status: 400 }); - } - - const { data: membership, error: membershipError } = await supabase - .from("workspace_members") - .select("workspace_id") - .eq("workspace_id", workspaceId) - .eq("user_id", session.user.id) - .maybeSingle(); - - if (membershipError) { - return NextResponse.json({ error: membershipError.message }, { status: 500 }); - } - if (!membership) { - return NextResponse.json({ error: "无权访问该工作空间" }, { status: 403 }); - } - - const { data, error } = await supabase.rpc("list_backlinks", { - p_workspace_id: workspaceId, - p_target_page_id: pageId, - p_limit: Number.isFinite(limit) ? limit : 50, - p_offset: Number.isFinite(offset) ? offset : 0, - }); - - if (error) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } - - return NextResponse.json({ backlinks: data ?? [] }); - */ -} diff --git a/wolai-frontend/src/app/api/references/record/route.ts b/wolai-frontend/src/app/api/references/record/route.ts deleted file mode 100644 index f58900db..00000000 --- a/wolai-frontend/src/app/api/references/record/route.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { getAuthedConvexClient } from "@/lib/convex/route"; -import { api } from "@/lib/convex/api"; - -type DisplayMode = "inline" | "embed"; - -interface RecordReferencePayload { - workspaceId: string; - sourcePageId: string; - targetPageId: string; - sourceBlockId?: string | null; - alias?: string | null; - displayMode: DisplayMode; - isPreviewable?: boolean; -} - -const isValidDisplayMode = (mode: string): mode is DisplayMode => mode === "inline" || mode === "embed"; - -export async function POST(request: Request) { - if (isConvexEnabled()) { - const { auth, client } = await getAuthedConvexClient(); - const body = (await request.json()) as RecordReferencePayload; - if (!body?.workspaceId || !body?.sourcePageId || !body?.targetPageId) { - return NextResponse.json({ error: "缺少必要参数" }, { status: 400 }); - } - if (!isValidDisplayMode(String(body.displayMode ?? ""))) { - return NextResponse.json({ error: "非法的引用模式" }, { status: 400 }); - } - - const reference = await client.mutation(api.references.record, { - userId: auth.userId, - workspaceId: body.workspaceId, - sourcePageId: body.sourcePageId, - targetPageId: body.targetPageId, - sourceBlockId: body.sourceBlockId ?? null, - alias: body.alias ?? null, - displayMode: body.displayMode, - isPreviewable: Boolean(body.isPreviewable ?? true), - }); - - return NextResponse.json({ reference }); - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - - /* - const supabase = await createSupabaseRouteClient(); - const { - data: { session }, - } = await supabase.auth.getSession(); - - if (!session) { - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - - const body = (await request.json()) as RecordReferencePayload; - const { workspaceId, sourcePageId, targetPageId, sourceBlockId, alias, displayMode, isPreviewable = true } = body; - - if (!workspaceId || !sourcePageId || !targetPageId) { - return NextResponse.json({ error: "缺少必要参数" }, { status: 400 }); - } - if (!isValidDisplayMode(displayMode)) { - return NextResponse.json({ error: "非法的引用模式" }, { status: 400 }); - } - - const { data: membership, error: membershipError } = await supabase - .from("workspace_members") - .select("workspace_id") - .eq("workspace_id", workspaceId) - .eq("user_id", session.user.id) - .maybeSingle(); - - if (membershipError) { - return NextResponse.json({ error: membershipError.message }, { status: 500 }); - } - if (!membership) { - return NextResponse.json({ error: "无权访问该工作空间" }, { status: 403 }); - } - - const { data, error } = await supabase.rpc("record_page_ref", { - p_workspace_id: workspaceId, - p_source_page_id: sourcePageId, - p_source_block_id: sourceBlockId ?? null, - p_target_page_id: targetPageId, - p_alias: alias ?? null, - p_display_mode: displayMode, - p_is_previewable: isPreviewable, - p_created_by: session.user.id, - }); - - if (error) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } - - return NextResponse.json({ reference: data }); - */ -} diff --git a/wolai-frontend/src/app/api/search/documents/route.ts b/wolai-frontend/src/app/api/search/documents/route.ts deleted file mode 100644 index ea202b5b..00000000 --- a/wolai-frontend/src/app/api/search/documents/route.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { HttpError } from "@/lib/auth/authContext"; -import { documentBridgeErrorResponse } from "@/lib/documents/bridge"; -import { executeDocumentSearchQuery } from "@/lib/search/search-query-adapter"; -import type { DocumentSearchRequest } from "@/types/search"; - -export const runtime = "nodejs"; - -export async function POST(request: Request) { - try { - if (!isConvexEnabled()) { - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - } - - const payload = (await request.json()) as DocumentSearchRequest; - const { response, meta } = await executeDocumentSearchQuery({ - request, - payload, - }); - - return NextResponse.json(response, { - headers: { - "x-request-id": meta.requestId, - "x-trace-id": meta.traceId, - "x-query-name": meta.queryName, - }, - }); - } catch (error) { - if (error instanceof HttpError) { - return NextResponse.json({ error: error.message }, { status: error.status }); - } - return documentBridgeErrorResponse(error); - } -} diff --git a/wolai-frontend/src/app/api/search/recent/route.ts b/wolai-frontend/src/app/api/search/recent/route.ts deleted file mode 100644 index a345db1e..00000000 --- a/wolai-frontend/src/app/api/search/recent/route.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { getAuthedConvexClient } from "@/lib/convex/route"; -import { api } from "@/lib/convex/api"; - -interface RecentPayload { - workspaceId: string; - documentId: string; -} - -export async function POST(request: Request) { - if (isConvexEnabled()) { - const { auth, client } = await getAuthedConvexClient(); - const { workspaceId, documentId }: RecentPayload = await request.json(); - - if (!workspaceId || !documentId) { - return NextResponse.json({ error: "缺少参数" }, { status: 400 }); - } - - await client.mutation(api.recents.upsert, { - userId: auth.userId, - workspaceId, - documentId, - lastAccessedAt: new Date().toISOString(), - }); - - return NextResponse.json({ ok: true }); - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - - /* - const supabase = await createSupabaseRouteClient(); - const { - data: { session }, - } = await supabase.auth.getSession(); - - if (!session) { - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - - const { workspaceId, documentId }: RecentPayload = await request.json(); - - if (!workspaceId || !documentId) { - return NextResponse.json({ error: "缺少参数" }, { status: 400 }); - } - - const { data: membership, error: membershipError } = await supabase - .from("workspace_members") - .select("workspace_id") - .eq("workspace_id", workspaceId) - .eq("user_id", session.user.id) - .maybeSingle(); - - if (membershipError) { - return NextResponse.json({ error: membershipError.message }, { status: 500 }); - } - if (!membership) { - return NextResponse.json({ error: "无权访问该工作空间" }, { status: 403 }); - } - - const { error } = await supabase.from("user_recent_pages").upsert( - { - user_id: session.user.id, - workspace_id: workspaceId, - document_id: documentId, - last_accessed_at: new Date().toISOString(), - }, - { onConflict: "user_id,document_id" }, - ); - - if (error) { - return NextResponse.json({ error: error.message }, { status: 500 }); - } - - return NextResponse.json({ ok: true }); - */ -} diff --git a/wolai-frontend/src/app/api/sidebar/route.ts b/wolai-frontend/src/app/api/sidebar/route.ts deleted file mode 100644 index b22ead77..00000000 --- a/wolai-frontend/src/app/api/sidebar/route.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { NextResponse } from "next/server"; -import { randomUUID } from "crypto"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { getAuthedConvexClient } from "@/lib/convex/route"; -import { api } from "@/lib/convex/api"; -import { - buildDocumentBridgeContext, - buildDocumentQueryEnvelope, - documentBridgeErrorResponse, -} from "@/lib/documents/bridge"; -import { - executeRustBridgeQueryTransport, - resolveRustBridgeQueryPlan, -} from "@/lib/documents/rust-runtime"; -import { - mapSidebarDatasetListQueryResultToInitialData, - type SidebarDatasetListQueryResult, -} from "@/lib/sidebar-data"; -import { - attachKernelFileTreeProjection, - resolveKernelFileTreeProjection, -} from "@/lib/server/kernel-file-tree"; - -export const dynamic = "force-dynamic"; - -export async function GET(request: Request) { - if (isConvexEnabled()) { - const { auth, client } = await getAuthedConvexClient(); - - const url = new URL(request.url); - const workspaceIdParam = url.searchParams.get("workspaceId"); - const bootstrap = await client.mutation(api.workspaces.ensureDefaultWorkspace, { - fallbackName: auth.email ?? auth.name ?? "我的空间", - workspaceIdIfCreate: randomUUID(), - }); - const targetWorkspaceId = workspaceIdParam?.trim() || bootstrap.activeWorkspaceId || null; - - if (!targetWorkspaceId) { - return NextResponse.json({ error: "暂无可用工作空间" }, { status: 404 }); - } - - try { - const bridgeContext = await buildDocumentBridgeContext({ - request, - workspaceId: targetWorkspaceId, - }); - const envelope = buildDocumentQueryEnvelope({ - name: "sidebar.dataset.list", - payload: { - workspaceId: targetWorkspaceId, - }, - }); - const plan = await resolveRustBridgeQueryPlan({ - context: bridgeContext, - envelope, - }); - const sidebarDataset = await executeRustBridgeQueryTransport({ - client, - plan, - }); - const sidebarDatasetWithFileTree = attachKernelFileTreeProjection({ - dataset: sidebarDataset, - projection: await resolveKernelFileTreeProjection({ - client, - request, - workspaceId: targetWorkspaceId, - actor: { - actorType: "user", - actorId: auth.userId, - sessionId: null, - }, - dataset: sidebarDataset, - }), - }); - const result = mapSidebarDatasetListQueryResultToInitialData(sidebarDatasetWithFileTree); - - return NextResponse.json({ - ...result, - meta: { - requestId: bridgeContext.requestId, - traceId: bridgeContext.traceId, - queryName: envelope.name, - }, - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - - /* - const supabase = await createSupabaseRouteClient(); - const { - data: { session }, - } = await supabase.auth.getSession(); - - if (!session) { - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - - const url = new URL(request.url); - const workspaceIdParam = url.searchParams.get("workspaceId"); - - await ensureDefaultWorkspace(supabase, session.user.id, session.user.email ?? "我的空间"); - const { workspaces, activeWorkspaceId } = await fetchWorkspaceSummaries(supabase, session.user.id); - const targetWorkspaceId = workspaceIdParam || activeWorkspaceId; - - if (!targetWorkspaceId) { - return NextResponse.json({ error: "暂无可用工作空间" }, { status: 404 }); - } - - try { - const dataset = await fetchSidebarDataset(supabase, targetWorkspaceId); - const docIds = dataset.documents.map((d) => d.id); - const localMindmapFiles = await detectLocalMindmapFiles(docIds); - const mindmapAssetChildren = - await detectLocalMindmapImageAssetIdsByMindmapId(localMindmapFiles); - const mindmapDocs = Array.from(new Set([...(dataset.mindmapDocs ?? []), ...(await detectLocalMindmapDocs(docIds))])); - const trashedMindmapAssets = await detectLocalTrashedMindmapAssets(targetWorkspaceId, docIds); - const docById = new Map(dataset.documents.map((d) => [d.id, d])); - const mindmapAssets: MediaAsset[] = localMindmapFiles.map((item) => { - const doc = docById.get(item.documentId); - const workspaceId = doc?.workspace_id ?? targetWorkspaceId; - const fileUrlBase = item.source === "legacy" ? `/mindmaps/${item.documentId}` : `/documents/${item.documentId}`; - return { - id: item.mindmapId, - workspace_id: workspaceId, - document_id: item.documentId, - asset_type: "mindmap", - file_url: `${fileUrlBase}/${item.fileName}`, - thumbnail_url: null, - bucket: null, - storage_path: null, - file_name: item.fileName, - file_size: null, - mime_type: "application/json", - ocr_payload: undefined, - ocr_strategy: null, - ocr_text: null, - ocr_status: null, - signed_url: null, - created_at: "", - updated_at: "", - }; - }); - - const tableAssets: MediaAsset[] = (dataset.tables ?? []).map((row) => { - const base = (row.title ?? "未命名表格").trim() || "未命名表格"; - const fileName = base.toLowerCase().endsWith(".luckysheet") ? base : `${base}.luckysheet`; - return { - id: row.id, - workspace_id: row.workspace_id ?? targetWorkspaceId, - document_id: row.document_id, - asset_type: "luckysheet", - file_url: `/tables/${row.id}/view`, - thumbnail_url: null, - bucket: null, - storage_path: null, - file_name: fileName, - file_size: null, - mime_type: "application/json", - ocr_payload: undefined, - ocr_strategy: null, - ocr_text: null, - ocr_status: null, - signed_url: null, - created_at: row.created_at ?? "", - updated_at: row.updated_at ?? "", - }; - }); - - const payload: SidebarInitialData = { - activeWorkspaceId: targetWorkspaceId, - workspaces, - documents: dataset.documents, - trashedDocuments: dataset.trashedDocuments, - trashedMediaAssets: dataset.trashedMediaAssets ?? [], - trashedMindmapAssets, - mindmapDocs, - mindmapAssets, - mindmapAssetChildren, - tableAssets, - mediaAssets: dataset.mediaAssets ?? [], - }; - - return NextResponse.json(payload); - } catch (error) { - return NextResponse.json( - { error: error instanceof Error ? error.message : "拉取侧边栏数据失败" }, - { status: 500 }, - ); - } - */ -} diff --git a/wolai-frontend/src/app/api/tables/[tableId]/route.ts b/wolai-frontend/src/app/api/tables/[tableId]/route.ts deleted file mode 100644 index d6fbc8b5..00000000 --- a/wolai-frontend/src/app/api/tables/[tableId]/route.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { NextResponse } from "next/server"; -import type { DocumentTableSnapshot, TableRowData } from "@/types/online-table"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { getAuthedConvexClient } from "@/lib/convex/route"; -import { api } from "@/lib/convex/api"; - -interface RouteContext { - params: Promise<{ - tableId: string; - }>; -} - -type UpdateTableRequest = { - title?: string; - snapshot?: DocumentTableSnapshot | null; - rows?: TableRowData[]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - viewPreferences?: Record; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - schema?: Record; -}; - -const extractTableId = async (context: RouteContext) => { - return (await context.params).tableId; -}; - -export async function GET(_request: Request, context: RouteContext) { - const tableId = await extractTableId(context); - if (!tableId) { - return NextResponse.json({ error: "Missing tableId" }, { status: 400 }); - } - - if (isConvexEnabled()) { - const { auth, client } = await getAuthedConvexClient(); - try { - const table = await client.query(api.tables.get, { - userId: auth.userId, - tableId, - }); - - if (!table) { - return NextResponse.json({ error: "Table not found" }, { status: 404 }); - } - - return NextResponse.json(table, { status: 200 }); - } catch (error) { - console.error("Convex API error:", error); - return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); - } - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); -} - -export async function PATCH(request: Request, context: RouteContext) { - const tableId = await extractTableId(context); - if (!tableId) { - return NextResponse.json({ error: "Missing tableId" }, { status: 400 }); - } - - if (isConvexEnabled()) { - const { auth, client } = await getAuthedConvexClient(); - let body: UpdateTableRequest; - try { - body = (await request.json()) as UpdateTableRequest; - } catch (error) { - console.error("Invalid payload:", error); - return NextResponse.json({ error: "Invalid payload" }, { status: 400 }); - } - - try { - await client.mutation(api.tables.update, { - userId: auth.userId, - tableId, - title: body.title, - schema: body.schema, - view_preferences: body.viewPreferences, - snapshot: body.snapshot, - rows: body.rows, - }); - - const updated = await client.query(api.tables.get, { - userId: auth.userId, - tableId, - }); - - return NextResponse.json(updated, { status: 200 }); - } catch (error) { - console.error("Convex API error:", error); - return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); - } - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); -} - -export async function DELETE(_request: Request, context: RouteContext) { - const tableId = await extractTableId(context); - if (!tableId) { - return NextResponse.json({ error: "Missing tableId" }, { status: 400 }); - } - - if (isConvexEnabled()) { - const { auth, client } = await getAuthedConvexClient(); - try { - // 软删除:进入垃圾桶(可恢复) - await client.mutation(api.tables.remove, { - userId: auth.userId, - tableId, - }); - - return NextResponse.json({ success: true }, { status: 200 }); - } catch (error) { - console.error("Convex API error:", error); - return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); - } - } - - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); -} diff --git a/wolai-frontend/src/app/api/tables/create/route.ts b/wolai-frontend/src/app/api/tables/create/route.ts deleted file mode 100644 index 5f58874e..00000000 --- a/wolai-frontend/src/app/api/tables/create/route.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { NextResponse } from "next/server"; -import { DocumentTableSnapshot, TableSchema } from "@/types/online-table"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { getAuthedConvexClient } from "@/lib/convex/route"; -import { api } from "@/lib/convex/api"; - -// 定义请求体类型 -interface CreateTableRequestBody { - documentId: string; - title: string; - schema: TableSchema; - snapshot?: DocumentTableSnapshot | null; -} - -export async function POST(request: Request) { - // Convex 模式 - if (isConvexEnabled()) { - const { auth, client } = await getAuthedConvexClient(); - - try { - const { documentId, title, schema, snapshot } = await request.json() as CreateTableRequestBody; - - if (!documentId || !title || !schema) { - return NextResponse.json({ error: "Missing required fields" }, { status: 400 }); - } - - // 获取 document 所在的 workspace_id - const document = await client.query(api.documents.getMeta, { - id: documentId, - }); - - if (!document) { - return NextResponse.json({ error: "Document not found" }, { status: 404 }); - } - - // 创建表格 - const result = await client.mutation(api.tables.create, { - userId: auth.userId, - workspaceId: document.workspace_id, - documentId, - title, - schema, - snapshot, - }); - - return NextResponse.json(result, { status: 201 }); - - } catch (error) { - console.error("Convex API error:", error); - return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); - } - } - - // Supabase 模式 - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - - /* - const supabase = await createSupabaseRouteClient(); - - const { data: { user } } = await supabase.auth.getUser(); - - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - try { - const { documentId, title, schema, snapshot } = await request.json() as CreateTableRequestBody; - - if (!documentId || !title || !schema) { - return NextResponse.json({ error: "Missing required fields" }, { status: 400 }); - } - - // 1. 获取 document 所在的 workspace_id - const { data: documentData, error: documentError } = await supabase - .from("documents") - .select("workspace_id") - .eq("id", documentId) - .single(); - - if (documentError || !documentData?.workspace_id) { - console.error("Error fetching document or workspace:", documentError); - return NextResponse.json({ error: "Document not found or missing workspace_id" }, { status: 404 }); - } - - const workspaceId = documentData.workspace_id; - - // 2. 插入新的 document_tables 记录 - const { data: newTable, error: insertError } = await supabase - .from("document_tables") - .insert({ - workspace_id: workspaceId, - document_id: documentId, - title: title, - schema: schema, - view_preferences: {}, - is_archived: false, - created_by: user.id, - updated_by: user.id, - snapshot: snapshot ?? {}, - }) - .select() - .single(); - - if (insertError) { - console.error("Error inserting table:", insertError); - return NextResponse.json({ error: "Failed to create table in database" }, { status: 500 }); - } - - // 假设 document_tables 返回的结构与 DocumentTable 接口兼容 - return NextResponse.json(newTable, { status: 201 }); - - } catch (error) { - console.error("API error:", error); - return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); - } - */ -} diff --git a/wolai-frontend/src/app/api/tables/empty-trash/route.ts b/wolai-frontend/src/app/api/tables/empty-trash/route.ts deleted file mode 100644 index 20e2166b..00000000 --- a/wolai-frontend/src/app/api/tables/empty-trash/route.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { HttpError, requireAuthContext } from "@/lib/auth/authContext"; -import { api } from "@/lib/convex/api"; -import { getConvexAuthedHttpClient } from "@/lib/convex/server"; - -export const dynamic = "force-dynamic"; - -interface EmptyTrashPayload { - workspaceId?: string; -} - -function makeExpiredDeletedAtForForceEmpty(): string { - // “清空附件垃圾桶”是一个显式的强制操作:直接清空所有垃圾桶内的表格,不受 10 分钟宽限期限制。 - // Convex 侧按 `deleted_at <= expiredDeletedAt` 做过滤,因此这里给一个很靠后的时间上界即可。 - return new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 100).toISOString(); -} - -export async function POST(request: Request) { - if (!isConvexEnabled()) { - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - } - - let auth; - try { - auth = await requireAuthContext(); - } catch (err) { - if (err instanceof HttpError) { - return NextResponse.json({ error: "未登录" }, { status: err.status }); - } - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - - const { workspaceId }: EmptyTrashPayload = await request.json().catch(() => ({})); - if (!workspaceId) { - return NextResponse.json({ error: "缺少 workspaceId" }, { status: 400 }); - } - - const client = await getConvexAuthedHttpClient(); - try { - const res = await client.mutation(api.tables.emptyTrashByWorkspace, { - userId: auth.userId, - workspaceId, - expiredDeletedAt: makeExpiredDeletedAtForForceEmpty(), - }); - return NextResponse.json({ success: true, ...(res as any) }); - } catch (err) { - const message = err instanceof Error ? err.message : "清空失败"; - return NextResponse.json({ error: message }, { status: 500 }); - } -} diff --git a/wolai-frontend/src/app/api/tables/purge/route.ts b/wolai-frontend/src/app/api/tables/purge/route.ts deleted file mode 100644 index a5f3c4c5..00000000 --- a/wolai-frontend/src/app/api/tables/purge/route.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { getAuthedConvexClient } from "@/lib/convex/route"; -import { api } from "@/lib/convex/api"; - -export const dynamic = "force-dynamic"; - -export async function POST(request: Request) { - if (!isConvexEnabled()) { - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - } - - const { tableId } = (await request.json().catch(() => ({}))) as { tableId?: string }; - if (!tableId) { - return NextResponse.json({ error: "缺少 tableId" }, { status: 400 }); - } - - const { auth, client } = await getAuthedConvexClient(); - try { - await client.mutation(api.tables.purge, { userId: auth.userId, tableId }); - return NextResponse.json({ success: true }, { status: 200 }); - } catch (error) { - console.error("Convex API error:", error); - return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); - } -} - diff --git a/wolai-frontend/src/app/api/tables/restore/route.ts b/wolai-frontend/src/app/api/tables/restore/route.ts deleted file mode 100644 index 20f337d1..00000000 --- a/wolai-frontend/src/app/api/tables/restore/route.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { getAuthedConvexClient } from "@/lib/convex/route"; -import { api } from "@/lib/convex/api"; - -export const dynamic = "force-dynamic"; - -export async function POST(request: Request) { - if (!isConvexEnabled()) { - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - } - - const { tableId } = (await request.json().catch(() => ({}))) as { tableId?: string }; - if (!tableId) { - return NextResponse.json({ error: "缺少 tableId" }, { status: 400 }); - } - - const { auth, client } = await getAuthedConvexClient(); - try { - await client.mutation(api.tables.restore, { userId: auth.userId, tableId }); - return NextResponse.json({ success: true }, { status: 200 }); - } catch (error) { - console.error("Convex API error:", error); - return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); - } -} - diff --git a/wolai-frontend/src/app/api/tree-shell-runtime/[...asset]/route.test.ts b/wolai-frontend/src/app/api/tree-shell-runtime/[...asset]/route.test.ts deleted file mode 100644 index a028aa13..00000000 --- a/wolai-frontend/src/app/api/tree-shell-runtime/[...asset]/route.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { promises as fs } from "node:fs"; -import os from "node:os"; -import path from "node:path"; - -let generatedRoot = ""; - -const JS_ASSET_NAME = "mnote-tree-shell-runtime.js"; -const WASM_ASSET_NAME = "mnote-tree-shell-runtime_bg.wasm"; - -async function writeFixtureFile(relativePath: string, content: string | Uint8Array) { - const absolutePath = path.join(generatedRoot, relativePath); - await fs.mkdir(path.dirname(absolutePath), { recursive: true }); - await fs.writeFile(absolutePath, content); -} - -describe("/api/tree-shell-runtime/[...asset] route", () => { - beforeEach(async () => { - generatedRoot = await fs.mkdtemp(path.join(os.tmpdir(), "mnote-tree-shell-runtime-")); - process.env.TREE_SHELL_RUNTIME_GENERATED_ROOT = generatedRoot; - }); - - afterEach(async () => { - delete process.env.TREE_SHELL_RUNTIME_GENERATED_ROOT; - await fs.rm(generatedRoot, { recursive: true, force: true }); - generatedRoot = ""; - }); - - it("返回固定资源名 manifest,并暴露同源 js/wasm 资源路径", async () => { - await writeFixtureFile(JS_ASSET_NAME, "export function reduceTreeShellRuntime() {}\n"); - await writeFixtureFile(WASM_ASSET_NAME, new Uint8Array([0x00, 0x61, 0x73, 0x6d])); - - const { GET } = await import("./route"); - const response = await GET(new Request("http://127.0.0.1:3000/api/tree-shell-runtime/manifest.json"), { - params: Promise.resolve({ asset: ["manifest.json"] }), - }); - - expect(response.status).toBe(200); - expect(response.headers.get("cache-control")).toBe("no-store"); - expect(await response.json()).toEqual({ - jsGlueAssetPath: `/api/tree-shell-runtime/${JS_ASSET_NAME}`, - wasmAssetPath: `/api/tree-shell-runtime/${WASM_ASSET_NAME}`, - assetPaths: [ - `/api/tree-shell-runtime/${JS_ASSET_NAME}`, - `/api/tree-shell-runtime/${WASM_ASSET_NAME}`, - ], - generatedRootPath: generatedRoot.split(path.sep).join("/"), - }); - }); - - it("返回具体 wasm/js 资源内容,并拒绝越界路径", async () => { - await writeFixtureFile(JS_ASSET_NAME, "export const runtimeVersion = 1;\n"); - await writeFixtureFile(WASM_ASSET_NAME, new Uint8Array([0x00, 0x61, 0x73, 0x6d])); - - const { GET } = await import("./route"); - - const jsResponse = await GET( - new Request(`http://127.0.0.1:3000/api/tree-shell-runtime/${JS_ASSET_NAME}`), - { params: Promise.resolve({ asset: [JS_ASSET_NAME] }) }, - ); - expect(jsResponse.status).toBe(200); - expect(jsResponse.headers.get("content-type")).toContain("application/javascript"); - expect(await jsResponse.text()).toContain("runtimeVersion"); - - const wasmResponse = await GET( - new Request(`http://127.0.0.1:3000/api/tree-shell-runtime/${WASM_ASSET_NAME}`), - { params: Promise.resolve({ asset: [WASM_ASSET_NAME] }) }, - ); - expect(wasmResponse.status).toBe(200); - expect(wasmResponse.headers.get("content-type")).toBe("application/wasm"); - expect(new Uint8Array(await wasmResponse.arrayBuffer())).toEqual( - new Uint8Array([0x00, 0x61, 0x73, 0x6d]), - ); - - const invalidResponse = await GET( - new Request("http://127.0.0.1:3000/api/tree-shell-runtime/../../secret.txt"), - { params: Promise.resolve({ asset: ["..", "..", "secret.txt"] }) }, - ); - expect(invalidResponse.status).toBe(400); - expect(await invalidResponse.json()).toEqual({ - error: "非法 tree shell runtime 资源路径", - }); - }); -}); diff --git a/wolai-frontend/src/app/api/tree-shell-runtime/[...asset]/route.ts b/wolai-frontend/src/app/api/tree-shell-runtime/[...asset]/route.ts deleted file mode 100644 index 2092a474..00000000 --- a/wolai-frontend/src/app/api/tree-shell-runtime/[...asset]/route.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { promises as fs } from "node:fs"; -import path from "node:path"; -import { NextResponse } from "next/server"; - -export const runtime = "nodejs"; -export const dynamic = "force-dynamic"; - -const DEFAULT_GENERATED_ROOT = path.resolve( - process.cwd(), - "..", - "rust", - "crates", - "tree-shell-runtime-wasm", - "generated", -); -const JS_ASSET_NAME = "mnote-tree-shell-runtime.js"; -const WASM_ASSET_NAME = "mnote-tree-shell-runtime_bg.wasm"; - -type TreeShellRuntimeManifest = { - jsGlueAssetPath: string; - wasmAssetPath: string; - assetPaths: string[]; - generatedRootPath: string; -}; - -function toPosixPath(value: string): string { - return value.split(path.sep).join("/"); -} - -function resolveGeneratedRoot(): string { - return path.resolve(process.env.TREE_SHELL_RUNTIME_GENERATED_ROOT || DEFAULT_GENERATED_ROOT); -} - -function toAssetRoutePath(fileName: string): string { - return `/api/tree-shell-runtime/${fileName}`; -} - -function sanitizeRelativePath(asset: string[]): string | null { - if (!Array.isArray(asset) || asset.length === 0) { - return null; - } - const decoded = asset.map((segment) => decodeURIComponent(segment)); - const joined = decoded.join("/"); - if (!joined || joined.includes("\0")) { - return null; - } - const normalized = path.posix.normalize(joined); - if (normalized === "." || normalized.startsWith("../") || normalized.includes("/../")) { - return null; - } - return normalized; -} - -async function fileExists(absolutePath: string): Promise { - try { - await fs.access(absolutePath); - return true; - } catch { - return false; - } -} - -async function buildManifest(): Promise { - const generatedRoot = resolveGeneratedRoot(); - const assetCandidates = [JS_ASSET_NAME, WASM_ASSET_NAME]; - const existingAssets: string[] = []; - - for (const assetName of assetCandidates) { - const absolutePath = path.join(generatedRoot, assetName); - if (await fileExists(absolutePath)) { - existingAssets.push(toAssetRoutePath(assetName)); - } - } - - return { - jsGlueAssetPath: toAssetRoutePath(JS_ASSET_NAME), - wasmAssetPath: toAssetRoutePath(WASM_ASSET_NAME), - assetPaths: existingAssets, - generatedRootPath: toPosixPath(generatedRoot), - }; -} - -function guessContentType(absolutePath: string): string { - const extension = path.extname(absolutePath).toLowerCase(); - if (extension === ".js" || extension === ".mjs") { - return "application/javascript; charset=utf-8"; - } - if (extension === ".wasm") { - return "application/wasm"; - } - if (extension === ".json") { - return "application/json; charset=utf-8"; - } - return "application/octet-stream"; -} - -export async function GET( - _request: Request, - context: { params: Promise<{ asset?: string[] }> }, -) { - const params = await context.params; - const asset = params.asset ?? []; - - if (asset.length === 1 && asset[0] === "manifest.json") { - try { - const manifest = await buildManifest(); - return NextResponse.json(manifest, { - headers: { "Cache-Control": "no-store" }, - }); - } catch (error) { - return NextResponse.json( - { - error: "无法生成 tree shell runtime 清单", - detail: error instanceof Error ? error.message : "unknown", - }, - { status: 500 }, - ); - } - } - - const relativePath = sanitizeRelativePath(asset); - if (!relativePath) { - return NextResponse.json({ error: "非法 tree shell runtime 资源路径" }, { status: 400 }); - } - - const generatedRoot = resolveGeneratedRoot(); - const absolutePath = path.resolve(generatedRoot, relativePath); - if (!absolutePath.startsWith(generatedRoot + path.sep)) { - return NextResponse.json({ error: "越界访问 tree shell runtime 资源被拒绝" }, { status: 403 }); - } - - try { - const fileContent = await fs.readFile(absolutePath); - return new NextResponse(fileContent, { - status: 200, - headers: { - "Content-Type": guessContentType(absolutePath), - "Cache-Control": "no-store", - }, - }); - } catch (error) { - if ((error as NodeJS.ErrnoException)?.code === "ENOENT") { - return NextResponse.json({ error: "tree shell runtime 资源不存在" }, { status: 404 }); - } - return NextResponse.json( - { - error: "读取 tree shell runtime 资源失败", - detail: error instanceof Error ? error.message : "unknown", - }, - { status: 500 }, - ); - } -} diff --git a/wolai-frontend/src/app/api/tree/commands/route.test.ts b/wolai-frontend/src/app/api/tree/commands/route.test.ts deleted file mode 100644 index 526df238..00000000 --- a/wolai-frontend/src/app/api/tree/commands/route.test.ts +++ /dev/null @@ -1,1756 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { TREE_3000_ROUTE_BOUNDARY_MANIFEST } from "@/lib/tree-route-boundary"; - -const mockIsConvexEnabled = vi.fn(() => true); -const mockGetAuthedConvexClient = vi.fn(); -const mockBuildDocumentBridgeContext = vi.fn(); -const mockBuildDocumentCommandEnvelope = vi.fn(); -const mockResolveRustBridgeCommandPlan = vi.fn(); -const mockExecuteRustBridgeMutationTransport = vi.fn(); -const mockRecordRustBridgeCommandArtifacts = vi.fn(); -const mockRecordBridgeCommandArtifacts = vi.fn(); -const mockRecordBridgeCommandFailureArtifacts = vi.fn(); -const mockDocumentBridgeErrorResponse = vi.fn((error: unknown) => - Response.json( - { - error: error instanceof Error ? error.message : String(error), - }, - { - status: - typeof (error as { status?: unknown })?.status === "number" - ? ((error as { status: number }).status ?? 500) - : 500, - }, - ), -); -const mockEnsureDocumentScaffold = vi.fn(); -const mockCopyMindmapFilesIfExists = vi.fn(); -const mockLoadSidebarDataFromConvex = vi.fn(); - -vi.mock("@/lib/convex/enabled", () => ({ - isConvexEnabled: () => mockIsConvexEnabled(), -})); - -vi.mock("@/lib/convex/api", () => ({ - api: { - documents: { - getMeta: "documents:getMeta", - getContent: "documents:getContent", - }, - workspaces: { - ensureDefaultWorkspace: "workspaces:ensureDefaultWorkspace", - }, - }, -})); - -vi.mock("@/lib/convex/route", () => ({ - getAuthedConvexClient: mockGetAuthedConvexClient, -})); - -vi.mock("@/lib/documents/bridge", () => ({ - assertDocumentId: (value: string | null | undefined) => { - const normalized = typeof value === "string" ? value.trim() : ""; - if (!normalized) { - throw new Error("缺少 documentId"); - } - return normalized; - }, - assertTitle: (value: string | null | undefined) => { - const normalized = typeof value === "string" ? value.trim() : ""; - if (!normalized) { - throw new Error("缺少标题"); - } - return normalized; - }, - buildDocumentBridgeContext: mockBuildDocumentBridgeContext, - buildDocumentCommandEnvelope: mockBuildDocumentCommandEnvelope, - documentBridgeErrorResponse: (...args: Parameters) => - mockDocumentBridgeErrorResponse(...args), -})); - -vi.mock("@/lib/documents/rust-runtime", () => ({ - resolveRustBridgeCommandPlan: (...args: Parameters) => - mockResolveRustBridgeCommandPlan(...args), - executeRustBridgeMutationTransport: (...args: Parameters) => - mockExecuteRustBridgeMutationTransport(...args), - recordRustBridgeCommandArtifacts: (...args: Parameters) => - mockRecordRustBridgeCommandArtifacts(...args), - readRustTreeDomainEventType: (plan: { argsJson?: Record }) => { - const eventPlan = plan.argsJson?.domainEventPlan as - | { - family?: string; - eventType?: string; - } - | undefined; - if (eventPlan?.family === "tree" && typeof eventPlan.eventType === "string") { - return eventPlan.eventType; - } - const hint = plan.argsJson?.domainEventHint as - | { - family?: string; - eventType?: string; - } - | undefined; - return hint?.family === "tree" && typeof hint.eventType === "string" ? hint.eventType : null; - }, - materializeRustTreeDomainEventPlan: (input: { - plan: { argsJson?: Record }; - streamDelta?: Record | null; - }) => { - const eventPlan = input.plan.argsJson?.domainEventPlan as - | { - family?: string; - schema?: string; - schemaVersion?: number; - eventType?: string; - } - | undefined; - if ( - eventPlan?.family !== "tree" || - eventPlan.schema !== "mnote.tree.domain_event" || - eventPlan.schemaVersion !== 1 || - typeof eventPlan.eventType !== "string" - ) { - return null; - } - return { - family: "tree", - schema: "mnote.tree.domain_event", - schemaVersion: 1, - eventType: eventPlan.eventType, - ...(input.streamDelta ? { streamDelta: input.streamDelta } : {}), - }; - }, - materializeRustTreeStreamDelta: (input: { plan: { argsJson?: Record }; result: unknown }) => { - const hint = input.plan.argsJson?.streamDeltaHint as - | { - family?: string; - kind?: string; - args?: Record; - } - | undefined; - if (hint?.family !== "tree" || !hint.kind) return null; - const args = hint.args ?? {}; - const result = input.result as Record; - if (hint.kind === "document_result") { - return result.document ? { op: "upsert_document", document: result.document } : null; - } - if (hint.kind === "upsert_document_patch") { - return { - op: "upsert_document", - document: { - id: args.documentId, - ...(args.patch as Record), - updated_at: result.updated_at ?? null, - }, - }; - } - if (hint.kind === "move_document") { - return { - op: "move_document", - documentId: args.documentId, - parentId: result.parent_id ?? args.parentId ?? null, - sortOrder: result.sort_order ?? args.sortOrder, - updatedAt: result.updated_at, - }; - } - if (hint.kind === "remove_document") { - return { op: "remove_document", documentId: args.documentId }; - } - if (hint.kind === "noop") { - return { op: "noop" }; - } - if (hint.kind === "copy_result") { - return { - op: "upsert_documents", - upsertDocuments: Array.isArray(result.items) - ? result.items.map((item) => item?.document).filter(Boolean) - : [], - }; - } - return null; - }, -})); - -vi.mock("@/lib/documents/bridge-log", () => ({ - recordBridgeCommandArtifacts: (...args: unknown[]) => mockRecordBridgeCommandArtifacts(...args), - recordBridgeCommandFailureArtifacts: (...args: unknown[]) => mockRecordBridgeCommandFailureArtifacts(...args), -})); - -vi.mock("@/lib/documents/page-lifecycle-side-effects", () => ({ - ensureDocumentScaffold: (...args: unknown[]) => mockEnsureDocumentScaffold(...args), - copyMindmapFilesIfExists: (...args: unknown[]) => mockCopyMindmapFilesIfExists(...args), -})); - -vi.mock("@/lib/server/sidebar-data", () => ({ - loadSidebarDataFromConvex: (...args: unknown[]) => mockLoadSidebarDataFromConvex(...args), -})); - -describe("/api/tree/commands route", () => { - beforeEach(() => { - mockIsConvexEnabled.mockReturnValue(true); - mockGetAuthedConvexClient.mockReset(); - mockBuildDocumentBridgeContext.mockReset(); - mockBuildDocumentCommandEnvelope.mockReset(); - mockResolveRustBridgeCommandPlan.mockReset(); - mockExecuteRustBridgeMutationTransport.mockReset(); - mockRecordRustBridgeCommandArtifacts.mockReset().mockResolvedValue(null); - mockRecordBridgeCommandArtifacts.mockReset(); - mockRecordBridgeCommandFailureArtifacts.mockReset(); - mockEnsureDocumentScaffold.mockReset(); - mockCopyMindmapFilesIfExists.mockReset(); - mockLoadSidebarDataFromConvex.mockReset(); - mockDocumentBridgeErrorResponse.mockClear(); - }); - - it("create action 走 tree.node.create,并保留本地 scaffold 副作用", async () => { - expect(TREE_3000_ROUTE_BOUNDARY_MANIFEST.routes).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: "tree.commands", - role: "rust-owned", - owner: "rust-web-gateway", - }), - ]), - ); - expect(TREE_3000_ROUTE_BOUNDARY_MANIFEST.rustOwnedSemantics).toContain("tree.node.create"); - expect(TREE_3000_ROUTE_BOUNDARY_MANIFEST.browserSubstrateDuties).toContain( - "新页面 scaffold 文件创建", - ); - - const client = { - mutation: vi.fn(async () => ({ activeWorkspaceId: "ws_root" })), - query: vi.fn(), - }; - mockGetAuthedConvexClient.mockResolvedValue({ - auth: { - userId: "user_1", - email: "dev@example.com", - name: "开发用户", - }, - client, - }); - mockBuildDocumentBridgeContext.mockResolvedValue({ - requestId: "req_tree_1", - traceId: "trace_tree_1", - workspaceId: "ws_root", - actor: { - actorType: "user", - actorId: "user_1", - sessionId: null, - }, - source: { - channel: "next-route", - client: "wolai-frontend", - }, - tenantId: null, - deploymentId: null, - projectId: null, - authToken: null, - idempotencyKey: null, - validateOnly: false, - dryRun: false, - }); - mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); - mockResolveRustBridgeCommandPlan.mockResolvedValue({ - kind: "command", - commandName: "tree.node.create", - commandId: "cmd_tree_create_1", - functionName: "documents:createWithParentReference", - workspaceId: "ws_root", - requestId: "req_tree_1", - traceId: "trace_tree_1", - actorId: "user_1", - idempotencyKey: null, - payloadJson: "{}", - argsJson: { - streamDeltaHint: { - family: "tree", - kind: "document_result", - args: { documentField: "document" }, - }, - domainEventHint: { - family: "tree", - eventType: "tree.node.created", - }, - }, - }); - mockExecuteRustBridgeMutationTransport.mockResolvedValue({ - id: "doc_new", - title: "无标题", - parent_id: null, - sort_order: 0, - workspace_id: "ws_root", - access_scope: "private", - is_template: false, - created_at: "2026-04-23T00:00:00Z", - updated_at: "2026-04-23T00:00:00Z", - document: { - id: "doc_new", - workspace_id: "ws_root", - title: "无标题", - parent_id: null, - sort_order: 0, - is_starred: false, - access_scope: "private", - is_template: false, - created_at: "2026-04-23T00:00:00Z", - updated_at: "2026-04-23T00:00:00Z", - }, - }); - - const { POST } = await import("./route"); - const response = await POST( - new Request("http://127.0.0.1:3000/api/tree/commands", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - action: "create", - parentId: null, - }), - }), - ); - - expect(response.status).toBe(200); - const payload = await response.json() as { - result: { - action: string; - documentId: string; - workspaceId: string; - title: string; - }; - }; - expect(payload.result.action).toBe("create"); - expect(payload.result.documentId).toBe("doc_new"); - expect(payload.result.workspaceId).toBe("ws_root"); - expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( - expect.objectContaining({ - name: "tree.node.create", - payload: expect.objectContaining({ - workspaceId: "ws_root", - parentId: null, - title: "无标题", - accessScope: "private", - }), - }), - ); - expect(mockEnsureDocumentScaffold).toHaveBeenCalledWith("doc_new", "无标题"); - expect(mockRecordRustBridgeCommandArtifacts).toHaveBeenCalledWith( - expect.objectContaining({ - context: expect.objectContaining({ - requestId: "req_tree_1", - traceId: "trace_tree_1", - }), - envelope: expect.objectContaining({ - name: "tree.node.create", - }), - plan: expect.objectContaining({ - commandName: "tree.node.create", - functionName: "documents:createWithParentReference", - }), - result: expect.objectContaining({ - id: "doc_new", - workspace_id: "ws_root", - }), - }), - ); - expect(mockRecordBridgeCommandArtifacts).not.toHaveBeenCalled(); - }); - - it("move action 走 tree.subtree.move,并把 position 归一化为 sortOrder", async () => { - let docMetaReadCount = 0; - const client = { - mutation: vi.fn(), - query: vi.fn(async (name: string, args: { id: string }) => { - if (name === "documents:getMeta" && args.id === "doc_1") { - docMetaReadCount += 1; - return { - id: "doc_1", - workspace_id: "ws_1", - parent_id: docMetaReadCount === 1 ? null : "parent_1", - sort_order: docMetaReadCount === 1 ? 4 : 1, - updated_at: docMetaReadCount === 1 ? "2026-04-23T00:00:00Z" : "2026-04-23T00:01:00Z", - }; - } - if (name === "documents:getMeta" && args.id === "parent_1") { - return { - id: "parent_1", - workspace_id: "ws_1", - parent_id: null, - }; - } - return null; - }), - }; - mockGetAuthedConvexClient.mockResolvedValue({ - auth: { - userId: "user_1", - email: "dev@example.com", - name: "开发用户", - }, - client, - }); - mockBuildDocumentBridgeContext.mockResolvedValue({ - requestId: "req_tree_2", - traceId: "trace_tree_2", - workspaceId: "ws_1", - actor: { - actorType: "user", - actorId: "user_1", - sessionId: null, - }, - source: { - channel: "next-route", - client: "wolai-frontend", - }, - tenantId: null, - deploymentId: null, - projectId: null, - authToken: null, - idempotencyKey: null, - validateOnly: false, - dryRun: false, - }); - mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); - mockResolveRustBridgeCommandPlan.mockResolvedValue({ - kind: "command", - commandName: "tree.subtree.move", - commandId: "cmd_tree_move_1", - functionName: "documents:move", - workspaceId: "ws_1", - requestId: "req_tree_2", - traceId: "trace_tree_2", - actorId: "user_1", - idempotencyKey: null, - payloadJson: "{}", - argsJson: { - streamDeltaHint: { - family: "tree", - kind: "move_document", - args: { - documentId: "doc_1", - parentId: "parent_1", - sortOrder: 2, - }, - }, - domainEventHint: { - family: "tree", - eventType: "tree.subtree.moved", - }, - }, - }); - mockExecuteRustBridgeMutationTransport.mockResolvedValue({ - ok: true, - parent_id: "parent_1", - sort_order: 1, - workspace_id: "ws_1", - updated_at: "2026-04-23T00:01:00Z", - }); - mockLoadSidebarDataFromConvex.mockResolvedValue({ - sidebarInitialData: { - activeWorkspaceId: "ws_1", - workspaces: [], - documents: [ - { - id: "doc_1", - workspace_id: "ws_1", - parent_id: null, - }, - { - id: "parent_1", - workspace_id: "ws_1", - parent_id: null, - }, - ], - rootDocuments: [], - sidebarTree: [], - trashedDocuments: [], - mediaAssets: [], - trashedMediaAssets: [], - mindmapDocs: [], - mindmapAssets: [], - trashedMindmapAssets: [], - mindmapAssetChildren: {}, - tableAssets: [], - trashedTableAssets: [], - kernelSidebarProjection: { - projectionId: "kernel_projection:sidebar_tree:workspace_root", - projection: "sidebar_tree", - rootNodeId: null, - items: [], - edges: [], - }, - kernelFileTreeProjection: { - projectionId: "kernel_projection:file_tree:workspace_root", - projection: "file_tree", - rootNodeId: null, - items: [], - edges: [], - }, - }, - }); - - const { POST } = await import("./route"); - const response = await POST( - new Request("http://127.0.0.1:3000/api/tree/commands", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - action: "move", - documentId: "doc_1", - parentId: "parent_1", - sortOrder: 2.9, - workspaceId: "ws_1", - }), - }), - ); - - expect(response.status).toBe(200); - const payload = await response.json() as { - result: { - action: string; - documentId: string; - parentId: string | null; - sortOrder: number | null; - }; - }; - expect(payload.result).toMatchObject({ - action: "move", - documentId: "doc_1", - parentId: "parent_1", - sortOrder: 1, - workspaceId: "ws_1", - }); - expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( - expect.objectContaining({ - name: "tree.subtree.move", - preflightData: { - documents: [ - { - id: "doc_1", - workspace_id: "ws_1", - parent_id: null, - }, - { - id: "parent_1", - workspace_id: "ws_1", - parent_id: null, - }, - ], - }, - payload: expect.objectContaining({ - documentId: "doc_1", - parentId: "parent_1", - sortOrder: 2, - }), - }), - ); - expect(mockEnsureDocumentScaffold).not.toHaveBeenCalled(); - expect(mockRecordRustBridgeCommandArtifacts).toHaveBeenCalledWith( - expect.objectContaining({ - envelope: expect.objectContaining({ - name: "tree.subtree.move", - }), - plan: expect.objectContaining({ - commandName: "tree.subtree.move", - functionName: "documents:move", - }), - result: expect.objectContaining({ - parent_id: "parent_1", - sort_order: 1, - }), - }), - ); - }); - - it("move action 应优先使用 mutation 返回的 canonical 结果,而不是再次回读页面元数据", async () => { - const client = { - mutation: vi.fn(), - query: vi.fn(async (name: string, args: { id: string }) => { - if (name === "documents:getMeta" && args.id === "doc_1") { - return { - id: "doc_1", - workspace_id: "ws_1", - parent_id: "parent_old", - sort_order: 4, - updated_at: "2026-04-23T00:00:00Z", - }; - } - if (name === "documents:getMeta" && args.id === "parent_1") { - return { - id: "parent_1", - workspace_id: "ws_1", - parent_id: null, - }; - } - return null; - }), - }; - mockGetAuthedConvexClient.mockResolvedValue({ - auth: { - userId: "user_1", - email: "dev@example.com", - name: "开发用户", - }, - client, - }); - mockBuildDocumentBridgeContext.mockResolvedValue({ - requestId: "req_tree_2b", - traceId: "trace_tree_2b", - workspaceId: "ws_1", - actor: { - actorType: "user", - actorId: "user_1", - sessionId: null, - }, - source: { - channel: "next-route", - client: "wolai-frontend", - }, - tenantId: null, - deploymentId: null, - projectId: null, - authToken: null, - idempotencyKey: null, - validateOnly: false, - dryRun: false, - }); - mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); - mockResolveRustBridgeCommandPlan.mockResolvedValue({ - kind: "command", - commandName: "tree.subtree.move", - commandId: "cmd_tree_move_2", - functionName: "documents:move", - workspaceId: "ws_1", - requestId: "req_tree_2b", - traceId: "trace_tree_2b", - actorId: "user_1", - idempotencyKey: null, - payloadJson: "{}", - argsJson: { - streamDeltaHint: { - family: "tree", - kind: "move_document", - args: { - documentId: "doc_1", - parentId: "parent_1", - sortOrder: 2, - }, - }, - domainEventHint: { - family: "tree", - eventType: "tree.subtree.moved", - }, - }, - }); - mockExecuteRustBridgeMutationTransport.mockResolvedValue({ - ok: true, - parent_id: "parent_1", - sort_order: 1, - workspace_id: "ws_1", - updated_at: "2026-04-23T00:01:00Z", - }); - mockLoadSidebarDataFromConvex.mockResolvedValue({ - sidebarInitialData: { - activeWorkspaceId: "ws_1", - workspaces: [], - documents: [ - { - id: "doc_1", - workspace_id: "ws_1", - parent_id: "parent_old", - }, - { - id: "parent_1", - workspace_id: "ws_1", - parent_id: null, - }, - ], - rootDocuments: [], - sidebarTree: [], - trashedDocuments: [], - mediaAssets: [], - trashedMediaAssets: [], - mindmapDocs: [], - mindmapAssets: [], - trashedMindmapAssets: [], - mindmapAssetChildren: {}, - tableAssets: [], - trashedTableAssets: [], - kernelSidebarProjection: { - projectionId: "kernel_projection:sidebar_tree:workspace_root", - projection: "sidebar_tree", - rootNodeId: null, - items: [], - edges: [], - }, - kernelFileTreeProjection: { - projectionId: "kernel_projection:file_tree:workspace_root", - projection: "file_tree", - rootNodeId: null, - items: [], - edges: [], - }, - }, - }); - - const { POST } = await import("./route"); - const response = await POST( - new Request("http://127.0.0.1:3000/api/tree/commands", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - action: "move", - documentId: "doc_1", - parentId: "parent_1", - sortOrder: 3, - workspaceId: "ws_1", - }), - }), - ); - - expect(response.status).toBe(200); - const payload = await response.json() as { - result: { - action: string; - documentId: string; - workspaceId: string; - parentId: string | null; - sortOrder: number | null; - updatedAt: string | null; - execution: { - parent_id: string | null; - sort_order: number | null; - workspace_id: string | null; - }; - }; - }; - expect(payload.result).toMatchObject({ - action: "move", - documentId: "doc_1", - workspaceId: "ws_1", - parentId: "parent_1", - sortOrder: 1, - updatedAt: "2026-04-23T00:01:00Z", - execution: { - parent_id: "parent_1", - sort_order: 1, - workspace_id: "ws_1", - }, - }); - expect(client.query).toHaveBeenCalledTimes(1); - }); - - it("move action 应把 self move 的 preflight 交给 Rust,并由 Rust 拒绝", async () => { - const client = { - mutation: vi.fn(), - query: vi.fn(async (name: string, args: { id: string }) => { - if (name === "documents:getMeta" && args.id === "doc_1") { - return { - id: "doc_1", - workspace_id: "ws_1", - parent_id: null, - }; - } - return null; - }), - }; - mockGetAuthedConvexClient.mockResolvedValue({ - auth: { - userId: "user_1", - email: "dev@example.com", - name: "开发用户", - }, - client, - }); - mockBuildDocumentBridgeContext.mockResolvedValue({ - requestId: "req_tree_move_self", - traceId: "trace_tree_move_self", - workspaceId: "ws_1", - actor: { - actorType: "user", - actorId: "user_1", - sessionId: null, - }, - source: { - channel: "next-route", - client: "wolai-frontend", - }, - tenantId: null, - deploymentId: null, - projectId: null, - authToken: null, - idempotencyKey: null, - validateOnly: false, - dryRun: false, - }); - mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); - mockResolveRustBridgeCommandPlan.mockRejectedValue( - Object.assign(new Error("不能把页面移动到自身下面"), { status: 400 }), - ); - mockLoadSidebarDataFromConvex.mockResolvedValue({ - sidebarInitialData: { - activeWorkspaceId: "ws_1", - workspaces: [], - documents: [ - { - id: "doc_1", - workspace_id: "ws_1", - parent_id: null, - }, - ], - rootDocuments: [], - sidebarTree: [], - trashedDocuments: [], - mediaAssets: [], - trashedMediaAssets: [], - mindmapDocs: [], - mindmapAssets: [], - trashedMindmapAssets: [], - mindmapAssetChildren: {}, - tableAssets: [], - trashedTableAssets: [], - kernelSidebarProjection: { - projectionId: "kernel_projection:sidebar_tree:workspace_root", - projection: "sidebar_tree", - rootNodeId: null, - items: [], - edges: [], - }, - kernelFileTreeProjection: { - projectionId: "kernel_projection:file_tree:workspace_root", - projection: "file_tree", - rootNodeId: null, - items: [], - edges: [], - }, - }, - }); - - const { POST } = await import("./route"); - const response = await POST( - new Request("http://127.0.0.1:3000/api/tree/commands", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - action: "move", - documentId: "doc_1", - parentId: "doc_1", - sortOrder: 0, - }), - }), - ); - - expect(response.status).toBe(400); - expect(await response.json()).toEqual({ - error: "不能把页面移动到自身下面", - }); - expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( - expect.objectContaining({ - name: "tree.subtree.move", - preflightData: { - documents: [ - { - id: "doc_1", - workspace_id: "ws_1", - parent_id: null, - }, - ], - }, - payload: expect.objectContaining({ - documentId: "doc_1", - parentId: "doc_1", - sortOrder: 0, - }), - }), - ); - expect(mockResolveRustBridgeCommandPlan).toHaveBeenCalledTimes(1); - expect(mockExecuteRustBridgeMutationTransport).not.toHaveBeenCalled(); - }); - - it("move action 应把 descendant move 的 preflight 交给 Rust,并由 Rust 拒绝", async () => { - const client = { - mutation: vi.fn(), - query: vi.fn(async (name: string, args: { id: string }) => { - if (name === "documents:getMeta" && args.id === "doc_1") { - return { - id: "doc_1", - workspace_id: "ws_1", - parent_id: null, - }; - } - if (name === "documents:getMeta" && args.id === "child_1") { - return { - id: "child_1", - workspace_id: "ws_1", - parent_id: "doc_1", - }; - } - return null; - }), - }; - mockGetAuthedConvexClient.mockResolvedValue({ - auth: { - userId: "user_1", - email: "dev@example.com", - name: "开发用户", - }, - client, - }); - mockBuildDocumentBridgeContext.mockResolvedValue({ - requestId: "req_tree_move_desc", - traceId: "trace_tree_move_desc", - workspaceId: "ws_1", - actor: { - actorType: "user", - actorId: "user_1", - sessionId: null, - }, - source: { - channel: "next-route", - client: "wolai-frontend", - }, - tenantId: null, - deploymentId: null, - projectId: null, - authToken: null, - idempotencyKey: null, - validateOnly: false, - dryRun: false, - }); - mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); - mockResolveRustBridgeCommandPlan.mockRejectedValue( - Object.assign(new Error("不能把页面移动到自己的后代下面"), { status: 400 }), - ); - mockLoadSidebarDataFromConvex.mockResolvedValue({ - sidebarInitialData: { - activeWorkspaceId: "ws_1", - workspaces: [], - documents: [ - { - id: "doc_1", - workspace_id: "ws_1", - parent_id: null, - }, - { - id: "child_1", - workspace_id: "ws_1", - parent_id: "doc_1", - }, - ], - rootDocuments: [], - sidebarTree: [], - trashedDocuments: [], - mediaAssets: [], - trashedMediaAssets: [], - mindmapDocs: [], - mindmapAssets: [], - trashedMindmapAssets: [], - mindmapAssetChildren: {}, - tableAssets: [], - trashedTableAssets: [], - kernelSidebarProjection: { - projectionId: "kernel_projection:sidebar_tree:workspace_root", - projection: "sidebar_tree", - rootNodeId: null, - items: [], - edges: [], - }, - kernelFileTreeProjection: { - projectionId: "kernel_projection:file_tree:workspace_root", - projection: "file_tree", - rootNodeId: null, - items: [], - edges: [], - }, - }, - }); - - const { POST } = await import("./route"); - const response = await POST( - new Request("http://127.0.0.1:3000/api/tree/commands", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - action: "move", - documentId: "doc_1", - parentId: "child_1", - sortOrder: 0, - }), - }), - ); - - expect(response.status).toBe(400); - expect(await response.json()).toEqual({ - error: "不能把页面移动到自己的后代下面", - }); - expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( - expect.objectContaining({ - name: "tree.subtree.move", - preflightData: { - documents: [ - { - id: "doc_1", - workspace_id: "ws_1", - parent_id: null, - }, - { - id: "child_1", - workspace_id: "ws_1", - parent_id: "doc_1", - }, - ], - }, - payload: expect.objectContaining({ - documentId: "doc_1", - parentId: "child_1", - sortOrder: 0, - }), - }), - ); - expect(mockResolveRustBridgeCommandPlan).toHaveBeenCalledTimes(1); - expect(mockExecuteRustBridgeMutationTransport).not.toHaveBeenCalled(); - }); - - it("rename action 走 tree.node.rename,并保留标题 payload", async () => { - const client = { - mutation: vi.fn(), - query: vi.fn(async () => ({ - id: "doc_1", - workspace_id: "ws_1", - })), - }; - mockGetAuthedConvexClient.mockResolvedValue({ - auth: { - userId: "user_1", - email: "dev@example.com", - name: "开发用户", - }, - client, - }); - mockBuildDocumentBridgeContext.mockResolvedValue({ - requestId: "req_tree_3", - traceId: "trace_tree_3", - workspaceId: "ws_1", - actor: { - actorType: "user", - actorId: "user_1", - sessionId: null, - }, - source: { - channel: "next-route", - client: "wolai-frontend", - }, - tenantId: null, - deploymentId: null, - projectId: null, - authToken: null, - idempotencyKey: null, - validateOnly: false, - dryRun: false, - }); - mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); - mockResolveRustBridgeCommandPlan.mockResolvedValue({ - kind: "command", - commandName: "tree.node.rename", - commandId: "cmd_tree_rename_1", - functionName: "documents:updateTitle", - workspaceId: "ws_1", - requestId: "req_tree_3", - traceId: "trace_tree_3", - actorId: "user_1", - idempotencyKey: null, - payloadJson: "{}", - argsJson: { - streamDeltaHint: { - family: "tree", - kind: "upsert_document_patch", - args: { - documentId: "doc_1", - patch: { title: "新标题" }, - }, - }, - domainEventHint: { - family: "tree", - eventType: "tree.node.renamed", - }, - }, - }); - mockExecuteRustBridgeMutationTransport.mockResolvedValue({ - ok: true, - updated_at: "2026-04-23T00:00:00Z", - }); - - const { POST } = await import("./route"); - const response = await POST( - new Request("http://127.0.0.1:3000/api/tree/commands", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - action: "rename", - documentId: "doc_1", - workspaceId: "ws_1", - title: "新标题", - }), - }), - ); - - expect(response.status).toBe(200); - const payload = await response.json() as { - result: { - action: string; - documentId: string; - workspaceId: string; - title: string; - }; - }; - expect(payload.result).toMatchObject({ - action: "rename", - documentId: "doc_1", - workspaceId: "ws_1", - title: "新标题", - }); - expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( - expect.objectContaining({ - name: "tree.node.rename", - payload: { - documentId: "doc_1", - workspaceId: "ws_1", - title: "新标题", - }, - }), - ); - expect(mockEnsureDocumentScaffold).not.toHaveBeenCalled(); - expect(mockRecordRustBridgeCommandArtifacts).toHaveBeenCalledWith( - expect.objectContaining({ - envelope: expect.objectContaining({ - name: "tree.node.rename", - }), - plan: expect.objectContaining({ - commandName: "tree.node.rename", - }), - result: expect.objectContaining({ - updated_at: "2026-04-23T00:00:00Z", - }), - }), - ); - }); - - it("archive action 走 tree.node.archive", async () => { - const client = { - mutation: vi.fn(), - query: vi.fn(async () => ({ - id: "doc_1", - workspace_id: "ws_1", - })), - }; - mockGetAuthedConvexClient.mockResolvedValue({ - auth: { - userId: "user_1", - email: "dev@example.com", - name: "开发用户", - }, - client, - }); - mockBuildDocumentBridgeContext.mockResolvedValue({ - requestId: "req_tree_4", - traceId: "trace_tree_4", - workspaceId: "ws_1", - }); - mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); - mockResolveRustBridgeCommandPlan.mockResolvedValue({ - kind: "command", - commandName: "tree.node.archive", - commandId: "cmd_tree_archive_1", - functionName: "documents:softDelete", - workspaceId: "ws_1", - requestId: "req_tree_4", - traceId: "trace_tree_4", - actorId: "user_1", - idempotencyKey: null, - payloadJson: "{}", - argsJson: { - streamDeltaHint: { - family: "tree", - kind: "remove_document", - args: { documentId: "doc_1" }, - }, - domainEventHint: { - family: "tree", - eventType: "tree.node.archived", - }, - }, - }); - mockExecuteRustBridgeMutationTransport.mockResolvedValue({ - ok: true, - updated_at: "2026-04-24T00:00:00Z", - }); - - const { POST } = await import("./route"); - const response = await POST( - new Request("http://127.0.0.1:3000/api/tree/commands", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - action: "archive", - documentId: "doc_1", - workspaceId: "ws_1", - }), - }), - ); - - expect(response.status).toBe(200); - const payload = await response.json() as { - result: { - action: string; - documentId: string; - workspaceId: string; - }; - }; - expect(payload.result).toMatchObject({ - action: "archive", - documentId: "doc_1", - workspaceId: "ws_1", - }); - expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( - expect.objectContaining({ - name: "tree.node.archive", - payload: { - documentId: "doc_1", - workspaceId: "ws_1", - }, - }), - ); - expect(mockRecordRustBridgeCommandArtifacts).toHaveBeenCalledWith( - expect.objectContaining({ - envelope: expect.objectContaining({ - name: "tree.node.archive", - }), - plan: expect.objectContaining({ - commandName: "tree.node.archive", - }), - result: expect.any(Object), - }), - ); - }); - - it("embed action 走 tree.node.embed,并把 pageReference 组装交给 Rust Page Aggregate preflight", async () => { - const client = { - mutation: vi.fn(), - query: vi.fn(async (name: string, args: { id: string }) => { - if (name === "documents:getMeta" && args.id === "doc_source") { - return { - id: "doc_source", - title: "来源页面", - workspace_id: "ws_1", - }; - } - if (name === "documents:getMeta" && args.id === "doc_target") { - return { - id: "doc_target", - workspace_id: "ws_1", - embed_default_block_id: "anchor_1", - }; - } - if (name === "documents:getContent" && args.id === "doc_target") { - return { - content: [ - { id: "anchor_1", type: "paragraph" }, - ], - revision: 7, - conflict_detection_key: "doc_target:7", - }; - } - return null; - }), - }; - mockGetAuthedConvexClient.mockResolvedValue({ - auth: { - userId: "user_1", - email: "dev@example.com", - name: "开发用户", - }, - client, - }); - mockBuildDocumentBridgeContext.mockResolvedValue({ - requestId: "req_tree_5", - traceId: "trace_tree_5", - workspaceId: "ws_1", - }); - mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); - mockResolveRustBridgeCommandPlan.mockResolvedValue({ - kind: "command", - commandName: "tree.node.embed", - commandId: "cmd_tree_embed_1", - functionName: "documents:updateContent", - workspaceId: "ws_1", - requestId: "req_tree_5", - traceId: "trace_tree_5", - actorId: "user_1", - idempotencyKey: null, - payloadJson: "{}", - argsJson: { - streamDeltaHint: { - family: "tree", - kind: "noop", - args: {}, - }, - domainEventHint: { - family: "tree", - eventType: "tree.node.embedded", - }, - }, - }); - mockExecuteRustBridgeMutationTransport.mockResolvedValue({ - revision: 8, - conflict_detection_key: "doc_target:8", - }); - - const { POST } = await import("./route"); - const response = await POST( - new Request("http://127.0.0.1:3000/api/tree/commands", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - action: "embed", - sourceId: "doc_source", - targetId: "doc_target", - }), - }), - ); - - expect(response.status).toBe(200); - const payload = await response.json() as { - result: { - action: string; - documentId: string; - sourceDocumentId: string; - targetDocumentId: string; - }; - }; - expect(payload.result).toMatchObject({ - action: "embed", - documentId: "doc_target", - sourceDocumentId: "doc_source", - targetDocumentId: "doc_target", - }); - expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( - expect.objectContaining({ - name: "tree.node.embed", - payload: expect.objectContaining({ - documentId: "doc_target", - sourceDocumentId: "doc_source", - targetDocumentId: "doc_target", - revision: 7, - conflictDetectionKey: "doc_target:7", - }), - preflightData: { - pageAggregateEmbed: { - sourceDocumentId: "doc_source", - sourceTitle: "来源页面", - targetDocumentId: "doc_target", - targetContent: [ - { id: "anchor_1", type: "paragraph" }, - ], - anchorBlockId: "anchor_1", - blockId: expect.any(String), - }, - }, - }), - ); - expect(mockRecordRustBridgeCommandArtifacts).toHaveBeenCalledWith( - expect.objectContaining({ - envelope: expect.objectContaining({ - name: "tree.node.embed", - }), - plan: expect.objectContaining({ - commandName: "tree.node.embed", - }), - result: expect.any(Object), - }), - ); - }); - - it("copy action 走 tree.subtree.copy,并保留 scaffold 与导图文件副作用", async () => { - const client = { - mutation: vi.fn(), - query: vi.fn(async (name: string, args: { id: string }) => { - if (name === "documents:getMeta" && args.id === "parent_1") { - return { - id: "parent_1", - workspace_id: "ws_1", - }; - } - return null; - }), - }; - mockGetAuthedConvexClient.mockResolvedValue({ - auth: { - userId: "user_1", - email: "dev@example.com", - name: "开发用户", - }, - client, - }); - mockBuildDocumentBridgeContext.mockResolvedValue({ - requestId: "req_tree_6", - traceId: "trace_tree_6", - workspaceId: "ws_1", - }); - mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); - mockResolveRustBridgeCommandPlan.mockResolvedValue({ - kind: "command", - commandName: "tree.subtree.copy", - commandId: "cmd_tree_copy_1", - functionName: "documents:copyTree", - workspaceId: "ws_1", - requestId: "req_tree_6", - traceId: "trace_tree_6", - actorId: "user_1", - idempotencyKey: null, - payloadJson: "{}", - argsJson: { - streamDeltaHint: { - family: "tree", - kind: "copy_result", - args: { itemsField: "items", documentField: "document" }, - }, - domainEventHint: { - family: "tree", - eventType: "tree.subtree.copied", - }, - }, - }); - mockExecuteRustBridgeMutationTransport.mockResolvedValue({ - items: [ - { - oldId: "doc_1", - newId: "doc_2", - title: "复制页面", - document: { - id: "doc_2", - workspace_id: "ws_1", - title: "复制页面", - parent_id: "parent_1", - sort_order: 0, - is_starred: false, - access_scope: "private", - is_template: false, - created_at: "2026-04-24T00:00:00Z", - updated_at: "2026-04-24T00:00:00Z", - }, - }, - ], - }); - mockLoadSidebarDataFromConvex.mockResolvedValue({ - sidebarInitialData: { - activeWorkspaceId: "ws_1", - workspaces: [], - documents: [], - rootDocuments: [], - sidebarTree: [], - trashedDocuments: [], - mediaAssets: [], - trashedMediaAssets: [], - mindmapDocs: [], - mindmapAssets: [], - trashedMindmapAssets: [], - mindmapAssetChildren: {}, - tableAssets: [], - trashedTableAssets: [], - kernelSidebarProjection: { - projectionId: "kernel_projection:sidebar_tree:workspace_root", - projection: "sidebar_tree", - rootNodeId: null, - items: [], - edges: [], - }, - kernelFileTreeProjection: { - projectionId: "kernel_projection:file_tree:workspace_root", - projection: "file_tree", - rootNodeId: null, - items: [], - edges: [], - }, - }, - }); - - const { POST } = await import("./route"); - const response = await POST( - new Request("http://127.0.0.1:3000/api/tree/commands", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - action: "copy", - targetParentId: "parent_1", - items: [{ documentId: "doc_1", recursive: true }], - }), - }), - ); - - expect(response.status).toBe(200); - const payload = await response.json() as { - result: { - action: string; - items: Array<{ oldId: string; newId: string }>; - }; - }; - expect(payload.result).toMatchObject({ - action: "copy", - items: [{ oldId: "doc_1", newId: "doc_2" }], - }); - expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( - expect.objectContaining({ - name: "tree.subtree.copy", - payload: { - workspaceId: "ws_1", - targetParentId: "parent_1", - items: [{ documentId: "doc_1", recursive: true }], - }, - }), - ); - expect(mockEnsureDocumentScaffold).toHaveBeenCalledWith("doc_2", "复制页面"); - expect(mockCopyMindmapFilesIfExists).toHaveBeenCalledWith("doc_1", "doc_2"); - expect(mockRecordRustBridgeCommandArtifacts).toHaveBeenCalledWith( - expect.objectContaining({ - envelope: expect.objectContaining({ - name: "tree.subtree.copy", - }), - plan: expect.objectContaining({ - commandName: "tree.subtree.copy", - }), - result: expect.objectContaining({ - items: [expect.objectContaining({ newId: "doc_2" })], - }), - }), - ); - }); - - it("restore action 走 tree.node.restore,并附带 upsert_document delta", async () => { - const client = { - mutation: vi.fn(), - query: vi.fn(async (name: string, args: { id?: string; includeDeleted?: boolean }) => { - if (name === "documents:getMeta" && args.id === "doc_restore_1" && args.includeDeleted === true) { - return { - id: "doc_restore_1", - workspace_id: "ws_1", - }; - } - return null; - }), - }; - mockGetAuthedConvexClient.mockResolvedValue({ - auth: { - userId: "user_1", - email: "dev@example.com", - name: "开发用户", - }, - client, - }); - mockBuildDocumentBridgeContext.mockResolvedValue({ - requestId: "req_tree_restore_1", - traceId: "trace_tree_restore_1", - workspaceId: "ws_1", - }); - mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); - mockResolveRustBridgeCommandPlan.mockResolvedValue({ - kind: "command", - commandName: "tree.node.restore", - commandId: "cmd_tree_restore_1", - functionName: "documents:restore", - workspaceId: "ws_1", - requestId: "req_tree_restore_1", - traceId: "trace_tree_restore_1", - actorId: "user_1", - idempotencyKey: null, - payloadJson: "{}", - argsJson: { - streamDeltaHint: { - family: "tree", - kind: "document_result", - args: { documentField: "document" }, - }, - domainEventHint: { - family: "tree", - eventType: "tree.node.restored", - }, - }, - }); - mockExecuteRustBridgeMutationTransport.mockResolvedValue({ - ok: true, - document: { - id: "doc_restore_1", - workspace_id: "ws_1", - title: "恢复页面", - parent_id: null, - sort_order: 0, - is_starred: false, - access_scope: "private", - is_template: false, - created_at: "2026-04-23T00:00:00Z", - updated_at: "2026-04-24T00:00:00Z", - }, - updated_at: "2026-04-24T00:00:00Z", - }); - mockLoadSidebarDataFromConvex.mockResolvedValue({ - sidebarInitialData: { - activeWorkspaceId: "ws_1", - workspaces: [], - documents: [], - rootDocuments: [], - sidebarTree: [], - trashedDocuments: [], - mediaAssets: [], - trashedMediaAssets: [], - mindmapDocs: [], - mindmapAssets: [], - trashedMindmapAssets: [], - mindmapAssetChildren: {}, - tableAssets: [], - trashedTableAssets: [], - kernelSidebarProjection: { - projectionId: "kernel_projection:sidebar_tree:workspace_root", - projection: "sidebar_tree", - rootNodeId: null, - items: [], - edges: [], - }, - kernelFileTreeProjection: { - projectionId: "kernel_projection:file_tree:workspace_root", - projection: "file_tree", - rootNodeId: null, - items: [], - edges: [], - }, - }, - }); - - const { POST } = await import("./route"); - const response = await POST( - new Request("http://127.0.0.1:3000/api/tree/commands", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - action: "restore", - documentId: "doc_restore_1", - workspaceId: "ws_1", - }), - }), - ); - - expect(response.status).toBe(200); - expect(client.query).toHaveBeenCalledWith("documents:getMeta", { - id: "doc_restore_1", - includeDeleted: true, - }); - expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( - expect.objectContaining({ - name: "tree.node.restore", - payload: { - documentId: "doc_restore_1", - workspaceId: "ws_1", - }, - }), - ); - expect(mockRecordRustBridgeCommandArtifacts).toHaveBeenCalledWith( - expect.objectContaining({ - envelope: expect.objectContaining({ - name: "tree.node.restore", - }), - plan: expect.objectContaining({ - commandName: "tree.node.restore", - }), - result: expect.objectContaining({ - document: expect.objectContaining({ id: "doc_restore_1" }), - }), - }), - ); - }); - - it("tree mutation 失败时应记录 failure bridge log", async () => { - const client = { - mutation: vi.fn(), - query: vi.fn(async () => ({ - id: "doc_1", - workspace_id: "ws_1", - })), - }; - mockGetAuthedConvexClient.mockResolvedValue({ - auth: { - userId: "user_1", - email: "dev@example.com", - name: "开发用户", - }, - client, - }); - mockBuildDocumentBridgeContext.mockResolvedValue({ - requestId: "req_tree_fail_1", - traceId: "trace_tree_fail_1", - workspaceId: "ws_1", - actor: { - actorType: "user", - actorId: "user_1", - sessionId: null, - }, - source: { - channel: "next-route", - client: "wolai-frontend", - }, - tenantId: null, - deploymentId: null, - projectId: null, - authToken: null, - idempotencyKey: null, - validateOnly: false, - dryRun: false, - }); - mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); - mockResolveRustBridgeCommandPlan.mockResolvedValue({ - kind: "command", - commandName: "tree.node.archive", - commandId: "cmd_tree_archive_fail_1", - functionName: "documents:softDelete", - workspaceId: "ws_1", - requestId: "req_tree_fail_1", - traceId: "trace_tree_fail_1", - actorId: "user_1", - idempotencyKey: null, - payloadJson: "{}", - argsJson: { - streamDeltaHint: { - family: "tree", - kind: "remove_document", - args: { documentId: "doc_1" }, - }, - domainEventHint: { - family: "tree", - eventType: "tree.node.archived", - }, - }, - }); - mockExecuteRustBridgeMutationTransport.mockRejectedValue(new Error("archive failed")); - - const { POST } = await import("./route"); - const response = await POST( - new Request("http://127.0.0.1:3000/api/tree/commands", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - action: "archive", - documentId: "doc_1", - workspaceId: "ws_1", - }), - }), - ); - - expect(response.status).toBe(500); - expect(mockRecordBridgeCommandFailureArtifacts).toHaveBeenCalledWith( - expect.objectContaining({ - envelope: expect.objectContaining({ - name: "tree.node.archive", - }), - error: expect.any(Error), - }), - ); - }); -}); diff --git a/wolai-frontend/src/app/api/tree/commands/route.ts b/wolai-frontend/src/app/api/tree/commands/route.ts deleted file mode 100644 index 551e6cb4..00000000 --- a/wolai-frontend/src/app/api/tree/commands/route.ts +++ /dev/null @@ -1,743 +0,0 @@ -import { randomUUID } from "node:crypto"; -import { NextResponse } from "next/server"; -import { api } from "@/lib/convex/api"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { getAuthedConvexClient } from "@/lib/convex/route"; -import { - assertDocumentId, - assertTitle, - buildDocumentBridgeContext, - buildDocumentCommandEnvelope, - documentBridgeErrorResponse, -} from "@/lib/documents/bridge"; -import { - recordBridgeCommandFailureArtifacts, -} from "@/lib/documents/bridge-log"; -import { - copyMindmapFilesIfExists, - ensureDocumentScaffold, -} from "@/lib/documents/page-lifecycle-side-effects"; -import { loadSidebarDataFromConvex } from "@/lib/server/sidebar-data"; -import { - executeRustBridgeMutationTransport, - recordRustBridgeCommandArtifacts, - resolveRustBridgeCommandPlan, -} from "@/lib/documents/rust-runtime"; - -type TreeCommandAction = - | "create" - | "rename" - | "move" - | "archive" - | "restore" - | "purge" - | "embed" - | "copy"; - -type TreeCopyItem = { - documentId: string; - recursive: boolean; -}; - -type TreeCommandPayload = { - action?: TreeCommandAction; - workspaceId?: string | null; - documentId?: string | null; - parentId?: string | null; - targetParentId?: string | null; - title?: string | null; - accessScope?: "private" | "shared" | "public" | null; - content?: unknown; - sortOrder?: number | null; - sourceId?: string | null; - targetId?: string | null; - items?: TreeCopyItem[] | null; -}; - -type TreeDeltaDocument = { - id: string; - workspace_id: string; - title: string | null; - parent_id: string | null; - sort_order: number | null; - is_starred: boolean | null; - access_scope: "private" | "shared" | "public"; - is_template: boolean; - created_at: string; - updated_at: string | null; -}; - -type TreeMutationResult = { - context: Awaited>; - envelope: ReturnType; - plan: Awaited>; - result: TResult; -}; - -function trimOrNull(value: unknown) { - if (typeof value !== "string") return null; - const trimmed = value.trim(); - return trimmed.length > 0 ? trimmed : null; -} - -function normalizeTitle(value: unknown) { - const title = typeof value === "string" ? value.trim() : ""; - return title || "无标题"; -} - -function normalizeSortOrder(value: unknown) { - return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0; -} - -function isRecord(value: unknown): value is Record { - return Boolean(value) && typeof value === "object" && !Array.isArray(value); -} - -async function recordTreeCommandSuccess(args: { - context: Awaited>; - envelope: ReturnType; - client: Awaited>["client"]; - plan: Awaited>; - result: unknown; -}) { - try { - await recordRustBridgeCommandArtifacts({ - context: args.context, - envelope: args.envelope, - client: args.client, - plan: args.plan, - result: args.result, - }); - } catch (error) { - console.warn("[tree.commands] Rust bridge success artifacts skipped:", error); - } -} - -async function loadTreeCommandSidebarSnapshot(args: { - client: Awaited>["client"]; - auth: Awaited>["auth"]; - workspaceId: string | null; -}) { - if (!args.workspaceId) { - return null; - } - try { - const result = await loadSidebarDataFromConvex({ - client: args.client, - auth: { - userId: args.auth.userId, - email: args.auth.email, - name: args.auth.name, - }, - fallbackName: args.auth.email ?? args.auth.name ?? "我的空间", - requestedWorkspaceId: args.workspaceId, - }); - return result.sidebarInitialData ?? null; - } catch (error) { - console.warn("[tree.commands] sidebar snapshot skipped:", error); - return null; - } -} - -function buildTreeMovePreflightDataFromSidebarSnapshot( - sidebarSnapshot: unknown, -): Record | null { - if (!isRecord(sidebarSnapshot) || !Array.isArray(sidebarSnapshot.documents)) { - return null; - } - return { - documents: sidebarSnapshot.documents, - }; -} - -async function resolveTreeMutationResult(args: { - request: Request; - workspaceId: string | null; - commandName: string; - payload: unknown; - preflightData?: Record | null; - pageId?: string | null; - client: Awaited>["client"]; -}) { - const context = await buildDocumentBridgeContext({ - request: args.request, - workspaceId: args.workspaceId, - }); - const envelope = buildDocumentCommandEnvelope({ - name: args.commandName, - payload: args.payload, - context, - preflightData: args.preflightData ?? null, - target: { - workspaceId: args.workspaceId, - pageId: args.pageId ?? undefined, - }, - reason: `tree-route ${args.commandName}`, - refs: ["next-tree-route"], - }); - const plan = await resolveRustBridgeCommandPlan({ - context, - envelope, - }); - try { - const result = await executeRustBridgeMutationTransport({ - client: args.client, - plan, - }); - - return { - context, - envelope, - plan, - result, - }; - } catch (error) { - await recordBridgeCommandFailureArtifacts({ - context, - envelope, - client: args.client, - error, - }); - throw error; - } -} - -export async function POST(request: Request) { - if (!isConvexEnabled()) { - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - } - - try { - const payload = (await request.json()) as TreeCommandPayload; - switch (payload.action) { - case "create": - return await handleCreate(request, payload); - case "move": - return await handleMove(request, payload); - case "rename": - return await handleRename(request, payload); - case "archive": - return await handleArchive(request, payload); - case "restore": - return await handleRestore(request, payload); - case "purge": - return await handlePurge(request, payload); - case "embed": - return await handleEmbed(request, payload); - case "copy": - return await handleCopy(request, payload); - default: - return NextResponse.json({ error: "不支持的 tree action" }, { status: 400 }); - } - } catch (error) { - return documentBridgeErrorResponse(error); - } -} - -async function handleCreate(request: Request, payload: TreeCommandPayload) { - const { auth, client } = await getAuthedConvexClient(); - const parentId = trimOrNull(payload.parentId); - const workspaceBootstrap = await client.mutation(api.workspaces.ensureDefaultWorkspace, { - fallbackName: auth.email ?? auth.name ?? "我的空间", - workspaceIdIfCreate: randomUUID(), - }); - - let workspaceId: string | null = trimOrNull(payload.workspaceId); - let accessScope: "private" | "shared" | "public" = "private"; - if (parentId) { - const parentDoc = await client.query(api.documents.getMeta, { id: parentId }); - if (!parentDoc) { - return NextResponse.json({ error: "父页面不存在或无权限" }, { status: 404 }); - } - workspaceId = trimOrNull(parentDoc.workspace_id); - accessScope = (parentDoc.access_scope ?? "private") as typeof accessScope; - } else if (!workspaceId) { - workspaceId = trimOrNull(workspaceBootstrap.activeWorkspaceId); - } - - if (!workspaceId) { - return NextResponse.json({ error: "缺少目标工作空间" }, { status: 400 }); - } - - const documentId = trimOrNull(payload.documentId) ?? randomUUID(); - const title = normalizeTitle(payload.title); - const mutation = await resolveTreeMutationResult<{ - id: string; - document?: TreeDeltaDocument | null; - title: string | null; - parent_id: string | null; - sort_order: number | null; - workspace_id: string; - access_scope: "private" | "shared" | "public"; - is_template: boolean; - created_at: string; - updated_at: string; - }>({ - request, - workspaceId, - commandName: "tree.node.create", - payload: { - documentId, - workspaceId, - parentId, - title, - accessScope: trimOrNull(payload.accessScope) ?? accessScope, - content: Array.isArray(payload.content) ? payload.content : [], - }, - pageId: documentId, - client, - }); - const { context, envelope, result } = mutation; - - await ensureDocumentScaffold(result.id, result.title ?? title); - await recordTreeCommandSuccess({ - context, - envelope, - client, - plan: mutation.plan, - result, - }); - - return NextResponse.json({ - requestId: context.requestId, - traceId: context.traceId, - result: { - action: "create", - workspaceId, - documentId: result.id, - parentId: result.parent_id ?? parentId, - title: result.title ?? title, - sortOrder: result.sort_order ?? null, - updatedAt: result.updated_at ?? null, - execution: result, - }, - }); -} - -async function handleMove(request: Request, payload: TreeCommandPayload) { - const { auth, client } = await getAuthedConvexClient(); - const documentId = assertDocumentId(payload.documentId ?? null); - const sourceDoc = await client.query(api.documents.getMeta, { id: documentId, includeDeleted: true }); - if (!sourceDoc) { - return NextResponse.json({ error: "页面不存在或无权访问" }, { status: 404 }); - } - - const workspaceId = trimOrNull(payload.workspaceId) ?? trimOrNull(sourceDoc.workspace_id); - const parentId = trimOrNull(payload.parentId); - const sortOrder = normalizeSortOrder(payload.sortOrder); - const sidebarSnapshot = await loadTreeCommandSidebarSnapshot({ - client, - auth, - workspaceId, - }); - const movePreflightData = buildTreeMovePreflightDataFromSidebarSnapshot(sidebarSnapshot); - const mutation = await resolveTreeMutationResult<{ - ok?: boolean; - parent_id?: string | null; - sort_order?: number | null; - workspace_id?: string | null; - updated_at?: string | null; - }>({ - request, - workspaceId, - commandName: "tree.subtree.move", - payload: { - documentId, - parentId, - sortOrder, - }, - preflightData: movePreflightData, - pageId: documentId, - client, - }); - const { context, envelope, result } = mutation; - await recordTreeCommandSuccess({ - context, - envelope, - client, - plan: mutation.plan, - result, - }); - - return NextResponse.json({ - requestId: context.requestId, - traceId: context.traceId, - result: { - action: "move", - workspaceId: trimOrNull(result?.workspace_id) ?? workspaceId, - documentId, - parentId: trimOrNull(result?.parent_id) ?? parentId, - sortOrder: - typeof result?.sort_order === "number" && Number.isFinite(result.sort_order) - ? result.sort_order - : sortOrder, - updatedAt: trimOrNull(result?.updated_at) ?? null, - execution: { - ...(result ?? null), - parent_id: trimOrNull(result?.parent_id) ?? parentId, - sort_order: - typeof result?.sort_order === "number" && Number.isFinite(result.sort_order) - ? result.sort_order - : sortOrder, - workspace_id: trimOrNull(result?.workspace_id) ?? workspaceId, - }, - }, - }); -} - -async function handleRename(request: Request, payload: TreeCommandPayload) { - const { client } = await getAuthedConvexClient(); - const documentId = assertDocumentId(payload.documentId ?? null); - const sourceDoc = await client.query(api.documents.getMeta, { id: documentId }); - if (!sourceDoc) { - return NextResponse.json({ error: "页面不存在或无权访问" }, { status: 404 }); - } - - const workspaceId = trimOrNull(payload.workspaceId) ?? trimOrNull(sourceDoc.workspace_id); - const title = assertTitle(payload.title ?? null); - const mutation = await resolveTreeMutationResult<{ - ok?: boolean; - document?: TreeDeltaDocument | null; - updated_at?: string | null; - }>({ - request, - workspaceId, - commandName: "tree.node.rename", - payload: { - documentId, - workspaceId, - title, - }, - pageId: documentId, - client, - }); - const { context, envelope, result } = mutation; - await recordTreeCommandSuccess({ - context, - envelope, - client, - plan: mutation.plan, - result, - }); - - return NextResponse.json({ - requestId: context.requestId, - traceId: context.traceId, - result: { - action: "rename", - workspaceId, - documentId, - title, - updatedAt: result?.updated_at ?? null, - execution: result ?? null, - }, - }); -} - -async function handleArchive(request: Request, payload: TreeCommandPayload) { - const { client } = await getAuthedConvexClient(); - const documentId = assertDocumentId(payload.documentId ?? null); - const sourceDoc = await client.query(api.documents.getMeta, { id: documentId }); - if (!sourceDoc) { - return NextResponse.json({ error: "页面不存在或无权访问" }, { status: 404 }); - } - - const workspaceId = trimOrNull(payload.workspaceId) ?? trimOrNull(sourceDoc.workspace_id); - const mutation = await resolveTreeMutationResult<{ - ok?: boolean; - document?: TreeDeltaDocument | null; - updated_at?: string | null; - }>({ - request, - workspaceId, - commandName: "tree.node.archive", - payload: { - documentId, - workspaceId, - }, - pageId: documentId, - client, - }); - const { context, envelope, result } = mutation; - await recordTreeCommandSuccess({ - context, - envelope, - client, - plan: mutation.plan, - result, - }); - - return NextResponse.json({ - requestId: context.requestId, - traceId: context.traceId, - result: { - action: "archive", - workspaceId, - documentId, - updatedAt: result?.updated_at ?? null, - execution: result ?? null, - }, - }); -} - -async function handleRestore(request: Request, payload: TreeCommandPayload) { - const { client } = await getAuthedConvexClient(); - const documentId = assertDocumentId(payload.documentId ?? null); - const sourceDoc = await client.query(api.documents.getMeta, { id: documentId, includeDeleted: true }); - if (!sourceDoc) { - return NextResponse.json({ error: "页面不存在或无权访问" }, { status: 404 }); - } - - const workspaceId = trimOrNull(payload.workspaceId) ?? trimOrNull(sourceDoc.workspace_id); - const mutation = await resolveTreeMutationResult<{ - ok?: boolean; - document?: TreeDeltaDocument | null; - updated_at?: string | null; - }>({ - request, - workspaceId, - commandName: "tree.node.restore", - payload: { - documentId, - workspaceId, - }, - pageId: documentId, - client, - }); - const { context, envelope, result } = mutation; - await recordTreeCommandSuccess({ - context, - envelope, - client, - plan: mutation.plan, - result, - }); - - return NextResponse.json({ - requestId: context.requestId, - traceId: context.traceId, - result: { - action: "restore", - workspaceId, - documentId, - updatedAt: result?.updated_at ?? null, - execution: result ?? null, - }, - }); -} - -async function handlePurge(request: Request, payload: TreeCommandPayload) { - const { client } = await getAuthedConvexClient(); - const documentId = assertDocumentId(payload.documentId ?? null); - const sourceDoc = await client.query(api.documents.getMeta, { id: documentId }); - if (!sourceDoc) { - return NextResponse.json({ error: "页面不存在或无权访问" }, { status: 404 }); - } - - const workspaceId = trimOrNull(payload.workspaceId) ?? trimOrNull(sourceDoc.workspace_id); - const mutation = await resolveTreeMutationResult<{ - ok?: boolean; - purged?: boolean; - purged_at?: string | null; - }>({ - request, - workspaceId, - commandName: "tree.node.purge", - payload: { - documentId, - }, - pageId: documentId, - client, - }); - const { context, envelope, result } = mutation; - await recordTreeCommandSuccess({ - context, - envelope, - client, - plan: mutation.plan, - result, - }); - - return NextResponse.json({ - requestId: context.requestId, - traceId: context.traceId, - result: { - action: "purge", - workspaceId, - documentId, - purged: result?.purged ?? true, - updatedAt: result?.purged_at ?? null, - execution: result ?? null, - }, - }); -} - -async function handleEmbed(request: Request, payload: TreeCommandPayload) { - const { client } = await getAuthedConvexClient(); - const sourceId = assertDocumentId(payload.sourceId ?? null); - const targetId = assertDocumentId(payload.targetId ?? null); - - const sourceDoc = await client.query(api.documents.getMeta, { id: sourceId }); - if (!sourceDoc) { - return NextResponse.json({ error: "原始页面不存在或无权限" }, { status: 404 }); - } - - const targetContent = await client.query(api.documents.getContent, { id: targetId }); - if (!targetContent) { - return NextResponse.json({ error: "目标页面不存在或无权限" }, { status: 404 }); - } - const targetMeta = await client.query(api.documents.getMeta, { id: targetId }); - - const anchorId = trimOrNull( - (targetMeta as { embed_default_block_id?: string | null } | null)?.embed_default_block_id, - ); - const workspaceId = - trimOrNull(sourceDoc.workspace_id) ?? - trimOrNull((targetMeta as { workspace_id?: string | null } | null)?.workspace_id); - - const mutation = await resolveTreeMutationResult<{ - revision?: number | null; - conflict_detection_key?: string | null; - }>({ - request, - workspaceId, - commandName: "tree.node.embed", - payload: { - documentId: targetId, - workspaceId, - revision: - typeof targetContent.revision === "number" && Number.isInteger(targetContent.revision) - ? targetContent.revision - : null, - conflictDetectionKey: - typeof targetContent.conflict_detection_key === "string" - ? targetContent.conflict_detection_key - : null, - sourceDocumentId: sourceId, - targetDocumentId: targetId, - anchorBlockId: anchorId, - }, - preflightData: { - pageAggregateEmbed: { - sourceDocumentId: sourceId, - sourceTitle: sourceDoc.title ?? "无标题", - targetDocumentId: targetId, - targetContent: targetContent.content, - anchorBlockId: anchorId, - blockId: randomUUID(), - }, - }, - pageId: targetId, - client, - }); - const { context, envelope, result } = mutation; - await recordTreeCommandSuccess({ - context, - envelope, - client, - plan: mutation.plan, - result, - }); - - return NextResponse.json({ - requestId: context.requestId, - traceId: context.traceId, - result: { - action: "embed", - workspaceId, - documentId: targetId, - sourceDocumentId: sourceId, - targetDocumentId: targetId, - execution: result ?? null, - }, - }); -} - -async function handleCopy(request: Request, payload: TreeCommandPayload) { - const { client } = await getAuthedConvexClient(); - const normalizedItems = (payload.items ?? []) - .filter((item) => item?.documentId) - .map((item) => ({ - documentId: assertDocumentId(item.documentId), - recursive: Boolean(item.recursive), - })); - - if (normalizedItems.length === 0) { - return NextResponse.json({ error: "items 为空" }, { status: 400 }); - } - - const targetParentId = trimOrNull(payload.targetParentId); - let workspaceId: string | null = null; - - if (targetParentId) { - const targetDoc = await client.query(api.documents.getMeta, { id: targetParentId }); - if (!targetDoc) { - return NextResponse.json({ error: "目标页面不存在或无权限" }, { status: 404 }); - } - workspaceId = trimOrNull(targetDoc.workspace_id); - } else { - const firstDoc = await client.query(api.documents.getMeta, { - id: normalizedItems[0]?.documentId ?? "", - }); - if (!firstDoc) { - return NextResponse.json({ error: "源页面不存在或无权限" }, { status: 404 }); - } - workspaceId = trimOrNull(firstDoc.workspace_id); - } - - if (!workspaceId) { - return NextResponse.json({ error: "缺少目标工作空间" }, { status: 400 }); - } - - const mutation = await resolveTreeMutationResult<{ - items: Array<{ - oldId: string; - newId: string; - title?: string | null; - document?: TreeDeltaDocument | null; - }>; - }>({ - request, - workspaceId, - commandName: "tree.subtree.copy", - payload: { - workspaceId, - targetParentId, - items: normalizedItems, - }, - pageId: targetParentId, - client, - }); - const { context, envelope, result } = mutation; - - await Promise.all( - (result.items ?? []).map(async (item) => { - await ensureDocumentScaffold(item.newId, item.title ?? "无标题"); - await copyMindmapFilesIfExists(item.oldId, item.newId); - }), - ); - await recordTreeCommandSuccess({ - context, - envelope, - client, - plan: mutation.plan, - result, - }); - - return NextResponse.json({ - requestId: context.requestId, - traceId: context.traceId, - result: { - action: "copy", - workspaceId, - targetParentId, - items: (result.items ?? []).map((item) => ({ - oldId: item.oldId, - newId: item.newId, - })), - execution: result ?? null, - }, - }); -} - -export const runtime = "nodejs"; diff --git a/wolai-frontend/src/app/api/tree/filetree/delete-preflight/route.test.ts b/wolai-frontend/src/app/api/tree/filetree/delete-preflight/route.test.ts deleted file mode 100644 index d9e71db1..00000000 --- a/wolai-frontend/src/app/api/tree/filetree/delete-preflight/route.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const mockIsConvexEnabled = vi.fn(() => true); -const mockBuildDocumentBridgeContext = vi.fn(); -const mockBuildDocumentCommandEnvelope = vi.fn(); -const mockResolveRustBridgeCommandPlan = vi.fn(); -const mockDocumentBridgeErrorResponse = vi.fn((error: unknown) => - Response.json( - { - error: error instanceof Error ? error.message : String(error), - }, - { - status: - typeof (error as { status?: unknown })?.status === "number" - ? ((error as { status: number }).status ?? 500) - : 500, - }, - ), -); - -vi.mock("@/lib/convex/enabled", () => ({ - isConvexEnabled: () => mockIsConvexEnabled(), -})); - -vi.mock("@/lib/documents/bridge", () => ({ - buildDocumentBridgeContext: mockBuildDocumentBridgeContext, - buildDocumentCommandEnvelope: mockBuildDocumentCommandEnvelope, - documentBridgeErrorResponse: (...args: Parameters) => - mockDocumentBridgeErrorResponse(...args), -})); - -vi.mock("@/lib/documents/rust-runtime", () => ({ - resolveRustBridgeCommandPlan: (...args: Parameters) => - mockResolveRustBridgeCommandPlan(...args), -})); - -describe("/api/tree/filetree/delete-preflight route", () => { - beforeEach(() => { - mockIsConvexEnabled.mockReturnValue(true); - mockBuildDocumentBridgeContext.mockReset(); - mockBuildDocumentCommandEnvelope.mockReset(); - mockResolveRustBridgeCommandPlan.mockReset(); - mockDocumentBridgeErrorResponse.mockClear(); - }); - - it("应通过 Rust tree.filetree.delete.preflight 返回规范化 delete plan", async () => { - mockBuildDocumentBridgeContext.mockResolvedValue({ - requestId: "req_filetree_delete_1", - traceId: "trace_filetree_delete_1", - workspaceId: "ws_1", - actor: { - actorType: "user", - actorId: "user_1", - sessionId: null, - }, - source: { - channel: "next-route", - client: "wolai-frontend", - }, - tenantId: null, - deploymentId: null, - projectId: null, - authToken: null, - idempotencyKey: null, - validateOnly: true, - dryRun: true, - }); - mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); - mockResolveRustBridgeCommandPlan.mockResolvedValue({ - kind: "command", - commandName: "tree.filetree.delete.preflight", - commandId: "cmd_filetree_delete_1", - functionName: "tree:fileTreeDeletePreflight", - workspaceId: "ws_1", - requestId: "req_filetree_delete_1", - traceId: "trace_filetree_delete_1", - actorId: "user_1", - idempotencyKey: null, - payloadJson: "{}", - argsJson: { - fileTreeDeletePlan: { - rowIds: ["doc:doc_1", "asset:asset_1"], - docIds: ["doc_1"], - assetIds: ["asset_1"], - assetDocumentIds: ["doc_other"], - }, - }, - }); - - const { POST } = await import("./route"); - const response = await POST( - new Request("http://127.0.0.1:3000/api/tree/filetree/delete-preflight", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - workspaceId: "ws_1", - rowIds: ["doc:doc_1", "asset:asset_1"], - rows: [], - documentParents: [], - }), - }), - ); - - expect(response.status).toBe(200); - expect(await response.json()).toEqual({ - requestId: "req_filetree_delete_1", - traceId: "trace_filetree_delete_1", - plan: { - rowIds: ["doc:doc_1", "asset:asset_1"], - docIds: ["doc_1"], - assetIds: ["asset_1"], - assetDocumentIds: ["doc_other"], - }, - }); - expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( - expect.objectContaining({ - name: "tree.filetree.delete.preflight", - payload: expect.objectContaining({ - workspaceId: "ws_1", - rowIds: ["doc:doc_1", "asset:asset_1"], - }), - reason: "filetree-delete-preflight tree.filetree.delete.preflight", - refs: ["file-tree-shell"], - }), - ); - }); -}); diff --git a/wolai-frontend/src/app/api/tree/filetree/delete-preflight/route.ts b/wolai-frontend/src/app/api/tree/filetree/delete-preflight/route.ts deleted file mode 100644 index 2e7b4a5d..00000000 --- a/wolai-frontend/src/app/api/tree/filetree/delete-preflight/route.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { - buildDocumentBridgeContext, - buildDocumentCommandEnvelope, - documentBridgeErrorResponse, -} from "@/lib/documents/bridge"; -import { resolveRustBridgeCommandPlan } from "@/lib/documents/rust-runtime"; - -type FileTreeDeletePreflightPayload = { - workspaceId?: string | null; - rowIds?: string[]; - rows?: unknown[]; - documentParents?: Array<{ documentId?: string | null; parentId?: string | null }>; -}; - -function trimOrNull(value: unknown) { - if (typeof value !== "string") return null; - const trimmed = value.trim(); - return trimmed.length > 0 ? trimmed : null; -} - -function normalizeStringArray(value: unknown): string[] { - if (!Array.isArray(value)) return []; - return value.map((item) => (typeof item === "string" ? item : "")).filter(Boolean); -} - -function readFileTreeDeletePlan(plan: Awaited>) { - const value = plan.argsJson.fileTreeDeletePlan; - if (!value || typeof value !== "object" || Array.isArray(value)) { - throw new Error("Rust runtime 未返回 fileTreeDeletePlan"); - } - return value; -} - -export async function POST(request: Request) { - if (!isConvexEnabled()) { - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - } - - try { - const payload = (await request.json()) as FileTreeDeletePreflightPayload; - const workspaceId = trimOrNull(payload.workspaceId); - const normalizedPayload = { - workspaceId, - rowIds: normalizeStringArray(payload.rowIds), - rows: Array.isArray(payload.rows) ? payload.rows : [], - documentParents: Array.isArray(payload.documentParents) - ? payload.documentParents.map((item) => ({ - documentId: trimOrNull(item?.documentId), - parentId: trimOrNull(item?.parentId), - })) - : [], - }; - const context = await buildDocumentBridgeContext({ - request, - workspaceId, - validateOnly: true, - dryRun: true, - }); - const envelope = buildDocumentCommandEnvelope({ - name: "tree.filetree.delete.preflight", - payload: normalizedPayload, - context, - target: { - workspaceId, - }, - reason: "filetree-delete-preflight tree.filetree.delete.preflight", - refs: ["file-tree-shell"], - }); - const plan = await resolveRustBridgeCommandPlan({ - context, - envelope, - }); - - return NextResponse.json({ - requestId: context.requestId, - traceId: context.traceId, - plan: readFileTreeDeletePlan(plan), - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } -} diff --git a/wolai-frontend/src/app/api/tree/filetree/drop-preflight/route.test.ts b/wolai-frontend/src/app/api/tree/filetree/drop-preflight/route.test.ts deleted file mode 100644 index 2b795dc6..00000000 --- a/wolai-frontend/src/app/api/tree/filetree/drop-preflight/route.test.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const mockIsConvexEnabled = vi.fn(() => true); -const mockBuildDocumentBridgeContext = vi.fn(); -const mockBuildDocumentCommandEnvelope = vi.fn(); -const mockResolveRustBridgeCommandPlan = vi.fn(); -const mockDocumentBridgeErrorResponse = vi.fn((error: unknown) => - Response.json( - { - error: error instanceof Error ? error.message : String(error), - }, - { - status: - typeof (error as { status?: unknown })?.status === "number" - ? ((error as { status: number }).status ?? 500) - : 500, - }, - ), -); - -vi.mock("@/lib/convex/enabled", () => ({ - isConvexEnabled: () => mockIsConvexEnabled(), -})); - -vi.mock("@/lib/documents/bridge", () => ({ - buildDocumentBridgeContext: mockBuildDocumentBridgeContext, - buildDocumentCommandEnvelope: mockBuildDocumentCommandEnvelope, - documentBridgeErrorResponse: (...args: Parameters) => - mockDocumentBridgeErrorResponse(...args), -})); - -vi.mock("@/lib/documents/rust-runtime", () => ({ - resolveRustBridgeCommandPlan: (...args: Parameters) => - mockResolveRustBridgeCommandPlan(...args), -})); - -describe("/api/tree/filetree/drop-preflight route", () => { - beforeEach(() => { - mockIsConvexEnabled.mockReturnValue(true); - mockBuildDocumentBridgeContext.mockReset(); - mockBuildDocumentCommandEnvelope.mockReset(); - mockResolveRustBridgeCommandPlan.mockReset(); - mockDocumentBridgeErrorResponse.mockClear(); - }); - - it("应通过 Rust tree.filetree.drop.preflight 返回规范化 drop plan", async () => { - mockBuildDocumentBridgeContext.mockResolvedValue({ - requestId: "req_filetree_drop_1", - traceId: "trace_filetree_drop_1", - workspaceId: "ws_1", - actor: { - actorType: "user", - actorId: "user_1", - sessionId: null, - }, - source: { - channel: "next-route", - client: "wolai-frontend", - }, - tenantId: null, - deploymentId: null, - projectId: null, - authToken: null, - idempotencyKey: null, - validateOnly: true, - dryRun: true, - }); - mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); - mockResolveRustBridgeCommandPlan.mockResolvedValue({ - kind: "command", - commandName: "tree.filetree.drop.preflight", - commandId: "cmd_filetree_drop_1", - functionName: "tree:fileTreeDropPreflight", - workspaceId: "ws_1", - requestId: "req_filetree_drop_1", - traceId: "trace_filetree_drop_1", - actorId: "user_1", - idempotencyKey: null, - payloadJson: "{}", - argsJson: { - fileTreeDropPlan: { - copy: false, - targetDocumentId: "doc_target", - targetMindmapId: null, - targetSubPath: null, - rowIds: ["doc:doc_1"], - docIds: ["doc_1"], - topLevelDocIds: ["doc_1"], - copyableAssetIds: [], - sourceAssetDocumentIds: [], - documentTransferPlan: { - action: "move", - targetParentId: "doc_target", - documentIds: ["doc_1"], - topLevelDocumentIds: ["doc_1"], - copyItems: [{ documentId: "doc_1", recursive: true }], - }, - resourceTransferPlan: null, - }, - }, - }); - - const { POST } = await import("./route"); - const response = await POST( - new Request("http://127.0.0.1:3000/api/tree/filetree/drop-preflight", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - workspaceId: "ws_1", - copy: false, - targetDocumentId: "doc_target", - targetRowId: null, - focusedRowId: null, - activeDocumentId: null, - rowIds: ["doc:doc_1"], - rows: [], - documentParents: [], - }), - }), - ); - - expect(response.status).toBe(200); - expect(await response.json()).toEqual({ - requestId: "req_filetree_drop_1", - traceId: "trace_filetree_drop_1", - plan: { - copy: false, - targetDocumentId: "doc_target", - targetMindmapId: null, - targetSubPath: null, - rowIds: ["doc:doc_1"], - docIds: ["doc_1"], - topLevelDocIds: ["doc_1"], - copyableAssetIds: [], - sourceAssetDocumentIds: [], - documentTransferPlan: { - action: "move", - targetParentId: "doc_target", - documentIds: ["doc_1"], - topLevelDocumentIds: ["doc_1"], - copyItems: [{ documentId: "doc_1", recursive: true }], - }, - resourceTransferPlan: null, - }, - }); - expect(mockBuildDocumentBridgeContext).toHaveBeenCalledWith( - expect.objectContaining({ - workspaceId: "ws_1", - validateOnly: true, - dryRun: true, - }), - ); - expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( - expect.objectContaining({ - name: "tree.filetree.drop.preflight", - payload: expect.objectContaining({ - workspaceId: "ws_1", - targetDocumentId: "doc_target", - rowIds: ["doc:doc_1"], - }), - reason: "filetree-drop-preflight tree.filetree.drop.preflight", - refs: ["file-tree-shell"], - }), - ); - }); -}); diff --git a/wolai-frontend/src/app/api/tree/filetree/drop-preflight/route.ts b/wolai-frontend/src/app/api/tree/filetree/drop-preflight/route.ts deleted file mode 100644 index 12a40259..00000000 --- a/wolai-frontend/src/app/api/tree/filetree/drop-preflight/route.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { - buildDocumentBridgeContext, - buildDocumentCommandEnvelope, - documentBridgeErrorResponse, -} from "@/lib/documents/bridge"; -import { resolveRustBridgeCommandPlan } from "@/lib/documents/rust-runtime"; - -type FileTreeDropPreflightPayload = { - workspaceId?: string | null; - copy?: boolean; - targetDocumentId?: string | null; - targetRowId?: string | null; - focusedRowId?: string | null; - activeDocumentId?: string | null; - rowIds?: string[]; - rows?: unknown[]; - documentParents?: Array<{ documentId?: string | null; parentId?: string | null }>; -}; - -function trimOrNull(value: unknown) { - if (typeof value !== "string") return null; - const trimmed = value.trim(); - return trimmed.length > 0 ? trimmed : null; -} - -function normalizeStringArray(value: unknown): string[] { - if (!Array.isArray(value)) return []; - return value.map((item) => (typeof item === "string" ? item : "")).filter(Boolean); -} - -function readFileTreeDropPlan(plan: Awaited>) { - const value = plan.argsJson.fileTreeDropPlan; - if (!value || typeof value !== "object" || Array.isArray(value)) { - throw new Error("Rust runtime 未返回 fileTreeDropPlan"); - } - return value; -} - -export async function POST(request: Request) { - if (!isConvexEnabled()) { - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - } - - try { - const payload = (await request.json()) as FileTreeDropPreflightPayload; - const workspaceId = trimOrNull(payload.workspaceId); - const normalizedPayload = { - workspaceId, - copy: Boolean(payload.copy), - targetDocumentId: trimOrNull(payload.targetDocumentId), - targetRowId: trimOrNull(payload.targetRowId), - focusedRowId: trimOrNull(payload.focusedRowId), - activeDocumentId: trimOrNull(payload.activeDocumentId), - rowIds: normalizeStringArray(payload.rowIds), - rows: Array.isArray(payload.rows) ? payload.rows : [], - documentParents: Array.isArray(payload.documentParents) - ? payload.documentParents.map((item) => ({ - documentId: trimOrNull(item?.documentId), - parentId: trimOrNull(item?.parentId), - })) - : [], - }; - const context = await buildDocumentBridgeContext({ - request, - workspaceId, - validateOnly: true, - dryRun: true, - }); - const envelope = buildDocumentCommandEnvelope({ - name: "tree.filetree.drop.preflight", - payload: normalizedPayload, - context, - target: { - workspaceId, - pageId: normalizedPayload.targetDocumentId ?? undefined, - }, - reason: "filetree-drop-preflight tree.filetree.drop.preflight", - refs: ["file-tree-shell"], - }); - const plan = await resolveRustBridgeCommandPlan({ - context, - envelope, - }); - - return NextResponse.json({ - requestId: context.requestId, - traceId: context.traceId, - plan: readFileTreeDropPlan(plan), - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } -} diff --git a/wolai-frontend/src/app/api/tree/filetree/paste-preflight/route.test.ts b/wolai-frontend/src/app/api/tree/filetree/paste-preflight/route.test.ts deleted file mode 100644 index 4a065964..00000000 --- a/wolai-frontend/src/app/api/tree/filetree/paste-preflight/route.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const mockIsConvexEnabled = vi.fn(() => true); -const mockBuildDocumentBridgeContext = vi.fn(); -const mockBuildDocumentCommandEnvelope = vi.fn(); -const mockResolveRustBridgeCommandPlan = vi.fn(); -const mockDocumentBridgeErrorResponse = vi.fn((error: unknown) => - Response.json( - { - error: error instanceof Error ? error.message : String(error), - }, - { - status: - typeof (error as { status?: unknown })?.status === "number" - ? ((error as { status: number }).status ?? 500) - : 500, - }, - ), -); - -vi.mock("@/lib/convex/enabled", () => ({ - isConvexEnabled: () => mockIsConvexEnabled(), -})); - -vi.mock("@/lib/documents/bridge", () => ({ - buildDocumentBridgeContext: mockBuildDocumentBridgeContext, - buildDocumentCommandEnvelope: mockBuildDocumentCommandEnvelope, - documentBridgeErrorResponse: (...args: Parameters) => - mockDocumentBridgeErrorResponse(...args), -})); - -vi.mock("@/lib/documents/rust-runtime", () => ({ - resolveRustBridgeCommandPlan: (...args: Parameters) => - mockResolveRustBridgeCommandPlan(...args), -})); - -describe("/api/tree/filetree/paste-preflight route", () => { - beforeEach(() => { - mockIsConvexEnabled.mockReturnValue(true); - mockBuildDocumentBridgeContext.mockReset(); - mockBuildDocumentCommandEnvelope.mockReset(); - mockResolveRustBridgeCommandPlan.mockReset(); - mockDocumentBridgeErrorResponse.mockClear(); - }); - - it("应通过 Rust tree.filetree.paste.preflight 返回规范化 paste plan", async () => { - mockBuildDocumentBridgeContext.mockResolvedValue({ - requestId: "req_filetree_paste_1", - traceId: "trace_filetree_paste_1", - workspaceId: "ws_1", - actor: { - actorType: "user", - actorId: "user_1", - sessionId: null, - }, - source: { - channel: "next-route", - client: "wolai-frontend", - }, - tenantId: null, - deploymentId: null, - projectId: null, - authToken: null, - idempotencyKey: null, - validateOnly: true, - dryRun: true, - }); - mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); - mockResolveRustBridgeCommandPlan.mockResolvedValue({ - kind: "command", - commandName: "tree.filetree.paste.preflight", - commandId: "cmd_filetree_paste_1", - functionName: "tree:fileTreePastePreflight", - workspaceId: "ws_1", - requestId: "req_filetree_paste_1", - traceId: "trace_filetree_paste_1", - actorId: "user_1", - idempotencyKey: null, - payloadJson: "{}", - argsJson: { - fileTreePastePlan: { - targetDocumentId: "doc_target", - targetMindmapId: "mind_1", - targetSubPath: "mindmaps/mind_1", - rowIds: ["index:doc_1", "asset:asset_1"], - docItems: [{ documentId: "doc_1", recursive: false }], - copyableAssetIds: ["asset_1"], - resourceTransferPlan: { - action: "copy", - assetIds: ["asset_1"], - targetDocumentId: "doc_target", - targetSubPath: "mindmaps/mind_1", - }, - }, - }, - }); - - const { POST } = await import("./route"); - const response = await POST( - new Request("http://127.0.0.1:3000/api/tree/filetree/paste-preflight", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - workspaceId: "ws_1", - targetDocumentId: null, - focusedRowId: "asset-folder:mind_1", - activeDocumentId: "doc_active", - rowIds: ["index:doc_1", "asset:asset_1"], - rows: [], - }), - }), - ); - - expect(response.status).toBe(200); - expect(await response.json()).toEqual({ - requestId: "req_filetree_paste_1", - traceId: "trace_filetree_paste_1", - plan: { - targetDocumentId: "doc_target", - targetMindmapId: "mind_1", - targetSubPath: "mindmaps/mind_1", - rowIds: ["index:doc_1", "asset:asset_1"], - docItems: [{ documentId: "doc_1", recursive: false }], - copyableAssetIds: ["asset_1"], - resourceTransferPlan: { - action: "copy", - assetIds: ["asset_1"], - targetDocumentId: "doc_target", - targetSubPath: "mindmaps/mind_1", - }, - }, - }); - expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( - expect.objectContaining({ - name: "tree.filetree.paste.preflight", - payload: expect.objectContaining({ - workspaceId: "ws_1", - focusedRowId: "asset-folder:mind_1", - rowIds: ["index:doc_1", "asset:asset_1"], - }), - reason: "filetree-paste-preflight tree.filetree.paste.preflight", - refs: ["file-tree-shell"], - }), - ); - }); -}); diff --git a/wolai-frontend/src/app/api/tree/filetree/paste-preflight/route.ts b/wolai-frontend/src/app/api/tree/filetree/paste-preflight/route.ts deleted file mode 100644 index f9051738..00000000 --- a/wolai-frontend/src/app/api/tree/filetree/paste-preflight/route.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { - buildDocumentBridgeContext, - buildDocumentCommandEnvelope, - documentBridgeErrorResponse, -} from "@/lib/documents/bridge"; -import { resolveRustBridgeCommandPlan } from "@/lib/documents/rust-runtime"; - -type FileTreePastePreflightPayload = { - workspaceId?: string | null; - targetDocumentId?: string | null; - focusedRowId?: string | null; - activeDocumentId?: string | null; - rowIds?: string[]; - rows?: unknown[]; -}; - -function trimOrNull(value: unknown) { - if (typeof value !== "string") return null; - const trimmed = value.trim(); - return trimmed.length > 0 ? trimmed : null; -} - -function normalizeStringArray(value: unknown): string[] { - if (!Array.isArray(value)) return []; - return value.map((item) => (typeof item === "string" ? item : "")).filter(Boolean); -} - -function readFileTreePastePlan(plan: Awaited>) { - const value = plan.argsJson.fileTreePastePlan; - if (!value || typeof value !== "object" || Array.isArray(value)) { - throw new Error("Rust runtime 未返回 fileTreePastePlan"); - } - return value; -} - -export async function POST(request: Request) { - if (!isConvexEnabled()) { - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - } - - try { - const payload = (await request.json()) as FileTreePastePreflightPayload; - const workspaceId = trimOrNull(payload.workspaceId); - const normalizedPayload = { - workspaceId, - targetDocumentId: trimOrNull(payload.targetDocumentId), - focusedRowId: trimOrNull(payload.focusedRowId), - activeDocumentId: trimOrNull(payload.activeDocumentId), - rowIds: normalizeStringArray(payload.rowIds), - rows: Array.isArray(payload.rows) ? payload.rows : [], - }; - const context = await buildDocumentBridgeContext({ - request, - workspaceId, - validateOnly: true, - dryRun: true, - }); - const envelope = buildDocumentCommandEnvelope({ - name: "tree.filetree.paste.preflight", - payload: normalizedPayload, - context, - target: { - workspaceId, - pageId: normalizedPayload.targetDocumentId ?? undefined, - }, - reason: "filetree-paste-preflight tree.filetree.paste.preflight", - refs: ["file-tree-shell"], - }); - const plan = await resolveRustBridgeCommandPlan({ - context, - envelope, - }); - - return NextResponse.json({ - requestId: context.requestId, - traceId: context.traceId, - plan: readFileTreePastePlan(plan), - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } -} diff --git a/wolai-frontend/src/app/api/tree/filetree/upload-target-preflight/route.test.ts b/wolai-frontend/src/app/api/tree/filetree/upload-target-preflight/route.test.ts deleted file mode 100644 index f8b43ade..00000000 --- a/wolai-frontend/src/app/api/tree/filetree/upload-target-preflight/route.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const mockIsConvexEnabled = vi.fn(() => true); -const mockBuildDocumentBridgeContext = vi.fn(); -const mockBuildDocumentCommandEnvelope = vi.fn(); -const mockResolveRustBridgeCommandPlan = vi.fn(); -const mockDocumentBridgeErrorResponse = vi.fn((error: unknown) => - Response.json( - { - error: error instanceof Error ? error.message : String(error), - }, - { - status: - typeof (error as { status?: unknown })?.status === "number" - ? ((error as { status: number }).status ?? 500) - : 500, - }, - ), -); - -vi.mock("@/lib/convex/enabled", () => ({ - isConvexEnabled: () => mockIsConvexEnabled(), -})); - -vi.mock("@/lib/documents/bridge", () => ({ - buildDocumentBridgeContext: mockBuildDocumentBridgeContext, - buildDocumentCommandEnvelope: mockBuildDocumentCommandEnvelope, - documentBridgeErrorResponse: (...args: Parameters) => - mockDocumentBridgeErrorResponse(...args), -})); - -vi.mock("@/lib/documents/rust-runtime", () => ({ - resolveRustBridgeCommandPlan: (...args: Parameters) => - mockResolveRustBridgeCommandPlan(...args), -})); - -describe("/api/tree/filetree/upload-target-preflight route", () => { - beforeEach(() => { - mockIsConvexEnabled.mockReturnValue(true); - mockBuildDocumentBridgeContext.mockReset(); - mockBuildDocumentCommandEnvelope.mockReset(); - mockResolveRustBridgeCommandPlan.mockReset(); - mockDocumentBridgeErrorResponse.mockClear(); - }); - - it("应通过 Rust tree.filetree.upload-target.preflight 返回规范化 upload target plan", async () => { - mockBuildDocumentBridgeContext.mockResolvedValue({ - requestId: "req_filetree_upload_target_1", - traceId: "trace_filetree_upload_target_1", - workspaceId: "ws_1", - actor: { - actorType: "user", - actorId: "user_1", - sessionId: null, - }, - source: { - channel: "next-route", - client: "wolai-frontend", - }, - tenantId: null, - deploymentId: null, - projectId: null, - authToken: null, - idempotencyKey: null, - validateOnly: true, - dryRun: true, - }); - mockBuildDocumentCommandEnvelope.mockImplementation((input: unknown) => input); - mockResolveRustBridgeCommandPlan.mockResolvedValue({ - kind: "command", - commandName: "tree.filetree.upload-target.preflight", - commandId: "cmd_filetree_upload_target_1", - functionName: "tree:fileTreeUploadTargetPreflight", - workspaceId: "ws_1", - requestId: "req_filetree_upload_target_1", - traceId: "trace_filetree_upload_target_1", - actorId: "user_1", - idempotencyKey: null, - payloadJson: "{}", - argsJson: { - fileTreeUploadTargetPlan: { - workspaceId: "ws_1", - targetDocumentId: "doc_target", - targetMindmapId: "mind_1", - targetSubPath: "mindmaps/mind_1", - }, - }, - }); - - const { POST } = await import("./route"); - const response = await POST( - new Request("http://127.0.0.1:3000/api/tree/filetree/upload-target-preflight", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - workspaceId: "ws_fallback", - targetDocumentId: null, - targetRowId: "asset:asset_child_1", - focusedRowId: null, - activeDocumentId: "doc_active", - rows: [], - documentWorkspaces: [{ documentId: "doc_target", workspaceId: "ws_1" }], - }), - }), - ); - - expect(response.status).toBe(200); - expect(await response.json()).toEqual({ - requestId: "req_filetree_upload_target_1", - traceId: "trace_filetree_upload_target_1", - plan: { - workspaceId: "ws_1", - targetDocumentId: "doc_target", - targetMindmapId: "mind_1", - targetSubPath: "mindmaps/mind_1", - }, - }); - expect(mockBuildDocumentCommandEnvelope).toHaveBeenCalledWith( - expect.objectContaining({ - name: "tree.filetree.upload-target.preflight", - payload: expect.objectContaining({ - workspaceId: "ws_fallback", - targetRowId: "asset:asset_child_1", - activeDocumentId: "doc_active", - }), - reason: "filetree-upload-target-preflight tree.filetree.upload-target.preflight", - refs: ["file-tree-shell"], - }), - ); - }); -}); diff --git a/wolai-frontend/src/app/api/tree/filetree/upload-target-preflight/route.ts b/wolai-frontend/src/app/api/tree/filetree/upload-target-preflight/route.ts deleted file mode 100644 index 4190ba62..00000000 --- a/wolai-frontend/src/app/api/tree/filetree/upload-target-preflight/route.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { - buildDocumentBridgeContext, - buildDocumentCommandEnvelope, - documentBridgeErrorResponse, -} from "@/lib/documents/bridge"; -import { resolveRustBridgeCommandPlan } from "@/lib/documents/rust-runtime"; - -type FileTreeUploadTargetPreflightPayload = { - workspaceId?: string | null; - targetDocumentId?: string | null; - targetRowId?: string | null; - focusedRowId?: string | null; - activeDocumentId?: string | null; - rows?: unknown[]; - documentWorkspaces?: Array<{ documentId?: string | null; workspaceId?: string | null }>; -}; - -function trimOrNull(value: unknown) { - if (typeof value !== "string") return null; - const trimmed = value.trim(); - return trimmed.length > 0 ? trimmed : null; -} - -function readFileTreeUploadTargetPlan( - plan: Awaited>, -) { - const value = plan.argsJson.fileTreeUploadTargetPlan; - if (!value || typeof value !== "object" || Array.isArray(value)) { - throw new Error("Rust runtime 未返回 fileTreeUploadTargetPlan"); - } - return value; -} - -export async function POST(request: Request) { - if (!isConvexEnabled()) { - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - } - - try { - const payload = (await request.json()) as FileTreeUploadTargetPreflightPayload; - const workspaceId = trimOrNull(payload.workspaceId); - const normalizedPayload = { - workspaceId, - targetDocumentId: trimOrNull(payload.targetDocumentId), - targetRowId: trimOrNull(payload.targetRowId), - focusedRowId: trimOrNull(payload.focusedRowId), - activeDocumentId: trimOrNull(payload.activeDocumentId), - rows: Array.isArray(payload.rows) ? payload.rows : [], - documentWorkspaces: Array.isArray(payload.documentWorkspaces) - ? payload.documentWorkspaces.map((item) => ({ - documentId: trimOrNull(item?.documentId), - workspaceId: trimOrNull(item?.workspaceId), - })) - : [], - }; - const context = await buildDocumentBridgeContext({ - request, - workspaceId, - validateOnly: true, - dryRun: true, - }); - const envelope = buildDocumentCommandEnvelope({ - name: "tree.filetree.upload-target.preflight", - payload: normalizedPayload, - context, - target: { - workspaceId, - pageId: normalizedPayload.targetDocumentId ?? undefined, - }, - reason: "filetree-upload-target-preflight tree.filetree.upload-target.preflight", - refs: ["file-tree-shell"], - }); - const plan = await resolveRustBridgeCommandPlan({ - context, - envelope, - }); - - return NextResponse.json({ - requestId: context.requestId, - traceId: context.traceId, - plan: readFileTreeUploadTargetPlan(plan), - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } -} diff --git a/wolai-frontend/src/app/api/tree/projections/file/route.test.ts b/wolai-frontend/src/app/api/tree/projections/file/route.test.ts deleted file mode 100644 index 0748e024..00000000 --- a/wolai-frontend/src/app/api/tree/projections/file/route.test.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const mockIsConvexEnabled = vi.fn(); -const mockGetAuthedConvexClient = vi.fn(); -const mockBuildDocumentBridgeContext = vi.fn(); -const mockBuildDocumentQueryEnvelope = vi.fn(); -const mockResolveRustBridgeQueryPlan = vi.fn(); -const mockExecuteRustBridgeQueryTransport = vi.fn(); -const mockResolveKernelFileTreeProjection = vi.fn(); -const mockDocumentBridgeErrorResponse = vi.fn((error: unknown) => - Response.json( - { - error: error instanceof Error ? error.message : String(error), - }, - { status: 500 }, - ), -); - -vi.mock("@/lib/convex/enabled", () => ({ - isConvexEnabled: () => mockIsConvexEnabled(), -})); - -vi.mock("@/lib/convex/route", () => ({ - getAuthedConvexClient: () => mockGetAuthedConvexClient(), -})); - -vi.mock("@/lib/documents/bridge", () => ({ - buildDocumentBridgeContext: (...args: unknown[]) => mockBuildDocumentBridgeContext(...args), - buildDocumentQueryEnvelope: (...args: unknown[]) => mockBuildDocumentQueryEnvelope(...args), - documentBridgeErrorResponse: (...args: unknown[]) => mockDocumentBridgeErrorResponse(...args), -})); - -vi.mock("@/lib/documents/rust-runtime", () => ({ - executeRustBridgeQueryTransport: (...args: unknown[]) => - mockExecuteRustBridgeQueryTransport(...args), - resolveRustBridgeQueryPlan: (...args: unknown[]) => mockResolveRustBridgeQueryPlan(...args), -})); - -vi.mock("@/lib/server/kernel-file-tree", async () => { - const actual = await vi.importActual( - "@/lib/server/kernel-file-tree", - ); - return { - ...actual, - resolveKernelFileTreeProjection: (...args: unknown[]) => - mockResolveKernelFileTreeProjection(...args), - }; -}); - -describe("/api/tree/projections/file route", () => { - beforeEach(() => { - vi.resetModules(); - mockIsConvexEnabled.mockReset().mockReturnValue(true); - mockGetAuthedConvexClient.mockReset().mockResolvedValue({ - auth: { - userId: "user_1", - }, - client: { - query: vi.fn(), - mutation: vi.fn(), - }, - }); - mockBuildDocumentBridgeContext.mockReset().mockResolvedValue({ - requestId: "req_1", - traceId: "trace_1", - }); - mockBuildDocumentQueryEnvelope.mockReset().mockImplementation((input) => input); - mockResolveRustBridgeQueryPlan.mockReset().mockResolvedValue({ - functionName: "sidebar:datasetList", - argsJson: { - workspaceId: "ws_1", - }, - }); - mockExecuteRustBridgeQueryTransport.mockReset().mockResolvedValue({ - active_workspace_id: "ws_1", - documents: [], - media_assets: [], - mindmap_assets: [], - table_assets: [], - mindmap_asset_children: {}, - }); - mockResolveKernelFileTreeProjection.mockReset().mockResolvedValue({ - projectionId: "kernel_projection:file_tree:page_root", - projection: "file_tree", - rootNodeId: "page_root", - items: [ - { - rowId: "doc:page_root", - nodeId: "page_root", - projectionKind: "file_tree", - rowKind: "document", - }, - { - rowId: "asset:table_1", - nodeId: "asset:table_1", - projectionKind: "file_tree", - rowKind: "asset", - }, - ], - edges: [], - meta: { - search: { - indexingVisibility: { - schema: "mnote.file_tree.indexing_visibility", - schemaVersion: 1, - source: "kernel.project_view", - status: "visible", - requestKey: "page_root:预算", - indexedResourceKinds: ["document", "index", "asset"], - visibleResourceKinds: ["document", "index", "asset"], - metrics: { - visibleRows: 2, - visibleEdges: 0, - }, - }, - }, - }, - }); - mockDocumentBridgeErrorResponse.mockClear(); - }); - - it("通过 3000 同源 route 返回 Rust file_tree 搜索 projection", async () => { - const fetchSpy = vi.spyOn(globalThis, "fetch"); - - const { GET } = await import("./route"); - const response = await GET( - new Request( - "http://127.0.0.1:3000/api/tree/projections/file?workspaceId=ws_1&rootNodeId=page_root&depth=3&query=%E9%A2%84%E7%AE%97&maxResults=12", - { method: "GET" }, - ), - ); - - expect(response.status).toBe(200); - expect(fetchSpy).not.toHaveBeenCalled(); - expect(mockResolveKernelFileTreeProjection).toHaveBeenCalledWith( - expect.objectContaining({ - workspaceId: "ws_1", - rootNodeId: "page_root", - depth: 3, - query: "预算", - maxResults: 12, - }), - ); - const body = await response.json(); - expect(body).toMatchObject({ - ok: true, - result: { - projection: "file_tree", - rootNodeId: "page_root", - meta: { - search: { - indexingVisibility: { - schema: "mnote.file_tree.indexing_visibility", - status: "visible", - requestKey: "page_root:预算", - }, - }, - }, - }, - }); - expect(body.result.items.map((item: { rowId: string }) => item.rowId)).toEqual([ - "doc:page_root", - "asset:table_1", - ]); - }); - - it("Convex 未启用时返回 501", async () => { - mockIsConvexEnabled.mockReturnValue(false); - - const { GET } = await import("./route"); - const response = await GET( - new Request("http://127.0.0.1:3000/api/tree/projections/file?workspaceId=ws_1"), - ); - - expect(response.status).toBe(501); - expect(await response.json()).toEqual({ error: "当前仅支持 Convex 模式" }); - }); -}); diff --git a/wolai-frontend/src/app/api/tree/projections/file/route.ts b/wolai-frontend/src/app/api/tree/projections/file/route.ts deleted file mode 100644 index 4da362b8..00000000 --- a/wolai-frontend/src/app/api/tree/projections/file/route.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { getAuthedConvexClient } from "@/lib/convex/route"; -import { - buildDocumentBridgeContext, - buildDocumentQueryEnvelope, - documentBridgeErrorResponse, -} from "@/lib/documents/bridge"; -import { - executeRustBridgeQueryTransport, - resolveRustBridgeQueryPlan, -} from "@/lib/documents/rust-runtime"; -import type { SidebarDatasetListQueryResult } from "@/lib/sidebar-data"; -import { resolveKernelFileTreeProjection } from "@/lib/server/kernel-file-tree"; - -export const dynamic = "force-dynamic"; -export const runtime = "nodejs"; - -function readNumberParam(url: URL, name: string): number | null { - const raw = url.searchParams.get(name); - if (!raw?.trim()) { - return null; - } - const parsed = Number(raw); - return Number.isFinite(parsed) ? parsed : null; -} - -export async function GET(request: Request) { - if (!isConvexEnabled()) { - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - } - - const url = new URL(request.url); - const workspaceId = url.searchParams.get("workspaceId")?.trim(); - if (!workspaceId) { - return NextResponse.json({ error: "缺少 workspaceId" }, { status: 400 }); - } - - try { - const { auth, client } = await getAuthedConvexClient(); - const context = await buildDocumentBridgeContext({ - request, - workspaceId, - }); - const envelope = buildDocumentQueryEnvelope({ - name: "sidebar.dataset.list", - payload: { - workspaceId, - }, - }); - const plan = await resolveRustBridgeQueryPlan({ - context, - envelope, - }); - const dataset = await executeRustBridgeQueryTransport({ - client, - plan, - }); - const projection = await resolveKernelFileTreeProjection({ - client, - request, - workspaceId, - actor: { - actorType: "user", - actorId: auth.userId, - sessionId: null, - }, - dataset, - rootNodeId: url.searchParams.get("rootNodeId")?.trim() || null, - depth: readNumberParam(url, "depth"), - query: url.searchParams.get("query")?.trim() || null, - maxResults: readNumberParam(url, "maxResults"), - }); - - return NextResponse.json({ - ok: true, - requestId: context.requestId, - traceId: context.traceId, - result: projection, - }); - } catch (error) { - return documentBridgeErrorResponse(error); - } -} diff --git a/wolai-frontend/src/app/api/tree/runtime/reduce/route.test.ts b/wolai-frontend/src/app/api/tree/runtime/reduce/route.test.ts deleted file mode 100644 index 8cf520ab..00000000 --- a/wolai-frontend/src/app/api/tree/runtime/reduce/route.test.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const mockResolveMnoteWebInternalUrl = vi.fn(); -const mockBuildForwardHeaders = vi.fn(); -const mockFetch = vi.fn(); - -vi.mock("@/lib/mnote-web/internal-url", () => ({ - resolveMnoteWebInternalUrl: (...args: unknown[]) => mockResolveMnoteWebInternalUrl(...args), -})); - -vi.mock("@/lib/server/forward-headers", () => ({ - buildForwardHeaders: (...args: unknown[]) => mockBuildForwardHeaders(...args), -})); - -describe("/api/tree/runtime/reduce route", () => { - beforeEach(() => { - vi.resetModules(); - mockResolveMnoteWebInternalUrl.mockReset(); - mockBuildForwardHeaders.mockReset(); - mockFetch.mockReset(); - vi.stubGlobal("fetch", mockFetch); - }); - - it("通过 3000 同源 POST 代理到 mnote-web runtime reduce,并原样转发 JSON body 与来源 header", async () => { - mockResolveMnoteWebInternalUrl.mockResolvedValue("http://127.0.0.1:3104"); - mockBuildForwardHeaders.mockResolvedValue( - new Headers({ - cookie: "session=abc", - authorization: "Bearer test-token", - }), - ); - mockFetch.mockResolvedValue( - new Response(JSON.stringify({ ok: true, revision: 7 }), { - status: 200, - statusText: "OK", - headers: { - "content-type": "application/json; charset=utf-8", - "set-cookie": "debug=1", - connection: "keep-alive", - "transfer-encoding": "chunked", - "x-upstream": "mnote-web-runtime", - }, - }), - ); - - const { POST } = await import("./route"); - const body = JSON.stringify({ - workspaceId: "ws_body", - op: "tree.node.rename", - payload: { documentId: "doc_1", title: "新标题" }, - }); - const response = await POST( - new Request("http://127.0.0.1:3000/api/tree/runtime/reduce?workspaceId=ws_query", { - method: "POST", - headers: { - "content-type": "application/json", - accept: "application/json", - cookie: "session=abc", - }, - body, - }), - ); - - expect(mockFetch).toHaveBeenCalledWith( - "http://127.0.0.1:3104/api/tree/runtime/reduce?workspaceId=ws_query", - expect.objectContaining({ - method: "POST", - headers: expect.any(Headers), - body, - cache: "no-store", - redirect: "manual", - }), - ); - - const fetchHeaders = mockFetch.mock.calls[0]?.[1]?.headers as Headers; - expect(fetchHeaders.get("cookie")).toBe("session=abc"); - expect(fetchHeaders.get("authorization")).toBe("Bearer test-token"); - expect(fetchHeaders.get("content-type")).toBe("application/json"); - expect(fetchHeaders.get("accept")).toBe("application/json"); - expect(fetchHeaders.get("x-mnote-source-channel")).toBe("next_tree_runtime_reduce_proxy"); - expect(fetchHeaders.get("x-mnote-source-client")).toBe("wolai-frontend"); - expect(fetchHeaders.get("x-mnote-workspace-id")).toBe("ws_query"); - - expect(response.status).toBe(200); - expect(response.headers.get("content-type")).toContain("application/json"); - expect(response.headers.get("cache-control")).toBe("no-store"); - expect(response.headers.get("set-cookie")).toBeNull(); - expect(response.headers.get("connection")).toBeNull(); - expect(response.headers.get("transfer-encoding")).toBeNull(); - expect(response.headers.get("x-upstream")).toBe("mnote-web-runtime"); - expect(await response.json()).toEqual({ ok: true, revision: 7 }); - }); - - it("没有 workspaceId query 时使用 x-mnote-workspace-id header fallback", async () => { - mockResolveMnoteWebInternalUrl.mockResolvedValue("http://127.0.0.1:3104"); - mockBuildForwardHeaders.mockResolvedValue( - new Headers({ - "x-mnote-workspace-id": "ws_header", - }), - ); - mockFetch.mockResolvedValue( - new Response(JSON.stringify({ ok: true }), { - status: 202, - headers: { - "content-type": "application/json", - }, - }), - ); - - const { POST } = await import("./route"); - await POST( - new Request("http://127.0.0.1:3000/api/tree/runtime/reduce", { - method: "POST", - headers: { - "content-type": "application/json", - "x-mnote-workspace-id": "ws_header", - }, - body: JSON.stringify({ op: "noop" }), - }), - ); - - expect(mockFetch).toHaveBeenCalledWith( - "http://127.0.0.1:3104/api/tree/runtime/reduce", - expect.objectContaining({ - method: "POST", - }), - ); - const fetchHeaders = mockFetch.mock.calls[0]?.[1]?.headers as Headers; - expect(fetchHeaders.get("x-mnote-workspace-id")).toBe("ws_header"); - }); - - it("上游调用异常时返回 502 JSON", async () => { - mockResolveMnoteWebInternalUrl.mockResolvedValue("http://127.0.0.1:3104"); - mockBuildForwardHeaders.mockResolvedValue(new Headers()); - mockFetch.mockRejectedValue(new Error("runtime reduce unavailable")); - - const { POST } = await import("./route"); - const response = await POST( - new Request("http://127.0.0.1:3000/api/tree/runtime/reduce", { - method: "POST", - headers: { - "content-type": "application/json", - }, - body: JSON.stringify({ op: "noop" }), - }), - ); - - expect(response.status).toBe(502); - expect(response.headers.get("cache-control")).toBe("no-store"); - expect(await response.json()).toEqual({ - error: "runtime reduce unavailable", - }); - }); -}); diff --git a/wolai-frontend/src/app/api/tree/runtime/reduce/route.ts b/wolai-frontend/src/app/api/tree/runtime/reduce/route.ts deleted file mode 100644 index b1a2a6cf..00000000 --- a/wolai-frontend/src/app/api/tree/runtime/reduce/route.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { NextResponse } from "next/server"; -import { resolveMnoteWebInternalUrl } from "@/lib/mnote-web/internal-url"; -import { buildForwardHeaders } from "@/lib/server/forward-headers"; - -export const dynamic = "force-dynamic"; -export const runtime = "nodejs"; - -const stripHopByHopHeaders = (headers: Headers) => { - // 说明:代理响应不应继续透传 hop-by-hop headers,避免浏览器拿到无效连接语义。 - const hopByHopHeaders = [ - "connection", - "keep-alive", - "proxy-authenticate", - "proxy-authorization", - "te", - "trailer", - "transfer-encoding", - "upgrade", - "content-length", - ]; - hopByHopHeaders.forEach((name) => headers.delete(name)); -}; - -export async function POST(request: Request) { - try { - const requestUrl = new URL(request.url); - const internalBaseUrl = await resolveMnoteWebInternalUrl(); - const targetUrl = new URL("/api/tree/runtime/reduce", `${internalBaseUrl}/`); - targetUrl.search = requestUrl.search; - - const headers = await buildForwardHeaders(request); - headers.set("content-type", "application/json"); - headers.set("accept", "application/json"); - headers.set("x-mnote-source-channel", "next_tree_runtime_reduce_proxy"); - headers.set("x-mnote-source-client", "wolai-frontend"); - - const workspaceId = requestUrl.searchParams.get("workspaceId")?.trim(); - if (workspaceId && !headers.has("x-mnote-workspace-id")) { - headers.set("x-mnote-workspace-id", workspaceId); - } - - const upstream = await fetch(targetUrl.toString(), { - method: "POST", - headers, - body: await request.text(), - cache: "no-store", - redirect: "manual", - }); - const body = await upstream.arrayBuffer(); - const responseHeaders = new Headers(upstream.headers); - stripHopByHopHeaders(responseHeaders); - responseHeaders.delete("set-cookie"); - responseHeaders.set("cache-control", "no-store"); - - return new NextResponse(body, { - status: upstream.status, - statusText: upstream.statusText, - headers: responseHeaders, - }); - } catch (error) { - const message = error instanceof Error ? error.message : "tree runtime reduce 代理失败"; - return NextResponse.json( - { - error: message, - }, - { - status: 502, - headers: { - "cache-control": "no-store", - }, - }, - ); - } -} diff --git a/wolai-frontend/src/app/api/wolai-import/route.ts b/wolai-frontend/src/app/api/wolai-import/route.ts deleted file mode 100644 index 1ad7ef9e..00000000 --- a/wolai-frontend/src/app/api/wolai-import/route.ts +++ /dev/null @@ -1,985 +0,0 @@ -import { NextResponse } from "next/server"; -import { randomUUID } from "crypto"; -import path from "path"; -import { JSDOM } from "jsdom"; -import { BlockNoteEditor, BlockNoteSchema, markdownToBlocks } from "@blocknote/core"; -import type { Block } from "@blocknote/core"; -import type { Json } from "@/types/supabase"; -import type { MediaAsset } from "@/types/media"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { HttpError, requireAuthContext } from "@/lib/auth/authContext"; -import { getConvexAuthedHttpClient } from "@/lib/convex/server"; -import { api } from "@/lib/convex/api"; -import { getDocumentsBaseDir } from "@/lib/server/local-paths"; -import { promises as fs } from "fs"; -import os from "os"; -import { spawn } from "child_process"; - -export const runtime = "nodejs"; -export const dynamic = "force-dynamic"; - -let domShimReady = false; -function ensureDomShim() { - if (domShimReady) return; - const dom = new JSDOM(""); - const g = globalThis as any; - - // 说明:Node.js(以及 Next.js 运行时)可能已经提供了只读的 globalThis.navigator(getter-only)。 - // 这里仅在必要时注入 window/document 等基础对象,避免覆盖只读属性导致 500。 - if (!g.window) g.window = dom.window; - if (!g.document) g.document = dom.window.document; - if (!g.DOMParser) g.DOMParser = dom.window.DOMParser; - if (!g.HTMLElement) g.HTMLElement = dom.window.HTMLElement; - if (!g.Node) g.Node = dom.window.Node; - domShimReady = true; -} - -const documentsBaseDir = getDocumentsBaseDir(); - -async function ensureDocumentScaffold(id: string, title: string) { - const folder = path.join(documentsBaseDir, id); - const indexFile = path.join(folder, "index.md"); - await fs.mkdir(folder, { recursive: true }); - try { - await fs.access(indexFile); - } catch { - const safeTitle = title && title.trim() ? title.trim() : "无标题"; - await fs.writeFile(indexFile, `# ${safeTitle}\n`, "utf8"); - } -} - -type InputStore = { - kind: "dir"; - baseDir: string; - listFiles: () => Promise; - readText: (relPath: string) => Promise; - readBytes: (relPath: string) => Promise; - exists: (relPath: string) => Promise; -}; - -type ImportNode = - | { kind: "doc"; key: string; title: string; mdPath: string; parentKey: string | null } - | { kind: "folder"; key: string; title: string; dirPath: string; parentKey: string | null }; - -type TokenRecord = - | { kind: "image"; token: string; url: string; alt: string } - | { kind: "file"; token: string; url: string; label: string } - | { kind: "page"; token: string; targetMdRel: string; label: string }; - -function normalizeZipPath(value: string): string { - return value.replace(/\\/g, "/").replace(/^\.\//, ""); -} - -function isRemoteUrl(url: string): boolean { - // 说明:Wolai 导出里可能包含 mailto/tel/#锚点等“非文件路径”链接,这里统一视为远端(不当作本地文件处理)。 - return /^(https?:\/\/|data:|mailto:|tel:)/i.test(url) || url.trim().startsWith("#"); -} - -function stripAngleBrackets(url: string): string { - const trimmed = url.trim(); - if (trimmed.startsWith("<") && trimmed.endsWith(">")) { - return trimmed.slice(1, -1).trim(); - } - return trimmed; -} - -function safeBasename(filePath: string): string { - const base = path.posix.basename(filePath); - return base || "未命名资源"; -} - -function parseTopHeadingTitle(markdown: string): string | null { - const lines = markdown.split(/\r?\n/); - for (const line of lines) { - const t = line.trim(); - if (!t) continue; - const m = /^#\s+(.+)$/.exec(t); - if (!m) return null; - const title = m[1]?.trim() ?? ""; - return title || null; - } - return null; -} - -function stripTopHeadingIfMatches(markdown: string, title: string): string { - const lines = markdown.split(/\r?\n/); - let idx = 0; - while (idx < lines.length && !lines[idx].trim()) idx += 1; - const first = lines[idx] ?? ""; - const m = /^#\s+(.+)$/.exec(first.trim()); - if (!m) return markdown; - const head = (m[1] ?? "").trim(); - if (!head || head !== title.trim()) return markdown; - const rest = [...lines.slice(0, idx), ...lines.slice(idx + 1)]; - while (rest.length > 0 && !rest[0].trim()) rest.shift(); - return rest.join("\n"); -} - -function resolveRelativePosix(baseDirRel: string, rel: string): string { - const cleaned = normalizeZipPath(stripAngleBrackets(rel)); - if (!cleaned) return cleaned; - if (cleaned.startsWith("/")) return cleaned.replace(/^\/+/, ""); - return path.posix.normalize(path.posix.join(baseDirRel, cleaned)); -} - -function stripMarkdownLinkTitle(raw: string): string { - // 说明:处理 Wolai 导出的 title 属性:path "title" - return raw.split(/\s+\"/)[0]?.trim() ?? raw.trim(); -} - -function stripLocalQueryAndHash(url: string): string { - // 说明:本地文件路径不应包含 ?query/#hash;否则会导致资源无法匹配。 - const noHash = url.split("#")[0] ?? url; - const noQuery = (noHash ?? url).split("?")[0] ?? noHash ?? url; - return noQuery.trim(); -} - -function guessMimeType(fileName: string): string { - const ext = path.extname(fileName).toLowerCase(); - switch (ext) { - case ".png": - return "image/png"; - case ".jpg": - case ".jpeg": - return "image/jpeg"; - case ".gif": - return "image/gif"; - case ".webp": - return "image/webp"; - case ".svg": - return "image/svg+xml"; - case ".pdf": - return "application/pdf"; - case ".doc": - return "application/msword"; - case ".docx": - return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; - case ".xls": - return "application/vnd.ms-excel"; - case ".xlsx": - return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; - case ".ppt": - return "application/vnd.ms-powerpoint"; - case ".pptx": - return "application/vnd.openxmlformats-officedocument.presentationml.presentation"; - case ".mp3": - return "audio/mpeg"; - case ".wav": - return "audio/wav"; - case ".m4a": - return "audio/mp4"; - case ".mp4": - return "video/mp4"; - case ".mov": - return "video/quicktime"; - default: - return "application/octet-stream"; - } -} - -function resolveAssetTypeByMime(mime: string): "image" | "video" | "audio" | "file" { - if (mime.startsWith("image/")) return "image"; - if (mime.startsWith("video/")) return "video"; - if (mime.startsWith("audio/")) return "audio"; - return "file"; -} - -function toFsPath(baseDir: string, relPath: string): string { - const normalized = normalizeZipPath(relPath); - const parts = normalized.split("/").filter(Boolean); - return path.join(baseDir, ...parts); -} - -async function listFilesRecursive(rootDir: string): Promise { - const out: string[] = []; - const stack: Array<{ abs: string; rel: string }> = [{ abs: rootDir, rel: "" }]; - - while (stack.length > 0) { - const current = stack.pop()!; - const entries = await fs.readdir(current.abs, { withFileTypes: true }); - for (const entry of entries) { - const abs = path.join(current.abs, entry.name); - const rel = current.rel ? `${current.rel}/${entry.name}` : entry.name; - if (entry.isDirectory()) { - stack.push({ abs, rel }); - } else if (entry.isFile()) { - out.push(normalizeZipPath(rel)); - } - } - } - - return out; -} - -function escapePwshSingleQuoted(value: string): string { - return value.replace(/'/g, "''"); -} - -async function extractZipToTemp(zipPath: string): Promise { - const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "wolai-import-")); - const literalZip = escapePwshSingleQuoted(zipPath); - const literalOut = escapePwshSingleQuoted(tmp); - const command = `Expand-Archive -LiteralPath '${literalZip}' -DestinationPath '${literalOut}' -Force`; - - await new Promise((resolve, reject) => { - const child = spawn("powershell", ["-NoProfile", "-Command", command], { - stdio: ["ignore", "pipe", "pipe"], - }); - let stderr = ""; - child.stderr.on("data", (chunk) => { - stderr += chunk.toString(); - }); - child.on("error", (err) => reject(err)); - child.on("close", (code) => { - if (code === 0) resolve(); - else reject(new Error(`解压失败(code=${code}):${stderr || "unknown"}`)); - }); - }); - - return tmp; -} - -async function createDirStore(baseDir: string): Promise { - const abs = path.resolve(baseDir); - return { - kind: "dir", - baseDir: abs, - listFiles: async () => await listFilesRecursive(abs), - readText: async (relPath) => await fs.readFile(toFsPath(abs, relPath), "utf8"), - readBytes: async (relPath) => new Uint8Array(await fs.readFile(toFsPath(abs, relPath))), - exists: async (relPath) => { - try { - await fs.access(toFsPath(abs, relPath)); - return true; - } catch { - return false; - } - }, - }; -} - -const MAX_ASSET_BYTES = 100 * 1024 * 1024; - -function detectRootMdPath(mdPaths: string[]): { rootMdPath: string | null; candidates: string[] } { - const normalized = mdPaths.map(normalizeZipPath); - const topDirs = Array.from(new Set(normalized.map((p) => p.split("/")[0]).filter(Boolean))); - if (topDirs.length === 1) { - const top = topDirs[0]!; - const candidates = normalized - .filter((p) => p.startsWith(`${top}/`)) - .filter((p) => p.split("/").length === 2) - .sort((a, b) => a.localeCompare(b, "zh-Hans-CN")); - if (candidates.length === 1) return { rootMdPath: candidates[0]!, candidates }; - if (candidates.length > 1) return { rootMdPath: null, candidates }; - } - - const sorted = normalized - .slice() - .sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b, "zh-Hans-CN")); - if (sorted.length === 0) return { rootMdPath: null, candidates: [] }; - return { rootMdPath: sorted[0]!, candidates: sorted.slice(0, 8) }; -} - -function buildImportNodes(opts: { - scopePrefix: string; - rootMdPath: string; - mdPaths: string[]; -}): { nodes: ImportNode[]; mdRelToKey: Map } { - const scope = opts.scopePrefix.endsWith("/") ? opts.scopePrefix : `${opts.scopePrefix}/`; - const withinScope = opts.mdPaths.map(normalizeZipPath).filter((p) => p.startsWith(scope)); - const root = normalizeZipPath(opts.rootMdPath); - const mdSet = new Set(withinScope); - - const allDirs: Set = new Set(); - for (const md of withinScope) { - const dir = path.posix.dirname(md); - const segments = dir.split("/").filter(Boolean); - let accum = ""; - for (const seg of segments) { - accum = accum ? `${accum}/${seg}` : seg; - allDirs.add(`${accum}/`); - } - } - - const representing: Map = new Map(); - for (const dir of allDirs) { - const dirName = dir.split("/").filter(Boolean).pop() ?? ""; - if (!dirName) continue; - const prefer = `${dir}${dirName}.md`; - const index = `${dir}index.md`; - if (mdSet.has(prefer)) representing.set(dir, prefer); - else if (mdSet.has(index)) representing.set(dir, index); - } - representing.set(scope, root); - - // 说明:仅为“包含 markdown 的目录”创建合成文件夹页(避免 image/ 这类纯资源目录)。 - const folderNodes: Array> = []; - for (const dir of allDirs) { - if (dir === scope) continue; - if (representing.has(dir)) continue; - folderNodes.push({ - kind: "folder", - key: `__dir__${dir}`, - title: (dir.split("/").filter(Boolean).pop() ?? "目录").trim() || "目录", - dirPath: dir, - parentKey: null, - }); - } - - const docNodes: Array> = withinScope.map((md) => ({ - kind: "doc", - key: md, - title: path.posix.basename(md, ".md") || "无标题", - mdPath: md, - parentKey: null, - })); - - const nodesByKey = new Map(); - for (const n of [...folderNodes, ...docNodes]) nodesByKey.set(n.key, n); - - const getContainerKeyForDir = (dir: string): string => { - const normalizedDir = dir.endsWith("/") ? dir : `${dir}/`; - const rep = representing.get(normalizedDir); - if (rep) return rep; - return `__dir__${normalizedDir}`; - }; - - for (const node of folderNodes) { - const parentDir = path.posix.dirname(node.dirPath.endsWith("/") ? node.dirPath.slice(0, -1) : node.dirPath); - const parentDirNorm = parentDir === "." ? "" : `${parentDir}/`; - node.parentKey = parentDirNorm ? getContainerKeyForDir(parentDirNorm) : root; - } - - for (const node of docNodes) { - if (node.mdPath === root) { - node.parentKey = null; - continue; - } - const dir = `${path.posix.dirname(node.mdPath)}/`; - const rep = representing.get(dir); - if (rep && rep !== node.mdPath) { - node.parentKey = rep; - continue; - } - if (rep && rep === node.mdPath) { - const parentDir = path.posix.dirname(dir.endsWith("/") ? dir.slice(0, -1) : dir); - const parentDirNorm = parentDir === "." ? "" : `${parentDir}/`; - node.parentKey = parentDirNorm ? getContainerKeyForDir(parentDirNorm) : root; - continue; - } - node.parentKey = getContainerKeyForDir(dir); - } - - for (const node of [...folderNodes, ...docNodes]) { - if (node.key === root) continue; - if (!node.parentKey) continue; - if (!nodesByKey.has(node.parentKey)) node.parentKey = root; - } - - // 说明:构建 md 相对路径 -> key 的映射,用于内部链接替换 - const mdRelToKey = new Map(); - for (const md of withinScope) { - const rel = md.startsWith(scope) ? md.slice(scope.length) : md; - mdRelToKey.set(rel, md); - } - - // 说明:输出需要按“父先子后”的顺序创建 - const all = [...folderNodes, ...docNodes]; - const depth = (key: string): number => { - let d = 0; - let cur = nodesByKey.get(key) ?? null; - const seen = new Set(); - while (cur && cur.parentKey) { - if (seen.has(cur.parentKey)) break; - seen.add(cur.parentKey); - d += 1; - cur = nodesByKey.get(cur.parentKey) ?? null; - } - return d; - }; - - all.sort((a, b) => depth(a.key) - depth(b.key) || a.title.localeCompare(b.title, "zh-Hans-CN")); - return { nodes: all, mdRelToKey }; -} - -function extractStandaloneMarkdownLink(line: string): { label: string; href: string } | null { - const trimmed = line.trim(); - if (!trimmed.startsWith("[") || !trimmed.includes("](") || !trimmed.endsWith(")")) return null; - const m = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(trimmed); - if (!m) return null; - const label = (m[1] ?? "").trim(); - const raw = (m[2] ?? "").trim(); - const href = stripAngleBrackets(stripMarkdownLinkTitle(raw)); - return { label, href }; -} - -function replaceImagesAndLinks(opts: { - markdown: string; - mdDirRel: string; - mdRelToDocId: Map; - uploadedUrlByRelAssetPath: Map< - string, - { - url: string; - assetId: string; - fileName: string; - mimeType: string; - fileSize: number; - assetType: "image" | "video" | "audio" | "file"; - } - >; -}): { markdown: string; tokens: TokenRecord[]; warnings: string[] } { - const warnings: string[] = []; - const tokens: TokenRecord[] = []; - - const lines = opts.markdown.split(/\r?\n/); - const out: string[] = []; - - const imageRe = /!\[([^\]]*)\]\(([^)]+)\)/g; - const linkRe = /\[([^\]]+)\]\(([^)]+)\)/g; - - for (const rawLine of lines) { - let line = rawLine; - - // 1) 先处理“整行的页面链接/附件链接”,便于转成块 - const standalone = extractStandaloneMarkdownLink(line); - if (standalone) { - const href0 = standalone.href; - const href = isRemoteUrl(href0) ? href0 : stripLocalQueryAndHash(href0); - if (!isRemoteUrl(href)) { - const target = resolveRelativePosix(opts.mdDirRel, href); - if (target.toLowerCase().endsWith(".md")) { - const docId = opts.mdRelToDocId.get(target); - if (docId) { - const token = `[[[MNOTE_PAGE_REF:${randomUUID()}]]]`; - tokens.push({ kind: "page", token, targetMdRel: target, label: standalone.label }); - out.push(token); - continue; - } - } - - const asset = opts.uploadedUrlByRelAssetPath.get(target); - if (asset) { - const token = `[[[MNOTE_FILE:${randomUUID()}]]]`; - tokens.push({ kind: "file", token, url: asset.url, label: standalone.label }); - out.push(token); - continue; - } - } - } - - // 2) 图片语法替换为 token(避免 BlockNote 的图片解析在 Node 环境报错) - line = line.replace(imageRe, (_full, altRaw, urlRaw) => { - const alt = String(altRaw ?? "").trim(); - const raw0 = stripAngleBrackets(stripMarkdownLinkTitle(String(urlRaw ?? "").trim())); - const url = isRemoteUrl(raw0) ? raw0 : stripLocalQueryAndHash(raw0); - const token = `[[[MNOTE_IMAGE:${randomUUID()}]]]`; - tokens.push({ kind: "image", token, url, alt }); - // 说明:用空行包起来,尽量让 markdownToBlocks 生成“单独段落”,便于后续替换为 media block。 - return `\n\n${token}\n\n`; - }); - - // 3) 重写内联链接(页面链接 -> /documents/,附件链接 -> Convex url) - line = line.replace(linkRe, (full, labelRaw, hrefRaw) => { - const label = String(labelRaw ?? ""); - const href0 = stripAngleBrackets(stripMarkdownLinkTitle(String(hrefRaw ?? "").trim())); - const href = isRemoteUrl(href0) ? href0 : stripLocalQueryAndHash(href0); - if (isRemoteUrl(href)) return full; - - const target = resolveRelativePosix(opts.mdDirRel, href); - if (target.toLowerCase().endsWith(".md")) { - const docId = opts.mdRelToDocId.get(target); - if (docId) return `[${label}](/documents/${encodeURIComponent(docId)})`; - return full; - } - - const asset = opts.uploadedUrlByRelAssetPath.get(target); - if (asset) return `[${label}](${asset.url})`; - return full; - }); - - out.push(line); - } - - return { markdown: out.join("\n"), tokens, warnings }; -} - -function isSingleTokenParagraph(block: any, token: string): boolean { - if (!block || typeof block !== "object") return false; - if (!Array.isArray(block.content)) return false; - if (block.content.length !== 1) return false; - const node = block.content[0]; - if (!node || typeof node !== "object") return false; - if (node.type !== "text") return false; - return String(node.text ?? "").trim() === token; -} - -function replaceTokensInBlocks(opts: { - blocks: any[]; - tokens: TokenRecord[]; - pageRefResolver: (rel: string) => { pageId: string; title: string } | null; - mediaResolver: (url: string) => { props: Record } | null; -}): { blocks: any[]; unresolved: TokenRecord[] } { - const tokenMap = new Map(opts.tokens.map((t) => [t.token, t])); - const unresolved = new Set(opts.tokens.map((t) => t.token)); - - const walk = (items: any[]): any[] => { - const out: any[] = []; - for (const block of items) { - const maybe = - block && Array.isArray(block.content) && block.content.length === 1 && block.content[0]?.type === "text" - ? String(block.content[0].text ?? "").trim() - : null; - - if (maybe && tokenMap.has(maybe) && isSingleTokenParagraph(block, maybe)) { - const token = tokenMap.get(maybe)!; - unresolved.delete(maybe); - - if (token.kind === "page") { - const resolved = opts.pageRefResolver(token.targetMdRel); - if (!resolved) { - out.push(block); - continue; - } - out.push({ - id: randomUUID(), - type: "pageReference", - props: { pageId: resolved.pageId, title: token.label || resolved.title || "无标题" }, - content: [], - children: [], - }); - continue; - } - - if (token.kind === "image" || token.kind === "file") { - const resolved = opts.mediaResolver(token.url); - if (!resolved) { - out.push(block); - continue; - } - out.push({ id: randomUUID(), type: "media", props: resolved.props, content: [], children: [] }); - continue; - } - } - - const next = { ...block }; - if (Array.isArray(next.children) && next.children.length > 0) { - next.children = walk(next.children); - } - out.push(next); - } - return out; - }; - - return { blocks: walk(opts.blocks), unresolved: opts.tokens.filter((t) => unresolved.has(t.token)) }; -} - -async function uploadToConvexMedia(opts: { - client: any; - userId: string; - workspaceId: string; - documentId: string; - fileName: string; - bytes: Uint8Array; - mimeType: string; -}): Promise { - const assetId = randomUUID(); - const assetType = resolveAssetTypeByMime(opts.mimeType); - - const uploadUrl = await opts.client.mutation(api.mediaAssets.generateUploadUrl, { userId: opts.userId }); - if (!uploadUrl || typeof uploadUrl !== "string") throw new Error("获取上传地址失败"); - - const uploadRes = await fetch(uploadUrl, { - method: "POST", - headers: { "Content-Type": opts.mimeType || "application/octet-stream" }, - body: Buffer.from(opts.bytes), - }); - if (!uploadRes.ok) { - const text = await uploadRes.text().catch(() => ""); - throw new Error(`上传到 Convex 失败:${uploadRes.status} ${text}`); - } - const uploadJson = (await uploadRes.json().catch(() => null)) as { storageId?: string } | null; - const storageId = uploadJson?.storageId ?? ""; - if (!storageId) throw new Error("上传到 Convex 失败:缺少 storageId"); - - const created = await opts.client.mutation(api.mediaAssets.createWithStorage, { - userId: opts.userId, - storageId: storageId as any, - asset: { - id: assetId, - workspace_id: opts.workspaceId, - document_id: opts.documentId, - asset_type: assetType, - file_name: opts.fileName || null, - file_size: opts.bytes.length, - mime_type: opts.mimeType || null, - }, - }); - - return created as unknown as MediaAsset; -} - -export async function POST(request: Request) { - if (!isConvexEnabled()) { - return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 }); - } - - let auth; - try { - auth = await requireAuthContext(); - } catch (err) { - if (err instanceof HttpError) { - return NextResponse.json({ error: "未登录" }, { status: err.status }); - } - return NextResponse.json({ error: "未登录" }, { status: 401 }); - } - - try { - ensureDomShim(); - - const contentType = request.headers.get("content-type") ?? ""; - const isJson = contentType.includes("application/json"); - - let parentId: string | null = null; - let rootMdPathInput: string | null = null; - let zipPath: string | null = null; - let extractedDir: string | null = null; - let store: InputStore | null = null; - - if (isJson) { - const payload = (await request.json().catch(() => null)) as any; - parentId = String(payload?.parentId ?? "").trim() || null; - rootMdPathInput = String(payload?.rootMdPath ?? "").trim() || null; - zipPath = String(payload?.zipPath ?? "").trim() || null; - extractedDir = String(payload?.extractedDir ?? "").trim() || null; - } else { - const formData = await request.formData(); - parentId = String(formData.get("parentId") ?? "").trim() || null; - rootMdPathInput = String(formData.get("rootMdPath") ?? "").trim() || null; - const localZipPath = String(formData.get("zipPath") ?? "").trim(); - zipPath = localZipPath || null; - - const file = formData.get("file"); - if (file instanceof File) { - // 说明:浏览器上传大 ZIP(比如 1GB)会导致内存/超时问题,这里直接给出提示。 - const size = Number((file as any).size ?? 0); - if (size > 200 * 1024 * 1024) { - return NextResponse.json( - { error: "ZIP 太大,请使用“本地 ZIP 路径”方式导入(不要上传文件)" }, - { status: 413 }, - ); - } - return NextResponse.json( - { error: "当前仅支持“本地 ZIP 路径”导入(大文件避免上传)" }, - { status: 400 }, - ); - } - } - - if (extractedDir) { - store = await createDirStore(extractedDir); - } else if (zipPath) { - const stat = await fs.stat(zipPath); - if (!stat.isFile()) { - return NextResponse.json({ error: "zipPath 不是文件" }, { status: 400 }); - } - const extractDir = await extractZipToTemp(zipPath); - // 说明:使用解压输出目录作为 store 根目录,使相对路径保持 dQeAax/... 这种形态。 - store = await createDirStore(extractDir); - extractedDir = extractDir; - } else { - return NextResponse.json({ error: "缺少 zipPath(本地 ZIP 路径)" }, { status: 400 }); - } - - const allFilePaths = await store.listFiles(); - const mdPaths = allFilePaths.filter((p) => p.toLowerCase().endsWith(".md")); - if (mdPaths.length === 0) return NextResponse.json({ error: "未发现 .md 文件" }, { status: 400 }); - - let rootMdPath = rootMdPathInput ? normalizeZipPath(rootMdPathInput) : null; - if (rootMdPath && !(await store.exists(rootMdPath))) { - return NextResponse.json({ error: "rootMdPath 不存在", rootMdPath }, { status: 400 }); - } - if (!rootMdPath) { - const detected = detectRootMdPath(mdPaths); - if (!detected.rootMdPath) { - return NextResponse.json( - { error: "无法自动判断入口页面,请指定 rootMdPath", candidates: detected.candidates }, - { status: 409 }, - ); - } - rootMdPath = detected.rootMdPath; - } - - const scopePrefix = `${path.posix.dirname(rootMdPath)}/`; - const { nodes, mdRelToKey } = buildImportNodes({ scopePrefix, rootMdPath, mdPaths }); - - let client; - try { - client = await getConvexAuthedHttpClient(); - } catch (err) { - const msg = err instanceof Error ? err.message : "未登录"; - const status = msg.includes("未登录") ? 401 : 500; - return NextResponse.json({ error: msg }, { status }); - } - - // workspace / accessScope:有 parentId 则继承,否则走默认 workspace - let workspaceId = ""; - let accessScope: "private" | "shared" | "public" = "private"; - if (parentId) { - const parentMeta = await client.query(api.documents.getMeta, { id: parentId }); - if (!parentMeta) return NextResponse.json({ error: "父页面不存在或无权限" }, { status: 404 }); - workspaceId = String((parentMeta as any).workspace_id ?? ""); - accessScope = ((parentMeta as any).access_scope ?? "private") as typeof accessScope; - if (!workspaceId) return NextResponse.json({ error: "无法读取父页面 workspaceId" }, { status: 400 }); - } else { - const workspaceBootstrap = await client.mutation(api.workspaces.ensureDefaultWorkspace, { - fallbackName: auth.email ?? auth.name ?? "我的空间", - workspaceIdIfCreate: randomUUID(), - }); - workspaceId = String((workspaceBootstrap as any)?.activeWorkspaceId ?? ""); - if (!workspaceId) return NextResponse.json({ error: "缺少目标工作空间" }, { status: 400 }); - } - - // 读取每个 md 的标题(优先 # 顶部标题),并写回 node.title - for (const node of nodes) { - if (node.kind !== "doc") continue; - const text = await store.readText(node.mdPath).catch(() => ""); - if (!text) continue; - node.title = parseTopHeadingTitle(text) ?? node.title; - } - - // 先创建全部节点(documents / 合成目录页) - const keyToDocId = new Map(); - for (const node of nodes) { - const docId = randomUUID(); - const parentDocId = - node.key === rootMdPath - ? parentId - : node.parentKey - ? keyToDocId.get(node.parentKey) ?? parentId - : parentId; - - const created = await client.mutation(api.documents.create, { - id: docId, - workspaceId, - parentId: parentDocId ?? null, - title: node.title, - accessScope, - content: [], - }); - - if (!created || (created as any).id !== docId) { - return NextResponse.json({ error: "创建页面失败(Convex 返回异常)" }, { status: 500 }); - } - keyToDocId.set(node.key, docId); - await ensureDocumentScaffold(docId, node.title); - } - - // 逐页导入内容(markdown -> blocks;图片/附件 -> media block + media_assets) - const schema = BlockNoteSchema.create(); - const editor = BlockNoteEditor.create({ schema }); - const warnings: string[] = []; - let importedCount = 0; - - for (const node of nodes) { - if (node.kind !== "doc") continue; - const docId = keyToDocId.get(node.key) ?? ""; - if (!docId) continue; - let markdown = await store.readText(node.mdPath); - markdown = stripTopHeadingIfMatches(markdown, node.title); - - const mdDirFull = `${path.posix.dirname(node.mdPath)}/`; - const mdDirRel = mdDirFull.startsWith(scopePrefix) ? mdDirFull.slice(scopePrefix.length) : mdDirFull; - - // 预扫:把本页引用到的本地资源上传到 Convex,并建立 relPath -> url 映射 - const uploadedUrlByRelAssetPath = new Map< - string, - { - url: string; - assetId: string; - fileName: string; - mimeType: string; - fileSize: number; - assetType: "image" | "video" | "audio" | "file"; - } - >(); - - const localAssetPaths = new Set(); - { - const imgRe = /!\[[^\]]*\]\(([^)]+)\)/g; - let m: RegExpExecArray | null; - while ((m = imgRe.exec(markdown))) { - const raw0 = stripAngleBrackets(stripMarkdownLinkTitle(String(m[1] ?? "").trim())); - const raw = isRemoteUrl(raw0) ? raw0 : stripLocalQueryAndHash(raw0); - if (!raw || isRemoteUrl(raw)) continue; - const resolved = resolveRelativePosix(mdDirRel, raw); - if (resolved) localAssetPaths.add(resolved); - } - } - { - const linkRe = /\[[^\]]+\]\(([^)]+)\)/g; - let m: RegExpExecArray | null; - while ((m = linkRe.exec(markdown))) { - const raw0 = stripAngleBrackets(stripMarkdownLinkTitle(String(m[1] ?? "").trim())); - const raw = isRemoteUrl(raw0) ? raw0 : stripLocalQueryAndHash(raw0); - if (!raw || isRemoteUrl(raw)) continue; - const resolved = resolveRelativePosix(mdDirRel, raw); - if (!resolved) continue; - if (resolved.toLowerCase().endsWith(".md")) continue; - localAssetPaths.add(resolved); - } - } - - for (const relAssetPath of Array.from(localAssetPaths)) { - const zipPath = `${scopePrefix}${relAssetPath}`.replace(/\/{2,}/g, "/"); - if (!(await store.exists(zipPath))) { - warnings.push(`[${node.title}] 未找到资源文件:${relAssetPath}`); - continue; - } - const absAssetPath = toFsPath(store.baseDir, zipPath); - const stat = await fs.stat(absAssetPath).catch(() => null); - if (stat && typeof stat.size === "number" && stat.size > MAX_ASSET_BYTES) { - const mb = (stat.size / 1024 / 1024).toFixed(1); - warnings.push(`[${node.title}] 资源过大已跳过(${mb}MB):${relAssetPath}`); - continue; - } - - const bytes = await store.readBytes(zipPath); - const fileName = safeBasename(relAssetPath); - const mimeType = guessMimeType(fileName); - const assetType = resolveAssetTypeByMime(mimeType); - const created = await uploadToConvexMedia({ - client, - userId: auth.userId, - workspaceId, - documentId: docId, - fileName, - bytes, - mimeType, - }); - uploadedUrlByRelAssetPath.set(relAssetPath, { - url: String((created as any).file_url ?? ""), - assetId: String((created as any).id ?? ""), - fileName: String((created as any).file_name ?? fileName), - mimeType: String((created as any).mime_type ?? mimeType), - fileSize: Number((created as any).file_size ?? bytes.length), - assetType, - }); - } - - const mdRelToDocId = new Map(); - for (const [rel, key] of mdRelToKey.entries()) { - const id = keyToDocId.get(key); - if (id) mdRelToDocId.set(rel, id); - } - - const replaced = replaceImagesAndLinks({ - markdown, - mdDirRel, - mdRelToDocId, - uploadedUrlByRelAssetPath, - }); - markdown = replaced.markdown; - warnings.push(...replaced.warnings); - - let blocks: Block[] = []; - try { - blocks = markdownToBlocks(markdown, editor.pmSchema) as any; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - warnings.push(`[${node.title}] Markdown 解析失败,已降级为纯文本:${msg}`); - blocks = [ - { - id: randomUUID(), - type: "paragraph", - props: { backgroundColor: "default", textColor: "default", textAlignment: "left" }, - content: [{ type: "text", text: markdown, styles: {} }], - children: [], - } as any, - ]; - } - - const replacedBlocks = replaceTokensInBlocks({ - blocks: blocks as any[], - tokens: replaced.tokens, - pageRefResolver: (rel) => { - const id = mdRelToDocId.get(rel) ?? ""; - if (!id) return null; - return { pageId: id, title: path.posix.basename(rel, ".md") || "无标题" }; - }, - mediaResolver: (url) => { - if (isRemoteUrl(url)) { - const fileName = safeBasename(url); - const mimeType = guessMimeType(fileName); - const assetType = resolveAssetTypeByMime(mimeType); - return { - props: { - fileUrl: url, - thumbnailUrl: url, - caption: "", - captionAlign: "left", - hasBorder: true, - linkUrl: "", - assetId: "", - assetType, - fileName, - fileSize: 0, - mimeType, - width: 0, - ocrStatus: "idle", - documentId: docId, - }, - }; - } - - const relAssetPath = resolveRelativePosix(mdDirRel, url); - const asset = uploadedUrlByRelAssetPath.get(relAssetPath); - if (!asset || !asset.url) return null; - return { - props: { - fileUrl: asset.url, - thumbnailUrl: asset.url, - caption: "", - captionAlign: "left", - hasBorder: true, - linkUrl: "", - assetId: asset.assetId || "", - assetType: asset.assetType, - fileName: asset.fileName || safeBasename(relAssetPath), - fileSize: asset.fileSize || 0, - mimeType: asset.mimeType || guessMimeType(asset.fileName || safeBasename(relAssetPath)), - width: 0, - ocrStatus: "idle", - documentId: docId, - }, - }; - }, - }); - - for (const token of replacedBlocks.unresolved) { - warnings.push(`[${node.title}] 未能解析 token:${token.token}`); - } - - // 说明:Convex 的 Json 不允许出现 undefined(尤其是表格类 block 里可能带有 columnWidths: [undefined])。 - // 用 JSON 序列化做一次深度净化:对象属性的 undefined 会被移除,数组里的 undefined 会变成 null。 - const sanitizedBlocks = JSON.parse(JSON.stringify(replacedBlocks.blocks)) as Json; - await client.mutation(api.documents.updateContent, { id: docId, content: sanitizedBlocks }); - importedCount += 1; - } - - const rootId = keyToDocId.get(rootMdPath) ?? ""; - return NextResponse.json({ - ok: true, - rootDocumentId: rootId, - importedCount, - warnings: warnings.slice(0, 200), - documentsBaseDir, - extractedDir, - }); - } catch (error) { - console.error("Wolai 导入失败", error); - const message = error instanceof Error ? error.message : "导入失败"; - const stack = error instanceof Error ? error.stack : undefined; - return NextResponse.json({ error: message, stack }, { status: 500 }); - } -} diff --git a/wolai-frontend/src/app/api/workspaces/switch/route.ts b/wolai-frontend/src/app/api/workspaces/switch/route.ts deleted file mode 100644 index f5189896..00000000 --- a/wolai-frontend/src/app/api/workspaces/switch/route.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { NextResponse } from "next/server"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { getAuthedConvexClient } from "@/lib/convex/route"; -import { api } from "@/lib/convex/api"; - -export const dynamic = "force-dynamic"; - -export async function POST(request: Request) { - if (isConvexEnabled()) { - const { client } = await getAuthedConvexClient(); - const payload = await request.json().catch(() => ({})); - const workspaceId = payload.workspaceId as string | undefined; - if (!workspaceId) { - return NextResponse.json({ error: "缺少 workspaceId" }, { status: 400 }); - } - - await client.mutation(api.workspaces.switchDefaultWorkspace, { - workspaceId, - }); - - return NextResponse.json({ success: true }); - } - return NextResponse.json({ error: "仅支持 Convex 模式" }, { status: 501 }); -} diff --git a/wolai-frontend/src/app/cache/[...path]/route.ts b/wolai-frontend/src/app/cache/[...path]/route.ts deleted file mode 100644 index 81e35956..00000000 --- a/wolai-frontend/src/app/cache/[...path]/route.ts +++ /dev/null @@ -1,161 +0,0 @@ -import type { NextRequest } from "next/server"; -import { NextResponse } from "next/server"; -import { gzipSync } from "node:zlib"; -import { resolveOnlyOfficeInternalUrl } from "@/lib/onlyoffice/internal-url"; - -export const dynamic = "force-dynamic"; -export const runtime = "nodejs"; - -// 说明:ONLYOFFICE 文档服务器在编辑过程中会从 `/cache/*` 拉取二进制缓存(例如 Editor.bin)。 -// 当我们通过 `/onlyoffice-server/*` 反代文档服务器时,这些 `/cache/*` 请求会落到 Next 上, -// 若未额外反代,会导致 404,进而触发 ONLYOFFICE “下载失败(-4)/无法打开文档”。 -// 因此这里把 `/cache/*` 同样反代到本机 ONLYOFFICE。 -const stripHopByHopHeaders = (headers: Headers) => { - // 说明:Hop-by-hop headers 不应被代理转发/透传 - const hopByHop = [ - "connection", - "keep-alive", - "proxy-authenticate", - "proxy-authorization", - "te", - "trailer", - "transfer-encoding", - "upgrade", - ]; - for (const key of hopByHop) { - headers.delete(key); - } -}; - -const pickCacheControl = (pathParts: string[], contentType: string, method: string) => { - const m = String(method || "").toUpperCase(); - if (m !== "GET" && m !== "HEAD") return "no-store"; - if (contentType.includes("text/html")) return "no-store"; - - const p = `/${(pathParts ?? []).join("/")}`.toLowerCase(); - // 说明:/cache 主要是 ONLYOFFICE 运行期二进制缓存,适合短缓存提升性能,但不宜过长。 - if (p.includes("editor.bin") || p.endsWith(".bin")) { - return "public, max-age=604800, stale-while-revalidate=86400, immutable"; - } - return "public, max-age=86400, stale-while-revalidate=3600"; -}; - -const buildStableEtag = (pathParts: string[], search: string) => { - const p = `/${(pathParts ?? []).join("/")}`; - const s = String(search || ""); - return `W/"mnote-oo-cache:${p}${s}"`; -}; - -const isStrongCache = (cc: string) => { - const v = String(cc || ""); - return v.includes("max-age=604800"); -}; - -const shouldGzip = (request: NextRequest, contentType: string) => { - const ae = String(request.headers.get("accept-encoding") || "").toLowerCase(); - if (!ae.includes("gzip")) return false; - - const ct = String(contentType || "").toLowerCase(); - const compressible = - ct.startsWith("text/") || - ct.includes("javascript") || - ct.includes("json") || - ct.includes("xml") || - ct.includes("svg"); - - return compressible; -}; - -const proxyCache = async (request: NextRequest, pathParts: string[]) => { - const onlyofficeInternalUrl = await resolveOnlyOfficeInternalUrl(); - const incomingUrl = new URL(request.url); - const target = new URL( - `${onlyofficeInternalUrl}/cache/${(pathParts ?? []).map(encodeURIComponent).join("/")}`, - ); - target.search = incomingUrl.search; - - const headers = new Headers(request.headers); - headers.delete("host"); - // 说明:让上游返回明文(identity),我们在此处按需 gzip 压缩,保证 content-encoding 与 body 一致。 - headers.set("accept-encoding", "identity"); - stripHopByHopHeaders(headers); - - const method = request.method.toUpperCase(); - const hasBody = !["GET", "HEAD"].includes(method); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const init: any = { - method, - headers, - body: hasBody ? request.body : undefined, - duplex: "half", - redirect: "follow", - }; - - const upstream = await fetch(target, init); - - const upstreamContentLength = upstream.headers.get("content-length"); - const outHeaders = new Headers(upstream.headers); - stripHopByHopHeaders(outHeaders); - outHeaders.delete("content-encoding"); - outHeaders.delete("content-length"); - const contentType = upstream.headers.get("content-type") || ""; - const cacheControl = pickCacheControl(pathParts, contentType, request.method); - outHeaders.set("cache-control", cacheControl); - outHeaders.delete("pragma"); - outHeaders.delete("expires"); - outHeaders.delete("set-cookie"); - - if (isStrongCache(cacheControl)) { - const etag = buildStableEtag(pathParts, incomingUrl.search); - outHeaders.set("etag", etag); - const inm = String(request.headers.get("if-none-match") || "").trim(); - if (inm && (inm === etag || inm.split(",").map((s) => s.trim()).includes(etag))) { - return new NextResponse(null, { status: 304, headers: outHeaders }); - } - } - - // 说明:同 onlyoffice-server 路由,对强缓存二进制改为定长响应,提升浏览器落盘缓存命中率。 - if (isStrongCache(cacheControl)) { - outHeaders.delete("vary"); - outHeaders.set("vary", "Accept-Encoding"); - if (method === "HEAD") { - if (upstreamContentLength) outHeaders.set("content-length", upstreamContentLength); - return new NextResponse(null, { status: upstream.status, headers: outHeaders }); - } - const buf = await upstream.arrayBuffer(); - - if (shouldGzip(request, contentType)) { - try { - const gz = gzipSync(Buffer.from(buf), { level: 6 }); - outHeaders.set("content-encoding", "gzip"); - outHeaders.set("content-length", String(gz.byteLength)); - return new NextResponse(gz, { status: upstream.status, headers: outHeaders }); - } catch { - // ignore - } - } - - outHeaders.set("content-length", String(buf.byteLength)); - return new NextResponse(buf, { status: upstream.status, headers: outHeaders }); - } - - return new NextResponse(upstream.body, { status: upstream.status, headers: outHeaders }); -}; - -type RouteCtx = { params: Promise<{ path: string[] }> }; - -export async function GET(request: NextRequest, ctx: RouteCtx) { - const { path } = await ctx.params; - return proxyCache(request, path ?? []); -} - -export async function HEAD(request: NextRequest, ctx: RouteCtx) { - const { path } = await ctx.params; - return proxyCache(request, path ?? []); -} - -export async function OPTIONS(request: NextRequest, ctx: RouteCtx) { - const { path } = await ctx.params; - return proxyCache(request, path ?? []); -} diff --git a/wolai-frontend/src/app/favicon.ico b/wolai-frontend/src/app/favicon.ico deleted file mode 100644 index 718d6fea..00000000 Binary files a/wolai-frontend/src/app/favicon.ico and /dev/null differ diff --git a/wolai-frontend/src/app/globals.css b/wolai-frontend/src/app/globals.css deleted file mode 100644 index a4bf7698..00000000 --- a/wolai-frontend/src/app/globals.css +++ /dev/null @@ -1,819 +0,0 @@ -@import url("https://rsms.me/inter/inter.css"); -@import "tailwindcss"; -@import "tw-animate-css"; -@import "./iconfont.css"; - -@custom-variant dark (&:is(.dark *)); - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); - --color-sidebar-ring: var(--sidebar-ring); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar: var(--sidebar); - --color-chart-5: var(--chart-5); - --color-chart-4: var(--chart-4); - --color-chart-3: var(--chart-3); - --color-chart-2: var(--chart-2); - --color-chart-1: var(--chart-1); - --color-ring: var(--ring); - --color-input: var(--input); - --color-border: var(--border); - --color-destructive: var(--destructive); - --color-accent-foreground: var(--accent-foreground); - --color-accent: var(--accent); - --color-muted-foreground: var(--muted-foreground); - --color-muted: var(--muted); - --color-secondary-foreground: var(--secondary-foreground); - --color-secondary: var(--secondary); - --color-primary-foreground: var(--primary-foreground); - --color-primary: var(--primary); - --color-popover-foreground: var(--popover-foreground); - --color-popover: var(--popover); - --color-card-foreground: var(--card-foreground); - --color-card: var(--card); - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - - /* Wolai 设计系统颜色变量 */ - --color-wolai-bg: #FFFFFF; - --color-wolai-bg-sidebar: #F7F7F5; - --color-wolai-bg-hover: #EFEFEF; - --color-wolai-bg-active: #E8E8E7; - --color-wolai-text-primary: #37352F; - --color-wolai-text-secondary: #9B9A97; - --color-wolai-border: #E9E9E8; - --color-wolai-brand: #22A559; - --font-wolai-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", "PingFang SC", Helvetica, Arial, sans-serif; -} - -:root { - --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); -} - -html, -body, -#__next { - height: 100%; - min-height: 100%; -} - -/* Wolai 字体栈 */ -body { - font-family: -apple-system, "Microsoft YaHei", sans-serif; - color: #37352F; - background-color: #FFFFFF; -} - -/* 滚动条美化 - 悬停时显示 */ -::-webkit-scrollbar { - width: 8px; - height: 8px; -} -::-webkit-scrollbar-track { - background: transparent; -} -::-webkit-scrollbar-thumb { - background: transparent; - border-radius: 4px; -} -:hover::-webkit-scrollbar-thumb { - background: rgba(0, 0, 0, 0.1); -} - -/* BlockNote 变量覆盖 */ -:root { - --bn-colors-editor-background: #FFFFFF; - --bn-colors-editor-text: #37352F; - --bn-font-family: -apple-system, "Microsoft YaHei", sans-serif; - --bn-border-radius: 4px; -} - -/* 调整编辑器内容宽度 */ -.bn-editor { - padding-left: 5rem; - padding-right: 5rem; - max-width: 900px; - margin: 0 auto; -} - -/* 说明:leptos-tiptap / ProseMirror 需要最基础的 white-space 与换行样式, - 否则浏览器输入与光标行为会不稳定,并触发 ProseMirror 警告。 */ -.ProseMirror { - white-space: pre-wrap; - word-break: break-word; - overflow-wrap: anywhere; -} - -.ProseMirror pre { - white-space: pre-wrap; -} - -.dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.205 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.205 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.556 0 0); -} - -@layer base { - * { - @apply border-border outline-ring/50; - } - html { - font-family: "Inter", system-ui, sans-serif; - } - body { - @apply bg-background text-foreground; - } -} - -@layer components { - .wolai-hover { - @apply transition-colors duration-150 hover:bg-gray-50; - } - .wolai-selected { - @apply border-l-2 border-blue-500 bg-blue-50; - } - .wolai-blue { - @apply cursor-pointer text-[#2563eb] hover:underline; - } - .wolai-editor .bn-block-outer { - padding: 2px 0; - } - .wolai-editor .bn-inline-content a { - color: #2563eb; - text-decoration: underline; - } - .wolai-editor .bn-drag-handle { - opacity: 0; - transition: opacity 0.2s ease; - } - .wolai-editor .bn-block-group:hover .bn-drag-handle { - opacity: 1; - } - .wolai-editor.wolai-heading-numbering { - counter-reset: wolai-h1 wolai-h2 wolai-h3 wolai-h4 wolai-h5; - } - - /* 说明:BlockNote 的 heading 主要由 .bn-block-content[data-content-type=heading] 承载。 - 为避免不同渲染结构导致“标题编号”失效,这里以块级容器为准实现编号。 */ - .wolai-editor.wolai-heading-numbering .bn-block-content[data-content-type="heading"]:not([data-level]), - .wolai-editor.wolai-heading-numbering .bn-block-content[data-content-type="heading"][data-level="1"] { - counter-increment: wolai-h1; - counter-reset: wolai-h2; - } - .wolai-editor.wolai-heading-numbering .bn-block-content[data-content-type="heading"]:not([data-level])::before, - .wolai-editor.wolai-heading-numbering .bn-block-content[data-content-type="heading"][data-level="1"]::before { - content: counter(wolai-h1) ". " !important; - color: #94a3b8; - margin-right: 8px !important; - display: inline-flex; - align-items: baseline; - flex: 0 0 auto; - } - .wolai-editor.wolai-heading-numbering .bn-block-content[data-content-type="heading"][data-level="2"] { - counter-increment: wolai-h2; - counter-reset: wolai-h3; - } - .wolai-editor.wolai-heading-numbering .bn-block-content[data-content-type="heading"][data-level="2"]::before { - content: counter(wolai-h1) "." counter(wolai-h2) ". " !important; - color: #94a3b8; - margin-right: 6px !important; - display: inline-flex; - align-items: baseline; - flex: 0 0 auto; - } - .wolai-editor.wolai-heading-numbering .bn-block-content[data-content-type="heading"][data-level="3"] { - counter-increment: wolai-h3; - counter-reset: wolai-h4; - } - .wolai-editor.wolai-heading-numbering .bn-block-content[data-content-type="heading"][data-level="3"]::before { - content: counter(wolai-h1) "." counter(wolai-h2) "." counter(wolai-h3) ". " !important; - color: #cbd5f5; - margin-right: 4px !important; - display: inline-flex; - align-items: baseline; - flex: 0 0 auto; - } - .wolai-editor.wolai-heading-numbering .bn-block-content[data-content-type="heading"][data-level="4"] { - counter-increment: wolai-h4; - counter-reset: wolai-h5; - } - .wolai-editor.wolai-heading-numbering .bn-block-content[data-content-type="heading"][data-level="4"]::before { - content: counter(wolai-h1) "." counter(wolai-h2) "." counter(wolai-h3) "." counter(wolai-h4) ". " !important; - color: #d0d7e7; - margin-right: 4px !important; - display: inline-flex; - align-items: baseline; - flex: 0 0 auto; - } - .wolai-editor.wolai-heading-numbering .bn-block-content[data-content-type="heading"][data-level="5"] { - counter-increment: wolai-h5; - } - .wolai-editor.wolai-heading-numbering .bn-block-content[data-content-type="heading"][data-level="5"]::before { - content: counter(wolai-h1) "." counter(wolai-h2) "." counter(wolai-h3) "." counter(wolai-h4) "." counter(wolai-h5) ". " !important; - color: #d4d4d8; - margin-right: 4px !important; - display: inline-flex; - align-items: baseline; - flex: 0 0 auto; - } - .wolai-editor-show-structure .bn-block-outer { - outline: 1px dashed #d4d4d8; - border-radius: 8px; - padding: 4px 8px; - } - .wolai-advanced-todo { - display: flex; - align-items: center; - gap: 10px; - } - .wolai-advanced-todo__status { - border-radius: 999px; - border: 1px solid #c7d2fe; - padding: 2px 10px; - font-size: 12px; - cursor: pointer; - background-color: #eef2ff; - color: #312e81; - } - .wolai-advanced-todo__status.status-doing { - border-color: #fcd34d; - background-color: #fff7ed; - color: #92400e; - } - .wolai-advanced-todo__status.status-done { - border-color: #4ade80; - background-color: #ecfdf5; - color: #047857; - } - .wolai-advanced-todo__status.status-cancelled { - border-color: #fecdd3; - background-color: #fff1f2; - color: #9f1239; - } - .wolai-progress { - border-radius: 12px; - padding: 12px; - border: 1px solid #e5e7eb; - background-color: #f8fafc; - margin: 8px 0; - } - .wolai-progress__header { - display: flex; - align-items: center; - justify-content: space-between; - font-size: 12px; - color: #475569; - margin-bottom: 8px; - } - .wolai-progress__mode { - font-size: 11px; - border-radius: 12px; - background-color: #e0edff; - color: #1d4ed8; - padding: 2px 8px; - } - .wolai-progress__bar { - position: relative; - width: 100%; - height: 10px; - border-radius: 999px; - background-color: #e4e4e7; - overflow: hidden; - cursor: pointer; - } - .wolai-progress__fill { - position: absolute; - top: 0; - left: 0; - bottom: 0; - border-radius: inherit; - background-image: linear-gradient(90deg, #60a5fa, #2563eb); - } - .wolai-progress__percent { - display: block; - margin-top: 6px; - font-size: 12px; - color: #1d4ed8; - font-weight: 600; - } - .wolai-progress__description { - font-size: 13px; - color: #6b7280; - margin-top: 6px; - } - .wolai-media { - position: relative; - margin: 24px 0; - border-radius: 18px; - /* 说明:Wolai 的附件/媒体块在选中时不显示蓝色外框(ProseMirror-selectednode 默认会加 outline)。 */ - outline: none !important; - } - /* 说明:文件附件需要表现为“普通行”,不应该出现外层大卡片/大外框 */ - .wolai-media--file { - margin: 0; - border-radius: 4px; - } - .wolai-media--file::after { - content: none; - } - .wolai-media--file .wolai-media__canvas { - background: transparent; - box-shadow: none; - border-radius: 0; - overflow: visible; - } - .wolai-media--file .wolai-media__figure { - background: transparent; - border-radius: 0; - overflow: visible; - } - .wolai-media::after { - content: ""; - position: absolute; - inset: -8px; - border: 1px solid transparent; - border-radius: 24px; - transition: border-color 0.2s ease; - pointer-events: none; - } - .wolai-media:hover::after { - border-color: rgba(37, 99, 235, 0.15); - } - .wolai-media--empty { - border: 1px dashed #bed5ff; - background-color: #fff; - padding: 20px; - border-radius: 16px; - } - .wolai-media__canvas { - position: relative; - border-radius: 18px; - overflow: hidden; - background-color: #fff; - box-shadow: 0 10px 40px rgba(15, 23, 42, 0.05); - margin: 0 auto; - } - .wolai-media__figure { - border-radius: inherit; - overflow: hidden; - background-color: #fff; - } - .wolai-media__figure img { - width: 100%; - border-radius: inherit; - object-fit: cover; - } - .wolai-media__figure--border img { - border: 1px solid #e2e8f0; - } - .wolai-media__figure figcaption { - padding: 8px 12px; - } - .wolai-media__figure--center figcaption { - text-align: center; - } - .wolai-media__figure--right figcaption { - text-align: right; - } - .wolai-media__quickbar { - position: absolute; - top: 12px; - right: 12px; - display: flex; - gap: 6px; - padding: 4px; - border-radius: 9999px; - background-color: rgba(15, 23, 42, 0.85); - box-shadow: 0 12px 32px rgba(15, 23, 42, 0.35); - opacity: 0; - transform: translateY(-4px); - transition: opacity 0.15s ease, transform 0.15s ease; - pointer-events: none; - } - .wolai-media:hover .wolai-media__quickbar, - .wolai-media:focus-within .wolai-media__quickbar { - opacity: 1; - transform: translateY(0); - pointer-events: auto; - } - .wolai-media__quickbutton { - display: inline-flex; - align-items: center; - justify-content: center; - width: 34px; - height: 34px; - border-radius: 9999px; - background-color: transparent; - color: #e2e8f0; - border: none; - cursor: pointer; - transition: background-color 0.15s ease, color 0.15s ease; - } - .wolai-media__quickbutton:hover, - .wolai-media__quickbutton:focus-visible { - background-color: rgba(255, 255, 255, 0.15); - color: #fff; - outline: none; - } - .wolai-media__hint { - margin-top: 6px; - font-size: 12px; - color: #94a3b8; - text-align: left; - } - .wolai-media__resize-handle { - position: absolute; - top: 50%; - width: 12px; - height: 44px; - border-radius: 2px; - background: rgba(15, 23, 42, 0.5); - transform: translateY(-50%); - opacity: 0; - transition: opacity 0.15s ease; - cursor: ew-resize; - z-index: 20; - } - .wolai-media__resize-handle--left { - left: -6px; - } - .wolai-media__resize-handle--right { - right: -6px; - } - .wolai-media:hover .wolai-media__resize-handle, - .wolai-media:focus-within .wolai-media__resize-handle { - opacity: 1; - } - .wolai-media__resize-handle.is-dragging { - background: rgba(37, 99, 235, 0.85); - } - - .online-table-block { - position: relative; - } - - .online-table-block:hover .wolai-table-resize-handle, - .online-table-block:focus-within .wolai-table-resize-handle { - opacity: 1; - } - - .wolai-table-resize-handle { - position: absolute; - background: rgba(37, 99, 235, 0.12); - border-radius: 999px; - transition: opacity 0.15s ease, background 0.2s ease, transform 0.12s ease; - opacity: 0; - z-index: 40; - box-shadow: 0 6px 18px rgba(37, 99, 235, 0.08); - } - - .wolai-table-resize-handle.is-dragging { - background: rgba(37, 99, 235, 0.38); - } - - .wolai-table-resize-handle--left, - .wolai-table-resize-handle--right { - top: 50%; - width: 10px; - height: 34px; - transform: translateY(-50%); - cursor: ew-resize; - } - - .wolai-table-resize-handle--left { - left: 6px; - } - - .wolai-table-resize-handle--right { - right: 6px; - } - - .wolai-table-resize-handle--top, - .wolai-table-resize-handle--bottom { - left: 50%; - width: 34px; - height: 10px; - transform: translateX(-50%); - cursor: ns-resize; - } - - .wolai-table-resize-handle--top { - top: 6px; - } - - .wolai-table-resize-handle--bottom { - bottom: 6px; - } - - .wolai-table-resize-handle--top-left, - .wolai-table-resize-handle--top-right, - .wolai-table-resize-handle--bottom-left, - .wolai-table-resize-handle--bottom-right { - width: 12px; - height: 12px; - border-radius: 6px; - } - - .wolai-table-resize-handle--top-left { - top: 6px; - left: 6px; - cursor: nwse-resize; - } - - .wolai-table-resize-handle--bottom-right { - bottom: 6px; - right: 6px; - cursor: nwse-resize; - } - - .wolai-table-resize-handle--top-right { - top: 6px; - right: 6px; - cursor: nesw-resize; - } - - .wolai-table-resize-handle--bottom-left { - bottom: 6px; - left: 6px; - cursor: nesw-resize; - } - /* 提升 Luckysheet 内联编辑浮层的层级,避免被全屏遮罩挡住 */ - .luckysheet-input-box, - #luckysheet-rich-text-editor { - z-index: 100 !important; - } - - /* Wolai 编辑器样式 */ - .wolai-editor .bn-block-content { - color: #37352F; - line-height: 1.6; - } - - /* 页面选项:小字体(覆盖 BlockNote 的 .bn-default-styles 默认字号) */ - .wolai-small-text .wolai-editor.bn-default-styles, - .wolai-small-text .wolai-editor .bn-default-styles { - font-size: 15px; - } - - /* 自定义页面:字体 */ - .wolai-page-font-song { - font-family: "宋体", SimSun, "Songti SC", serif; - } - - .wolai-page-font-song .wolai-editor.bn-default-styles, - .wolai-page-font-song .wolai-editor .bn-default-styles { - font-family: "宋体", SimSun, "Songti SC", serif; - } - - .wolai-page-font-kai { - font-family: "楷体", "楷体_GB2312", SimKai, STKaiti, serif; - } - - .wolai-page-font-kai .wolai-editor.bn-default-styles, - .wolai-page-font-kai .wolai-editor .bn-default-styles { - font-family: "楷体", "楷体_GB2312", SimKai, STKaiti, serif; - } - - /* 自定义页面:布局密度(紧凑/默认/宽容) */ - .wolai-page-density-compact .wolai-editor .bn-block-content { - line-height: 1.45; - padding-top: 1px; - padding-bottom: 1px; - } - - .wolai-page-density-spacious .wolai-editor .bn-block-content { - line-height: 1.75; - padding-top: 6px; - padding-bottom: 6px; - } - - /* 页面选项:小字体(强制覆盖 BlockNote 默认字号) */ - .wolai-small-text .wolai-editor.bn-default-styles, - .wolai-small-text .wolai-editor .bn-default-styles, - .wolai-editor.wolai-small-text.bn-default-styles, - .wolai-editor.wolai-small-text .bn-default-styles { - font-size: 15px !important; - } - - /* 自定义页面:字体(强制覆盖 BlockNote 默认字体) */ - .wolai-page-font-song .wolai-editor.bn-default-styles, - .wolai-page-font-song .wolai-editor .bn-default-styles, - .wolai-editor.wolai-page-font-song.bn-default-styles, - .wolai-editor.wolai-page-font-song .bn-default-styles { - font-family: "宋体", SimSun, "Songti SC", serif !important; - } - - .wolai-page-font-kai .wolai-editor.bn-default-styles, - .wolai-page-font-kai .wolai-editor .bn-default-styles, - .wolai-editor.wolai-page-font-kai.bn-default-styles, - .wolai-editor.wolai-page-font-kai .bn-default-styles { - font-family: "楷体", "楷体_GB2312", SimKai, STKaiti, serif !important; - } - - /* 自定义页面:布局密度(强制覆盖 BlockNote 默认行高/间距) */ - .wolai-page-density-compact .wolai-editor .bn-block-content, - .wolai-editor.wolai-page-density-compact .bn-block-content { - line-height: 1.45 !important; - padding-top: 1px !important; - padding-bottom: 1px !important; - } - - .wolai-page-density-spacious .wolai-editor .bn-block-content, - .wolai-editor.wolai-page-density-spacious .bn-block-content { - line-height: 1.75 !important; - padding-top: 6px !important; - padding-bottom: 6px !important; - } - - .wolai-page-density-compact .wolai-editor h1 { - margin-top: 1.2em; - } - - .wolai-page-density-spacious .wolai-editor h1 { - margin-top: 1.8em; - } - - .wolai-page-density-compact .wolai-editor h2, - .wolai-page-density-compact .wolai-editor h3 { - margin-top: 1.1em; - } - - .wolai-page-density-spacious .wolai-editor h2, - .wolai-page-density-spacious .wolai-editor h3 { - margin-top: 1.6em; - } - - /* 自定义页面:隐藏子页面(仅隐藏通过 /ym 创建的子页面块) */ - .wolai-hide-child-pages [data-child-page="true"] { - display: none !important; - } - - /* 标题样式 */ - .wolai-editor h1 { - font-size: 30px; - font-weight: 700; - margin-top: 1.5em; - margin-bottom: 0.5em; - color: #37352F; - line-height: 1.3; - } - - .wolai-editor h2 { - font-size: 24px; - font-weight: 600; - margin-top: 1.4em; - margin-bottom: 0.4em; - color: #37352F; - line-height: 1.4; - } - - .wolai-editor h3 { - font-size: 20px; - font-weight: 600; - margin-top: 1.3em; - margin-bottom: 0.4em; - color: #37352F; - line-height: 1.4; - } - - /* 列表样式 */ - .wolai-editor ul, - .wolai-editor ol { - padding-left: 1.5em; - } - - .wolai-editor li { - margin: 2px 0; - } - - /* 引用块样式 */ - .wolai-editor blockquote { - border-left: 3px solid #E9E9E8; - padding-left: 1em; - color: #9B9A97; - font-style: italic; - } - - /* 代码块样式 */ - .wolai-editor pre { - background-color: #F7F7F5; - border-radius: 4px; - padding: 1em; - font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; - font-size: 14px; - } - - /* 分割线样式 */ - .wolai-editor hr { - border-color: #E9E9E8; - margin: 1.5em 0; - } - - /* 链接样式 */ - .wolai-editor a { - color: #2563eb; - text-decoration: underline; - } - - .wolai-editor a:hover { - color: #1d4ed8; - } - - /* Slash 菜单样式 */ - .bn-suggestion-menu { - background-color: #FFFFFF; - border: 1px solid #E9E9E8; - border-radius: 4px; - box-shadow: rgba(15, 15, 15, 0.05) 0px 0px 0px 1px, rgba(15, 15, 15, 0.1) 0px 3px 6px, rgba(15, 15, 15, 0.2) 0px 9px 24px; - } - - .bn-suggestion-menu-item:hover, - .bn-suggestion-menu-item.selected { - background-color: #EFEFEF; - } - - .bn-suggestion-menu-item-title { - color: #37352F; - font-weight: 500; - } - - .bn-suggestion-menu-item-subtext { - color: #9B9A97; - } - - .bn-suggestion-menu-item-group-title { - color: #9B9A97; - font-size: 12px; - font-weight: 600; - } -} diff --git a/wolai-frontend/src/app/iconfont.css b/wolai-frontend/src/app/iconfont.css deleted file mode 100644 index 4a08ceec..00000000 --- a/wolai-frontend/src/app/iconfont.css +++ /dev/null @@ -1,430 +0,0 @@ -@font-face { - font-family: "iconfont"; /* Project id 2479351 */ - src: url('/mindmap-iconfont/iconfont.woff2?t=1719815803051') format('woff2'), - url('/mindmap-iconfont/iconfont.woff?t=1719815803051') format('woff'), - url('/mindmap-iconfont/iconfont.ttf?t=1719815803051') format('truetype'); -} - -.iconfont { - font-family: "iconfont" !important; - font-size: 16px; - font-style: normal; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.iconwaikuang:before { - content: "\e640"; -} - -.iconhighlight:before { - content: "\e6b8"; -} - -.iconyanshibofang:before { - content: "\e648"; -} - -.iconfujian:before { - content: "\e88a"; -} - -.icongeshihua:before { - content: "\e7a3"; -} - -.iconyuanma:before { - content: "\e658"; -} - -.icongundongtiao:before { - content: "\e670"; -} - -.iconxietongwendang:before { - content: "\e60d"; -} - -.iconTXT:before { - content: "\e6e1"; -} - -.iconwenjian1:before { - content: "\e69f"; -} - -.icondodeparent:before { - content: "\e70f"; -} - -.icongongshi:before { - content: "\e617"; -} - -.icontouming:before { - content: "\e60c"; -} - -.iconlieri:before { - content: "\e60b"; -} - -.iconmoon_line:before { - content: "\e745"; -} - -.iconsousuo:before { - content: "\e693"; -} - -.iconjiantouyou:before { - content: "\e62d"; -} - -.iconbianji1:before { - content: "\e60a"; -} - -.icondaohang1:before { - content: "\e632"; -} - -.iconyanjing:before { - content: "\e8bf"; -} - -.iconwangzhan:before { - content: "\e628"; -} - -.iconcsdn:before { - content: "\e608"; -} - -.iconshejiaotubiao-10:before { - content: "\e644"; -} - -.iconstar:before { - content: "\e7df"; -} - -.iconfork:before { - content: "\e641"; -} - -.iconxiazai:before { - content: "\e613"; -} - -.iconteamwork:before { - content: "\e870"; -} - -.iconshuiyin:before { - content: "\e67a"; -} - -.iconxmind:before { - content: "\ea57"; -} - -.iconmouseR:before { - content: "\e6bd"; -} - -.iconmouseL:before { - content: "\e6c0"; -} - -.iconwenjian:before { - content: "\e607"; -} - -.iconpdf:before { - content: "\e740"; -} - -.iconPNG:before { - content: "\ec18"; -} - -.iconSVG:before { - content: "\e621"; -} - -.iconmarkdown:before { - content: "\ec04"; -} - -.iconjson:before { - content: "\ea42"; -} - -.iconlianjiexian:before { - content: "\e75b"; -} - -.iconbangzhu:before { - content: "\e620"; -} - -.iconshezhi:before { - content: "\e8b7"; -} - -.iconwushuju:before { - content: "\e643"; -} - -.iconzuijinliulan:before { - content: "\e62f"; -} - -.icon3zuidahua-3:before { - content: "\e692"; -} - -.iconzuixiaohua:before { - content: "\e650"; -} - -.iconzuidahua:before { - content: "\e651"; -} - -.iconguanbi:before { - content: "\e652"; -} - -.icondiannao:before { - content: "\eac0"; -} - -.iconzhuye:before { - content: "\e65c"; -} - -.iconbendi1x:before { - content: "\e606"; -} - -.iconbeijingyanse:before { - content: "\e6f8"; -} - -.iconqingchu:before { - content: "\e605"; -} - -.iconcase:before { - content: "\e6c6"; -} - -.iconxingzhuang-wenzi:before { - content: "\eb99"; -} - -.iconzitijiacu:before { - content: "\ec83"; -} - -.iconzitixiahuaxian:before { - content: "\ec85"; -} - -.iconzitixieti:before { - content: "\ec86"; -} - -.iconshanchuxian:before { - content: "\e612"; -} - -.iconzitiyanse:before { - content: "\e854"; -} - -.icongithub:before { - content: "\e64f"; -} - -.iconchoose1:before { - content: "\e6c5"; -} - -.iconzhuti:before { - content: "\e7aa"; -} - -.icondaochu1:before { - content: "\e63e"; -} - -.iconlingcunwei:before { - content: "\e657"; -} - -.iconexport:before { - content: "\e642"; -} - -.icondakai:before { - content: "\ebdf"; -} - -.iconxinjian:before { - content: "\e64e"; -} - -.iconjianqie:before { - content: "\e601"; -} - -.iconzhengli:before { - content: "\e83b"; -} - -.iconfuzhi:before { - content: "\e604"; -} - -.iconniantie:before { - content: "\e63f"; -} - -.iconshangyi:before { - content: "\e6be"; -} - -.iconxiayi:before { - content: "\e6bf"; -} - -.icongaikuozonglan:before { - content: "\e609"; -} - -.iconquanxuan:before { - content: "\f199"; -} - -.icondaoru:before { - content: "\e6a3"; -} - -.iconhoutui-shi:before { - content: "\e656"; -} - -.iconqianjin1:before { - content: "\e654"; -} - -.iconwithdraw:before { - content: "\e603"; -} - -.iconqianjin:before { - content: "\e600"; -} - -.iconhuifumoren:before { - content: "\e60e"; -} - -.iconhuanhang:before { - content: "\e61e"; -} - -.iconsuoxiao:before { - content: "\ec13"; -} - -.iconbianji:before { - content: "\e626"; -} - -.iconfangda:before { - content: "\e663"; -} - -.iconquanping1:before { - content: "\e664"; -} - -.icondingwei:before { - content: "\e616"; -} - -.icondaohang:before { - content: "\e611"; -} - -.iconjianpan:before { - content: "\e64d"; -} - -.iconquanping:before { - content: "\e602"; -} - -.icondaochu:before { - content: "\e63d"; -} - -.iconbiaoqian:before { - content: "\e63c"; -} - -.iconflow-Mark:before { - content: "\e65b"; -} - -.iconchaolianjie:before { - content: "\e6f4"; -} - -.iconjingzi:before { - content: "\e610"; -} - -.iconxiaolian:before { - content: "\e60f"; -} - -.iconimage:before { - content: "\e629"; -} - -.iconjiegou:before { - content: "\e61d"; -} - -.iconyangshi:before { - content: "\e631"; -} - -.iconfuhao-dagangshu:before { - content: "\e71f"; -} - -.icontianjiazijiedian:before { - content: "\e622"; -} - -.iconjiedian:before { - content: "\e655"; -} - -.iconshanchu:before { - content: "\e696"; -} - -.iconzhankai:before { - content: "\e64c"; -} - -.iconzhankai1:before { - content: "\e673"; -} diff --git a/wolai-frontend/src/app/layout.tsx b/wolai-frontend/src/app/layout.tsx deleted file mode 100644 index b625eb2d..00000000 --- a/wolai-frontend/src/app/layout.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import type { Metadata } from "next"; -import "./globals.css"; -import { QueryProvider } from "@/components/providers/query-provider"; -import { ConvexClientProvider } from "@/components/providers/convex-provider"; -import { AppPreferencesHydrator } from "@/components/providers/app-preferences-hydrator"; -import { getMnotePublicRuntimeConfig } from "@/lib/runtime-config"; -import { isConvexEnabled } from "@/lib/convex/enabled"; -import { ConvexAuthNextjsServerProvider } from "@convex-dev/auth/nextjs/server"; - -export const metadata: Metadata = { - title: "Wolai Clone", - description: "Convex 自部署模式", -}; - -export default async function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - const isDesktop = process.env.MNOTE_DESKTOP === "1"; - const useConvex = isConvexEnabled(); - - const runtimeConfig = { ...getMnotePublicRuntimeConfig(), isDesktop, useConvex }; - const runtimeConfigJson = JSON.stringify(runtimeConfig).replace(/ - - -`.trim(); - -const XHR_REWRITE_SNIPPET = ` - -`.trim(); - -const stripHopByHopHeaders = (headers: Headers) => { - // 说明:Hop-by-hop headers 不应被代理转发/透传 - const hopByHop = [ - "connection", - "keep-alive", - "proxy-authenticate", - "proxy-authorization", - "te", - "trailer", - "transfer-encoding", - "upgrade", - ]; - for (const key of hopByHop) { - headers.delete(key); - } -}; - -const injectHtmlFixups = (html: string) => { - // 说明:某些网络/DNS 环境下无法解析 `static.cloudflareinsights.com`, - // ONLYOFFICE 页面若被注入了 Cloudflare 的 beacon 脚本,会在控制台反复报错, - // 并可能因为 DNS 超时而拖慢首次加载。这里在反代层面做一次轻量清理。 - const cleaned = html - // 说明:移除 Cloudflare Insights/Web Analytics beacon - .replace( - /]*\bsrc=(["'])https?:\/\/static\.cloudflareinsights\.com\/beacon\.min\.js[^>]*>\s*<\/script>/gi, - "", - ) - // 说明:部分场景会有 `cdn-cgi` 相关注入(如 rocket-loader),同样会指向 Cloudflare 域名 - .replace(/]*\bsrc=(["'])\/cdn-cgi\/[^>]*>\s*<\/script>/gi, ""); - - // 说明:只注入一次,避免重复拼接 - const hasSw = cleaned.includes("window.__MNOTE_ONLYOFFICE_SW_PATCH__"); - const hasXhr = cleaned.includes("window.__MNOTE_ONLYOFFICE_XHR_REWRITE__"); - if (hasSw && hasXhr) return cleaned; - const injected = [ - hasSw ? "" : SERVICE_WORKER_SAFE_PATCH_SNIPPET, - hasXhr ? "" : XHR_REWRITE_SNIPPET, - ] - .filter(Boolean) - .join("\n"); - return cleaned.replace(/]*>/i, (m) => `${m}\n${injected}\n`); -}; - -const pickCacheControl = (pathParts: string[], contentType: string, method: string) => { - const m = String(method || "").toUpperCase(); - if (m !== "GET" && m !== "HEAD") return "no-store"; - if (contentType.includes("text/html")) return "no-store"; - - const p = `/${(pathParts ?? []).join("/")}`; - const lower = p.toLowerCase(); - - // 说明:这些路径通常是运行期接口/动态响应,不应缓存。 - if ( - lower.includes("/docservice/") || - lower.includes("/coauthoring/") || - lower.includes("/converter/") || - lower.includes("/healthcheck") || - lower.includes("/metrics") - ) { - return "no-store"; - } - - // 说明:/cache 下是 ONLYOFFICE 运行期二进制缓存(例如 Editor.bin、字体包、拼写词典等), - // 通常以内容 ID 命名(可能没有扩展名),但内容在同一路径下应保持稳定。 - // 对这类资源做较长缓存可以显著提升二次打开速度。 - if (lower.includes("/cache/")) { - return "public, max-age=604800, stale-while-revalidate=86400"; - } - - const isStaticByPath = - lower.includes("/web-apps/") || - lower.includes("/sdkjs/") || - lower.includes("/fonts/") || - lower.endsWith(".js") || - lower.endsWith(".css") || - lower.endsWith(".map") || - lower.endsWith(".png") || - lower.endsWith(".jpg") || - lower.endsWith(".jpeg") || - lower.endsWith(".gif") || - lower.endsWith(".svg") || - lower.endsWith(".ico") || - lower.endsWith(".woff") || - lower.endsWith(".woff2") || - lower.endsWith(".ttf") || - lower.endsWith(".otf") || - lower.endsWith(".json") || - lower.endsWith(".wasm") || - lower.endsWith(".bin"); - - // 说明:ONLYOFFICE 静态资源体积大,且文件名通常稳定;这里尽量给浏览器缓存,提升二次打开速度。 - if (isStaticByPath) { - return "public, max-age=604800, stale-while-revalidate=86400, immutable"; - } - - return "public, max-age=300, stale-while-revalidate=300"; -}; - -const buildStableEtag = (pathParts: string[], search: string) => { - // 说明:ONLYOFFICE 的静态资源路径通常包含版本号/哈希(例如 /9.2.1-/sdkjs/...)。 - // 在 frp/隧道场景下,如果浏览器端请求带 `cache-control: no-cache`,会强制走 revalidate; - // 但只要我们提供稳定 ETag,就可以快速返回 304,避免重复下载几十 MB 的资源。 - const p = `/${(pathParts ?? []).join("/")}`; - const s = String(search || ""); - return `W/"mnote-oo:${p}${s}"`; -}; - -const isStaticCacheControl = (cc: string) => { - const v = String(cc || ""); - // 与 pickCacheControl 的“静态资源”分支对齐 - return v.includes("max-age=604800"); -}; - -const shouldGzip = (request: NextRequest, contentType: string) => { - // 说明:在 TCP 隧道/跨地域场景下,静态资源体积大时开启 gzip 能显著降低首开耗时。 - // 仅对典型文本资源启用,避免对 woff2/wasm/bin 等已压缩或不适合压缩的内容浪费 CPU。 - const ae = String(request.headers.get("accept-encoding") || "").toLowerCase(); - if (!ae.includes("gzip")) return false; - - const ct = String(contentType || "").toLowerCase(); - const compressible = - ct.startsWith("text/") || - ct.includes("javascript") || - ct.includes("json") || - ct.includes("xml") || - ct.includes("svg"); - - return compressible; -}; - -const proxy = async (request: NextRequest, pathParts: string[]) => { - const onlyofficeInternalUrl = await resolveOnlyOfficeInternalUrl(); - const incomingUrl = new URL(request.url); - const target = new URL(`${onlyofficeInternalUrl}/${pathParts.map(encodeURIComponent).join("/")}`); - target.search = incomingUrl.search; - - const headers = new Headers(request.headers); - // 说明:避免把外部 Host 传给上游 - headers.delete("host"); - // 说明:ONLYOFFICE 在被反向代理时,会根据 X-Forwarded-* 推导自身对外地址, - // 用于生成静态资源/缓存文件的 URL。若缺失这些信息,可能会返回指向内部端口 - //(例如 http://127.0.0.1:8081/cache/...)的绝对 URL,导致浏览器跨域请求被 CORS 拦截。 - const runtimeCfg = getMnoteRuntimeConfig(); - const xfp = (request.headers.get("x-forwarded-proto") || "").split(",")[0].trim(); - const xfh = (request.headers.get("x-forwarded-host") || "").split(",")[0].trim(); - const xfpPort = (request.headers.get("x-forwarded-port") || "").split(",")[0].trim(); - - const originLike = - request.headers.get("origin") || - request.headers.get("referer") || - ""; - const originUrl = (() => { - try { - if (!originLike) return null; - return new URL(originLike); - } catch { - return null; - } - })(); - - const cloudflare = (() => { - try { - const raw = String(runtimeCfg.cloudflareAppOrigin || "").trim(); - if (!raw) return null; - return new URL(raw); - } catch { - return null; - } - })(); - - const inferredProto = - xfp || - (originUrl ? originUrl.protocol.replace(":", "") : "") || - (cloudflare && cloudflare.host === incomingUrl.host ? cloudflare.protocol.replace(":", "") : "") || - incomingUrl.protocol.replace(":", ""); - const inferredHost = - xfh || - (originUrl ? originUrl.host : "") || - incomingUrl.host; - const inferredPort = - xfpPort || - (originUrl ? originUrl.port : "") || - (cloudflare && cloudflare.host === incomingUrl.host ? cloudflare.port : "") || - incomingUrl.port || - (inferredProto === "https" ? "443" : "80"); - - const proto = inferredProto || "http"; - const host = inferredHost || incomingUrl.host; - const port = inferredPort || (proto === "https" ? "443" : "80"); - - headers.set("x-forwarded-host", host); - headers.set("x-forwarded-proto", proto); - headers.set("x-forwarded-port", port); - headers.set("x-forwarded-prefix", "/onlyoffice-server"); - // 说明:让上游返回明文(identity),我们在此处按需 gzip 压缩,保证 content-encoding 与 body 一致。 - // 避免 Node fetch 自动解压后仍携带 content-encoding,导致浏览器报 ERR_CONTENT_DECODING_FAILED。 - headers.set("accept-encoding", "identity"); - stripHopByHopHeaders(headers); - - const method = request.method.toUpperCase(); - const hasBody = !["GET", "HEAD"].includes(method); - - // 说明:在 Node 的 fetch(Undici)里,ReadableStream body 需要设置 duplex: "half"。 - // 但 TS 的 RequestInit 类型不包含 duplex,这里用 any 兜底。 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const init: any = { - method, - headers, - body: hasBody ? request.body : undefined, - duplex: "half", - redirect: "follow", - }; - const upstream = await fetch(target, init); - - const upstreamContentLength = upstream.headers.get("content-length"); - const outHeaders = new Headers(upstream.headers); - stripHopByHopHeaders(outHeaders); - outHeaders.delete("content-encoding"); - outHeaders.delete("content-length"); - - const contentType = upstream.headers.get("content-type") || ""; - const cacheControl = pickCacheControl(pathParts, contentType, request.method); - outHeaders.set("cache-control", cacheControl); - outHeaders.delete("pragma"); - outHeaders.delete("expires"); - outHeaders.delete("set-cookie"); - - // 说明:为静态资源提供稳定 ETag,并支持 if-none-match → 304,提升重复打开速度。 - // 注意:仅对我们判定为“强缓存静态资源”的请求启用,避免影响协同接口/动态响应。 - if (isStaticCacheControl(cacheControl)) { - const etag = buildStableEtag(pathParts, incomingUrl.search); - outHeaders.set("etag", etag); - const inm = String(request.headers.get("if-none-match") || "").trim(); - if (inm && (inm === etag || inm.split(",").map((s) => s.trim()).includes(etag))) { - return new NextResponse(null, { status: 304, headers: outHeaders }); - } - } - - // 说明:在部分隧道/反代场景下,浏览器对 “chunked” 的大静态资源不会写入磁盘缓存, - // 导致重复打开仍然完整下载(几十 MB)。这里对“强缓存静态资源”改为定长响应: - // - 先把上游 body 缓冲到 ArrayBuffer - // - 写入 content-length - // 这样浏览器更容易落盘缓存,二次打开会明显提速。 - if (isStaticCacheControl(cacheControl) && !contentType.includes("text/html")) { - outHeaders.delete("vary"); - outHeaders.set("vary", "Accept-Encoding"); - if (method === "HEAD") { - if (upstreamContentLength) outHeaders.set("content-length", upstreamContentLength); - return new NextResponse(null, { status: upstream.status, headers: outHeaders }); - } - const buf = await upstream.arrayBuffer(); - - // 说明:优先 gzip(如果客户端支持),可显著降低跨地域/隧道场景下首开耗时。 - if (shouldGzip(request, contentType)) { - try { - const gz = gzipSync(Buffer.from(buf), { level: 6 }); - outHeaders.set("content-encoding", "gzip"); - outHeaders.set("content-length", String(gz.byteLength)); - return new NextResponse(gz, { status: upstream.status, headers: outHeaders }); - } catch { - // ignore,回退到未压缩 - } - } - - outHeaders.set("content-length", String(buf.byteLength)); - return new NextResponse(buf, { status: upstream.status, headers: outHeaders }); - } - - if (contentType.includes("text/html")) { - const html = await upstream.text(); - const injected = injectHtmlFixups(html); - return new NextResponse(injected, { - status: upstream.status, - headers: outHeaders, - }); - } - - return new NextResponse(upstream.body, { - status: upstream.status, - headers: outHeaders, - }); -}; - -type RouteCtx = { params: Promise<{ path: string[] }> }; - -export async function GET(request: NextRequest, ctx: RouteCtx) { - const { path } = await ctx.params; - return proxy(request, path ?? []); -} - -export async function POST(request: NextRequest, ctx: RouteCtx) { - const { path } = await ctx.params; - return proxy(request, path ?? []); -} - -export async function PUT(request: NextRequest, ctx: RouteCtx) { - const { path } = await ctx.params; - return proxy(request, path ?? []); -} - -export async function PATCH(request: NextRequest, ctx: RouteCtx) { - const { path } = await ctx.params; - return proxy(request, path ?? []); -} - -export async function DELETE(request: NextRequest, ctx: RouteCtx) { - const { path } = await ctx.params; - return proxy(request, path ?? []); -} - -export async function HEAD(request: NextRequest, ctx: RouteCtx) { - const { path } = await ctx.params; - return proxy(request, path ?? []); -} - -export async function OPTIONS(request: NextRequest, ctx: RouteCtx) { - const { path } = await ctx.params; - return proxy(request, path ?? []); -} diff --git a/wolai-frontend/src/app/onlyoffice/OnlyOfficeClientPage.tsx b/wolai-frontend/src/app/onlyoffice/OnlyOfficeClientPage.tsx deleted file mode 100644 index 11c0a40a..00000000 --- a/wolai-frontend/src/app/onlyoffice/OnlyOfficeClientPage.tsx +++ /dev/null @@ -1,1056 +0,0 @@ -"use client"; - -import { useEffect, useMemo, useRef, useState } from "react"; -import { useSearchParams } from "next/navigation"; -import { OnlyOfficeAiAgentPanel } from "@/components/onlyoffice/OnlyOfficeAiAgentPanel"; -import { getMnoteRuntimeConfig } from "@/lib/runtime-config"; -import { useConvex } from "convex/react"; -import { api } from "@/lib/convex/api"; -import { - buildOnlyOfficeCallbackUrl, - buildOnlyOfficeForcesaveUrl, - buildOnlyOfficeOpenFileId, - docTypeFromExt, - hashOnlyOfficeKey, - resolveOnlyOfficeDocumentUrl, -} from "@/lib/onlyoffice/client-session"; - -type EditorMode = "view" | "edit"; - -const MNOTE_AGENT_PLUGIN_GUID = "asc.{F2B9A7B4-3A22-4E0E-8B32-3B0DE7AE8A60}"; - -declare global { - interface Window { - __MNOTE_ONLYOFFICE_ERRLOG__?: Array>; - __MNOTE_ONLYOFFICE_ERR_HOOKED__?: boolean; - __MNOTE_ONLYOFFICE_DOMPATCHED__?: boolean; - __MNOTE_ONLYOFFICE_DEBUG__?: Record; - } -} - -const setupOnlyOfficeGlobalErrorCapture = () => { - if (typeof window === "undefined") return; - if (window.__MNOTE_ONLYOFFICE_ERR_HOOKED__) return; - window.__MNOTE_ONLYOFFICE_ERR_HOOKED__ = true; - - const push = (payload: Record) => { - try { - window.__MNOTE_ONLYOFFICE_ERRLOG__ = window.__MNOTE_ONLYOFFICE_ERRLOG__ || []; - window.__MNOTE_ONLYOFFICE_ERRLOG__.push(payload); - // 说明:写入 localStorage,便于“发生崩溃导致整页 reload”时仍能回溯最近错误。 - window.localStorage.setItem("mnote_onlyoffice_last_error", JSON.stringify(payload)); - } catch { - // ignore - } - }; - - window.addEventListener( - "error", - (e) => { - try { - push({ - kind: "error", - message: String((e as ErrorEvent).message || ""), - filename: String((e as ErrorEvent).filename || ""), - lineno: Number((e as ErrorEvent).lineno || 0), - colno: Number((e as ErrorEvent).colno || 0), - name: String(((e as any)?.error as any)?.name || ""), - }); - } catch { - // ignore - } - }, - true, - ); - - window.addEventListener( - "unhandledrejection", - (e) => { - try { - const r = (e as PromiseRejectionEvent).reason as any; - push({ - kind: "rejection", - message: String(r?.message ?? r ?? ""), - name: String(r?.name ?? ""), - }); - } catch { - // ignore - } - }, - true, - ); - - // 说明:ONLYOFFICE 内部偶发触发 removeChild 的 NotFoundError(不同环境下 message 可能为空)。 - // 该异常会导致 Next.js 直接显示“客户端异常”白屏,因此这里在 ONLYOFFICE 页面内对 removeChild 做兜底补丁。 - // 仅在 /onlyoffice 页面生效,不影响其它页面。 - if (!window.__MNOTE_ONLYOFFICE_DOMPATCHED__) { - window.__MNOTE_ONLYOFFICE_DOMPATCHED__ = true; - try { - const orig = Node.prototype.removeChild; - - (Node.prototype as any).removeChild = function removeChildPatched(child: T): T { - try { - return orig.call(this, child) as T; - } catch (e) { - const name = (e as any)?.name ? String((e as any).name) : ""; - if (name === "NotFoundError") { - return child; - } - throw e; - } - }; - } catch { - // ignore - } - } -}; - -setupOnlyOfficeGlobalErrorCapture(); - -const loadScript = (src: string) => - new Promise((resolve, reject) => { - const existing = document.querySelector(`script[src="${src}"]`) as HTMLScriptElement | null; - if (existing) { - if (existing.dataset.mnoteLoaded === "1") { - resolve(); - return; - } - if (existing.dataset.mnoteFailed === "1") { - reject(new Error(`加载 ONLYOFFICE 脚本失败: ${src}`)); - return; - } - existing.addEventListener("load", () => resolve(), { once: true }); - existing.addEventListener("error", () => reject(new Error(`加载 ONLYOFFICE 脚本失败: ${src}`)), { - once: true, - }); - return; - } - const script = document.createElement("script"); - script.src = src; - script.async = true; - script.onload = () => { - script.dataset.mnoteLoaded = "1"; - delete script.dataset.mnoteFailed; - resolve(); - }; - script.onerror = () => { - script.dataset.mnoteFailed = "1"; - reject(new Error(`加载 ONLYOFFICE 脚本失败: ${src}`)); - }; - document.body.appendChild(script); - }); - -const loadScriptCandidates = async (candidates: string[]) => { - let lastError: Error | null = null; - for (const src of candidates) { - try { - await loadScript(src); - return src; - } catch (error) { - lastError = error instanceof Error ? error : new Error(String(error)); - } - } - throw lastError ?? new Error("加载 ONLYOFFICE 脚本失败"); -}; - -const setupOnlyOfficeInternalRequestRewrite = (baseUrl: string, onlyofficeBaseUrlDesktop?: string | null) => { - // 说明:当我们用 `/onlyoffice-server` 反代 ONLYOFFICE 时,编辑器运行期仍可能发起指向 - // `http://127.0.0.1:8081/cache/...` 的绝对请求(来自 ONLYOFFICE 内部),导致浏览器跨域被 CORS 拦截。 - // 这里在 ONLYOFFICE 页面内对 XHR/fetch/window.open/location.assign 等做 URL 重写: - // 把 “内部 8081” 的请求改写回同源 `/onlyoffice-server/*`。 - // 注意:ONLYOFFICE 会创建多个 iframe(同源但不同 realm),因此这里也会周期性给新出现的 iframe 打补丁。 - if (typeof window === "undefined") return; - if (!baseUrl) return; - - const normalizedBase = String(baseUrl || "").trim().replace(/\/+$/, ""); - const isProxyMode = normalizedBase === "/onlyoffice-server" || normalizedBase.endsWith("/onlyoffice-server"); - if (!isProxyMode) return; - - const proxyPrefix = (() => { - if (/^https?:\/\//i.test(normalizedBase)) return normalizedBase; - return `${window.location.origin.replace(/\/+$/, "")}${normalizedBase}`; - })(); - - const internalOrigins = new Set([ - "http://127.0.0.1:8081", - "http://localhost:8081", - "http://127.0.0.1:8082", - "http://localhost:8082", - // 说明:部分环境下 ONLYOFFICE 会错误拼出 https://127.0.0.1:8081 这类 URL, - // 浏览器会报 ERR_SSL_PROTOCOL_ERROR(因为 8081 实际是 http)。这里也一起兜底重写。 - "https://127.0.0.1:8081", - "https://localhost:8081", - "https://127.0.0.1:8082", - "https://localhost:8082", - ]); - try { - if (onlyofficeBaseUrlDesktop) { - const u = new URL(onlyofficeBaseUrlDesktop); - internalOrigins.add(`${u.protocol}//${u.host}`); - } - } catch { - // ignore - } - - const patchWindow = (win: Window) => { - try { - if ((win as any).__MNOTE_ONLYOFFICE_XHR_REWRITE__) return; - const rewriteUrl = (input: string) => { - try { - const u = new (win as any).URL(input, (win as any).location?.origin || window.location.origin); - // 说明:外网 https 访问时,若 ONLYOFFICE 错误生成 http:///onlyoffice-server/... 会被 Mixed Content 阻止; - // 这里提前升级为 https,避免浏览器直接拦截请求。 - try { - const loc = (win as any).location; - if (loc?.protocol === "https:" && u.protocol === "http:" && u.host === loc.host) { - if (u.pathname === "/onlyoffice-server" || u.pathname.startsWith("/onlyoffice-server/")) { - u.protocol = "https:"; - return u.toString(); - } - } - } catch { - // ignore - } - const origin = `${u.protocol}//${u.host}`; - if (!internalOrigins.has(origin)) return input; - return `${proxyPrefix}${u.pathname}${u.search}${u.hash}`; - } catch { - return input; - } - }; - - // 说明:修复 “下载为 xlsx/docx” 等场景:这类下载常通过 window.open/location 跳转, - // 如果 URL 指向 127.0.0.1:8081,会导致浏览器无法下载(因为 8081 只在 Docker 内部/代理后可用)。 - try { - const origWinOpen = (win as any).open; - if (typeof origWinOpen === "function") { - (win as any).open = function openPatched( - url?: string | URL, - target?: string, - features?: string, - ) { - const nextUrl = - typeof url === "string" - ? rewriteUrl(url) - : url instanceof URL - ? new URL(rewriteUrl(url.toString())) - : url; - return origWinOpen.call(this, nextUrl as any, target as any, features as any); - }; - } - } catch { - // ignore - } - - try { - const loc = (win as any).location as Location | undefined; - if (loc && typeof loc.assign === "function") { - const origAssign = loc.assign.bind(loc); - loc.assign = ((url: string) => origAssign(rewriteUrl(url))) as any; - } - if (loc && typeof loc.replace === "function") { - const origReplace = loc.replace.bind(loc); - loc.replace = ((url: string) => origReplace(rewriteUrl(url))) as any; - } - } catch { - // ignore - } - - try { - const origFetch = (win as any).fetch; - if (typeof origFetch === "function") { - (win as any).fetch = function fetchPatched(input: any, init?: any) { - try { - if (typeof input === "string") { - return origFetch.call(this, rewriteUrl(input), init); - } - if (input instanceof URL) { - return origFetch.call(this, new URL(rewriteUrl(input.toString())), init); - } - if (input && typeof input === "object" && typeof input.url === "string") { - const next = new Request(rewriteUrl(input.url), input); - return origFetch.call(this, next, init); - } - } catch { - // ignore - } - return origFetch.call(this, input, init); - }; - } - } catch { - // ignore - } - - try { - const origOpen = (win as any).XMLHttpRequest?.prototype?.open; - if (typeof origOpen === "function") { - (win as any).XMLHttpRequest.prototype.open = function openPatched( - method: string, - url: string, - async?: boolean, - user?: string | null, - password?: string | null, - ) { - const nextUrl = typeof url === "string" ? rewriteUrl(url) : url; - return origOpen.call(this, method, nextUrl, async, user as any, password as any); - }; - } - } catch { - // ignore - } - - (win as any).__MNOTE_ONLYOFFICE_XHR_REWRITE__ = true; - } catch { - // ignore - } - }; - - patchWindow(window); - - try { - const start = Date.now(); - const timer = window.setInterval(() => { - try { - const frames = Array.from(document.querySelectorAll("iframe")); - for (const f of frames) { - try { - const w = (f as HTMLIFrameElement).contentWindow; - if (!w) continue; - // 说明:同源时才能访问 location;跨域会抛异常,直接跳过。 - - w.location?.origin; - patchWindow(w); - } catch { - // ignore - } - } - } catch { - // ignore - } - if (Date.now() - start > 120_000) { - window.clearInterval(timer); - } - }, 1000); - } catch { - // ignore - } -}; - -const waitForDocEditorReady = async (timeoutMs = 120_000) => { - const start = Date.now(); - - while (true) { - // @ts-expect-error ONLYOFFICE 全局对象 - const ok = Boolean(window.DocsAPI && window.DocsAPI.DocEditor); - if (ok) return; - if (Date.now() - start > timeoutMs) { - throw new Error("等待 ONLYOFFICE DocEditor 初始化超时"); - } - - await new Promise((r) => setTimeout(r, 250)); - } -}; - -export default function OnlyOfficePage() { - const params = useSearchParams(); - const fileUrl = params.get("fileUrl") ?? ""; - const fileName = params.get("fileName") ?? "未命名文档"; - const fileType = (params.get("fileType") ?? "docx").toLowerCase(); - const mode = (params.get("mode") ?? "edit") as EditorMode; - const assetId = params.get("assetId") ?? ""; - const documentId = params.get("documentId") ?? ""; - const initialUserId = params.get("userId") ?? ""; - const channel = (params.get("channel") ?? "").trim().toLowerCase(); - const convex = useConvex(); - const [error, setError] = useState(null); - const [authedUserId, setAuthedUserId] = useState(initialUserId); - const [resolvedMode, setResolvedMode] = useState(mode === "view" ? "view" : null); - const [resolvedDisableDownload, setResolvedDisableDownload] = useState(false); - const [resolvedDisableCopy, setResolvedDisableCopy] = useState(false); - const [permissionResolved, setPermissionResolved] = useState(false); - const [assetSignedUrl, setAssetSignedUrl] = useState(""); - const [assetStorageId, setAssetStorageId] = useState(""); - const [forceSaveState, setForceSaveState] = useState<{ - busy: boolean; - message: string | null; - ok: boolean | null; - }>({ busy: false, message: null, ok: null }); - const runtimeConfig = useMemo(() => getMnoteRuntimeConfig(), []); - const baseUrlCandidates = useMemo(() => { - const uniq: string[] = []; - const push = (v?: string | null) => { - const s = String(v || "").trim().replace(/\/+$/, ""); - if (!s) return; - if (!uniq.includes(s)) uniq.push(s); - }; - - const isPageLocal = (() => { - if (typeof window === "undefined") return true; - const host = window.location.hostname; - return host === "127.0.0.1" || host === "localhost"; - })(); - - // 说明:网页端优先走同源 /onlyoffice-server(Next 代理到 ONLYOFFICE_INTERNAL_URL), - // 避免配置里误写成 https 自签证书域名导致浏览器报 ERR_CERT_AUTHORITY_INVALID。 - // 同时同源路径也更利于缓存与跨环境迁移(无需改域名/端口)。 - try { - push("/onlyoffice-server"); - push(`${window.location.origin.replace(/\/+$/, "")}/onlyoffice-server`); - } catch { - // ignore - } - - // 说明:默认优先使用运行期根据 isDesktop 归一化后的 onlyofficeBaseUrl; - // 如遇到端口转发/本机服务不可达,可自动回退到另一套配置。 - if (channel === "web") { - push(runtimeConfig.onlyofficeBaseUrlWeb); - push(runtimeConfig.onlyofficeBaseUrl); - // 说明:Web 端通过公网访问时,不应回退到 localhost/127(会触发浏览器“本地网络”权限提示,且必然不可达)。 - if (isPageLocal) { - push(runtimeConfig.onlyofficeBaseUrlDesktop); - } - return uniq; - } - if (channel === "desktop") { - push(runtimeConfig.onlyofficeBaseUrlDesktop); - push(runtimeConfig.onlyofficeBaseUrl); - push(runtimeConfig.onlyofficeBaseUrlWeb); - return uniq; - } - - push(runtimeConfig.onlyofficeBaseUrl); - if (runtimeConfig.isDesktop) { - push(runtimeConfig.onlyofficeBaseUrlWeb); - } else { - if (isPageLocal) { - push(runtimeConfig.onlyofficeBaseUrlDesktop); - } - } - return uniq; - }, [channel, runtimeConfig]); - - const [baseUrlIndex, setBaseUrlIndex] = useState(0); - const baseUrl = baseUrlCandidates[baseUrlIndex] || ""; - const storageHostOverride = runtimeConfig.onlyofficeStorageHostOverride; - const proxyOrigin = runtimeConfig.onlyofficeProxyOrigin; - const callbackOrigin = runtimeConfig.onlyofficeCallbackOrigin; - const docEditorRef = useRef(null); - - useEffect(() => { - // 说明:为了让 document.key 在同一附件的多次打开之间保持稳定(提升缓存命中与加载速度), - // 这里在有 assetId 时拉取一次附件元信息(storage_id)。 - if (!assetId) return; - let canceled = false; - fetch(`/api/media/sign?assetId=${encodeURIComponent(assetId)}`, { method: "GET" }) - .then(async (r) => { - if (!r.ok) return null; - return (await r.json().catch(() => null)) as - | { signedUrl?: string; asset?: { storage_id?: string | null } | null } - | null; - }) - .then((payload) => { - if (!payload || canceled) return; - const signed = String(payload.signedUrl || "").trim(); - const sid = String(payload.asset?.storage_id || "").trim(); - if (signed) setAssetSignedUrl(signed); - if (sid) setAssetStorageId(sid); - }) - .catch(() => { - // ignore - }); - return () => { - canceled = true; - }; - }, [assetId]); - - useEffect(() => { - // 说明:ONLYOFFICE 回调由文档服务器触发,不携带用户 Cookie。 - // 为了让 /api/onlyoffice/callback 能以真实用户身份写回存储, - // 这里尽量从当前会话获取 userId,并透传到 callbackUrl。 - if (authedUserId) return undefined; - let canceled = false; - fetch("/api/auth/whoami", { method: "GET" }) - .then(async (r) => { - if (!r.ok) return null; - return (await r.json().catch(() => null)) as { userId?: string } | null; - }) - .then((payload) => { - const uid = String(payload?.userId || "").trim(); - if (!uid) return; - if (canceled) return; - setAuthedUserId(uid); - }) - .catch(() => { - // ignore - }); - return () => { - canceled = true; - }; - }, [authedUserId]); - - useEffect(() => { - // 只读权限:强制以 view 模式打开(避免分享页面只读但 ONLYOFFICE 仍可编辑)。 - if (!documentId) { - setResolvedMode(mode === "view" ? "view" : mode); - setResolvedDisableDownload(false); - setResolvedDisableCopy(false); - setPermissionResolved(true); - return; - } - if (!authedUserId) { - setPermissionResolved(false); - return; - } - - let canceled = false; - void (async () => { - setPermissionResolved(false); - try { - const perm = await convex.query(api.documents.getPermissionForUser, { - userId: authedUserId, - id: documentId, - }); - if (canceled) return; - const disableDownload = Boolean((perm as any)?.disableDownload); - const disableCopy = Boolean((perm as any)?.disableCopy); - setResolvedDisableDownload(disableDownload); - setResolvedDisableCopy(disableCopy); - setPermissionResolved(true); - - if (mode === "view") { - setResolvedMode("view"); - return; - } - - if (!perm || (perm as any).permission !== "edit") { - setResolvedMode("view"); - } else { - setResolvedMode("edit"); - } - } catch { - if (canceled) return; - setResolvedMode(mode === "view" ? "view" : mode); - setResolvedDisableDownload(false); - setResolvedDisableCopy(false); - setPermissionResolved(true); - } - })(); - - return () => { - canceled = true; - }; - }, [authedUserId, convex, documentId, mode]); - - useEffect(() => { - // 说明:在部分 ONLYOFFICE 版本/环境下,编辑器内部会触发 DOMException(NotFoundError: removeChild), - // 该异常会被 Next.js 捕获并显示“客户端异常”大红屏,但实际文档仍可继续使用。 - // 这里仅对该特定错误做兜底拦截,避免误伤其它真实错误。 - const onError = (event: ErrorEvent) => { - const err = event.error as unknown; - const msgFromError = (() => { - try { - return typeof err === "string" ? err : String((err as any)?.message ?? err); - } catch { - return ""; - } - })(); - const msg = msgFromError || event.message || ""; - const name = (err as any)?.name ? String((err as any).name) : ""; - - try { - window.__MNOTE_ONLYOFFICE_ERRLOG__ = window.__MNOTE_ONLYOFFICE_ERRLOG__ || []; - window.__MNOTE_ONLYOFFICE_ERRLOG__.push({ - kind: "error", - name, - message: msg, - filename: event.filename || "", - lineno: event.lineno || 0, - colno: event.colno || 0, - }); - } catch { - // ignore - } - - // 说明:ONLYOFFICE 内部偶发抛出 DOMException(NotFoundError),在部分版本下 message 可能为空, - // 但会被 Next.js 捕获后直接显示“客户端异常”白屏;该错误通常不影响文档继续使用。 - if (name === "NotFoundError") { - event.preventDefault(); - try { - event.stopImmediatePropagation(); - event.stopPropagation(); - } catch { - // ignore - } - return; - } - - // 说明:部分隧道/证书环境下,ONLYOFFICE 会尝试注册 ServiceWorker(用于缓存静态资源)。 - // 但浏览器会以 SecurityError 失败,并触发全局错误,导致 Next.js 显示“客户端异常”白屏。 - // 该错误不影响文档实际编辑能力,因此这里对其进行兜底拦截。 - if ( - name === "SecurityError" || - msg.includes("Failed to register a ServiceWorker") || - msg.includes("An SSL certificate error occurred when fetching the script") - ) { - event.preventDefault(); - try { - event.stopImmediatePropagation(); - event.stopPropagation(); - } catch { - // ignore - } - } - }; - const onRejection = (event: PromiseRejectionEvent) => { - const reason = event.reason as unknown; - const msg = (() => { - try { - return typeof reason === "string" ? reason : String((reason as any)?.message ?? reason); - } catch { - return ""; - } - })(); - const name = (reason as any)?.name ? String((reason as any).name) : ""; - try { - window.__MNOTE_ONLYOFFICE_ERRLOG__ = window.__MNOTE_ONLYOFFICE_ERRLOG__ || []; - window.__MNOTE_ONLYOFFICE_ERRLOG__.push({ - kind: "rejection", - name, - message: msg, - }); - } catch { - // ignore - } - if (name === "NotFoundError") { - event.preventDefault(); - try { - event.stopImmediatePropagation(); - event.stopPropagation(); - } catch { - // ignore - } - return; - } - if ( - name === "SecurityError" || - msg.includes("Failed to register a ServiceWorker") || - msg.includes("An SSL certificate error occurred when fetching the script") - ) { - event.preventDefault(); - try { - event.stopImmediatePropagation(); - event.stopPropagation(); - } catch { - // ignore - } - } - }; - window.addEventListener("error", onError); - window.addEventListener("unhandledrejection", onRejection); - return () => { - window.removeEventListener("error", onError); - window.removeEventListener("unhandledrejection", onRejection); - }; - }, []); - - const targetDocType = useMemo(() => docTypeFromExt(fileType), [fileType]); - const effectiveFileUrl = assetSignedUrl || fileUrl; - const docKey = useMemo(() => { - // 说明:ONLYOFFICE 的 document.key 会参与其内部缓存/分片路由(部分版本会把它拼进请求参数), - // 如果 key 每次打开都变化,会导致浏览器缓存命中率极低。 - // - 有 assetId:优先用 assetId + storage_id(保存写回后 storage_id 会变化,自然失效) - // - 无 assetId:退回到现有 hash 逻辑 - if (assetId) { - const sid = String(assetStorageId || "").trim(); - // 说明:ONLYOFFICE 的 document.key 允许字符集为 [0-9a-zA-Z_.=-],不包含 ":" 等字符; - // 否则可能报错(例如 errorCode=-23)。这里用 hash 生成安全 key,同时在 storage_id 变化时自动失效。 - return sid ? `${assetId}_${hashOnlyOfficeKey(sid)}` : assetId; - } - return hashOnlyOfficeKey(`${effectiveFileUrl}-${fileName}`); - }, [assetId, assetStorageId, effectiveFileUrl, fileName]); - const resolvedFileUrl = useMemo(() => { - return resolveOnlyOfficeDocumentUrl({ - effectiveFileUrl, - proxyOrigin, - storageHostOverride, - runtimeSupabaseUrl: runtimeConfig.supabaseUrl || "", - useConvex: Boolean(runtimeConfig.useConvex), - }); - }, [effectiveFileUrl, proxyOrigin, storageHostOverride, runtimeConfig.supabaseUrl, runtimeConfig.useConvex]); - - useEffect(() => { - try { - window.__MNOTE_ONLYOFFICE_DEBUG__ = { - pageOrigin: window.location.origin, - baseUrl, - proxyOrigin, - callbackOrigin, - fileUrlInput: effectiveFileUrl, - resolvedFileUrl, - fileName, - fileType, - mode, - resolvedMode, - resolvedDisableDownload, - resolvedDisableCopy, - permissionResolved, - computedPermissions: { - edit: resolvedMode === "edit", - download: !resolvedDisableDownload, - print: !resolvedDisableDownload, - copy: !resolvedDisableCopy, - }, - assetId, - documentId, - docKey, - }; - } catch { - // ignore - } - }, [ - assetId, - authedUserId, - baseUrl, - callbackOrigin, - docKey, - effectiveFileUrl, - fileName, - fileType, - mode, - resolvedMode, - resolvedDisableCopy, - resolvedDisableDownload, - permissionResolved, - documentId, - proxyOrigin, - resolvedFileUrl, - ]); - - useEffect(() => { - let canceled = false; - const destroyEditor = () => { - try { - const inst = docEditorRef.current; - docEditorRef.current = null; - if (inst && typeof inst.destroyEditor === "function") { - inst.destroyEditor(); - } - } catch { - // ignore - } - try { - const el = document.getElementById("onlyoffice-frame"); - if (el) el.innerHTML = ""; - } catch { - // ignore - } - }; - - // 说明:如果是附件(assetId)编辑模式,则必须拿到真实 userId 才能让 - // /api/onlyoffice/callback 通过工作空间成员校验并把文件写回存储。 - // 否则会出现:编辑器里看似“已保存”,但下载/再次打开仍是旧文件。 - if (mode !== "view" && assetId && !authedUserId) { - return; - } - if (!baseUrl) { - setError("缺少 NEXT_PUBLIC_ONLYOFFICE_BASE_URL 配置,无法加载编辑器。"); - return; - } - if (!effectiveFileUrl) { - setError("缺少 fileUrl 参数。"); - return; - } - - setupOnlyOfficeInternalRequestRewrite(baseUrl, runtimeConfig.onlyofficeBaseUrlDesktop); - - // 说明:外网访问(例如 frp/隧道)时,OnlyOffice 文档服务器运行在本机 Docker 容器内,无法直接访问 - // document.url 里的 127.0.0.1/localhost。此时必须把 document.url 指向一个“容器可访问”的 Next Origin - //(onlyofficeProxyOrigin / onlyofficeProxyOriginWeb),让 Next 服务端代为回源下载。 - try { - const pageHost = window.location.hostname; - const isPageLocal = pageHost === "127.0.0.1" || pageHost === "localhost"; - const isPageRemote = !isPageLocal; - const u = new URL(effectiveFileUrl); - const isFileLocal = u.hostname === "127.0.0.1" || u.hostname === "localhost" || u.hostname === "host.docker.internal"; - if (isPageRemote && isFileLocal && !proxyOrigin) { - setError( - "外网访问时检测到 fileUrl 为本机地址(127.0.0.1/localhost),但未配置 onlyofficeProxyOriginWeb。请在 public/mnote-env.json 配置 onlyofficeProxyOriginWeb/onlyofficeCallbackOriginWeb(例如 http://host.docker.internal:3000 或当前 Docker 可达的主机 IP)。", - ); - return; - } - } catch { - // ignore - } - - if (!resolvedMode) return; - if (documentId && !permissionResolved) return; - if (resolvedMode !== "view" && assetId && !authedUserId) return; - - const scriptUrls = [ - `${baseUrl.replace(/\/$/, "")}/web-apps/apps/api/documents/api.js`, - `${baseUrl.replace(/\/$/, "")}/web-apps/apps/api/documents/api-all.js`, - `${baseUrl.replace(/\/$/, "")}/web-apps/apps/api/documents/editor.js`, - ]; - loadScriptCandidates(scriptUrls) - .then(async () => { - // 说明:api.js 的 onload 并不代表 DocsAPI/DocEditor 已完全就绪(在慢网/高负载时会出现空白页)。 - // 因此这里额外等待 DocEditor 挂载,避免偶发“白屏但无错误”的体验。 - await waitForDocEditorReady(120_000); - - const pluginConfigUrl = `${window.location.origin}/onlyoffice/plugins/agent-tools/config.json`; - - const config: any = { - width: "100%", - height: "100%", - document: { - fileType, - title: fileName, - url: resolvedFileUrl, - // 说明:key 用于 ONLYOFFICE 内部区分文档实例;应随 URL/文件名变化,避免缓存/冲突。 - // 这里改为“对同一附件尽量稳定”的 key,以提升远端场景的缓存命中与加载速度。 - key: docKey, - }, - documentType: targetDocType, - events: { - // 说明:用于 E2E 判定“文档已真正打开”,避免仅靠 iframe/canvas 误判。 - // 注意:部分版本回调名是 onDocumentReady,也有文档提到 onAppReady;两者都注册。 - onDocumentReady: () => { - (window as any).__MNOTE_ONLYOFFICE_READY__ = true; - }, - onAppReady: () => { - (window as any).__MNOTE_ONLYOFFICE_READY__ = true; - }, - onError: (e: unknown) => { - const msg = (() => { - try { - if (typeof e === "string") return e; - return JSON.stringify(e); - } catch { - return String(e); - } - })(); - setError(msg); - }, - }, - editorConfig: { - mode: mode === "view" ? "view" : "edit", - lang: "zh-CN", - // 说明:ONLYOFFICE 文档服务器会通过 callbackUrl 回传保存事件, - // 我们在 /api/onlyoffice/callback 中接收并回写到 Supabase Storage。 - callbackUrl: (() => { - return buildOnlyOfficeCallbackUrl({ - callbackOrigin, - proxyOrigin, - windowOrigin: window.location.origin, - assetId, - userId: authedUserId, - }); - })(), - customization: { - feedback: { visible: false }, - // 说明:启用 forcesave 后,用户点击 ONLYOFFICE 的“保存”会触发 status=6 回调, - // 从而立即把最新版本写回主存储(无需等待关闭文档触发 status=2)。 - forcesave: true, - }, - plugins: { - autostart: [MNOTE_AGENT_PLUGIN_GUID], - pluginsData: [pluginConfigUrl], - }, - }, - }; - - // 兜底:有些版本不会触发 onDocumentReady/onAppReady,这里用轮询判断“编辑器 DOM 已出现” - // 来设置 ready flag,保证远程 E2E 判定稳定。 - // 只读权限兜底:即使外部传了 mode=edit,也强制改为 view,且禁用编辑写回。 - try { - config.document = config.document || {}; - config.document.permissions = { - ...(config.document.permissions || {}), - edit: resolvedMode !== "view", - download: !resolvedDisableDownload, - print: !resolvedDisableDownload, - copy: !resolvedDisableCopy, - }; - config.editorConfig = config.editorConfig || {}; - config.editorConfig.mode = resolvedMode === "view" ? "view" : "edit"; - config.editorConfig.customization = config.editorConfig.customization || {}; - config.editorConfig.customization.forcesave = resolvedMode !== "view"; - } catch { - // ignore - } - - (window as any).__MNOTE_ONLYOFFICE_READY__ = false; - const readyDeadline = Date.now() + 120_000; - const timer = window.setInterval(() => { - const root = document.querySelector("#onlyoffice-frame") as HTMLElement | null; - const body = document.body as HTMLElement | null; - const count = - (root ? root.querySelectorAll("iframe,canvas").length : 0) + - (body ? body.querySelectorAll("iframe,canvas").length : 0); - if (count > 0) { - (window as any).__MNOTE_ONLYOFFICE_READY__ = true; - window.clearInterval(timer); - } else if (Date.now() > readyDeadline) { - window.clearInterval(timer); - } - }, 500); - - fetch("/api/onlyoffice/sign", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ config }), - }) - .then(async (r) => { - if (!r.ok) { - const payload = await r.json().catch(() => null); - throw new Error(payload?.error ?? "OnlyOffice 签名失败"); - } - const { token, documentToken, editorConfigToken } = (await r.json()) as { - token: string | null; - documentToken: string | null; - editorConfigToken: string | null; - }; - if (canceled) return; - - // 兼容不同 ONLYOFFICE 配置:有些版本/配置会校验 document.token/editorConfig.token。 - if (token) config.token = token; - if (documentToken) { - config.document = config.document || {}; - config.document.token = documentToken; - } - if (editorConfigToken) { - config.editorConfig = config.editorConfig || {}; - config.editorConfig.token = editorConfigToken; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (canceled) return; - docEditorRef.current = new (window as any).DocsAPI.DocEditor("onlyoffice-frame", config); - }) - .catch((err: Error) => { - // 说明:如果服务端强制 JWT 且你未配置 ONLYOFFICE_JWT_SECRET,会在这里失败或在编辑器内报错。 - setError(err.message); - }); - }) - .catch((err: Error) => { - // 说明:优先“无感回退”到备选 baseUrl(常见于本机 8081 未启动/外网转发不可达)。 - const hasNext = baseUrlIndex + 1 < baseUrlCandidates.length; - if (hasNext) { - setBaseUrlIndex((i) => i + 1); - return; - } - setError(err.message); - }); - - return () => { - canceled = true; - destroyEditor(); - }; - }, [ - assetId, - authedUserId, - baseUrl, - baseUrlCandidates.length, - baseUrlIndex, - callbackOrigin, - docKey, - effectiveFileUrl, - fileName, - fileType, - mode, - proxyOrigin, - resolvedFileUrl, - documentId, - permissionResolved, - resolvedDisableCopy, - resolvedDisableDownload, - resolvedMode, - targetDocType, - runtimeConfig.onlyofficeBaseUrlDesktop, - ]); - - const canForceSave = resolvedMode === "edit" && Boolean(assetId) && Boolean(docKey); - const triggerForceSave = async () => { - if (!canForceSave) return; - if (forceSaveState.busy) return; - setForceSaveState({ busy: true, message: "正在触发同步保存…", ok: null }); - try { - const r = await fetch( - buildOnlyOfficeForcesaveUrl({ - windowOrigin: window.location.origin, - assetId, - key: docKey, - }), - { method: "POST" }, - ); - if (!r.ok) { - const payload = (await r.json().catch(() => null)) as { error?: string } | null; - throw new Error(payload?.error ?? "触发失败"); - } - setForceSaveState({ busy: false, message: "已触发同步保存:请稍等 1~3 秒后再下载/刷新。", ok: true }); - window.setTimeout(() => { - setForceSaveState((s) => (s.ok ? { ...s, message: null } : s)); - }, 4000); - } catch (e) { - setForceSaveState({ - busy: false, - message: `同步保存失败:${(e as Error).message}`, - ok: false, - }); - } - }; - - if (!error && mode !== "view" && assetId && !authedUserId) { - return ( -
- 正在获取用户身份,用于写回保存… -
- ); - } - - if (error) { - return ( -
-

ONLYOFFICE 加载失败

-

{error}

-
- ); - } - - return ( -
-
- {canForceSave && ( -
- - {forceSaveState.message && ( -
- {forceSaveState.message} -
- )} -
- )} - -
- ); -} diff --git a/wolai-frontend/src/app/onlyoffice/page.tsx b/wolai-frontend/src/app/onlyoffice/page.tsx deleted file mode 100644 index ad75d35e..00000000 --- a/wolai-frontend/src/app/onlyoffice/page.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { Suspense } from "react"; -import OnlyOfficeClientPage from "./OnlyOfficeClientPage"; - -export const dynamic = "force-dynamic"; - -const ONLYOFFICE_EARLY_PATCH = ` -(function () { - try { - if (window.__MNOTE_ONLYOFFICE_EARLYPATCHED__) return; - window.__MNOTE_ONLYOFFICE_EARLYPATCHED__ = true; - - // 说明:ONLYOFFICE 在部分环境下会触发 DOMException(NotFoundError), - // 常见于 removeChild/insertBefore/replaceChild 等 DOM 操作。 - // 该异常会被 Next.js 视为“客户端异常”并白屏,但通常不影响编辑器继续使用。 - // 这里尽可能早地打补丁,避免异常冒泡导致整页崩溃。 - try { - var origRemoveChild = Node.prototype.removeChild; - Node.prototype.removeChild = function (child) { - try { - return origRemoveChild.call(this, child); - } catch (e) { - if (e && e.name === "NotFoundError") return child; - throw e; - } - }; - } catch (e) {} - - try { - var origInsertBefore = Node.prototype.insertBefore; - Node.prototype.insertBefore = function (newNode, referenceNode) { - try { - return origInsertBefore.call(this, newNode, referenceNode); - } catch (e) { - if (e && e.name === "NotFoundError") return newNode; - throw e; - } - }; - } catch (e) {} - - try { - var origReplaceChild = Node.prototype.replaceChild; - Node.prototype.replaceChild = function (newChild, oldChild) { - try { - return origReplaceChild.call(this, newChild, oldChild); - } catch (e) { - if (e && e.name === "NotFoundError") return oldChild; - throw e; - } - }; - } catch (e) {} - - // 说明:记录错误,便于远程白屏时回溯(不会阻断页面)。 - try { - window.__MNOTE_ONLYOFFICE_ERRLOG__ = window.__MNOTE_ONLYOFFICE_ERRLOG__ || []; - var push = function (payload) { - try { - window.__MNOTE_ONLYOFFICE_ERRLOG__.push(payload); - window.localStorage.setItem("mnote_onlyoffice_last_error", JSON.stringify(payload)); - } catch (e) {} - }; - - window.addEventListener( - "error", - function (e) { - try { - push({ - kind: "error", - message: String(e.message || ""), - filename: String(e.filename || ""), - lineno: Number(e.lineno || 0), - colno: Number(e.colno || 0), - name: String((e.error && e.error.name) || ""), - }); - } catch (err) {} - }, - true - ); - - window.addEventListener( - "unhandledrejection", - function (e) { - try { - var r = e.reason; - push({ - kind: "rejection", - message: String((r && (r.message || r)) || ""), - name: String((r && r.name) || ""), - }); - } catch (err) {} - }, - true - ); - } catch (e) {} - } catch (e) {} -})(); -`.trim(); - -export default function OnlyOfficePage() { - return ( - - 正在加载 ONLYOFFICE… -
- } - > -