@shawnstack/quickforge 1.7.3 → 1.7.4

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 (210) hide show
  1. package/README.md +1 -1
  2. package/bin/quickforge.mjs +0 -21
  3. package/dist/assets/AgentProfilesPage-B84w-r9S.js +1 -0
  4. package/dist/assets/{ChatPanelHost-wfGCaCzD.js → ChatPanelHost-CyO8fEoj.js} +91 -91
  5. package/dist/assets/{PluginsPage-NAGroBm3.js → PluginsPage-D2862wul.js} +1 -1
  6. package/dist/assets/{ScheduledTasksPage-Bp0MNfQD.js → ScheduledTasksPage-GqbiJDPX.js} +2 -2
  7. package/dist/assets/SettingsWorkspacePage-B53_eDf2.js +1628 -0
  8. package/dist/assets/SharedConversationPage-Cw2QFnbC.js +1 -0
  9. package/dist/assets/TerminalDock-Dgg-_ZmU.js +2 -0
  10. package/dist/assets/WorkspaceInspector-3uLiu6Bv.js +13 -0
  11. package/dist/assets/{abnfDiagram-VRR7QNED-Dl_Fvlqg.js → abnfDiagram-VRR7QNED-Dy7veffk.js} +1 -1
  12. package/dist/assets/{arc-Bhg4c0ht.js → arc-Bo3p2lGy.js} +1 -1
  13. package/dist/assets/architecture-TIHT7OUA-7FQo5GbP.js +1 -0
  14. package/dist/assets/{architectureDiagram-ZJ3FMSHR-BuojN27B.js → architectureDiagram-ZJ3FMSHR-Cwx8qOvu.js} +1 -1
  15. package/dist/assets/{blockDiagram-677ZJIJ3-alDLbrR1.js → blockDiagram-677ZJIJ3-BiuaAOFa.js} +1 -1
  16. package/dist/assets/{c4Diagram-LMCZKHZV-D1lKa4VT.js → c4Diagram-LMCZKHZV-BTcGfhK2.js} +1 -1
  17. package/dist/assets/channel-DToA8d-4.js +1 -0
  18. package/dist/assets/chunk-2Q5K7J3B-D_Wxpk-T.js +1 -0
  19. package/dist/assets/{chunk-32BRIVSS-CwWLNjMZ.js → chunk-32BRIVSS-B9PuF_RF.js} +1 -1
  20. package/dist/assets/{chunk-52WLFC77-Be569m7d.js → chunk-52WLFC77-3zGItfIM.js} +1 -1
  21. package/dist/assets/{chunk-5FCAYU7R-BSvoiWV1.js → chunk-5FCAYU7R-DQ66qlmt.js} +1 -1
  22. package/dist/assets/{chunk-5HE753X5-CRgsy8WS.js → chunk-5HE753X5-CLqTO-VF.js} +1 -1
  23. package/dist/assets/{chunk-5JV3BV7I-CdW477UR.js → chunk-5JV3BV7I-Csxv1jr6.js} +1 -1
  24. package/dist/assets/{chunk-5TONJI2A-CdtKzSmp.js → chunk-5TONJI2A-BCQttpBr.js} +1 -1
  25. package/dist/assets/{chunk-5VM5RSS4-BKjjssLZ.js → chunk-5VM5RSS4-DEWJGSTb.js} +1 -1
  26. package/dist/assets/{chunk-7BUUIJ7U-ByAXiXdn.js → chunk-7BUUIJ7U-DNorm9MM.js} +1 -1
  27. package/dist/assets/{chunk-BIQX33UG-DeG8SCT1.js → chunk-BIQX33UG-DgvY_BOT.js} +1 -1
  28. package/dist/assets/{chunk-C7G6YPKG-D_zHT68Q.js → chunk-C7G6YPKG-DMvn9IPs.js} +1 -1
  29. package/dist/assets/{chunk-CQNSW5MT-CHpHoUSu.js → chunk-CQNSW5MT-Dq2duUgr.js} +1 -1
  30. package/dist/assets/{chunk-CYSBUYHQ-CRP50Vov.js → chunk-CYSBUYHQ-BovYyc4Y.js} +1 -1
  31. package/dist/assets/{chunk-EMLP6XTP-DX6uQQ2v.js → chunk-EMLP6XTP-BMDOpuYN.js} +1 -1
  32. package/dist/assets/{chunk-EX3LRPZG-DAJRNkHR.js → chunk-EX3LRPZG-6eP2UPOf.js} +1 -1
  33. package/dist/assets/{chunk-FWX5IMBZ-XbM5yomC.js → chunk-FWX5IMBZ-D4q-wTSy.js} +2 -2
  34. package/dist/assets/{chunk-HOUHSVGY-Ci0ljnb1.js → chunk-HOUHSVGY-XjIcuzH7.js} +1 -1
  35. package/dist/assets/{chunk-ICXQ74PX-Dy2WJ71z.js → chunk-ICXQ74PX-YBXbanq9.js} +1 -1
  36. package/dist/assets/{chunk-JG7HCLWE-2PrkzHkt.js → chunk-JG7HCLWE-CcN3uNXF.js} +1 -1
  37. package/dist/assets/{chunk-JWPE2WC7-D32uacSM.js → chunk-JWPE2WC7-Cwg6AUAt.js} +1 -1
  38. package/dist/assets/{chunk-MOJQB5TN-DIAFLtiS.js → chunk-MOJQB5TN-NNspcGIU.js} +1 -1
  39. package/dist/assets/{chunk-MOZMSUNE-BbscOaEz.js → chunk-MOZMSUNE-pMJ_jMos.js} +1 -1
  40. package/dist/assets/{chunk-OGEWGWER-ivBVxzKe.js → chunk-OGEWGWER-CJy-3Ldv.js} +1 -1
  41. package/dist/assets/{chunk-OSBZ3O6U-frD0hkdx.js → chunk-OSBZ3O6U-D7cLHHOA.js} +1 -1
  42. package/dist/assets/{chunk-PUDLZKDR-B-37Rxae.js → chunk-PUDLZKDR-D5YBi1Pi.js} +1 -1
  43. package/dist/assets/{chunk-Q4XR5HBZ-Y90m2RTe.js → chunk-Q4XR5HBZ-DYmqQdIf.js} +1 -1
  44. package/dist/assets/{chunk-QBLGF6JB-DixBqslB.js → chunk-QBLGF6JB-Bfcx00X0.js} +1 -1
  45. package/dist/assets/{chunk-R7FJI6CG-DXS1UXn_.js → chunk-R7FJI6CG-oXzadpZY.js} +1 -1
  46. package/dist/assets/{chunk-RYQCIY6F-n4JxhNd0.js → chunk-RYQCIY6F-Dt3-8yUh.js} +1 -1
  47. package/dist/assets/{chunk-U6XO7XAA-yHlgxMmn.js → chunk-U6XO7XAA-B55eSvpx.js} +1 -1
  48. package/dist/assets/{chunk-V7JOEXUC-Z6kuZfbl.js → chunk-V7JOEXUC-Btk4JwCt.js} +1 -1
  49. package/dist/assets/chunk-VAUOI2AC-CIZseNLi.js +1 -0
  50. package/dist/assets/{chunk-VR4S4FIN-Cs_SIQma.js → chunk-VR4S4FIN-DgaSiVOY.js} +1 -1
  51. package/dist/assets/{chunk-WYO6CB5R-CGExYbV-.js → chunk-WYO6CB5R-rRDZ_Tta.js} +3 -3
  52. package/dist/assets/chunk-XXDRQBXY-Do-XYxat.js +1 -0
  53. package/dist/assets/{chunk-YOTPTUD7-DopPXWSl.js → chunk-YOTPTUD7-BnFxj5o4.js} +1 -1
  54. package/dist/assets/{chunk-ZGVPDNZ5-CJHOeqMb.js → chunk-ZGVPDNZ5-CrnlPlmJ.js} +1 -1
  55. package/dist/assets/{chunk-ZIRB5QZD-BBVc5mf6.js → chunk-ZIRB5QZD-BYTiATIH.js} +1 -1
  56. package/dist/assets/classDiagram-OUVF2IWQ-Bu24Sy-k.js +1 -0
  57. package/dist/assets/classDiagram-v2-EOCWNBFH-DKhh5HSW.js +1 -0
  58. package/dist/assets/{cose-bilkent-JH36ORCC-DLjV-dTm.js → cose-bilkent-JH36ORCC-BORKJRwF.js} +1 -1
  59. package/dist/assets/cynefin-VYW2F7L2-oyiAgYC4.js +1 -0
  60. package/dist/assets/{cynefinDiagram-TSTJHNR4-BfH3zVS-.js → cynefinDiagram-TSTJHNR4-BXKeaSl8.js} +1 -1
  61. package/dist/assets/{dagre-CY3D84Cl.js → dagre-CrA0XVwJ.js} +1 -1
  62. package/dist/assets/{dagre-VKFMJZFB-BIhwZCIZ.js → dagre-VKFMJZFB-ouq_lA1a.js} +1 -1
  63. package/dist/assets/{diagram-FQU43EPY-BHnzNM2w.js → diagram-FQU43EPY-BxR5kUiQ.js} +1 -1
  64. package/dist/assets/{diagram-G47NLZAW-C7-ZlmFT.js → diagram-G47NLZAW-DWonk1sL.js} +1 -1
  65. package/dist/assets/{diagram-NH7WQ7WH-C-P9Y7IR.js → diagram-NH7WQ7WH-fmV_wV5e.js} +1 -1
  66. package/dist/assets/{diagram-OA4YK3LP-Ci5lZ_T5.js → diagram-OA4YK3LP-DvOSRRiV.js} +1 -1
  67. package/dist/assets/{diagram-WEI45ONY-C1xuK2bs.js → diagram-WEI45ONY-Bp-xb3uS.js} +1 -1
  68. package/dist/assets/dist-DLKC_odd.js +1 -0
  69. package/dist/assets/{ebnfDiagram-CCIWWBDH-DKQ5NWMD.js → ebnfDiagram-CCIWWBDH-BTlnwneA.js} +1 -1
  70. package/dist/assets/{erDiagram-Q63AITRT-HAWySadP.js → erDiagram-Q63AITRT-C3fFWGCm.js} +1 -1
  71. package/dist/assets/esm-oa69b5u4.js +2 -0
  72. package/dist/assets/eventmodeling-45OFAUF4-YB_0MefX.js +1 -0
  73. package/dist/assets/flowDiagram-23GEKE2U-Bc73xdBd.js +1 -0
  74. package/dist/assets/{ganttDiagram-NO4QXBWP-C1hwIcOA.js → ganttDiagram-NO4QXBWP-DzAiojNi.js} +1 -1
  75. package/dist/assets/gitGraph-TEB2WS4Q-Bvp27L0n.js +1 -0
  76. package/dist/assets/{gitGraphDiagram-IHSO6WYX-DYUYxs1R.js → gitGraphDiagram-IHSO6WYX-Cv57oNEA.js} +1 -1
  77. package/dist/assets/icons-cTkqD_Gi.js +1 -0
  78. package/dist/assets/index-B48prgLL.js +65 -0
  79. package/dist/assets/index-DQhwHHer.css +3 -0
  80. package/dist/assets/info-DKCQHKI2-BsLOivgq.js +1 -0
  81. package/dist/assets/{infoDiagram-FWYZ7A6U-CnIL6tv4.js → infoDiagram-FWYZ7A6U-B5Z_O3Ro.js} +1 -1
  82. package/dist/assets/{ishikawaDiagram-FXEZZL3T-B7WXNycQ.js → ishikawaDiagram-FXEZZL3T-CKTK_TKV.js} +1 -1
  83. package/dist/assets/{journeyDiagram-5HDEW3XC-D4HvioyY.js → journeyDiagram-5HDEW3XC-Cm9k4ywW.js} +1 -1
  84. package/dist/assets/{kanban-definition-HUTT4EX6-CDs763eS.js → kanban-definition-HUTT4EX6-DzNteLwx.js} +1 -1
  85. package/dist/assets/{line-ikKkQiFp.js → line-BG78c_WJ.js} +1 -1
  86. package/dist/assets/{linear-GqPyxQkN.js → linear-1X20_EGu.js} +1 -1
  87. package/dist/assets/{map-CJwjCvwr.js → map-CCI8WZkS.js} +1 -1
  88. package/dist/assets/{mcp-servers-dialog-BQbFywVL.js → mcp-servers-dialog-GXXD9K0U.js} +2 -2
  89. package/dist/assets/{mermaid-parser.core-CoE8cccr.js → mermaid-parser.core-Bb8HI_OS.js} +2 -2
  90. package/dist/assets/{mermaid-renderer-BmbSAmpG.js → mermaid-renderer-PIaNiXAz.js} +2 -2
  91. package/dist/assets/{mermaid.core-DlFSL90j.js → mermaid.core-DUHOIwL5.js} +3 -3
  92. package/dist/assets/{mindmap-definition-LN4V7U3C-DLdq0K9C.js → mindmap-definition-LN4V7U3C-Tnbszo7b.js} +1 -1
  93. package/dist/assets/{monaco-BTsVCDWS.js → monaco-BVtCspYZ.js} +1 -1
  94. package/dist/assets/{ordinal-CibZU3PM.js → ordinal-v4prQcuq.js} +1 -1
  95. package/dist/assets/{otel-BNGM50C-.js → otel-D1LswXpM.js} +1 -1
  96. package/dist/assets/packet-7NZHBO7P-BKQkXRtk.js +1 -0
  97. package/dist/assets/{pegDiagram-2B236MQR-Cm1kPRC2.js → pegDiagram-2B236MQR-CX824Kd0.js} +1 -1
  98. package/dist/assets/{pi-ai-Cr3rpCXT.js → pi-ai-DAJlO9ZB.js} +2 -2
  99. package/dist/assets/{pi-web-ui-DoRGtREc.js → pi-web-ui-B-RJSxjR.js} +1 -1
  100. package/dist/assets/pie-RZYD4A2V-BTiTN5Ge.js +1 -0
  101. package/dist/assets/{pieDiagram-ENE6RG2P-Df05wjjo.js → pieDiagram-ENE6RG2P-BT04VWPQ.js} +1 -1
  102. package/dist/assets/{quadrantDiagram-ABIIQ3AL-BjCwGxJC.js → quadrantDiagram-ABIIQ3AL-BvOZxkBr.js} +1 -1
  103. package/dist/assets/radar-I7S5WNFK-BU-oVjKM.js +1 -0
  104. package/dist/assets/railroad-3IZDKUUU-7H2hxNbL.js +1 -0
  105. package/dist/assets/railroad-abnf-AHOZXSZD-Mx6wf6XQ.js +1 -0
  106. package/dist/assets/railroad-ebnf-EBAXGLYW-BgXSKhYy.js +1 -0
  107. package/dist/assets/railroad-peg-LSFZ7HO6-DTEl2rAC.js +1 -0
  108. package/dist/assets/{railroadDiagram-RFXS5EU6-CTUvpKkv.js → railroadDiagram-RFXS5EU6-DnTU--BV.js} +1 -1
  109. package/dist/assets/{react-vendor-Dr5xvL-e.js → react-vendor-eWH0QhNU.js} +1 -1
  110. package/dist/assets/{requirementDiagram-TGXJPOKE-BU1kvEku.js → requirementDiagram-TGXJPOKE--MZX_yEv.js} +1 -1
  111. package/dist/assets/{sankeyDiagram-HTMAVEWB-BHmvxVqH.js → sankeyDiagram-HTMAVEWB-SEZkHhYn.js} +1 -1
  112. package/dist/assets/{sequenceDiagram-DBY2YBRQ-CTpwICgg.js → sequenceDiagram-DBY2YBRQ-BhC2OHiz.js} +1 -1
  113. package/dist/assets/{sizeCapture-X5ZJPWSS-BbLhGaa4.js → sizeCapture-X5ZJPWSS-Ci5LeToV.js} +1 -1
  114. package/dist/assets/{skills-dialog-REBeTFSH.js → skills-dialog-DDh1h8eH.js} +3 -1
  115. package/dist/assets/{src-B4Tvg3gT.js → src-CovulcRC.js} +1 -1
  116. package/dist/assets/{stateDiagram-2N3HPSRC-BxuDwIgT.js → stateDiagram-2N3HPSRC-DKK5tFT4.js} +1 -1
  117. package/dist/assets/stateDiagram-v2-6OUMAXLB-D8x3mkni.js +1 -0
  118. package/dist/assets/{swimlanes-5IMT3BWC-EkZsiylV.js → swimlanes-5IMT3BWC-DxxGCk67.js} +2 -2
  119. package/dist/assets/swimlanesDiagram-G3AALYLV-C4nOBNp8.js +8 -0
  120. package/dist/assets/{timeline-definition-FHXFAJF6-C9ewOCTa.js → timeline-definition-FHXFAJF6-BPXB3Sue.js} +1 -1
  121. package/dist/assets/treeView-QDETBFTQ-DOE6NSdf.js +1 -0
  122. package/dist/assets/treemap-6X3UGDF4-Ca7QD_b4.js +1 -0
  123. package/dist/assets/{vennDiagram-L72KCM5P-CR49Y4Lp.js → vennDiagram-L72KCM5P-CxcBCKye.js} +1 -1
  124. package/dist/assets/wardley-OPB4EBWU-D9O4RKRm.js +1 -0
  125. package/dist/assets/{wardleyDiagram-EHGQE667-Cn0GgwWi.js → wardleyDiagram-EHGQE667-DEL6Yes5.js} +1 -1
  126. package/dist/assets/web-RgX7H-OH.js +1 -0
  127. package/dist/assets/{xychartDiagram-FW5EYKEG-DlmPNAvd.js → xychartDiagram-FW5EYKEG-N8cPbcjC.js} +1 -1
  128. package/dist/index.html +12 -11
  129. package/dist/mobile-error.html +24 -0
  130. package/package.json +4 -1
  131. package/server/acp/server.mjs +14 -9
  132. package/server/agent-manager.mjs +215 -20
  133. package/server/ai-http-logger.mjs +68 -11
  134. package/server/ai-provider-options.mjs +3 -0
  135. package/server/channels/channel-logs.mjs +13 -0
  136. package/server/channels/event-relay.mjs +33 -0
  137. package/server/channels/process-channel.mjs +29 -0
  138. package/server/channels/providers/wechat.mjs +9 -3
  139. package/server/channels/registry.mjs +5 -1
  140. package/server/index.mjs +36 -23
  141. package/server/lan-access-store.mjs +93 -3
  142. package/server/network-proxy.mjs +68 -3
  143. package/server/plugins/registry.mjs +64 -4
  144. package/server/public-api.mjs +6 -2
  145. package/server/routes/agent-profiles.mjs +2 -2
  146. package/server/routes/agent.mjs +28 -0
  147. package/server/routes/channels.mjs +47 -0
  148. package/server/routes/lan-access.mjs +246 -21
  149. package/server/routes/models.mjs +6 -3
  150. package/server/routes/project.mjs +19 -1
  151. package/server/routes/scheduled-tasks.mjs +2 -2
  152. package/server/routes/static.mjs +26 -1
  153. package/server/routes/system.mjs +11 -0
  154. package/server/routes/terminal.mjs +7 -2
  155. package/server/routes/workspace.mjs +60 -9
  156. package/server/session-utils.mjs +2 -2
  157. package/server/subagents.mjs +1 -1
  158. package/server/terminal/terminal-manager.mjs +2 -4
  159. package/server/tools/definitions.mjs +2 -2
  160. package/server/tools/index.mjs +5 -3
  161. package/server/utils/package-update.mjs +70 -47
  162. package/server/utils/process-env.mjs +7 -0
  163. package/server/utils/response.mjs +2 -2
  164. package/dist/assets/AgentProfilesPage-D7pbg8nm.js +0 -1
  165. package/dist/assets/SettingsWorkspacePage-BtJZMIsZ.js +0 -1576
  166. package/dist/assets/SharedConversationPage-Ce3KoVZt.js +0 -1
  167. package/dist/assets/TerminalDock-4eUuXP7X.js +0 -2
  168. package/dist/assets/WorkspaceInspector-kM3BRPjY.js +0 -13
  169. package/dist/assets/architecture-TIHT7OUA-BgP-IMeo.js +0 -1
  170. package/dist/assets/channel-Rz20dKTK.js +0 -1
  171. package/dist/assets/chunk-2Q5K7J3B-Cb14ZF_k.js +0 -1
  172. package/dist/assets/chunk-VAUOI2AC-BOZvd2Sn.js +0 -1
  173. package/dist/assets/chunk-XXDRQBXY-CD_EGCtT.js +0 -1
  174. package/dist/assets/classDiagram-OUVF2IWQ-C_ozTKCt.js +0 -1
  175. package/dist/assets/classDiagram-v2-EOCWNBFH-FOfFbbZF.js +0 -1
  176. package/dist/assets/cynefin-VYW2F7L2-BzPhLtd5.js +0 -1
  177. package/dist/assets/eventmodeling-45OFAUF4-BcgGbVJ-.js +0 -1
  178. package/dist/assets/flowDiagram-23GEKE2U-dtkzcC8E.js +0 -1
  179. package/dist/assets/gitGraph-TEB2WS4Q-VByWboBU.js +0 -1
  180. package/dist/assets/icons-BP8YOS-Z.js +0 -1
  181. package/dist/assets/index-C6k5taeb.js +0 -66
  182. package/dist/assets/index-CzN8NSKC.css +0 -3
  183. package/dist/assets/info-DKCQHKI2-CZeDLZxx.js +0 -1
  184. package/dist/assets/packet-7NZHBO7P-decrrxyX.js +0 -1
  185. package/dist/assets/pie-RZYD4A2V-J1CasQZl.js +0 -1
  186. package/dist/assets/radar-I7S5WNFK-B-sZytUA.js +0 -1
  187. package/dist/assets/railroad-3IZDKUUU-BSaqNPm8.js +0 -1
  188. package/dist/assets/railroad-abnf-AHOZXSZD-lzZR7PxT.js +0 -1
  189. package/dist/assets/railroad-ebnf-EBAXGLYW-DQtz1Fh9.js +0 -1
  190. package/dist/assets/railroad-peg-LSFZ7HO6-CpMsuGY3.js +0 -1
  191. package/dist/assets/stateDiagram-v2-6OUMAXLB-BGLshxcY.js +0 -1
  192. package/dist/assets/swimlanesDiagram-G3AALYLV-CqdqxA-b.js +0 -8
  193. package/dist/assets/treeView-QDETBFTQ-Byb71heW.js +0 -1
  194. package/dist/assets/treemap-6X3UGDF4-CMV1SJsq.js +0 -1
  195. package/dist/assets/wardley-OPB4EBWU-DtCHtr2V.js +0 -1
  196. /package/dist/assets/{array-CYkMkqnU.js → array-DqLCdDFv.js} +0 -0
  197. /package/dist/assets/{chunk-KEIR6QF5-77hsebd7.js → chunk-KEIR6QF5-s7Qq-R3i.js} +0 -0
  198. /package/dist/assets/{chunk-Y2CYZVJY-CQltPwYk.js → chunk-Y2CYZVJY-BgISNSiw.js} +0 -0
  199. /package/dist/assets/{css-utils-rkE68RDy.js → css-utils-31qzCXsk.js} +0 -0
  200. /package/dist/assets/{cytoscape.esm-DwdZnGOm.js → cytoscape.esm-iekjSZ2n.js} +0 -0
  201. /package/dist/assets/{defaultLocale-Beh6XjaL.js → defaultLocale-CrJzLgRD.js} +0 -0
  202. /package/dist/assets/{dist-DelZBZpK.js → dist-zzfqk5hx.js} +0 -0
  203. /package/dist/assets/{graphlib-CAISHZh4.js → graphlib-CIz_CfxW.js} +0 -0
  204. /package/dist/assets/{init-Rr1s_RiX.js → init-C0r9Gk5G.js} +0 -0
  205. /package/dist/assets/{lit-vendor-Dr3cpBGF.js → lit-vendor-DvaQX_UQ.js} +0 -0
  206. /package/dist/assets/{logger-B65Akg8A.js → logger-8REuLPGK.js} +0 -0
  207. /package/dist/assets/{path-BAQ3hXlG.js → path-DIKpVbHL.js} +0 -0
  208. /package/dist/assets/{plugin-api-BbUkYm3Y.js → plugin-api-C8hMT5W6.js} +0 -0
  209. /package/dist/assets/{rough.esm-Djo4Abte.js → rough.esm-BMOtDaaS.js} +0 -0
  210. /package/dist/assets/{xterm-CGiQzDiR.js → xterm-BFzZ1FH4.js} +0 -0
