Replies: 6 comments 3 replies
|
补充两条信息:
协助撰写:查到底的大肥鱼姐姐(DSH 内的 AI agent) |
你们把源码都核过了——请把那段代码位置直接贴出来,我这边就能立刻对比1. 先说我能确认的一侧Markdown 渲染器对链接是有策略的:本地链接不会直接由渲染器打开,而是走一个由 owner 提供的回调( ⇒ 所以"相对路径能开、绝对路径报「文件不存在」"这个不对称,最可能出在回调那一侧对路径的归一化/解析上,而不是渲染器本身:
这个区分很重要:它决定修法是"把绝对路径也正确地归一化并允许",还是"明确拒绝并给出可理解的提示"。 2. 请把你们核到的那处贴出来(你写了"见代码位置一节")你们在正文里说"问题在官方 master 的对应源码中同样存在(逐字比对过)"。⇒ 请把那段代码与文件路径内联进正文(不要只放在附件或只说"代码位置")。理由:
给了文件 + 符号(+ 那几行),我就能和当前 3. 建议的诉求(可判定、且不依赖你我的定位)
**"报错要区分『不存在』与『不允许』"**这一点值得单列——现在两者都表现为同一句文案,用户无法判断是自己写错了路径还是策略拦了。 4. 请补三样
5. 一点方法论上的肯定你们写明"复现/定位由 agent 协助、结论已在本机实测",并标注了安装形态与 FileVersion——这类来源声明很有用(它让读者知道证据是谁测的)。建议保留,并把第 4 节的三样一并放进"证据"一节。 一条边界我确认的是**渲染器把本地链接交给 owner 回调、且无回退到"直接打开"**这一层策略。具体的解析不对称出在哪一行,要等你们把那段代码贴出来——我不想在没有它的情况下替你们断言"是包含性检查"还是"拼接方式"。 |
更正:原报告的核心判断不成立先更正一件事:原报告标题与正文写的「绝对路径被当成相对路径」是错的, 行为级(已证):含「 机制层位(强推断):丢掉的位置恰好是 决定性证据一:预览器标签直出的就是客户端解析结果
两处变化:① 分隔符变 文件系统核对: 决定性证据二:证伪实验(把文件放到「错位后」的路径上)在 → 它打开了,显示的正是那个文件,标签上是 ⇒ 行为级已证:客户端最终请求的目标路径,就是那个「少了一个分隔符」的 实测对照环境:DSH
统一解释:唯一的变量是「有没有合法转义对」全部 12 条用同一条规则解释得通:
规则:链接目标里只要出现「反斜杠 + ASCII 标点」这一合法转义对,那个反斜杠就被 Markdown 吃掉 逐条对照:
回应你的第 2 点:代码位置(含内联)以下均取自本机 1. 渲染器 → owner 回调这一层,我们核实成立
2. 但不对称不在回调里。 这条分两级说清证据强度:
对应代码是 export function parseFileLink(value: string): { path: string; line?: number } | undefined {
const hash = value.indexOf('#')
const destination = hash < 0 ? value : value.slice(0, hash)
if (destination.includes('?')) return undefined
let path: string
try {
path = decodeURIComponent(destination)
} catch (_error) {
// Malformed percent escapes cannot identify a file unambiguously.
return undefined
}
if (path.length === 0 || /[\u0000-\u001f\u007f]/.test(path)
|| /^[\\/]{2}/.test(path)
|| (/^[a-z][a-z\d+.-]*:/i.test(path) && !/^[a-z]:[\\/]/i.test(path))) return undefined
if (hash < 0) return { path }
const fragment = value.slice(hash + 1)
const match = /^L([1-9]\d*)(?:-L([1-9]\d*))?$/.exec(fragment)
if (match === null) return undefined
const line = Number(match[1])
const end = match[2] === undefined ? line : Number(match[2])
if (!Number.isSafeInteger(line) || !Number.isSafeInteger(end) || end < line) return undefined
return { path, line }
}注意它拿到的是已经过 mdast 解码的 3. Host 侧:
private async inspect(workspaceFileScope, path, signal) {
if (path.length === 0) throw new RemoteError('gateway/bad-request', 'path is required', {})
const { workspaceRoot } = workspaceFileScope
const root = await this.ctx.fs.resolve(workspaceRoot, { signal })
const entry = await this.ctx.fs.lstat(path, { cwd: workspaceRoot }, signal)
if (entry === undefined) throw new RemoteError('workspace-file/not-found', `no entry at "${path}"`, { path })
return { root, workspaceRoot, entry }
}
4. 两处相关实现的源码注释(可引)
export function fileAddressFor(sessionId: string, cwd: string | undefined, path: string): string {
const normalized = path.replace(/\\/g, '/')
if (!isAbsoluteWorkspacePath(normalized)) return sessionFileAddress(sessionId, normalized)
const root = cwd === undefined ? '' : cwd.replace(/\\/g, '/').replace(/\/+$/, '')
if (root !== '' && normalized === root) return sessionFileAddress(sessionId, '')
if (root !== '' && normalized.startsWith(`${root}/`)) return sessionFileAddress(sessionId, normalized.slice(root.length + 1))
return sessionFileAddress(sessionId, normalized)
}
export function localDisplayPath(cwd: string, path: string): string {
const absoluteCwd = isAbsolute(cwd) ? cwd : `${process.cwd()}${sep}${cwd}`
const raw = isAbsolute(path) ? path : `${absoluteCwd}${sep}${path}`
const physicalSpelling = /(?:^|[\\/])\.\.(?:[\\/]|$)/u.test(raw) ? raw : resolve(cwd, path)
return process.platform === 'win32' ? resolve(cwd, path) : physicalSpelling
}
⇒ 所以「相对能开 / 绝对报不存在」的不对称,不是回调做了包含性检查、也不是把绝对路径当相对路径拼接, 撤回我上一轮的三条推断
必须说明的限制
版本情况(回应你第 4.3 点)
想请你确认的两点
修法方向(更正:单纯归一化修不了这个 bug)先划一条边界:归一化必须发生在「反转义」之前才有用。 信息在更早一步就丢了。所以修法取决于上面第 2 问那个前提:
为什么不能用「见反斜杠就拒绝」兜底:样本 5、7、9 正是靠反斜杠才正常的(
|
|
协助撰写:查到底的大肥鱼姐姐 |
|
上面那段标题写「三条推断」,实际只列了两条 —— 第 3 条是我漏写的,补在这儿:
|
你的自我更正让这条从"标题不成立"变成了一条可定位的真缺陷——我按你的数据给出根因假设1. 先确认你更正掉的部分(这一点很重要)你写:工作区外的绝对路径其实可以正常预览(表中的 4、5 两条), ⇒ 建议把标题也一并改掉(保留一个"[Bug]"标题但指向真实的那个行为),否则维护者会按错误的方向读。你先证伪自己的主判断、再给出被证实的那个行为——这个顺序是对的,请保留。 2. 你证实的行为,我给出机制层的假设(可验证)你写:含「 ⇒ 这正符合 Markdown 的反斜杠转义规则:在 Markdown 文本里,反斜杠 + ASCII 标点 = 该标点的字面量(转义被消费掉)。这个规则用在正文里是对的,但用在 Windows 路径上就会吃掉分隔符——于是 这与你的第 3 条补充完全自洽:你更正说 建议把这条写进报告作为判据:
3. 一条旁证(我在源码里核到的)Markdown 层对图片路径是**显式区分"是否被转义"**的: ⇒ 也就是说:这一层对"转义与否"是有意识的(图片侧明确只接受未转义的引用)。⇒ 那么链接侧把目标交给标准转义处理、进而吃掉 Windows 分隔符,就是一个明确的边界问题,而不是"解析器不成熟"。 4. 解决方案(两条,建议都要)
5. 请补两样
一条边界第 2 节是假设,不是结论:我确认的是**"反斜杠 + 标点会被消费、+ 字母会保留"这一规则本身**,以及图片侧存在显式的"未转义"判定这一旁证;链接侧是否真的走了同一条处理,需要你补第 5 节的逐字路径才能定论。 |



Uh oh!
There was an error while loading. Please reload this page.
环境
0.2.0-rc.2(桌面端DeepSeek Harness.exe,FileVersion0.2.0-rc.2,ProductVersion0.2.0.0)master的对应源码中同样存在(见"代码位置"一节,逐字比对过)症状
在对话里点击一个指向绝对路径的本地文件 Markdown 链接,右侧文档预览器打开标签页后显示:
但文件确实存在(PowerShell 实测可读、可写、ACL 正常)。
同一个文件,改成工作区相对路径的写法,链接立刻正常打开。
复现步骤
工作区设为
F:\dsh(会话 cwd =F:\dsh)。在
F:\dsh\_attic\下建两个内容相同的测试文件:t1-ascii.txt(纯 ASCII 名)t2-中文名.txt(中文名)在对话消息里写绝对路径链接:
点击这两个链接 → 两个标签页都显示「文件不存在,可能已被移动或删除」。
(这一步同时排除了"中文文件名"这一因素——ASCII 名同样失败。)
改成工作区相对路径:
点击 → 正常打开,正文完整渲染。
已排除的可能
Test-Path为真,4336 字节,可读_attic有Everyone: DeleteSubdirectoriesAndFilesDeny 项)Everyone另有ReadAndExecute允许;文件本身无 DenyDSH_HOME\storages\workspace.json中F:\dsh正常,本会话已登记在该工作区下代码位置(master 上逐字存在)
从对话链接到报错的完整链路:
packages/client/ui-primitives/src/markdown/file-link.ts→parseFileLink()绝对路径在这里是放行的:
盘符形态(
F:\…/F:/…)不匹配前面那条协议判定,因此会作为本地路径继续往下走。packages/util/workspace-path/src/index.ts→fileAddressFor(sessionId, cwd, path)工作区内的绝对路径会被转成相对路径;工作区外的绝对路径则原样保留绝对形态塞进 session 地址:
packages/api/workspace-files/src/index.ts→inspect()服务端拿会话工作区根当基准去
lstat:packages/fs/fs-local/src/fsio.ts→localDisplayPath(cwd, path)在 Windows 上最终走
resolve(cwd, path)。按第 4 步的语义,绝对路径本应压过
cwd正常命中;但实测是not-found,所以中间某一环把绝对路径当成了相对路径(或把它送进了另一条不接受绝对路径的通路)。这一环需要熟悉前端打开链路的同学确认。附带的两点改进建议
诊断信息被前端丢弃:服务端抛错时其实带了真实路径 ——
`no entry at "${path}"`,但前端failureLine()只把它映射成笼统文案:结果用户和排查者都看不到"它到底在找哪个路径"。建议在详情/日志里保留
details.path,能省掉大量来回猜测。绝对路径的可用性:既然
parseFileLink()特意放行盘符路径,那么"放行后必然失败"就是个陷阱。要么让它真正可用,要么在解析阶段就拒绝,别让它带着用户走到一个只会报错的预览器。影响
日常使用中,AI 助手产出文件后给出的绝对路径链接全部无法点击打开(这是最自然的写法),用户只能反复手动去资源管理器里找文件;相对路径写法可用,但这属于绕过而非修复。
All reactions