Compare commits
2 Commits
e5940266ca
...
36dbdb2dda
| Author | SHA1 | Date |
|---|---|---|
|
|
36dbdb2dda | |
|
|
40fefdffef |
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。
|
||||
|
||||
最后更新:2026-06-07(前端模块化 Step 2:抽出 layout / auth / preview / files / media.js)
|
||||
最后更新:2026-06-07(前端模块化 Step 2:抽出 … / newtask / embed.js;main 1154 行)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -23,6 +23,8 @@
|
|||
|
||||
### 2026-06-06
|
||||
|
||||
- **前端模块化 Step 2:抽出 `embed.js`(iframe 模式)**:父页面经 postMessage 推 token 进入应用 + 401 重签(原 main.js 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 关栈,待最后一并处理 tasks+stream。
|
||||
- **前端模块化 Step 2:抽出 `newtask.js`(新建任务弹框)**:任务名 / 工作目录(新建 sentinel 或复用已有 + 二级 input 联动)/ 描述 / skill / 模型 select,提交 `POST /v1/tasks`(原 main.js 1146–1320)→ `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 1134–1359)。**收敛点**: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 **两段非连续区**(1133–1459 文件列表/选入/拖拽 + 1697–1786 上传 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 过。
|
||||
- **前端模块化 Step 2:抽出 `preview.js`(文件预览 + mini 预览)**:文件预览主弹框(图/视频/PDF/文本/markdown/docx/xlsx,大文件降级下载,docx/xlsx 走 `loadScript` 懒加载 vendor)+ 同时再开的小窗预览(原 main.js 1687–2048)→ `preview.js`(379 行)。导出 `openFilePreview`/`openPasteFilePreview`/`closeFilePreview`/`closeMiniPreview`/`_categorize`(媒体段判图/视频用)。反向 import `downloadFile`(main 媒体段,加 `export`)、`logout`(auth)。**Esc 关弹窗栈处理器留 main**(跨模块协调 chpw/选入/文件预览/小预览,加了节注释)。**一处去耦**:`deletePastedFile`(留 main)原直接读 preview 私有 `_fpCurrentRel`/`_mpCurrentRel` 判断要不要关预览 → 改为 preview 导出封装 `closePreviewIfShowing(rel)`,行为不变但不泄漏内部 current-rel 状态(模块边界更干净;唯一非纯剪切的微调)。main.js 删至 2034 行。`node --check` 双过、preview 私有符号在 main 清零、无未导入 glue 引用、静态测试 2 过。
|
||||
|
|
|
|||
|
|
@ -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,刷新后默认那个
|
||||
|
|
|
|||
|
|
@ -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" });
|
||||
}
|
||||
|
||||
|
|
@ -11,7 +11,8 @@ import { escapeHtml, humanSize } from "./format.js";
|
|||
import { openFilePreview } from "./preview.js";
|
||||
import { logout } from "./auth.js";
|
||||
import { downloadFile } from "./media.js";
|
||||
import { selectTask, loadTaskList, loadFolderSuggestions } from "./main.js";
|
||||
import { selectTask, loadTaskList } from "./main.js";
|
||||
import { loadFolderSuggestions } from "./newtask.js";
|
||||
|
||||
// ───── files(user-rooted,不绑 task) ─────
|
||||
$("btn-refresh-files").onclick = () => loadFiles();
|
||||
|
|
|
|||
|
|
@ -18,9 +18,8 @@ import { logout, closeChpwModal } from "./auth.js";
|
|||
import { openFilePreview, openPasteFilePreview, closeFilePreview, closeMiniPreview, closePreviewIfShowing, _categorize } from "./preview.js";
|
||||
import { loadFiles, scheduleFilesRefresh, closeSrcPicker, uploadFiles } from "./files.js";
|
||||
import { toolActivityLabel, _workingDirName, extractMediaBanner, extractArtifactRels, renderArtifactBarHtml, upgradeMediaArtifacts, ARTIFACT_PRODUCING_TOOLS, _flushMediaArtifactCache } from "./media.js";
|
||||
|
||||
// embed 首个 task 自动定位的一次性标志(仅 embed 段使用)
|
||||
let _embedInitialTaskHandled = false;
|
||||
import { loadFolderSuggestions } from "./newtask.js";
|
||||
import { embedInit } from "./embed.js";
|
||||
|
||||
// ───── enter app ─────
|
||||
export function enterApp() {
|
||||
|
|
@ -62,7 +61,7 @@ async function loadStorage() {
|
|||
el.classList.add("show");
|
||||
}
|
||||
|
||||
async function loadModels() {
|
||||
export async function loadModels() {
|
||||
try {
|
||||
const data = await api("GET", "/v1/models");
|
||||
state.models = data.models || [];
|
||||
|
|
@ -1143,245 +1142,6 @@ document.addEventListener("keydown", (e) => {
|
|||
});
|
||||
|
||||
|
||||
// ───── new task ─────
|
||||
// wd 二级 input 默认跟随 name;用户一旦手改二级 input → 脱钩;清空二级 input 重置 flag
|
||||
let wdManuallyEdited = false;
|
||||
$("hd-new").onclick = async () => {
|
||||
$("nt-name").value = "";
|
||||
$("nt-wd-sel").value = "__new__"; // 默认选 sentinel
|
||||
$("nt-wd-new").value = "";
|
||||
$("nt-wd-new").style.display = ""; // sentinel 选中态 → 二级 input 可见
|
||||
$("nt-desc").value = ""; $("nt-skill").value = "";
|
||||
$("nt-err").textContent = "";
|
||||
$("nt-wd-hint").textContent = "";
|
||||
wdManuallyEdited = false;
|
||||
$("new-task-modal").classList.add("show");
|
||||
await Promise.all([loadFolderSuggestions(), loadSkillOptions(), loadModels()]);
|
||||
$("nt-wd-sel").value = "__new__"; // populateFolderSelects 重渲后再保险一次
|
||||
populateModelSelect();
|
||||
$("nt-name").focus();
|
||||
};
|
||||
function populateModelSelect() {
|
||||
const sel = $("nt-model");
|
||||
const models = state.models || [];
|
||||
if (models.length === 0) {
|
||||
sel.innerHTML = `<option value="">(默认)</option>`;
|
||||
return;
|
||||
}
|
||||
sel.innerHTML = models.map(m =>
|
||||
`<option value="${escapeHtml(m.profile)}" ${m.is_default ? "selected" : ""}>${escapeHtml(m.display_name)}</option>`
|
||||
).join("");
|
||||
}
|
||||
$("nt-cancel").onclick = () => $("new-task-modal").classList.remove("show");
|
||||
$("nt-go").onclick = async () => {
|
||||
const name = $("nt-name").value.trim();
|
||||
const sel = $("nt-wd-sel").value;
|
||||
// sentinel:用二级 input 值,空则 fallback name;选已有目录:直接用 value
|
||||
const working_dir = sel === "__new__"
|
||||
? ($("nt-wd-new").value.trim() || name)
|
||||
: sel;
|
||||
const desc = $("nt-desc").value.trim();
|
||||
const skill = $("nt-skill").value;
|
||||
const model_profile = $("nt-model").value;
|
||||
$("nt-err").textContent = "";
|
||||
if (!name) { $("nt-err").textContent = "任务名为必填项"; return; }
|
||||
try {
|
||||
const t = await api("POST", "/v1/tasks",
|
||||
{ name, working_dir, description: desc, skill, model_profile });
|
||||
$("new-task-modal").classList.remove("show");
|
||||
await loadTaskList();
|
||||
selectTask(t.task_id);
|
||||
} catch (e) {
|
||||
if (e.status === 401) { logout(); return; }
|
||||
$("nt-err").textContent = e.message;
|
||||
}
|
||||
};
|
||||
|
||||
// 工作目录:拉数据 + 灌两个 select(顶部 filter-wd 和 modal nt-wd-sel)
|
||||
export async function loadFolderSuggestions() {
|
||||
try {
|
||||
const data = await api("GET", "/v1/folders");
|
||||
state.folders = data.folders || [];
|
||||
} catch (e) {
|
||||
state.folders = state.folders || [];
|
||||
}
|
||||
populateFolderSelects();
|
||||
}
|
||||
|
||||
// 灌 filter-wd + nt-wd-sel options;保留当前选中值
|
||||
function populateFolderSelects() {
|
||||
const folders = state.folders || [];
|
||||
// 顶部 filter:第一项 "(全部目录)" sentinel
|
||||
const filterSel = $("filter-wd");
|
||||
const filterCur = filterSel.value;
|
||||
const filterOpts = ['<option value="">(全部目录)</option>'];
|
||||
for (const f of folders) {
|
||||
const tag = f.n_tasks ? `${f.n_tasks} 个任务` : `空目录`;
|
||||
filterOpts.push(`<option value="${escapeHtml(f.name)}">${escapeHtml(f.name)} — ${escapeHtml(tag)}</option>`);
|
||||
}
|
||||
filterSel.innerHTML = filterOpts.join("");
|
||||
filterSel.value = filterCur; // 重渲后恢复选中
|
||||
// modal wd:第一项 "+ 新建(跟随任务名)" sentinel(label 由 updateSentinelLabel 实时刷)
|
||||
const wdSel = $("nt-wd-sel");
|
||||
const wdCur = wdSel.value || "__new__";
|
||||
const wdOpts = [`<option value="__new__">+ 新建(跟随任务名)</option>`];
|
||||
for (const f of folders) {
|
||||
const tag = f.n_tasks ? `${f.n_tasks} 个任务` : `空目录`;
|
||||
wdOpts.push(`<option value="${escapeHtml(f.name)}">${escapeHtml(f.name)} — ${escapeHtml(tag)}</option>`);
|
||||
}
|
||||
wdSel.innerHTML = wdOpts.join("");
|
||||
wdSel.value = wdCur;
|
||||
updateSentinelLabel(); // 用最新的 name 刷 sentinel
|
||||
}
|
||||
|
||||
// 智能体类型下拉:skill registry 服务器端静态,首次加载后缓存到 state.skills
|
||||
async function loadSkillOptions() {
|
||||
const sel = $("nt-skill");
|
||||
if (!state.skills) {
|
||||
try {
|
||||
const data = await api("GET", "/v1/skills");
|
||||
state.skills = data.skills || [];
|
||||
} catch (e) {
|
||||
state.skills = []; // 静默兜底,select 仍保留"(默认)"项
|
||||
}
|
||||
}
|
||||
// 渲染:第一项固定为"默认"(空 value),其后逐 skill 一项
|
||||
const opts = ['<option value="">(默认 · 不限定)</option>'];
|
||||
for (const s of state.skills) {
|
||||
const label = `${s.name}${s.description ? " — " + s.description : ""}`;
|
||||
opts.push(`<option value="${escapeHtml(s.name)}" title="${escapeHtml(s.description || "")}">${escapeHtml(label)}</option>`);
|
||||
}
|
||||
sel.innerHTML = opts.join("");
|
||||
sel.value = ""; // hd-new 已清空,这里幂等再保一次
|
||||
}
|
||||
|
||||
// === modal wd select + 二级 input 联动 ===
|
||||
// select 选 "__new__" sentinel → 显示二级 input(默认值跟随 name);选已有目录 → 隐藏二级 input
|
||||
// wdManuallyEdited:用户改过二级 input 后置 true,name 不再覆盖;清空二级 input 重置 false
|
||||
|
||||
function updateSentinelLabel() {
|
||||
// sentinel 永远是 select 第一项,labels 实时含 name 让用户一眼知会建什么
|
||||
const sel = $("nt-wd-sel");
|
||||
const opt = sel.options[0];
|
||||
if (!opt || opt.value !== "__new__") return;
|
||||
const name = $("nt-name").value.trim();
|
||||
opt.textContent = name ? `+ 新建「${name}」` : `+ 新建(跟随任务名)`;
|
||||
}
|
||||
|
||||
function updateWdHint() {
|
||||
const hint = $("nt-wd-hint");
|
||||
const sel = $("nt-wd-sel").value;
|
||||
if (sel === "__new__") {
|
||||
const v = $("nt-wd-new").value.trim();
|
||||
const name = $("nt-name").value.trim();
|
||||
const target = v || name;
|
||||
if (!target) { hint.textContent = ""; return; }
|
||||
// 用户手输的新名恰好命中已有目录 → 提示会复用而非新建
|
||||
const collision = (state.folders || []).find(f => f.name === target);
|
||||
if (collision) {
|
||||
const n = collision.n_tasks || 0;
|
||||
hint.innerHTML = `<span style="color:var(--accent);">! 已有同名目录,将复用</span> · ${n} 个任务`;
|
||||
} else {
|
||||
hint.innerHTML = `<span class="muted">→ 新建目录 ${escapeHtml(target)}</span>`;
|
||||
}
|
||||
} else {
|
||||
const f = (state.folders || []).find(x => x.name === sel);
|
||||
const n = f ? (f.n_tasks || 0) : 0;
|
||||
hint.innerHTML = `<span style="color:var(--accent);">→ 复用已有目录</span> · ${n} 个任务`;
|
||||
}
|
||||
}
|
||||
|
||||
// name 改变 → 更新 sentinel label;若未脱钩且当前是 sentinel,二级 input 跟随 name
|
||||
$("nt-name").addEventListener("input", () => {
|
||||
updateSentinelLabel();
|
||||
if (!wdManuallyEdited && $("nt-wd-sel").value === "__new__") {
|
||||
$("nt-wd-new").value = $("nt-name").value;
|
||||
}
|
||||
updateWdHint();
|
||||
});
|
||||
|
||||
// wd select 切换 → 切显示二级 input + 刷 hint
|
||||
$("nt-wd-sel").addEventListener("change", () => {
|
||||
const v = $("nt-wd-sel").value;
|
||||
if (v === "__new__") {
|
||||
$("nt-wd-new").style.display = "";
|
||||
if (!wdManuallyEdited) $("nt-wd-new").value = $("nt-name").value;
|
||||
} else {
|
||||
$("nt-wd-new").style.display = "none";
|
||||
}
|
||||
updateWdHint();
|
||||
});
|
||||
|
||||
// 二级 input 改变 → 非空视为手动修改;清空重置 flag 但保持空(避免 backspace 想换名时被打断)
|
||||
$("nt-wd-new").addEventListener("input", () => {
|
||||
wdManuallyEdited = $("nt-wd-new").value.trim() !== "";
|
||||
updateWdHint();
|
||||
});
|
||||
|
||||
// ───── 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();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,185 @@
|
|||
// 新建任务弹框:任务名 / 工作目录(新建 sentinel 或复用已有,二级 input 联动)/
|
||||
// 描述 / 智能体(skill)/ 模型 select,提交 POST /v1/tasks。
|
||||
// 顶层自绑 hd-new 打开、nt-go 提交、各 input 联动;唯一对外导出 loadFolderSuggestions
|
||||
//(供 main enterApp 初始化顶部 filter-wd、files 复制/移动后刷新目录列表)。
|
||||
import { state } from "./state.js";
|
||||
import { api } from "./api.js";
|
||||
import { escapeHtml } from "./format.js";
|
||||
import { logout } from "./auth.js";
|
||||
import { loadModels, loadTaskList, selectTask } from "./main.js";
|
||||
|
||||
// ───── new task ─────
|
||||
// wd 二级 input 默认跟随 name;用户一旦手改二级 input → 脱钩;清空二级 input 重置 flag
|
||||
let wdManuallyEdited = false;
|
||||
$("hd-new").onclick = async () => {
|
||||
$("nt-name").value = "";
|
||||
$("nt-wd-sel").value = "__new__"; // 默认选 sentinel
|
||||
$("nt-wd-new").value = "";
|
||||
$("nt-wd-new").style.display = ""; // sentinel 选中态 → 二级 input 可见
|
||||
$("nt-desc").value = ""; $("nt-skill").value = "";
|
||||
$("nt-err").textContent = "";
|
||||
$("nt-wd-hint").textContent = "";
|
||||
wdManuallyEdited = false;
|
||||
$("new-task-modal").classList.add("show");
|
||||
await Promise.all([loadFolderSuggestions(), loadSkillOptions(), loadModels()]);
|
||||
$("nt-wd-sel").value = "__new__"; // populateFolderSelects 重渲后再保险一次
|
||||
populateModelSelect();
|
||||
$("nt-name").focus();
|
||||
};
|
||||
function populateModelSelect() {
|
||||
const sel = $("nt-model");
|
||||
const models = state.models || [];
|
||||
if (models.length === 0) {
|
||||
sel.innerHTML = `<option value="">(默认)</option>`;
|
||||
return;
|
||||
}
|
||||
sel.innerHTML = models.map(m =>
|
||||
`<option value="${escapeHtml(m.profile)}" ${m.is_default ? "selected" : ""}>${escapeHtml(m.display_name)}</option>`
|
||||
).join("");
|
||||
}
|
||||
$("nt-cancel").onclick = () => $("new-task-modal").classList.remove("show");
|
||||
$("nt-go").onclick = async () => {
|
||||
const name = $("nt-name").value.trim();
|
||||
const sel = $("nt-wd-sel").value;
|
||||
// sentinel:用二级 input 值,空则 fallback name;选已有目录:直接用 value
|
||||
const working_dir = sel === "__new__"
|
||||
? ($("nt-wd-new").value.trim() || name)
|
||||
: sel;
|
||||
const desc = $("nt-desc").value.trim();
|
||||
const skill = $("nt-skill").value;
|
||||
const model_profile = $("nt-model").value;
|
||||
$("nt-err").textContent = "";
|
||||
if (!name) { $("nt-err").textContent = "任务名为必填项"; return; }
|
||||
try {
|
||||
const t = await api("POST", "/v1/tasks",
|
||||
{ name, working_dir, description: desc, skill, model_profile });
|
||||
$("new-task-modal").classList.remove("show");
|
||||
await loadTaskList();
|
||||
selectTask(t.task_id);
|
||||
} catch (e) {
|
||||
if (e.status === 401) { logout(); return; }
|
||||
$("nt-err").textContent = e.message;
|
||||
}
|
||||
};
|
||||
|
||||
// 工作目录:拉数据 + 灌两个 select(顶部 filter-wd 和 modal nt-wd-sel)
|
||||
export async function loadFolderSuggestions() {
|
||||
try {
|
||||
const data = await api("GET", "/v1/folders");
|
||||
state.folders = data.folders || [];
|
||||
} catch (e) {
|
||||
state.folders = state.folders || [];
|
||||
}
|
||||
populateFolderSelects();
|
||||
}
|
||||
|
||||
// 灌 filter-wd + nt-wd-sel options;保留当前选中值
|
||||
function populateFolderSelects() {
|
||||
const folders = state.folders || [];
|
||||
// 顶部 filter:第一项 "(全部目录)" sentinel
|
||||
const filterSel = $("filter-wd");
|
||||
const filterCur = filterSel.value;
|
||||
const filterOpts = ['<option value="">(全部目录)</option>'];
|
||||
for (const f of folders) {
|
||||
const tag = f.n_tasks ? `${f.n_tasks} 个任务` : `空目录`;
|
||||
filterOpts.push(`<option value="${escapeHtml(f.name)}">${escapeHtml(f.name)} — ${escapeHtml(tag)}</option>`);
|
||||
}
|
||||
filterSel.innerHTML = filterOpts.join("");
|
||||
filterSel.value = filterCur; // 重渲后恢复选中
|
||||
// modal wd:第一项 "+ 新建(跟随任务名)" sentinel(label 由 updateSentinelLabel 实时刷)
|
||||
const wdSel = $("nt-wd-sel");
|
||||
const wdCur = wdSel.value || "__new__";
|
||||
const wdOpts = [`<option value="__new__">+ 新建(跟随任务名)</option>`];
|
||||
for (const f of folders) {
|
||||
const tag = f.n_tasks ? `${f.n_tasks} 个任务` : `空目录`;
|
||||
wdOpts.push(`<option value="${escapeHtml(f.name)}">${escapeHtml(f.name)} — ${escapeHtml(tag)}</option>`);
|
||||
}
|
||||
wdSel.innerHTML = wdOpts.join("");
|
||||
wdSel.value = wdCur;
|
||||
updateSentinelLabel(); // 用最新的 name 刷 sentinel
|
||||
}
|
||||
|
||||
// 智能体类型下拉:skill registry 服务器端静态,首次加载后缓存到 state.skills
|
||||
async function loadSkillOptions() {
|
||||
const sel = $("nt-skill");
|
||||
if (!state.skills) {
|
||||
try {
|
||||
const data = await api("GET", "/v1/skills");
|
||||
state.skills = data.skills || [];
|
||||
} catch (e) {
|
||||
state.skills = []; // 静默兜底,select 仍保留"(默认)"项
|
||||
}
|
||||
}
|
||||
// 渲染:第一项固定为"默认"(空 value),其后逐 skill 一项
|
||||
const opts = ['<option value="">(默认 · 不限定)</option>'];
|
||||
for (const s of state.skills) {
|
||||
const label = `${s.name}${s.description ? " — " + s.description : ""}`;
|
||||
opts.push(`<option value="${escapeHtml(s.name)}" title="${escapeHtml(s.description || "")}">${escapeHtml(label)}</option>`);
|
||||
}
|
||||
sel.innerHTML = opts.join("");
|
||||
sel.value = ""; // hd-new 已清空,这里幂等再保一次
|
||||
}
|
||||
|
||||
// === modal wd select + 二级 input 联动 ===
|
||||
// select 选 "__new__" sentinel → 显示二级 input(默认值跟随 name);选已有目录 → 隐藏二级 input
|
||||
// wdManuallyEdited:用户改过二级 input 后置 true,name 不再覆盖;清空二级 input 重置 false
|
||||
|
||||
function updateSentinelLabel() {
|
||||
// sentinel 永远是 select 第一项,labels 实时含 name 让用户一眼知会建什么
|
||||
const sel = $("nt-wd-sel");
|
||||
const opt = sel.options[0];
|
||||
if (!opt || opt.value !== "__new__") return;
|
||||
const name = $("nt-name").value.trim();
|
||||
opt.textContent = name ? `+ 新建「${name}」` : `+ 新建(跟随任务名)`;
|
||||
}
|
||||
|
||||
function updateWdHint() {
|
||||
const hint = $("nt-wd-hint");
|
||||
const sel = $("nt-wd-sel").value;
|
||||
if (sel === "__new__") {
|
||||
const v = $("nt-wd-new").value.trim();
|
||||
const name = $("nt-name").value.trim();
|
||||
const target = v || name;
|
||||
if (!target) { hint.textContent = ""; return; }
|
||||
// 用户手输的新名恰好命中已有目录 → 提示会复用而非新建
|
||||
const collision = (state.folders || []).find(f => f.name === target);
|
||||
if (collision) {
|
||||
const n = collision.n_tasks || 0;
|
||||
hint.innerHTML = `<span style="color:var(--accent);">! 已有同名目录,将复用</span> · ${n} 个任务`;
|
||||
} else {
|
||||
hint.innerHTML = `<span class="muted">→ 新建目录 ${escapeHtml(target)}</span>`;
|
||||
}
|
||||
} else {
|
||||
const f = (state.folders || []).find(x => x.name === sel);
|
||||
const n = f ? (f.n_tasks || 0) : 0;
|
||||
hint.innerHTML = `<span style="color:var(--accent);">→ 复用已有目录</span> · ${n} 个任务`;
|
||||
}
|
||||
}
|
||||
|
||||
// name 改变 → 更新 sentinel label;若未脱钩且当前是 sentinel,二级 input 跟随 name
|
||||
$("nt-name").addEventListener("input", () => {
|
||||
updateSentinelLabel();
|
||||
if (!wdManuallyEdited && $("nt-wd-sel").value === "__new__") {
|
||||
$("nt-wd-new").value = $("nt-name").value;
|
||||
}
|
||||
updateWdHint();
|
||||
});
|
||||
|
||||
// wd select 切换 → 切显示二级 input + 刷 hint
|
||||
$("nt-wd-sel").addEventListener("change", () => {
|
||||
const v = $("nt-wd-sel").value;
|
||||
if (v === "__new__") {
|
||||
$("nt-wd-new").style.display = "";
|
||||
if (!wdManuallyEdited) $("nt-wd-new").value = $("nt-name").value;
|
||||
} else {
|
||||
$("nt-wd-new").style.display = "none";
|
||||
}
|
||||
updateWdHint();
|
||||
});
|
||||
|
||||
// 二级 input 改变 → 非空视为手动修改;清空重置 flag 但保持空(避免 backspace 想换名时被打断)
|
||||
$("nt-wd-new").addEventListener("input", () => {
|
||||
wdManuallyEdited = $("nt-wd-new").value.trim() !== "";
|
||||
updateWdHint();
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue