`,SKILL/system 早期写 `task_name` 在分叉场景下会误导助手拼错前缀。否决:(a) 后端 `_display` 改 task-relative 让 tool 输出本身就裸 —— `Tool` 基类 + fs/skill_tool/seedream/seedance/agent_builder/smoke 改 8 个文件,且 fs 跨 task 时要分层 fallback(working_dir → user_root → 绝对),复杂度超过收益;(b) 后端补 HEAD 探针让前端验文件存在再挂 chip —— 工程量与开发期需求不匹配;(c) 白名单常驻服务所有简写形式 —— 维护负担+清单可能膨胀,改成"一次性兼容历史消息"角色后边界清晰;(d) 每个写产物的 SKILL 各加一句"按 system 协议" —— 协议漂移源,违反"system 谈通用、SKILL 谈领域"边界。
diff --git a/RUN.md b/RUN.md
index 79089dc..1bc14d9 100644
--- a/RUN.md
+++ b/RUN.md
@@ -2,7 +2,7 @@
> 怎么把 zcbot 跑起来。env / 常用命令 / 故障兜底。设计看 `DESIGN.md`,进度看 `PROGRESS.md`。
-最后更新:2026-05-22(seedance 视频生成接入 — 同 ARK_API_KEY,新增 videogen skill)
+最后更新:2026-05-22(dev SPA 加 iframe embed 模式 — `?embed=1&parent_origin=...`,对接见 `EMBED.md`)
---
@@ -117,6 +117,8 @@ curl --noproxy '*' -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8765/v1/ta
**dev SPA**:打开 `http://127.0.0.1:8765/`(自动 302 → `/static/dev.html`),登录页两 tab(默认"邮箱密码",备用"UUID + PLATFORM_KEY",last-used 持久化 LS)进入 3 栏(task / chat / files)。给同事试用:`main.py user add` 发用户,**不用重启** web(每次 login 都查 DB),把 URL + 邮箱密码分别发给同事。
+**iframe 嵌入**(platform 主页内嵌):URL 加 `?embed=1&parent_origin=<父页面 origin>`,触发 embed 模式 —— 藏左上 brand / 退出按钮,登录页不显示,新建任务挪到任务面板;父页面通过 `postMessage` 协议推 JWT(`zcbot-ready` / `zcbot-token` / `zcbot-401`)。完整对接手册见 `EMBED.md`(URL 参数 / 协议 / 后端 SSO 示例 / 父端前端示例 / 安全 / 故障兜底)。
+
### 路由表
全 JSON,CORS `allow_origins=["*"]`;详细 schema 见 `/docs`。
diff --git a/web/static/dev.html b/web/static/dev.html
index e92b1cf..7980b12 100644
--- a/web/static/dev.html
+++ b/web/static/dev.html
@@ -620,6 +620,33 @@
border-radius: 0;
}
}
+
+ /* ───── embed mode (?embed=1&parent_origin=...) —— 父页面 iframe 嵌入 ─────
+ 藏左上 brand / 用户名 / 退出登录;桌面整层 header 去掉(没 mobile-tabs 切换需求);
+ "+ 新建任务" 由 JS 移到任务面板 pane-head。 */
+ body.embed-mode #login { display: none !important; }
+ body.embed-mode header .brand,
+ body.embed-mode header #hd-who,
+ body.embed-mode header #hd-logout { display: none; }
+ @media (min-width: 641px) {
+ body.embed-mode header { display: none; }
+ }
+ #embed-waiting {
+ position: fixed; inset: 0; z-index: 90;
+ display: none; align-items: center; justify-content: center;
+ background: var(--bg); color: var(--muted); font-size: 13px;
+ flex-direction: column; gap: 12px; padding: 24px;
+ }
+ body.embed-mode.embed-waiting #embed-waiting { display: flex; }
+ body.embed-mode.embed-waiting #app { visibility: hidden; }
+ #embed-waiting .text { text-align: center; max-width: 80%; }
+ #embed-waiting .err { color: var(--accent); font-size: 12px; max-width: 80%; text-align: center; min-height: 1em; }
+ @keyframes embed-spin { to { transform: rotate(360deg); } }
+ #embed-waiting .spinner {
+ width: 24px; height: 24px; border-radius: 50%;
+ border: 2px solid var(--border); border-top-color: var(--accent);
+ animation: embed-spin .8s linear infinite;
+ }
@@ -687,6 +714,13 @@
+
+
+
@@ -854,6 +888,11 @@ const LS_UID = "zcbot.user_id";
const LS_NAME = "zcbot.name";
const LS_LEFT_COLLAPSED = "zcbot.left-collapsed";
+// ?embed=1&parent_origin=https://... → iframe 模式;父页面用 postMessage 推 token
+const _embedQS = new URLSearchParams(location.search);
+const EMBED = _embedQS.get("embed") === "1";
+const EMBED_PARENT_ORIGIN = (_embedQS.get("parent_origin") || "").trim();
+
const state = {
token: localStorage.getItem(LS_TOKEN) || "",
userId: localStorage.getItem(LS_UID) || "",
@@ -1121,6 +1160,12 @@ function logout() {
localStorage.removeItem(LS_UID);
localStorage.removeItem(LS_NAME);
if (state.evtSrc) state.evtSrc.close();
+ if (EMBED) {
+ embedPostToParent({ type: "zcbot-401" });
+ embedShowWaiting("登录已失效,等待父页面重新签发…", false);
+ document.body.classList.add("embed-waiting");
+ return;
+ }
location.reload();
}
$("hd-logout").onclick = logout;
@@ -3145,8 +3190,73 @@ $("nt-wd-new").addEventListener("input", () => {
updateWdHint();
});
+// ───── embed mode ─────
+function embedPostToParent(msg) {
+ if (!EMBED_PARENT_ORIGIN || window.parent === window) return;
+ try { window.parent.postMessage(msg, EMBED_PARENT_ORIGIN); } catch (e) {}
+}
+function embedShowWaiting(text, isErr) {
+ const w = $("embed-waiting");
+ if (!w) return;
+ if (isErr) {
+ w.querySelector(".text").textContent = "";
+ w.querySelector(".err").textContent = text || "";
+ w.querySelector(".spinner").style.display = "none";
+ } else {
+ w.querySelector(".text").textContent = text || "等待登录…";
+ w.querySelector(".err").textContent = "";
+ w.querySelector(".spinner").style.display = "";
+ }
+}
+function embedHandleMessage(e) {
+ if (e.origin !== EMBED_PARENT_ORIGIN) return;
+ const d = e.data || {};
+ if (d.type === "zcbot-token" && d.token && d.user_id) {
+ state.token = d.token;
+ state.userId = d.user_id;
+ state.userName = d.user_name || "";
+ localStorage.setItem(LS_TOKEN, state.token);
+ localStorage.setItem(LS_UID, state.userId);
+ if (state.userName) localStorage.setItem(LS_NAME, state.userName);
+ else localStorage.removeItem(LS_NAME);
+ document.body.classList.remove("embed-waiting");
+ if ($("app").classList.contains("ready")) {
+ // 401 后重签:重载列表,不重复 enterApp
+ loadTaskList();
+ } else {
+ enterApp();
+ }
+ }
+}
+function embedInit() {
+ if (!EMBED_PARENT_ORIGIN) {
+ document.body.classList.add("embed-mode", "embed-waiting");
+ embedShowWaiting("embed 模式缺少 parent_origin 参数 (URL 必须形如 ?embed=1&parent_origin=https://your-portal.com)", true);
+ return;
+ }
+ document.body.classList.add("embed-mode");
+ // 把 #hd-new 从 header 移到任务面板 pane-head(spacer 之后、filter-status 之前)
+ const newBtn = $("hd-new");
+ const head = document.querySelector("#pane-left .pane-head");
+ const ref = $("filter-status");
+ if (newBtn && head && ref) {
+ newBtn.classList.add("small");
+ head.insertBefore(newBtn, ref);
+ }
+ window.addEventListener("message", embedHandleMessage);
+ if (state.token) {
+ enterApp();
+ } else {
+ document.body.classList.add("embed-waiting");
+ embedShowWaiting("等待登录…", false);
+ }
+ embedPostToParent({ type: "zcbot-ready" });
+}
+
// ───── boot ─────
-if (state.token) {
+if (EMBED) {
+ embedInit();
+} else if (state.token) {
// 已有 token:试探一下,失败回登录页
enterApp();
} else {