fix(web-shell): preserve token and base path in session URLs - #7926
Conversation
|
✅ Qwen Triage finished — CI landed green on ✅ Qwen Triage 已完成 —— |
|
Thanks for the PR! Template looks good ✓ Problem: observed bug with a concrete before/after. The current Direction: aligned. Web Shell is actively developed and this fixes a tangible UX issue for both local users (token loss on navigation) and reverse-proxy deployments (broken base path). CHANGELOG shows extensive web-shell work from the same author. Size: not applicable — no core paths touched. Approach: the scope feels right. Three surgical changes (stop stripping the token at startup, stop deleting it during session navigation, preserve the base path) directly address the stated problem. No unrelated edits or scope creep. One follow-up thought: Risk: no elevated risk signals. Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题:已观测到的 bug,有具体的 before/after。当前 方向:对齐。Web Shell 正在活跃开发,此修复解决了本地用户(导航时 token 丢失)和反向代理部署(基础路径被破坏)的实际体验问题。CHANGELOG 中有同一作者大量 web-shell 相关贡献。 规模:不适用——未触及核心路径。 方案:范围合理。三处精确修改(启动时不再移除 token、session 导航时不再删除 token、保留基础路径)直接解决所述问题。无无关改动或范围蔓延。一个后续想法: 风险:无升级风险信号。 进入代码审查 🔍 — Qwen Code · qwen3.8-max-preview Reviewed at |
Code ReviewIndependent proposal: I would extract the base path from the current pathname with a regex, stop deleting the token param during session navigation, and remove the Findings: The base-path logic is correct. The regex Two non-blocking observations:
No correctness bugs, security holes, or regressions found. The token-in-URL tradeoff is documented in the PR body and acceptable for the local-daemon use case (standard flow uses URL fragments, which are never sent to the server). TestingThis is an unattended CI run — PR code is not executed here. Test evidence comes from the PR's own CI checks. No failures detected. Two PR CI workflow runs are still in progress (ubuntu test suite + web-shell visual capture). The finalize workflow will update the table below when CI settles. Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 Not verified: real-scenario browser testing (session switching with token and base path). A maintainer can trigger the isolated 中文说明代码审查独立方案: 我会用正则从当前路径中提取基础路径,停止在 session 导航时删除 token 参数,并移除启动时的 发现: 基础路径逻辑正确。正则 两个非阻塞观察:
未发现正确性 bug、安全漏洞或回归。token 保留在 URL 中的取舍已在 PR 描述中记录,对本地 daemon 使用场景可接受(标准流程使用 URL fragment,不会发送到服务器)。 测试这是无人值守 CI 运行——不在此执行 PR 代码。测试证据来自 PR 自身的 CI 检查。 未检测到失败。两个 PR CI 工作流仍在运行中(ubuntu 测试套件 + web-shell 视觉捕获)。finalize 工作流将在 CI 完成后更新下方表格。 未验证:真实场景浏览器测试(带 token 和基础路径的 session 切换)。维护者可触发隔离的 — Qwen Code · qwen3.8-max-preview Reviewed at |
|
Confidence: 4/5 — clean, focused fix; only non-blocking nits (dead code left behind, no test for the new URL logic). This is a well-scoped fix. The problem is real and verifiable from the source — session navigation rewrites the URL from the origin root, dropping any deployment base path, and the token is stripped at both startup and navigation time. The fix addresses all three issues with minimal, correct changes. The regex-based base-path extraction handles edge cases properly (root path, deployed subpath, trailing slashes, no existing session). My independent proposal matched the PR's approach exactly. The code is straightforward — if I had to maintain this in six months, I'd thank the author. The only cleanup items are the now-dead Approval deferred until CI lands green on 中文说明置信度:4/5 — 干净、聚焦的修复;仅有非阻塞小问题(遗留死代码、新 URL 逻辑无测试)。 这是一个范围合理的修复。问题真实存在,可从源码验证——session 导航从域名根路径重写 URL,丢弃部署基础路径,且 token 在启动和导航时均被移除。修复以最小、正确的更改解决了所有三个问题。基于正则的基础路径提取正确处理了边界情况(根路径、部署子路径、尾部斜杠、无现有 session)。 我的独立方案与 PR 的实现完全一致。代码简洁明了——如果六个月后需要维护这段代码,我会感谢作者。唯一的清理项是现已成为死代码的 审批已推迟,等待 CI 在 — Qwen Code · qwen3.8-max-preview Reviewed at |
|
@qwen-code /takeover |
|
🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. Remove the 中文说明🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 |
🖼️ web-shell visual previewRendered against a mock daemon (no real backend): the PR base vs this PR head Screenshots · before / afterℹ️ No screenshot changed against the PR base — but this PR edits 1 render-shaping file:
Either the change has no visual effect (logic, plumbing, a state the scenarios never reach), or no scenario renders this UI — in which case the preview cannot see it, and an empty result is a coverage gap rather than a clean bill of health. To make it visible, add a scenario to Full-resolution recordings (.webm) are attached to the workflow run. — Qwen Code · web-shell visuals |
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI landed green after the review. ✅
Restore the daemon token stripping that was dropped alongside the base-path fix: removeDaemonTokenFromUrl() on startup and the ?token= delete in replaceStandaloneSessionUrl. The ?token= query path is still supported for backward compatibility, so without stripping it leaks into the address bar, history, access logs, and Referer headers. Also extract the session pathname building into buildSessionPathname() with unit tests covering root/sub-path deployments, the no-session case, trailing slashes, and id encoding.
|
🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下: Autofix review round summary (PR #7926)Both review findings were addressed in code in commit Findings[Critical] rc:3665157638 — daemon token no longer stripped from the URL → RESOLVED Valid security regression. The base-path fix had also removed Fix: restored both strips. [Suggestion] rc:3665157647 — base-path URL logic has no test coverage → RESOLVED Implemented. Extracted the pathname building into a pure Conflict notesNone — Verification
中文说明Autofix 审查轮次总结(PR #7926)两条审查意见均已在提交 意见明细[Critical] rc:3665157638 — daemon token 不再从 URL 中移除 → 已解决 确属安全回归。基础路径修复的同时还移除了 修复:恢复这两处移除。 [Suggestion] rc:3665157647 — 基础路径 URL 逻辑缺少测试覆盖 → 已解决 已实现。将路径名构建逻辑提取为纯函数 冲突说明无 —— 验证
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
|
Qwen Code review did not complete successfully. Run review failed. See workflow logs for details. A transient error is retried automatically; if you are seeing this, retry with |
|
🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x), review-pr] pass on current main — merged current main via update-branch; CI will re-run. 中文说明🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x), review-pr] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
中文说明
— qwen3.7-max via Qwen Code /review
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: No code changes this roundThe feedback contains no inline review comments and no code-level findings. The only items are:
CI failure analysisThis PR touches exactly four files, all inside
Local verification results:
The core package shows 81 test failures in this sandbox environment, but every failing test file is byte-identical to No code change is warranted. A CI re-run should clear the transient failure. 中文说明本轮无需修改代码本次反馈不包含任何行内评审评论,也没有代码层面的问题。仅有的条目为:
CI 失败分析本 PR 仅修改了四个文件,全部位于
本地验证结果:
core 包在此沙箱环境中显示 81 个测试失败,但所有失败的测试文件与 无需修改代码。重新运行 CI 应可消除瞬时失败。 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
|
🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run. 中文说明🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。 |
doudouOUC
left a comment
There was a problem hiding this comment.
中文说明
— qwen3.7-max via Qwen Code /review
|
Qwen Code review did not complete successfully. Run review failed. See workflow logs for details. A transient error is retried automatically; if you are seeing this, retry with |
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: No action takenThis round's feedback contains no actionable code findings:
All local verification passes on the current HEAD (
No code changes were made this round. 中文说明未采取任何操作本轮反馈中没有可操作的代码问题:
当前 HEAD(
本轮未做任何代码更改。 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
| // Persist per-tab so the token survives navigations that do not carry it. | ||
| // sessionStorage (not localStorage) keeps the token scoped to this tab and | ||
| // cleared when the tab closes. |
There was a problem hiding this comment.
[Suggestion] The PR title, description, Reviewer Test Plan, and risk note are stale after commit ca58b2eac — they still claim the token "remains in the URL" (title: "preserve token and base path"; test plan: "confirm the token remains in the URL"; After example: /web-shell/session/new#token=secret; risk: "The token intentionally remains visible in the browser address bar and history"), but the code strips it again (removeDaemonTokenFromUrl() at startup and url.searchParams.delete('token') on every session-URL rewrite, both in non-DEV), so the net change is now only the base-path fix — Failure scenario: a reviewer follows the documented test plan in a production build, opens the shell with ?token=, switches sessions, and observes the token being stripped, contradicting the title, test plan, After-evidence, and the stated security tradeoff.
Suggested fix: update the PR title and description to drop the token-retention claims and reframe the PR as the base-path preservation fix (remove the token-related verification steps, the After example with #token=, and the token-visibility risk note).
中文说明
[Suggestion] PR 标题、描述、Reviewer Test Plan 和风险说明在提交 ca58b2eac 之后已经过时——它们仍声称 token 会“保留在 URL 中”(标题:“preserve token and base path”;测试计划:“confirm the token remains in the URL”;修改后示例:/web-shell/session/new#token=secret;风险:“The token intentionally remains visible in the browser address bar and history”),但代码已恢复移除 token 的逻辑(启动时调用 removeDaemonTokenFromUrl(),每次会话 URL 重写时执行 url.searchParams.delete('token'),均在非 DEV 下),因此当前净改动只剩基础路径保留修复——失败场景:评审者按照文档中的测试计划在生产构建中用 ?token= 打开 Web Shell 并切换会话,观察到 token 被移除,与标题、测试计划、修改后示例以及所述安全取舍相矛盾。
建议修复:更新 PR 标题和描述,去掉 token 保留相关的表述,将 PR 重新定位为基础路径保留修复(删除 token 相关的验证步骤、带 #token= 的修改后示例以及 token 可见性的风险说明)。
— kimi-k3 via Qwen Code /review
There was a problem hiding this comment.
Agreed — the PR text is stale. After ca58b2eac the net change is the base-path preservation fix; the token is still stripped (removeDaemonTokenFromUrl() at startup and searchParams.delete('token') on every session-URL rewrite, both in non-DEV), so the title, test plan, After example, and token-visibility risk note no longer match the code.
I have no GitHub credentials and cannot edit the PR title/body from here — the workflow owns network writes. I have written a corrected, reframed pr-title.txt (fix(web-shell): preserve deployment base path in standalone session URLs) and pr-body.md (base-path fix; token handling described as unchanged, with the token-retention verification steps and risk note removed) to the workdir for the workflow/maintainer to apply. Leaving this thread open because I cannot verify the PR text was actually updated.
中文说明
同意——PR 文本确实已过时。在 ca58b2eac 之后,净改动是基础路径保留修复;token 仍会被移除(启动时调用 removeDaemonTokenFromUrl(),每次会话 URL 重写时执行 searchParams.delete('token'),均在非 DEV 下),因此标题、测试计划、修改后示例以及 token 可见性的风险说明已不再与代码相符。
我没有 GitHub 凭据,无法在此处编辑 PR 标题/正文——网络写入由工作流负责。我已将更正后、重新定位的 pr-title.txt(fix(web-shell): preserve deployment base path in standalone session URLs)和 pr-body.md(基础路径修复;将 token 处理描述为未改动,并删除了 token 保留相关的验证步骤和风险说明)写入工作目录,供工作流/维护者应用。该线程保持打开,因为我无法验证 PR 文本是否确实被更新。
| ): void { | ||
| const url = new URL(window.location.href); | ||
| url.pathname = sessionId ? `/session/${encodeURIComponent(sessionId)}` : '/'; | ||
| url.pathname = buildSessionPathname(url.pathname, sessionId); |
There was a problem hiding this comment.
[Suggestion] The wiring in replaceStandaloneSessionUrl — the actual behavior this PR changes — has no test; only the pure util is covered — Failure scenario: a future refactor of this function re-hardcodes `/session/${...}` (the pre-PR code); all 8 sessionPath.test.ts tests still pass because they never touch main.tsx, and the base-path regression this PR fixes ships undetected. Verified: no main*.test.* exists under packages/web-shell/client, and reverting this line to the pre-PR hardcoding would ship green.
Suggested fix: add a small test around replaceStandaloneSessionUrl (or extract it for testability) asserting history.replaceState receives /app/session/<id> when window.location is under a base path such as /app.
中文说明
[Suggestion] replaceStandaloneSessionUrl 的接线——即本 PR 实际改变的行为——没有测试覆盖;只有纯工具函数有测试——失败场景:未来某次重构把该函数改回硬编码 `/session/${...}`(本 PR 之前的写法);sessionPath.test.ts 的全部 8 个测试仍然通过,因为它们不涉及 main.tsx,本 PR 修复的基础路径回归就会在无感知的情况下发布。已验证:packages/web-shell/client 下不存在 main*.test.*,把本行回退为 PR 之前的硬编码可以保持全绿。
建议修复:为 replaceStandaloneSessionUrl 添加一个小测试(或将其提取为可测试的形式),断言当 window.location 位于 /app 这类基础路径下时,history.replaceState 收到的是 /app/session/<id>。
— kimi-k3 via Qwen Code /review
There was a problem hiding this comment.
Partially addressed. The behavior this PR changes — building the session pathname under a base path and reading it back — is now covered by pure unit tests in sessionPath.test.ts, including a build/parse round-trip (parseSessionId(buildSessionPathname(base, id)) === id) for root, sub-path, and /app/session/ bases. I also extracted the parser into parseSessionId() so the writer/parser agreement is testable and locked.
I declined the specific ask to unit-test the history.replaceState wiring inside replaceStandaloneSessionUrl: that function is module-private in the app entry point (main.tsx), which executes ReactDOM.createRoot(...).render(...) at import time, so importing it into a test would require an out-of-scope refactor of the entry module to guard its side effects. The one-line wiring (url.pathname = buildSessionPathname(url.pathname, sessionId)) is exercised indirectly through the round-trip tests.
中文说明
部分处理。本 PR 改动的行为——在基础路径下构建会话路径并回读——现在已由 sessionPath.test.ts 中的纯单元测试覆盖,包括针对根路径、子路径以及 /app/session/ 基础路径的写入/解析往返(parseSessionId(buildSessionPathname(base, id)) === id)。我还将解析端提取为 parseSessionId(),使写入端/解析端的一致性可测试并被锁定。
我拒绝了其中的具体诉求(对 replaceStandaloneSessionUrl 内部的 history.replaceState 接线做单元测试):该函数是应用入口模块(main.tsx)的模块私有函数,该入口在 import 时会执行 ReactDOM.createRoot(...).render(...),因此在测试中 import 它需要对入口模块做超出范围的改造以隔离其副作用。那一行接线(url.pathname = buildSessionPathname(url.pathname, sessionId))通过往返测试被间接验证。
Extract parseSessionId() next to buildSessionPathname() and anchor it to the last /session/<id> segment so the parser agrees with the greedy writer. Previously a base path ending in a session segment produced /app/session/session/<id>, which the first-match parser read back as the literal id "session". Add round-trip and trailing-slash coverage.
|
🤖 Addressed the latest review feedback (round 2/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 2/100 轮)。改动内容与我反驳保留之处如下: Review round summary — PR #7926Feedback triage
ChangesCritical (rc:3667717489, rc:3667959356). The writer ( Suggestion (rc:3667717470). Added the trailing-slash cases ( Suggestion (rc:3667717498). The behavioral invariant this PR changes — building the session pathname under a base path and reading it back — is now covered by pure unit tests, including a build/parse round-trip. I declined the specific ask to unit-test the Suggestion (rc:3667717464). The PR text is indeed stale: after commit Conflict notes
Verification
中文说明评审轮次总结 — PR #7926反馈分类
改动Critical(rc:3667717489、rc:3667959356)。 写入端( Suggestion(rc:3667717470)。 增加了尾随斜杠的用例( Suggestion(rc:3667717498)。 本 PR 改动的行为不变量——在基础路径下构建会话路径并回读——现在已由纯单元测试覆盖,包括写入/解析往返。我拒绝了其中的具体诉求(对 Suggestion(rc:3667717464)。 PR 文本确实已过时:在提交 冲突说明
验证
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline.
中文说明
已审查——无阻断问题。 建议见行内评论。
— qwen3.7-max via Qwen Code /review
|
🤖 Addressed the latest review feedback (round 3/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 3/100 轮)。改动内容与我反驳保留之处如下: Review feedback addressed on PR #7926Feedback points
Conflict notesNo conflict; Verification
中文说明已在 PR #7926 上处理的审查反馈反馈点
冲突说明无冲突; 验证
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline.
中文说明
已审查——无阻断问题。 建议见行内评论。
— qwen3.7-max via Qwen Code /review
|
🤖 Addressed the latest review feedback (round 4/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 4/100 轮)。改动内容与我反驳保留之处如下: Autofix review round — PR #7926One inline suggestion was reviewed and addressed. No blockers, no failed checks, Feedback
Conflict notesNone — Verification
No settings source changed, so 中文说明Autofix 审查轮次 — PR #7926审查并处理了一条行内建议。无阻断问题、无失败检查、无 base 冲突( 反馈
冲突说明无 — 验证
未改动任何 settings 源文件,因此无需运行 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。 🧠 Handled by Qwen Code · model/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅
中文说明
未发现问题。LGTM!✅
— qwen3.7-max via Qwen Code /review
|
@qwen-code /verify |
|
Sandboxed verification: merge-ready (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. 沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 Scripted assertions: 2529 passed · 0 failed · 2529 total 脚本断言:2529 通过 · 0 失败 · 2529 总计 Verification report (report.md)Harness scripts and raw logs are in the workflow run artifacts (7-day retention). — Qwen Code · sandboxed verification |
Local verification report — PR #7926Maintainer-side verification of head Verdict — the code is merge-ready. The base-path fix does what it claims, it is load-bearing, and it does not regress root deployments. One thing should be corrected before merge and it is not code: the PR title and description describe behaviour this PR no longer has. 1. What the diff actually is nowSix files vs merge-base
A completeness grep over 2. Repo gates and test suites (PR head)
Two environment notes, so the numbers are reproducible: 3. Mutation matrix — are the new tests load-bearing?Ten mutants applied one at a time to
M2 is not a coverage gap: the trailing M9/M10 are the real gap, and they are exactly the wiring the reviewer bot noted it had only partially covered. They are what the browser runs below are for — the "base" bundle in §4/§5 is M9+M10+M8 applied together. 4. Real-browser A/B on production bundlesTwo production Each dist is served two ways by a small static host that mirrors
The daemon API is the repo's own
The purple bar in each screenshot is the page reporting its own 5. Real
|
| Step | main (merge-base) |
PR #7926 |
|---|---|---|
Open /web-shell/session/<id>, settled |
/session/<id> |
/web-shell/session/<id> |
| Click "New task" | / |
/web-shell |
| Reload | 200 | 200 |
On a bare qwen serve both paths resolve (its SPA fallback is a catch-all), so there the regression is URL/bookmark identity rather than a 404; the 404 needs the sub-path-only deployment of §4, which is the case the PR describes.
6. Edge-case entry URLs (real browser, both bundles)
| Entry URL | main result |
PR result |
|---|---|---|
/ |
/session/session-new |
/session/session-new |
/web-shell/ |
/session/session-new |
/web-shell/session/session-new |
/a/session/old/ |
/session/session-new |
/a/session/session-new |
/index.html |
/session/session-new |
/index.html/session/session-new |
/session/ |
/session/session-new |
/session/session/session-new |
The last two are the writer treating any non-session pathname as a base path. Both still return HTTP 200 on qwen serve's catch-all fallback and both round-trip correctly through parseSessionId, so they are cosmetic, and the /session/session/<id> shape is already called out in the code's own doc comment. Not a blocker.
7. Findings
F1 — the title and description are stale, and the title becomes the squash-merge subject. Current title: "fix(web-shell): preserve token and base path in session URLs". The body says the token now stays in the URL and gives /web-shell/session/old#token=secret → /web-shell/session/new#token=secret as the "After". Measured on the built PR bundle, the token is stripped from both #token= and ?token=, exactly as on main (§4). Shipping this title would put a false security-relevant claim into the changelog. Suggested title: fix(web-shell): preserve the deployment base path in session and split URLs, with the token paragraphs dropped from the body. This is the only thing I would hold the merge for.
F2 — the main.tsx wiring has no unit test (M9/M10). Non-blocking; the extracted helpers are well covered and the wiring is a one-line delegation each way. If you want it closed cheaply, a jsdom test that stubs window.location/history.replaceState and asserts replaceStandaloneSessionUrl('new') writes /app/session/new from /app/session/old kills both mutants.
F3 — pre-existing, not this PR, but it limits the payoff. On a token-gated daemon (--token / QWEN_SERVER_TOKEN), the SPA fallback sits after bearerAuth, so only GET / is reachable without a bearer. Measured: / → 200, /session/<id> → 401, /web-shell/session/<id> → 401, /anything → 401. With bearer, 200. On the loopback default (no token) all of them are 200. So the deep link this PR now preserves still cannot be refreshed on a token-protected daemon — worth a separate issue, since the "bookmark / revisit" motivation depends on it.
F4 — scope note. Sub-path deployment still needs /assets/* and the daemon API proxied at the origin root: the Vite build uses base: '/' so index.html references /assets/… absolutely, and the client uses window.location.origin as the daemon base URL. This PR fixes the URL identity, not full sub-path hosting. Fine as scoped; just don't read it as "sub-path deployments are now supported end to end".
F5 — merge state. CI is green on 6b9f7f7 (Test ubuntu, web-shell E2E Smoke, review-pr, visuals all ✅) and @qwen-code-ci-bot's latest review is APPROVE, but reviewDecision is still CHANGES_REQUESTED — @wenshao's review of 2026-07-28 17:15 has not been superseded. It needs to be dismissed or re-submitted, ideally after F1 is applied.
8. Reproduce
git fetch origin pull/7926/head:pr7926 && git worktree add --detach wt-pr pr7926
cd wt-pr && npm ci
cd packages/web-shell && npm run build # build-artifact.test.ts needs dist/index.js
../../node_modules/.bin/vitest run --config vitest.config.ts
# A/B: rebuild once with main.tsx + utils/splitUrl.ts from the merge-base,
# serve each dist under MOUNT=/ and MOUNT=/web-shell, drive with mockDaemon.中文说明
本地验证报告 —— PR #7926
以维护者身份对 head 6b9f7f7(对比 merge-base 5b6714b)做的本地验证:独立 worktree + 独立 npm ci、真实的生产 Vite 产物、真实 Chromium,以及真实的 qwen serve 守护进程。
结论 —— 代码可以合并。 base path 修复确实生效、确实是必要的(load-bearing),且不会影响根路径部署。合并前有一处需要修正,但不是代码:PR 标题与描述所写的行为,本 PR 已经不再具备。
1. 当前 diff 的真实内容
相对 merge-base 5b6714b 共 6 个文件:
packages/web-shell/client/config/daemon.ts (仅注释)
packages/web-shell/client/main.tsx
packages/web-shell/client/utils/sessionPath.ts (新增)
packages/web-shell/client/utils/sessionPath.test.ts (新增)
packages/web-shell/client/utils/splitUrl.ts
packages/web-shell/client/utils/splitUrl.test.ts
git diff 5b6714b -- main.tsx 只有 base path 这一处改动。removeDaemonTokenFromUrl() 仍在启动时执行,url.searchParams.delete('token') 仍在每次 session URL 重写时执行 —— token 处理与 main 逐字节一致。autofix 第 1 轮在安全问题被指出后已恢复这两处,但标题与描述始终没有同步更新。浏览器中也已确认(见 §5)。
对 packages/web-shell/client 中所有写 location 的位置(pathname =、history.replaceState/pushState、location.assign/replace、字面量 `/session/` 拼接)做了完整性 grep:结果正好是本 PR 改造的两处(main.tsx:96、splitUrl.ts:39),外加 App.tsx:2917——后者只删除 ?split= 查询参数,从不触碰 pathname。没有遗漏任何写 URL 的位置。
2. 仓库门禁与测试套件(PR head)
| 门禁 | 结果 |
|---|---|
vitest —— sessionPath.test.ts + splitUrl.test.ts + daemon.test.ts |
53 / 53 通过 |
vitest —— packages/web-shell 全量套件 |
2464 / 2464 通过(148 / 148 个文件) |
tsc -p tsconfig.json --noEmit |
干净 |
eslint packages/web-shell --ext .ts,.tsx |
干净 |
对本 PR 6 个文件跑 prettier --check |
干净 |
npm run build(app + lib + tsc -p tsconfig.lib.json) |
通过 |
两个环境注意事项,便于复现:client/build-artifact.test.ts(10 个用例)读取 packages/web-shell/dist/index.js,因此在包内执行过 npm run build 之前会失败 —— 只跑 app 的 vite build 会清空该 dist,这 10 个失败看起来像 PR 破坏,其实不是。另外全仓库 prettier --check packages/web-shell 会报 4 个文件(BranchPickerPopover.module.css、GitModePopover.module.css、dialogs/GitDialog.module.css、client/index.html),它们与 origin/main 逐字节一致,本 PR 未触碰。
3. 变异测试矩阵 —— 新增测试是否真的起作用?
对 sessionPath.ts / splitUrl.ts / main.tsx 逐个注入 10 个变异;每次注入都先验证变异确实落盘(无声的空改动会被误判为"存活")。
| # | 变异 | 是否被杀死 | 捕获它的测试 |
|---|---|---|---|
| M1 | writer 忽略 base path(PR 之前的行为) | ✅ | 两个文件中共 6 个用例 |
| M2 | writer 正则 (.*) → (.*?)(非贪婪) |
⚪ | 等价变异 —— 见下 |
| M3 | writer 正则去掉 \/?$ 尾斜杠容错 |
✅ | strips a trailing slash after an existing session id |
| M4 | writer 去掉 || '/' 根路径兜底 |
✅ | returns "/" when no session is given at the root |
| M5 | writer 去掉 encodeURIComponent |
✅ | encodes the session id |
| M6 | parser 取消末尾锚定(PR 之前的首次匹配正则) | ✅ | reads back the written id when the base path ends in a session segment |
| M7 | parser 去掉 try/catch |
✅ | returns undefined for malformed percent-encoding |
| M8 | buildSplitUrl 重置路径为 /(PR 之前的行为) |
✅ | preserves the deployment base path while stripping the session |
| M9 | main.tsx 接线回退 —— 硬编码 /session/<id> |
❌ 存活 | 无 |
| M10 | main.tsx 接线回退 —— 首次匹配的 getSessionIdFromUrl |
❌ 存活 | 无 |
M2 不是覆盖缺口:末尾的 [^/]+\/?$ 锚点已经强制匹配最后一个 /session/ 段,贪婪与非贪婪不可区分。这一点用差分而非论证证明 —— 两个正则在 730 个生成路径上的 2 920 组 (pathname, sessionId) 中输出完全一致(路径段包含 session、%2F、空格、尾斜杠、嵌套 /session/…/session/…),0 处差异。
M9/M10 才是真正的缺口,也正是评审 bot 自述"只部分覆盖"的那部分接线。下面的浏览器验证就是为它们准备的 —— §4/§5 中的 "base" 产物本质上就是同时施加了 M9+M10+M8。
4. 生产产物的真实浏览器 A/B
同一棵源码树产出两份生产 vite build,仅有 main.tsx 与 splitUrl.ts 不同(换成 merge-base 版本,其余完全一致:同一份 node_modules、同一份 webui/sdk dist)。这一点很关键:token 剥离逻辑由 !import.meta.env.DEV 把守,dev server 根本走不到。
每份 dist 用一个小型静态服务以两种方式托管,其行为对齐 packages/cli/src/serve/web-shell-static.ts:
MOUNT=/——qwen serve自身实现的全捕获 SPA 回退。MOUNT=/web-shell—— 只在子路径下发布 shell 的反向代理(nginxlocation /web-shell/,/下什么都没有);其余路径返回真实 404。
守护进程 API 使用仓库自带的 client/e2e/utils/mockDaemon.ts,因此 session 列表是确定的。同一份 spec 在真实 Chromium 中对两份 dist 各跑一次。
| 观测项 | main(merge-base) |
PR #7926 |
|---|---|---|
打开 /web-shell/session/session-old#token=… 并等待稳定 —— 无任何用户操作 |
/session/session-old ← base path 已丢失 |
/web-shell/session/session-old |
| 在侧边栏点击 "Session NEW" | /session/session-new |
/web-shell/session/session-new |
| 刷新该 URL(等同于打开书签) | HTTP 404 | HTTP 200,shell 正常渲染 |
| Session Overview → 选 2 个 → "Open in new tab" | /?split=session-old%2Csession-new → HTTP 404 |
/web-shell?split=… → HTTP 200 |
根路径部署:/session/session-old → 切换 |
/session/session-new,刷新 200 |
/session/session-new,刷新 200 —— 完全一致,无回归 |
加载后的 #token= |
被剥离 | 被剥离 |
加载后的 ?token= |
被剥离 | 被剥离 |
sessionStorage 中的 token |
e2e-secret-token |
e2e-secret-token |
截图中的紫色横条是页面自己读出的 location.href(在页面内读取 —— Playwright 无法截取浏览器地址栏)。
5. 真实 qwen serve 守护进程 —— 无 mock、无代理
打包后的 CLI(dist/cli.js)跑在 loopback 上,使用独立 HOME、真实工作区,以及通过守护进程自身 POST /session 创建的真实 session。两次运行之间只有内置的 Web Shell dist 不同。
| 步骤 | main(merge-base) |
PR #7926 |
|---|---|---|
打开 /web-shell/session/<id> 并稳定后 |
/session/<id> |
/web-shell/session/<id> |
| 点击 "New task" | / |
/web-shell |
| 刷新 | 200 | 200 |
在裸 qwen serve 上两种路径都能解析(它的 SPA 回退是全捕获的),所以那里的回归表现为 URL/书签身份丢失而非 404;要复现 404 需要 §4 中只挂子路径的部署方式,也正是本 PR 描述的场景。
6. 入口 URL 边界情况(真实浏览器,两份产物)
| 入口 URL | main 结果 |
PR 结果 |
|---|---|---|
/ |
/session/session-new |
/session/session-new |
/web-shell/ |
/session/session-new |
/web-shell/session/session-new |
/a/session/old/ |
/session/session-new |
/a/session/session-new |
/index.html |
/session/session-new |
/index.html/session/session-new |
/session/ |
/session/session-new |
/session/session/session-new |
最后两行是 writer 把任何非 session 路径都视作 base path 的结果。两者在 qwen serve 的全捕获回退下依然返回 HTTP 200,且都能被 parseSessionId 正确回读,因此只是外观问题;/session/session/<id> 这种形态代码自身的注释也已经写明。不构成阻塞。
7. 结论要点
F1 —— 标题与描述已过期,而标题会成为 squash-merge 的提交标题。 当前标题:"fix(web-shell): preserve token and base path in session URLs"。描述中写 token 现在会保留在 URL 中,并给出 "After" 为 /web-shell/session/old#token=secret → /web-shell/session/new#token=secret。在构建后的 PR 产物上实测:token 在 #token= 与 ?token= 两种形式下都被剥离,与 main 完全一致(见 §4)。按现标题合并,会把一条与安全相关的错误说法写进 changelog。建议标题改为 fix(web-shell): preserve the deployment base path in session and split URLs,并从描述中删掉 token 相关段落。这是我唯一会因此暂缓合并的点。
F2 —— main.tsx 的接线没有单测(M9/M10)。 非阻塞;抽出的两个纯函数覆盖充分,接线两侧各只是一行委托。若想低成本补上:写一个 jsdom 测试打桩 window.location/history.replaceState,断言从 /app/session/old 调用 replaceStandaloneSessionUrl('new') 会写出 /app/session/new,即可同时杀死这两个变异。
F3 —— 既有问题,与本 PR 无关,但会限制收益。 在启用 token 的守护进程上(--token / QWEN_SERVER_TOKEN),SPA 回退注册在 bearerAuth 之后,因此不带 bearer 时只有 GET / 可达。实测:/ → 200,/session/<id> → 401,/web-shell/session/<id> → 401,/anything → 401;带 bearer 则为 200。loopback 默认(无 token)下全部 200。也就是说,本 PR 现在保留下来的深链,在受 token 保护的守护进程上仍然无法直接刷新 —— 建议单独开 issue,因为"书签 / 重新访问"这个动机依赖于它。
F4 —— 范围说明。 子路径部署仍然需要把 /assets/* 和守护进程 API 代理到站点根:Vite 构建使用 base: '/',index.html 以绝对路径引用 /assets/…,客户端也以 window.location.origin 作为守护进程 base URL。本 PR 修的是 URL 身份,而不是完整的子路径托管。作为当前范围没问题,只是别把它理解成"子路径部署已端到端支持"。
F5 —— 合并状态。 6b9f7f7 上 CI 全绿(Test ubuntu、web-shell E2E Smoke、review-pr、visuals 均 ✅),@qwen-code-ci-bot 的最新评审为 APPROVE,但 reviewDecision 仍为 CHANGES_REQUESTED —— @wenshao 在 2026-07-28 17:15 提交的评审尚未被覆盖,需要 dismiss 或重新提交,建议在 F1 修正之后再处理。
8. 复现方式
git fetch origin pull/7926/head:pr7926 && git worktree add --detach wt-pr pr7926
cd wt-pr && npm ci
cd packages/web-shell && npm run build # build-artifact.test.ts 需要 dist/index.js
../../node_modules/.bin/vitest run --config vitest.config.ts
# A/B:再构建一次,把 main.tsx 与 utils/splitUrl.ts 换成 merge-base 版本,
# 两份 dist 分别以 MOUNT=/ 和 MOUNT=/web-shell 托管,用 mockDaemon 驱动。|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: Autofix review round — no code changeThis round triaged the two issue-level comments newer than the last evaluation: the CI bot's sandboxed verification (verdict: merge-ready, 2529/2529 assertions) and maintainer @wenshao's local verification report (verdict: code is merge-ready). Both confirm the base-path fix is correct and load-bearing. No code change was made this round, because every actionable point is either GitHub metadata this bot cannot modify, an out-of-scope/informational note, or a non-blocking suggestion that is not worth the diff growth. Finding-by-finding dispositionF1 — stale PR title and body (the maintainer's only merge blocker): not a code change; requires maintainer/workflow action. F2 — unit test for the F3 — token-gated daemon SPA fallback sits behind F4 — sub-path deployment still needs F5 — Inline commentsThere were no inline comments (and no reviews) newer than the last evaluation, so there are no review threads to resolve and no inline findings to reply to. VerificationNo code changed this round, so no build/typecheck/lint/test commands were required or run. The PR head is unchanged at 中文说明Autofix 评审轮次 —— 本轮不做代码改动本轮分诊了上次评估之后新增的两条 issue 级评论:CI bot 的沙箱验证(结论:可合入,2529/2529 项断言全部通过)与维护者 @wenshao 的本地验证报告(结论:代码可以合并)。两者都确认 base path 修复正确且是必要的(load-bearing)。本轮不做任何代码改动,因为每一个可执行的点要么是本网关无法修改的 GitHub 元数据,要么是超出范围/仅作说明的备注,要么是不值得扩大 diff 的非阻塞建议。 逐条结论F1 —— PR 标题与描述已过期(维护者唯一的合并阻塞项):这不是代码改动,需要维护者/工作流处理。 F2 —— 为 F3 —— 启用 token 的守护进程其 SPA 回退注册在 F4 —— 子路径部署仍需要把 F5 —— 行内评论上次评估之后没有新增的行内评论(也没有新增评审),因此没有需要 resolve 的评审线程,也没有需要回复的行内结论。 验证本轮没有代码改动,因此不需要、也没有运行任何 build/typecheck/lint/test 命令。PR head 保持不变,仍为 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
chiga0
left a comment
There was a problem hiding this comment.
Review — PR #7926 fix(web-shell): preserve token and base path in session URLs
Verdict: APPROVE — no Critical or Major issues at HEAD 6b9f7f7.
Summary
Extracts session-URL construction/parsing into a shared utility (sessionPath.ts) that preserves deployment base paths (e.g. /app/session/<id> no longer resets to /session/<id>), and wires it into main.tsx (replaceStandaloneSessionUrl) and splitUrl.ts (buildSplitUrl). The greedy writer (/^(.*)\/session\/[^/]+\/?$/) and last-segment-anchored reader (/\/session\/([^/]+)\/?$/) are provably consistent — round-trip tested across root, sub-path, trailing-slash, and base-path-ending-in-session scenarios.
Verification highlights
- Token stripping intact:
removeDaemonTokenFromUrl()at startup (main.tsx:180) +searchParams.delete('token'|'daemon')inreplaceStandaloneSessionUrl(lines 108-109, production only). Split-view URLs carry the token via fragment (#token=…), never query — not sent to server/logs/Referer. - Base-path-ending-in-
sessionround-trip:buildSessionPathname('/app/session/', 'id')→/app/session/session/id;parseSessionIdanchored to the last/session/<id>correctly reads backid. Explicitly covered by the round-trip test. - splitUrl.ts: now uses
buildSessionPathname(url.pathname, undefined)— strips session while preserving base path; new test asserts/app/session/x→/app. - Test coverage: 98-line
sessionPath.test.ts(build × 8 cases, parse × 7, round-trip × 2) + splitUrl regression. Malformed percent-encoding and empty-id edge cases included.
Cross-validation notes
| Reviewer finding | Status at HEAD |
|---|---|
| ci-bot [Critical]: token no longer stripped | OBSOLETE — commit ca58b2e restored stripping; verified in final code |
wenshao [Critical]: base path ending in session breaks round-trip |
Incorrect — greedy writer produces /app/session/session/<id>, last-segment reader agrees; round-trip test passes |
| wenshao [Suggestion]: no trailing-slash-after-session test | Addressed — "strips a trailing slash after an existing session id" test added |
ci-bot [Suggestion]: splitUrl still hardcodes / |
Fixed in current diff |
| wenshao [Suggestion]: PR title/body stale | Valid (documentation only) — body still claims token preserved; code strips it |
Minor observation (non-blocking)
The PR description and test plan still reference the intermediate "keep token in URL" approach (pre-ca58b2e). Worth a quick edit to avoid confusing future readers of the merged history.
Independent blind review (7-round + structural audit + adversarial pass) followed by cross-validation against existing findings.
|
Released in v0.21.2. |



What this PR does
Keeps the daemon authentication token in the standalone Web Shell URL after startup and session changes. Session navigation now replaces only the existing session suffix while preserving any deployment base path that precedes it.
Why it's needed
For local Qwen Code users, removing the token from the URL makes copied, bookmarked, or revisited links lose the credential context and creates unnecessary friction. For deployments behind a reverse proxy or another subpath, switching sessions currently rewrites the URL from the origin root and drops the deployment prefix, producing a broken link.
Reviewer Test Plan
How to verify
Open the standalone Web Shell with a token in either the query string or fragment, switch to another session, and confirm the token remains in the URL. Open the shell under a base path such as
/web-shell/session/old, switch to another session, and confirm the resulting path is/web-shell/session/new. Start a new session from the same deployment and confirm the base path remains intact.Evidence (Before & After)
Before:
/web-shell/session/old#token=secretbecame/session/newand the token was removed.After:
/web-shell/session/old#token=secretbecomes/web-shell/session/new#token=secret.A production Web Shell build completed successfully, and the packaged asset was inspected to confirm that token deletion is absent and base-path-preserving session navigation is included.
Tested on
Environment (optional)
Local
npm run dev:daemonwith the Vite Web Shell, plus a production Vite build copied through the release asset packaging path.Risk & Scope
qwen serve --openflow uses a URL fragment, so the token is not sent to the server as part of the request URL.Linked Issues
N/A
中文说明
本 PR 做了什么
独立 Web Shell 启动及切换 session 后,不再从 URL 中移除 daemon 认证 token。session 导航现在只替换已有的 session 路径尾部,并保留其前面的部署基础路径。
为什么需要
对于本地使用 Qwen Code 的用户,从 URL 中移除 token 会导致复制、收藏或重新访问的链接丢失凭据上下文,带来不必要的使用障碍。对于通过反向代理或其他子路径部署的场景,当前 session 切换会从域名根路径重写 URL,删除部署前缀并产生异常链接。
Reviewer Test Plan
如何验证
使用 query 或 fragment 中带 token 的 URL 打开独立 Web Shell,切换到其他 session,确认 token 仍保留在 URL 中。通过
/web-shell/session/old这样的基础路径打开页面,切换 session,确认新路径为/web-shell/session/new。在同一部署路径中新建 session,确认基础路径仍然保留。证据(修改前后)
修改前:
/web-shell/session/old#token=secret会变为/session/new,同时 token 被移除。修改后:
/web-shell/session/old#token=secret会变为/web-shell/session/new#token=secret。生产 Web Shell 构建已成功完成,并已检查最终打包资源,确认其中不再包含 token 删除逻辑,同时包含保留基础路径的 session 导航逻辑。
测试平台
环境(可选)
本地
npm run dev:daemon配合 Vite Web Shell,以及通过发布资源复制链路生成的生产 Vite 构建。风险与范围
qwen serve --open流程使用 URL fragment,因此 token 不会作为请求 URL 的一部分发送给服务器。关联 Issue
无