diff --git a/.gitignore b/.gitignore index aa23d61..4d771a1 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,5 @@ node_modules/ /assets/papirus_icons-master.zip /Gitea.md /.work/ -/TyporaRef \ No newline at end of file +/TyporaRef +/oauth.env diff --git a/PRD.md b/PRD.md index f00299d..decdcfa 100644 --- a/PRD.md +++ b/PRD.md @@ -22,12 +22,12 @@ Ely Office 提供八个桌面应用。用户可以写文档、计算表格、制 | ElyTable | 电子表格编辑与计算 | LOK | XLSX、ODS、CSV | | ElyShow | 演示文稿编辑与放映 | LOK | PPTX、ODP | | ElyPDF | PDF 阅读、编辑、批注、表单和页面处理 | MuPDF;OCR 复用 Tesseract | PDF | -| ElyMail | 邮件收发、本地存储和搜索 | MailCore 2,加本地同步与存储 | 邮件、附件、草稿 | +| ElyMail | 邮件收发、本地存储和搜索 | Rust 的 async-imap、async-pop、mail-send、mail-parser、mail-builder,加本地同步与存储 | 邮件、附件、草稿 | | ElyTree | Git 图形化版本管理 | Git CLI | 本地 Git 仓库 | | ElyMark | 本地 Markdown 编辑 | Ely-GPUI-Components 的 Markdown 文档组件,markdown-rs 解析;参考 MarkText、ColaMD、Markion | Markdown 与本地附件 | | ElyAgent | 独立的 Agent 配置与会话管理 | 嵌入的 Kigi CLI Rust 代码 | 配置、会话记录、运行状态 | -LOK、MuPDF 和 GPUI 是已确定的方向。MailCore 2、Markdown 文档组件、邮件正文渲染和下文的发行默认值,是本 PRD 采用的工程基线,仍须通过三端接入验收。不得把未测试的接口写成已支持功能。 +LOK、MuPDF 和 GPUI 是已确定的方向。邮件协议 crate、Markdown 文档组件、邮件正文渲染和下文的发行默认值,是本 PRD 采用的工程基线,仍须通过三端接入验收。不得把未测试的接口写成已支持功能。 ElyAgent 参考 Magpie 的独立工具定位,但不要求复制其全部功能。它不是其他七个应用的聊天侧栏,也不是整个套件的控制中枢。[S19] @@ -375,16 +375,18 @@ XFA 动态表单、任意嵌入脚本执行、专业印前分色及 PDF 到 Offi ### 12.1 定位与后端 -提供多账号邮件客户端,不建设邮件托管服务。MailCore 2 负责 IMAP、POP、SMTP 和邮件解析等能力。Ely 负责账号流程、本地存储、搜索、同步队列和界面。MailCore 2 官方列出 C++ API 与三端支持,但发行前仍须验证实际构建和依赖。[S10] +提供多账号邮件客户端,不建设邮件托管服务。协议与 MIME 用 Rust crate(用户 2026-10-19 定,取代 MailCore 2):async-imap 收 IMAP,async-pop 收 POP3,mail-send 发 SMTP,mail-parser 读 MIME,mail-builder 写 MIME。它们在应用进程的后台任务里运行,不另起工作进程。Ely 负责账号流程、本地存储、搜索、同步队列和界面。 -默认布局为账号与文件夹、邮件列表、阅读区。写信可在独立窗口打开。HTML 正文和富文本撰写区不用 CEF 与 Electron,渲染方式随 ElyMail 任务另定;不能把正文 HTML 解析当成完整页面渲染。 +默认布局为账号与文件夹、邮件列表、阅读区。写信可在独立窗口打开。HTML 正文与写信不用 CEF 与 Electron(用户 2026-10-19 定):正文以 html5ever 解析,净化后按块的子集由 GPUI 绘制(段落、标题、列表、引用、表格、图片、链接、粗斜体与颜色),脚本、定位、浮动与样式表之外的版式不画,不当作完整页面渲染;写信用组件库的 Markdown 文档组件,发出纯文本与由 markdown-rs 生成的 HTML 两部分。 + +内置登录:Gmail 与 Microsoft 走 OAuth;iCloud 不给第三方 OAuth,走应用专用密码向导(服务器 `imap.mail.me.com:993`、`smtp.mail.me.com:587`,无 POP3)[S10]。OAuth 的客户端 ID 在构建时注入,缺 ID 的构建里该登录写明不可用。 ### 12.2 功能要求 | 编号 | 功能 | 完成要求 | | --- | --- | --- | | MAIL-01 | 账号 | 支持多个 IMAP/SMTP 账号、POP3 账号、服务器配置、TLS 与连通性检查 | -| MAIL-02 | 授权 | 提供 Gmail 与 Microsoft 邮箱的 OAuth 流程;支持服务商允许的应用密码或自定义认证配置 | +| MAIL-02 | 授权 | 提供 Gmail 与 Microsoft 邮箱的 OAuth 流程,iCloud 的应用专用密码向导;支持服务商允许的应用密码或自定义认证配置 | | MAIL-03 | 邮箱视图 | 支持统一收件箱、账号独立视图、文件夹、未读、标记和附件筛选 | | MAIL-04 | 阅读 | 支持纯文本、HTML、内嵌图片、附件、原始邮件头与会话线程 | | MAIL-05 | 写信 | 支持收件人、抄送、密送、回复、回复全部、转发、签名、附件和纯文本/富文本撰写 | @@ -630,7 +632,7 @@ C/C++ 异常、原生错误与资源释放在 FFI 边界处理,不能跨 Rust ### 17.3 内容渲染 -不用 CEF,不用 Electron(用户 2026-10-11 定)。Mark 的编辑与预览全部由 GPUI 绘制(§14.1)。Mail 的 HTML 正文和富文本撰写的渲染方式随 ElyMail 任务另定,定前不写成已有。 +不用 CEF,不用 Electron(用户 2026-10-11 定)。Mark 的编辑与预览全部由 GPUI 绘制(§14.1)。Mail 的 HTML 正文按块的子集由 GPUI 绘制,写信用 Markdown 文档组件(§12.1)。 Ely 主题 token 用于内容区。键盘、焦点、选择、缩放、右键菜单和输入法进入统一适配规则。对话框和应用菜单由 GPUI 展示。 @@ -766,7 +768,6 @@ Doc、Table、Show 的 PDF 输出由 LOK 完成,不要求安装 ElyPDF。ElyMa | --- | --- | | MuPDF | 按锁定版本的 AGPL 条款提供对应源码、修改、集成和构建资料;不把公开 UI 仓库当成已完成合规 [S8][S9] | | LibreOffice/LOK | 保留 MPL 2.0 及附带依赖的相应声明;发布对受约束文件的修改与所需资料 [S5] | -| MailCore 2 | 保留其许可、版权和免责条款,并检查其传递依赖 [S24] | | Git | 作为独立程序分发并遵守其 GPLv2 等实际适用条款;提供所发行版本的源码取得方式 [S23] | | Markdown | 保留 markdown-rs、RaTeX(含随带字体)、mermaid-rs-renderer 及其依赖的许可;逐项检查实际发行包 [S14][S15][S16] | | OCR、字体、词典和媒体组件 | 记录每个资源的来源、版本和再分发条件,不从系统目录随意拷贝受限资源 | @@ -841,7 +842,7 @@ UI 自动测试、内核测试和手工交互测试互相补充。三端的输 | LOK 三端嵌入 | Doc/Table/Show 的编辑、保存、中文输入与窗口操作录像及日志 | 修复适配或更新设计决策;不能只交付查看器 | | LOK 高级功能 | 母版、放映、图表、修订、输入法、打印和无障碍的接口映射及用例 | 补齐桥接或维护必要上游补丁,不跳回上游原生 UI | | MuPDF 编辑 | 字体、对象修改、表单、删除及保存的样本结果 | 修复对象编辑与输出;不得改为视觉覆盖冒充编辑 | -| MailCore 2 | 三端可重复构建、TLS、OAuth、同步和 MIME 测试 | 处理依赖及适配问题;替换后端必须显式修订选型 | +| 邮件协议 crate | 三端 TLS、OAuth、IMAP 与 POP3 同步、SMTP 发送和 MIME 测试 | 修复或换 crate;换后端须显式修订选型 | | Markdown 原生编辑 | 可视、源码、预览的输入、焦点、缩放、输入法和辅助技术测试 | 未通过不能以 CEF、WebView 或 Electron 代替 | | Markdown 原文保留 | 无编辑保存、模式切换、局部修改和外部冲突的字节级差异 | 修复映射与保存;未知语法回到原文模式,不删除 | | 邮箱服务注册 | 正式 OAuth 配置、适用审核状态与用户流程 | 不将未获许可的内置登录描述为可用 | @@ -895,8 +896,8 @@ UI 自动测试、内核测试和手工交互测试互相补充。三端的输 [S9] GNU Affero General Public License 第 3 版原文。 `https://www.gnu.org/licenses/agpl-3.0.en.html` -[S10] MailCore 2 官方介绍,协议、API 与平台范围。 -`https://etpan.org/mailcore2.html` +[S10] Apple,iCloud 邮件用于其他邮件应用的服务器设置。 +`https://support.apple.com/en-us/102525` [S11] Google,Gmail IMAP/POP/SMTP OAuth 2.0 与权限要求。 `https://developers.google.com/workspace/gmail/imap/xoauth2-protocol` @@ -935,8 +936,5 @@ UI 自动测试、内核测试和手工交互测试互相补充。三端的输 [S23] Git 官方 COPYING 文件。 `https://raw.githubusercontent.com/git/git/master/COPYING` -[S24] MailCore 2 官方 LICENSE 文件。 -`https://raw.githubusercontent.com/MailCore/mailcore2/master/LICENSE` - [S25] Typora 官方快捷键说明;对照的程序为 Typora 1.14.10 Windows 安装包(本机 `TyporaRef`,不入库)。 `https://support.typora.io/Shortcut-Keys/` diff --git a/agents.md b/agents.md index a083d56..6c372f3 100644 --- a/agents.md +++ b/agents.md @@ -14,7 +14,7 @@ | Doc / Table / Show | LibreOfficeKit,独立工作进程 | | Show 媒体 | 各平台原生:macOS AVFoundation,Windows Media Foundation,Linux GStreamer(LOK 不放媒体) | | PDF | MuPDF(C 接口),Tesseract OCR,独立工作进程 | -| Mail | MailCore 2 + SQLite;正文渲染随其任务另定(不用 CEF 与 Electron) | +| Mail | async-imap、async-pop、mail-send、mail-parser、mail-builder(`crates/ely-mail-engine`,应用进程内的后台任务,无 worker)+ SQLite(FTS5);正文以 html5ever 解析、净化后按块的子集由 GPUI 绘制;写信用 `documents::MarkdownDocument`,发出纯文本与 HTML;Gmail、Microsoft 走 OAuth,iCloud 走应用专用密码 | | Tree | Git CLI,参数数组启动 | | Mark | 组件库 `documents::MarkdownDocument`(GPUI,原文即文本,无序列化);markdown-rs 解析与导出 HTML,RaTeX 公式,mermaid-rs-renderer 图;PDF 经 `ely-pdf-worker` 由 MuPDF 排版 | | Agent | Kigi CLI 的 Rust 代码直接嵌入(不用 ACP),沿用其提供商登录与模型配置 | @@ -23,7 +23,7 @@ ```text apps/ 八个应用,各自一个二进制 -crates/ ely-ui ely-platform ely-ipc(应用与引擎 worker 之间的帧);ely-files ely-lok ely-office(文档应用共用的外壳:标签、会话、保存、关闭询问、冲突、密码;引擎为 LOK 或 MuPDF)ely-pdf-engine(MuPDF worker 与客户端);ely-mail-engine 随其任务建 +crates/ ely-ui ely-platform ely-ipc(应用与引擎 worker 之间的帧);ely-files ely-lok ely-office(文档应用共用的外壳:标签、会话、保存、关闭询问、冲突、密码;引擎为 LOK 或 MuPDF)ely-pdf-engine(MuPDF worker 与客户端);ely-mail-engine(邮件协议、同步与本地存储) reference/ 上游源码,只读,不属于本项目,不审查 engines/ 下载的二进制与引擎构建产物,不入库,不审查;mupdf-src 是 reference/mupdf 加 patches/mupdf 的构建副本 patches/ 对上游的补丁,随源码发行(libreoffice、mupdf) @@ -149,7 +149,7 @@ packaging/icons/masters.py 写出 assets/icons 的全部 SVG 母版(图标 packaging/icons/build.sh 由 assets/icons 的母版渲染 icns、png、ico(只在 macOS) tests/scripts/icons.sh 八个图标齐全、未过期,macOS 为 squircle,其余无底形 tests/scripts/release-macos.sh dist 里首批六个 .app:以访达的方式打开文件(启动与运行中),引擎都取自包内,带齐许可,不含系统会登记的内嵌包,每个二进制要求的系统不高于应用所写;源码包齐全 -tests/engines/*.sh 引擎探测(LOK、MuPDF、libetpan);LOK 探测本构建的内核,不挂载官方镜像 +tests/engines/*.sh 引擎探测(LOK、MuPDF);LOK 探测本构建的内核,不挂载官方镜像 scripts/inert-lo.sh 路径.app 构建出的 LibreOffice.app 去掉文档类型、URL 类型、快速查看与聚焦插件并注销(build-libreoffice.sh 调用) scripts/extract-official-lo.sh 用 7zz 把官方镜像的 Frameworks 与 Resources 解到 engines/lo-official(不挂载,无包,“未打补丁即拒绝”的单测用) tests/scripts/lo-unregistered.sh “打开方式”里没有 LibreOffice,构建出的包不声明任何文件类型 @@ -206,7 +206,6 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely - 2026-10-04:ElyTable 公式、计算、名称(C09.2,TABLE-03 至 06)。公式语法为 Excel A1(用户定):`Sheet2!A1`、逗号分隔参数,由安装层 `packaging/libreoffice/ely.xcd` 设定;载入选项 `Language=en-US,DeviceFormFactor=desktop`,内核界面与数字区域都是 en-US(小数点、月/日/年输入)。单元格内编辑由内核完成:键入时内核给出匹配的函数(`editor::CompletionMenu` 显示在单元格下,上下键选,Tab 补全,经 `completeFunction` 入有序队列),参数提示(`editor::SignatureHelp`,取内核的用法文字),点选单元格插入引用并按内核颜色画框;内核只为点选的引用与再次编辑(F2)的已有公式画框,新键入的引用不画(同桌面版)。第一下 Esc 关提示,第二下取消(同内核)。语法错误的公式照原样输入,状态栏按界面语言解释当前格的错误(内核英文错误串换成界面文案,同统计);循环引用同此提示。“公式”菜单:自动计算开关(关时状态栏显示手动计算)、重新计算、全部重新计算、定义名称(名称、引用位置默认为所选区域的绝对引用、范围为工作簿或某表;名称先经内核判定)、名称列表(改引用、转到所命名的绝对区域、删除,失效引用标出;内核给出每个名称的区域,`.uno:SelectedRange` 给所选区域)、追踪错误、追踪引用与从属单元格、移去箭头。名称框可直接输入名称跳转,跳转后显示该名称。手动计算在 ods、xlsx、xls、fods 存后重开都保持。 - 2026-10-04:ElyTable 格式、数据整理、规则(C09.3,TABLE-07 至 09)。补丁 `0016-lok-calc-formats-and-conditions.patch`(数字格式、条件格式、两处 sdi 参数)、`0017-lok-calc-filter-validation-columns.patch`(筛选、验证、分列)、`0018-lok-calc-reads-requests-search.patch`(读值、请求、查找全部):数字格式按代码设(`.uno:NumberFormat` 的 `FormatCode`),`.uno:NumberFormatCode` 读当前格代码与类别(常规按区域块首判定)或校验代码;标准筛选收 JSON 条件(`.uno:DataFilterStandardFilter` 的 `Query`,含按列取值),`.uno:AutoFilterValues` 照内核自己的筛选菜单给出条目、他列隐藏与本列勾选;数据验证收 JSON 规则(`.uno:Validation` 的 `Rule`),`.uno:Validation` 读当前格规则,`.uno:DataSelectValues` 给序列;条件格式收 JSON(单元格规则配内核单元格样式、色阶、数据条、按键删除,`cellsh.sdi` 的同名槽也加参数),`.uno:ConditionalFormats` 列出本表;分列收 `Options`(ScAsciiOptions 串);`.uno:SelectedRange` 另给所选区域内数据区的四角、首行文字与内核的标题判定。LOK 下内核不再弹自己的筛选菜单与序列下拉,改发 `LOK_CALLBACK_TOOLTIP`(`autofilter`、`datalist`,带列行),无效输入发 `validityerror`(标题、内容、样式、是否接受;接受与否取内核对话框的默认:信息接受,警告与停止拒绝),Ely 以 `ely_lok::Request` 接收后自建菜单与提示。查找全部按格报矩形(内核原把相邻命中并成一块,计数偏少)。查找替换移入 `ely-office`(`Finding`,ElyDoc 与 ElyTable 共用,`Host::search_leaves` 供 ElyDoc 记返回位置)。“粘贴为值/公式”只在系统剪贴板仍是本视图上次的复制时进行(内核自己的单元格剪贴板;Ely 复制的 HTML 末尾带一个进程内唯一的标记注释,以标记而不是文字识别,同样文字的外来复制不算);其他内容(含别的工作簿复制的单元格)明确拒绝并提示改用粘贴:内核无法从外来内容单取值或公式。排序最多三个关键字(内核限制)。右侧格式面板(数字、对齐、边框、填充)、“数据”菜单(排序、筛选、删除重复项、分列、查找替换、粘贴为值或公式、数据验证)、“格式”菜单(格式面板开关、条件格式)。组件库 `SortBuilder`、`FilterBuilder` 的文案 a4bf696 起接 i18n(原为英文)。 - 2026-10-04:右侧属性栏宽取 `theme.sheet_size()`(380),左栏仍是 `sidebar_width`。`PropertyGrid` 名称列固定 120,240 宽时编辑器只剩约 76。颜色经色块弹出 `ColorPalette`,一次点击一条命令。 -- 2026-10-01:MailCore 2 的子模块 libetpan 过旧,改钉 libetpan master `4d47862`。 - 2026-10-01:每个应用的全部数据在 `<本机数据目录>/ElyOffice//`,路径由 `ely_platform::data_dir` 给出。卸载只动这里,且先问。用户文档永不碰。 - 2026-10-04(用户定):macOS 包标识前缀 `ely.zacharyzhang.office`(`packaging/macos/bundle.sh`);发布者 ZacharyZhang,网址 zacharyzhang.com(macOS `NSHumanReadableCopyright`,Windows 卸载项 `Publisher` 与 `URLInfoAbout`;Linux 桌面文件无此字段)。界面语言英文与简体中文,以后再加。拼写词典只随英文:LibreOffice 的词典库没有中文(中文不用 Hunspell 拼写)。内核界面语言固定英文,函数名照英文;ElyTable 把内核英文的选区统计(Sum、Average 等七个固定名)换成界面语言,数值照内核。批注与修订的作者取各用户的账户全名(无全名用登录名,与内核自身次序一致),由补丁 0013 的 `setOption("author")` 在 worker 启动时设定;作者设置页随设置任务接入。邮件合并字段写法 `«列名»` 由我定(用户交我决定)。 - 2026-10-01:更新清单是 JSON:`{"version", "assets": {"-": {"url", "sha256"}}}`,只走 https(本机回环除外)。检查、下载、校验在 `ely_platform::update`,从不静默安装。包管理器安装的 Linux 版本不自更新。2026-10-09 起(用户定:0.1.2 加自动更新):每个应用一份清单,`.json`,随发布上传,应用经 git.zacharyzhang.com 的 API 读最新发布(`api/v1/repos/ZacharyZhang-NY/ElyOffice/releases/latest`),在其文件里找 `.json`(`ely_platform::release::asset_url`;Gitea 没有 `releases/latest/download` 路径;`ELY_UPDATE_MANIFEST` 直接给清单地址,测试用);只在从 `.app` 运行时可用(开发构建没有);照 PRD §4.2 自动检查默认关闭:设置“通用”的“启动时检查更新”(通用设置 `check_updates`)打开后启动时查一次、查不到只记日志,应用菜单的“检查更新…”随时手动查(已是最新或查不到都以提示写明);有新版即在窗口上方显示组件库的 `UpdateBanner`(文案进内建中英目录,组件库 5d6efd4):横幅在标题栏下方(`Shell` 的页眉里,`Updates` 自己是一个视图);下载(进度)、校验散列、“重新启动以更新”只走正常的退出流程(有未保存的照常询问;取消即什么也不装,按钮仍在);应用真正退出时(`on_app_quit`)以本程序的安装模式(`--ely-install-update dmg 包 进程号`,`ely_ui::run` 最先识别)起一个独立进程:等本进程结束,`ely_platform::install` 挂载 dmg、把同名 .app 拷到旁边再换掉原包(换上前失败即放回原包),无论成败都 `open` 该包;失败的原因写在数据目录 `updates/failed.txt`,重开的应用以提示写明。下载完成只归属于它下载的那个版本;下载中或已下载时再查不覆盖。`packaging/macos/release.sh` 为每个应用写出清单(版本、本机架构的 dmg 地址与 SHA-256),dmg 里另有指向“应用程序”的替身,可直接拖入。新包换上即为装好,旧副本删不掉只记警告。`tests/scripts/update.sh`:装好的 ElyMark 在默认设置下启动不发任何请求;按“检查更新…”后从本机服务的 9.9.9 下载;有未保存的文字时“重新启动以更新”先问,取消则原包不动,“不保存”后装上新版并重新打开(新版本身的进程在跑)。 @@ -321,3 +320,4 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely - 2026-10-18:Word 公式(R8.2,Gitea #9)。附件 math.docx 的六类公式逐类核对(本机无 Word,以文件所写为准):键盘输入、OMML(内核转为公式对象)、MathType(OLE,显示随带的 WMF 预览)、上下标、图片原本就对;差别在两处根因。其一,运行的 `w:position`(升降)原先只给文字,图片与 OLE 公式所在的运行失去它(25 张图片与一个 ½ 浮在行上):补丁 `0049-writer-inline-objects-move-with-their-run.patch` 把它作为固定升降写到对象的锚字符上(按锚字符自己的字号换算百分比,不扩展到其后的文字),版面让“作为字符”的对象随锚字符的固定升降移动(vertAlign 的自动上下标不动对象,同原样);各导出本就写出锚字符的升降,DOCX 往返仍为 `w:position`。其二,EQ 域原先只认 `\o`、`\s\do` 与注音,其余(如分数 `\f`)整个消失:补丁 `0050-writer-eq-field-parser.patch`(读法)与 `0051-writer-eq-fields-as-formulas.patch`(导入)把 EQ 域转为公式对象:`\f` 分数、`\r` 根式、`\s` 上下标、`\b` 括号、`\l` 列表、`\a` 阵列、`\i` 积分与求和求积;`\x` 与 `\o` 只留内容(无框、无叠印),`\d` 只前移,`\ai`、`\di` 只是行距(不升降),开关不分大小写;每个命令字符保留其运行的上下标(Word 照此画),同一段上下标里字样变了仍是一个上下标;斜体字母写成标识符(StarMath 与 OMML 都画成斜体,DOCX 往返不丢),斜体的数字与符号写成 `ital` 文字,存为 DOCX 后成正体(OMML 无此写法,已知限制);没有底的上下标以空文字为底(空的底在 OMML 里是占位符,重开成 ``);公式取域的字号;读不通的命令以其域代码显示为文字,不消失。只有整个域是 `\s\do N(文字)` 时照原样成为下移的文字,其余都经读法(原先小写的 `\s\do` 读到第一个括号即止,其后丢失)。`client/tests/word.rs` 以 `tests/fixtures/office/word.docx`(由 `.work/r8/gen/eq.py` 生成,不入库)核对公式文本(含开头的空格、大写开关、读不通的域、字样变化的下标、`\ai`、`\di`、小写 `\s\do` 的尾部与嵌套)、DOCX 往返后的斜体、下标与无占位符、锚字符的升降与图块里图片低于文字(去掉版面的升降即失败)。审查三轮:第 3 轮的四处已修并以上述测试核对,未再审查(每条至多三轮)。仍与 Word 不同:编辑区里公式与图片四周有内核的辅助框线(`ShowBoundaries`,视图辅助,不印出);DOCX 另存后重开行距变宽,因内核导出改写文档默认字体与段距,与公式无关。 - 2026-10-18:工作表过多时标签条(R8.3,Gitea #8)。组件库 `navigation::Tabs` 横排时可横向滚动(滚轮同样),标签可窄于自然宽度;所选标签在所选或标签条宽度变了时滚入视口,在绘制时以刚排好的宽度判定(原先只在首帧滚一次,其后右栏打开、标签条变窄,所选标签被挤出视口);手动滚动保留到二者之一再变;横排的标签至多与标签条同宽,名字过长以省略号结尾(组件库 e56a343、3a68d1b、7d704d9,同批带入上游列外壳拖动的四个提交)。ElyTable 的标签条随之收缩,添加按钮留在其旁;标签条的状态按工作簿各记一份(元素 id 带标签号),切到另一工作簿时其所选标签照样滚入。`navigation/tests/tabs.rs` 以 30 个标签核对:首帧滚到所选、滚轮回到第一个、新选的滚入、变窄后所选仍在末端(去掉按宽度判定即失败)、长名的所选标签整个在标签条内(去掉宽度上限即失败);ElyTable 的 `tests/strip.rs` 以 `sheets::strip` 画两个各有 30 张表、都选第 30 张的工作簿,滚回开头后来回切换,所选标签每次都在标签条内(两个工作簿共用一个 id 即失败)。Windows 真窗口(`.work/r8/strip3.png`):30 张表、第 30 张为当前,1100 宽时标签条末端为它。 - 2026-10-18:ElyMark 公式(R8.4,用户定两项都做)。行内公式照块公式由 RaTeX 排版(原只显示着色的 TeX 原文):gpui 的文本里放不进图片,组件库 `render/math_text.rs` 把每个行内公式换成等宽的不断行空格(U+00A0,按字体量出个数),排好的公式居中画在其上,行高随最高的公式撑开;空格串与词一样整个折到下一行,画在其所在行(折行处的序号 gpui 读作上一行末);按下与右键所在的字节经换算读回公式的原文;排不出的公式照旧显示 TeX 原文,其后以危险色写出 RaTeX 给的原因与位置,记警告(不含公式)。`\(…\)` 读作行内公式,`\[…\]` 读作块公式(ChatGPT 等常写的分隔符;GitHub 不认,Typora 默认不认):解析前逐字节把成对的分隔符换成 `$`(长度不变,位置照旧对得上)。成对只在同一段落、标题或单元格里(跨列表项、CR 空行不成对);不跨代码、公式、HTML 标签、链接地址与引用标签、图片、脚注引用,``、`` 之间(含嵌套;交叉闭合照可视的读法关闭与重开)的文字照原文;`\\(` 不换。公式里与段落里另有的 `$`(含 `\$`)解析时暂写成文本里没有的控制字节,解析后还原,不与换上的 `$` 配对;相邻的两个公式之间暂插一个空格,解析后去掉,仍是两个公式。公式节点的范围是整对分隔符,值是写下的 TeX 原文。独占一段的 `\[…\]` 为块公式(段首可有一至三个空格)。组件库公开 `documents::markdown_tree`,ElyMark 的 HTML、PDF、纯文本导出都经它解析(`markdown_options` 不再公开),纯文本里独占一段的公式照块公式写其 TeX。同时查出(组件库 e095f06):RaTeX 的 SVG 以 pt 定尺寸,usvg 按 px 量墨迹,两者差 4/3,每个公式的画面右侧与下方多出约四分之一的空白(块公式因此偏左)、导出的公式比正文大 4/3;现生成时即把根改为 px。`markdown/tex/tests.rs` 与 `tree/tests.rs` 核对分隔符的各种情形(相邻、`\$`、链接地址、``、跨块),`math_text.rs` 的窗口测试核对公式排进并读回原文、折到第二行的公式画在第二行首(改回按起点定位即失败)、排不出的公式写出原因(组件库 a20e712、5fba7b3、de961eb),`math/tests.rs` 核对画面与公式同宽(改回 pt 即失败),ElyMark 的 `visual.rs` 核对导出与纯文本。Windows 真窗口(`.work/r8/mark/visual2.png`):`$…$`、`\(…\)` 排在行内,`\[…\]` 居中成块,行内代码与转义的 `\\(` 照原文。 +- 2026-10-19(用户定):ElyMail 的后端改为 Rust crate,取代 MailCore 2(其 Windows 构建是 VS 2013 工程加 2015 年前后的预编译依赖,POP 无 OAuth2):async-imap、async-pop、mail-send、mail-parser、mail-builder,在应用进程的后台任务里运行,不另起 worker;MailCore 2 与 libetpan 的取码、构建与探测随之删去。正文以 html5ever 解析、净化后按块的子集由 GPUI 绘制,不当作完整页面;写信用 Markdown 文档组件,发出纯文本与 HTML。Gmail 与 Microsoft 走 OAuth(授权码加 PKCE,本机回环),客户端 ID 在构建时注入:`ELY_GOOGLE_CLIENT_ID`、`ELY_GOOGLE_CLIENT_SECRET`、`ELY_MICROSOFT_CLIENT_ID`,本机写在项目根的 `oauth.env`(不入库),CI 为仓库机密;缺 ID 的构建里该登录写明不可用,不退回别的方式。iCloud 不给第三方 OAuth,走应用专用密码向导(`imap.mail.me.com:993`、`smtp.mail.me.com:587`,无 POP3)。注册由用户做(Google Desktop 客户端,Entra 公共客户端,委托权限 IMAP、POP、SMTP 与 offline_access)。 diff --git a/docs/licenses.md b/docs/licenses.md index 1745e44..884005e 100644 --- a/docs/licenses.md +++ b/docs/licenses.md @@ -32,5 +32,5 @@ Ely Office 的自有代码以 AGPL-3.0-or-later 发布,全文在仓库根的 ` ## 未尽之处 - LibreOffice 的整份源码(数 GB)不随发行,只给钉住的上游提交与我们的补丁;内核构建时下载的外部库(`external/tarballs`)是否需随包提供源码,待法律审查。 -- 第二批的 MailCore 2 与 ElyAgent 依赖随第二批补上。 +- ElyAgent 的依赖随第二批补上;ElyMail 的 crate 由 `rust-crates.txt` 收集。 - Windows 与 Linux 的打包脚本未在本机运行,其许可收集未验证。 diff --git a/scripts/build-libetpan.sh b/scripts/build-libetpan.sh deleted file mode 100755 index 828953d..0000000 --- a/scripts/build-libetpan.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env bash -# Builds the pinned libetpan into engines/libetpan. -set -euo pipefail - -root=$(cd "$(dirname "$0")/.." && pwd) -src=$root/reference/libetpan -prefix=$root/engines/libetpan -work=$root/engines/libetpan-src -pin=$(git -C "$src" rev-parse HEAD) - -if [ "$(cat "$prefix/PIN" 2>/dev/null)" = "$pin" ] && [ -f "$prefix/lib/libetpan.a" ] && - grep -q "^#define HAVE_OPENSSL 1" "$prefix/BUILD-config.h" 2>/dev/null && - grep -q "^#define HAVE_JSON 1" "$prefix/BUILD-config.h" 2>/dev/null; then - echo "ok libetpan $pin" - exit 0 -fi - -ssl=${OPENSSL_DIR:-$(brew --prefix openssl@3)} || { - echo "build-libetpan.sh: set OPENSSL_DIR to an OpenSSL 3 prefix" >&2 - exit 1 -} -[ -f "$ssl/include/openssl/ssl.h" ] || { - echo "build-libetpan.sh: no OpenSSL headers under $ssl" >&2 - exit 1 -} -jobs=$(sysctl -n hw.ncpu 2>/dev/null || nproc) -rm -rf "$work" "$prefix" -rsync -a --exclude .git "$src/" "$work/" -cd "$work" -./autogen.sh --prefix="$prefix" --enable-static --disable-shared \ - --with-openssl="$ssl" --with-json --disable-db -for feature in HAVE_OPENSSL HAVE_JSON; do - grep -q "^#define $feature 1" config.h || { - echo "build-libetpan.sh: configure left out $feature" >&2 - exit 1 - } -done -make -j"$jobs" -make install -cp config.h "$prefix/BUILD-config.h" -echo "$pin" >"$prefix/PIN" -echo "built libetpan $pin" diff --git a/scripts/fetch-references.sh b/scripts/fetch-references.sh index d1fc72a..d2c26e6 100755 --- a/scripts/fetch-references.sh +++ b/scripts/fetch-references.sh @@ -36,7 +36,7 @@ fetch() { # name url rev [sparse paths...] echo "fetch $name $rev" } -fetch Ely-GPUI-Components https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git 5a53c08c844210ea80c6a92aec708e2de8ffa721 +fetch Ely-GPUI-Components https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git de961eb164c1c88797ea35f0860aba9d18c95e4f fetch zed https://github.com/zed-industries/zed.git 1a28cff4b409169bac058bca40dfbfeb7621d19b fetch libreoffice-core https://github.com/LibreOffice/core.git 8221e31b3ac356a1623c672912a3d2b492f7e3d1 \ include/LibreOfficeKit libreofficekit desktop/source/lib desktop/inc \ @@ -45,9 +45,6 @@ fetch mupdf https://github.com/ArtifexSoftware/mupdf.git 8ad45e92f0935d3d87f1db3 git -C "$ref/mupdf" submodule update --init --depth 1 --recursive fetch git https://github.com/git/git.git a018953688f1b10bddf91bff8747068f5f4746a4 fetch tesseract https://github.com/tesseract-ocr/tesseract.git db0ec62f81b0737fbbe184d8fea40af5738f8eef -fetch mailcore2 https://github.com/MailCore/mailcore2.git 1b48bab20b2492fa8ed3965abcbbe099d4ce9d92 -git -C "$ref/mailcore2" submodule update --init --depth 1 --recursive -fetch libetpan https://github.com/dinhviethoa/libetpan.git 4d478625c4c43721191ae1bd7aed387a66c28ec0 fetch marktext https://github.com/marktext/marktext.git b29cb5621e12cedb801c1cd886d9005159feaa2e fetch colamd https://github.com/marswaveai/ColaMD.git 5e0f3c3584bb771180afe1a4670efbaf28dee65b fetch magpie https://github.com/yetone/magpie.git e4c8316e3fa9d73683079df3f9142561265bc1bb diff --git a/tasks/TASKS.md b/tasks/TASKS.md index b9c84b9..d157817 100644 --- a/tasks/TASKS.md +++ b/tasks/TASKS.md @@ -131,14 +131,32 @@ 第二批发布(用户定,2026-10-08):首版不发布 ElyMail,首版的完整测试与打包不等它。 -- [ ] C12.1 MailCore 2 构建、C 垫片、Rust 绑定,三类协议连通与 MIME 解析 -- [ ] C12.2 本地存储:SQLite 索引、内容目录、消息身份(账号、邮箱、UIDVALIDITY、UID)、容量(§12.3、§18.1) -- [ ] C12.3 账号、TLS、OAuth 流程、应用密码(MAIL-01、02) -- [ ] C12.4 同步:头与 90 天正文、按需附件、队列、POP3、同步控制(MAIL-14,§12.3) -- [ ] C12.5 邮箱视图、阅读、会话线程(MAIL-03、04) -- [ ] C12.6 写信、草稿、发送队列与发送状态机(MAIL-05、06、07,§12.4,QA-17) -- [ ] C12.7 组织、搜索、附件(MAIL-08、09、10) -- [ ] C12.8 规则、联系人、迁移、通知(MAIL-11、12、13、15,QA-16、18) +用户 2026-10-19 定:后端改为 Rust crate(async-imap、async-pop、mail-send、mail-parser、mail-builder),取代 MailCore 2;Gmail 与 Microsoft 走 OAuth,iCloud 走应用专用密码;正文按块的子集由 GPUI 绘制,写信用 Markdown 文档组件;OAuth 客户端 ID 构建时注入(`oauth.env` 与 CI 机密,注册由用户做)。协议测试一律对本机的替身服务器(测试里按脚本应答的 IMAP、POP3、SMTP 与 OAuth 端点),不靠真网络;真服务器只在用户给出账号或 ID 后另验。界面 120 fps:界面线程不等网络与数据库,长列表只画可见行。 + +- [x] C12.0 选型修订:PRD §1.1、§12.1、§17.3、§21.2、§23 与 `agents.md` 改为上述选型;删去 MailCore 2 与 libetpan 的取码、构建与探测脚本;`oauth.env` 不入库;`references.sh` 随之(并修上游钉与工作区不一致的组件库钉) — R1 ok +- [ ] C12.1 `crates/ely-mail-engine` 与连通(MAIL-01 前半):IMAP、POP3、SMTP 经 TLS(rustls,系统根证书)或 STARTTLS 连接,密码与 XOAUTH2 认证,失败带服务器原话;替身服务器测三协议的登录、拒绝、列邮箱、取信、发信 +- [ ] C12.2 MIME(MAIL-04、05 的基础):mail-parser 读出头、纯文本、HTML、cid 内嵌图片、附件与原始头;mail-builder 写出多部分邮件、附件、Message-ID、In-Reply-To 与 References;样本 EML 往返测试 +- [ ] C12.3 本地存储(§12.3、§18.1、§18.3):SQLite 索引(账号、邮箱、消息身份为账号加邮箱加 UIDVALIDITY 加 UID、标记、头字段)、原文按内容存进内容目录、FTS5 全文索引(可重建)、结构版本与升级前备份、升级失败只读;各类大小可查 +- [ ] C12.4 凭据与 OAuth(MAIL-02,§18.2):密码与刷新令牌只进系统凭据库(不可用即只留到本次退出并写明);Gmail 与 Microsoft 的授权码加 PKCE、本机回环、换令牌与刷新;客户端 ID 编进构建,缺 ID 即写明不可用;刷新被拒转为“需重新登录”;替身授权端点测试 +- [ ] C12.5 账号(MAIL-01、02):ElyMail 主窗口与添加账号对话框:Gmail、Microsoft、iCloud(应用专用密码向导)、其他 IMAP/SMTP、POP3/SMTP;服务器、端口、TLS 方式、认证方式;连通性检查逐项报结果;多账号;删除账号先写明删去的本地缓存、草稿与凭据,不动服务端 +- [ ] C12.6 IMAP 同步(MAIL-14,§12.3,QA-18):文件夹列表与特殊用途;邮件头全部、正文只取 90 天内;UIDVALIDITY 变化即重建该邮箱;服务端删除与标记变化随之;后台任务执行,界面显示最后同步时间、下载范围、未缓存的数量与失败原因 +- [ ] C12.7 POP3 收取(MAIL-01,§12.3):按 UIDL 去重下载到本地收件箱,“保留服务端副本”默认开,不做伪文件夹同步 +- [ ] C12.8 同步控制(MAIL-14):手动刷新、运行期间定时同步、暂停、按文件夹完整离线下载 +- [ ] C12.9 邮箱视图(MAIL-03):账号与文件夹、邮件列表、阅读区三栏;统一收件箱与账号视图;未读、标记、带附件筛选;列表只画可见行,10 万封滚动不掉帧 +- [ ] C12.10 阅读(MAIL-04,§12.4):纯文本;HTML 经 html5ever 解析、净化后按块绘制;远程图片与跟踪资源默认拦截,可对当前邮件或发件人放行;cid 内嵌图片;附件列表;原始邮件头 +- [ ] C12.11 会话线程(MAIL-04,§12.3):按 Message-ID、In-Reply-To、References 组织,不按主题合并 +- [ ] C12.12 写信(MAIL-05):收件人、抄送、密送;回复、回复全部、转发;签名;附件;纯文本或 Markdown(发出纯文本与 HTML);可在独立窗口打开 +- [ ] C12.13 草稿(MAIL-06):编辑中存本地草稿,离线可恢复;可选同步到服务端草稿箱 +- [ ] C12.14 发送(MAIL-07,§12.4,QA-17):持久发件队列;草稿、排队、正在发送、服务器已接受、失败、结果未知、已取消七态;重试与取消;附件大小提示;“已发送”副本失败只重存副本;结果未知不自动重发,重启后亦然;离线发送与关闭应用时写明队列行为 +- [ ] C12.15 组织与待同步队列(MAIL-08,§12.3):已读未读、标记、移动、归档、删除与恢复,按服务端能力(MOVE、Gmail 标签、垃圾箱)映射;离线操作入待同步队列,服务端已变即核对,不无限重试 +- [ ] C12.16 搜索(MAIL-09,PERF-06):发件人、收件人、主题、正文、日期、附件文件名,结果注明只覆盖本地缓存;10 万封首屏 1 秒内 +- [ ] C12.17 附件(MAIL-10,§16):下载、离线保留、另存、预览;用 Ely 应用打开时先复制为工作副本;显示缓存状态;文件作为附件交给 ElyMail 即开新草稿(不自动发送) +- [ ] C12.18 规则(MAIL-11):按发件人、收件人、主题的本地规则,执行移动、标记或归档 +- [ ] C12.19 联系人(MAIL-12):本地地址补全与联系人,vCard 导入导出 +- [ ] C12.20 迁移(MAIL-13):EML 查看与导出、MBOX 导入导出,导入不写远程邮箱 +- [ ] C12.21 通知(MAIL-15):新邮件通知与静音,锁屏通知默认不显示正文 +- [ ] C12.22 设置与数据管理(§18.3):缓存容量与清理、邮件导出、账号删除;诊断不含正文与令牌 +- [ ] C12.23 验收(QA-16、17、18,PERF-06,120 fps):离线、发送故障、同步变化的整窗测试;滚动与键入的帧时间实测;三端构建与打包(`apps.tsv` 第二批) ## R1 首版:六个应用的打包(§2.2、§21,用户 2026-10-08 定) diff --git a/tasks/reviews/C12.0-r1.md b/tasks/reviews/C12.0-r1.md new file mode 100644 index 0000000..7ef22e9 --- /dev/null +++ b/tasks/reviews/C12.0-r1.md @@ -0,0 +1 @@ +PASS diff --git a/tests/engines/libetpan.sh b/tests/engines/libetpan.sh deleted file mode 100755 index 11d57a4..0000000 --- a/tests/engines/libetpan.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -# Pinned libetpan has every header MailCore 2 includes. -set -euo pipefail - -root=$(cd "$(dirname "$0")/../.." && pwd) -prefix=$root/engines/libetpan -work=$(mktemp -d) -trap 'rm -rf "$work"' EXIT -fail() { - echo "FAIL: $*" >&2 - exit 1 -} - -"$root/scripts/build-libetpan.sh" >"$work/build.log" 2>&1 || { - cat "$work/build.log" >&2 - fail "build failed" -} -[ -s "$prefix/lib/libetpan.a" ] || fail "no libetpan.a" -config=$prefix/BUILD-config.h -grep -q '^#define HAVE_OPENSSL 1' "$config" || fail "libetpan built without TLS" -grep -q '^#define HAVE_JSON 1' "$config" || fail "libetpan built without json-c" - -headers=$(grep -rhoE '#[[:space:]]*include[[:space:]]*' "$root/reference/mailcore2/src" | - sed -E 's/.*<(libetpan\/[^>]+)>.*/\1/' | sort -u) -[ -n "$headers" ] || fail "MailCore 2 sources include no libetpan header" -for header in $headers; do - [ -f "$prefix/include/$header" ] || fail "pinned libetpan lacks $header" -done -echo "libetpan: $(wc -l <<<"$headers" | tr -d ' ') headers MailCore 2 includes are present" diff --git a/tests/scripts/build-libetpan.sh b/tests/scripts/build-libetpan.sh deleted file mode 100755 index 7f1c1b8..0000000 --- a/tests/scripts/build-libetpan.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash -# build-libetpan.sh rebuilds stale caches and shows build errors. -set -euo pipefail - -root=$(cd "$(dirname "$0")/../.." && pwd) -src=$root/reference/libetpan -[ -d "$src/.git" ] || { echo "SKIP build-libetpan: reference/libetpan is not checked out"; exit 0; } -work=$(mktemp -d) -trap 'rm -rf "$work"' EXIT -fail() { - echo "FAIL: $*" >&2 - exit 1 -} - -mkdir -p "$work/scripts" "$work/engines/libetpan/lib" "$work/reference" "$work/tests/engines" -cp "$root/scripts/build-libetpan.sh" "$work/scripts/" -cp "$root/tests/engines/libetpan.sh" "$work/tests/engines/" -ln -s "$src" "$work/reference/libetpan" -git -C "$src" rev-parse HEAD >"$work/engines/libetpan/PIN" -touch "$work/engines/libetpan/lib/libetpan.a" -: >"$work/engines/libetpan/BUILD-config.h" - -if OPENSSL_DIR=/nonexistent "$work/scripts/build-libetpan.sh" >"$work/out" 2>&1; then - fail "a cached build without TLS was reused" -fi -grep -q "no OpenSSL headers under /nonexistent" "$work/out" || fail "the OpenSSL error was not shown" - -if OPENSSL_DIR=/nonexistent "$work/tests/engines/libetpan.sh" >"$work/out" 2>&1; then - fail "the probe passed on a TLS-less cached build" -fi -grep -q "no OpenSSL headers under /nonexistent" "$work/out" || fail "the probe hid the build error" - -printf '#define HAVE_OPENSSL 1\n#define HAVE_JSON 1\n' >"$work/engines/libetpan/BUILD-config.h" -OPENSSL_DIR=/nonexistent "$work/scripts/build-libetpan.sh" >"$work/out" 2>&1 || fail "a valid cache was not reused" -grep -q "^ok libetpan" "$work/out" || fail "a valid cache did not print ok" - -echo "build-libetpan: stale caches are rebuilt, errors reach stderr, valid caches are reused" diff --git a/tests/scripts/references.sh b/tests/scripts/references.sh index 04d638b..d5b84c5 100755 --- a/tests/scripts/references.sh +++ b/tests/scripts/references.sh @@ -4,7 +4,7 @@ set -euo pipefail root=$(cd "$(dirname "$0")/../.." && pwd) script=$root/scripts/fetch-references.sh -names="Ely-GPUI-Components zed libreoffice-core mupdf git tesseract mailcore2 libetpan marktext colamd magpie agent-client-protocol markion markdown-rs omarchy" +names="Ely-GPUI-Components zed libreoffice-core mupdf git tesseract marktext colamd magpie agent-client-protocol markion markdown-rs omarchy" fail() { echo "FAIL: $*" >&2 @@ -26,10 +26,7 @@ for header in \ libreoffice-core/include/LibreOfficeKit/LibreOfficeKit.h \ mupdf/include/mupdf/fitz.h \ mupdf/thirdparty/freetype/include/freetype/freetype.h \ - tesseract/include/tesseract/capi.h \ - mailcore2/CMakeLists.txt \ - libetpan/src/low-level/gmail/mailgmail.h \ - libetpan/src/low-level/activesync/mailactivesync.h; do + tesseract/include/tesseract/capi.h; do [ -s "$root/reference/$header" ] || fail "missing reference/$header" done @@ -37,12 +34,10 @@ want=$(printf '%s\n' desktop/inc desktop/source/lib include/LibreOfficeKit libre have=$(git -C "$root/reference/libreoffice-core" sparse-checkout list | sort) [ "$have" = "$want" ] || fail "libreoffice-core sparse set differs: $have" -for name in mupdf mailcore2; do - status=$(git -C "$root/reference/$name" submodule status --recursive) - [ -n "$status" ] || fail "$name lists no submodules" - bad=$(grep '^[-+U]' <<<"$status" || true) - [ -z "$bad" ] || fail "$name submodules off their pins: $bad" -done +status=$(git -C "$root/reference/mupdf" submodule status --recursive) +[ -n "$status" ] || fail "mupdf lists no submodules" +bad=$(grep '^[-+U]' <<<"$status" || true) +[ -z "$bad" ] || fail "mupdf submodules off their pins: $bad" grep -q "rev = \"$(pin_of Ely-GPUI-Components)\"" "$root/Cargo.toml" || fail "the component library pin differs from the workspace's"