Skip to content

Instantly share code, notes, and snippets.

@goldengrape
Created October 4, 2026 03:42
Show Gist options
  • Select an option

  • Save goldengrape/1e17f461906337612eb2b379286ef09f to your computer and use it in GitHub Desktop.

Select an option

Save goldengrape/1e17f461906337612eb2b379286ef09f to your computer and use it in GitHub Desktop.
Insta360 Offline Capture:交给 AI 的完整重建提示词.md
# Insta360 Offline Capture:交给 AI 的完整重建提示词
下面正文可以直接作为另一个 AI 的任务。它不要求对方拥有原项目、历史聊天、已下载模型或我的电脑环境。目标是重建功能与行为,不要求源码逐字相同。
---
你是负责实现和测试的工程助手。请在当前工作目录重建一个 Chrome 扩展项目。
请直接完成源代码、必要文档、测试和安装包,不要只给方案、伪代码或界面样稿。本任务已经给出技术栈、范围和验收标准;一般实现选择由你判断,不需要反复确认。遇到真实环境限制时,完成可以独立完成的工作,准确记录未验证项,不要虚构测试结果。
当前任务仅要求本地交付,不要求发布商店、部署网站、推送远程仓库或联系任何人。不要修改用户的日常浏览器资料。如果目录已有文件,先查看并保留用户改动;不要盲目删除、重置或覆盖。
## 1. 用户需求和范围
用户在 Insta360 3D Space 分享页面查看一个 3D Gaussian Splat 场景,希望通过插件下载一个完整 ZIP 包。解压后有 HTML、原始 SOG、相机数据、查看器代码和说明。用户双击 HTML 就能离线查看,能够旋转、移动和缩放,不必安装 Node.js、不必启动服务器、不必重新访问 Insta360。
主流程是:
1. 用户打开正常显示的 Insta360 场景页面。
2. 打开插件,点击“扫描并重新加载页面”。
3. 等待模型加载,可移动视角触发延迟资源。
4. 点击“停止扫描”。
5. 点击“下载离线场景 ZIP”。
6. 插件打开独立打包进度页,下载原始资源,生成 ZIP,并通过 Chrome 保存。
7. 用户解压,双击 `viewer-offline.html`。
同时保留原始资源选择、分别下载和单独导出 `manifest.json` 的功能。提供开发者用的离线 HTML 构建命令,支持 SOG,其他受转换器支持的格式作为辅助路径。
明确范围:
- 运行于 Windows 的 Chrome;扩展使用 Manifest V3,最低 Chrome 125。
- ZIP 导出只承诺支持一个 ZIP 格式的 `.sog` 模型,不做多模型合并。
- 可以识别其它模型或分块元数据,并提供原始资源下载;不能把“发现了若干分块”说成“已完整导出整个场景”。
- 不复刻 Insta360 的网页、社交功能、评论或账号系统。
- 不增加后端、云上传、遥测、登录、数据库或付费功能。
- 不猜测其它场景 ID,不绕过访问限制,不导出 Cookie,不绕过 401/403。
- 保留现成查看器的 WebXR VR/AR 能力,但没有头显时不能宣称真实 XR 会话测试通过。
## 2. 编程与沟通要求
遵循公理设计、契约式设计、函数式编程、数据导向编程和奥卡姆剃刀原则,把原则落实到模块和接口:
- URL 识别、评分、资源合并、路径生成、模型与相机匹配、相机变换、HTML 填充采用纯函数。
- Chrome API、网络、DOM、文件读写分别放在有明确职责的模块。
- 对消息来源、标签页、URL 协议、HTTP 结果、输入格式、大小、相机矩阵和 ZIP 路径建立明确契约。
- 使用普通对象、数组和 Map 保存数据;不为此项目增加大型框架或复杂类继承。
- 不复制大段相机计算逻辑,插件和 CLI 共用同一实现。
- 避免无必要的抽象。文档只解释实际需求、重要取舍和可验证行为。
使用平实、直接、克制的技术中文。禁止隐喻、大厂黑话和故作高深的汇报句式。发现问题先说明具体事实,再说明处理方式。
## 3. 技术栈和依赖
- 扩展:原生 JavaScript ES Modules、HTML、CSS。
- 开发与测试:Node.js 22 或更新版本、npm、`node:test`、`node:assert/strict`。
- package.json 设置 `"type":"module"`、`"private":true`、`"engines":{"node":">=22"}`,让 `.js` 纯函数模块可被 Node 测试直接导入。
- 查看器:固定 `@playcanvas/supersplat-viewer` 为 `1.37.0`。
- CLI 转换器:固定 `@playcanvas/splat-transform` 为 `3.9.0`。
- 原项目解析到的 PlayCanvas peer dependency 为 `2.23.0`。在重建项目中显式固定或通过锁文件保持这个版本,避免未来安装得到不同引擎。
- 生成并保留 `package-lock.json`。安装依赖后检查真实导出接口,不凭记忆编造 API。
- 正常使用插件不需要 npm 或 Node.js;查看器代码必须已包含在安装包里。
推荐 npm 脚本:
```json
{
"test": "node --test tests/*.test.mjs",
"check": "node tools/check-extension.mjs",
"build:viewer": "node tools/build-extension-viewer.mjs",
"build-viewer": "node tools/build-viewer.mjs"
}
```
建议再提供 `package` 命令,能够可靠地生成安装 ZIP 和源码 ZIP。打包工具采用最少依赖即可;Windows PowerShell 的 `Compress-Archive` 或经验证的 Node 打包方案均可。
## 4. 文件结构与职责
```text
insta360-offline-chrome/
package.json
package-lock.json
.gitignore
README.md
SECURITY.md
extension/
manifest.json
background.js
shared.js
popup.html
popup.js
popup.css
archive.js
viewer-settings.js
export.html
export.js
export.css
icons/
icon16.png
icon32.png
icon48.png
icon128.png
viewer/
template.html
settings.json
viewer.js
viewer.css
LICENSE.txt
tools/
build-extension-viewer.mjs
build-viewer.mjs
check-extension.mjs
package-extension.mjs # 或同等可靠打包工具
tests/
shared.test.mjs
background.test.mjs
archive.test.mjs
viewer-settings.test.mjs
docs/
URD.md
ALGORITHM.md
TEST-REPORT.md
output/ # 生成文件,不属于源码安装依赖
```
`.gitignore` 至少忽略 `node_modules/`、`output/`、测试浏览器资料、Playwright 临时产物和生成的 ZIP。若有现成 Git 仓库,在重要实现阶段记录本地检查点;没有远程时不创建推送或合并要求。不要为了文档数量增加无用文件。
## 5. Manifest 和权限
使用模块 Service Worker:`background.js`;默认弹窗为 `popup.html`。扩展和 package 版本保持一致,图标引用必须真实存在。
所需权限:
```json
{
"permissions": ["activeTab", "debugger", "downloads", "scripting", "storage", "tabs"],
"host_permissions": [
"https://app.insta360.com/*",
"https://app-x-va-prod.s3.amazonaws.com/*"
],
"optional_host_permissions": ["https://*/*"]
}
```
不要在安装时要求读取所有网站。打包遇到其它实际观察到的 HTTPS 资源源站时,只请求选中模型和匹配相机数据的 origin 权限。
`chrome.permissions.request({origins})` 必须从用户点击下载按钮的事件处理器发起,避免异步流程丢失 user gesture。拒绝权限时显示中文错误;不能把拒绝处理成打包成功。进度页再次用 `permissions.contains` 检查。
所有资源获取只使用当前扫描观察到的地址。扩展消息要求 sender 的扩展 ID 等于自身 ID,sender URL 属于自身 `chrome-extension://`,tabId 是有效整数。外部页面不能通过消息命令让插件下载任意 URL。
遵循 MV3 CSP:插件的运行脚本用本地模块文件,不启用 `unsafe-eval`,不执行远程 JS。查看器模板中的内联代码只作为文本用于生成下载文件,不在扩展页面中执行。
## 6. 资源识别:shared.js
提供以下接口或语义等价接口:
```text
sceneIdFromUrl(url) -> sceneId | null
extensionOf(url) -> 格式/精确元数据文件名 | 空字符串
classifyResource({url,mimeType,type,size}) -> {score,kind,reasons,ext}
extractUrls(value, baseUrl) -> 去重 HTTP(S) URL 数组
safeSegment(text, fallback) -> 安全文件名片段
downloadPathFor(url, sceneId, index) -> Chrome 相对下载路径
dedupeResources(resources) -> 按完整 URL 合并的资源数组
canDownloadResource(resource) -> boolean
```
页面校验必须检查:协议是 `https:`、hostname 恰为 `app.insta360.com`、pathname 完整匹配 `/3dspace/detail/<scene-id>`,可有末尾斜线。不接受其它域名、相似域名或仅路径里碰巧出现字符串的页面。
识别模型扩展名:`.sog`、`.ply`、`.compressed.ply`、`.splat`、`.ksplat`、`.spz`、`.lcc`、`.lcc2`。优先识别 `.compressed.ply` 等完整后缀。
按 basename 精确识别 `meta.json`、`lod-meta.json`、`cameras.json`,不能让 `lod-meta.json` 被后缀判断误判为 `meta.json`,不能把 `notmeta.json` 当模型入口。
参考评分规则,用于复现现有候选列表:
| 条件 | 分数 |
|---|---:|
| 已知模型后缀 | +100 |
| meta.json / lod-meta.json | +90 |
| cameras.json | +70 |
| 普通 JSON 路径含模型关键词 | +45 |
| pathname 含 gaussian、splat、sog、pointcloud、point-cloud、3dgs、lod-meta、compressed.ply、model、scene 等关键词 | +35 |
| octet-stream、application/zip、model/* MIME | +20 |
| Fetch/XHR 大于 2,000,000 字节 | +15 |
| WEBP 路径含 sh、means、quat、scale | +35 |
| 普通 WEBP | -15 |
| 路径含 thumbnail、cover、avatar、poster、preview、logo、icon | -45 |
| JS/CSS/HTML 文件或 MIME | 分数最高为 0 |
| 由模型入口 JSON 明确引用的资源 | 额外 +45 |
关键词只检查 URL pathname,不检查 query,防止 `script.js?model=x.sog` 误判。`kind` 可以是 model、metadata、payload、candidate、other。
递归提取 JSON 中的 URL,但只接受绝对 HTTP(S) URL 或看起来是文件路径的字符串。标题、UUID、枚举、普通文本不能自动变成相对资源。恢复 `\\u0026` 和转义斜线;页面 HTML 补扫还需要恢复 `&amp;`。签名 URL 必须保留完整 query,不能自行删除、重排签名参数或猜测过期后的新签名。
资源合并必须保留已知 HTTP 状态、MIME、请求类型、最大已知大小和原始 firstSeenAt。页面补扫发现同一 URL 时不能把这些事实覆盖为 0 或空值。评分用最大值,不能把真实 0 分用 `||` 当成缺失。sources 和 reasons 去重;新的网络成功响应可以清除该 URL 的失败状态。
可下载条件:HTTP(S)、请求方法未知或 GET、状态未知或 2xx,并且没有明确 loadingFailed。状态未知只说明待下载验证,不代表成功。
路径要求:保留 host 和原始路径层级,去掉 query 对文件名的影响,过滤 Windows 不允许字符、控制字符、路径穿越、末尾点/空格和保留名 CON/PRN/AUX/NUL/COM*/LPT*。损坏的百分号编码不能导致整次操作崩溃。同一次下载出现路径冲突时加编号。
## 7. 扫描与状态:background.js
按 tabId 管理扫描状态,使用 `chrome.storage.session` 保存小型状态,Service Worker 重启后可以恢复。不要把模型 Blob、Base64 或整个离线 HTML 放进 session storage。
状态最少包含:
```js
{
tabId, pageUrl, sceneId,
attached: false, scanning: false,
startedAt: null, stoppedAt: null,
resources: [], requests: {}, errors: [], downloads: [], message: ''
}
```
资源保存 url、ext、kind、score、reasons、sources、requestId、method、status、mimeType、type、size、failed、failure、firstSeenAt。requests 保存请求方法、原地址、响应地址、状态、类型、Content-Length 和有限的认证头。
所有影响同一标签页状态的操作放入按 tabId 的 Promise 队列,包括 UI 命令、调试事件、存储定时器和下载状态更新。单个操作失败不能导致后续队列永远拒绝。持久化可以约 500 ms 防抖,但定时器也必须进入同一队列,避免旧状态覆盖新状态。
开始扫描:
1. 校验页面;重复开始同一正在扫描的标签页不重复 attach。
2. `chrome.debugger.attach({tabId}, '1.3')`。
3. `Network.enable`,总缓冲约 20 MiB,单资源缓冲约 5 MiB。
4. `Page.enable`。
5. `Target.setAutoAttach({autoAttach:true, waitForDebuggerOnStart:true, flatten:true})`。
6. 标记并保存状态,再 `chrome.tabs.reload(tabId, {bypassCache:true})`。
7. attach、Network.enable 或 reload 失败时解除已建立连接,恢复 scanning/attached 为 false,记录可读错误。
必须捕获 Worker/子调试会话。收到 `Target.attachedToTarget` 后,对 `{tabId,sessionId}` 开启 Network,并继续设置 autoAttach;在 finally 中调用 `Runtime.runIfWaitingForDebugger`,不能把网页 Worker 永久暂停。
请求唯一键用 `${sessionId || 'root'}:${requestId}`,避免不同会话中相同 requestId 相撞。对子会话调用 `Network.getResponseBody` 时,必须使用正确 Debuggee sessionId 和 CDP 原 requestId,而不是合成键。
处理这些事件:requestWillBeSent、responseReceived、loadingFinished、loadingFailed、Page.loadEventFired、debugger.onDetach、tabs.onRemoved。
- 请求事件保存方法和认证头;只允许 authorization、x-auth-token、x-api-key,不读取/导出 Cookie。
- 响应事件保存实际 HTTP 状态、MIME、大小、类型;Content-Length 不区分头名称大小写。
- 完成事件补充 encodedDataLength,并重新计算大资源评分。
- 成功的小型 JSON 可以读取 body;上限 5 MiB,同时检查 Content-Length、encodedDataLength 和解码后的实际大小。
- `getResponseBody` 失败或 body 被回收不应停止扫描;继续依赖 URL 和页面补扫。
- 模型入口 meta.json/lod-meta.json 的引用加权;其它 JSON 不自动获得同样的模型依赖加权。
- loadingFailed 标记资源失败,禁止将其当作可下载成功资源。
- 页面加载约 1.8 秒后补扫;执行时重新判断当前扫描状态,不污染已停止或新开始的会话。
页面补扫用 `chrome.scripting.executeScript` 遍历可访问 frame,收集 performance resource entries、DOM 的 src/href、HTML 内嵌的 HTTP(S) URL。适当限制返回数量,例如 5000 条;不得向页面注入远程脚本。
停止扫描先补扫再 detach,更新停止时间。标签页关闭后清除 timer、内存状态和 session storage,忽略关闭标签页的迟到事件。
消息接口:START_SCAN、STOP_SCAN、GET_STATE、DOWNLOAD_RESOURCES、DOWNLOAD_MANIFEST。统一返回 `{ok:true,...}` 或 `{ok:false,error}`。异步 onMessage 保持消息通道打开,并保证回应一次。
## 8. 原始资源下载与弹窗
弹窗宽约 440 px,使用原生 DOM 和 textContent,不能把抓到的 URL 或标题作为 HTML 执行。
必须有:场景标识、扫描状态、扫描按钮、停止按钮、主按钮“下载离线场景 ZIP”、提示信息、资源列表、选择高置信度、清空、下载选中资源、下载 manifest.json、实际下载结果。
资源显示文件名、格式、评分、大小、HTTP 状态和失败原因。URL 过长时截断显示,完整地址可在 tooltip 查看。默认列表可显示 score >=20;“选择高置信度”选 score >=60 且可下载的资源。
约 1.2 秒轮询 GET_STATE;用户操作期间避免重复命令;popup 关闭后扫描不丢失。支持 `popup.html?tabId=<id>`,便于隔离浏览器测试指定场景页。正常弹窗使用当前活动标签页。
普通资源用 `chrome.downloads.download` 保存到:
```text
Insta360-Offline/<scene-id>/assets/<host>/<原始路径>
```
使用 `conflictAction:'uniquify'`。download API 返回 id 仅表示任务创建;必须用 `downloads.onChanged` 和 `downloads.search` 跟踪 in_progress、complete、interrupted、bytesReceived、totalBytes 和 error。
清单包含观察到的资源、实际下载状态和本地相对路径。若 Chrome 改了重名文件名,以真实下载 filename 为准,不只输出预先计划的路径。单独原始资源清单可以记录观察到的完整资源 URL,但明确签名可能过期;任何归档不得写出认证头。
修正按钮 hover 的 CSS 优先级:主按钮保持深色背景与白色文字,不能 hover 后变成浅灰底白字。
## 9. 查看器构建和 HTML 填充
安装 npm 依赖后,在开发阶段生成 `extension/viewer/`,不能让最终用户执行此步骤。
使用库的公开接口:
```js
import { renderViewerHtml, js, css } from '@playcanvas/supersplat-viewer';
import { defaultSettings } from '@playcanvas/supersplat-viewer/settings';
const template = renderViewerHtml({
bootstrap: {
settings: '__INSTA360_SETTINGS__',
contentUrl: '__INSTA360_MODEL__',
contentFilename: 'scene.sog'
},
inlineCss: true,
inlineJs: true
});
```
把 template 写入 template.html;把 `defaultSettings('environment')` 写入 settings.json;js/css 导出字符串分别写入 viewer.js/viewer.css;保留该 npm 包 LICENSE,并按实际分发内容保留其它必要许可证。
运行时读取静态模板文本,用实际 settings JSON 替换 `"__INSTA360_SETTINGS__"`,用 `data:application/octet-stream;base64,<原始SOG>` 替换模型 token。
两个 token 必须各出现一次;缺失或重复要报模板错误。模型数据必须是预期 Base64 data URL。JSON 替换文本里的 `<` 转成 `\\u003c`,防止字符串提前结束 script 标签。不要靠删除或改写库私有压缩代码来构造查看器。
最终 HTML 必须内嵌查看器 JS、CSS、初始视角和模型。因此它可以直接从 `file://` 运行,不通过 fetch('./assets/3DGS.sog') 读取旁边的模型,不依赖远程字体、CDN、原始场景接口或签名地址。
## 10. 相机转换:必须准确实现
SOG 直接放进通用 Model Viewer 后看起来混乱,不足以证明文件损坏。默认相机可能贴在墙面或离开采集范围;此项目必须优先恢复相机数据中的初始视角,而不是用模型包围盒中心代替。
`cameras.json` 是数组。以数组第一项为初始相机,验证后读取 position、rotation、height、fy;第一项无效时按下述失败策略处理,不静默跳到其它相机。rotation 是 3×3 camera-to-world 矩阵。当前 SuperSplat 对 splat 应用绕 Z 轴 180° 的变换,需要同时转换相机位置与朝向。
复现计算如下:
```js
const position = [-camera.position[0], -camera.position[1], camera.position[2]];
const direction = [
-camera.rotation[0][2],
-camera.rotation[1][2],
camera.rotation[2][2]
];
const target = position.map((value, index) => value + direction[index]);
const fov = 2 * Math.atan(camera.height / (2 * camera.fy)) * 180 / Math.PI;
settings.cameras = [{ initial: { position, target, fov } }];
```
纯函数 `initialCameraFromCameras(cameras)` 返回 `{position,target,fov}`。
检查 position 恰为 3 个有限数值,rotation 恰为 3×3 有限数值,height/fy 是正有限数,direction 长度非零,结果不会产生 NaN/Infinity。
相机文件必须从模型同一 origin 和同一目录匹配 basename `cameras.json`,两者可以有不同签名 query。不能取另一个执行目录或另一个模型的 cameras。
如果扫描结果没发现相机文件,弹窗和包内说明明确提示默认视角;允许用查看器默认相机打包。如果已找到相机 URL,但下载失败、JSON 无效或矩阵不合法,则本次 ZIP 打包失败,不能静默退回默认相机并宣称完整导出。
原始 cameras.json 保留原始字节,viewer/settings.json 保存转换后的设置;两者含义不同,不相互覆盖。
## 11. 独立打包页和 ZIP
不要在 popup 中执行整个大文件打包,因为 popup 关闭会中断任务;不要在 Service Worker 中存大模型或创建需要 DOM 的 FileReader 流程。
点击主按钮时:
1. 从当前扫描结果选择 SOG。
2. 有一个 SOG 时自动选择,不必勾选相机。
3. 有多个 SOG 时要求用户只勾选一个;多个勾选或没有明确选择时禁用并说明原因。
4. 请求实际资源 origin 的必要权限。
5. 打开 `export.html?tabId=...&model=...`,或使用等价的仅插件内部任务标识。
打包页显示准备、资源下载、封装、保存、完成或失败状态,提供取消与失败重试。说明“下载完成前请保留此页面”。重复点击或重试要避免重入。
打包页从 background 获取扫描状态,再检查模型确实仍在观察结果里。fetch 时使用捕获并过滤的认证头;注意 background 保存 `{name,value}` 数组,而 fetch 的 HeadersInit 需要 `[name,value]` 对或合法对象,不能直接混用这两种结构。
下载使用 AbortController、`credentials:'omit'`、`cache:'no-store'`。流式读取时报告已接收字节并检查大小:
- 模型上限 128 MiB。
- 相机上限 8 MiB。
- 既检查 Content-Length,也检查实际累计字节,不能只相信响应头。
- 非 2xx、网络失败、空文件、超限明确报错。
- SOG 开头必须为 ZIP local file header `0x04034b50`,即字节 `50 4b 03 04`。此检查只是格式初筛,不宣称已完成内部语义验证。
模型原样封装,不重新量化、不做 GPU 转码、不改变 SH 数据。Blob 使用 application/octet-stream MIME,FileReader 转 Base64 data URL。
场景 ZIP 名称:`Insta360-Offline/<scene-id>-offline.zip`,冲突自动改名。ZIP 内部根目录如下:
```text
viewer-offline.html
assets/3DGS.sog
assets/cameras.json # 原场景有相机数据时包含
viewer/viewer.js
viewer/viewer.css
viewer/settings.json
viewer/LICENSE.txt
manifest.json
README.txt
```
HTML 内嵌模型,assets 另保留一份原始模型;这会增加包大小,是为了同时满足双击查看和保留原始文件。viewer.js/css 是独立代码备份,HTML 运行并不依赖相邻文件。
ZIP 清单记录 schema/version、sceneId、来源页面、生成时间、查看器名称和固定版本、入口 HTML、原始文件路径/大小、初始相机来源和缺少相机的警告。不需要把带签名的 CDN URL 或认证头写入这个完整包的清单。
ZIP 实现可以使用原生、无外部依赖的 stored ZIP:
- 每个条目 local header + 原始 Blob,central directory,EOCD。
- 压缩方式 0;CRC32 用标准多项式 `0xedb88320`。
- 正确填写小端字段、原始大小、UTF-8 文件名长度、local header offset、中央目录大小和起点。
- UTF-8 flag 0x800,合法 DOS 日期,例如 1980-01-01。
- 拒绝绝对路径、路径穿越、反斜线导致的歧义、重复文件名、ZIP32 超限和过多条目。
- 用 CRC 标准向量 `123456789 -> 0xcbf43926` 和独立 ZIP 阅读器验证,不能只用自己写的解码逻辑证明自己正确。
最终用 Blob URL 和 `chrome.downloads.download` 保存,跟踪实际 complete/interrupted。创建成功时只显示“正在保存”,不能提前显示“下载完成”。下载完成或中断后释放 Blob URL;错误与取消也清理资源。
## 12. 辅助 CLI
提供 `npm run build-viewer -- <模型文件> [输出html]`。
- 输入必须存在且是文件;输出必须为不同的 `.html` 路径,不能覆盖输入。
- `.sog` 且没有额外转换选项时,检查 ZIP 文件头,原样读取;寻找同目录 cameras.json,使用同一个相机纯函数;通过 renderViewerHtml 生成内嵌数据的 HTML。
- CLI 可明确警告无效相机并使用默认设置;这与主 ZIP 流程“已发现相机但无效则失败”的严格策略要在文档中说明,不能混淆。
- 其它支持格式或明确额外转换选项走 splat-transform CLI。
- Windows 上不要直接依赖 spawnSync 执行 `.cmd`。解析 `node_modules/@playcanvas/splat-transform/bin/cli.mjs`,用 `process.execPath` 启动。
- 转换默认 `--gpu cpu`,允许用户显式覆盖。已有真实测试遇到过 Windows D3D12 GPU 压缩驱动挂起,直接 SOG 封装不应走这条路径。
- 透传错误与退出码,不把进程启动成功当作转换成功。
## 13. WebXR:保留能力,准确报告
现成 SuperSplat Viewer 1.37.0 包含 `immersive-vr` 和 `immersive-ar` 检测、XR 控制和启动入口。支持的浏览器/设备上显示相应按钮,不支持时隐藏或正确提示,不能模拟按钮成功。
模板默认倾向 WebGPU,支持 `?webgl`。该版本在当前后端不能直接启动 XR、但检测到 WebGL 可用时,会提示切换并重新加载。保留这个行为,不要擅自把普通离线页面改成只能 WebGL。
普通 `file://` 打开测试不等于 WebXR 会话验证。XR 还依赖设备、浏览器、安全上下文和用户授权。一般使用 HTTPS 或同一设备的 localhost;头显访问电脑的 `http://192.168...` 不自动享有 localhost 特例。没有真实硬件时只记录代码能力和未验证项,不保证 Quest、Vision Pro 或其它设备都可用。本版不要求新增 HTTPS 服务器、证书管理或 XR 启动脚本。
## 14. 测试计划与判断标准
使用有明确正确结果的测试,不用“有一个 canvas”代替“模型渲染正确”。至少覆盖以下行为,测试数量不必与原项目相同,但不能丢失关键契约。
### A. 纯函数
- 严格场景域名/协议/路由识别,拒绝相似域名和其它路径。
- 精确元数据文件名及 compressed.ply 后缀。
- query 中的模型关键词不会给页面脚本高分。
- JSON 提取绝对/相对文件引用,排除普通文本和危险协议。
- 转义签名地址恢复完整。
- 损坏的百分号编码与 Windows 保留文件名处理。
- 页面补扫合并保留网络事实和零分语义。
- HTTP 403、loadingFailed 和非 GET 不可下载。
- 同目录相机匹配,不跨执行目录;多 SOG 选择必须明确。
- HTML token 缺失/重复失败,settings 中 `</script>` 不会破坏脚本边界。
- CRC 标准向量、ZIP offset、UTF-8 名称、原始字节及路径拒绝。
- 相机 position、rotation 第三列、target、fov 的具体数值;非法相机不能输出 NaN。
### B. 模拟 Chrome API 的 background 契约测试
- 外部消息拒绝,tabId 校验,异步响应。
- attach/enable/reload 失败时 detach 和 stopped 状态。
- 密集网络事件与 timer 顺序不会丢状态。
- 主页面与 Worker 相同 requestId 不相撞。
- 子会话失败也要恢复 Worker 运行。
- body 被回收时仍继续扫描。
- HTTP 失败资源不能建立普通下载任务。
- 下载 complete/interrupted 正确显示,单纯取得 downloadId 不算完成。
- 导出清单记录 Chrome 实际改名路径,不包含认证头。
### C. 真实浏览器验收
用隔离的浏览器资料加载实际扩展,通过真实 popup 按钮完成扫描和 ZIP 导出。使用已有可用浏览器自动化能力;Windows 上若普通 Chrome 不接受命令行加载扩展,使用支持该测试方式的 Chromium,不要修改用户的主浏览器。扩展更新后确保测试实际运行最新文件,必要时使用新测试 profile。
实际验收必须完成:
1. 扩展可加载,manifest 可读,Service Worker 没有启动错误。
2. 打开场景,通过按钮扫描,再停止;至少识别 SOG 和该目录的 cameras.json。
3. 点击 ZIP 按钮,看到打包进度,确认 Chrome 最终 complete,并保存实际生成的文件。
4. 用独立 ZIP 阅读器检查全部条目和 CRC;Python `zipfile.testzip()` 可作为判断工具,但不是最终用户依赖。
5. ZIP 原始 SOG 与实际下载响应或已有可靠原文件逐字节一致;从 HTML Bootstrap 提取 Base64 后与 assets/3DGS.sog 一致。
6. 检查原始相机没有被改写,viewer/settings.json 与 HTML 内嵌设置一致。
7. 解压后先启用浏览器离线模式,再从 `file://` 打开或重新加载 HTML;等待资源真正完成,截图检查正确初始视角。
8. 拖动操作确实改变模型画面;脚本错误为 0,网络记录中没有远程资源请求。保存操作前后的截图。
9. 在隔离测试页拦截模型响应为 403:明确失败,未创建新 ZIP 下载。
10. 拦截相机响应为无效数组:明确失败,未静默生成默认视角 ZIP。
11. 如可行,测试取消、大小限制和下载保存中断;没测到的项目准确标为未验证。
不要把“扫描资源状态为 0”直接当失败:页面可能从缓存或 IndexedDB 恢复;但最终 fetch/下载必须验证实际响应。若参考页面不可用,记录真实原因,完成本地与模拟测试,保留真实样本验收为未完成;不能用伪造场景宣称参考页面测试成功。
## 15. 静态检查、安装包与交付
`npm run check` 至少检查:MV3、manifest/package 版本一致、JS 模块语法、popup/export 页面和 CSS 存在、图标存在、全部查看器静态文件存在、两个模板 token 各一次。
最终交付:
```text
output/insta360-offline-chrome-0.3.0.zip
output/insta360-offline-chrome-0.3.0-source.zip
output/insta360-offline-chrome-0.3.0/ # 可直接加载的解压目录
```
安装 ZIP 根目录必须直接有 manifest.json、background.js、popup、export、共享模块、icons/ 和 viewer/。不能仅在根目录放 extension/、docs/ 等文件,然后让用户选错误层级。这个错误会导致 “Manifest file is missing or unreadable”。
安装 ZIP 不包含 node_modules、开发文档、浏览器 profile 或样例模型;源码 ZIP 包含源码、开发工具、测试、文档、package.json/lock,排除 node_modules、output 和敏感测试产物。
独立验证安装 ZIP 的 CRC、manifest 版本、被引用文件存在、与被测 extension/ 文件字节一致。README 清楚说明 Chrome 开发者模式、加载包含 manifest.json 的目录、升级后重新加载、主流程、包内重复保存模型的原因、128 MiB 限制、相机策略、可选权限和 XR 验证范围。
`docs/URD.md` 写确认需求和范围;`docs/ALGORITHM.md` 写模块职责、数据流、接口契约与关键取舍;`docs/TEST-REPORT.md` 写本次真实测试、命令、结果、产物位置及未验证项。用一张简短表把核心需求对应到模块和验收测试,避免仅罗列术语。
## 16. 推荐执行顺序
1. 检查目录和工具环境,建立最小文件结构及简短需求/检查计划。
2. 实现 URL/评分/合并/路径和相机纯函数,运行相关契约测试。
3. 实现 background 状态队列、捕获、Worker 会话、失败清理和普通下载。
4. 实现 popup,通过真实页面验证扫描与资源发现。
5. 安装固定依赖,通过公开 API 生成查看器静态文件。
6. 实现 HTML 填充和 ZIP,验证 CRC、文件内容和字节一致性。
7. 实现独立打包页、权限请求、取消、错误和真实保存状态。
8. 完成真实 ZIP 下载、解压、断网打开、初始视角和交互验收。
9. 生成安装包与源码包,检查安装根目录及完整性,更新文档。
10. 最终报告已完成行为、测试证据、交付文件和实际限制,不只报告“代码已写完”。
不要仅因 Node 单元测试通过就结束,也不要为追求测试数量反复测试没有变化的部分。修复真实失败后重测受影响流程;无法验证真实设备或资源时如实说明。
## 17. 技术参考
以下用于核对接口和平台约束,不允许在最终离线页面运行时依赖这些站点:
- Chrome debugger:https://developer.chrome.com/docs/extensions/reference/api/debugger
- Chrome downloads:https://developer.chrome.com/docs/extensions/reference/api/downloads
- Chrome permissions:https://developer.chrome.com/docs/extensions/reference/api/permissions
- Chrome storage:https://developer.chrome.com/docs/extensions/reference/api/storage
- SuperSplat Viewer:https://developer.playcanvas.com/user-manual/supersplat/viewer/
- Viewer 自托管:https://developer.playcanvas.com/user-manual/supersplat/viewer/self-hosting/
- Viewer 嵌入:https://developer.playcanvas.com/user-manual/supersplat/viewer/embedding/
- SOG 格式:https://developer.playcanvas.com/user-manual/gaussian-splatting/formats/sog/
- splat-transform CLI:https://developer.playcanvas.com/user-manual/splat-transform/cli-reference/
- WebXR 环境要求:https://developer.mozilla.org/en-US/docs/Web/API/WebXR_Device_API/Startup_and_shutdown
如果网页文档与固定 npm 版本的实际接口不一致,以该版本实际代码和接口测试为准,并记录差异。不要悄悄升级依赖来规避需要理解的问题。
请现在开始实现,持续完成到上述交付和验收结束。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment