fix(preview): support document previews in app webviews
This commit is contained in:
parent
9032c5b517
commit
8d741e4141
3
APP.md
3
APP.md
|
|
@ -55,9 +55,12 @@ https://<zcbot 域名>/static/dev.html?embed=1&relogin_url=<回登录入口的
|
||||||
| DOM storage | `setDomStorageEnabled(true)`(必须) | 自带 |
|
| DOM storage | `setDomStorageEnabled(true)`(必须) | 自带 |
|
||||||
| 键盘顶起 | `windowSoftInputMode="adjustResize"` | 自带 |
|
| 键盘顶起 | `windowSoftInputMode="adjustResize"` | 自带 |
|
||||||
| 外链 | 非 platform/zcbot 域交给系统浏览器 | 同左 |
|
| 外链 | 非 platform/zcbot 域交给系统浏览器 | 同左 |
|
||||||
|
| PDF/PPT/HTML 预览 | zcbot 前端内置渲染,无需原生 PDF 组件 | 同左 |
|
||||||
|
|
||||||
SSE 流式输出两端 WebView 原生支持,无需处理。iOS 上架提醒:纯套壳有 App Store 4.2(最小功能)被拒风险;企业分发 / TestFlight / 仅 Android 无此问题。
|
SSE 流式输出两端 WebView 原生支持,无需处理。iOS 上架提醒:纯套壳有 App Store 4.2(最小功能)被拒风险;企业分发 / TestFlight / 仅 Android 无此问题。
|
||||||
|
|
||||||
|
URL 白名单只处理**主 frame**导航。Android 检查 `request.isForMainFrame`,iOS 检查 `navigationAction.targetFrame?.isMainFrame`;不要拦截 zcbot 页面内部的 worker 或子 frame 请求,否则 PDF.js worker、HTML sandbox 等站内预览仍会白屏。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. 备选:H5 登录页
|
## 4. 备选:H5 登录页
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,10 @@
|
||||||
> 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。
|
> 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。
|
||||||
> 工程口径的完整记录见 `PROGRESS.md` / git log。
|
> 工程口径的完整记录见 `PROGRESS.md` / git log。
|
||||||
|
|
||||||
|
## 0.63.2 — 2026-08-10
|
||||||
|
|
||||||
|
- App 内的 PDF 和 PPT 预览不再依赖 WebView 自带的 PDF 查看能力,现可直接翻页、适宽和缩放;HTML 预览同步修复部分 WebView 中加载空白的问题。
|
||||||
|
|
||||||
## 0.63.1 — 2026-08-07
|
## 0.63.1 — 2026-08-07
|
||||||
|
|
||||||
- 管理员调整外部系统定义时不再清除用户凭据:查询策略、提示和限额变化不中断连接;接口目标或认证配置变化后只暂停调用,用户在“外部”页面测试成功即可恢复。
|
- 管理员调整外部系统定义时不再清除用户凭据:查询策略、提示和限额变化不中断连接;接口目标或认证配置变化后只暂停调用,用户在“外部”页面测试成功即可恢复。
|
||||||
|
|
|
||||||
|
|
@ -311,7 +311,7 @@ scheduled_jobs(§8.5) channel_bindings(§8.7,判别列+JSONB)
|
||||||
|
|
||||||
### 8.3 PPTX 前端在线预览(✅ Stage 1)
|
### 8.3 PPTX 前端在线预览(✅ Stage 1)
|
||||||
|
|
||||||
关键洞察:前端已有 PDF iframe 路径 → 后端 soffice 转 PDF 即可,前端几乎不动。选 LibreOffice(像素级保真、任意 pptx)不选轻量 HTML(复杂失真)/PDF→PNG(失矢量)。转换在 web host 不进沙盒;宏安全 high + 禁网 + 仅本人 user_root。与 `pptx_preview.py`(agent 生成期自检)分工。Stage 2 未做:常驻 listener / eager 预转。
|
关键洞察:后端 soffice 把 PPT/PPTX 统一转成 PDF,前端只维护一条 PDF 展示路径。选 LibreOffice(像素级保真、任意 pptx)不选轻量 HTML(复杂失真)/PDF→PNG(失矢量)。转换在 web host 不进沙盒;宏安全 high + 禁网 + 仅本人 user_root。2026-08 App WebView 暴露浏览器内置 PDF 插件不可用,展示层由 blob iframe 改为本地 PDF.js canvas(单页按需渲染,避免长文档一次性占满内存);Web 端与 App 共用,不新增免鉴权文件 URL。任意 HTML 仍在 opaque-origin sandbox iframe 内运行,但内容入口改为同源静态宿主页 + postMessage,避开 WebView 的 `srcdoc` 空白和原生 URL 白名单误拦 `blob:`。与 `pptx_preview.py`(agent 生成期自检)分工。Stage 2 未做:常驻 listener / eager 预转。
|
||||||
|
|
||||||
### 8.4 运维监控 / 无感更新(监控 ✅ / 换版 design)
|
### 8.4 运维监控 / 无感更新(监控 ✅ / 换版 design)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。
|
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。
|
||||||
|
|
||||||
最后更新:2026-08-07(外部连接定义更新保凭据 + agent 状态可见,bump 0.63.1)
|
最后更新:2026-08-10(App WebView PDF/PPT/HTML 预览兼容,bump 0.63.2)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -21,6 +21,10 @@
|
||||||
|
|
||||||
## 已完成关键能力
|
## 已完成关键能力
|
||||||
|
|
||||||
|
### 2026-08-10
|
||||||
|
|
||||||
|
- **08-10 / 0.63.2 / App WebView PDF/PPT/HTML 预览兼容**:PDF 与 PPT 转换结果不再通过 blob iframe 依赖浏览器内置 PDF 插件,改为本地固化 PDF.js 后按单页 Canvas 渲染,支持翻页、适宽与缩放,并在关闭或切换预览时释放 loading/render task、worker 和文档资源。HTML 保留 opaque-origin sandbox 安全边界,内容入口由兼容不稳的 `srcdoc` 改为同源静态宿主页 + postMessage,避开原生 URL 白名单误拦 `blob:`;App 对接文档明确白名单仅处理主 frame。Node 前端 18 项、静态资源 unittest 12 项、JavaScript/PDF.js bundle 语法及 diff 检查通过;无 schema、migration、HTTP API 或 Python 依赖变化,未连接生产 DB。
|
||||||
|
|
||||||
### 2026-08-07
|
### 2026-08-07
|
||||||
|
|
||||||
- **08-07 / 0.63.1 / 外部定义更新保凭据 + agent 状态可见**:生产 task `96bef58e` 暴露旧版 Factory 定义补齐默认认证字段时被误判为认证绑定变化,用户密文被清空为 `needs_credentials`,而工具组整体隐藏又让模型转查本地与互联网。现对新旧配置先做默认值补全后的语义比较:查询提示、推荐入口、执行策略和响应限额变化由 active 连接原子跟随 revision;目标、登录、认证绑定或 TLS 变化保留密文并置 `needs_reverify`,用户主动测试成功后恢复。只读 `external_system_list` 在任意连接状态下均可挂载并返回操作提示,search/call/read/export 仍仅对 active 且 revision 匹配的连接开放。完整 512 项 unittest 全绿(17 skip),外部系统专项 46 项、无 DB 路由 23 项、外部模块 mypy、Python 编译、Ruff 致命规则与 diff 检查通过;无 schema、migration、依赖或 HTTP API 变化,只读排查生产 task,未写生产 DB。
|
- **08-07 / 0.63.1 / 外部定义更新保凭据 + agent 状态可见**:生产 task `96bef58e` 暴露旧版 Factory 定义补齐默认认证字段时被误判为认证绑定变化,用户密文被清空为 `needs_credentials`,而工具组整体隐藏又让模型转查本地与互联网。现对新旧配置先做默认值补全后的语义比较:查询提示、推荐入口、执行策略和响应限额变化由 active 连接原子跟随 revision;目标、登录、认证绑定或 TLS 变化保留密文并置 `needs_reverify`,用户主动测试成功后恢复。只读 `external_system_list` 在任意连接状态下均可挂载并返回操作提示,search/call/read/export 仍仅对 active 且 revision 匹配的连接开放。完整 512 项 unittest 全绿(17 skip),外部系统专项 46 项、无 DB 路由 23 项、外部模块 mypy、Python 编译、Ruff 致命规则与 diff 检查通过;无 schema、migration、依赖或 HTTP API 变化,只读排查生产 task,未写生产 DB。
|
||||||
|
|
|
||||||
2
RUN.md
2
RUN.md
|
|
@ -1032,7 +1032,7 @@ sudo xfs_quota -x -c "limit -p bhard=10g zcbot_<user_uuid>" /opt
|
||||||
- **入口**:`main.py`(`web / db / probe / user`)→ `core/agent_builder.py::build_agent`
|
- **入口**:`main.py`(`web / db / probe / user`)→ `core/agent_builder.py::build_agent`
|
||||||
- **核心**:`core/{agent_builder, loop, session, task, llm, memory, paths}.py` + `core/storage/{engine,models,utils}.py` + `db/migrations/`
|
- **核心**:`core/{agent_builder, loop, session, task, llm, memory, paths}.py` + `core/storage/{engine,models,utils}.py` + `db/migrations/`
|
||||||
- **工具**:`tools/{fs, shell, run_python, skill_tool}.py`
|
- **工具**:`tools/{fs, shell, run_python, skill_tool}.py`
|
||||||
- **Web**:`web/{app.py, auth.py, broker.py, sinks.py}` + `web/static/dev.html`(dev SPA)+ `web/static/vendor/`(office 预览 jszip/docx-preview/xlsx)
|
- **Web**:`web/{app.py, auth.py, broker.py, sinks.py}` + `web/static/dev.html`(dev SPA)+ `web/static/html_preview_host.html`(WebView 兼容的 sandbox HTML 宿主页)+ `web/static/vendor/`(PDF.js 与 office 预览 jszip/docx-preview/xlsx)
|
||||||
- **配置**:`config/agent.yaml` + `config/models/*.yaml`(§3.2 Model Profile)
|
- **配置**:`config/agent.yaml` + `config/models/*.yaml`(§3.2 Model Profile)
|
||||||
- **模型档位(per-account 模型访问)**:`config/agent.yaml` `model_tiers` 段定义「档位→可用模型 id 集合」;`users.plan` 存档位名,空/未知 → `default` 档,`role=admin` 全开。管理后台「各用户用量」表的「档位」下拉改 plan(`PATCH /v1/admin/users/{uid}/plan`);档位定义见 `GET /v1/admin/tiers`。改 `model_tiers` 后**重启 web** 生效;无需 migration(`plan` 列 0001 起就有)。模型 id:文本=`family.variant`,图/视频=variant key。行为:用户只看到本档模型;显式选档外模型 403;老 task 下次发消息若模型已不在档位内 → 自动落回 `deepseek_v4.flash`。
|
- **模型档位(per-account 模型访问)**:`config/agent.yaml` `model_tiers` 段定义「档位→可用模型 id 集合」;`users.plan` 存档位名,空/未知 → `default` 档,`role=admin` 全开。管理后台「各用户用量」表的「档位」下拉改 plan(`PATCH /v1/admin/users/{uid}/plan`);档位定义见 `GET /v1/admin/tiers`。改 `model_tiers` 后**重启 web** 生效;无需 migration(`plan` 列 0001 起就有)。模型 id:文本=`family.variant`,图/视频=variant key。行为:用户只看到本档模型;显式选档外模型 403;老 task 下次发消息若模型已不在档位内 → 自动落回 `deepseek_v4.flash`。
|
||||||
- **Skill**:`skills/{coding,ppt,proposal}/SKILL.md`(渐进披露,§3.5)
|
- **Skill**:`skills/{coding,ppt,proposal}/SKILL.md`(渐进披露,§3.5)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
||||||
# 改版本只动这一行。
|
# 改版本只动这一行。
|
||||||
__version__ = "0.63.1"
|
__version__ = "0.63.2"
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,35 @@ test("HTML preview allows HTTPS scripts while blocking host privileges and navig
|
||||||
assert.match(document, /base-uri 'none'/);
|
assert.match(document, /base-uri 'none'/);
|
||||||
assert.ok(document.indexOf("Content-Security-Policy") < document.indexOf("<title>"));
|
assert.ok(document.indexOf("Content-Security-Policy") < document.indexOf("<title>"));
|
||||||
const attrs = {};
|
const attrs = {};
|
||||||
const frame = { setAttribute(name, value) { attrs[name] = value; } };
|
let onload = null;
|
||||||
|
let posted = null;
|
||||||
|
const frame = {
|
||||||
|
contentWindow: { postMessage(value, origin) { posted = { value, origin }; } },
|
||||||
|
setAttribute(name, value) { attrs[name] = value; },
|
||||||
|
addEventListener(type, fn) { if (type === "load") onload = fn; },
|
||||||
|
};
|
||||||
configureHtmlPreviewFrame(frame, "<h1>x</h1>");
|
configureHtmlPreviewFrame(frame, "<h1>x</h1>");
|
||||||
assert.equal(attrs.sandbox, "allow-scripts");
|
assert.equal(attrs.sandbox, "allow-scripts");
|
||||||
|
assert.equal(attrs.csp, undefined);
|
||||||
assert.doesNotMatch(attrs.sandbox, /allow-same-origin|allow-forms|allow-top-navigation|allow-popups/);
|
assert.doesNotMatch(attrs.sandbox, /allow-same-origin|allow-forms|allow-top-navigation|allow-popups/);
|
||||||
assert.equal(frame.referrerPolicy, "no-referrer");
|
assert.equal(frame.referrerPolicy, "no-referrer");
|
||||||
|
assert.equal(frame.src, "/static/html_preview_host.html");
|
||||||
|
assert.equal(frame.srcdoc, undefined);
|
||||||
|
onload();
|
||||||
|
assert.equal(posted.origin, "*");
|
||||||
|
assert.equal(posted.value.type, "zcbot-html-preview");
|
||||||
|
assert.match(posted.value.document, /<h1>x<\/h1>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("HTML preview uses a same-origin message host instead of srcdoc or blob navigation", () => {
|
||||||
|
assert.match(previewJs, /configureHtmlPreviewFrame\(frame, text\)/);
|
||||||
|
const source = readFileSync(new URL("../web/static/js/preview_content.js", import.meta.url), "utf8");
|
||||||
|
const host = readFileSync(new URL("../web/static/html_preview_host.html", import.meta.url), "utf8");
|
||||||
|
assert.match(source, /HTML_PREVIEW_HOST = "\/static\/html_preview_host\.html"/);
|
||||||
|
assert.match(source, /contentWindow\.postMessage/);
|
||||||
|
assert.doesNotMatch(source, /URL\.createObjectURL|frame\.srcdoc\s*=/);
|
||||||
|
assert.match(host, /event\.source !== parent/);
|
||||||
|
assert.match(host, /document\.write\(event\.data\.document\)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("HTML fragments receive a restrictive head before their content", () => {
|
test("HTML fragments receive a restrictive head before their content", () => {
|
||||||
|
|
@ -49,6 +73,15 @@ test("main and mini previews expose preview/source mode controls", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("PDF previews use the bundled PDF.js canvas renderer for WebView compatibility", () => {
|
||||||
|
assert.match(previewJs, /vendor\/pdfjs\/pdf\.min\.js/);
|
||||||
|
assert.match(previewJs, /pdfjsLib\.getDocument\(\{ data \}\)/);
|
||||||
|
assert.match(previewJs, /page\.render\(\{/);
|
||||||
|
assert.doesNotMatch(previewJs, /iframe class="preview-frame" src="\$\{[^}]*application\/pdf/);
|
||||||
|
assert.match(pageHtml, /\.pdf-viewport/);
|
||||||
|
assert.match(pageHtml, /\.pdf-canvas/);
|
||||||
|
});
|
||||||
|
|
||||||
test("assistant HTML artifacts render inline with lazy loading and an expand action", () => {
|
test("assistant HTML artifacts render inline with lazy loading and an expand action", () => {
|
||||||
const mediaJs = readFileSync(new URL("../web/static/js/media.js", import.meta.url), "utf8");
|
const mediaJs = readFileSync(new URL("../web/static/js/media.js", import.meta.url), "utf8");
|
||||||
assert.match(mediaJs, /cat === "html"/);
|
assert.match(mediaJs, /cat === "html"/);
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ UI_CSS = STATIC_DIR / "ui.css"
|
||||||
JS_DIR = STATIC_DIR / "js"
|
JS_DIR = STATIC_DIR / "js"
|
||||||
VENDOR_DIR = STATIC_DIR / "vendor" / "markdown"
|
VENDOR_DIR = STATIC_DIR / "vendor" / "markdown"
|
||||||
MERMAID_VENDOR_DIR = STATIC_DIR / "vendor" / "mermaid"
|
MERMAID_VENDOR_DIR = STATIC_DIR / "vendor" / "mermaid"
|
||||||
|
PDFJS_VENDOR_DIR = STATIC_DIR / "vendor" / "pdfjs"
|
||||||
|
|
||||||
|
|
||||||
def _frontend_source() -> str:
|
def _frontend_source() -> str:
|
||||||
|
|
@ -50,6 +51,19 @@ class StaticVendorTests(unittest.TestCase):
|
||||||
self.assertGreater(mermaid_bundle.stat().st_size, 1_000_000)
|
self.assertGreater(mermaid_bundle.stat().st_size, 1_000_000)
|
||||||
self.assertTrue(mermaid_license.exists(), f"missing vendored license: {mermaid_license}")
|
self.assertTrue(mermaid_license.exists(), f"missing vendored license: {mermaid_license}")
|
||||||
|
|
||||||
|
def test_pdfjs_assets_are_vendored_with_license(self) -> None:
|
||||||
|
for name in ("pdf.min.js", "pdf.worker.min.js", "LICENSE.txt"):
|
||||||
|
path = PDFJS_VENDOR_DIR / name
|
||||||
|
self.assertTrue(path.exists(), f"missing vendored asset: {path}")
|
||||||
|
self.assertGreater(path.stat().st_size, 0, f"empty vendored asset: {path}")
|
||||||
|
|
||||||
|
def test_html_preview_host_is_shipped(self) -> None:
|
||||||
|
host = STATIC_DIR / "html_preview_host.html"
|
||||||
|
self.assertTrue(host.exists())
|
||||||
|
source = host.read_text(encoding="utf-8")
|
||||||
|
self.assertIn("zcbot-html-preview", source)
|
||||||
|
self.assertIn("event.source !== parent", source)
|
||||||
|
|
||||||
def test_dev_html_uses_local_ui_foundation(self) -> None:
|
def test_dev_html_uses_local_ui_foundation(self) -> None:
|
||||||
html = DEV_HTML.read_text(encoding="utf-8")
|
html = DEV_HTML.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1363,6 +1363,17 @@
|
||||||
}
|
}
|
||||||
#file-preview-modal .body iframe.preview-frame { width: 100%; height: 100%; border: 0; }
|
#file-preview-modal .body iframe.preview-frame { width: 100%; height: 100%; border: 0; }
|
||||||
iframe.preview-html-frame { background: #fff; }
|
iframe.preview-html-frame { background: #fff; }
|
||||||
|
.pdf-preview { padding: 0 !important; overflow: hidden !important; background: #eef1f5; }
|
||||||
|
.pdf-toolbar {
|
||||||
|
min-height: 44px; padding: 6px 10px; display: flex; align-items: center; gap: 7px;
|
||||||
|
background: var(--panel); border-bottom: 1px solid var(--border); flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.pdf-toolbar label { display: flex; align-items: center; gap: 5px; font-size: 12px; color: var(--muted); white-space: nowrap; }
|
||||||
|
.pdf-toolbar .pdf-page { width: 48px; padding: 5px 4px; text-align: center; }
|
||||||
|
.pdf-toolbar-spacer { flex: 1; }
|
||||||
|
.pdf-zoom { min-width: 38px; text-align: center; font-size: 12px; color: var(--muted); }
|
||||||
|
.pdf-viewport { flex: 1; min-height: 0; overflow: auto; padding: 12px; overscroll-behavior: contain; }
|
||||||
|
.pdf-canvas { display: block; margin: 0 auto; background: #fff; box-shadow: 0 2px 12px rgba(15,23,42,.18); }
|
||||||
#file-preview-modal .body pre.preview-text {
|
#file-preview-modal .body pre.preview-text {
|
||||||
margin: 0; padding: 8px; background: var(--code-bg);
|
margin: 0; padding: 8px; background: var(--code-bg);
|
||||||
border-radius: var(--r-md); white-space: pre-wrap; word-break: break-word;
|
border-radius: var(--r-md); white-space: pre-wrap; word-break: break-word;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; object-src 'none'; base-uri 'none'; form-action 'none'">
|
||||||
|
<title>HTML 文件预览</title>
|
||||||
|
<style>html,body{height:100%;margin:0}body{display:grid;place-items:center;color:#667085;font:13px system-ui,sans-serif}</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<span>加载 HTML 中…</span>
|
||||||
|
<script>
|
||||||
|
window.addEventListener("message", function receivePreview(event) {
|
||||||
|
if (event.source !== parent || !event.data || event.data.type !== "zcbot-html-preview") return;
|
||||||
|
if (typeof event.data.document !== "string") return;
|
||||||
|
window.removeEventListener("message", receivePreview);
|
||||||
|
document.open();
|
||||||
|
document.write(event.data.document);
|
||||||
|
document.close();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
// 文件预览:主弹框(图/视频/PDF/文本/markdown/html/docx/xlsx,大文件降级下载)+
|
// 文件预览:主弹框(图/视频/PDF/文本/markdown/html/docx/xlsx,大文件降级下载)+
|
||||||
// 同时再开一个的小窗预览(mini)。docx/xlsx 走 loadScript 懒加载 vendor。
|
// 同时再开一个的小窗预览(mini)。PDF 用 PDF.js canvas 渲染以兼容 App WebView,
|
||||||
|
// docx/xlsx 走 loadScript 懒加载 vendor。
|
||||||
// 导出 open*/close* 供 files / 媒体 chip / 粘贴文件 / main 的 Esc 关栈调用;
|
// 导出 open*/close* 供 files / 媒体 chip / 粘贴文件 / main 的 Esc 关栈调用;
|
||||||
// _categorize 也供 media 段判图/视频。反向依赖 downloadFile(media)、logout(auth)。
|
// _categorize 也供 media 段判图/视频。反向依赖 downloadFile(media)、logout(auth)。
|
||||||
import { state } from "./state.js";
|
import { state } from "./state.js";
|
||||||
|
|
@ -13,6 +14,8 @@ import { downloadFile } from "./media.js";
|
||||||
// ───── file preview ─────
|
// ───── file preview ─────
|
||||||
const PREVIEW_TEXT_MAX = 2 * 1024 * 1024;
|
const PREVIEW_TEXT_MAX = 2 * 1024 * 1024;
|
||||||
const PREVIEW_BIN_MAX = 50 * 1024 * 1024;
|
const PREVIEW_BIN_MAX = 50 * 1024 * 1024;
|
||||||
|
const PDFJS_SRC = "/static/vendor/pdfjs/pdf.min.js";
|
||||||
|
const PDFJS_WORKER_SRC = "/static/vendor/pdfjs/pdf.worker.min.js";
|
||||||
|
|
||||||
const _scriptCache = new Map();
|
const _scriptCache = new Map();
|
||||||
function loadScript(src) {
|
function loadScript(src) {
|
||||||
|
|
@ -76,6 +79,15 @@ function _resetTextModes(prefix) {
|
||||||
if (modes) modes.hidden = true;
|
if (modes) modes.hidden = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function _loadPdfJs() {
|
||||||
|
await loadScript(PDFJS_SRC);
|
||||||
|
if (!window.pdfjsLib || !window.pdfjsLib.getDocument) {
|
||||||
|
throw new Error("PDF.js 不可用");
|
||||||
|
}
|
||||||
|
window.pdfjsLib.GlobalWorkerOptions.workerSrc = PDFJS_WORKER_SRC;
|
||||||
|
return window.pdfjsLib;
|
||||||
|
}
|
||||||
|
|
||||||
function _showSource(body, text) {
|
function _showSource(body, text) {
|
||||||
body.className = "body";
|
body.className = "body";
|
||||||
body.innerHTML = "";
|
body.innerHTML = "";
|
||||||
|
|
@ -272,7 +284,7 @@ export async function openFilePreview(rel, taskId = "", legacy = false) {
|
||||||
|
|
||||||
const cat = _categorize(rel);
|
const cat = _categorize(rel);
|
||||||
// pptx/ppt:后端转 PDF 再复用现成 PDF iframe(非下载原文件),首次稍候 + 失败回退下载。
|
// pptx/ppt:后端转 PDF 再复用现成 PDF iframe(非下载原文件),首次稍候 + 失败回退下载。
|
||||||
if (cat === "ppt") { await _showPptAsPdf(rel, $("fp-body"), $("fp-meta"), _showFallback, _trackBlobUrl, taskId, legacy); return; }
|
if (cat === "ppt") { await _showPptAsPdf(rel, $("fp-body"), $("fp-meta"), _showFallback, taskId, legacy); return; }
|
||||||
try {
|
try {
|
||||||
const r = await fetch(_fileDownloadUrl(rel, taskId, legacy), {
|
const r = await fetch(_fileDownloadUrl(rel, taskId, legacy), {
|
||||||
headers: { "Authorization": "Bearer " + state.token },
|
headers: { "Authorization": "Bearer " + state.token },
|
||||||
|
|
@ -297,7 +309,7 @@ export async function openFilePreview(rel, taskId = "", legacy = false) {
|
||||||
}
|
}
|
||||||
if (cat === "image") _showImage(blob);
|
if (cat === "image") _showImage(blob);
|
||||||
else if (cat === "video") _showVideo(blob);
|
else if (cat === "video") _showVideo(blob);
|
||||||
else if (cat === "pdf") _showPdf(blob);
|
else if (cat === "pdf") await _showPdf(blob);
|
||||||
else if (cat === "docx") await _showDocx(blob);
|
else if (cat === "docx") await _showDocx(blob);
|
||||||
else if (cat === "xlsx") await _showXlsx(blob);
|
else if (cat === "xlsx") await _showXlsx(blob);
|
||||||
else _showFallback("暂不支持在线预览此格式,请下载查看");
|
else _showFallback("暂不支持在线预览此格式,请下载查看");
|
||||||
|
|
@ -336,15 +348,167 @@ function _showVideo(blob) {
|
||||||
body.appendChild(v);
|
body.appendChild(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _showPdf(blob) {
|
const _pdfPreviewStates = new WeakMap();
|
||||||
const url = _trackBlobUrl(blob, "application/pdf");
|
|
||||||
const body = $("fp-body");
|
function _clearPdfPreview(body) {
|
||||||
body.className = "body";
|
const state = _pdfPreviewStates.get(body);
|
||||||
body.innerHTML = `<iframe class="preview-frame" src="${url}"></iframe>`;
|
if (!state) return;
|
||||||
|
state.disposed = true;
|
||||||
|
if (state.renderTask) state.renderTask.cancel();
|
||||||
|
if (state.loadingTask) state.loadingTask.destroy();
|
||||||
|
if (state.resizeObserver) state.resizeObserver.disconnect();
|
||||||
|
if (state.onResize) window.removeEventListener("resize", state.onResize);
|
||||||
|
if (state.pdf) state.pdf.destroy();
|
||||||
|
_pdfPreviewStates.delete(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
// pptx/ppt → 后端转 PDF → iframe。main / mini 共用:传各自 body / meta / fallback / 追踪 blob 的 fn。
|
async function _showPdfIn(body, blob, fallbackFn) {
|
||||||
async function _showPptAsPdf(rel, body, metaEl, fallbackFn, trackFn = _trackBlobUrl, taskId = "", legacy = false) {
|
_clearPdfPreview(body);
|
||||||
|
const state = {
|
||||||
|
disposed: false,
|
||||||
|
pdf: null,
|
||||||
|
loadingTask: null,
|
||||||
|
pageNumber: 1,
|
||||||
|
zoom: 1,
|
||||||
|
renderTask: null,
|
||||||
|
renderSeq: 0,
|
||||||
|
resizeObserver: null,
|
||||||
|
onResize: null,
|
||||||
|
};
|
||||||
|
_pdfPreviewStates.set(body, state);
|
||||||
|
body.className = "body pdf-preview";
|
||||||
|
body.innerHTML = `
|
||||||
|
<div class="pdf-toolbar">
|
||||||
|
<button class="small pdf-prev" type="button" aria-label="上一页" disabled>上一页</button>
|
||||||
|
<label>第 <input class="pdf-page" type="number" min="1" value="1" inputmode="numeric" disabled> / <span class="pdf-pages">…</span> 页</label>
|
||||||
|
<button class="small pdf-next" type="button" aria-label="下一页" disabled>下一页</button>
|
||||||
|
<span class="pdf-toolbar-spacer"></span>
|
||||||
|
<button class="small pdf-zoom-out" type="button" aria-label="缩小" disabled>−</button>
|
||||||
|
<span class="pdf-zoom">适宽</span>
|
||||||
|
<button class="small pdf-zoom-in" type="button" aria-label="放大" disabled>+</button>
|
||||||
|
</div>
|
||||||
|
<div class="pdf-viewport"><div class="ph">解析 PDF 中…</div></div>`;
|
||||||
|
|
||||||
|
const viewportHost = body.querySelector(".pdf-viewport");
|
||||||
|
const pageInput = body.querySelector(".pdf-page");
|
||||||
|
const pagesLabel = body.querySelector(".pdf-pages");
|
||||||
|
const zoomLabel = body.querySelector(".pdf-zoom");
|
||||||
|
const prev = body.querySelector(".pdf-prev");
|
||||||
|
const next = body.querySelector(".pdf-next");
|
||||||
|
const zoomOut = body.querySelector(".pdf-zoom-out");
|
||||||
|
const zoomIn = body.querySelector(".pdf-zoom-in");
|
||||||
|
|
||||||
|
const renderPage = async () => {
|
||||||
|
if (state.disposed || !state.pdf) return;
|
||||||
|
const seq = ++state.renderSeq;
|
||||||
|
if (state.renderTask) state.renderTask.cancel();
|
||||||
|
const page = await state.pdf.getPage(state.pageNumber);
|
||||||
|
if (state.disposed || seq !== state.renderSeq) return;
|
||||||
|
const baseViewport = page.getViewport({ scale: 1 });
|
||||||
|
const fitScale = Math.max(0.1, (viewportHost.clientWidth - 24) / baseViewport.width);
|
||||||
|
const viewport = page.getViewport({ scale: fitScale * state.zoom });
|
||||||
|
const outputScale = Math.min(window.devicePixelRatio || 1, 2);
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
canvas.className = "pdf-canvas";
|
||||||
|
canvas.width = Math.floor(viewport.width * outputScale);
|
||||||
|
canvas.height = Math.floor(viewport.height * outputScale);
|
||||||
|
canvas.style.width = Math.floor(viewport.width) + "px";
|
||||||
|
canvas.style.height = Math.floor(viewport.height) + "px";
|
||||||
|
viewportHost.innerHTML = "";
|
||||||
|
viewportHost.appendChild(canvas);
|
||||||
|
state.renderTask = page.render({
|
||||||
|
canvasContext: context,
|
||||||
|
viewport,
|
||||||
|
transform: outputScale === 1 ? null : [outputScale, 0, 0, outputScale, 0, 0],
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await state.renderTask.promise;
|
||||||
|
} catch (e) {
|
||||||
|
if (e && e.name === "RenderingCancelledException") return;
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
if (seq === state.renderSeq) state.renderTask = null;
|
||||||
|
}
|
||||||
|
if (state.disposed || seq !== state.renderSeq) return;
|
||||||
|
pageInput.value = String(state.pageNumber);
|
||||||
|
prev.disabled = state.pageNumber <= 1;
|
||||||
|
next.disabled = state.pageNumber >= state.pdf.numPages;
|
||||||
|
zoomLabel.textContent = state.zoom === 1 ? "适宽" : Math.round(state.zoom * 100) + "%";
|
||||||
|
viewportHost.scrollTo({ top: 0, left: 0 });
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToPage = (pageNumber) => {
|
||||||
|
if (!state.pdf) return;
|
||||||
|
state.pageNumber = Math.min(state.pdf.numPages, Math.max(1, pageNumber));
|
||||||
|
renderPage().catch((e) => {
|
||||||
|
if (state.disposed) return;
|
||||||
|
_clearPdfPreview(body);
|
||||||
|
fallbackFn("PDF 渲染失败:" + e.message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
prev.onclick = () => goToPage(state.pageNumber - 1);
|
||||||
|
next.onclick = () => goToPage(state.pageNumber + 1);
|
||||||
|
pageInput.onchange = () => goToPage(parseInt(pageInput.value, 10) || 1);
|
||||||
|
zoomOut.onclick = () => {
|
||||||
|
state.zoom = Math.max(0.5, +(state.zoom / 1.25).toFixed(2));
|
||||||
|
renderPage().catch((e) => {
|
||||||
|
if (state.disposed) return;
|
||||||
|
_clearPdfPreview(body);
|
||||||
|
fallbackFn("PDF 渲染失败:" + e.message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
zoomIn.onclick = () => {
|
||||||
|
state.zoom = Math.min(3, +(state.zoom * 1.25).toFixed(2));
|
||||||
|
renderPage().catch((e) => {
|
||||||
|
if (state.disposed) return;
|
||||||
|
_clearPdfPreview(body);
|
||||||
|
fallbackFn("PDF 渲染失败:" + e.message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pdfjsLib = await _loadPdfJs();
|
||||||
|
const data = new Uint8Array(await blob.arrayBuffer());
|
||||||
|
state.loadingTask = pdfjsLib.getDocument({ data });
|
||||||
|
const pdf = await state.loadingTask.promise;
|
||||||
|
state.loadingTask = null;
|
||||||
|
if (state.disposed || _pdfPreviewStates.get(body) !== state) {
|
||||||
|
pdf.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.pdf = pdf;
|
||||||
|
pagesLabel.textContent = String(pdf.numPages);
|
||||||
|
pageInput.max = String(pdf.numPages);
|
||||||
|
pageInput.disabled = false;
|
||||||
|
zoomOut.disabled = false;
|
||||||
|
zoomIn.disabled = false;
|
||||||
|
await renderPage();
|
||||||
|
const rerender = () => {
|
||||||
|
if (state.disposed) return;
|
||||||
|
renderPage().catch(() => {});
|
||||||
|
};
|
||||||
|
if (window.ResizeObserver) {
|
||||||
|
state.resizeObserver = new ResizeObserver(rerender);
|
||||||
|
state.resizeObserver.observe(viewportHost);
|
||||||
|
} else {
|
||||||
|
state.onResize = rerender;
|
||||||
|
window.addEventListener("resize", rerender);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (!state.disposed) {
|
||||||
|
_clearPdfPreview(body);
|
||||||
|
fallbackFn("PDF 预览失败:" + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _showPdf(blob) {
|
||||||
|
await _showPdfIn($("fp-body"), blob, _showFallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// pptx/ppt → 后端转 PDF → PDF.js。main / mini 共用各自 body / meta / fallback。
|
||||||
|
async function _showPptAsPdf(rel, body, metaEl, fallbackFn, taskId = "", legacy = false) {
|
||||||
body.className = "body center";
|
body.className = "body center";
|
||||||
body.innerHTML = `<div class="ph"><div class="preview-spinner"></div>由 PPT 转换为 PDF · 首次稍候…</div>`;
|
body.innerHTML = `<div class="ph"><div class="preview-spinner"></div>由 PPT 转换为 PDF · 首次稍候…</div>`;
|
||||||
if (metaEl) metaEl.textContent = "";
|
if (metaEl) metaEl.textContent = "";
|
||||||
|
|
@ -367,8 +531,7 @@ async function _showPptAsPdf(rel, body, metaEl, fallbackFn, trackFn = _trackBlob
|
||||||
}
|
}
|
||||||
const blob = await r.blob();
|
const blob = await r.blob();
|
||||||
if (metaEl) metaEl.textContent = humanSize(blob.size) + " · PDF 预览";
|
if (metaEl) metaEl.textContent = humanSize(blob.size) + " · PDF 预览";
|
||||||
body.className = "body";
|
await _showPdfIn(body, blob, fallbackFn);
|
||||||
body.innerHTML = `<iframe class="preview-frame" src="${trackFn(blob, "application/pdf")}"></iframe>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _showText(text) { _showSource($("fp-body"), text); }
|
function _showText(text) { _showSource($("fp-body"), text); }
|
||||||
|
|
@ -467,6 +630,7 @@ export function closeFilePreview() {
|
||||||
document.body.classList.remove("fp-open");
|
document.body.classList.remove("fp-open");
|
||||||
$("file-preview-modal").style.removeProperty("--preview-bottom-inset");
|
$("file-preview-modal").style.removeProperty("--preview-bottom-inset");
|
||||||
_clearZoom($("fp-body"));
|
_clearZoom($("fp-body"));
|
||||||
|
_clearPdfPreview($("fp-body"));
|
||||||
_resetTextModes("fp");
|
_resetTextModes("fp");
|
||||||
$("fp-body").innerHTML = "";
|
$("fp-body").innerHTML = "";
|
||||||
_flushBlobUrls();
|
_flushBlobUrls();
|
||||||
|
|
@ -499,6 +663,7 @@ async function openMiniFilePreview(rel) {
|
||||||
const body = $("mp-body");
|
const body = $("mp-body");
|
||||||
_resetTextModes("mp");
|
_resetTextModes("mp");
|
||||||
_clearZoom(body);
|
_clearZoom(body);
|
||||||
|
_clearPdfPreview(body);
|
||||||
body.className = "body center";
|
body.className = "body center";
|
||||||
body.innerHTML = `<div class="ph">加载中…</div>`;
|
body.innerHTML = `<div class="ph">加载中…</div>`;
|
||||||
_flushMiniBlobUrls();
|
_flushMiniBlobUrls();
|
||||||
|
|
@ -506,7 +671,7 @@ async function openMiniFilePreview(rel) {
|
||||||
|
|
||||||
const cat = _categorize(rel);
|
const cat = _categorize(rel);
|
||||||
if (cat === "ppt") {
|
if (cat === "ppt") {
|
||||||
await _showPptAsPdf(rel, $("mp-body"), $("mp-meta"), _showMiniFallback, _trackMiniBlobUrl);
|
await _showPptAsPdf(rel, $("mp-body"), $("mp-meta"), _showMiniFallback);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|
@ -547,8 +712,7 @@ async function openMiniFilePreview(rel) {
|
||||||
v.controls = true;
|
v.controls = true;
|
||||||
body.appendChild(v);
|
body.appendChild(v);
|
||||||
} else if (cat === "pdf") {
|
} else if (cat === "pdf") {
|
||||||
body.className = "body";
|
await _showPdfIn(body, blob, _showMiniFallback);
|
||||||
body.innerHTML = `<iframe class="preview-frame" src="${_trackMiniBlobUrl(blob, "application/pdf")}"></iframe>`;
|
|
||||||
} else {
|
} else {
|
||||||
_showMiniFallback("暂不支持小窗预览此格式,请下载查看");
|
_showMiniFallback("暂不支持小窗预览此格式,请下载查看");
|
||||||
}
|
}
|
||||||
|
|
@ -576,6 +740,7 @@ function _showMiniFallback(msg) {
|
||||||
export function closeMiniPreview() {
|
export function closeMiniPreview() {
|
||||||
$("mini-preview-modal").classList.remove("show");
|
$("mini-preview-modal").classList.remove("show");
|
||||||
_clearZoom($("mp-body"));
|
_clearZoom($("mp-body"));
|
||||||
|
_clearPdfPreview($("mp-body"));
|
||||||
_resetTextModes("mp");
|
_resetTextModes("mp");
|
||||||
$("mp-body").innerHTML = "";
|
$("mp-body").innerHTML = "";
|
||||||
_flushMiniBlobUrls();
|
_flushMiniBlobUrls();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
// 可独立测试的 HTML 预览安全策略。脚本和 HTTPS 外部资源可运行,满足图表、
|
// 可独立测试的 HTML 预览安全策略。脚本和 HTTPS 外部资源可运行,满足图表、
|
||||||
// 地图、WebGL 等通用产物;iframe 保持 opaque origin,不能触达 zcbot 宿主权限。
|
// 地图、WebGL 等通用产物;iframe 保持 opaque origin,不能触达 zcbot 宿主权限。
|
||||||
|
// 使用同源静态宿主页 + postMessage,而非 iframe.srcdoc / blob URL:部分 WebView
|
||||||
|
// 对 srcdoc 支持不完整,原生壳的 URL 白名单又常会误拦 blob: 子 frame。
|
||||||
export const HTML_PREVIEW_CSP = "default-src 'none'; " +
|
export const HTML_PREVIEW_CSP = "default-src 'none'; " +
|
||||||
"script-src 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' https:; " +
|
"script-src 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' https:; " +
|
||||||
"style-src 'unsafe-inline' https:; img-src data: blob: https:; " +
|
"style-src 'unsafe-inline' https:; img-src data: blob: https:; " +
|
||||||
|
|
@ -7,6 +9,7 @@ export const HTML_PREVIEW_CSP = "default-src 'none'; " +
|
||||||
"worker-src blob:; child-src blob: https:; frame-src https:; manifest-src https:; " +
|
"worker-src blob:; child-src blob: https:; frame-src https:; manifest-src https:; " +
|
||||||
"object-src 'none'; form-action 'none'; base-uri 'none'";
|
"object-src 'none'; form-action 'none'; base-uri 'none'";
|
||||||
export const HTML_PREVIEW_SANDBOX = "allow-scripts";
|
export const HTML_PREVIEW_SANDBOX = "allow-scripts";
|
||||||
|
export const HTML_PREVIEW_HOST = "/static/html_preview_host.html";
|
||||||
|
|
||||||
export function htmlPreviewDocument(text) {
|
export function htmlPreviewDocument(text) {
|
||||||
const meta = `<meta http-equiv="Content-Security-Policy" content="${HTML_PREVIEW_CSP}">`;
|
const meta = `<meta http-equiv="Content-Security-Policy" content="${HTML_PREVIEW_CSP}">`;
|
||||||
|
|
@ -20,8 +23,11 @@ export function htmlPreviewDocument(text) {
|
||||||
export function configureHtmlPreviewFrame(frame, text, title = "HTML 文件预览") {
|
export function configureHtmlPreviewFrame(frame, text, title = "HTML 文件预览") {
|
||||||
frame.title = title;
|
frame.title = title;
|
||||||
frame.setAttribute("sandbox", HTML_PREVIEW_SANDBOX);
|
frame.setAttribute("sandbox", HTML_PREVIEW_SANDBOX);
|
||||||
frame.setAttribute("csp", HTML_PREVIEW_CSP);
|
|
||||||
frame.referrerPolicy = "no-referrer";
|
frame.referrerPolicy = "no-referrer";
|
||||||
frame.srcdoc = htmlPreviewDocument(text);
|
const document = htmlPreviewDocument(text);
|
||||||
|
frame.addEventListener("load", () => {
|
||||||
|
frame.contentWindow.postMessage({ type: "zcbot-html-preview", document }, "*");
|
||||||
|
}, { once: true });
|
||||||
|
frame.src = HTML_PREVIEW_HOST;
|
||||||
return frame;
|
return frame;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue