Files
Neko 8ad5b8bfd7 🔨 chore: sync release/weekly-20260827 with latest canary (#18786)
* ✨ feat(heterogeneous-agent): support Grok provider bindings (#18737)

* ✨ feat(db): add workspace document comment schema (#18726)

* ✨ feat(db): add workspace document comment schema

* ♻️ refactor(db): use UUIDs for document comments

* 🐛 fix(db): sync comments during document transfers

* 🐛 fix(db): harden document comment transfers

* 🔨 chore(db): regenerate document comment migration

* 🐛 fix: align chat output cost estimate ratio (#18748)

* ♻️ refactor(desktop): migrate renderer OTA to V2 packs (#18745)

* ♻️ refactor(desktop): migrate renderer OTA to V2 packs

* ⚡️ perf(desktop): decode renderer packs asynchronously

* 💄 style(toast): align toast radius and offsets with desktop layout container (#18755)

* 👷 ci(desktop): read the overlay repository from a variable instead of hardcoding (#18759)

Claude-Session: https://claude.ai/code/session_01TQcoZnDo12NGCxWZDJpciZ

* 👷 ci(share): add PR-time verify workflow for the share micro app (#18757)

* 👷 ci(share): add PR-time verify workflow for the share micro app

Claude-Session: https://claude.ai/code/session_01TQcoZnDo12NGCxWZDJpciZ

* 👷 ci(share): build same-repo PR previews with the cloud overlay

Claude-Session: https://claude.ai/code/session_01TQcoZnDo12NGCxWZDJpciZ

* 📝 docs(skill): record dual-repo share verify and skeleton layout parity

Claude-Session: https://claude.ai/code/session_01TQcoZnDo12NGCxWZDJpciZ

* 👷 ci(share): keep overlay naming neutral in public workflow

Claude-Session: https://claude.ai/code/session_01TQcoZnDo12NGCxWZDJpciZ

* 👷 ci(share): reuse the existing overlay token secret

Claude-Session: https://claude.ai/code/session_01TQcoZnDo12NGCxWZDJpciZ

* ♻️ refactor: migrate lobe-cron to lobe-task (#18753)

* 💄 style(share): center skeleton hero to match hydrated layout (#18756)

Claude-Session: https://claude.ai/code/session_01TQcoZnDo12NGCxWZDJpciZ

* 💄 style: register dedicated skeletons for all lazy routes (#18752)

* 💄 style: add dedicated home route skeleton

* 💄 style: register route skeletons for all lazy routes

* ✅ test: align unit test mocks with base-ui component migration

* ✅ test: complete base-ui mock alignment for migrated components

Claude-Session: https://claude.ai/code/session_01UTDivfaszDuBjpPn7PV7gQ

* 📌 chore: pin @lobehub/ui to 5.33.1 until base-ui migration completes

Claude-Session: https://claude.ai/code/session_01UTDivfaszDuBjpPn7PV7gQ

* ♻️ refactor: finish ActionIcon, Avatar, Tag and Text base-ui migration

Completes #18729: migrates the 45 remaining call sites (plus type-only
imports) to @lobehub/ui/base-ui and drops the temporary 5.33.1 pin now
that the repo is clean under 5.33.3's expanded deprecation list.

Claude-Session: https://claude.ai/code/session_01UTDivfaszDuBjpPn7PV7gQ

* ♻️ refactor: migrate direct antd Tag and Avatar imports to base-ui

Claude-Session: https://claude.ai/code/session_01UTDivfaszDuBjpPn7PV7gQ

* ✅ test: stub base-ui ActionIcon in input banner queue case (#18760)

* ✅ test: stub base-ui ActionIcon in input banner queue case

The real base-ui ActionIcon requires the app-level MotionProvider, which
the unit env does not set up.

Claude-Session: https://claude.ai/code/session_01UTDivfaszDuBjpPn7PV7gQ

* ✅ test: render real base-ui components via a global MotionProvider stub

Redirect @lobehub/ui's internal MotionProvider module to a static stub in
vitest config so base-ui components render in unit tests without the
app-level ConfigProvider, and add composable canonical base-ui stubs
(~base-ui-stubs) for tests that want simplified DOM.

Claude-Session: https://claude.ai/code/session_01UTDivfaszDuBjpPn7PV7gQ

* ✅ test: unblock closed antd-style factories against migrated base-ui imports

Six antd-style mocks were closed factories missing createStaticStyles /
keyframes / createGlobalStyle that real base-ui style modules now need;
compose them over importOriginal. PluginTag and GroupItem retarget their
UI stubs to the base-ui import path the components migrated to.

Claude-Session: https://claude.ai/code/session_01UTDivfaszDuBjpPn7PV7gQ

* ✨ feat: support workspace document comments (#18750)

* ✨ feat: support workspace document comments

* ✨ feat: enhance document comments with mentions and attachments

* 🐛 fix: synchronize document comment activity

* 🐛 fix: guard document comment attachment uploads

* 🐛 fix: preserve comment synchronization metadata

* 🐛 fix: scope document comment API routes

* ✅ test: align base-ui component mocks

* ✅ test: complete base-ui mock migration

* ✅ test: clean up rebased component mock

* ♻️ refactor: align comment imports with base-ui

* ✅ test: align mocks with latest canary

* 🐛 fix(connect-agent): prevent device loading state flash (#18768)

* 🐛 fix(connector): canonicalize connector ownership (#18763)

* 🐛 fix(eval): rm eval hints in run topic titles (#18771)

* 🐛 fix(eval): isolate run topic titles

* 🐛 fix(eval): keep ordinal topic titles

* 🐛 fix(eval): retain run topic input

* 🐛 fix(eval): remove fallback case label

* 🐛 fix(agent): scope heterogeneous default names to shared workspaces (#18770)

* 👷 ci: upgrade pnpm setup action to v6 (#18777)

* 🐛 fix(queue): encode QStash deduplication IDs (#18778)

* 🐛 fix(agent): recover sub-agent bridge answer from isolation thread for hetero children (#18781)

* 🐛 fix(agent): recover the sub-agent bridge's answer from its isolation thread for hetero children

callAgent-invoked heterogeneous (CLI-driven) sub-agents backfill their
parent's placeholder tool message with "Sub-agent completed without a
textual answer." even when the CLI produced a real reply — always, in
production's queue mode (AGENT_RUNTIME_MODE=queue), not intermittently.

completeSubAgentBridge/completeGroupActionMember resolve content from
params.finalState, falling back to coordinator.loadAgentState(operationId)
(the standard runtime's Redis-backed step state) when finalState isn't
passed in-process. Both createSubAgentBridgeHook and
createGroupActionMemberBridgeHook's webhook config deliberately strip
finalState/lastAssistantContent from the QStash payload via `eventFields:
['operationId', 'reason', 'status']` (to keep the payload lean), so the
queue-mode callback always arrives with finalState undefined — and hetero
children never write into the coordinator's runtime state at all (confirmed
zero `saveAgentState` calls under services/heterogeneousAgent/), so the
fallback is structurally dead for them. heteroFinish already resolves the
real answer server-side (via topics.metadata.heteroCurrentMsgId ->
messageModel.findById) before dispatching, but that resolution never
reaches the callback.

Add resolveLastAssistantContentFromThread(threadId): queries the child's own
isolation thread directly (thread ids are globally unique, no agentId/topicId
needed to disambiguate) — the same source heteroFinish itself reads. Used as
a fallback in both completeSubAgentBridge and completeGroupActionMember
(isolated mode) when the existing finalState/coordinator resolution comes up
empty, before falling through to the "no textual answer" stub.

Two regression tests added (one per bridge method), reproducing the actual
hetero shape (loadAgentState resolves null, not a message-stripped state) —
verified to fail against the pre-fix code and pass with it. Full
AgentRuntimeService.test.ts suite (114 tests) still passes.

Fixes LOBE-13586

* 🐛 fix(agent): gate the thread content fallback on missing finalState

Address a P2 flagged by Codex review on 15a401edd1: the thread-fallback
guard checked only `!lastAssistantContent`, which also fires when a REAL,
authoritative finalState is present but its last turn is legitimately
textless (image-only, or the "preserve an empty leaf" case in
normalizeCompletionMessages). In that case the fallback would still query
the child's isolation thread — and a lagging read that surfaces an EARLIER
real reply from the same thread would silently show stale text instead of
the correct "no textual answer" stub.

Narrow the guard to `!finalState` in both completeSubAgentBridge and
completeGroupActionMember: this is exactly the heterogeneous-child case the
fallback exists for (finalState is never populated for them at all), and
`!finalState` already implies an empty lastAssistantContent, so nothing is
lost for the case the fallback is meant to cover.

Two regression tests added (one per bridge method) proving the fallback is
never invoked when a real finalState says the answer is textless — verified
to fail against the pre-fix (`!lastAssistantContent`) guard and pass with
the narrowed one. Full AgentRuntimeService.test.ts suite (116 tests) passes.

---------

Co-authored-by: AmAzing- <115673583+AmAzing129@users.noreply.github.com>
Co-authored-by: Rdmclin2 <rdmclin2@gmail.com>
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
Co-authored-by: Innei <i@innei.in>
Co-authored-by: Rylan Cai <67412196+cy948@users.noreply.github.com>
Co-authored-by: Tsuki <76603360+sudongyuer@users.noreply.github.com>
Co-authored-by: LiJian <onlyyoulove3@gmail.com>
2026-08-28 17:40:47 +08:00
..

Renderer OTA 本地 E2E 测试

在本地完整走通 V2 协议:检查 → 下载一个 full/delta pack → staged toast → 刷新应用 → boot ping 提交,以及坏增量包自动改下 full pack。全程 dev 模式,不需要打包。

所有命令在 apps/desktop/ 下执行。

协议边界

  • V2 客户端只访问 /<channel>/<appVersion>/renderer/v2,本地状态只写入 renderer-ota-v2。
  • latest.json / versions/rN.json 只负责选择 pack;目标文件 tree 与 delta 重建信息放在所选 ZIP 的 meta.json 中。
  • 新构建不包含 V1 manifest/CAS 解析、URL fallback 或 pointer 迁移。
  • 发布流程不会删除或覆盖远端既有 V1 feed/object;旧客户端继续停留在冻结的 V1 数据上。

0. 生成测试密钥 (一次)

node scripts/buildRendererManifest.mjs --gen-key > /tmp/ota-keys.pem
# 拆成两个文件:第一段 PRIVATE KEY 存 /tmp/ota-priv.pem,第二段 PUBLIC KEY 存 /tmp/ota-pub.pem

1. 构建 v0 renderer (内置基线)

npm run build:renderer
rm -rf /tmp/lobehub-ota-r0
cp -R dist/renderer /tmp/lobehub-ota-r0

2. 以「生产形态」启动 dev 应用

静态 renderer (不走 Vite 代理)+ 强制启用 OTA + 5 秒后首查:

DESKTOP_RENDERER_STATIC=1 \
  RENDERER_OTA_FORCE=1 \
  RENDERER_OTA_CHECK_DELAY=5000 \
  RENDERER_OTA_PUBLIC_KEY="$(cat /tmp/ota-pub.pem)" \
  UPDATE_SERVER_URL=http://127.0.0.1:8787 \
  npm run dev

此时 feed 还没起,日志应出现 Renderer OTA check failed(fetch 拒连) 或 404 —— 属预期。

3. 做一个肉眼可见的 renderer 改动并发布 r1

改任意 renderer 侧文案 (例如 src/features/ 下某个标题), 然后:

npm run build:renderer
RENDERER_OTA_PRIVATE_KEY="$(cat /tmp/ota-priv.pem)" \
  node scripts/buildRendererManifest.mjs \
  --renderer=dist/renderer --out=/tmp/ota-feed --channel=stable --version=r1 \
  --from-dir=/tmp/lobehub-ota-r0 --from-version=r0
node scripts/renderer-ota-test/serveOta.mjs /tmp/ota-feed 8787

注意 --channel 要和应用实际渠道一致 (dev 默认 stable; feed 路径为 /<channel>/<appVersion>/renderer/v2)。

4. 验收 happy path

  • 等下一轮检查,或在 DevTools console 手动触发: await window.electronAPI.invoke('rendererOta.checkNow')
  • serveOta 日志:每次检查只下载一个 packs/<sha256>.zip;pack 内 meta.json 携带目标 tree,增量 pack 另含新增对象与 zstd dictionary patch
  • 应用左下角出现「新版本已就绪,刷新即可使用」toast
  • 点「立即刷新」: 窗口 reload (应用不重启), 改动的文案出现
  • ~/Library/Application Support/<dev userData>/renderer-ota-v2/stable/pointer.json: current: "r1", 收到 boot ping 后 pendingBootCheck: false
  • versions/ 只留 current (+previous)

5. 验收回退路径

发布一个必挂的 r2: 构建后把 dist/renderer/assets/entry-*.js 的内容整体替换为 throw new Error('boom')(bundle 求值即抛 → 发不出 loaded ping; 注意不能删 script 引用 —— 没有 script 的 index.html 会被 staging 期完整性检查直接拒掉), 再按步骤 3 发布 --version=r2。

  • checkNow → toast → 刷新:白屏 / 报错页
  • 约 3 秒自动回退 (loaded ping 未到); 若 bundle 能求值但挂不上,则 15 秒兜底
  • pointer.json:current: "r1",blacklist: ["r2"]
  • 再次 checkNow:r2 被拉黑,不再下载

6. 验收 mainHash 门禁

改一行 src/main/ 下的代码 (不重启应用), 重新按步骤 3 发布 r3: manifest 的 mainHash 会与运行中应用注入的不一致 → 日志 Manifest mainHash mismatch, 拒绝更新。

已知差异 (dev vs 打包)

  • dev 的 MAIN_HASH 在 vite 启动时计算;启动后改 main 源码不会让运行中的 应用变 hash (打包产物没有这个问题)。
  • RENDERER_OTA_FORCE / RENDERER_OTA_CHECK_DELAY 是运行时 env, 打包产物 不设置即无效,不影响生产行为。