Repository navigation
Releases
v1.7.1
手机端交互缺陷集中修整,并把"状态同步"从轮询改成帧驱动。
Added
- 聊天正文本地缓存:断网重进直接显示上次那一屏,不再只剩"无法连接"(每会话一层、7 天过期、只留最近 10 个会话)。
- 离线可进:连不上宿主但本地有内容时照常进主界面,顶部一条离线提示。
Fixed
- "运行中"不实时:帧到即知(实测 1 秒出现 / 0.3 秒消失),长静默回合判死。
- 过程条计时会从小重新长:一轮的起点只许变早,晚 30 秒以上的跳变不采用。
- 交付卡时序:未收工不画卡;发新消息也不再弄没上一轮的卡。
- 排队消息显示消失:在跑时不按时间淘汰。
- 点发送完全没反应:发送互斥改成带时间戳。
- 输入框删不干净:删光了就真的删掉草稿。
- 收工提示改到顶部;交付卡默认折叠。
dsh-palm 1.7.0
dsh-palm 1.7.0
手机端这一版的主线是把「状态」交给宿主说了算,加上两处用户当场提的体验修整。
完整逐条记录见 CHANGELOG。
升级须知
| 项 | 说明 |
|---|---|
| 需要的 DSH | >=0.2.1-rc.2 <0.3.0-0(package.json 的 dsh.engines.dsh 与 peer 同口径) |
| 已在 0.2.2-alpha.2 上验证 | 是。按 0.2.2-alpha.2 的 SDK 重新编译,宿主起得来、插件加载正常、5/5 手机 API 通 |
| 0.1.x 还能跑但要接受两处退化 | ①宿主没有 SettingsForms.configure,注册不了宿主侧设置表单;②后台任务完成推送降级为轮询 |
| 升级动作 | 换掉插件版本后重启 DSH(宿主侧改动不重启不生效),手机端刷新一次 |
本版要点
- 手机端能发文件了:附件可带图片/文件,单条上限 5 个(宿主侧强制,换客户端也绕不过);
@路径含空格自动加引号。 - 运行中页重做(v3):进度细条、子代理可点进只读页、「刚结束」清单常驻,收工的会话立刻记账。
- 状态只有一个判据:
session-status.ts单例 + 租约 + 源优先级,目标条/排队条/任务条收进同一个 HUD,顺序固定、渲染点唯一。 - 状态对齐宿主真值:不再由手机端每 10 秒猜「还在跑吗」——宿主在
turn/start、turn/end到达那一刻主动推送。 - 用量查询覆盖主流供应商:余额/累计已用分开表述(不会再把人家的「累计已用 $537」读成「还剩 537」),不支持的提供方给控制台深链。
- 计划(goal)有了入口与动作:目标条显示已跑时长与第几步,动作点下去立刻转圈。
- 收工提示不再挡住输入框:聊天页改走顶部 HUD(原来固定在底部的浮层压在输入框上),打字期间一律不弹。
- 首页不再单列「测试项目」:按路径分词分组会把正经项目误判进去,这类工作区并入代码项目。
已修复(挑用户能感知的)
- 进会话时同一轮被画成多张过程卡;过程条秒表节奏怪、偶尔跳 2 秒、后台回来后卡住不走。
- 会话明明还在跑,过程条却写着「已完成」。
- 会话标题被截成一截加省略号;列表里冒出大量空白会话;无标题会话统一叫「未命名会话」。
- 搜索:首页翻页重复、单字查询 320ms、全角输入搜不到、fork 副本重复行。
- 会话列表页「?」点了没反应(宿主给的预设都没有说明,按钮曾被禁用);若干「禁用即死按钮」一并收口。
安装
npm i @eternalloveone/dsh-palm@1.7.0
# 或换个渠道
npm i @eternalloveone/dsh-palm@1.7.0 --registry https://npm.pkg.github.com这一版怎么验的
- 单测 2109 通过 / 9 跳过(含 41 条真机用例的骨架)。
- 手机端端到端验收 41 条 / 8 组,真宿主 + 真浏览器(390×844),连续多轮稳定。
- SDK 适配矩阵:0.1.5-rc.2 / 0.2.0-rc.1 / 0.2.1-alpha.1 / 0.2.1-rc.2 / 0.2.1-rc.4 / 0.2.2-alpha.2 —— 全部 0 类型错误、测试全绿。
- 发布门禁
pnpm verify --ci8/8(install / build / coverage / typecheck / audit / hygiene / pack / commitlint)。
v1.6.4
Fixed
-
手机会话列表里冒出大量空白会话。 桌面端每开一个新会话/切换工作区都会落一条
blank会话,桌面自己在侧栏里隐藏它们(tree.ts),手机端却照单全收。现在手机端的可见行过滤掉blank: true与子代理来源的会话;顺带修掉一个回归:快照落盘时把行裁成{sessionId, updatedAt},标题字段丢失,导致列表里所有会话都叫「新会话」——现在按{ ...item }浅拷贝保留投影,并补了回归测试。 -
会话标题被截成一截加省略号,看不出是哪个会话。 标题原来和「· 10-19 10:17」挤在同一行,宽度不够就省略。现在标题独占一行、可完整换行,日期与「新/进行中/出错」徽标移到标题下方的小字元信息行。
-
会话行的标题区域太窄。 图标列 48→36px(图标 32px)、右侧箭头列 48→24px、列间距 12→8px、卡片左右内缩 16→12px,标题列实际可用宽度增加约 24%。改动只作用于会话行(
.mobile-row-sess):工作区/设置行保持原来的 48px 列,因为它们的图标是 48px 的.ws-icon,套窄列会溢出。 -
运行中的绿色状态点变成一个绿色大方块。 图标盒规则(
.card-icon > *→ 40px 圆角块)把所有直接子元素都撑成图标尺寸,8px 状态点也被套上。现在状态点与勾选标记用双类名选择器压回自然尺寸。 -
移除「回到上次」悬浮按钮。 它会在读者根本没翻历史时出现:打开会话的程序化定位会连发多个 scroll 事件,防重标记只吸收第一个,随后的高度校正回声被当成"用户上滑",于是写入阅读位置。两轮收窄(加手势门槛、跳转即清除、清空 v2 旧记录)之后仍有噪音,改为整块删除:按钮、
lastReadTurn状态、滚动防抖保存、退出时写入、以及list-persist的阅读位置 API。老手机里的历史记录由维护流程与解除配对清理(dsh-palm.reading.v1/v2/v3前缀)。「回到最新消息」、轮次手柄、未读徽标不受影响。
v1.6.3
Added
- 桌面端面板底部新增版本行与「一键更新」。 显示当前 dsh-palm 版本;有新版本时用强调色加圆点提示(不再是灰字),点一下即可更新并自动重启 DSH:① npm 安装走
npm install --prefix <真实 profile>,装完校验版本、入口文件与新包声明的每一个运行依赖,缺一个就回滚 —— 避免"报成功、重启后插件加载失败";② 源码链接安装走git pull → pnpm install → pnpm build,工作树有未提交改动时直接拒绝,不弄乱手上的活儿;③ 更新成功后由独立 helper 进程按这次进程的真实启动配方重启,环境变量只带 8 个白名单键,绝不携带 token;10 秒倒计时可取消,连点第二次会被挡回"上一次更新还在跑"。四条路由仅在回环地址可用。
Fixed
-
重启 DSH 后手机就要重新扫二维码。 根因不是"配对丢了":插件
apply收到的.volatile()配置字段是包装对象,value.devicesFile ?? DEFAULT兜不住对象,于是readFileSync拿到的是[object Object],设备表从未被读取过 —— 内存里永远是 0 台设备,手机一连上就被当成新设备重新配对(审计日志里表现为反复出现paired the first device (primary))。现在:① 配置字段先解包(get()/value/ 嵌套)再按类型校验,不符即退回 schema 默认值(src/config-values.ts,附 9 条单测);② 设备表每次落盘同时写一份.bak,启动时主表为空或缺失就从副本恢复并记一条store-restore审计;进程内有意的清除(踢设备、停止远程)会同时更新两份,已踢掉的设备不会被复活;③ 每次启动打一行[dsh-palm] device store: N device(s) from <路径>,这类故障不必等手机连上才暴露。 -
一键更新的两处硬伤。 ① 旧实现把新版装到临时目录再
cpSync包本体,新版本若新增运行依赖会被丢掉,更新报成功、重启后插件加载失败;现在改为装进真实 profile 并逐个校验依赖。② 源不可达时 npm 默认重试会让人干等约 70 秒,且只得到一句"退出码 1";现在--fetch-retries=1 --fetch-timeout=20000快速失败,并把 npm stderr 的首行带进错误文案,实测 1.4 秒。另修两处:连点两次会并发跑两个更新(守卫移到第一个await之前);--fetch-retry-maxtimeout小于 npm 默认fetch-retry-mintimeout会导致所有安装直接失败(已补--fetch-retry-mintimeout=1000,并加一条"min ≤ max"的测试锁死)。
v1.6.1
Fixed
-
手机端不再"隔几天就要重新配对"。 根因:设备会话默认 7 天不活跃就被清掉(
liveSession()命中空闲超期后直接删设备并落盘),而手机 Cookie 有效期是 90 天 —— cookie 还在、服务端已经把人踢了,于是下次打开就是配对页;宿主重启时loadPersisted()还会再淘汰一次,所以"隔几天/重启后"最明显。现在:① 主力设备(primary)不再参与空闲淘汰,只有显式踢除或停用插件才会消失;② 默认空闲期 7 天 → 90 天,且与 Cookie 有效期同源推导(COOKIE_MAX_AGE_SEC由DEFAULT_IDLE_EXPIRE_MS换算),两者不会再各自漂移。非主力设备的淘汰行为保持不变。 -
设备消失终于有据可查。 新增尽力而为的审计日志(与设备表同目录的
dsh-palm-devices.audit.log),记录stop/revoke/idle-evict/live-evict/accept五类事件(时间、设备 id 前 8 位、原因、UA),超过 1 MB 轮转成.audit.log.1;写日志失败只打一行错误,不影响配对与鉴权主流程。以后再遇到"又要配对",可以直接看出是谁、在什么时候删的。
v1.6.0
Added
-
手机端一轮会话的"过程"收成一个折叠组,思考与工具按事件顺序混排(对齐桌面端的 turn-process)。 此前思考藏在一个「深度思考」折叠块里、工具步骤藏在另一个「过程」折叠块里,两块各占一行、彼此割裂,手机看到的顺序和模型实际做事的顺序对不上——桌面端是同一棵子树里思考与工具交错排布。现在合并成一个过程组:折叠时只有一行,头里是这一轮花了多久、走了几步、改了多少行(
本轮用了 1分20秒 · 8 步++120 −36;运行中第一行是深度求索中,用时3分33秒);展开后按事件顺序混排:思考块(缩进 + 左侧强调线,折叠时只说「深度思考 · 42 行」,展开才读正文)、工具行(族图标 + 中文动词 + 参数派生摘要 + 中文状态字运行中/完成/失败/已中断,替换原来的色点——不靠颜色也能读)、步骤之间的叙述文本、以及就地展开的 diff 卡。行不编号:层级只靠缩进与行首字形区分(用户拍板)。没有可展开的行时(纯文本回复)头退化成静态一行,照样显示用时,与桌面端disabled={!canCollapse}同形。关掉「显示工具调用」只挡工具行,diff 卡与改动量照旧可见(卡是这一轮的产出,不是过程噪音),也不再连带藏掉思考与用时——只是那一行的步数会跟着工具行一起不报,免得"看不见的行"还被计数。新增纯模块process-rows.ts(行模型、折叠头文案、diff 差值汇总,diffTallyOf/formatDuration从 message-row 迁入,保持"一张 diff 视图只有一个数字")与process-rows.test.ts(16 例:步骤判据、混排顺序、老数据兜底、文案)。(1.6.0 收尾:按桌面端对齐,设置页里的「工具调用」「系统提示词」两个开关已删除 ——showToolCalls恒为显示、showSystemMessages恒为不显示,组件级 prop 语义不变,本节其余描述照旧成立。) -
过程卡改成两行、秒表搬进第一行(用户 2026-10-13 定的形态)。 第一行是这一轮的状态与用时(运行中
深度求索中,用时3分33秒,不足 3 秒只写「深度求索中」,且不报步数;已结束本轮用了 1分20秒 · 8 步),第二行是此刻进行到哪一步的一行简报(运行中的工具给「中文动词 · 摘要」,例如运行命令 · pytest -q;模型在想就给「思考中」;在写回复给「正在回答」;已结束的回合报最后一步做了什么,没有可说的就整行不渲染),第一行右侧仍跟改动量+N −M。同时删除底部「输出中…」后面的秒表——同一时刻只有一个秒表,它在过程卡第一行(底部那条只留措辞与三个点)。秒表走宿主时间(复用hostClockOffsetRef的帧校准偏移,否则手机 NTP 与宿主差几秒、秒表就和桌面端对不上),1Hz 定时器只挂在"运行中"的那一行(不把每秒变化的数字做成 prop 传进列表,否则整份消息列表每秒全体重渲染);锚点优先turn/start,新增firstSeenAt(该行第一次出现事件的宿主时间)兜底尾页中途加载拿不到turn/start的情况,运行中的回合不会再出现没有秒表的空档。 -
过程卡改走灰阶层级(用户 2026-10-13 看图后选的 B 方案):颜色只给"运行中",其余全靠深浅与文案。 原先这张卡几乎全灰,却把仅有的两处颜色花在最没人看得懂的信息上(运行指示一枚强调色 +
+120 −36的红绿数字),真正在发生的事反而用最暗的灰;而且失败/已中断/已完成长得一模一样(折叠头一律✓+本轮用了 X),最该一眼看清的成败反而看不到。现在:第一行=状态词(已完成/已中断/失败/深度求索中,正文色加粗)+右侧用时(第三档灰、等宽数字,运行中不足 3 秒不报秒数,且不报步数),第二行=此刻在做什么(正文色 + 族图标,这张卡的主角)+右侧把人话写出来(改了 3 个文件,去重后的 diff 路径数),红绿数字与折叠头里的+N −M一并删除(展开后的工具行仍保留逐步骤的增删行数,那是给要看细节的人)。成败走真源:turn/end的reason.kind === 'error'→失败、=== 'max-tokens'→已中断、被turn/end收尾的stopped工具 →已中断,其余已完成(messages.ts早已记下failed/maxTokens,此前只用于回合尾)。"运行中"那一点强调=比默认亮一档的灰边(--border-default)+唯一一枚强调色指示符◐;安静态字形退到--text-quaternary。新增processOutcome/processStateText/processTimeText/processFilesText四个纯函数,ProcessModel带上outcome与changedFiles。 -
"哪段是答案"按步骤判定:最后一步的文本渲染在过程组之外,之前各步的文本留在组内当过程叙述。 折叠后不再把整轮文本一股脑塞进折叠块,也不会把过程叙述误当结论:coalesce 记下的
stepSeqs/stepTexts给出每一步的身份,最后一个步骤的文本即答案(渲染在组后、组外,带data-step-seq锚点,搜索定位照旧能滚到那一步),前面各步的文本按位置留在组内;最后一步以工具调用收尾(没有文本)时诚实不给答案槽。为此思考在 fold 里也变成有位置的部件(flow新增{kind:'reasoning'}这一支),assistant/message的历史重放改走内容块顺序(新增flowFromContent)而不是按类型拼接——按类型拼接会把思考与文本的先后关系整段丢掉;最终文本与流式累积不一致时,文本段收敛为末尾一段(原来放在最前,等于把结论挪到产生它的步骤之前)。契约锁在messages.test.ts(流式思考落位、历史保序、终稿收敛到末尾)与新增的process-rows.test.ts。 -
手机端每一轮结束多一张"回合尾"报告卡(对齐桌面端的 turn-tail)。 折叠
deliverables/presented({turn, callId, files[]})得到交付物清单,turn/end时在回合末追加一行kind:'turn'的报告:交付物(描述 + 路径 + 「已交付」徽标,可点开)+ 本轮产出文件(该回合 diff 视图改过的文件,已声明的交付物不重复列)。用时只在过程组折叠头里报一次(此前头尾各报一次,同一轮出现两个数字,正是用户反馈"运行时间对不上"的一半),所以尾巴只在真有交付物或产出文件时才生成——没有东西可报的回合不再挂一张只有时间的空壳卡。按 turn 幂等(固定turn-tail:<turn>行 id),历史窗口重放不会叠卡;点开走既有的mobile.readFile→ 文件预览面板(文本/图片内嵌,其他类型由面板给出降级提示)。契约锁在messages.test.ts(交付物、产出文件去重、重放不重复、不被 coalesce 吞掉、只看时间不再生成尾巴)与ChatView.test.tsx(渲染与可点)。
Changed
-
两个官方 peer 上界再放一条线:
<0.2.1-0→<0.2.2-0(dsh-client-ui-slots与dsh-settings各一处)。 0.1.6 起 DSH 装载插件前读peerDependencies,不覆盖当前运行版本就把整个 bundle 静默跳过;声明停在<0.2.1-0时 0.2.1 线会被跳过,手机面/m/整套不存在而桌面端毫无提示。上界放宽后 0.2.1-alpha.2 上探针 PASS(boot + 契约自检 + 真配对),运行时零改动——插件对所有官方包都是import type、编译期擦除。这一处原本计划单独发 1.5.5,按排期与本轮功能增量合并为 1.6.0。 -
手机端读会话列表改成"投影式读取":首屏先用上次落盘的快照立即渲染,后台再刷新(SWR)。 宿主侧新增快照落盘(
roster-snapshot.json,超过 16 MB 才跳过)与刷新节流(最快 60 秒一次),并把并发枚举合并成单飞——同一时刻只跑一次枚举,其余请求直接复用结果。客户端工作区列表另加 5 秒 TTL + 单飞,每次导航的往返次数由 3 次降到约 1 次。实测冷启动session.list由最长 29.8 秒降到 0~23 毫秒。
Fixed
-
手机端的"输出中"指示灯与运行时间不再突然消失,计时也不再被一次网络抖动清零。 三个缺陷叠加:① 一轮之内连续 3 次
listSessions失败(约 30 秒的重试,手机在后台、锁屏、隧道抖动或 EventSource 重连时很常见)就会把running置假,于是指示灯连同计时整块消失,而回合其实还在跑;② 中途重开会话时,若宿主花名册的running恰好为假(agent 正停在子代理上),尾部页里的turnStartAt虽然证明回合开着,界面却一直不亮;③ 计时挂在turnStartAt锚点上,但"停止"入口不可用那条路径没有清掉过期锚点,下一次回合的时长会从上一次算起。现在把指示灯的存活与计时解耦:列表失败只把文案切成"连接中断"(轮询与对账照旧重试、自己恢复),计时继续按turnStartAt走;尾部页带turnStartAt就直接点亮回合;锚点缺失才退回挂载时刻兜底,前台恢复时重算;门限由 15 秒收紧到 3 秒(短回合也看得到时长);"停止"不可用时清掉锚点。契约锁在ChatView.test.tsx(断连时计时继续走、恢复后回到"输出中"、尾部页证明回合开着时点亮、短回合不显示时长)。 -
手机端过程卡按用户当场定的形态收口:一回合不再裂成多张卡,卡面只留人看得懂的信号,计时与桌面端对齐。 ① 一个回合被拆成 3 张过程卡的根因有两个:coalesce 被宿主夹在回合中间的隐藏注入行挡住;步键
(turn,step)反解宿主 uuid 行 id 时指错对象,同一回合后续的tool/call被静默丢弃(实际 3 个工具的步骤只报 1 步)。现在隐藏注入行不参与打断,步键直接取自消息自身记录的step。② 宿主按步发assistant/message(一行会在回合中途就被定格)时也照样算"运行中":活跃轮次(ChatView 的running→runningTurn)贯穿到isRowRunning,秒表不再中途变成"已完成"。③ 卡面(用户拍板):第一行=状态词 + 用时,步数移出卡面;本轮确有工具报错时多一枚极小的报错标记(stopped不算报错);第二行=此刻在做什么,右侧只在回合结束后给改了 N 个文件/未改动文件;步数与红绿统计移进展开体。④ 运行中不再"闪一下":卡外那一行不再瞬间空掉(继续显示最近一段文本),回合结束后才把最后一步的文本提到卡外;计时锚点只认宿主时间(行上的turnStartAt/ 页级liveTurnStartAt),删掉会读数偏小的firstSeenAt兜底 —— 拿不到宿主锚点就不报秒表,宁缺勿错。⑤ 新增唤醒消息卡(对齐桌面端TurnTriggerNodeView):子代理/任务/定时/团队/外部事件等唤醒消息出折叠卡(标题按source.kind取、时间右对齐、正文点开才看),数值逐条对齐桌面端 CSS(16px 圆角、代码块灰底、头部 12px/16px、标题 13px、时间 12px、正文左缩进 40px、正文区最高 240px,外边距单独给足);其余注入行照旧不渲染。契约锁在process-rows.test.ts、message-row-process-group.test.tsx、trigger-card.test.tsx。 -
手机端唤醒消息卡(真机反馈两轮修完):宿主唤醒消息(子代理结算 / 收到任务消息 / 团队消息 / 定时任务 / 后台任务 / 外部事件 / 插件状态 / 继续执行目标,共 8 类来源)在手机端折叠成卡,文案与卡片几何逐条对齐桌面端
ui-chat的TurnTriggerNodeView(16px 圆角、代码块底色、头部 12px/16px、正文左缩进 40px、正文最高 240px)。真机实测"卡片不出现"的根因是折叠阶段把唤醒消息当成了透明注入行、并进了助手那一轮:isHiddenInjectedRow收窄为只有四种纯噪音注入行透明(压缩检查点 / 指令 / skill 目录 / 运行时上下文),触发类来源独立成行。标题与 kind 表合并在messages.ts(triggerTitleOf/isTriggerSourceKind)作为唯一真源,避免"判出卡"与"判透明"两份清单漂移。 -
时刻政策改为每条消息都带:原先"每个分钟桶只显示最后一行"的去重会让同分钟内的前几条看起来像丢了时间(真机反馈"有的时候发消息,消息下方没有出现时刻"),
timeVisibility改为恒真。 -
秒表只增不减:宿主时钟偏移每帧重新校准会让显示秒数来回跳(真机 3 秒 → 2 秒 → 3 秒),现在把校准后的目标值与已显示值取较大者 —— 校准造成的向上升照旧立刻生效(未校准 1分0秒 → 校准 +3 秒 ⇒ 1分3秒),只挡住"往回跳"。中途曾用"偏移只取一次并冻结"实现,撞坏两条用例(含"设备时钟偏差必须被宿主校准"这条真需求),已回退改用单调锁。
-
行间距收敛到 12px:消息列表
gap由 20px(--space-5)→ 24px → 最终按用户定的节奏改为 12px,卡片与紧随其后的正文另有 12px,卡片上下读起来是 12px + 12px。 -
手机端"加载超时"横幅在一次成功加载后立即清除。 此前首屏与续页的看门狗一旦超时就把错误横幅挂住,即使数据随后已经到达也一直显示,看起来像仍然失败;现在成功回调里先清横幅再渲染数据。
v1.5.4
Added
- 手机端能显示本地图片了(此前只有桌面端能)。 桌面端把
重写成自己的授权/api/fileURL,而手机端 bundle 既没有那个路由、也没做任何重写:safeUrl把D:\的盘符当成未知协议,把图片退化成 alt 文字;相对路径(./a.png)则相对/m/解析、稳定 404——只有 http(s) 图能显示。现在渲染器认领本地图片目标(盘符判断先于协议判断,D:/a.png是路径而不是未知协议,与桌面面板path-images.ts同序)、把解码后的路径挂在data-local-src上并故意不写src,消息体挂载后经mobile.readFile(自己的授权通道)换成 data URL 再赋值——之所以靠赋值而不是写进 markdown,是因为safeUrl对作者书写的 markdown 仍拒绝data:。新增local-images.ts((sessionId, path) 的 LRU 缓存 + 并发去重;sessionId 入 key,因为相对路径是按会话工作区解析的)。契约锁在新增的markdown-text.test.tsx:成功换 data URL(绝对/相对路径都带 sessionId)、readFile失败不留半张图(不给src)、远程图完全不走这条路(既有行为不变)。
Changed
- 插件在 DSH 0.2.0 上不再被版本门静默丢掉。 0.1.6 起 DSH 装载插件前会读它的
peerDependencies,不覆盖当前运行版本就把整个 bundle 静默跳过(不崩、不告警);两个官方包的上界停在<0.1.8-0,于是在 0.2.0-rc.2 上 DSH 给出skipping profile bundle "@eternalloveone/dsh-palm": ... is incompatible with dsh 0.2.0-rc.2,手机面/m/整套不存在,而桌面端毫无提示。上界放宽为<0.2.1-0(dsh-client-ui-slots与dsh-settings各一处),覆盖 0.2.0 线且不影响 0.1.5-rc.1 起的旧线。容器实测(镜像dsh-upgrade-verify:dsh-v0.2.0-rc.2,用打包件测这个未发布版本):改前 install ok / boot no / 契约与配对 skip(BOOT-FAIL,217s),改后 install ok / boot yes / 11 条契约全 ok / 真配对成功(80s PASS,矩阵 MATCH 1 / DRIFT 0)——运行时零改动,与"插件对所有官方包都是import type、编译期擦除"的静态事实一致。另记两条:0.2.0 把自动化任务移到可选插件包,未安装该包时「任务完成/失败」推送不会触发(功能层缺失,不是故障,session/jobs帧契约本身未变);应急场景可用 0.2.0 自带的dsh plugin allow-version给旧版本开精确豁免。
Fixed
-
手机端的问题面板不再"消失"。 适配层此前按"此刻是否存在活着的手机流"决定要不要把提问交给手机:手机恰好在后台、锁屏、隧道抖动或 EventSource 重连中时,它就不在在线名单里,于是这条
user-questions/request被静默整条交给桌面——不登记、不广播、也不通知。桌面面板照常弹出,用户顺手在桌面答掉,手机端则从头到尾没收到过那张卡片(通知链路只覆盖任务完成/失败、todo 与轮次结束,问答没有离线兜底),表现就是"手机端问题面板消失了";审批看着正常,只是因为它恰好在线。现在两处 waterfall 桥接都不再按在线与否跳过:请求照常登记并广播,手机的下一条流一建立,SSE 路由就把仍在待答的 approval/question 帧作为开场快照补发(pendingFrames(),与 mux 流同形;客户端按 rpcId 去重,重复回放不会重复入列),并补喂 host 侧待答追踪,让轮询兜底同样看得到。phoneConnected只保留一个用途:桌面链路报"没人能答"而一个手机流都没有时立即收尾(把桌面那条失败原样交回),不再让 agent 空悬。验证:以已配对设备身份旁听插件自己的/m/api/events.mux,实测提问帧带正确sessionId发出;撤掉旁听、只靠手机自己的连接复测,卡片出现,单次mobile.pending轮询 53/57 轮命中待答。契约锁在api-proxy-adapter.test.ts(离线时保留待答、快照可作答、无人可答时不悬停)与tests/mobile-api.spec.ts(新连接补发快照)。 -
手机端的 Ollama Cloud 用量回来了,含"最近 5 小时 / 本周"两条百分比。 Ollama 把云端接口拆成两个,插件原来只打一个,于是两处同时踩空:① 配额百分比不再来自
/api/usage(它的limits.weekly/session.usage字段已经消失),而是搬到GET /api/balance——included.session/weekly.remaining_percent(0..100,剩余)外加各自的resets_at;旧代码把百分比当用量报告里的字段去读,读到一片 undefined,于是那两条进度条整个消失。②/api/usage现在只报请求数({range, scope, granularity, totals:{request_count}, buckets:[…]}),插件也照旧当配额读。修法:余额管百分比、用量管请求数——把remaining_percent换算成已用(1 - remaining/100)喂回两条进度条,resets_at显示成真实重置时间(顺带修掉"无固定重置时刻"这句已经不对的说明),/api/usage?range=7d的请求数继续做摘要与逐日列表。两个端点各自独立失败:余额 500 仍报请求数,用量换形状仍报百分比,两者都认不出才诚实报失败。本机密钥实测:/api/balance回session.remaining_percent=99.31(重置于 2026-10-11T02:00Z)、weekly.remaining_percent=64.13(重置于 2026-10-12T00:00Z);/api/usage?range=7d回totals.request_count=9741+ 8 个按天桶(文档档位只有 24h / 7d / 30d)。契约锁在usage-check.test.ts(余额+用量合并、余额挂掉、用量换形状、全不识别 → error)与SettingsView.test.tsx(真实重置时间渲染)。
dsh-palm 1.5.2
dsh-palm 1.5.2 — 手机端「回到上次」修复
修复
- 「回到上次」阅读位置书签只在用户真的离开底部时才写入(判据与「回到最新消息」一致:
gap > BOTTOM_FOLLOW_THRESHOLD_PX),在最新消息处停留不再产生书签; - 程序化滚动(开场回到底部、轮次定位、前插锚点校正)不再被误判为用户手势(新增
setScrollTopProgrammatically,仅在实际移动容器时置位); - chip 的"是否最新轮"改用宿主轮次大纲投影(
turnOutline)优先、本地已加载轮次兜底,深会话分页加载时不再闪现; - 书签存储前缀
dsh-palm.reading.v1.→dsh-palm.reading.v2.,被写脏的旧书签立即失效并被维护/清配对路径回收。
背景
进入会话时容器仍使用估算高度,程序化回到底部被当成"用户上滚",于是把早期轮次写成了书签 —— 表现为明明没往上滚,下次进来却浮出「回到上次 · 第 N 轮」。
安装
npm i @eternalloveone/dsh-palm@1.5.2
验证
pnpm typecheck通过;pnpm test1176 passed / 9 skipped(新增 5 条回归);- 反证:把
ChatView.tsx回退到修复前,两条"不该写书签"的用例失败 ⇒ 用例确实能抓住该缺陷; - 真机实测:长会话不滚动进出无 chip;上滚后退出再进,chip 准确指回该轮。
v1.5.1
Changed
- The plugin loads on DSH 0.1.6 and later instead of being dropped before it starts. 0.1.6 added a version gate that reads a plugin's
peerDependenciesbefore loading it, and the two official packages were still declared as>=0.1.5-rc.1 <0.1.6-0: on 0.1.7-rc.1 DSH answeredskipping profile bundle "@eternalloveone/dsh-palm": ... is incompatible with dsh 0.1.7-rc.1and dropped the whole bundle, which left the phone surface dead while the desktop kept serving — the gate skips rather than fails, so nothing crashes and the web side shows no warning at all. Both ranges are now<0.1.8-0. Verified against 0.1.7-rc.1 with nothing but the declaration widened: the plugin boots, all 11 runtime contracts are present, a real pairing handshake succeeds, andhost-rpc/mux-streamanswer — the runtime needed no change, matching the static finding that every official-package import in this plugin isimport typeand therefore erased at build time.
Fixed
- The settings page survives DSH 0.1.6's client-settings migration. 0.1.6 moved a client plugin's settings surface from the injected
settingsScopeservice toremote.settings, and a plugin that still resolved the old name simply lost its page — no error, the rows were just gone. The client half now resolves whichever service the running shell actually provides (throughctx.get(name, false), never a static inject) and keeps the 0.1.5 path as the fallback, so one build serves both lines; the resolution is covered by the newtests/client-settings-api.spec.ts. - Every live
Configfield is declared.volatile()so 0.1.6 and later still render it. From 0.1.6 the Loader handsSettingsFormsonly the fields marked volatile; a field declared as a plain value arrives as a snapshot the form cannot write back, which surfaced as 「(无法确定)」 rows and an emptySettingsForms.describe(). The host half marks its live fields volatile and unwraps theVolatile<T>reference the Loader supplies.
v1.5.0
Added
- Long sessions are navigable on the phone again. A floating handle in the bottom-right corner reports 当前 / 总轮 (the turn being read, out of the whole session) as soon as the reader leaves the newest message. Tapping it opens a turn-list sheet — every turn with its prompt and reply preview — and pressing it slides a horizontal scrubber in above the composer: the finger's absolute x maps onto the list, so dragging to either screen edge reaches the oldest and the newest turn, and releasing jumps. The list merges the host's
turnOutlineprojection with the locally loaded turns (deduplicated, ascending) instead of relying on whichever turns happen to be paged in. - Jumping into deep history actually lands. A turn's outline
seqis itsturn/startEVENT seq, which fires before the turn's first message and therefore sits in the gap BETWEEN two rendered rows — matching a row by seq range alone never found it, so a deep jump loaded a page and then stayed put.turnRowIndexnow prefers the row carrying the turn number (the seq range stays as the fallback for search hits), and the page cursor is the NEXT turn's boundary: a page ends at the requestedbeforeSeq, so asking for the turn's own boundary fetched everything before it and excluded the turn itself. Jumps carry a page budget and say 「没能跳到那一轮:历史太深了」 rather than sitting on 「正在载入…」. Verified on a paired phone — a 172-turn session reaches turn 1 from the scrubber. - The phone buzzes when something needs a glance. 设置 → 通用 gains a 震动 switch (on by default), and the notify path taps
navigator.vibrate(15)— only for events that actually raise a notification while the app is in the background, never while the page is visible. Unsupported browsers (iOS Safari ships nonavigator.vibrate), a denied permission and a throwing call are all silently inert by design: haptics must never be able to break the notification path. - The chat remembers where you left off in a session. The reading position is stored per session and restored when it is opened again; it is written once scrolling pauses for a second (a write per pixel would churn localStorage) and flushed on unmount, so a session switch still writes the OLD session's position. Reading the newest turn does not move the bookmark — it points at the mid-history spot the reader actually left from, which is the one 当前/总轮 reports.
- Code and text can leave the app through the system share sheet.
shareTextprefersnavigator.shareand degrades to the clipboard (已复制,可粘贴分享) where the API is missing or the share itself fails; the user merely cancelling the sheet (AbortError) is silent, because cancelling is not a failure.
Fixed
- A connected phone no longer silences the desktop's approval and question panels. The host bridge intercepted both host waterfalls ahead of the desktop chain whenever a phone was online, so
api-remotesnever saw the request: the desktop panel never appeared at all, and a phone that was not looking at that session left the turn hanging until it was interrupted (measured: 1m55s of nothing, then ABORTED). The request now reaches both ends and the first answer wins — the bridge hands it downstream (the desktop) and broadcasts it to the phone, withdraws the desktop's pending event through the gateway's own cancellation path when the phone answers first, and retires the phone's panel and pending tracker withquestion/resolved/approval/resolvedwhen the desktop answers first. A desktop chain with no client looking at that session answersNO_PROVIDERimmediately; that is "the desktop has nothing to answer with", not the end of the race, so the phone's panel stays live and can still answer. Covered by the bridge suite (either order, the preserved turn signal, the desktop-gives-up case) and verified end to end on a paired phone: an answer from the phone drops the desktop's panel at once, an answer from the desktop retires the phone's panel without it coming back after leaving and re-entering the session, and a phone that is not connected leaves the desktop's panel working normally. - One closed phone stream no longer leaves the host believing a phone is online. The SSE close handler decremented the subscriber count and stopped; only the revocation path refreshed the "a phone is connected" flag, so after a backgrounded app or a tunnel blip the bridge kept forwarding approvals and questions to a stream nobody read — for the rest of the process. The flag now tracks the live subscriptions, which is what decides between the dual-answer race and the desktop-only path.
- The remaining horizontal overflow on phone surfaces is gone. Reply and question cards, tool pills, settings rows, run-overview session rows and the turn list could each push the page wider than the viewport (long paths, unbreakable tokens); those containers now shrink and wrap instead of overflowing.