Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
6118964
docs: PLAN 标记传递文本 v1 已落地、v2 实现中
rrbe Jun 25, 2026
4cba493
feat: 传递文本 v2 — 收文本服务端与手机端网页
rrbe Jun 26, 2026
1142191
feat: 传递文本 v2 — Mac 端收件箱、设置开关与只收模式
rrbe Jun 26, 2026
5106899
test: 收文本端到端冒烟 + 单测 + CI
rrbe Jun 26, 2026
6afb329
docs: PLAN 标记传递文本 v2 落地
rrbe Jun 26, 2026
6e083c2
feat: 传递文本 v2 — 主页内联接收开关、手机发送页已发送历史
rrbe Jun 26, 2026
5118c5a
feat: 传递文本收发并入独立二级页,主页回归功能选择页
rrbe Jun 26, 2026
53d4cb7
fix: 更新文本不再轮换 token,token 改回会话维度
rrbe Jun 26, 2026
20c345f
fix: 输入法组合期间不重写文本视图,修中文吞字
rrbe Jun 26, 2026
0c61451
style: 传递文本页二维码移到输入框上方
rrbe Jun 26, 2026
e7da77a
feat: 手机已发送历史每条加紧凑相对时间
rrbe Jun 26, 2026
e9fd2c3
fix: 收文本未读只用数字角标,接收开着用呼吸红点不再冒充未读
rrbe Jun 26, 2026
3aa5435
refactor: 传递文本文案去手机化,对端不限手机
rrbe Jun 26, 2026
aba1e88
feat: 手机发送页加「清空已发送历史」
rrbe Jun 26, 2026
8fa33c9
feat: 传递文本加「停止」,主页状态胶囊如实显运行态
rrbe Jun 26, 2026
d82d443
docs: PLAN 补记传递文本 v2.1 打磨串(停止/未读/吞字/手机端/去手机化)
rrbe Jun 26, 2026
9b90b95
docs: 修正 clearShare 注释中过时的 /ls/send 为 /ls/text
rrbe Jun 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,6 @@ jobs:

- name: 传递文本(/ls/text + 转义 + 虚拟根文本行)
run: ./tools/smoke-text.sh

