refactor(dev): 前端模块化 Step 2 — 抽出 embed.js(iframe 模式)

父页面经 postMessage 推 token 进入应用 + 401 重签(原 main 1147–1209 +
顶层 _embedInitialTaskHandled 一次性标志)→ embed.js(75 行)。

- 导出 embedInit(boot 调)+ embedPostToParent / embedShowWaiting
  (auth 的 logout 在 embed 下通知父页面/显示等待态)—— 后两个从 main
  迁出后,auth.js 对它们的 import 从 ./main.js 改指 ./embed.js
  (auth 仍从 main import enterApp)。
- 反向 import main glue enterApp / loadTaskList / selectTask。
  main↔embed、auth↔embed 均运行时调用环,安全。

main.js 删至 1154 行(2719 起,已搬出约 58%)。node 全检过、
import/export 一致性过、静态测试 2 过。剩 main 内:enterApp glue +
tasks(列表/选择/渲染消息)+ stream(发送/SSE)+ boot + Esc 关栈。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-06-07 19:35:01 +08:00
parent 40fefdffef
commit 36dbdb2dda
4 changed files with 79 additions and 69 deletions

View File

@ -2,7 +2,7 @@
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`
最后更新:2026-06-07(前端模块化 Step 2:抽出 … / media / newtask.js)
最后更新:2026-06-07(前端模块化 Step 2:抽出 … / newtask / embed.js;main 1154 行)
---
@ -23,6 +23,7 @@
### 2026-06-06
- **前端模块化 Step 2:抽出 `embed.js`(iframe 模式)**:父页面经 postMessage 推 token 进入应用 + 401 重签(原 main.js 11471209 + 顶层 `_embedInitialTaskHandled` 一次性标志)→ `embed.js`(75 行)。导出 `embedInit`(boot 调)+ `embedPostToParent`/`embedShowWaiting`(auth 的 logout 在 embed 下通知父页面/显示等待态)——后两个从 main 迁出后,`auth.js` 对它们的 import 从 `./main.js` 改指 `./embed.js`(auth 仍从 main import enterApp)。反向 import main glue `enterApp`/`loadTaskList`/`selectTask`。main↔embed、auth↔embed 均运行时调用环,安全。main.js 删至 **1154 行**(2719 行起,已搬出约 58%)。node 全检过、import/export 一致性过、静态测试 2 过。剩 main 内:`enterApp` glue + tasks(列表/选择/渲染消息)+ stream(发送/SSE)+ boot + Esc 关栈,待最后一并处理 tasks+stream。
- **前端模块化 Step 2:抽出 `newtask.js`(新建任务弹框)**:任务名 / 工作目录(新建 sentinel 或复用已有 + 二级 input 联动)/ 描述 / skill / 模型 select,提交 `POST /v1/tasks`(原 main.js 11461320)→ `newtask.js`(186 行)。顶层自绑 hd-new 打开 / nt-go 提交 / 各 input 联动;唯一对外导出 `loadFolderSuggestions`(供 main enterApp 初始化顶部 filter-wd、files 复制/移动后刷目录)——它从 main 迁来后,`files.js` 对它的 import 从 `./main.js` 改指 `./newtask.js`。反向 import main glue `loadModels`(加 `export`)/`loadTaskList`/`selectTask` + `logout`(auth)。main.js 删至 1220 行。node 全检过、import/export 一致性校验过、私有符号清零。
- **前端模块化 Step 2:抽出 `media.js`(工具活动标签 + artifact 抽取/渲染)+ 收敛 downloadFile 反向依赖**:对话内 `toolActivityLabel`(工具调用→中文活动名)、`extractArtifactRels`(从结果文本/working_dir 提产物路径)、`extractMediaBanner`(seedream/seedance 横幅)、`renderArtifactBarHtml`(产物 chip 条 + 图/视频内联占位)、`upgradeMediaArtifacts`(占位异步 fetch blob 填 `<img>`/`<video>` 带缓存)、`downloadFile`(blob 下载)→ `media.js`(237 行,原 main.js 11341359)。**收敛点**:downloadFile 移入 media 后,`preview.js`/`files.js` 对它的 import 从 `./main.js` 改指 `./media.js` —— 把这条反向依赖从 main 挪开。media 导入极少(`escapeHtml`/`_categorize`(preview)/`state`/`logout`),与 preview 成 media↔preview 环(均运行时调用,安全)。**两次险漏靠校验抓回**:① 共享 const `ARTIFACT_PRODUCING_TOOLS`(main renderMessages/SSE 用 4 处,`.has()` 访问非函数调用,"被调标识符"法漏掉)② 内部函数 `_flushMediaArtifactCache`(selectTask 切任务清缓存用)—— 残留符号检查发现后补 export。新增**全模块 import/export 一致性校验脚本**(每个 `import{X}` 必在目标 `export`),11 模块全过。main.js 删至 1393 行。`node --check` 11 模块全过、静态测试 2 过。
- **前端模块化 Step 2:抽出 `files.js`(文件面板 + 选入 + 拖拽上传)**:右栏文件列表浏览/导航/删除/重命名 + 刷新 + "选入"弹框(跨目录勾选复制/移动)+ 拖拽 overlay + 上传(XHR 带进度)+ 上传状态条。代码原分散在 main.js **两段非连续区**(11331459 文件列表/选入/拖拽 + 16971786 上传 helper,中间夹着 media 段)→ 合并进 `files.js`(433 行)。导出 `loadFiles`/`scheduleFilesRefresh`(SSE 文件事件刷新)/`closeSrcPicker`(main Esc 关栈)/`uploadFiles`(聊天区粘贴/拖拽复用);其余入口模块顶层自绑。反向 import `openFilePreview`(preview)、`logout`(auth)、main glue `downloadFile`/`selectTask`/`loadTaskList`/`loadFolderSuggestions`(后三个加 `export`,后续随 tasks/newtask 模块化再迁)。依赖分析用"段内被调标识符 段内定义 叶子/全局"全量提取,补回固定清单漏掉的 `loadFolderSuggestions`/`loadTaskList`。main.js 删至 1619 行。`node --check` 双过、main 残留 files 私有符号清零、files 无未导入 glue、静态测试 2 过。

View File

@ -6,7 +6,8 @@
import { state, LS_TOKEN, LS_UID, LS_NAME, EMBED } from "./state.js";
import { $ } from "./dom.js";
import { api } from "./api.js";
import { enterApp, embedPostToParent, embedShowWaiting } from "./main.js";
import { enterApp } from "./main.js";
import { embedPostToParent, embedShowWaiting } from "./embed.js";
// ───── login ─────
let loginTab = "pw"; // "pw" | "key";持久化 last-used tab 在 LS,刷新后默认那个

74
web/static/js/embed.js Normal file
View File

@ -0,0 +1,74 @@
// embed(iframe)模式:父页面经 postMessage 推送 token → 进入应用;401 后重签。
// 顶层无副作用,boot 决定是否调 embedInit。导出 embedInit(boot 调)+
// embedPostToParent / embedShowWaiting(auth 的 logout 在 embed 下通知父页面/显示等待态)。
import { state, LS_TOKEN, LS_UID, LS_NAME, EMBED_PARENT_ORIGIN, EMBED_INITIAL_TASK_ID } from "./state.js";
import { $ } from "./dom.js";
import { enterApp, loadTaskList, selectTask } from "./main.js";
// embed 首个 task 自动定位的一次性标志(仅 embed 段使用)
let _embedInitialTaskHandled = false;
// ───── embed mode ─────
export function embedPostToParent(msg) {
if (!EMBED_PARENT_ORIGIN || window.parent === window) return;
try { window.parent.postMessage(msg, EMBED_PARENT_ORIGIN); } catch (e) {}
}
export 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 / 不重复定位 task(尊重用户中间切过的选择)
loadTaskList();
} else {
enterApp();
// 首次签发:若 URL 带 task_id,定位到该 task(loadMessages 由 selectTask 触发)
if (EMBED_INITIAL_TASK_ID && !_embedInitialTaskHandled) {
_embedInitialTaskHandled = true;
selectTask(EMBED_INITIAL_TASK_ID);
}
}
}
}
export 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");
window.addEventListener("message", embedHandleMessage);
if (state.token) {
enterApp();
if (EMBED_INITIAL_TASK_ID && !_embedInitialTaskHandled) {
_embedInitialTaskHandled = true;
selectTask(EMBED_INITIAL_TASK_ID);
}
} else {
document.body.classList.add("embed-waiting");
embedShowWaiting("等待登录…", false);
}
embedPostToParent({ type: "zcbot-ready" });
}

View File

@ -19,9 +19,7 @@ import { openFilePreview, openPasteFilePreview, closeFilePreview, closeMiniPrevi
import { loadFiles, scheduleFilesRefresh, closeSrcPicker, uploadFiles } from "./files.js";
import { toolActivityLabel, _workingDirName, extractMediaBanner, extractArtifactRels, renderArtifactBarHtml, upgradeMediaArtifacts, ARTIFACT_PRODUCING_TOOLS, _flushMediaArtifactCache } from "./media.js";
import { loadFolderSuggestions } from "./newtask.js";
// embed 首个 task 自动定位的一次性标志(仅 embed 段使用)
let _embedInitialTaskHandled = false;
import { embedInit } from "./embed.js";
// ───── enter app ─────
export function enterApp() {
@ -1144,70 +1142,6 @@ document.addEventListener("keydown", (e) => {
});
// ───── embed mode ─────
export function embedPostToParent(msg) {
if (!EMBED_PARENT_ORIGIN || window.parent === window) return;
try { window.parent.postMessage(msg, EMBED_PARENT_ORIGIN); } catch (e) {}
}
export 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 / 不重复定位 task(尊重用户中间切过的选择)
loadTaskList();
} else {
enterApp();
// 首次签发:若 URL 带 task_id,定位到该 task(loadMessages 由 selectTask 触发)
if (EMBED_INITIAL_TASK_ID && !_embedInitialTaskHandled) {
_embedInitialTaskHandled = true;
selectTask(EMBED_INITIAL_TASK_ID);
}
}
}
}
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");
window.addEventListener("message", embedHandleMessage);
if (state.token) {
enterApp();
if (EMBED_INITIAL_TASK_ID && !_embedInitialTaskHandled) {
_embedInitialTaskHandled = true;
selectTask(EMBED_INITIAL_TASK_ID);
}
} else {
document.body.classList.add("embed-waiting");
embedShowWaiting("等待登录…", false);
}
embedPostToParent({ type: "zcbot-ready" });
}
// ───── boot ─────
if (EMBED) {
embedInit();