@@ -1,4 +1,5 @@
1
1
  import { sendJson, readJsonBody, decodeSegment } from '../utils/response.mjs'
2
+ import { logger } from '../utils/logger.mjs'
2
3
  import {
3
4
  createAgent,
4
5
  runPrompt,
@@ -96,6 +97,30 @@ export async function handleAgentApi(req, res, url) {
96
97
  return
97
98
  }
98
99
 
100
+ // POST /api/agents/:sessionId/restore — restore once and return the authoritative state
101
+ if (req.method === 'POST' && subPath === 'restore') {
102
+ const startedAt = performance.now()
103
+ const session = await restoreAgent(sessionId)
104
+ if (!session) {
105
+ const error = new Error('Session not found')
106
+ error.statusCode = 404
107
+ throw error
108
+ }
109
+ const state = getSessionState(sessionId)
110
+ if (!state) {
111
+ const error = new Error('Session not found')
112
+ error.statusCode = 404
113
+ throw error
114
+ }
115
+ logger.debug(`Restored session ${sessionId} for client`, {
116
+ sessionId,
117
+ messageCount: state.messages?.length ?? 0,
118
+ durationMs: Math.round((performance.now() - startedAt) * 10) / 10,
119
+ })
120
+ sendJson(res, 200, state)
121
+ return
122
+ }
123
+
99
124
  // GET /api/agents/:sessionId/state — get session state
100
125
  if (req.method === 'GET' && subPath === 'state') {
101
126
  let state = getSessionState(sessionId)
@@ -141,6 +166,9 @@ export async function handleAgentApi(req, res, url) {
141
166
  status: session.status,
142
167
  scope: session.scope,
143
168
  title: session.title,
169
+ source: session.source || undefined,
170
+ channelId: session.channelId || undefined,
171
+ channelName: session.channelName || undefined,
144
172
  accessMode: session.accessMode,
145
173
  yoloMode: session.yoloMode,
146
174
  })
@@ -1,4 +1,8 @@
1
+ import { promises as fs } from 'node:fs'
2
+ import { logger } from '../utils/logger.mjs'
1
3
  import { sendJson, decodeSegment, readJsonBody } from '../utils/response.mjs'
4
+ import { channelLogsDirectory } from '../channels/channel-logs.mjs'
5
+ import { openPathInFileManager } from '../utils/platform.mjs'
2
6
  import {
3
7
  channelEvents,
4
8
  getChannelStatus,
@@ -25,6 +29,24 @@ function assertActionHeader(req) {
25
29
  }
26
30
  }
27
31
 
32
+ function assertEventHeader(req) {
33
+ if (req.headers['x-quickforge-action'] !== 'channel-event') {
34
+ const error = new Error('Forbidden event')
35
+ error.statusCode = 403
36
+ throw error
37
+ }
38
+ }
39
+
40
+ async function readChannelEvent(req) {
41
+ const event = await readJsonBody(req, 256 * 1024)
42
+ if (!event || event.type !== 'sessions-changed' || typeof event.sessionId !== 'string' || !event.sessionId.trim()) {
43
+ const error = new Error('Invalid channel event')
44
+ error.statusCode = 400
45
+ throw error
46
+ }
47
+ return event
48
+ }
49
+
28
50
  async function readActionOptions(req) {
29
51
  if (!['POST', 'PUT', 'PATCH'].includes(req.method || '')) return {}
30
52
  const contentType = String(req.headers['content-type'] || '')
@@ -46,6 +68,15 @@ export async function handleChannelsApi(req, res, url, context = {}) {
46
68
  return
47
69
  }
48
70
 
71
+ if (req.method === 'POST' && pathname === '/api/channels/events') {
72
+ assertLocal(context)
73
+ assertEventHeader(req)
74
+ const event = await readChannelEvent(req)
75
+ channelEvents.emit('channel_event', event)
76
+ sendJson(res, 202, { ok: true })
77
+ return
78
+ }
79
+
49
80
  if (parts.length < 3 || parts[0] !== 'api' || parts[1] !== 'channels') {
50
81
  const error = new Error('Not found')
51
82
  error.statusCode = 404
@@ -67,6 +98,22 @@ export async function handleChannelsApi(req, res, url, context = {}) {
67
98
 
68
99
  assertLocal(context)
69
100
 
101
+ if (req.method === 'POST' && subPath.length === 1 && subPath[0] === 'open-logs') {
102
+ assertActionHeader(req)
103
+ getChannelStatus(channelId)
104
+ if (!context.logsDir) {
105
+ const error = new Error('Channel log directory is unavailable')
106
+ error.statusCode = 500
107
+ throw error
108
+ }
109
+ const directory = channelLogsDirectory(context.logsDir, channelId)
110
+ await fs.mkdir(directory, { recursive: true })
111
+ await (context.openPathInFileManager ?? openPathInFileManager)(directory)
112
+ logger.info('Opened channel log directory.', { channelId })
113
+ sendJson(res, 200, { ok: true })
114
+ return
115
+ }
116
+
70
117
  if (req.method === 'POST' && subPath[0] === 'start') {
71
118
  assertActionHeader(req)
72
119
  sendJson(res, 202, await startChannel(channelId, await readActionOptions(req)))
@@ -1,10 +1,13 @@
1
1
  import { sendJson, readJsonBody } from '../utils/response.mjs'
2
2
  import { getLanUrls } from '../utils/network.mjs'
3
3
  import { logger } from '../utils/logger.mjs'
4
+ import { parseCookies } from '../share-store.mjs'
4
5
  import {
5
6
  issueLanAccessToken,
6
7
  lanAccessCookieName,
7
8
  readLanAccessStatus,
9
+ revokeLanAccessToken,
10
+ revokeLanAccessTokenById,
8
11
  revokeLanAccessTokens,
9
12
  updateLanAccessSettings,
10
13
  } from '../lan-access-store.mjs'
@@ -102,36 +105,242 @@ export function renderLanUnlockPage(res) {
102
105
  <head>
103
106
  <meta charset="utf-8" />
104
107
  <meta name="viewport" content="width=device-width, initial-scale=1" />
108
+ <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
109
+ <meta name="theme-color" content="#171717" media="(prefers-color-scheme: dark)" />
110
+ <link rel="icon" href="/favicon.svg" />
105
111
  <title>QuickForge 局域网访问</title>
106
112
  <style>
107
- :root { color-scheme: light dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
108
- body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #0f172a; color: #e5e7eb; }
109
- main { width: min(420px, calc(100vw - 32px)); border: 1px solid rgba(148,163,184,.3); border-radius: 20px; padding: 28px; background: rgba(15,23,42,.92); box-shadow: 0 24px 80px rgba(0,0,0,.35); }
110
- h1 { margin: 0 0 8px; font-size: 22px; }
111
- p { margin: 0 0 18px; color: #94a3b8; line-height: 1.6; }
112
- label { display: block; margin-bottom: 8px; font-size: 14px; font-weight: 600; }
113
- input { width: 100%; box-sizing: border-box; height: 42px; border-radius: 10px; border: 1px solid #334155; background: #020617; color: #f8fafc; padding: 0 12px; font-size: 15px; }
114
- button { width: 100%; height: 42px; margin-top: 14px; border: 0; border-radius: 10px; background: #2563eb; color: white; font-weight: 700; cursor: pointer; }
115
- button:disabled { opacity: .6; cursor: default; }
116
- .error { min-height: 20px; margin-top: 12px; color: #fca5a5; font-size: 13px; }
113
+ :root {
114
+ color-scheme: light dark;
115
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
116
+ "Helvetica Neue", Arial, "Noto Sans", "PingFang SC", "Microsoft YaHei", sans-serif;
117
+ --background: #ffffff;
118
+ --foreground: #171717;
119
+ --muted-foreground: #737373;
120
+ --border: #e5e5e5;
121
+ --input-border: #dedede;
122
+ --surface: #ffffff;
123
+ --surface-muted: #fafafa;
124
+ --primary: #171717;
125
+ --primary-hover: #292929;
126
+ --primary-foreground: #fafafa;
127
+ --danger: #dc2626;
128
+ --shadow: 0 10px 26px -18px rgba(15, 23, 42, .48);
129
+ }
130
+ * { box-sizing: border-box; }
131
+ html, body { margin: 0; min-width: 0; min-height: 100%; }
132
+ body {
133
+ min-height: 100vh;
134
+ min-height: 100dvh;
135
+ display: grid;
136
+ place-items: center;
137
+ padding: 32px 20px;
138
+ background: var(--background);
139
+ color: var(--foreground);
140
+ -webkit-font-smoothing: antialiased;
141
+ }
142
+ main { width: min(100%, 440px); }
143
+ .intro { display: flex; align-items: center; gap: 13px; margin-bottom: 30px; }
144
+ .brand-mark {
145
+ display: grid;
146
+ width: 46px;
147
+ height: 46px;
148
+ flex: 0 0 auto;
149
+ place-items: center;
150
+ border: 1px solid var(--border);
151
+ border-radius: 13px;
152
+ background: var(--surface);
153
+ box-shadow: var(--shadow);
154
+ }
155
+ .brand-mark img { width: 29px; height: 29px; }
156
+ .eyebrow {
157
+ margin: 0 0 2px;
158
+ color: var(--muted-foreground);
159
+ font-size: 11px;
160
+ font-weight: 600;
161
+ letter-spacing: .1em;
162
+ }
163
+ h1 { margin: 0; font-size: 20px; font-weight: 600; letter-spacing: -.02em; line-height: 1.35; }
164
+ .subtitle { margin: 3px 0 0; color: var(--muted-foreground); font-size: 14px; line-height: 1.5; }
165
+ form {
166
+ padding: 22px;
167
+ border: 1px solid var(--border);
168
+ border-radius: 16px;
169
+ background: var(--surface);
170
+ box-shadow: var(--shadow);
171
+ }
172
+ label { display: block; margin-bottom: 9px; font-size: 14px; font-weight: 500; }
173
+ .input-wrap { position: relative; }
174
+ .input-icon {
175
+ position: absolute;
176
+ top: 50%;
177
+ left: 13px;
178
+ width: 17px;
179
+ height: 17px;
180
+ color: var(--muted-foreground);
181
+ pointer-events: none;
182
+ transform: translateY(-50%);
183
+ }
184
+ input {
185
+ width: 100%;
186
+ height: 44px;
187
+ border: 1px solid var(--input-border);
188
+ border-radius: 12px;
189
+ outline: none;
190
+ background: var(--surface);
191
+ color: var(--foreground);
192
+ padding: 0 13px 0 40px;
193
+ font: inherit;
194
+ font-size: 14px;
195
+ transition: border-color 150ms ease, box-shadow 150ms ease;
196
+ }
197
+ input::placeholder { color: var(--muted-foreground); opacity: .7; }
198
+ input:hover { border-color: color-mix(in srgb, var(--foreground) 24%, var(--border)); }
199
+ input:focus {
200
+ border-color: color-mix(in srgb, var(--foreground) 30%, var(--border));
201
+ box-shadow: var(--shadow);
202
+ }
203
+ input[aria-invalid="true"] { border-color: color-mix(in srgb, var(--danger) 60%, var(--border)); }
204
+ button {
205
+ width: 100%;
206
+ height: 44px;
207
+ margin-top: 18px;
208
+ display: inline-flex;
209
+ align-items: center;
210
+ justify-content: center;
211
+ gap: 8px;
212
+ border: 0;
213
+ border-radius: 12px;
214
+ background: var(--primary);
215
+ color: var(--primary-foreground);
216
+ font: inherit;
217
+ font-size: 14px;
218
+ font-weight: 500;
219
+ cursor: pointer;
220
+ transition: background-color 150ms ease, opacity 150ms ease;
221
+ }
222
+ button:hover:not(:disabled) { background: var(--primary-hover); }
223
+ button:active:not(:disabled) { opacity: .88; }
224
+ button:focus-visible { outline: 2px solid color-mix(in srgb, var(--foreground) 28%, transparent); outline-offset: 3px; }
225
+ button:disabled { cursor: default; opacity: .62; }
226
+ .button-icon { width: 16px; height: 16px; }
227
+ .spinner { display: none; width: 15px; height: 15px; animation: spin .8s linear infinite; }
228
+ button.is-loading .button-icon { display: none; }
229
+ button.is-loading .spinner { display: block; }
230
+ .error {
231
+ min-height: 20px;
232
+ margin: 10px 0 -2px;
233
+ color: var(--danger);
234
+ font-size: 13px;
235
+ line-height: 20px;
236
+ }
237
+ .note {
238
+ display: flex;
239
+ gap: 9px;
240
+ margin: 17px 3px 0;
241
+ color: var(--muted-foreground);
242
+ font-size: 12px;
243
+ line-height: 1.65;
244
+ }
245
+ .note svg { width: 15px; height: 15px; flex: 0 0 auto; margin-top: 2px; }
246
+ @keyframes spin { to { transform: rotate(360deg); } }
247
+ @media (prefers-color-scheme: dark) {
248
+ :root {
249
+ --background: #171717;
250
+ --foreground: #fafafa;
251
+ --muted-foreground: #a3a3a3;
252
+ --border: #353535;
253
+ --input-border: #404040;
254
+ --surface: #1d1d1d;
255
+ --surface-muted: #262626;
256
+ --primary: #e5e5e5;
257
+ --primary-hover: #fafafa;
258
+ --primary-foreground: #171717;
259
+ --danger: #f87171;
260
+ --shadow: 0 10px 26px -18px rgba(0, 0, 0, .9);
261
+ }
262
+ }
263
+ @media (max-width: 480px) {
264
+ body { place-items: start center; padding-top: max(28px, 8vh); }
265
+ .intro { margin-bottom: 24px; }
266
+ form { padding: 20px; }
267
+ }
268
+ @media (prefers-reduced-motion: reduce) {
269
+ *, *::before, *::after { scroll-behavior: auto !important; transition: none !important; }
270
+ }
117
271
  </style>
118
272
  </head>
119
273
  <body>
120
274
  <main>
121
- <h1>QuickForge 局域网访问</h1>
122
- <p>请输入本机设置中配置的局域网访问密码。</p>
123
- <label for="password">访问密码</label>
124
- <input id="password" type="password" autocomplete="current-password" autofocus />
125
- <button id="submit" type="button">进入 QuickForge</button>
126
- <div id="error" class="error" role="alert"></div>
275
+ <header class="intro">
276
+ <div class="brand-mark" aria-hidden="true"><img src="/favicon.svg" alt="" /></div>
277
+ <div>
278
+ <p class="eyebrow">QUICKFORGE</p>
279
+ <h1>验证局域网访问</h1>
280
+ <p class="subtitle">输入主机端设置的访问密码,继续进入工作区。</p>
281
+ </div>
282
+ </header>
283
+
284
+ <form id="unlock-form">
285
+ <label for="password">访问密码</label>
286
+ <div class="input-wrap">
287
+ <svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
288
+ <rect x="4.5" y="10" width="15" height="10" rx="2.5"></rect>
289
+ <path d="M8 10V7.5a4 4 0 0 1 8 0V10"></path>
290
+ </svg>
291
+ <input
292
+ id="password"
293
+ type="password"
294
+ autocomplete="current-password"
295
+ placeholder="请输入访问密码"
296
+ aria-describedby="error"
297
+ aria-invalid="false"
298
+ autofocus
299
+ />
300
+ </div>
301
+ <button id="submit" type="submit">
302
+ <svg class="spinner" viewBox="0 0 24 24" fill="none" aria-hidden="true">
303
+ <circle cx="12" cy="12" r="9" stroke="currentColor" stroke-opacity=".25" stroke-width="3"></circle>
304
+ <path d="M21 12a9 9 0 0 0-9-9" stroke="currentColor" stroke-width="3" stroke-linecap="round"></path>
305
+ </svg>
306
+ <span id="button-label">进入 QuickForge</span>
307
+ <svg class="button-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
308
+ <path d="M5 12h14M13 6l6 6-6 6" stroke-linecap="round" stroke-linejoin="round"></path>
309
+ </svg>
310
+ </button>
311
+ <p id="error" class="error" role="alert" aria-live="polite"></p>
312
+ </form>
313
+
314
+ <p class="note">
315
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
316
+ <path d="M12 3 5 6v5c0 4.6 2.9 8.4 7 10 4.1-1.6 7-5.4 7-10V6l-7-3Z" stroke-linejoin="round"></path>
317
+ <path d="m9.5 12 1.7 1.7 3.6-3.7" stroke-linecap="round" stroke-linejoin="round"></path>
318
+ </svg>
319
+ <span>密码仅用于验证本次局域网访问,不会保存在此设备。</span>
320
+ </p>
127
321
  </main>
128
322
  <script>
323
+ const form = document.getElementById('unlock-form')
129
324
  const password = document.getElementById('password')
130
325
  const button = document.getElementById('submit')
326
+ const buttonLabel = document.getElementById('button-label')
131
327
  const error = document.getElementById('error')
132
- async function unlock() {
328
+
329
+ password.addEventListener('input', () => {
330
+ error.textContent = ''
331
+ password.setAttribute('aria-invalid', 'false')
332
+ })
333
+
334
+ form.addEventListener('submit', async (event) => {
335
+ event.preventDefault()
336
+ if (button.disabled) return
337
+
133
338
  error.textContent = ''
339
+ password.setAttribute('aria-invalid', 'false')
134
340
  button.disabled = true
341
+ button.classList.add('is-loading')
342
+ buttonLabel.textContent = '正在验证…'
343
+
135
344
  try {
136
345
  const response = await fetch('/api/lan-access/unlock', {
137
346
  method: 'POST',
@@ -143,12 +352,14 @@ export function renderLanUnlockPage(res) {
143
352
  window.location.reload()
144
353
  } catch (err) {
145
354
  error.textContent = err instanceof Error ? err.message : '密码错误'
355
+ password.setAttribute('aria-invalid', 'true')
356
+ password.focus()
146
357
  } finally {
147
358
  button.disabled = false
359
+ button.classList.remove('is-loading')
360
+ buttonLabel.textContent = '进入 QuickForge'
148
361
  }
149
- }
150
- button.addEventListener('click', unlock)
151
- password.addEventListener('keydown', (event) => { if (event.key === 'Enter') unlock() })
362
+ })
152
363
  </script>
153
364
  </body>
154
365
  </html>`
@@ -186,7 +397,10 @@ export async function handleLanAccessApi(req, res, url, context = {}) {
186
397
  assertNotLocked(req)
187
398
  const body = await readJsonBody(req, 1024)
188
399
  try {
189
- const result = await issueLanAccessToken(body?.password)
400
+ const result = await issueLanAccessToken(body?.password, {
401
+ remoteAddress: req.socket.remoteAddress,
402
+ userAgent: req.headers['user-agent'],
403
+ })
190
404
  setLanCookie(res, result.token, result.maxAge)
191
405
  clearFailures(req)
192
406
  logger.info('LAN access unlock succeeded.', { remoteAddress: req.socket.remoteAddress })
@@ -202,11 +416,22 @@ export async function handleLanAccessApi(req, res, url, context = {}) {
202
416
  }
203
417
 
204
418
  if (req.method === 'POST' && pathname === '/api/lan-access/logout') {
419
+ const token = parseCookies(req.headers.cookie).get(lanAccessCookieName())
420
+ await revokeLanAccessToken(token)
205
421
  clearLanCookie(res)
206
422
  sendJson(res, 200, { ok: true })
207
423
  return
208
424
  }
209
425
 
426
+ if (req.method === 'POST' && pathname === '/api/lan-access/revoke') {
427
+ requireLocal(context)
428
+ const body = await readJsonBody(req, 1024)
429
+ const status = await revokeLanAccessTokenById(body?.id)
430
+ logger.info('LAN access session revoked.', { sessionId: body?.id })
431
+ sendJson(res, 200, { ok: true, ...status })
432
+ return
433
+ }
434
+
210
435
  if (req.method === 'POST' && pathname === '/api/lan-access/revoke-all') {
211
436
  requireLocal(context)
212
437
  const status = await revokeLanAccessTokens()
@@ -1,4 +1,4 @@
1
- import { streamSimple } from '@earendil-works/pi-ai/compat'
1
+ import { streamSimpleWithAiHttpLogging } from '../ai-http-logger.mjs'
2
2
  import { sendJson, readJsonBody } from '../utils/response.mjs'
3
3
  import { readStore } from '../storage.mjs'
4
4
  import { logger } from '../utils/logger.mjs'
@@ -22,7 +22,7 @@ async function getApiKey(provider) {
22
22
  // API key is valid. Returns { ok: true } on success; throws on failure.
23
23
  async function probeModelConnection(model, apiKeyOverride) {
24
24
  const apiKey = apiKeyOverride || (await getApiKey(model?.provider))
25
- const stream = streamSimple(
25
+ const stream = streamSimpleWithAiHttpLogging(
26
26
  model,
27
27
  {
28
28
  systemPrompt: 'You are a connectivity test. Reply with a single word.',
@@ -39,7 +39,10 @@ async function probeModelConnection(model, apiKeyOverride) {
39
39
  maxRetryDelayMs: 30000,
40
40
  },
41
41
  )
42
- await stream.result()
42
+ const message = await stream.result()
43
+ if (message?.stopReason === 'error' || message?.stopReason === 'aborted') {
44
+ throw new Error(message.errorMessage || `Model request ${message.stopReason}`)
45
+ }
43
46
  return { ok: true }
44
47
  }
45
48
 
@@ -8,7 +8,25 @@ import { getWorkspaceRoot, setWorkspaceRoot } from '../utils/workspace.mjs'
8
8
  import { selectDirectoryDialog, openPathInFileManager, openPathInIDEA, openPathInVSCode } from '../utils/platform.mjs'
9
9
  import path from 'node:path'
10
10
 
11
- export async function handleProjectApi(req, res, url) {
11
+ function assertLocalExternalAction(context) {
12
+ if (context.isLocalRequest === true) return
13
+ const error = new Error('Opening applications is only allowed from this computer')
14
+ error.statusCode = 403
15
+ throw error
16
+ }
17
+
18
+ export async function handleProjectApi(req, res, url, context = {}) {
19
+ const externalAction = req.method === 'POST' && (
20
+ url.pathname === '/api/project/select-directory'
21
+ || (url.pathname.startsWith('/api/project/') && (
22
+ url.pathname.endsWith('/open-in-explorer')
23
+ || url.pathname.endsWith('/open-in-vscode')
24
+ || url.pathname.endsWith('/open-in-idea')
25
+ ))
26
+ || url.pathname === '/api/project/open-path'
27
+ )
28
+ if (externalAction) assertLocalExternalAction(context)
29
+
12
30
  const config = await readProjectConfig()
13
31
 
14
32
  if (req.method === 'GET' && url.pathname === '/api/project') {
@@ -1,4 +1,4 @@
1
- import { streamSimple } from '@earendil-works/pi-ai/compat'
1
+ import { streamSimpleWithAiHttpLogging } from '../ai-http-logger.mjs'
2
2
  import { DEFAULT_AI_MAX_RETRIES } from '../ai-provider-options.mjs'
3
3
  import { readJsonBody, sendJson, decodeSegment } from '../utils/response.mjs'
4
4
  import { readStore, atomicUpdate } from '../storage.mjs'
@@ -154,7 +154,7 @@ async function parseScheduledTaskInstructionWithAi(instruction, model, thinkingL
154
154
  示例输出:{"title":"生成日报","instruction":"生成销售日报","cronExpression":"0 9 * * *","scheduleRule":"每天 09:00","question":""}`
155
155
 
156
156
  try {
157
- const stream = streamSimple(
157
+ const stream = streamSimpleWithAiHttpLogging(
158
158
  model,
159
159
  {
160
160
  systemPrompt,
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url'
4
4
 
5
5
  const __filename = fileURLToPath(import.meta.url)
6
6
  const projectRoot = path.resolve(path.dirname(__filename), '../..')
7
+ const androidApkPath = path.join(projectRoot, 'android', 'app', 'build', 'outputs', 'apk', 'debug', 'app-debug.apk')
7
8
 
8
9
  function getContentType(filePath) {
9
10
  const extension = path.extname(filePath).toLowerCase()
@@ -45,7 +46,31 @@ function shouldFallbackToIndex(url, pathname) {
45
46
  return !path.extname(pathname)
46
47
  }
47
48
 
49
+ function cacheControlFor(filePath) {
50
+ if (filePath.endsWith('index.html')) return 'no-cache'
51
+ if (path.extname(filePath).toLowerCase() === '.apk') return 'no-cache'
52
+ return 'public, max-age=31536000, immutable'
53
+ }
54
+
48
55
  export async function serveStatic(req, res, url) {
56
+ if (url.pathname === '/downloads/quickforge-android.apk') {
57
+ try {
58
+ const data = await fs.readFile(androidApkPath)
59
+ res.writeHead(200, {
60
+ 'content-type': 'application/vnd.android.package-archive',
61
+ 'content-length': data.length,
62
+ 'content-disposition': 'attachment; filename="quickforge-android.apk"',
63
+ 'cache-control': 'no-cache',
64
+ 'x-content-type-options': 'nosniff',
65
+ })
66
+ res.end(data)
67
+ } catch {
68
+ res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' })
69
+ res.end('Android APK not found. Run npm run android:build first.')
70
+ }
71
+ return
72
+ }
73
+
49
74
  const distDir = path.join(projectRoot, 'dist')
50
75
  const requested = decodeURIComponent(requestPathname(url))
51
76
  const normalized = path.normalize(requested).replace(/^([.][.][/])+/, '').replace(/^[/\\]+/, '')
@@ -74,7 +99,7 @@ export async function serveStatic(req, res, url) {
74
99
  const data = await fs.readFile(filePath)
75
100
  res.writeHead(200, {
76
101
  'content-type': getContentType(filePath),
77
- 'cache-control': filePath.endsWith('index.html') ? 'no-cache' : 'public, max-age=31536000, immutable',
102
+ 'cache-control': cacheControlFor(filePath),
78
103
  })
79
104
  res.end(data)
80
105
  } catch {
@@ -36,6 +36,12 @@ export async function handleSystemApi(req, res, url, context) {
36
36
  }
37
37
 
38
38
  if (req.method === 'POST' && url.pathname === '/api/system/restart') {
39
+ if (!context.isLocalRequest) {
40
+ const error = new Error('Restart is only allowed from this computer')
41
+ error.statusCode = 403
42
+ throw error
43
+ }
44
+
39
45
  if (req.headers['x-quickforge-action'] !== 'restart') {
40
46
  const error = new Error('Forbidden action')
41
47
  error.statusCode = 403
@@ -59,6 +65,11 @@ export async function handleSystemApi(req, res, url, context) {
59
65
  }
60
66
 
61
67
  if (req.method === 'PUT') {
68
+ if (!context.isLocalRequest) {
69
+ const error = new Error('Terminal shell settings can only be changed from this computer')
70
+ error.statusCode = 403
71
+ throw error
72
+ }
62
73
  const body = await readJsonBody(req, 64 * 1024) || {}
63
74
  if (Array.isArray(body.profiles) || typeof body.defaultProfileId === 'string') {
64
75
  sendJson(res, 200, await context.updateTerminalShellConfig(body))
@@ -59,7 +59,7 @@ export async function handleTerminalApi(req, res, url, options = {}) {
59
59
  await assertLocalTerminalRequest(req, options.isLocalRequest)
60
60
 
61
61
  if (req.method === 'GET' && url.pathname === '/api/terminal/capabilities') {
62
- sendJson(res, 200, { ...await terminalCapabilities(), ...await platformInfo() })
62
+ sendJson(res, 200, { ...await terminalCapabilities(), ...await platformInfo() }, 'private, max-age=300')
63
63
  return
64
64
  }
65
65
 
@@ -137,7 +137,12 @@ async function handleTerminalUpgradeAsync(req, socket, head, url, options = {})
137
137
  attachTerminalClient(sessionId, ws)
138
138
  } catch (err) {
139
139
  try {
140
- ws.send(JSON.stringify({ type: 'error', message: err?.message || 'Terminal connection failed' }), () => ws.close())
140
+ ws.send(JSON.stringify({
141
+ type: 'error',
142
+ code: err?.statusCode === 404 ? 'SESSION_NOT_FOUND' : 'TERMINAL_CONNECTION_FAILED',
143
+ message: err?.message || 'Terminal connection failed',
144
+ retryable: err?.statusCode !== 404,
145
+ }), () => ws.close())
141
146
  } catch {
142
147
  ws.close()
143
148
  }