- name: 收文本(/ls/send + POST /ls/text 双限 + 闸门 + 列表内嵌表单)
run: ./tools/smoke-text-receive.sh
50 changes: 43 additions & 7 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,41 @@ open dist/LocalShare.app # 本机自测
/ `LStr.i18nJSON`(JS 侧拼接,`jsEscape` 防 `</script>`)。详见 §3「国际化(i18n)」。
已验证:`LangTests` 单测(协商 / q 值 / 穷尽)+ `tools/smoke-accept-language.sh` 冒烟,CI 全过;
release 编译通过。
- [x] 传递文本 v1 — Mac→手机发文本(PR #25):`AppState.sharedText` 与 share 正交,保留路径 `/ls/text`
提供(导航发 `TextViewer` 壳页、`?raw=1`/curl 发 `text/plain` 原文),可独立分享或挂进多选虚拟根的
文本行;离散提交快照、点「分享/更新」提交文本(v2.1 起更新文本不再轮换 token,见下);空态加「分享文本」入口(自带 `NSTextView`、
placeholder 由其自绘以兼容中文输入法组合态);手机页纯文本 + 大「复制」按钮(`execCommand` 回退——
纯 http LAN 是非安全上下文、`navigator.clipboard` 不可用)+ http(s) 安全自动链接;设置「记住分享的
文本」默认关(开则重启回填草稿、**不自动广播**);历史复用 `RecentShare`(扩 `text:`)+ 逐条 ✕ 删除 +
「清空」二次确认。`textContent` 注入 + `<`→`<`(共享 `LStr.jsEscape`)双重防注入;`LS_TEXT`
headless 钩子;`tools/smoke-text.sh`(15 项)+ `TextShareTests` 入 CI。设计见 §7「传递文本」。
- [x] 传递文本 v2 — 手机→Mac 收文本(PR #26):独立收件箱通道,与 share 正交、不落盘、不依赖文件夹分享。
闸门 `textInboxEnabled`(设置「允许收文本」,opt-in 默认关,不限分享形态、开了就把服务拉起);保留路径
`POST /ls/text` 收一段纯文本(请求体即原文);列表页(同上传表单条件)内嵌发送表单(`SendText` 片段)。
**双上限**挡内存:单条 64KB(`textInboxLimit`,事后 413)+ 收件箱 100 条挤旧(`AppState.receivedTexts`)。
`onReceiveText` socket 线程 hop 回 MainActor 入收件箱卡片(复用「新收到」样式:来源设备名/IP + 时长 +
单条复制/删/清空 + 未读角标)。仅应用内提醒、不发系统通知。设置「记住收到的文本」默认关(对称 v1)。
`LS_RECV`/`LS_RECV_LOG` headless 钩子;`tools/smoke-text-receive.sh` + `ReceivedText` 单测入 CI。
- [x] 传递文本 v2.1 — 收发合一、主页回归选择页(PR #26):把「分享文本 / 接收文本」并进**一个二级页**
`TextScreen`(`Screen.text`),上半编辑器发文本、中间一个二维码、下半「允许收文本」开关 + 收件箱;
主页 `EmptyScreen` 收敛回纯功能选择(拖拽分享 / 传递文本),不再就地长接收卡。网页侧 `GET /ls/text`
**恒可渲染**:有共享文本发预览壳页(开着接收即自带发送框,`PreviewPage.canReceiveText`),无文本但开着
接收则退化成纯发送页——**一页一码、两端双向**。二维码(`makeURL`)与 headless URL 一律指 `/ls/text`;
旧 `/ls/send` 保留为 302 跳 `/ls/text` 兼容。「允许收文本」默认关,闸门仍是 `textInboxEnabled`(设置页与
文本页同一开关)。**token 改回会话维度**:`setSharedText` 不再轮换 token(v1/v2 每次更新都换、会把正在看的
对端刷掉,还误伤共存的文件分享链接),只在 `setShared`/`stop`/`clearShare`/`stopTextTransfer` 这些会话边界轮换。
`smoke-text-receive.sh` 改测 `/ls/text` 退化页 + `/ls/send` 302。
- [x] 传递文本 v2.1 打磨串(PR #26,承接上条):
· **停止机制**:文本页加 `stopTextTransfer()`(撤文本+关接收+停服务+回选择页),对齐文件票据「停止」;
修掉「服务在后台续跑、主页却显待命」——`EmptyScreen` 表头在 `isRunning` 时改用 `StatusPill` 如实显
运行态,「传递文本」入口呼吸点亦由 `isRunning` 驱动(发或收都亮)。文本页 ← 仍为非破坏式返回(可续跑/恢复)。
· **未读口径**:`recordReceivedText` 仅在 `screen != .text` 时累加未读;未读只走数字角标,「接收开着」改
缓慢呼吸红点(`PulsingDot`,动效区分于静态未读),不再用常驻红点冒充未读。
· **输入法吞字**:`PlainTextEditor.updateNSView` 在 `hasMarkedText()` 时直接返回——服务运行时每 2s 在线人数
轮询触发的周期性重渲染会在拼音组合中回写 string 致吞字。
· **手机端**:发送页「已发送」历史每条缀紧凑相对时间(24h 内 HH:MM / 一年内 MM/DD / 更久 YYYY;存储升级为
`{t,d}` 带迁移)+ 加「清空」按钮(清本机 localStorage,给共用设备主动抹痕)。
· **文案去手机化**:对端不限手机(可能是平板/电脑),Mac 端文案「手机」→「对方/设备」,枚举 `sendTextKicker`。

> 已知坑(已规避并注释):Swifter 1.5.0 的 `HttpParser` 会对请求 path 二次编码,导致 `request.path`
> 仍残留一层百分号编码 —— FileServer 落地文件系统前已用 `removingPercentEncoding` 解码,且不影响防穿越。
Expand All @@ -259,7 +294,7 @@ curl -s "http://127.0.0.1:8099/?t=testtoken" # 应返回目录列表
(访客上传 v1 已在 0.6 落地,见 §5 进度;当年切范围的理由:上传解决手机照片/文档传到 Mac
这 90% 的诉求,在线编辑在手机浏览器体验差、覆盖丢数据风险大,删除误删风险高,均往后放。)

### 传递文本(设计已定,待实现
### 传递文本(v1 / v2 已落地

让「选内容→手机扫码」的内容从「磁盘文件」扩到「一段文本」——剪贴板/链接/口令/说明从桌面甩到手机,
以及反向把手机上的文本收回电脑。**两条独立单向通道,不是同步便签**:发出去的 `sharedText` 与收回来的
Expand All @@ -270,14 +305,14 @@ curl -s "http://127.0.0.1:8099/?t=testtoken" # 应返回目录列表
- **收回来(手机→Mac)的难点不在传输**(一个表单 POST 比现有 multipart 上传还轻),而在于 **Mac 要长出
一个「收件箱」形态**:新原生 UI + 新生命周期(收到的文本往哪放/怎么清)+ push 模型。独立成 v2。

#### v1 — Mac → 手机·发文本
#### v1 — Mac → 手机·发文本(已落地,PR #25;落点见 §5 进度)

| 维度 | 决定 |
|---|---|
| 数据模型 | `AppState` 加 `sharedText: String?`(全局单一文本,一次只一段) |
| 与文件关系 | 混进多选**虚拟根**当一个条目;**也能独立分享**(一个文件都不选——这才是「传文本」的主力场景) |
| 「空」判定 | 从 `sharedItems.isEmpty` 改为 `sharedItems.isEmpty && sharedText == nil`,牵动 `start/stop/恢复/QR` 全链 |
| 交互模型 | **离散提交快照**:输入框打字不广播,点「分享」才把当前文本快照发出去 = 一次 `setShared` → **轮换 token**;要改就改完再点「更新分享」(再轮换)。与「一次分享=一次 setShared=换钥匙」完全一致,不存在边打字边失效 |
| 交互模型 | **离散提交快照**:输入框打字不广播,点「分享/更新」才把当前文本快照发出去。token 按**会话**维度(同分享文件)——`setSharedText` **不换 token**,只有换分享(`setShared`)/停止/清除这些会话边界才轮换作废旧链接;故**编辑/更新文本时正在看的手机刷新仍有效、无须重扫**(v2.1 修正;v1 曾每次更新都轮换,会把看的人刷掉,也会误伤共存的文件分享链接) |
| 路由 | 仅文本无文件 → 扫码直达文本页(同「单文件直接发那一个」);文本+任意文件 → 落虚拟根列表,文本是其中一条。文本服务在保留命名空间 `/ls/text`(像 `/ls/ping` 一样**先于**虚拟根 key 匹配),躲开「有文件正好叫 text」的 key 撞车 |
| 持久化 | 设置项「持久化分享文本」**默认关**。关 → 文本压根不写进 recents/UserDefaults;开 → 记一条可恢复(但**不自动 start**)的 history。理由:文本常是密码/验证码/一次性口令,自动恢复+自动起服务会在 LAN 上悄悄重新暴露上次那段 |
| 历史 | 复用现成 `.history` 屏 + `RecentShare`(扩 `text: String?`,无 path 的文本条目)。顺手把**「逐条删除」做成 history 通用能力**(文件条目也能删——路径同样泄信息)。删 history ≠ 停掉当前直播的 live 分享 |
Expand All @@ -286,17 +321,17 @@ curl -s "http://127.0.0.1:8099/?t=testtoken" # 应返回目录列表
| 取原文 | `?raw=1` / curl(Accept `*/*`)拿 `text/plain; charset=utf-8` 原文,导航(`text/html`)给壳页——同 md/json/csv 预览的同 URL 双形态 |
| 大小 | **不设上限**:文本是 Mac 端自己粘的、非不可信输入,不必像上传那套;空白文本禁用「分享」按钮 |

#### v2 — 手机 → Mac·收文本
#### v2 — 手机 → Mac·收文本(已落地,PR #26;落点见 §5 进度)

| 维度 | 决定 |
|---|---|
| 本质 | **独立收件箱通道**,不落盘、不依赖文件夹分享,与 v1 对称(都在 app 里以文本形态存在) |
| 收件箱 | 列表,每条带时间 + 来源(复用现成 `nameCache`/`getnameinfo` 反查设备名,查不到显 IP),单条复制/删除/清空 |
| 持久化 | 设置项「持久化收到的文本」**默认关**(对称 v1;收到的常更敏感/更像垃圾,默认易逝更稳) |
| 闸门 | opt-in 默认关(参照 `uploadEnabled`),但**不限分享形态**——收文本不依赖落点,任意模式甚至「什么都没分享」都能开,开了就把服务拉起并出一张指向发送页的 QR |
| 闸门 | opt-in 默认关(参照 `uploadEnabled`),但**不限分享形态**——收文本不依赖落点,任意模式甚至「什么都没分享」都能开,开了就把服务拉起并出一张指向发送页的 QR(v2.1 起这张 QR 与发文本合并为一页一码、统一指 `/ls/text`,见 §5) |
| 提醒 | **仅应用内**:复用 `onUpload` 的「新收到」卡片机制(socket 线程 hop 回 MainActor)+ 收件箱未读角标。不发系统通知(macOS 通知要授权,且未公证 ad-hoc 签名下可靠性存疑,往后放) |
| 防滥用 | **双上限**:单条文本 ~64KB(远小于上传 500MB)+ 收件箱 ~100 条满了挤掉最旧。不做速率限制(Swifter 不便做、opt-in+LAN 信任足够)。**必须双限**:Swifter 进 middleware 前已把整段 body 读进内存,光限单条挡不住「刷一堆刚好不超限的消息撑爆内存」 |
| 手机端 | 「发文本给电脑」textarea + 发送,放在 listing 页(及独立收文本页)**与现有上传表单同处、同样的出现条件**(收件箱关时不出现) |
| 手机端 | 「发文本给电脑」textarea + 发送,放在 listing 页(及 `/ls/text` 无共享文本时退化出的发送页)**与现有上传表单同处、同样的出现条件**(收件箱关时不出现) |

#### 贯穿约束(实现时必须守)

Expand All @@ -306,7 +341,8 @@ curl -s "http://127.0.0.1:8099/?t=testtoken" # 应返回目录列表
- **Swifter body 预读内存**:上限只能事后拒绝(同上传),故 v2 必须单条+总数双限。
- **token / 302 去 `?t=` / 网卡绑定 / i18n(`L`/`LStr`,文案过表)/ 转义** 全自动沿用现有机制;收到的文本在
SwiftUI `Text` 里显示天然不执行,但若回显进任何**服务页**须转义。
- v1/v2 各自一个持久化开关(可考虑合并成一项,留实现时定)。
- v1/v2 各自一个持久化开关(**各一个**:v1「记住分享的文本」、v2「记住收到的文本」均已落地,皆默认关;
两者隐私语义不同——发出去的是自己粘的、收回来的是他人投递的,分开开关更清晰)。

### 上传 v1.5:分片上传

Expand Down
Loading
Loading