Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,18 @@ function looksLikeFilePath(text) {
if (/[\w.\-]+\/[\w.\-]+\.\w{1,12}/.test(t)) return true;
return false;
}
var MENU_UI_SELECTOR = [
'[role="menu"]',
'[role="menuitem"]',
'[role="menuitemradio"]',
'[role="menuitemcheckbox"]',
'[role="listbox"]',
'[role="option"]',
'[aria-haspopup="menu"]'
].join(",");
function isMenuUi(el) {
return el.closest(MENU_UI_SELECTOR) !== null;
}
async function copyText(text) {
try {
if (navigator.clipboard?.writeText) {
Expand Down Expand Up @@ -486,6 +498,7 @@ function startFileGuard(readFile) {
if (target === null) return;
const el = target.closest("button, a");
if (el === null) return;
if (isMenuUi(el)) return;
if (!looksLikeFilePath(el.textContent)) return;
event.preventDefault();
event.stopImmediatePropagation();
Expand All @@ -496,6 +509,7 @@ function startFileGuard(readFile) {
const links = document.querySelectorAll("button, a");
links.forEach((el) => {
if (el.getAttribute("data-mobile-nav-copy") === "1") return;
if (isMenuUi(el)) return;
const txt = (el.textContent ?? "").trim();
if (!looksLikeFilePath(txt)) return;
el.setAttribute("data-mobile-nav-copy", "1");
Expand Down
22 changes: 22 additions & 0 deletions client/mobile/fileGuard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,26 @@ function looksLikeFilePath(text: string | null): boolean {
return false
}

// 菜单 / 下拉 / 选项列表类 UI 一律不算文件链接:模型选择器触发按钮带
// aria-haspopup="menu",各行是 role="menuitemradio"/"menuitem"(面板
// role="menu"/"group"),而行文案就是模型 ID(如 z-ai/glm-5.3)——会被
// looksLikeFilePath 误判成文件路径,导致模型点选被拦截、旁边被注入
// 「复制」按钮。凡位于这类 UI 内的元素直接跳过。
const MENU_UI_SELECTOR = [
'[role="menu"]',
'[role="menuitem"]',
'[role="menuitemradio"]',
'[role="menuitemcheckbox"]',
'[role="listbox"]',
'[role="option"]',
'[aria-haspopup="menu"]',
].join(',')

/** 元素是否位于菜单 / 选择器 UI 内(模型选择器、推理力度菜单等)。 */
function isMenuUi(el: Element): boolean {
return el.closest(MENU_UI_SELECTOR) !== null
}

/** 写剪贴板:优先 navigator.clipboard,非安全上下文(局域网 http)回退 execCommand。 */
async function copyText(text: string): Promise<boolean> {
try {
Expand Down Expand Up @@ -107,6 +127,7 @@ export function startFileGuard(
if (target === null) return
const el = target.closest('button, a') as HTMLElement | null
if (el === null) return
if (isMenuUi(el)) return
if (!looksLikeFilePath(el.textContent)) return
event.preventDefault()
event.stopImmediatePropagation()
Expand All @@ -121,6 +142,7 @@ export function startFileGuard(
const links = document.querySelectorAll('button, a')
links.forEach((el) => {
if (el.getAttribute('data-mobile-nav-copy') === '1') return
if (isMenuUi(el)) return
const txt = (el.textContent ?? '').trim()
if (!looksLikeFilePath(txt)) return
el.setAttribute('data-mobile-nav-copy', '1')
Expand Down
16 changes: 16 additions & 0 deletions test/file-guard.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ test('fileGuard 只依赖稳定结构(button/a + 路径文案),不依赖 h
assert.ok(!/\[class[*^$]?=/.test(src), 'fileGuard.ts 的检测不能出现 class 属性选择器(hash 类名每次构建都变)');
});

test('fileGuard 豁免菜单/选择器 UI:模型行文案是模型 ID,不能被当文件链接', () => {
// 模型选择器(dsh-client-ui-model-selection)触发按钮带 aria-haspopup="menu",
// 各行是 role="menuitemradio"/"menuitem",文案就是模型 ID(如 z-ai/glm-5.3)——
// 与文件路径(lib/proxy.mjs)在文本上无法区分,必须按 ARIA 上下文豁免,
// 否则模型点选被拦截(误弹「手机上无法直接打开电脑上的文件」)、行旁被注入复制按钮。
assert.ok(src.includes('function isMenuUi'), '必须先判断元素是否位于菜单/选择器 UI 内');
assert.ok(src.includes('[role="menuitemradio"]'), '豁免必须覆盖菜单单选项(模型行、推理力度行)');
assert.ok(src.includes('[role="menuitem"]'), '豁免必须覆盖菜单项(根面板单元格)');
assert.ok(src.includes('[aria-haspopup="menu"]'), '豁免必须覆盖菜单触发按钮');
// 点击拦截与复制按钮注入都要先过 isMenuUi,再看文本像不像路径
assert.ok(/if \(isMenuUi\(el\)\) return\n if \(!looksLikeFilePath/.test(src), '点击拦截必须先豁免菜单 UI');
assert.ok(/if \(isMenuUi\(el\)\) return\n const txt/.test(src), '复制按钮注入必须先豁免菜单 UI');
});

test('mobile-apply 已接线 startFileGuard(窄屏生效、传入 readFile,且不再接 fileCopy)', () => {
assert.ok(apply.includes("import { startFileGuard } from './fileGuard.ts'"), '必须 import 模块');
assert.ok(apply.includes('startFileGuard(readFile)'), '必须调用 startFileGuard 并传入 readFile 回调');
Expand All @@ -43,6 +57,8 @@ test('打包产物含守卫 + 复制按钮结构标记', () => {
// 复制按钮:注入标记 + 经 RPC 读文件(data-mobile-nav-copy 标记已处理链接,避免重复注入)
assert.ok(bundle.includes('copy-file'), '产物必须含复制按钮标记 data-mobile-nav="copy-file"');
assert.ok(bundle.includes('data-mobile-nav-copy'), '产物必须用标记避免重复注入复制按钮');
// 菜单 UI 豁免:模型选择器等菜单里的按钮文案是模型 ID,不能被当文件链接
assert.ok(bundle.includes('isMenuUi'), '产物必须含菜单 UI 豁免逻辑——先跑 node client/build.mjs');
});

test('CSS 隐藏「添加工作区」图标 + 复制按钮样式(窄屏)', () => {
Expand Down