@rexvane/piweb 0.3.18 → 0.3.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +8 -6
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +4 -4
  5. package/.next/react-loadable-manifest.json +23 -9
  6. package/.next/required-server-files.js +2 -1
  7. package/.next/required-server-files.json +2 -1
  8. package/.next/routes-manifest.json +12 -0
  9. package/.next/server/app/_global-error/page.js +2 -2
  10. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  11. package/.next/server/app/_global-error.html +1 -1
  12. package/.next/server/app/_global-error.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/server/app/_not-found/page.js +1 -1
  17. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/server/app/_not-found.html +1 -1
  19. package/.next/server/app/_not-found.rsc +2 -2
  20. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  21. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  22. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/server/app/api/agent/[id]/events/route.js +3 -3
  24. package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
  25. package/.next/server/app/api/agent/[id]/route.js +1 -1
  26. package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
  27. package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
  28. package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
  29. package/.next/server/app/api/agent/new/route.js +1 -1
  30. package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
  31. package/.next/server/app/api/config/route.js +1 -1
  32. package/.next/server/app/api/config/route.js.nft.json +1 -1
  33. package/.next/server/app/api/files/route.js +1 -1
  34. package/.next/server/app/api/files/route.js.nft.json +1 -1
  35. package/.next/server/app/api/fs/route.js +1 -0
  36. package/.next/server/app/api/fs/route.js.nft.json +1 -0
  37. package/.next/server/app/api/fs/route_client-reference-manifest.js +1 -0
  38. package/.next/server/app/api/git/route.js +2 -2
  39. package/.next/server/app/api/git/route.js.nft.json +1 -1
  40. package/.next/server/app/api/growth/route.js +1 -1
  41. package/.next/server/app/api/growth/route.js.nft.json +1 -1
  42. package/.next/server/app/api/health/route.js +1 -1
  43. package/.next/server/app/api/models/route.js +1 -1
  44. package/.next/server/app/api/models/route.js.nft.json +1 -1
  45. package/.next/server/app/api/pi-settings/route.js +1 -1
  46. package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
  47. package/.next/server/app/api/plugins/route.js +1 -1
  48. package/.next/server/app/api/plugins/route.js.nft.json +1 -1
  49. package/.next/server/app/api/prompts/route.js +1 -1
  50. package/.next/server/app/api/prompts/route.js.nft.json +1 -1
  51. package/.next/server/app/api/remote/route.js +8 -0
  52. package/.next/server/app/api/remote/route.js.nft.json +1 -0
  53. package/.next/server/app/api/remote/route_client-reference-manifest.js +1 -0
  54. package/.next/server/app/api/security/route.js +1 -1
  55. package/.next/server/app/api/security/route.js.nft.json +1 -1
  56. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  57. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  58. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  59. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  60. package/.next/server/app/api/sessions/import/route.js +2 -2
  61. package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
  62. package/.next/server/app/api/sessions/route.js +1 -1
  63. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  64. package/.next/server/app/api/skills/route.js +1 -1
  65. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  66. package/.next/server/app/api/update/route.js +2 -2
  67. package/.next/server/app/api/update/route.js.nft.json +1 -1
  68. package/.next/server/app/api/version/route.js +1 -1
  69. package/.next/server/app/api/version/route.js.nft.json +1 -1
  70. package/.next/server/app/api/web-auth/route.js +3 -1
  71. package/.next/server/app/api/workspaces/route.js +1 -1
  72. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  73. package/.next/server/app/icon.svg/route.js +1 -1
  74. package/.next/server/app/index.html +1 -1
  75. package/.next/server/app/index.rsc +4 -4
  76. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
  77. package/.next/server/app/index.segments/_full.segment.rsc +4 -4
  78. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  79. package/.next/server/app/login/page.js +1 -1
  80. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  81. package/.next/server/app/login.html +1 -1
  82. package/.next/server/app/login.rsc +3 -3
  83. package/.next/server/app/login.segments/_full.segment.rsc +3 -3
  84. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  85. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +3 -3
  86. package/.next/server/app/page.js +1 -1
  87. package/.next/server/app/page.js.nft.json +1 -1
  88. package/.next/server/app/page_client-reference-manifest.js +1 -1
  89. package/.next/server/app-paths-manifest.json +8 -6
  90. package/.next/server/chunks/229.js +1 -1
  91. package/.next/server/chunks/243.js +1 -0
  92. package/.next/server/chunks/393.js +1 -0
  93. package/.next/server/chunks/496.js +1 -1
  94. package/.next/server/chunks/538.js +1 -0
  95. package/.next/server/chunks/686.js +1 -4
  96. package/.next/server/chunks/802.js +2 -0
  97. package/.next/server/chunks/807.js +1 -0
  98. package/.next/server/chunks/920.js +4 -0
  99. package/.next/server/chunks/924.js +6 -0
  100. package/.next/server/instrumentation.js +1 -0
  101. package/.next/server/instrumentation.js.nft.json +1 -0
  102. package/.next/server/middleware-build-manifest.js +1 -1
  103. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  104. package/.next/server/middleware.js +4 -3
  105. package/.next/server/pages/404.html +1 -1
  106. package/.next/server/pages/500.html +1 -1
  107. package/.next/server/server-reference-manifest.json +1 -1
  108. package/.next/static/_Y3Lq0lI5RJ1tFSOnDi0L/_buildManifest.js +1 -0
  109. package/.next/static/chunks/2716.7ad073038e94a802.js +1 -0
  110. package/.next/static/chunks/316-5ea443ee6b512aa9.js +1 -0
  111. package/.next/static/chunks/4673.8a5a9e6855350e34.js +1 -0
  112. package/.next/static/chunks/4686-b1e9fa876d0ea671.js +1 -0
  113. package/.next/static/chunks/4778.e4c9ed093a413bc4.js +1 -0
  114. package/.next/static/chunks/5173.91ddb803785009f8.js +1 -0
  115. package/.next/static/chunks/839.d3fe337a9117c325.js +1 -0
  116. package/.next/static/chunks/8552.6bfe3e101250b20f.js +1 -0
  117. package/.next/static/chunks/app/_global-error/page-b179d9f7f6f2daf9.js +1 -0
  118. package/.next/static/chunks/app/api/agent/[id]/events/route-b179d9f7f6f2daf9.js +1 -0
  119. package/.next/static/chunks/app/api/agent/[id]/route-b179d9f7f6f2daf9.js +1 -0
  120. package/.next/static/chunks/app/api/agent/[id]/tree/route-b179d9f7f6f2daf9.js +1 -0
  121. package/.next/static/chunks/app/api/agent/new/route-b179d9f7f6f2daf9.js +1 -0
  122. package/.next/static/chunks/app/api/config/route-b179d9f7f6f2daf9.js +1 -0
  123. package/.next/static/chunks/app/api/files/route-b179d9f7f6f2daf9.js +1 -0
  124. package/.next/static/chunks/app/api/fs/route-b179d9f7f6f2daf9.js +1 -0
  125. package/.next/static/chunks/app/api/git/route-b179d9f7f6f2daf9.js +1 -0
  126. package/.next/static/chunks/app/api/growth/route-b179d9f7f6f2daf9.js +1 -0
  127. package/.next/static/chunks/app/api/health/route-b179d9f7f6f2daf9.js +1 -0
  128. package/.next/static/chunks/app/api/models/route-b179d9f7f6f2daf9.js +1 -0
  129. package/.next/static/chunks/app/api/pi-settings/route-b179d9f7f6f2daf9.js +1 -0
  130. package/.next/static/chunks/app/api/plugins/route-b179d9f7f6f2daf9.js +1 -0
  131. package/.next/static/chunks/app/api/prompts/route-b179d9f7f6f2daf9.js +1 -0
  132. package/.next/static/chunks/app/api/remote/route-b179d9f7f6f2daf9.js +1 -0
  133. package/.next/static/chunks/app/api/security/route-b179d9f7f6f2daf9.js +1 -0
  134. package/.next/static/chunks/app/api/sessions/[id]/export/route-b179d9f7f6f2daf9.js +1 -0
  135. package/.next/static/chunks/app/api/sessions/[id]/route-b179d9f7f6f2daf9.js +1 -0
  136. package/.next/static/chunks/app/api/sessions/import/route-b179d9f7f6f2daf9.js +1 -0
  137. package/.next/static/chunks/app/api/sessions/route-b179d9f7f6f2daf9.js +1 -0
  138. package/.next/static/chunks/app/api/skills/route-b179d9f7f6f2daf9.js +1 -0
  139. package/.next/static/chunks/app/api/update/route-b179d9f7f6f2daf9.js +1 -0
  140. package/.next/static/chunks/app/api/version/route-b179d9f7f6f2daf9.js +1 -0
  141. package/.next/static/chunks/app/api/web-auth/route-b179d9f7f6f2daf9.js +1 -0
  142. package/.next/static/chunks/app/api/workspaces/route-b179d9f7f6f2daf9.js +1 -0
  143. package/.next/static/chunks/app/login/page-5e276a1eeb8867df.js +1 -0
  144. package/.next/static/chunks/app/page-2e425ef83fba0782.js +1 -0
  145. package/.next/static/chunks/next/dist/client/components/builtin/app-error-b179d9f7f6f2daf9.js +1 -0
  146. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-b179d9f7f6f2daf9.js +1 -0
  147. package/.next/static/chunks/next/dist/client/components/builtin/not-found-b179d9f7f6f2daf9.js +1 -0
  148. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-b179d9f7f6f2daf9.js +1 -0
  149. package/.next/static/chunks/webpack-3614a8337843456b.js +1 -0
  150. package/.next/static/css/7391c42e1ae617f9.css +1 -0
  151. package/.next/types/app/api/fs/route.ts +353 -0
  152. package/.next/types/app/api/remote/route.ts +353 -0
  153. package/.next/types/routes.d.ts +3 -1
  154. package/.next/types/validator.ts +18 -0
  155. package/README.md +56 -10
  156. package/next.config.ts +3 -2
  157. package/package.json +3 -3
  158. package/scripts/launcher.mjs +194 -13
  159. package/scripts/release.mjs +0 -2
  160. package/src/app/api/agent/[id]/events/route.ts +11 -6
  161. package/src/app/api/fs/route.ts +119 -0
  162. package/src/app/api/growth/route.ts +11 -6
  163. package/src/app/api/models/route.ts +21 -9
  164. package/src/app/api/remote/route.ts +77 -0
  165. package/src/app/api/sessions/[id]/export/route.ts +11 -1
  166. package/src/app/api/sessions/route.ts +3 -1
  167. package/src/app/api/web-auth/route.ts +45 -49
  168. package/src/app/api/workspaces/route.ts +16 -0
  169. package/src/app/globals.css +6 -0
  170. package/src/app/login/page.tsx +8 -60
  171. package/src/components/AppShell.tsx +202 -155
  172. package/src/components/ChatInput.tsx +61 -6
  173. package/src/components/ChatWindow.tsx +274 -132
  174. package/src/components/DirectoryPicker.tsx +189 -0
  175. package/src/components/DrawerPanel.tsx +164 -0
  176. package/src/components/FileViewer.tsx +2 -2
  177. package/src/components/GrowthTree.tsx +6 -5
  178. package/src/components/MarkdownBlock.tsx +134 -0
  179. package/src/components/OutlineRail.tsx +22 -1
  180. package/src/components/ProjectPanel.tsx +197 -14
  181. package/src/components/RemoteSection.tsx +265 -0
  182. package/src/components/SessionImportSection.tsx +60 -38
  183. package/src/components/SettingsPanel.tsx +104 -57
  184. package/src/components/icons.tsx +23 -0
  185. package/src/hooks/useGrowth.ts +47 -25
  186. package/src/hooks/usePiWeb.ts +100 -14
  187. package/src/i18n.tsx +117 -28
  188. package/src/instrumentation-node.ts +35 -0
  189. package/src/instrumentation.ts +18 -0
  190. package/src/lib/agent-config.ts +134 -0
  191. package/src/lib/agent-manager.ts +243 -61
  192. package/src/lib/auth-throttle.ts +102 -0
  193. package/src/lib/auth.ts +35 -27
  194. package/src/lib/files-service.ts +61 -1
  195. package/src/lib/font.ts +84 -0
  196. package/src/lib/git-service.ts +5 -3
  197. package/src/lib/growth-service.ts +78 -30
  198. package/src/lib/growth-tracker.ts +91 -9
  199. package/src/lib/models-service.ts +36 -11
  200. package/src/lib/pi-settings.ts +10 -12
  201. package/src/lib/pi.ts +145 -18
  202. package/src/lib/plugins-service.ts +67 -35
  203. package/src/lib/remote/assets.json +4 -0
  204. package/src/lib/remote/mobile-proxy.ts +155 -0
  205. package/src/lib/remote/supervisor.ts +492 -0
  206. package/src/lib/security-service.ts +6 -11
  207. package/src/lib/session-import/claude.ts +45 -21
  208. package/src/lib/session-reader.ts +106 -3
  209. package/src/lib/skills-service.ts +34 -12
  210. package/src/lib/text-sanitize.ts +12 -0
  211. package/src/lib/theme.ts +3 -0
  212. package/src/lib/types.ts +16 -2
  213. package/src/lib/update-service.ts +0 -1
  214. package/src/lib/web-auth-shared.ts +5 -1
  215. package/src/lib/web-auth.ts +139 -22
  216. package/src/lib/web-launch-token.ts +55 -0
  217. package/src/lib/web-secret.ts +182 -0
  218. package/src/lib/workspace-store.ts +91 -7
  219. package/src/proxy.ts +46 -12
  220. package/.next/server/chunks/532.js +0 -1
  221. package/.next/static/chunks/2155.2f27fe7098987699.js +0 -1
  222. package/.next/static/chunks/2608.ec8ff1f5767c9056.js +0 -1
  223. package/.next/static/chunks/2716.93be8ad5c970c516.js +0 -1
  224. package/.next/static/chunks/4632.112d2e893d48d188.js +0 -1
  225. package/.next/static/chunks/4673.c1fb1580efc6ace7.js +0 -1
  226. package/.next/static/chunks/4686-f4a037d609cea3f2.js +0 -1
  227. package/.next/static/chunks/692-7992db0ae9cc3021.js +0 -1
  228. package/.next/static/chunks/app/_global-error/page-f77e9ede9808b18e.js +0 -1
  229. package/.next/static/chunks/app/api/agent/[id]/events/route-f77e9ede9808b18e.js +0 -1
  230. package/.next/static/chunks/app/api/agent/[id]/route-f77e9ede9808b18e.js +0 -1
  231. package/.next/static/chunks/app/api/agent/[id]/tree/route-f77e9ede9808b18e.js +0 -1
  232. package/.next/static/chunks/app/api/agent/new/route-f77e9ede9808b18e.js +0 -1
  233. package/.next/static/chunks/app/api/config/route-f77e9ede9808b18e.js +0 -1
  234. package/.next/static/chunks/app/api/files/route-f77e9ede9808b18e.js +0 -1
  235. package/.next/static/chunks/app/api/git/route-f77e9ede9808b18e.js +0 -1
  236. package/.next/static/chunks/app/api/growth/route-f77e9ede9808b18e.js +0 -1
  237. package/.next/static/chunks/app/api/health/route-f77e9ede9808b18e.js +0 -1
  238. package/.next/static/chunks/app/api/models/route-f77e9ede9808b18e.js +0 -1
  239. package/.next/static/chunks/app/api/pi-settings/route-f77e9ede9808b18e.js +0 -1
  240. package/.next/static/chunks/app/api/plugins/route-f77e9ede9808b18e.js +0 -1
  241. package/.next/static/chunks/app/api/prompts/route-f77e9ede9808b18e.js +0 -1
  242. package/.next/static/chunks/app/api/security/route-f77e9ede9808b18e.js +0 -1
  243. package/.next/static/chunks/app/api/sessions/[id]/export/route-f77e9ede9808b18e.js +0 -1
  244. package/.next/static/chunks/app/api/sessions/[id]/route-f77e9ede9808b18e.js +0 -1
  245. package/.next/static/chunks/app/api/sessions/import/route-f77e9ede9808b18e.js +0 -1
  246. package/.next/static/chunks/app/api/sessions/route-f77e9ede9808b18e.js +0 -1
  247. package/.next/static/chunks/app/api/skills/route-f77e9ede9808b18e.js +0 -1
  248. package/.next/static/chunks/app/api/update/route-f77e9ede9808b18e.js +0 -1
  249. package/.next/static/chunks/app/api/version/route-f77e9ede9808b18e.js +0 -1
  250. package/.next/static/chunks/app/api/web-auth/route-f77e9ede9808b18e.js +0 -1
  251. package/.next/static/chunks/app/api/workspaces/route-f77e9ede9808b18e.js +0 -1
  252. package/.next/static/chunks/app/login/page-52ef4841cbd199a8.js +0 -1
  253. package/.next/static/chunks/app/page-b51087831582ee37.js +0 -1
  254. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f77e9ede9808b18e.js +0 -1
  255. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f77e9ede9808b18e.js +0 -1
  256. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f77e9ede9808b18e.js +0 -1
  257. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f77e9ede9808b18e.js +0 -1
  258. package/.next/static/chunks/webpack-c31263fd83d75ce6.js +0 -1
  259. package/.next/static/css/0cd968b9b62035b5.css +0 -1
  260. package/.next/static/en03hnPiuro0rnf8QkFEe/_buildManifest.js +0 -1
  261. /package/.next/static/{en03hnPiuro0rnf8QkFEe → _Y3Lq0lI5RJ1tFSOnDi0L}/_ssgManifest.js +0 -0
@@ -4,6 +4,16 @@ import { BoundaryError, resolveSessionPath } from "@/lib/path-security";
4
4
 
5
5
  export const dynamic = "force-dynamic";
6
6
 
7
+ /**
8
+ * 下载文件名:filename= 里放 ASCII 兜底,filename*= 按 RFC 6266/5987 放 UTF-8 原名。
9
+ * 只写百分号编码的 filename= 时浏览器不解码,中文会话名下载下来就是一串 %E4%BD%A0…
10
+ */
11
+ function attachmentDisposition(filename: string): string {
12
+ const fallback = filename.replace(/[^\x20-\x7e]/g, "_").replace(/["\\]/g, "_") || "session";
13
+ const encoded = encodeURIComponent(filename).replace(/['()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
14
+ return `attachment; filename="${fallback}"; filename*=UTF-8''${encoded}`;
15
+ }
16
+
7
17
  export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
8
18
  const { id } = await params;
9
19
  const format = new URL(req.url).searchParams.get("format") === "html" ? "html" : "jsonl";
@@ -13,7 +23,7 @@ export async function GET(req: Request, { params }: { params: Promise<{ id: stri
13
23
  return new Response(content, {
14
24
  headers: {
15
25
  "Content-Type": `${contentType}; charset=utf-8`,
16
- "Content-Disposition": `attachment; filename="${encodeURIComponent(filename)}"`,
26
+ "Content-Disposition": attachmentDisposition(filename),
17
27
  },
18
28
  });
19
29
  } catch (error) {
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { NextResponse } from "next/server";
3
3
  import { listSessions } from "@/lib/session-reader";
4
- import { activeStatus } from "@/lib/agent-manager";
4
+ import { activeStatus, warmSessionResources } from "@/lib/agent-manager";
5
5
  import { getWorkspaceRegistry, registerCwds } from "@/lib/workspace-store";
6
6
 
7
7
  export const dynamic = "force-dynamic";
@@ -10,6 +10,8 @@ export async function GET(req: Request) {
10
10
  try {
11
11
  const q = new URL(req.url).searchParams.get("q") ?? undefined;
12
12
  const sessions = await listSessions(q);
13
+ // 侧栏一拿到列表就预热最近那个工作区的资源发现(约 140ms),别等用户点开会话才做
14
+ warmSessionResources(sessions.slice(0, 1).map((session) => session.cwd));
13
15
  await registerCwds(sessions.map((session) => session.cwd));
14
16
  const workspaceRegistry = await getWorkspaceRegistry();
15
17
  // 侧栏每 3 秒轮询:内容没变就 304,浏览器复用缓存体(no-cache = 每次都回源校验,不会用陈旧数据)
@@ -1,16 +1,21 @@
1
1
  /**
2
- * 网页认证:GET 状态 / POST 登录(写会话 Cookie)/ DELETE 登出(清 Cookie)。
3
- * 无密码配置时整体关闭(GET 返回 enabled:false,POST 404)。
2
+ * 网页认证:GET 状态 / POST 已废弃(密码登录移除)/ DELETE 登出。
3
+ * DELETE 在服务端撤回本设备这张 Cookie 再清掉它(被拷走的副本同样失效);
4
+ * DELETE ?scope=all 让此刻之前签发的所有 Cookie 失效(所有设备都要重新用启动令牌登录)。
5
+ * 没启用认证时整体关闭(GET 返回 enabled:false)。
6
+ *
7
+ * 唯一的登录路径是启动令牌换 Cookie(见 src/proxy.ts):根路径的 GET 带上 ?token=…
8
+ * 换一张签名 Cookie,之后带着 Cookie 请求即可。这里不再收任何凭据。
4
9
  */
5
10
  import { NextResponse } from "next/server";
6
- import { isSafeHost, isSafeOrigin, validBasicAuthorization } from "@/lib/auth";
7
- import { SESSION_COOKIE, clearedCookieOptions, createSessionToken, readCookieHeader, safeNextPath, sessionCookieOptions, validPassword, validSessionToken } from "@/lib/web-auth";
11
+ import { configuredTrustedHosts, isSafeHost, isSafeOrigin } from "@/lib/auth";
12
+ import { authEnabled, clearSessionCookie, clearedCookieOptions, sessionCookieFromRequest, validSessionCookie } from "@/lib/web-auth";
13
+ import { loadSessionRevocations, loadWebSecret, revokeAllSessions, revokeSessionCookie } from "@/lib/web-secret";
8
14
 
9
15
  export const dynamic = "force-dynamic";
10
16
 
11
17
  function untrusted(request: Request): NextResponse | null {
12
- const password = process.env.PI_WEB_PASSWORD;
13
- if (!isSafeHost(request, Boolean(password))) {
18
+ if (!isSafeHost(request, configuredTrustedHosts())) {
14
19
  return NextResponse.json({ success: false, error: "untrusted host" }, { status: 403 });
15
20
  }
16
21
  if (request.method !== "GET" && request.method !== "HEAD" && !isSafeOrigin(request)) {
@@ -22,59 +27,50 @@ function untrusted(request: Request): NextResponse | null {
22
27
  export async function GET(request: Request) {
23
28
  const blocked = untrusted(request);
24
29
  if (blocked) return blocked;
25
- const password = process.env.PI_WEB_PASSWORD;
26
- const enabled = Boolean(password);
27
- const authenticated = !enabled
28
- || validBasicAuthorization(request.headers.get("authorization"), password!)
29
- || validSessionToken(readCookieHeader(request.headers.get("cookie"), SESSION_COOKIE), password!);
30
- return NextResponse.json({ success: true, data: { enabled, authenticated } }, { headers: { "Cache-Control": "no-store" } });
30
+ const enabled = authEnabled();
31
+ const authenticated = !enabled || validSessionCookie(request, loadWebSecret(), Date.now(), loadSessionRevocations());
32
+ return NextResponse.json(
33
+ { success: true, data: { enabled, authenticated } },
34
+ { headers: { "Cache-Control": "no-store" } },
35
+ );
31
36
  }
32
37
 
38
+ /** 密码登录已移除:这里曾经用密码换会话 Cookie,现在只回答「此路已废」。 */
33
39
  export async function POST(request: Request) {
34
40
  const blocked = untrusted(request);
35
41
  if (blocked) return blocked;
36
- const password = process.env.PI_WEB_PASSWORD;
37
- if (!password) return NextResponse.json({ success: false, error: "password authentication is disabled" }, { status: 404 });
38
- if (!(request.headers.get("content-type") ?? "").includes("application/json")) {
39
- return NextResponse.json({ success: false, error: "Content-Type must be application/json" }, { status: 415 });
40
- }
41
- let body: { password?: unknown; next?: unknown };
42
- try {
43
- const reader = request.body?.getReader();
44
- if (!reader) return NextResponse.json({ success: false, error: "missing login payload" }, { status: 400 });
45
- const chunks: Uint8Array[] = [];
46
- let size = 0;
47
- try {
48
- for (;;) {
49
- const { value, done } = await reader.read();
50
- if (done) break;
51
- size += value.byteLength;
52
- if (size > 16 * 1024) return NextResponse.json({ success: false, error: "login payload too large" }, { status: 413 });
53
- chunks.push(value);
54
- }
55
- } finally {
56
- await reader.cancel().catch(() => undefined);
57
- reader.releaseLock();
58
- }
59
- body = JSON.parse(Buffer.concat(chunks).toString("utf8")) as { password?: unknown; next?: unknown };
60
- } catch {
61
- return NextResponse.json({ success: false, error: "invalid JSON body" }, { status: 400 });
62
- }
63
- if (!body || typeof body !== "object" || Array.isArray(body)) {
64
- return NextResponse.json({ success: false, error: "invalid login payload" }, { status: 400 });
65
- }
66
- if (typeof body.password !== "string" || !validPassword(body.password, password)) {
67
- return NextResponse.json({ success: false, error: "invalid password" }, { status: 401 });
68
- }
69
- const response = NextResponse.json({ success: true, data: { next: safeNextPath(typeof body.next === "string" ? body.next : null) } }, { headers: { "Cache-Control": "no-store" } });
70
- response.cookies.set(SESSION_COOKIE, createSessionToken(password), sessionCookieOptions(request));
71
- return response;
42
+ return NextResponse.json(
43
+ { success: false, error: "password login has been removed; use the launch token link printed at startup" },
44
+ { status: 410, headers: { "Cache-Control": "no-store" } },
45
+ );
72
46
  }
73
47
 
74
48
  export async function DELETE(request: Request) {
75
49
  const blocked = untrusted(request);
76
50
  if (blocked) return blocked;
51
+ if (authEnabled()) {
52
+ const secret = loadWebSecret();
53
+ const all = new URL(request.url).searchParams.get("scope") === "all";
54
+ // 「所有设备」只能由一个仍然有效的会话发起(DELETE 走不到 proxy 的拦截:/api/web-auth 是公开路径)
55
+ if (all && !validSessionCookie(request, secret, Date.now(), loadSessionRevocations())) {
56
+ return NextResponse.json({ success: false, error: "authentication required" }, { status: 401, headers: { "Cache-Control": "no-store" } });
57
+ }
58
+ try {
59
+ if (all) revokeAllSessions();
60
+ else {
61
+ const cookie = sessionCookieFromRequest(request, secret);
62
+ if (cookie && cookie.payload.expiresAt > Date.now()) revokeSessionCookie(cookie.id, cookie.payload.expiresAt);
63
+ }
64
+ } catch (error) {
65
+ // 吊销没记下来就别报「已退出」:Cookie 的副本还能用
66
+ return NextResponse.json(
67
+ { success: false, error: `could not revoke the session: ${error instanceof Error ? error.message : String(error)}` },
68
+ { status: 500, headers: { "Cache-Control": "no-store" } },
69
+ );
70
+ }
71
+ }
77
72
  const response = NextResponse.json({ success: true }, { headers: { "Cache-Control": "no-store" } });
78
- response.cookies.set(SESSION_COOKIE, "", clearedCookieOptions(request));
73
+ const cleared = clearSessionCookie(request);
74
+ response.cookies.set(cleared.name, cleared.value, clearedCookieOptions(request));
79
75
  return response;
80
76
  }
@@ -13,9 +13,21 @@ import {
13
13
  import { encodeSessionId } from "@/lib/pi";
14
14
  import { disposeSessionPath } from "@/lib/agent-manager";
15
15
  import { BoundaryError, resolveSessionPath, resolveWorkspacePath } from "@/lib/path-security";
16
+ import { isLoopbackHostname } from "@/lib/auth";
16
17
 
17
18
  export const dynamic = "force-dynamic";
18
19
 
20
+ /**
21
+ * 请求是否来自本机回环。
22
+ * 原生文件夹对话框只会在运行 PiWeb 的那台机器上弹出,所以对手机/平板这类远程设备
23
+ * 它等于没有反应——那种情况要让客户端改用应用内目录浏览器(/api/fs)。
24
+ */
25
+ function isLoopbackRequest(req: Request): boolean {
26
+ const host = req.headers.get("host") ?? new URL(req.url).host;
27
+ const hostname = host.startsWith("[") ? host.slice(0, host.indexOf("]") + 1) : host.split(":")[0];
28
+ return isLoopbackHostname(hostname);
29
+ }
30
+
19
31
  export async function GET() {
20
32
  try {
21
33
  return NextResponse.json({ success: true, data: await getWorkspaceRegistry() });
@@ -33,6 +45,10 @@ export async function POST(req: Request) {
33
45
  cwds?: string[];
34
46
  };
35
47
  if (body.action === "pick") {
48
+ if (!isLoopbackRequest(req)) {
49
+ // 远程设备:不弹主机上的对话框,让前端改用应用内目录浏览器。
50
+ return NextResponse.json({ success: true, data: { path: null, canceled: false, remote: true } });
51
+ }
36
52
  const r = await pickFolderNative();
37
53
  const registry = r.path ? await addWorkspace(await resolveWorkspacePath(r.path)) : null;
38
54
  return NextResponse.json({ success: true, data: { ...r, ...(registry ?? {}) } });
@@ -2121,6 +2121,12 @@ body[data-pebrel-theme="pebrel"][data-ds-dark-theme] {
2121
2121
  box-shadow: 0 0 0 1px var(--dsw-label-tertiary);
2122
2122
  }
2123
2123
 
2124
+ /* 回合懒渲染的纯文本占位:尽量贴近正式渲染的排版,升级前的过渡形态 */
2125
+ .pw-turn-lite > div {
2126
+ white-space: pre-wrap;
2127
+ overflow-wrap: anywhere;
2128
+ }
2129
+
2124
2130
  /* 窄屏:查看器全屏 */
2125
2131
  @media (max-width: 840px) {
2126
2132
  .pw-viewer {
@@ -1,10 +1,11 @@
1
1
  "use client";
2
2
 
3
3
  /**
4
- * 登录页:无密码配置时直接回主页;已认证(Cookie / Basic)自动放行。
4
+ * 登录页:认证未启用或已认证自动回主页。这里只有启动令牌提示 ——
5
+ * 密码登录已移除,本页不再收集任何凭据。
5
6
  * ?next= 只接受同源相对路径(safeNextPath 白名单)。
6
7
  */
7
- import { Suspense, useEffect, useRef, useState } from "react";
8
+ import { Suspense, useEffect } from "react";
8
9
  import { useRouter, useSearchParams } from "next/navigation";
9
10
  import { I18nProvider, useI18n } from "@/i18n";
10
11
  import { PiMark } from "@/components/PiMark";
@@ -14,21 +15,17 @@ export default function LoginPage() {
14
15
  return (
15
16
  <I18nProvider>
16
17
  <Suspense fallback={<div className="flex min-h-screen items-center justify-center" aria-busy="true"><PiMark size={40} /></div>}>
17
- <LoginForm />
18
+ <LoginNotice />
18
19
  </Suspense>
19
20
  </I18nProvider>
20
21
  );
21
22
  }
22
23
 
23
- function LoginForm() {
24
+ function LoginNotice() {
24
25
  const { t } = useI18n();
25
26
  const router = useRouter();
26
27
  const params = useSearchParams();
27
28
  const next = safeNextPath(params.get("next"));
28
- const [password, setPassword] = useState("");
29
- const [busy, setBusy] = useState(false);
30
- const [error, setError] = useState("");
31
- const inputRef = useRef<HTMLInputElement>(null);
32
29
 
33
30
  // 认证未启用 / 已登录:不用停留
34
31
  useEffect(() => {
@@ -40,7 +37,7 @@ function LoginForm() {
40
37
  if (!alive || !result?.success) return;
41
38
  if (!result.data.enabled || result.data.authenticated) router.replace(next);
42
39
  } catch {
43
- /* 网络异常时留在页面,提交时会再次尝试 */
40
+ /* 网络异常时留在页面:提示里已经写清怎么拿登录链接 */
44
41
  }
45
42
  })();
46
43
  return () => {
@@ -48,63 +45,14 @@ function LoginForm() {
48
45
  };
49
46
  }, [router, next]);
50
47
 
51
- const submit = async () => {
52
- if (busy || !password) return;
53
- setBusy(true);
54
- setError("");
55
- try {
56
- const response = await fetch("/api/web-auth", {
57
- method: "POST",
58
- headers: { "Content-Type": "application/json" },
59
- body: JSON.stringify({ password, next }),
60
- });
61
- const result = await response.json();
62
- if (result.success) {
63
- router.replace(safeNextPath(result.data?.next ?? next));
64
- return;
65
- }
66
- setError(response.status === 401 ? t.loginFailed : result.error || t.loginFailed);
67
- setPassword("");
68
- requestAnimationFrame(() => inputRef.current?.focus());
69
- } catch {
70
- setError(t.loginFailed);
71
- } finally {
72
- setBusy(false);
73
- }
74
- };
75
-
76
48
  return (
77
49
  <div className="flex min-h-screen items-center justify-center px-4" style={{ background: "var(--dsw-bg-base)" }}>
78
50
  <div className="glass-modal flex w-[min(92vw,380px)] flex-col gap-4 p-7" style={{ borderRadius: 24, boxShadow: "var(--dsw-elevation-prominent)" }}>
79
51
  <div className="flex flex-col items-center gap-2">
80
52
  <PiMark size={40} />
81
- <div style={{ fontSize: 16, fontWeight: 600 }}>{t.loginTitle}</div>
82
- <div className="text-center" style={{ fontSize: 12.5, color: "var(--dsw-label-caption)", lineHeight: 1.55 }}>{t.loginDesc}</div>
53
+ <div style={{ fontSize: 16, fontWeight: 600 }}>{t.loginTokenTitle}</div>
54
+ <div className="text-center" style={{ fontSize: 12.5, color: "var(--dsw-label-caption)", lineHeight: 1.55 }}>{t.loginTokenDesc}</div>
83
55
  </div>
84
- <input
85
- ref={inputRef}
86
- autoFocus
87
- type="password"
88
- autoComplete="current-password"
89
- aria-label={t.loginPassword}
90
- value={password}
91
- placeholder={t.loginPassword}
92
- onChange={(event) => setPassword(event.target.value)}
93
- onKeyDown={(event) => {
94
- if (event.key === "Enter" && !event.nativeEvent.isComposing) void submit();
95
- }}
96
- className="w-full rounded-xl px-3 py-2.5"
97
- style={{ fontSize: 14, background: "var(--dsw-hover)", border: "0.5px solid var(--dsw-border-l2)" }}
98
- />
99
- {error ? <div role="alert" style={{ fontSize: 12.5, color: "var(--dsw-danger)" }}>{error}</div> : null}
100
- <button
101
- className="btn-primary-white w-full"
102
- style={{ height: 38, opacity: busy || !password ? 0.55 : 1 }}
103
- disabled={busy || !password}
104
- onClick={() => void submit()}
105
- >
106
- {busy ? t.loginSubmitting : t.loginSubmit}
107
- </button>
108
56
  </div>
109
57
  </div>
110
58
  );