cli-claw-kit 0.0.1

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 (295) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +245 -0
  3. package/config/default-groups.json +1 -0
  4. package/config/global-agents-md.template.md +37 -0
  5. package/config/mount-allowlist.json +11 -0
  6. package/container/Dockerfile +160 -0
  7. package/container/agent-runner/dist/.tsbuildinfo +1 -0
  8. package/container/agent-runner/dist/agent-definitions.js +22 -0
  9. package/container/agent-runner/dist/channel-prefixes.js +16 -0
  10. package/container/agent-runner/dist/codex-config.js +29 -0
  11. package/container/agent-runner/dist/image-detector.js +96 -0
  12. package/container/agent-runner/dist/index.js +2587 -0
  13. package/container/agent-runner/dist/mcp-tools.js +1076 -0
  14. package/container/agent-runner/dist/stream-event.types.js +5 -0
  15. package/container/agent-runner/dist/stream-processor.js +867 -0
  16. package/container/agent-runner/dist/types.js +6 -0
  17. package/container/agent-runner/dist/utils.js +115 -0
  18. package/container/agent-runner/package.json +36 -0
  19. package/container/agent-runner/prompts/security-rules.md +31 -0
  20. package/container/agent-runner/src/agent-definitions.ts +27 -0
  21. package/container/agent-runner/src/channel-prefixes.ts +16 -0
  22. package/container/agent-runner/src/codex-config.ts +40 -0
  23. package/container/agent-runner/src/image-detector.ts +116 -0
  24. package/container/agent-runner/src/index.ts +3107 -0
  25. package/container/agent-runner/src/mcp-tools.ts +1295 -0
  26. package/container/agent-runner/src/stream-event.types.ts +10 -0
  27. package/container/agent-runner/src/stream-processor.ts +932 -0
  28. package/container/agent-runner/src/types.ts +75 -0
  29. package/container/agent-runner/src/utils.ts +114 -0
  30. package/container/agent-runner/tsconfig.json +17 -0
  31. package/container/build.sh +28 -0
  32. package/container/entrypoint.sh +64 -0
  33. package/container/skills/agent-browser/SKILL.md +159 -0
  34. package/container/skills/install-skill/SKILL.md +64 -0
  35. package/container/skills/post-test-cleanup/SKILL.md +121 -0
  36. package/dist/.tsbuildinfo +1 -0
  37. package/dist/agent-output-parser.js +459 -0
  38. package/dist/app-root.js +52 -0
  39. package/dist/assistant-meta-footer.js +1 -0
  40. package/dist/auth.js +91 -0
  41. package/dist/billing.js +694 -0
  42. package/dist/channel-prefixes.js +16 -0
  43. package/dist/cli.js +86 -0
  44. package/dist/commands.js +79 -0
  45. package/dist/config.js +120 -0
  46. package/dist/container-runner.js +981 -0
  47. package/dist/daily-summary.js +210 -0
  48. package/dist/db.js +3683 -0
  49. package/dist/dingtalk.js +1347 -0
  50. package/dist/feishu-markdown-style.js +97 -0
  51. package/dist/feishu-streaming-card.js +1875 -0
  52. package/dist/feishu.js +1628 -0
  53. package/dist/file-manager.js +270 -0
  54. package/dist/group-queue.js +1070 -0
  55. package/dist/group-runtime.js +35 -0
  56. package/dist/host-workspace-cwd.js +85 -0
  57. package/dist/im-channel.js +384 -0
  58. package/dist/im-command-utils.js +142 -0
  59. package/dist/im-downloader.js +45 -0
  60. package/dist/im-manager.js +527 -0
  61. package/dist/im-utils.js +53 -0
  62. package/dist/image-detector.js +96 -0
  63. package/dist/index.js +5828 -0
  64. package/dist/logger.js +22 -0
  65. package/dist/mcp-utils.js +66 -0
  66. package/dist/message-attachments.js +69 -0
  67. package/dist/message-notifier.js +36 -0
  68. package/dist/middleware/auth.js +85 -0
  69. package/dist/mount-security.js +315 -0
  70. package/dist/permissions.js +67 -0
  71. package/dist/project-memory.js +6 -0
  72. package/dist/provider-pool.js +189 -0
  73. package/dist/qq.js +826 -0
  74. package/dist/reset-admin.js +42 -0
  75. package/dist/routes/admin.js +543 -0
  76. package/dist/routes/agent-definitions.js +241 -0
  77. package/dist/routes/agents.js +533 -0
  78. package/dist/routes/auth.js +675 -0
  79. package/dist/routes/billing.js +490 -0
  80. package/dist/routes/browse.js +210 -0
  81. package/dist/routes/bug-report.js +387 -0
  82. package/dist/routes/config.js +1868 -0
  83. package/dist/routes/files.js +671 -0
  84. package/dist/routes/groups.js +1367 -0
  85. package/dist/routes/mcp-servers.js +320 -0
  86. package/dist/routes/memory.js +523 -0
  87. package/dist/routes/monitor.js +307 -0
  88. package/dist/routes/skills.js +777 -0
  89. package/dist/routes/tasks.js +509 -0
  90. package/dist/routes/usage.js +64 -0
  91. package/dist/routes/workspace-config.js +458 -0
  92. package/dist/runtime-build.js +112 -0
  93. package/dist/runtime-command-handler.js +189 -0
  94. package/dist/runtime-command-registry.js +1 -0
  95. package/dist/runtime-config.js +1777 -0
  96. package/dist/runtime-identity.js +52 -0
  97. package/dist/schemas.js +590 -0
  98. package/dist/script-runner.js +64 -0
  99. package/dist/sdk-query.js +82 -0
  100. package/dist/skill-utils.js +145 -0
  101. package/dist/sqlite-compat.js +19 -0
  102. package/dist/stream-event.types.js +5 -0
  103. package/dist/streaming-runtime-meta.js +29 -0
  104. package/dist/task-scheduler.js +695 -0
  105. package/dist/task-utils.js +13 -0
  106. package/dist/telegram-pairing.js +59 -0
  107. package/dist/telegram.js +897 -0
  108. package/dist/terminal-manager.js +307 -0
  109. package/dist/tool-step-display.js +1 -0
  110. package/dist/types.js +1 -0
  111. package/dist/utils.js +85 -0
  112. package/dist/web-context.js +161 -0
  113. package/dist/web.js +1377 -0
  114. package/dist/wechat-crypto.js +182 -0
  115. package/dist/wechat.js +589 -0
  116. package/dist/workspace-runtime-reset.js +35 -0
  117. package/package.json +107 -0
  118. package/shared/assistant-meta-footer.ts +127 -0
  119. package/shared/channel-prefixes.ts +16 -0
  120. package/shared/dist/assistant-meta-footer.d.ts +29 -0
  121. package/shared/dist/assistant-meta-footer.js +85 -0
  122. package/shared/dist/channel-prefixes.d.ts +4 -0
  123. package/shared/dist/channel-prefixes.js +16 -0
  124. package/shared/dist/image-detector.d.ts +20 -0
  125. package/shared/dist/image-detector.js +96 -0
  126. package/shared/dist/runtime-command-registry.d.ts +38 -0
  127. package/shared/dist/runtime-command-registry.js +185 -0
  128. package/shared/dist/stream-event.d.ts +65 -0
  129. package/shared/dist/stream-event.js +8 -0
  130. package/shared/dist/tool-step-display.d.ts +4 -0
  131. package/shared/dist/tool-step-display.js +11 -0
  132. package/shared/image-detector.ts +116 -0
  133. package/shared/runtime-command-registry.ts +252 -0
  134. package/shared/stream-event.ts +67 -0
  135. package/shared/tool-step-display.ts +21 -0
  136. package/shared/tsconfig.json +24 -0
  137. package/web/dist/assets/BillingPage-B1wBR_o-.js +52 -0
  138. package/web/dist/assets/ChatPage-6GBZ9nXN.css +32 -0
  139. package/web/dist/assets/ChatPage-BOJcXtaj.js +161 -0
  140. package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  141. package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  142. package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  143. package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  144. package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  145. package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  146. package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  147. package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  148. package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  149. package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  150. package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  151. package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  152. package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  153. package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  154. package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  155. package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  156. package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  157. package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  158. package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  159. package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  160. package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  161. package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  162. package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  163. package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  164. package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  165. package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  166. package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  167. package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  168. package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  169. package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  170. package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  171. package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  172. package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  173. package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  174. package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  175. package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  176. package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  177. package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  178. package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  179. package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  180. package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  181. package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  182. package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  183. package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  184. package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  185. package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  186. package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  187. package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  188. package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  189. package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  190. package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  191. package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  192. package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  193. package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  194. package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  195. package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  196. package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  197. package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  198. package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  199. package/web/dist/assets/SettingsPage-DoY7FoZ_.js +153 -0
  200. package/web/dist/assets/ShareImageDialog-C1ga8b7l.js +22 -0
  201. package/web/dist/assets/TasksPage-CRivnNsx.js +14 -0
  202. package/web/dist/assets/_basePickBy-Bf-bSoS9.js +1 -0
  203. package/web/dist/assets/_baseUniq-zAOaCuKw.js +1 -0
  204. package/web/dist/assets/arc-Dm9mVQ9U.js +1 -0
  205. package/web/dist/assets/architectureDiagram-2XIMDMQ5-BLmzX1wr.js +36 -0
  206. package/web/dist/assets/band-CquvqAHh.js +1 -0
  207. package/web/dist/assets/blockDiagram-WCTKOSBZ-B9pcqm3j.js +132 -0
  208. package/web/dist/assets/c4Diagram-IC4MRINW-Cytx1q3b.js +10 -0
  209. package/web/dist/assets/channel-BOVj73LR.js +1 -0
  210. package/web/dist/assets/channel-meta-CQD0Pei-.js +41 -0
  211. package/web/dist/assets/chunk-4BX2VUAB-0ToDr6RE.js +1 -0
  212. package/web/dist/assets/chunk-55IACEB6-DQDjnXfS.js +1 -0
  213. package/web/dist/assets/chunk-FMBD7UC4-Di8ABm6c.js +15 -0
  214. package/web/dist/assets/chunk-JSJVCQXG-BZQN6rnX.js +1 -0
  215. package/web/dist/assets/chunk-KX2RTZJC-zBbcpaN_.js +1 -0
  216. package/web/dist/assets/chunk-NQ4KR5QH-BCrLoU88.js +220 -0
  217. package/web/dist/assets/chunk-QZHKN3VN-Bqk8juan.js +1 -0
  218. package/web/dist/assets/chunk-WL4C6EOR-D2YX-MHY.js +189 -0
  219. package/web/dist/assets/classDiagram-VBA2DB6C-DUUoMyaK.js +1 -0
  220. package/web/dist/assets/classDiagram-v2-RAHNMMFH-DUUoMyaK.js +1 -0
  221. package/web/dist/assets/clone-BmaCesfa.js +1 -0
  222. package/web/dist/assets/cose-bilkent-S5V4N54A-CTsv6qQA.js +1 -0
  223. package/web/dist/assets/cytoscape.esm-BQaXIfA_.js +331 -0
  224. package/web/dist/assets/dagre-KLK3FWXG-Ci4Jh9nu.js +4 -0
  225. package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  226. package/web/dist/assets/diagram-E7M64L7V-BFRnfTI2.js +24 -0
  227. package/web/dist/assets/diagram-IFDJBPK2-B7Zhnp0b.js +43 -0
  228. package/web/dist/assets/diagram-P4PSJMXO-BVyP7nwq.js +24 -0
  229. package/web/dist/assets/erDiagram-INFDFZHY-NorKdTOF.js +70 -0
  230. package/web/dist/assets/error-CGD5mp5f.js +1 -0
  231. package/web/dist/assets/flowDiagram-PKNHOUZH-Ch97nABF.js +162 -0
  232. package/web/dist/assets/ganttDiagram-A5KZAMGK-BQ2pLWsy.js +292 -0
  233. package/web/dist/assets/gitGraphDiagram-K3NZZRJ6-bcvnBsD2.js +65 -0
  234. package/web/dist/assets/graph-CeAEckur.js +1 -0
  235. package/web/dist/assets/index-CPnL1_qC.js +768 -0
  236. package/web/dist/assets/index-DVevCbcO.css +10 -0
  237. package/web/dist/assets/infoDiagram-LFFYTUFH-CcsrFdj-.js +2 -0
  238. package/web/dist/assets/init-Dmth1JHB.js +1 -0
  239. package/web/dist/assets/ishikawaDiagram-PHBUUO56-1upyMfHN.js +70 -0
  240. package/web/dist/assets/journeyDiagram-4ABVD52K-CKUi-V0c.js +139 -0
  241. package/web/dist/assets/kanban-definition-K7BYSVSG-DOnQwXfL.js +89 -0
  242. package/web/dist/assets/layout-BmMMqTnJ.js +1 -0
  243. package/web/dist/assets/linear-DiaJloY5.js +1 -0
  244. package/web/dist/assets/mermaid.core-BWLV1B2v.js +254 -0
  245. package/web/dist/assets/mindmap-definition-YRQLILUH-BeAKHVWP.js +68 -0
  246. package/web/dist/assets/ordinal-DILIJJjt.js +1 -0
  247. package/web/dist/assets/pieDiagram-SKSYHLDU-DfiMSfWo.js +30 -0
  248. package/web/dist/assets/quadrantDiagram-337W2JSQ-wZxZOJxd.js +7 -0
  249. package/web/dist/assets/requirementDiagram-Z7DCOOCP-BK4HHm17.js +73 -0
  250. package/web/dist/assets/sankeyDiagram-WA2Y5GQK-BX6t2avX.js +10 -0
  251. package/web/dist/assets/sequenceDiagram-2WXFIKYE-BPQlkbAa.js +145 -0
  252. package/web/dist/assets/sheet-rI0FfB1g.js +6 -0
  253. package/web/dist/assets/sliders-horizontal-CuijWFNK.js +6 -0
  254. package/web/dist/assets/sparkles-BsMYXJoT.js +11 -0
  255. package/web/dist/assets/square-0CqMX1Q3.js +11 -0
  256. package/web/dist/assets/stateDiagram-RAJIS63D-DxkV0Vwd.js +1 -0
  257. package/web/dist/assets/stateDiagram-v2-FVOUBMTO-qLYoiOPe.js +1 -0
  258. package/web/dist/assets/step-D51IIHGA.js +1 -0
  259. package/web/dist/assets/tasks-D8JjBTwx.js +1 -0
  260. package/web/dist/assets/time-O8zIGux3.js +1 -0
  261. package/web/dist/assets/timeline-definition-YZTLITO2-kNp1DyFc.js +61 -0
  262. package/web/dist/assets/treemap-KZPCXAKY-CkrClVhk.js +162 -0
  263. package/web/dist/assets/utils-KGAn0XTg.js +11 -0
  264. package/web/dist/assets/vennDiagram-LZ73GAT5-CgdzEZz4.js +34 -0
  265. package/web/dist/assets/xychartDiagram-JWTSCODW-DfYGPfNB.js +7 -0
  266. package/web/dist/assets/zap-_hKJYy7J.js +6 -0
  267. package/web/dist/favicon.svg +332 -0
  268. package/web/dist/fonts/AlibabaPuHuiTi-3-55-Regular.woff2 +0 -0
  269. package/web/dist/fonts/AlibabaPuHuiTi-3-65-Medium.woff2 +0 -0
  270. package/web/dist/fonts/AlibabaPuHuiTi-3-75-SemiBold.woff2 +0 -0
  271. package/web/dist/fonts/DMSans-latin-ext.woff2 +0 -0
  272. package/web/dist/fonts/DMSans-latin.woff2 +0 -0
  273. package/web/dist/icons/README.md +20 -0
  274. package/web/dist/icons/apple-touch-icon-180.png +0 -0
  275. package/web/dist/icons/icon-128.png +0 -0
  276. package/web/dist/icons/icon-144.png +0 -0
  277. package/web/dist/icons/icon-152.png +0 -0
  278. package/web/dist/icons/icon-192.png +0 -0
  279. package/web/dist/icons/icon-192.svg +332 -0
  280. package/web/dist/icons/icon-384.png +0 -0
  281. package/web/dist/icons/icon-48.png +0 -0
  282. package/web/dist/icons/icon-512-maskable.png +0 -0
  283. package/web/dist/icons/icon-512.png +0 -0
  284. package/web/dist/icons/icon-512.svg +332 -0
  285. package/web/dist/icons/icon-72.png +0 -0
  286. package/web/dist/icons/icon-96.png +0 -0
  287. package/web/dist/icons/loading-logo.svg +332 -0
  288. package/web/dist/icons/logo-1024.png +0 -0
  289. package/web/dist/icons/logo-icon.svg +332 -0
  290. package/web/dist/icons/logo-text.svg +332 -0
  291. package/web/dist/index.html +30 -0
  292. package/web/dist/manifest.webmanifest +1 -0
  293. package/web/dist/registerSW.js +1 -0
  294. package/web/dist/sw.js +1 -0
  295. package/web/dist/workbox-08d6266a.js +1 -0
