diff --git a/AGENTS.md b/AGENTS.md index 627638aa..ac31ad3b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -63,6 +63,7 @@ app/api/ models-config/discover/route.ts POST fetch a configured provider's upstream model list models-config/test/route.ts POST test a configured model/provider plugins/route.ts GET/POST package plugin management + project-directories/route.ts GET/POST/DELETE persisted sidebar directories skills/route.ts GET/PATCH loaded skills and disable-model-invocation skills/install/route.ts POST install skills through npx skills add skills/search/route.ts GET/POST skills.sh search @@ -76,6 +77,7 @@ lib/ markdown.ts shared markdown helpers npx.ts npx runner used by skill install pi-types.ts local structural types for pi SDK objects + project-directories.ts persisted directory list in ~/.pi/agent/pi-web-projects.json rpc-manager.ts AgentSessionWrapper + registry + startRpcSession session-reader.ts SessionManager wrappers + path cache + buildSessionContext adapter tool-presets.ts PRESET_NONE/DEFAULT/FULL + getPresetFromTools() @@ -182,6 +184,13 @@ Newer pi emits `compaction_start` / `compaction_end`; older versions emitted `au - `hooks/useAudio.ts` stores the toggle in `localStorage` as `pi-sound-enabled` and reuses one `AudioContext`. - Browser autoplay policy means sound must be unlocked from a user gesture; `ChatInput` calls the unlock hook from interactive controls, and `ChatWindow` plays the tone from `onAgentEnd`. +### PWA 版本与 Service Worker 更新策略 +- 生产环境必须使用每次构建唯一的版本标识注册 `/sw.js?v=`,静态缓存名称也必须包含同一个版本;不能只使用长期不变的 `package.json` 版本,否则代码变化后浏览器可能继续命中旧 chunk。 +- 新 Service Worker 安装完成后保持 `waiting`,由界面提示“发现新版本”;用户确认后发送 `SKIP_WAITING`,并在 `controllerchange` 后刷新页面。不要在 `install` 阶段无条件调用 `skipWaiting()`。 +- 激活新 Service Worker 时只清理 `pi-web-` 前缀下的旧版本缓存,不得清理其它站点数据或认证信息。`/sw.js`、页面导航和 API 请求必须绕过静态资源缓存。 +- 开发环境不注册 Service Worker,并在 Next 客户端代码加载前注销同源旧注册、删除 `pi-web-` 缓存;清理完成后若页面仍被旧 worker 控制,只自动重载一次,避免刷新循环。 +- 重启 8081 的 Node/Next.js 进程不会注销浏览器中的 Service Worker。已有标签页或独立 PWA 窗口可能继续由旧 worker 控制,必须让页面重新加载并执行清理逻辑;必要时关闭该同源的全部页面后重新打开。 + ### Exported session HTML - `/api/sessions/[id]/export` delegates to pi's export helper, then patches recursive tree helpers in the generated HTML to iterative versions so very deep linear sessions do not overflow the browser call stack. diff --git a/README.md b/README.md index 0f666c08..a6e1f46f 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ Avoid running `next build` / `npm run build` during local development. It writes app/ api/ agent/ # creates/drives AgentSession and exposes SSE events - auth/ # OAuth and API key management + auth/ # password sessions, OAuth, and API key management cwd/browse/ # browsable server directory listing cwd/validate/ # custom working directory validation default-cwd/ # pi default working directory lookup diff --git a/README.zh-CN.md b/README.zh-CN.md index ae082007..fc466dde 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -109,7 +109,7 @@ npm run lint app/ api/ agent/ # 创建/驱动 AgentSession,提供 SSE 事件流 - auth/ # OAuth 和 API key 管理 + auth/ # 密码会话、OAuth 和 API key 管理 cwd/browse/ # 服务端目录浏览 cwd/validate/ # 自定义工作目录校验 default-cwd/ # 获取 pi 默认工作目录 diff --git a/app/api/project-directories/route.ts b/app/api/project-directories/route.ts new file mode 100644 index 00000000..cebbe465 --- /dev/null +++ b/app/api/project-directories/route.ts @@ -0,0 +1,41 @@ +import { NextResponse } from "next/server"; +import { allowFileRoot } from "@/lib/file-access"; +import { + addProjectDirectory, + normalizeProjectDirectory, + readProjectDirectories, + removeProjectDirectory, +} from "@/lib/project-directories"; + +export const dynamic = "force-dynamic"; + +export async function GET() { + const projects = readProjectDirectories(); + projects.forEach(allowFileRoot); + return NextResponse.json({ projects }); +} + +export async function POST(request: Request) { + try { + const body = await request.json() as { cwd?: unknown }; + const cwd = normalizeProjectDirectory(body.cwd); + allowFileRoot(cwd); + return NextResponse.json({ projects: addProjectDirectory(cwd), cwd }); + } catch (error) { + console.error("保存 Pi Web 项目目录失败", error); + return NextResponse.json({ error: error instanceof Error ? error.message : String(error) }, { status: 400 }); + } +} + +export async function DELETE(request: Request) { + try { + const body = await request.json() as { cwd?: unknown }; + if (typeof body.cwd !== "string" || !body.cwd.trim()) { + return NextResponse.json({ error: "cwd required" }, { status: 400 }); + } + return NextResponse.json({ projects: removeProjectDirectory(body.cwd.trim()) }); + } catch (error) { + console.error("移除 Pi Web 项目目录失败", error); + return NextResponse.json({ error: error instanceof Error ? error.message : String(error) }, { status: 500 }); + } +} diff --git a/app/layout.tsx b/app/layout.tsx index 83ce2f5f..6d5001a8 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata, Viewport } from "next"; import { Noto_Sans_Mono } from "next/font/google"; +import Script from "next/script"; import { PwaRegistration } from "@/components/PwaRegistration"; import "katex/dist/katex.min.css"; import "./globals.css"; @@ -10,6 +11,25 @@ const notoSansMono = Noto_Sans_Mono({ display: "swap", }); +const DEV_PWA_CLEANUP_SCRIPT = `(function(){ + if (!("serviceWorker" in navigator)) return; + Promise.all([ + navigator.serviceWorker.getRegistrations().then(function(registrations){ + return Promise.all(registrations.map(function(registration){ return registration.unregister(); })); + }), + ("caches" in window) ? caches.keys().then(function(keys){ + return Promise.all(keys.filter(function(key){ return key.indexOf("pi-web-") === 0; }).map(function(key){ return caches.delete(key); })); + }) : Promise.resolve() + ]).then(function(){ + if (navigator.serviceWorker.controller && !sessionStorage.getItem("pi-web-dev-sw-cleaned")) { + sessionStorage.setItem("pi-web-dev-sw-cleaned", "1"); + location.reload(); + } else { + sessionStorage.removeItem("pi-web-dev-sw-cleaned"); + } + }).catch(function(error){ console.error("PWA 开发缓存清理失败", error); }); +})();`; + export const metadata: Metadata = { title: "Pi Web", description: "Pi Web interface for the pi coding agent", @@ -62,6 +82,9 @@ export default function RootLayout({ __html: `(function(){try{var t=localStorage.getItem("pi-theme");if(t==="dark")document.documentElement.classList.add("dark")}catch(e){}})();`, }} /> + {process.env.NODE_ENV !== "production" && ( +