下载按钮真的导出了吗:把 JSON 文件读回来核对|AgentField执行信息
- 测试环境
- Node.js v22.22.0 / Playwright 1.62.1 / Chrome 154.0.8037.93 / macOS 本地下载回读
- 输入
- 请查看正文中的输入说明
- 产出
- 请查看正文中的产出说明
- 实测结果
- 请以作者提供的实测记录为准
AgentField 编辑整理|全部数据为虚构输入。本文实际验证了本地 Chrome 的下载事件,以及测试程序保存后的文件字节;没有验收用户的原生保存对话框、默认下载目录或生产导出接口。
点击之后出现“下载成功”,并不能回答三个问题:文件在哪里,内容是否完整,第二次导出有没有覆盖第一次。页面生成了正确字符串,也不等于那个字符串已经落在你准备交付的文件里。
这篇适合正在做小工具导出、或验收 AI 生成下载按钮的人。我们用两个很小的 JSON 记录,先写独立期望,再真实点击浏览器,从保存下来的文件重新解析。页面只说自己知道的事:已经发起下载请求;接收文件的验收程序另外报告保存和回读结果。
一、先把文件内容和成功条件写清楚
固定对象只有三个顶层字段:schemaVersion 为数字 1,title 为“虚构导出练习”,records 为两个对象的有序数组。第一条 id 是 demo-01,note 是 他说:"你好",接一个真正的换行,再接“第二行”,optional 为 JSON 的 null。第二条 id 是 demo-02,note 是“保留中文与反斜杠 ”后接一个反斜杠,optional 是空字符串。
这些值有意放在一起:中文不能乱码,引号与反斜杠不能丢失,字符串内的换行要在解析后还原,null 不能变成字符串 "null",空字符串也不能被当成缺字段。不是只检查“能打开”和记录条数。
文件约定为 UTF-8、无 BOM、两空格缩进、LF 换行,末尾恰好一个 LF,字段顺序与下面示例相同。独立手写的预期文件共 286 字节,SHA-256 为 f686d1f5df3e602a425a8793bc197988c346d936984fea68aa9a3887d4ab3f24。这个摘要在实现导出之前已经固定,验收答案没有从待测导出代码生成。
连续三次点击的建议文件名分别为 agentfield-demo-001.json、agentfield-demo-002.json、agentfield-demo-003.json。序号只在当前页面内递增,重新加载会从 1 开始;这不是全局唯一命名方案。浏览器可能调整建议名称,download 属性本身也不保证下载一定发生。MDN:download 属性
本次成功条件分开记录:真实点击发生、浏览器发出下载事件、验收程序把下载复制到明确目标、实际文件字节与预期相同、解析对象也相同。前两项不能代替后三项。
二、保存一个能直接运行的页面
把下面整段保存为 index.html。在仅放演示文件的目录启动 ,用浏览器打开 。这是本地演示,不需要上传真实资料。
相关实践
AD用 13 条虚构 ID 和每页五条的固定快照,先列出三页完整期望,再核对末页三条、导航、空集合、非法页号及越界。附完整 Node.js 示例与 19 组实际结果,明确本地函数与界面、接口验收的边界。
工作流AI 编程•administrator2026年9月30日120#自动化#开发
0AD用固定北京时间窗口与七行虚构事件,核对开始含、结束不含及同一事件的 UTC/+08:00 表示。附完整 Node.js 示例、25 组独立预期与实际结果,明确非法时间、缺时区、冲突 ID 和固定偏移的边界。
工作流AI 编程
python3 -m http.server 8765 --bind 127.0.0.1
http://127.0.0.1:8765/
<!doctype html>
<html lang="zh-CN">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>把下载文件读回来</title>
<style>
body { max-width: 720px; margin: 40px auto; padding: 0 20px; font: 17px/1.6 system-ui; color: #19303a; background: #f6f7f4; }
h1 { line-height: 1.25; }
button { padding: 12px 20px; border: 0; border-radius: 8px; color: white; background: #176b58; font: inherit; cursor: pointer; }
pre { padding: 20px; background: white; border: 1px solid #d7ded7; border-radius: 8px; overflow: auto; font: 14px/1.6 monospace; }
#status { min-height: 3em; }
</style>
<main>
<h1>把下载文件读回来</h1>
<p>这是虚构数据。页面只能发起下载请求;请找到实际文件,再核对内容。</p>
<button id="export" type="button">下载虚构 JSON</button>
<p id="status" role="status">尚未发起下载请求。</p>
<pre id="preview"></pre>
</main>
<script>
'use strict';
const source = {
schemaVersion: 1,
title: '虚构导出练习',
records: [
{ id: 'demo-01', note: '他说:"你好"\n第二行', optional: null },
{ id: 'demo-02', note: '保留中文与反斜杠 \\', optional: '' }
]
};
const text = JSON.stringify(source, null, 2) + '\n';
document.querySelector('#preview').textContent = text;
const status = document.querySelector('#status');
let sequence = 0;
let objectUrl = null;
document.querySelector('#export').addEventListener('click', () => {
let link;
try {
if (!objectUrl) {
const blob = new Blob([text], {
type: 'application/json;charset=utf-8', endings: 'transparent'
});
objectUrl = URL.createObjectURL(blob);
}
const next = sequence + 1;
const filename = `agentfield-demo-${String(next).padStart(3, '0')}.json`;
link = document.createElement('a');
link.href = objectUrl;
link.download = filename;
document.body.append(link);
link.click();
sequence = next;
status.textContent = `已发起下载请求(第 ${sequence} 次):${filename}。请检查实际文件。`;
} catch {
status.textContent = '准备下载请求时出现错误,请检查浏览器控制台;尚不能确认文件是否保存。';
} finally {
link?.remove();
}
});
window.addEventListener('pagehide', () => {
if (objectUrl) URL.revokeObjectURL(objectUrl);
objectUrl = null;
});
</script>
</html>
Blob 将这里的字符串编码为 UTF-8;endings: 'transparent' 保留文本换行,不转换成系统默认换行。MIME 类型只是文件类型说明,最终字节仍要从实际文件核对。MDN:Blob 构造器
页面的 catch 只处理这段同步准备与发起代码抛出的异常;它没有监听用户后续的磁盘保存结果。本次也没有人为触发该准备异常分支。无论浏览器是否弹出对话框、是否被用户取消,普通 a.download 都不能因此自动知道用户文件最终保存成功了。
三、从实际文件回读,而不是再读页面变量
在同一页面显式点击两次,找到浏览器交给你的两个实际文件。保存下面脚本为 read-back.cjs,在 Node.js 中运行 node read-back.cjs "第一个实际文件路径" "第二个实际文件路径"。文件路径来自你实际找到的文件,不要拿页面预览文本临时复制成两个文件来替代下载链路。
'use strict';
const fs = require('node:fs');
const path = require('node:path');
const assert = require('node:assert/strict');
const { createHash } = require('node:crypto');
const expected = {
schemaVersion: 1,
title: '虚构导出练习',
records: [
{ id: 'demo-01', note: '他说:"你好"\n第二行', optional: null },
{ id: 'demo-02', note: '保留中文与反斜杠 \\', optional: '' }
]
};
const expectedSha256 = 'f686d1f5df3e602a425a8793bc197988c346d936984fea68aa9a3887d4ab3f24';
function verifyFiles(first, second) {
assert(first && second, '请传入两个实际下载文件的路径');
assert.notEqual(path.resolve(first), path.resolve(second), '必须是两个不同路径');
return [first, second].map(file => {
const bytes = fs.readFileSync(file); // Read the actual file, not page memory.
const sha256 = createHash('sha256').update(bytes).digest('hex');
assert.equal(bytes.length, 286);
assert.equal(sha256, expectedSha256);
assert.notDeepEqual(bytes.subarray(0, 3), Buffer.from([0xef, 0xbb, 0xbf]));
const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
assert.deepEqual(JSON.parse(text), expected);
return { file: path.resolve(file), bytes: bytes.length, sha256, parsedObjectMatches: true };
});
}
if (require.main === module) {
console.log(JSON.stringify(verifyFiles(process.argv[2], process.argv[3]), null, 2));
}
module.exports = { verifyFiles };
这份脚本的期望对象与页面实现分开写;字节长度与摘要来自实现前的独立预期。它读取磁盘文件,先核对原始字节,再按 UTF-8 解码并深度比较对象。缺文件、内容不符或路径相同时直接报错,不打印一个看似通过的报告。
字节比较和对象比较解决不同问题。编辑器改变末尾换行或缩进,解析对象可能仍相同,但本例的精确文件契约会失败;相反,只看字节长度相同也不能保证值相同。更换数据或格式时应先重写独立期望,不能简单把失败时算出的摘要当成新答案。
四、这次真实点击与保存发生了什么
2026 年 10 月 2 日北京时间 09:04:09,先保存设计、虚构输入、手写预期字节和命名规则的冻结哈希,再实现页面和脚本。09:05:36—09:05:38,在 macOS、Node.js v22.22.0、Playwright 1.62.1 和独立无头 Chrome 154.0.8037.93 中运行验收。没有使用用户浏览器资料或普通下载目录。
测试先启动本地 HTTP 页面,在每次点击前开始等待 download 事件,再实际操作“下载虚构 JSON”按钮。Playwright 的事件表示下载已经开始;saveAs() 则等待下载并把它复制到测试程序指定路径。浏览器上下文关闭时,其管理的临时下载会删除,所以需要保留的验收文件另存到专用目录。Playwright:下载指南 · Download API
三次点击、两份成功文件和一个受控保存失败,得到以下具体结果;这是 32 项断言,不是 32 个独立用户场景:
- 第一次、第二次真实点击分别产生下载事件,建议文件名为
001、002 对应的两个名称。测试程序各选一个新的专用子目录调用 saveAs(),没有使用同一路径。
- 两个保存后的文件都为 286 字节,与预先冻结的完整字节逐一相同;SHA-256 均为上述
f686d1…3f24,解析对象与独立期望完全一致。上面的公开回读脚本也检查了这两个真实文件。
- 第二次导出及第三次失败之后,两份已保存文件仍保持原字节;浏览器上下文关闭后,两份副本仍存在。这支持本次两个测试目标路径的保留结论,不能推断浏览器默认目录会自动另存或永不覆盖。
- 第三次仍真实点击并收到下载事件,但验收程序故意把
saveAs() 目标设为一个已存在的普通文件下面,例如 not-a-directory/agentfield-demo-003.json。调用抛出 ENOTDIR,验收程序捕获并报告“指定目标保存失败”;该无效目标路径不存在,作为障碍的普通文件内容未变。
- 第三次的
download.failure() 仍是 null。因此失败发生在测试程序的目标复制步骤,不能写成“浏览器没有收到下载”或“任何地方都没有文件”。浏览器管理的临时下载与最终选定的保存目标是两层证据。
- 三次之后页面始终只显示“已发起下载请求……请检查实际文件”,没有宣称“已保存成功”;页面预览值未变,没有观察到页面运行异常。截图也核对了这一提示。
第 4 项是受控的测试保存路径失败,不是真实用户遇到磁盘满、权限故障或取消系统对话框。保存失败由掌握目标路径的验收程序发现;普通下载页面没有因此获得磁盘状态感知能力。两次成功的位置同样由测试程序选定,不代表已验收浏览器默认命名、覆盖、自动重命名或原生保存对话框。
五、给真实导出任务留下什么交接证据
可以把验收要求写成一段:“先固定虚构输入、编码、文件名建议和完整期望;实际点击导出,记录下载事件与真正保存位置;从实际文件读取大小、摘要和解析值。重复导出后检查两份文件,失败时写清失败在哪一层,不把请求已发起当作已保存。最后列出还没有验证的浏览器和生产链路。”
这次没有连接生产导出接口或对象存储,也没有验证业务权限、跨浏览器、大文件资源占用、并发导出、用户取消保存、真实磁盘故障或所有 Unicode 输入。若产品确实需要告诉用户“保存已完成”,还要选择能提供相应完成/错误反馈的保存机制,再为它设计独立验收;不能靠把提示文案改成成功来补证据。
这与“导入 JSON 前校验字段”不同:本篇检查的是从点击到下载、再到测试程序保存和文件回读的输出链路,不是证明数据已经导入数据库;也不涉及旧文件改名或恢复。欢迎用虚构数据描述你卡在哪一步:没出现下载事件、找不到保存文件、两次文件关系不清,还是回读值不一致。把步骤与证据对应起来,比单独贴一张“导出成功”截图更容易定位。
•
administrator
2026年9月28日