@@ -0,0 +1,459 @@
1
+ /**
2
+ * Shared output parsing and process lifecycle logic for container-runner.
3
+ * Extracted from runContainerAgent() and runHostAgent() to eliminate duplication.
4
+ */
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import { getSystemSettings } from './runtime-config.js';
8
+ import { logger } from './logger.js';
9
+ // Sentinel markers for robust output parsing (must match agent-runner)
10
+ export const OUTPUT_START_MARKER = '---CLI_CLAW_OUTPUT_START---';
11
+ export const OUTPUT_END_MARKER = '---CLI_CLAW_OUTPUT_END---';
12
+ export function createStdoutParserState() {
13
+ return {
14
+ stdout: '',
15
+ stdoutTruncated: false,
16
+ parseBuffer: '',
17
+ newSessionId: undefined,
18
+ outputChain: Promise.resolve(),
19
+ lastErrorOutput: null,
20
+ hasSuccessOutput: false,
21
+ hasClosedOutput: false,
22
+ hasInterruptedOutput: false,
23
+ };
24
+ }
25
+ export function attachStdoutHandler(stream, state, opts) {
26
+ stream.on('data', (data) => {
27
+ const chunk = data.toString();
28
+ // Always accumulate for logging
29
+ if (!state.stdoutTruncated) {
30
+ const remaining = getSystemSettings().containerMaxOutputSize - state.stdout.length;
31
+ if (chunk.length > remaining) {
32
+ state.stdout += chunk.slice(0, remaining);
33
+ state.stdoutTruncated = true;
34
+ logger.warn({ group: opts.groupName, size: state.stdout.length }, `${opts.label} stdout truncated due to size limit`);
35
+ }
36
+ else {
37
+ state.stdout += chunk;
38
+ }
39
+ }
40
+ // Stream-parse for output markers
41
+ if (opts.onOutput) {
42
+ state.parseBuffer += chunk;
43
+ const MAX_PARSE_BUFFER = 10 * 1024 * 1024; // 10MB
44
+ if (state.parseBuffer.length > MAX_PARSE_BUFFER) {
45
+ logger.warn({ group: opts.groupName }, 'Parse buffer overflow, truncating');
46
+ const lastMarkerIdx = state.parseBuffer.lastIndexOf(OUTPUT_START_MARKER);
47
+ state.parseBuffer =
48
+ lastMarkerIdx >= 0
49
+ ? state.parseBuffer.slice(lastMarkerIdx)
50
+ : state.parseBuffer.slice(-512);
51
+ }
52
+ let startIdx;
53
+ while ((startIdx = state.parseBuffer.indexOf(OUTPUT_START_MARKER)) !== -1) {
54
+ const endIdx = state.parseBuffer.indexOf(OUTPUT_END_MARKER, startIdx);
55
+ if (endIdx === -1)
56
+ break; // Incomplete pair, wait for more data
57
+ const jsonStr = state.parseBuffer
58
+ .slice(startIdx + OUTPUT_START_MARKER.length, endIdx)
59
+ .trim();
60
+ state.parseBuffer = state.parseBuffer.slice(endIdx + OUTPUT_END_MARKER.length);
61
+ try {
62
+ const parsed = JSON.parse(jsonStr);
63
+ if (parsed.newSessionId) {
64
+ state.newSessionId = parsed.newSessionId;
65
+ }
66
+ if (parsed.status === 'success') {
67
+ state.hasSuccessOutput = true;
68
+ }
69
+ if (parsed.status === 'error') {
70
+ state.lastErrorOutput = parsed;
71
+ }
72
+ if (parsed.status === 'closed') {
73
+ state.hasClosedOutput = true;
74
+ }
75
+ if (parsed.status === 'stream' &&
76
+ parsed.streamEvent?.statusText === 'interrupted') {
77
+ state.hasInterruptedOutput = true;
78
+ }
79
+ // Activity detected — reset the hard timeout
80
+ opts.resetTimeout();
81
+ // Call onOutput for all markers (including null results)
82
+ // so idle timers start even for "silent" query completions.
83
+ const onOutputFn = opts.onOutput;
84
+ state.outputChain = state.outputChain
85
+ .then(() => onOutputFn(parsed))
86
+ .catch((err) => {
87
+ logger.error({ group: opts.groupName, err }, 'onOutput callback error');
88
+ });
89
+ }
90
+ catch (err) {
91
+ logger.warn({ group: opts.groupName, error: err }, 'Failed to parse streamed output chunk');
92
+ }
93
+ }
94
+ }
95
+ });
96
+ }
97
+ export function createStderrState() {
98
+ return {
99
+ stderr: '',
100
+ stderrTruncated: false,
101
+ };
102
+ }
103
+ export function attachStderrHandler(stream, state, groupName,
104
+ /** Log context key: { container: folder } or { host: folder } */
105
+ logContext) {
106
+ stream.on('data', (data) => {
107
+ const chunk = data.toString();
108
+ const lines = chunk.trim().split('\n');
109
+ for (const line of lines) {
110
+ if (line)
111
+ logger.debug(logContext, line);
112
+ }
113
+ // Don't reset timeout on stderr — SDK writes debug logs continuously.
114
+ // Timeout only resets on actual output (OUTPUT_MARKER in stdout).
115
+ if (state.stderrTruncated)
116
+ return;
117
+ const remaining = getSystemSettings().containerMaxOutputSize - state.stderr.length;
118
+ if (chunk.length > remaining) {
119
+ state.stderr += chunk.slice(0, remaining);
120
+ state.stderrTruncated = true;
121
+ logger.warn({ group: groupName, size: state.stderr.length }, `${Object.keys(logContext)[0] === 'container' ? 'Container' : 'Host agent'} stderr truncated due to size limit`);
122
+ }
123
+ else {
124
+ state.stderr += chunk;
125
+ }
126
+ });
127
+ }
128
+ /**
129
+ * Handle the 'close' event for timeout case.
130
+ * Returns true if this was a timeout (caller should return early).
131
+ */
132
+ export function handleTimeoutClose(ctx, code, duration, timedOut) {
133
+ if (!timedOut)
134
+ return false;
135
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
136
+ fs.mkdirSync(ctx.logsDir, { recursive: true });
137
+ const timeoutLog = path.join(ctx.logsDir, `${ctx.filePrefix}-${ts}.log`);
138
+ fs.writeFileSync(timeoutLog, [
139
+ `=== ${ctx.label} Run Log (TIMEOUT) ===`,
140
+ `Timestamp: ${new Date().toISOString()}`,
141
+ `Group: ${ctx.groupName}`,
142
+ `${ctx.label === 'Container' ? 'Container' : 'Process ID'}: ${ctx.identifier}`,
143
+ `Duration: ${duration}ms`,
144
+ `Exit Code: ${code}`,
145
+ ].join('\n'));
146
+ logger.error({
147
+ group: ctx.groupName,
148
+ [ctx.filePrefix === 'container' ? 'containerName' : 'processId']: ctx.identifier,
149
+ duration,
150
+ code,
151
+ }, `${ctx.label} timed out`);
152
+ ctx.resolvePromise({
153
+ status: 'error',
154
+ result: null,
155
+ error: `${ctx.label} timed out after ${ctx.timeoutMs}ms`,
156
+ });
157
+ return true;
158
+ }
159
+ /**
160
+ * Write a run log file. Returns the log file path.
161
+ */
162
+ export function writeRunLog(ctx, code, duration) {
163
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
164
+ fs.mkdirSync(ctx.logsDir, { recursive: true });
165
+ const logFile = path.join(ctx.logsDir, `${ctx.filePrefix}-${timestamp}.log`);
166
+ const isVerbose = process.env.LOG_LEVEL === 'debug' || process.env.LOG_LEVEL === 'trace';
167
+ const logLines = [
168
+ `=== ${ctx.label} Run Log ===`,
169
+ `Timestamp: ${new Date().toISOString()}`,
170
+ `Group: ${ctx.groupName}`,
171
+ `IsMain: ${ctx.input.isMain}`,
172
+ `Duration: ${duration}ms`,
173
+ `Exit Code: ${code}`,
174
+ `Stdout Truncated: ${ctx.stdoutState.stdoutTruncated}`,
175
+ `Stderr Truncated: ${ctx.stderrState.stderrTruncated}`,
176
+ ``,
177
+ ];
178
+ const isError = code !== 0;
179
+ const { stderr, stderrTruncated } = ctx.stderrState;
180
+ const { stdout, stdoutTruncated } = ctx.stdoutState;
181
+ const LOG_TAIL_LIMIT = 4000;
182
+ const stderrLog = !isVerbose && !isError && stderr.length > LOG_TAIL_LIMIT
183
+ ? `... (truncated ${stderr.length - LOG_TAIL_LIMIT} chars) ...\n` +
184
+ stderr.slice(-LOG_TAIL_LIMIT)
185
+ : stderr;
186
+ const stdoutLog = !isVerbose && !isError && stdout.length > LOG_TAIL_LIMIT
187
+ ? `... (truncated ${stdout.length - LOG_TAIL_LIMIT} chars) ...\n` +
188
+ stdout.slice(-LOG_TAIL_LIMIT)
189
+ : stdout;
190
+ logLines.push(`=== Input Summary ===`, `Prompt length: ${ctx.input.prompt.length} chars`, `Session ID: ${ctx.input.sessionId || 'new'}`);
191
+ if (ctx.agentIdentity?.chatJid) {
192
+ logLines.push(`Chat JID: ${ctx.agentIdentity.chatJid}`);
193
+ }
194
+ if (ctx.agentIdentity?.groupFolder) {
195
+ logLines.push(`Group Folder: ${ctx.agentIdentity.groupFolder}`);
196
+ }
197
+ if (ctx.agentIdentity?.agentType) {
198
+ logLines.push(`Agent Type: ${ctx.agentIdentity.agentType}`);
199
+ }
200
+ if (ctx.agentIdentity?.executionMode) {
201
+ logLines.push(`Execution Mode: ${ctx.agentIdentity.executionMode}`);
202
+ }
203
+ if (ctx.agentIdentity?.selectedRunner) {
204
+ logLines.push(`Selected Runner: ${ctx.agentIdentity.selectedRunner}`);
205
+ }
206
+ if (ctx.agentIdentity?.agentId) {
207
+ logLines.push(`Agent ID: ${ctx.agentIdentity.agentId}`);
208
+ }
209
+ if (ctx.runtimeBuildInfo) {
210
+ logLines.push(`Backend PID: ${ctx.runtimeBuildInfo.backendPid}`, `Backend Started At: ${ctx.runtimeBuildInfo.backendStartedAt}`, `Backend Build Loaded: ${ctx.runtimeBuildInfo.backendBuildLoaded}`, `Backend Build Current: ${ctx.runtimeBuildInfo.backendBuildCurrent}`, `Backend Build Stale: ${ctx.runtimeBuildInfo.backendBuildStale}`, `Agent Runner Build Loaded: ${ctx.runtimeBuildInfo.agentRunnerBuildLoaded}`, `Agent Runner Build Current: ${ctx.runtimeBuildInfo.agentRunnerBuildCurrent}`, `Agent Runner Build Stale: ${ctx.runtimeBuildInfo.agentRunnerBuildStale}`);
211
+ }
212
+ if (ctx.extraSummaryLines) {
213
+ logLines.push(...ctx.extraSummaryLines);
214
+ }
215
+ logLines.push(``, `=== Stderr${stderrTruncated ? ' (TRUNCATED)' : ''} ===`, stderrLog, ``, `=== Stdout${stdoutTruncated ? ' (TRUNCATED)' : ''} ===`, stdoutLog);
216
+ if (isVerbose || isError) {
217
+ logLines.push(``, `=== Input ===`, JSON.stringify(ctx.input, null, 2));
218
+ if (ctx.extraVerboseLines) {
219
+ logLines.push(``, ...ctx.extraVerboseLines);
220
+ }
221
+ }
222
+ fs.writeFileSync(logFile, logLines.join('\n'));
223
+ logger.debug({ logFile, verbose: isVerbose }, `${ctx.label} log written`);
224
+ return logFile;
225
+ }
226
+ const OUTPUT_CHAIN_TIMEOUT = 30_000;
227
+ /**
228
+ * Wait for the output chain to settle with a safety timeout.
229
+ * Calls `then` callback on success, always ensures chain timer is cleaned up.
230
+ */
231
+ function waitForOutputChain(outputChain, groupName, logLabel, then) {
232
+ let chainTimer = null;
233
+ const chainTimeout = new Promise((resolve) => {
234
+ chainTimer = setTimeout(() => {
235
+ logger.warn({ group: groupName, timeoutMs: OUTPUT_CHAIN_TIMEOUT }, `Output chain settle timeout on ${logLabel}`);
236
+ resolve();
237
+ }, OUTPUT_CHAIN_TIMEOUT);
238
+ });
239
+ Promise.race([outputChain, chainTimeout])
240
+ .then(() => {
241
+ if (chainTimer)
242
+ clearTimeout(chainTimer);
243
+ then();
244
+ })
245
+ .catch(() => {
246
+ if (chainTimer)
247
+ clearTimeout(chainTimer);
248
+ then();
249
+ });
250
+ }
251
+ /**
252
+ * Handle the non-zero exit code path (force-kill detection, error output chain, resolve).
253
+ * Returns true if handled (caller should return early).
254
+ */
255
+ export function handleNonZeroExit(ctx, code, signal, duration, logFile) {
256
+ if (code === 0)
257
+ return false;
258
+ const exitLabel = code === null ? `signal ${signal || 'unknown'}` : `code ${code}`;
259
+ const { newSessionId, outputChain } = ctx.stdoutState;
260
+ // Graceful interrupt: agent emitted 'interrupted' status before exiting.
261
+ if (ctx.stdoutState.hasInterruptedOutput && ctx.onOutput) {
262
+ logger.info({ group: ctx.groupName, code, signal, duration, newSessionId }, `${ctx.label} exited after interrupt (treating as success)`);
263
+ waitForOutputChain(outputChain, ctx.groupName, `${ctx.filePrefix} interrupt path`, () => {
264
+ ctx.resolvePromise({ status: 'success', result: null, newSessionId });
265
+ });
266
+ return true;
267
+ }
268
+ // Graceful shutdown: agent was killed by SIGTERM/SIGKILL (e.g. user
269
+ // clicked stop, session reset, clear-history). Treat as normal
270
+ // completion instead of an error — BUT only if the agent had already
271
+ // produced some output. If killed before emitting ANY output markers
272
+ // (success/closed), it means the process died during initialization
273
+ // (e.g., race condition) and should be treated as an error so the UI
274
+ // waiting state gets cleared via sendSystemMessage('agent_error').
275
+ const isForceKilled = signal === 'SIGTERM' || signal === 'SIGKILL' || code === 137;
276
+ if (isForceKilled && ctx.onOutput) {
277
+ const hadOutput = ctx.stdoutState.hasSuccessOutput || ctx.stdoutState.hasClosedOutput;
278
+ if (hadOutput) {
279
+ logger.info({ group: ctx.groupName, signal, code, duration, newSessionId }, `${ctx.label} terminated by signal (user stop / graceful shutdown)`);
280
+ waitForOutputChain(outputChain, ctx.groupName, `${ctx.filePrefix} force-kill path`, () => {
281
+ ctx.resolvePromise({
282
+ status: 'success',
283
+ result: null,
284
+ newSessionId,
285
+ });
286
+ });
287
+ return true;
288
+ }
289
+ // Agent was killed before producing any output — fall through to
290
+ // error path so the caller can broadcast an error and clear the UI.
291
+ logger.warn({ group: ctx.groupName, signal, code, duration }, `${ctx.label} killed before producing any output — treating as error`);
292
+ }
293
+ // Build error output
294
+ const { stderr } = ctx.stderrState;
295
+ const enriched = ctx.enrichError
296
+ ? ctx.enrichError(stderr, exitLabel)
297
+ : {
298
+ result: null,
299
+ error: `${ctx.label} exited with ${exitLabel}: ${stderr.slice(-200)}`,
300
+ };
301
+ logger.error({
302
+ group: ctx.groupName,
303
+ code,
304
+ signal,
305
+ duration,
306
+ stderr,
307
+ stdout: ctx.stdoutState.stdout,
308
+ logFile,
309
+ }, `${ctx.label} exited with error`);
310
+ const finalizeError = () => {
311
+ if (ctx.stdoutState.lastErrorOutput) {
312
+ const streamedError = ctx.stdoutState.lastErrorOutput;
313
+ ctx.resolvePromise({
314
+ ...streamedError,
315
+ result: streamedError.result ?? enriched.result,
316
+ error: streamedError.error || enriched.error,
317
+ finalizationReason: streamedError.finalizationReason || 'error',
318
+ alreadyStreamedError: true,
319
+ });
320
+ return;
321
+ }
322
+ ctx.resolvePromise({
323
+ status: 'error',
324
+ result: enriched.result,
325
+ error: enriched.error,
326
+ });
327
+ };
328
+ // Even on error exits, wait for pending output callbacks to settle.
329
+ if (ctx.onOutput) {
330
+ waitForOutputChain(outputChain, ctx.groupName, `${ctx.filePrefix} error path`, finalizeError);
331
+ return true;
332
+ }
333
+ finalizeError();
334
+ return true;
335
+ }
336
+ /**
337
+ * Handle the success (code === 0) path — streaming mode or legacy parsing.
338
+ */
339
+ export function handleSuccessClose(ctx, duration) {
340
+ const { newSessionId, outputChain } = ctx.stdoutState;
341
+ // Streaming mode: wait for output chain to settle
342
+ if (ctx.onOutput) {
343
+ const { hasClosedOutput } = ctx.stdoutState;
344
+ waitForOutputChain(outputChain, ctx.groupName, `${ctx.filePrefix} success path`, () => {
345
+ // Propagate 'closed' status so the host can distinguish a _close-interrupted
346
+ // exit from a normal completion and avoid committing the message cursor.
347
+ const finalStatus = hasClosedOutput
348
+ ? 'closed'
349
+ : 'success';
350
+ logger.info({ group: ctx.groupName, duration, newSessionId, finalStatus }, `${ctx.label} completed (streaming mode)`);
351
+ ctx.resolvePromise({
352
+ status: finalStatus,
353
+ result: null,
354
+ newSessionId,
355
+ });
356
+ });
357
+ return;
358
+ }
359
+ // Legacy mode: parse the last output marker pair from accumulated stdout
360
+ parseLegacyOutput(ctx);
361
+ }
362
+ /**
363
+ * Parse legacy (non-streaming) output from accumulated stdout.
364
+ */
365
+ function parseLegacyOutput(ctx) {
366
+ const { stdout } = ctx.stdoutState;
367
+ try {
368
+ const startIdx = stdout.indexOf(OUTPUT_START_MARKER);
369
+ const endIdx = stdout.indexOf(OUTPUT_END_MARKER);
370
+ let jsonLine;
371
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
372
+ jsonLine = stdout
373
+ .slice(startIdx + OUTPUT_START_MARKER.length, endIdx)
374
+ .trim();
375
+ }
376
+ else {
377
+ // Fallback: last non-empty line (backwards compatibility)
378
+ const lines = stdout.trim().split('\n');
379
+ jsonLine = lines[lines.length - 1];
380
+ }
381
+ const output = JSON.parse(jsonLine);
382
+ logger.info({
383
+ group: ctx.groupName,
384
+ duration: Date.now() - ctx.startTime,
385
+ status: output.status,
386
+ hasResult: !!output.result,
387
+ }, `${ctx.label} completed`);
388
+ ctx.resolvePromise(output);
389
+ }
390
+ catch (err) {
391
+ logger.error({
392
+ group: ctx.groupName,
393
+ stdout,
394
+ stderr: ctx.stderrState.stderr,
395
+ error: err,
396
+ }, `Failed to parse ${ctx.filePrefix} output`);
397
+ ctx.resolvePromise({
398
+ status: 'error',
399
+ result: null,
400
+ error: `Failed to parse ${ctx.filePrefix} output: ${err instanceof Error ? err.message : String(err)}`,
401
+ });
402
+ }
403
+ }
404
+ // ─── API Error Classification ────────────────────────────────────────
405
+ function normalizeRuntimeErrorText(stderr) {
406
+ return stderr
407
+ .replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, ' ')
408
+ .replace(/\s+/g, ' ')
409
+ .trim();
410
+ }
411
+ export function formatUserFacingRuntimeError(stderr) {
412
+ const normalized = normalizeRuntimeErrorText(stderr);
413
+ if (!normalized)
414
+ return null;
415
+ if (/Codex CLI 未登录/u.test(normalized) ||
416
+ /auth_required|login required|please login|not logged in/i.test(normalized)) {
417
+ return 'Codex CLI 未登录。请先在服务器上执行:codex login';
418
+ }
419
+ if (/UsageLimitExceeded/i.test(normalized) ||
420
+ /purchase more credits/i.test(normalized) ||
421
+ /https:\/\/chatgpt\.com\/codex\/settings\/usage/i.test(normalized)) {
422
+ const usageUrl = normalized.match(/https:\/\/chatgpt\.com\/codex\/settings\/usage/i)?.[0] || 'https://chatgpt.com/codex/settings/usage';
423
+ const retryAt = normalized.match(/try again at ([^.]+)\.?/i)?.[1]?.trim();
424
+ return retryAt
425
+ ? `Codex CLI 用量已用尽。请前往 ${usageUrl} 购买额度,或在 ${retryAt} 后重试。`
426
+ : `Codex CLI 用量已用尽。请前往 ${usageUrl} 购买额度,或稍后重试。`;
427
+ }
428
+ return null;
429
+ }
430
+ /** Patterns that indicate an API-level error (provider issue, not user code bug) */
431
+ const API_ERROR_PATTERNS = [
432
+ /\bapi[_ ]?key\b.*\b(invalid|missing|expired|required)\b/i,
433
+ /\bauthentication\s+(failed|error|required)\b/i,
434
+ /\b(401|403)\b.*\bunauthorized\b/i,
435
+ /\brate[_ ]?limit(ed)?\b/i,
436
+ /\bquota\s+(exceeded|exhausted)\b/i,
437
+ /\boverloaded\b/i,
438
+ /\binternal\s+server\s+error\b/i,
439
+ /\b(502|503|504|529)\b/,
440
+ /ANTHROPIC_API_KEY/,
441
+ /ANTHROPIC_AUTH_TOKEN/,
442
+ /\binvalid[_ ]?api\b/i,
443
+ /\bbilling\s+(error|issue|limit)\b/i,
444
+ /\bcredit(s)?\s+(exhausted|insufficient)\b/i,
445
+ /connection\s*(refused|reset|timed?\s*out)/i,
446
+ /ECONNREFUSED|ECONNRESET|ETIMEDOUT/,
447
+ ];
448
+ /**
449
+ * Classify whether stderr output indicates an API-level error
450
+ * (provider unreachable, auth failure, rate limit, etc.)
451
+ * vs a normal agent exit or user code issue.
452
+ *
453
+ * Used by container-runner to decide whether to report failure to ProviderPool.
454
+ */
455
+ export function isApiError(stderr) {
456
+ if (!stderr)
457
+ return false;
458
+ return API_ERROR_PATTERNS.some((pattern) => pattern.test(stderr));
459
+ }
@@ -0,0 +1,52 @@
1
+ import fs from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ function toModuleDirectory(moduleLocation) {
6
+ const resolvedLocation = moduleLocation.startsWith('file:')
7
+ ? fileURLToPath(moduleLocation)
8
+ : path.resolve(moduleLocation);
9
+ try {
10
+ if (fs.statSync(resolvedLocation).isDirectory()) {
11
+ return resolvedLocation;
12
+ }
13
+ }
14
+ catch {
15
+ // Fall back to treating the input as a file path.
16
+ }
17
+ return path.dirname(resolvedLocation);
18
+ }
19
+ export function findPackageRoot(moduleLocation) {
20
+ let currentDir = toModuleDirectory(moduleLocation);
21
+ while (true) {
22
+ const packageJsonPath = path.join(currentDir, 'package.json');
23
+ if (fs.existsSync(packageJsonPath)) {
24
+ return currentDir;
25
+ }
26
+ const parentDir = path.dirname(currentDir);
27
+ if (parentDir === currentDir) {
28
+ throw new Error(`Unable to locate package.json from ${moduleLocation}`);
29
+ }
30
+ currentDir = parentDir;
31
+ }
32
+ }
33
+ export const PACKAGE_ROOT = findPackageRoot(import.meta.url);
34
+ export const APP_ROOT = PACKAGE_ROOT;
35
+ export const LAUNCH_CWD = (() => {
36
+ try {
37
+ return fs.realpathSync(process.cwd());
38
+ }
39
+ catch {
40
+ return path.resolve(process.cwd());
41
+ }
42
+ })();
43
+ export function resolveAppPath(...segments) {
44
+ return path.join(APP_ROOT, ...segments);
45
+ }
46
+ export function resolvePackagePath(...segments) {
47
+ return path.join(PACKAGE_ROOT, ...segments);
48
+ }
49
+ export function resolvePackageDependency(specifier) {
50
+ const packageRequire = createRequire(resolvePackagePath('package.json'));
51
+ return packageRequire.resolve(specifier);
52
+ }
@@ -0,0 +1 @@
1
+ export { formatAssistantMetaFooter, formatCompactNumber, getAssistantMetaFooterParts, parseAssistantTokenUsage, } from '../shared/dist/assistant-meta-footer.js';
package/dist/auth.js ADDED
@@ -0,0 +1,91 @@
1
+ import crypto from 'crypto';
2
+ import bcrypt from 'bcryptjs';
3
+ const BCRYPT_ROUNDS = 12;
4
+ // --- Password hashing ---
5
+ export async function hashPassword(password) {
6
+ return bcrypt.hash(password, BCRYPT_ROUNDS);
7
+ }
8
+ export async function verifyPassword(password, hash) {
9
+ return bcrypt.compare(password, hash);
10
+ }
11
+ // --- Session token generation ---
12
+ export function generateSessionToken() {
13
+ return crypto.randomBytes(32).toString('hex');
14
+ }
15
+ export function generateUserId() {
16
+ return crypto.randomUUID();
17
+ }
18
+ export function generateInviteCode() {
19
+ return crypto.randomBytes(16).toString('hex');
20
+ }
21
+ // --- Input validation ---
22
+ const USERNAME_RE = /^[a-zA-Z0-9_]{3,32}$/;
23
+ const PASSWORD_MIN = 8;
24
+ const PASSWORD_MAX = 128;
25
+ export function validateUsername(username) {
26
+ if (!username || typeof username !== 'string')
27
+ return '用户名不能为空';
28
+ if (!USERNAME_RE.test(username))
29
+ return '用户名须为3-32位字母、数字或下划线';
30
+ return null;
31
+ }
32
+ export function validatePassword(password) {
33
+ if (!password || typeof password !== 'string')
34
+ return '密码不能为空';
35
+ if (password.length < PASSWORD_MIN)
36
+ return `密码长度不能少于${PASSWORD_MIN}位`;
37
+ if (password.length > PASSWORD_MAX)
38
+ return `密码长度不能超过${PASSWORD_MAX}位`;
39
+ return null;
40
+ }
41
+ const loginAttempts = new Map();
42
+ // Sliding window: clean old entries every 10 minutes
43
+ setInterval(() => {
44
+ const now = Date.now();
45
+ for (const [key, record] of loginAttempts) {
46
+ // Remove entries older than lockout period * 2
47
+ if (now - record.lastAttempt > 30 * 60 * 1000) {
48
+ loginAttempts.delete(key);
49
+ }
50
+ }
51
+ }, 10 * 60 * 1000);
52
+ export function checkLoginRateLimit(username, ip, maxAttempts, lockoutMinutes) {
53
+ const key = `${username}:${ip}`;
54
+ const now = Date.now();
55
+ const windowMs = lockoutMinutes * 60 * 1000;
56
+ const record = loginAttempts.get(key);
57
+ if (!record)
58
+ return { allowed: true };
59
+ // Reset if window has passed since first attempt
60
+ if (now - record.firstAttempt > windowMs) {
61
+ loginAttempts.delete(key);
62
+ return { allowed: true };
63
+ }
64
+ if (record.count >= maxAttempts) {
65
+ const retryAfter = Math.ceil((record.firstAttempt + windowMs - now) / 1000);
66
+ return { allowed: false, retryAfterSeconds: Math.max(1, retryAfter) };
67
+ }
68
+ return { allowed: true };
69
+ }
70
+ export function recordLoginAttempt(username, ip) {
71
+ const key = `${username}:${ip}`;
72
+ const now = Date.now();
73
+ const record = loginAttempts.get(key);
74
+ if (record) {
75
+ record.count += 1;
76
+ record.lastAttempt = now;
77
+ }
78
+ else {
79
+ loginAttempts.set(key, { count: 1, firstAttempt: now, lastAttempt: now });
80
+ }
81
+ }
82
+ export function clearLoginAttempts(username, ip) {
83
+ loginAttempts.delete(`${username}:${ip}`);
84
+ }
85
+ // --- Session expiry ---
86
+ export function sessionExpiresAt() {
87
+ return new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
88
+ }
89
+ export function isSessionExpired(expiresAt) {
90
+ return new Date(expiresAt).getTime() < Date.now();
91
+ }