im-hub-pro 0.2.29

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 (384) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +497 -0
  3. package/README.zh-CN.md +496 -0
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +921 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/core/acp-server.d.ts +8 -0
  9. package/dist/core/acp-server.d.ts.map +1 -0
  10. package/dist/core/acp-server.js +266 -0
  11. package/dist/core/acp-server.js.map +1 -0
  12. package/dist/core/agent-base.d.ts +94 -0
  13. package/dist/core/agent-base.d.ts.map +1 -0
  14. package/dist/core/agent-base.js +374 -0
  15. package/dist/core/agent-base.js.map +1 -0
  16. package/dist/core/agent-cwd.d.ts +45 -0
  17. package/dist/core/agent-cwd.d.ts.map +1 -0
  18. package/dist/core/agent-cwd.js +178 -0
  19. package/dist/core/agent-cwd.js.map +1 -0
  20. package/dist/core/agent-cwd.test.d.ts +2 -0
  21. package/dist/core/agent-cwd.test.d.ts.map +1 -0
  22. package/dist/core/agent-cwd.test.js +149 -0
  23. package/dist/core/agent-cwd.test.js.map +1 -0
  24. package/dist/core/approval-bus.d.ts +232 -0
  25. package/dist/core/approval-bus.d.ts.map +1 -0
  26. package/dist/core/approval-bus.js +703 -0
  27. package/dist/core/approval-bus.js.map +1 -0
  28. package/dist/core/approval-bus.synthetic.test.d.ts +2 -0
  29. package/dist/core/approval-bus.synthetic.test.d.ts.map +1 -0
  30. package/dist/core/approval-bus.synthetic.test.js +182 -0
  31. package/dist/core/approval-bus.synthetic.test.js.map +1 -0
  32. package/dist/core/approval-bus.test.d.ts +2 -0
  33. package/dist/core/approval-bus.test.d.ts.map +1 -0
  34. package/dist/core/approval-bus.test.js +537 -0
  35. package/dist/core/approval-bus.test.js.map +1 -0
  36. package/dist/core/approval-router.d.ts +95 -0
  37. package/dist/core/approval-router.d.ts.map +1 -0
  38. package/dist/core/approval-router.js +450 -0
  39. package/dist/core/approval-router.js.map +1 -0
  40. package/dist/core/approval-router.test.d.ts +2 -0
  41. package/dist/core/approval-router.test.d.ts.map +1 -0
  42. package/dist/core/approval-router.test.js +413 -0
  43. package/dist/core/approval-router.test.js.map +1 -0
  44. package/dist/core/audit-log.d.ts +55 -0
  45. package/dist/core/audit-log.d.ts.map +1 -0
  46. package/dist/core/audit-log.js +203 -0
  47. package/dist/core/audit-log.js.map +1 -0
  48. package/dist/core/bgjob-reader.d.ts +65 -0
  49. package/dist/core/bgjob-reader.d.ts.map +1 -0
  50. package/dist/core/bgjob-reader.js +212 -0
  51. package/dist/core/bgjob-reader.js.map +1 -0
  52. package/dist/core/bgjob-reader.test.d.ts +2 -0
  53. package/dist/core/bgjob-reader.test.d.ts.map +1 -0
  54. package/dist/core/bgjob-reader.test.js +178 -0
  55. package/dist/core/bgjob-reader.test.js.map +1 -0
  56. package/dist/core/circuit-breaker.d.ts +37 -0
  57. package/dist/core/circuit-breaker.d.ts.map +1 -0
  58. package/dist/core/circuit-breaker.js +115 -0
  59. package/dist/core/circuit-breaker.js.map +1 -0
  60. package/dist/core/commands/agent.d.ts +4 -0
  61. package/dist/core/commands/agent.d.ts.map +1 -0
  62. package/dist/core/commands/agent.js +21 -0
  63. package/dist/core/commands/agent.js.map +1 -0
  64. package/dist/core/commands/approval.d.ts +3 -0
  65. package/dist/core/commands/approval.d.ts.map +1 -0
  66. package/dist/core/commands/approval.js +44 -0
  67. package/dist/core/commands/approval.js.map +1 -0
  68. package/dist/core/commands/approval.test.d.ts +2 -0
  69. package/dist/core/commands/approval.test.d.ts.map +1 -0
  70. package/dist/core/commands/approval.test.js +85 -0
  71. package/dist/core/commands/approval.test.js.map +1 -0
  72. package/dist/core/commands/audit.d.ts +3 -0
  73. package/dist/core/commands/audit.d.ts.map +1 -0
  74. package/dist/core/commands/audit.js +84 -0
  75. package/dist/core/commands/audit.js.map +1 -0
  76. package/dist/core/commands/builtin.d.ts +3 -0
  77. package/dist/core/commands/builtin.d.ts.map +1 -0
  78. package/dist/core/commands/builtin.js +26 -0
  79. package/dist/core/commands/builtin.js.map +1 -0
  80. package/dist/core/commands/job.d.ts +3 -0
  81. package/dist/core/commands/job.d.ts.map +1 -0
  82. package/dist/core/commands/job.js +195 -0
  83. package/dist/core/commands/job.js.map +1 -0
  84. package/dist/core/commands/model.d.ts +9 -0
  85. package/dist/core/commands/model.d.ts.map +1 -0
  86. package/dist/core/commands/model.js +183 -0
  87. package/dist/core/commands/model.js.map +1 -0
  88. package/dist/core/commands/plan.d.ts +3 -0
  89. package/dist/core/commands/plan.d.ts.map +1 -0
  90. package/dist/core/commands/plan.js +75 -0
  91. package/dist/core/commands/plan.js.map +1 -0
  92. package/dist/core/commands/plan.test.d.ts +2 -0
  93. package/dist/core/commands/plan.test.d.ts.map +1 -0
  94. package/dist/core/commands/plan.test.js +122 -0
  95. package/dist/core/commands/plan.test.js.map +1 -0
  96. package/dist/core/commands/router.d.ts +3 -0
  97. package/dist/core/commands/router.d.ts.map +1 -0
  98. package/dist/core/commands/router.js +71 -0
  99. package/dist/core/commands/router.js.map +1 -0
  100. package/dist/core/commands/schedule.d.ts +3 -0
  101. package/dist/core/commands/schedule.d.ts.map +1 -0
  102. package/dist/core/commands/schedule.js +123 -0
  103. package/dist/core/commands/schedule.js.map +1 -0
  104. package/dist/core/commands/sessions.d.ts +3 -0
  105. package/dist/core/commands/sessions.d.ts.map +1 -0
  106. package/dist/core/commands/sessions.js +88 -0
  107. package/dist/core/commands/sessions.js.map +1 -0
  108. package/dist/core/commands/stats.d.ts +3 -0
  109. package/dist/core/commands/stats.d.ts.map +1 -0
  110. package/dist/core/commands/stats.js +73 -0
  111. package/dist/core/commands/stats.js.map +1 -0
  112. package/dist/core/commands/think.d.ts +3 -0
  113. package/dist/core/commands/think.d.ts.map +1 -0
  114. package/dist/core/commands/think.js +28 -0
  115. package/dist/core/commands/think.js.map +1 -0
  116. package/dist/core/commands/workspaces.d.ts +3 -0
  117. package/dist/core/commands/workspaces.d.ts.map +1 -0
  118. package/dist/core/commands/workspaces.js +47 -0
  119. package/dist/core/commands/workspaces.js.map +1 -0
  120. package/dist/core/config-schema.d.ts +58 -0
  121. package/dist/core/config-schema.d.ts.map +1 -0
  122. package/dist/core/config-schema.js +63 -0
  123. package/dist/core/config-schema.js.map +1 -0
  124. package/dist/core/cron.d.ts +29 -0
  125. package/dist/core/cron.d.ts.map +1 -0
  126. package/dist/core/cron.js +184 -0
  127. package/dist/core/cron.js.map +1 -0
  128. package/dist/core/event-bus.d.ts +80 -0
  129. package/dist/core/event-bus.d.ts.map +1 -0
  130. package/dist/core/event-bus.js +62 -0
  131. package/dist/core/event-bus.js.map +1 -0
  132. package/dist/core/intent-llm.d.ts +27 -0
  133. package/dist/core/intent-llm.d.ts.map +1 -0
  134. package/dist/core/intent-llm.js +170 -0
  135. package/dist/core/intent-llm.js.map +1 -0
  136. package/dist/core/intent.d.ts +12 -0
  137. package/dist/core/intent.d.ts.map +1 -0
  138. package/dist/core/intent.js +187 -0
  139. package/dist/core/intent.js.map +1 -0
  140. package/dist/core/job-board.d.ts +84 -0
  141. package/dist/core/job-board.d.ts.map +1 -0
  142. package/dist/core/job-board.js +379 -0
  143. package/dist/core/job-board.js.map +1 -0
  144. package/dist/core/logger.d.ts +6 -0
  145. package/dist/core/logger.d.ts.map +1 -0
  146. package/dist/core/logger.js +54 -0
  147. package/dist/core/logger.js.map +1 -0
  148. package/dist/core/metrics.d.ts +55 -0
  149. package/dist/core/metrics.d.ts.map +1 -0
  150. package/dist/core/metrics.js +291 -0
  151. package/dist/core/metrics.js.map +1 -0
  152. package/dist/core/onboarding.d.ts +94 -0
  153. package/dist/core/onboarding.d.ts.map +1 -0
  154. package/dist/core/onboarding.js +426 -0
  155. package/dist/core/onboarding.js.map +1 -0
  156. package/dist/core/onboarding.test.d.ts +2 -0
  157. package/dist/core/onboarding.test.d.ts.map +1 -0
  158. package/dist/core/onboarding.test.js +112 -0
  159. package/dist/core/onboarding.test.js.map +1 -0
  160. package/dist/core/rate-limiter.d.ts +44 -0
  161. package/dist/core/rate-limiter.d.ts.map +1 -0
  162. package/dist/core/rate-limiter.js +115 -0
  163. package/dist/core/rate-limiter.js.map +1 -0
  164. package/dist/core/registry.d.ts +32 -0
  165. package/dist/core/registry.d.ts.map +1 -0
  166. package/dist/core/registry.js +122 -0
  167. package/dist/core/registry.js.map +1 -0
  168. package/dist/core/router.d.ts +41 -0
  169. package/dist/core/router.d.ts.map +1 -0
  170. package/dist/core/router.js +431 -0
  171. package/dist/core/router.js.map +1 -0
  172. package/dist/core/schedule.d.ts +65 -0
  173. package/dist/core/schedule.d.ts.map +1 -0
  174. package/dist/core/schedule.js +316 -0
  175. package/dist/core/schedule.js.map +1 -0
  176. package/dist/core/session-subtasks.test.d.ts +2 -0
  177. package/dist/core/session-subtasks.test.d.ts.map +1 -0
  178. package/dist/core/session-subtasks.test.js +88 -0
  179. package/dist/core/session-subtasks.test.js.map +1 -0
  180. package/dist/core/session.d.ts +182 -0
  181. package/dist/core/session.d.ts.map +1 -0
  182. package/dist/core/session.js +774 -0
  183. package/dist/core/session.js.map +1 -0
  184. package/dist/core/sqlite-helper.d.ts +37 -0
  185. package/dist/core/sqlite-helper.d.ts.map +1 -0
  186. package/dist/core/sqlite-helper.js +79 -0
  187. package/dist/core/sqlite-helper.js.map +1 -0
  188. package/dist/core/transcribe.d.ts +25 -0
  189. package/dist/core/transcribe.d.ts.map +1 -0
  190. package/dist/core/transcribe.js +217 -0
  191. package/dist/core/transcribe.js.map +1 -0
  192. package/dist/core/transcribe.test.d.ts +2 -0
  193. package/dist/core/transcribe.test.d.ts.map +1 -0
  194. package/dist/core/transcribe.test.js +163 -0
  195. package/dist/core/transcribe.test.js.map +1 -0
  196. package/dist/core/types.d.ts +352 -0
  197. package/dist/core/types.d.ts.map +1 -0
  198. package/dist/core/types.js +3 -0
  199. package/dist/core/types.js.map +1 -0
  200. package/dist/core/workspace.d.ts +67 -0
  201. package/dist/core/workspace.d.ts.map +1 -0
  202. package/dist/core/workspace.js +113 -0
  203. package/dist/core/workspace.js.map +1 -0
  204. package/dist/index.d.ts +5 -0
  205. package/dist/index.d.ts.map +1 -0
  206. package/dist/index.js +6 -0
  207. package/dist/index.js.map +1 -0
  208. package/dist/plugins/agents/acp/acp-adapter.d.ts +16 -0
  209. package/dist/plugins/agents/acp/acp-adapter.d.ts.map +1 -0
  210. package/dist/plugins/agents/acp/acp-adapter.js +49 -0
  211. package/dist/plugins/agents/acp/acp-adapter.js.map +1 -0
  212. package/dist/plugins/agents/acp/acp-client.d.ts +32 -0
  213. package/dist/plugins/agents/acp/acp-client.d.ts.map +1 -0
  214. package/dist/plugins/agents/acp/acp-client.js +175 -0
  215. package/dist/plugins/agents/acp/acp-client.js.map +1 -0
  216. package/dist/plugins/agents/acp/discovery.d.ts +19 -0
  217. package/dist/plugins/agents/acp/discovery.d.ts.map +1 -0
  218. package/dist/plugins/agents/acp/discovery.js +109 -0
  219. package/dist/plugins/agents/acp/discovery.js.map +1 -0
  220. package/dist/plugins/agents/acp/index.d.ts +4 -0
  221. package/dist/plugins/agents/acp/index.d.ts.map +1 -0
  222. package/dist/plugins/agents/acp/index.js +4 -0
  223. package/dist/plugins/agents/acp/index.js.map +1 -0
  224. package/dist/plugins/agents/acp/types.d.ts +62 -0
  225. package/dist/plugins/agents/acp/types.d.ts.map +1 -0
  226. package/dist/plugins/agents/acp/types.js +5 -0
  227. package/dist/plugins/agents/acp/types.js.map +1 -0
  228. package/dist/plugins/agents/claude-code/adapter.test.d.ts +2 -0
  229. package/dist/plugins/agents/claude-code/adapter.test.d.ts.map +1 -0
  230. package/dist/plugins/agents/claude-code/adapter.test.js +195 -0
  231. package/dist/plugins/agents/claude-code/adapter.test.js.map +1 -0
  232. package/dist/plugins/agents/claude-code/index.d.ts +25 -0
  233. package/dist/plugins/agents/claude-code/index.d.ts.map +1 -0
  234. package/dist/plugins/agents/claude-code/index.js +184 -0
  235. package/dist/plugins/agents/claude-code/index.js.map +1 -0
  236. package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts +42 -0
  237. package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts.map +1 -0
  238. package/dist/plugins/agents/claude-code/mcp-approval-server.js +235 -0
  239. package/dist/plugins/agents/claude-code/mcp-approval-server.js.map +1 -0
  240. package/dist/plugins/agents/claude-code/mcp-approval-server.test.d.ts +2 -0
  241. package/dist/plugins/agents/claude-code/mcp-approval-server.test.d.ts.map +1 -0
  242. package/dist/plugins/agents/claude-code/mcp-approval-server.test.js +188 -0
  243. package/dist/plugins/agents/claude-code/mcp-approval-server.test.js.map +1 -0
  244. package/dist/plugins/agents/codex/adapter.test.d.ts +2 -0
  245. package/dist/plugins/agents/codex/adapter.test.d.ts.map +1 -0
  246. package/dist/plugins/agents/codex/adapter.test.js +192 -0
  247. package/dist/plugins/agents/codex/adapter.test.js.map +1 -0
  248. package/dist/plugins/agents/codex/index.d.ts +37 -0
  249. package/dist/plugins/agents/codex/index.d.ts.map +1 -0
  250. package/dist/plugins/agents/codex/index.js +254 -0
  251. package/dist/plugins/agents/codex/index.js.map +1 -0
  252. package/dist/plugins/agents/copilot/index.d.ts +35 -0
  253. package/dist/plugins/agents/copilot/index.d.ts.map +1 -0
  254. package/dist/plugins/agents/copilot/index.js +182 -0
  255. package/dist/plugins/agents/copilot/index.js.map +1 -0
  256. package/dist/plugins/agents/opencode/adapter.test.d.ts +2 -0
  257. package/dist/plugins/agents/opencode/adapter.test.d.ts.map +1 -0
  258. package/dist/plugins/agents/opencode/adapter.test.js +139 -0
  259. package/dist/plugins/agents/opencode/adapter.test.js.map +1 -0
  260. package/dist/plugins/agents/opencode/http-adapter.test.d.ts +2 -0
  261. package/dist/plugins/agents/opencode/http-adapter.test.d.ts.map +1 -0
  262. package/dist/plugins/agents/opencode/http-adapter.test.js +492 -0
  263. package/dist/plugins/agents/opencode/http-adapter.test.js.map +1 -0
  264. package/dist/plugins/agents/opencode/index.d.ts +5 -0
  265. package/dist/plugins/agents/opencode/index.d.ts.map +1 -0
  266. package/dist/plugins/agents/opencode/index.js +30 -0
  267. package/dist/plugins/agents/opencode/index.js.map +1 -0
  268. package/dist/plugins/agents/opencode/opencode-http-adapter.d.ts +138 -0
  269. package/dist/plugins/agents/opencode/opencode-http-adapter.d.ts.map +1 -0
  270. package/dist/plugins/agents/opencode/opencode-http-adapter.js +549 -0
  271. package/dist/plugins/agents/opencode/opencode-http-adapter.js.map +1 -0
  272. package/dist/plugins/agents/opencode/opencode-stdio-adapter.d.ts +24 -0
  273. package/dist/plugins/agents/opencode/opencode-stdio-adapter.d.ts.map +1 -0
  274. package/dist/plugins/agents/opencode/opencode-stdio-adapter.js +103 -0
  275. package/dist/plugins/agents/opencode/opencode-stdio-adapter.js.map +1 -0
  276. package/dist/plugins/agents/opencode/serve-manager.d.ts +27 -0
  277. package/dist/plugins/agents/opencode/serve-manager.d.ts.map +1 -0
  278. package/dist/plugins/agents/opencode/serve-manager.js +190 -0
  279. package/dist/plugins/agents/opencode/serve-manager.js.map +1 -0
  280. package/dist/plugins/messengers/discord/discord-adapter.d.ts +22 -0
  281. package/dist/plugins/messengers/discord/discord-adapter.d.ts.map +1 -0
  282. package/dist/plugins/messengers/discord/discord-adapter.js +241 -0
  283. package/dist/plugins/messengers/discord/discord-adapter.js.map +1 -0
  284. package/dist/plugins/messengers/discord/discord-adapter.test.d.ts +2 -0
  285. package/dist/plugins/messengers/discord/discord-adapter.test.d.ts.map +1 -0
  286. package/dist/plugins/messengers/discord/discord-adapter.test.js +332 -0
  287. package/dist/plugins/messengers/discord/discord-adapter.test.js.map +1 -0
  288. package/dist/plugins/messengers/discord/index.d.ts +4 -0
  289. package/dist/plugins/messengers/discord/index.d.ts.map +1 -0
  290. package/dist/plugins/messengers/discord/index.js +4 -0
  291. package/dist/plugins/messengers/discord/index.js.map +1 -0
  292. package/dist/plugins/messengers/discord/markdown-to-discord.d.ts +11 -0
  293. package/dist/plugins/messengers/discord/markdown-to-discord.d.ts.map +1 -0
  294. package/dist/plugins/messengers/discord/markdown-to-discord.js +59 -0
  295. package/dist/plugins/messengers/discord/markdown-to-discord.js.map +1 -0
  296. package/dist/plugins/messengers/discord/types.d.ts +9 -0
  297. package/dist/plugins/messengers/discord/types.d.ts.map +1 -0
  298. package/dist/plugins/messengers/discord/types.js +3 -0
  299. package/dist/plugins/messengers/discord/types.js.map +1 -0
  300. package/dist/plugins/messengers/feishu/card-builder.d.ts +23 -0
  301. package/dist/plugins/messengers/feishu/card-builder.d.ts.map +1 -0
  302. package/dist/plugins/messengers/feishu/card-builder.js +89 -0
  303. package/dist/plugins/messengers/feishu/card-builder.js.map +1 -0
  304. package/dist/plugins/messengers/feishu/feishu-adapter.d.ts +33 -0
  305. package/dist/plugins/messengers/feishu/feishu-adapter.d.ts.map +1 -0
  306. package/dist/plugins/messengers/feishu/feishu-adapter.js +195 -0
  307. package/dist/plugins/messengers/feishu/feishu-adapter.js.map +1 -0
  308. package/dist/plugins/messengers/feishu/feishu-client.d.ts +44 -0
  309. package/dist/plugins/messengers/feishu/feishu-client.d.ts.map +1 -0
  310. package/dist/plugins/messengers/feishu/feishu-client.js +120 -0
  311. package/dist/plugins/messengers/feishu/feishu-client.js.map +1 -0
  312. package/dist/plugins/messengers/feishu/feishu-dedup.test.d.ts +2 -0
  313. package/dist/plugins/messengers/feishu/feishu-dedup.test.d.ts.map +1 -0
  314. package/dist/plugins/messengers/feishu/feishu-dedup.test.js +70 -0
  315. package/dist/plugins/messengers/feishu/feishu-dedup.test.js.map +1 -0
  316. package/dist/plugins/messengers/feishu/index.d.ts +4 -0
  317. package/dist/plugins/messengers/feishu/index.d.ts.map +1 -0
  318. package/dist/plugins/messengers/feishu/index.js +4 -0
  319. package/dist/plugins/messengers/feishu/index.js.map +1 -0
  320. package/dist/plugins/messengers/feishu/types.d.ts +113 -0
  321. package/dist/plugins/messengers/feishu/types.d.ts.map +1 -0
  322. package/dist/plugins/messengers/feishu/types.js +4 -0
  323. package/dist/plugins/messengers/feishu/types.js.map +1 -0
  324. package/dist/plugins/messengers/telegram/index.d.ts +4 -0
  325. package/dist/plugins/messengers/telegram/index.d.ts.map +1 -0
  326. package/dist/plugins/messengers/telegram/index.js +4 -0
  327. package/dist/plugins/messengers/telegram/index.js.map +1 -0
  328. package/dist/plugins/messengers/telegram/markdown-to-html.d.ts +5 -0
  329. package/dist/plugins/messengers/telegram/markdown-to-html.d.ts.map +1 -0
  330. package/dist/plugins/messengers/telegram/markdown-to-html.js +186 -0
  331. package/dist/plugins/messengers/telegram/markdown-to-html.js.map +1 -0
  332. package/dist/plugins/messengers/telegram/media-download.d.ts +51 -0
  333. package/dist/plugins/messengers/telegram/media-download.d.ts.map +1 -0
  334. package/dist/plugins/messengers/telegram/media-download.js +224 -0
  335. package/dist/plugins/messengers/telegram/media-download.js.map +1 -0
  336. package/dist/plugins/messengers/telegram/media-download.test.d.ts +2 -0
  337. package/dist/plugins/messengers/telegram/media-download.test.d.ts.map +1 -0
  338. package/dist/plugins/messengers/telegram/media-download.test.js +125 -0
  339. package/dist/plugins/messengers/telegram/media-download.test.js.map +1 -0
  340. package/dist/plugins/messengers/telegram/telegram-adapter.d.ts +62 -0
  341. package/dist/plugins/messengers/telegram/telegram-adapter.d.ts.map +1 -0
  342. package/dist/plugins/messengers/telegram/telegram-adapter.js +653 -0
  343. package/dist/plugins/messengers/telegram/telegram-adapter.js.map +1 -0
  344. package/dist/plugins/messengers/telegram/types.d.ts +47 -0
  345. package/dist/plugins/messengers/telegram/types.d.ts.map +1 -0
  346. package/dist/plugins/messengers/telegram/types.js +3 -0
  347. package/dist/plugins/messengers/telegram/types.js.map +1 -0
  348. package/dist/plugins/messengers/wechat/ilink-adapter.d.ts +68 -0
  349. package/dist/plugins/messengers/wechat/ilink-adapter.d.ts.map +1 -0
  350. package/dist/plugins/messengers/wechat/ilink-adapter.js +483 -0
  351. package/dist/plugins/messengers/wechat/ilink-adapter.js.map +1 -0
  352. package/dist/plugins/messengers/wechat/ilink-client.d.ts +66 -0
  353. package/dist/plugins/messengers/wechat/ilink-client.d.ts.map +1 -0
  354. package/dist/plugins/messengers/wechat/ilink-client.js +288 -0
  355. package/dist/plugins/messengers/wechat/ilink-client.js.map +1 -0
  356. package/dist/plugins/messengers/wechat/ilink-types.d.ts +173 -0
  357. package/dist/plugins/messengers/wechat/ilink-types.d.ts.map +1 -0
  358. package/dist/plugins/messengers/wechat/ilink-types.js +12 -0
  359. package/dist/plugins/messengers/wechat/ilink-types.js.map +1 -0
  360. package/dist/utils/backoff.d.ts +35 -0
  361. package/dist/utils/backoff.d.ts.map +1 -0
  362. package/dist/utils/backoff.js +59 -0
  363. package/dist/utils/backoff.js.map +1 -0
  364. package/dist/utils/cross-platform.d.ts +26 -0
  365. package/dist/utils/cross-platform.d.ts.map +1 -0
  366. package/dist/utils/cross-platform.js +58 -0
  367. package/dist/utils/cross-platform.js.map +1 -0
  368. package/dist/utils/message-split.d.ts +14 -0
  369. package/dist/utils/message-split.d.ts.map +1 -0
  370. package/dist/utils/message-split.js +65 -0
  371. package/dist/utils/message-split.js.map +1 -0
  372. package/dist/utils/safe-equal.d.ts +2 -0
  373. package/dist/utils/safe-equal.d.ts.map +1 -0
  374. package/dist/utils/safe-equal.js +11 -0
  375. package/dist/utils/safe-equal.js.map +1 -0
  376. package/dist/web/public/_app.js +196 -0
  377. package/dist/web/public/index.html +935 -0
  378. package/dist/web/public/settings.html +1181 -0
  379. package/dist/web/public/tasks.html +1827 -0
  380. package/dist/web/server.d.ts +11 -0
  381. package/dist/web/server.d.ts.map +1 -0
  382. package/dist/web/server.js +1820 -0
  383. package/dist/web/server.js.map +1 -0
  384. package/package.json +73 -0
@@ -0,0 +1,1181 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>im-hub-pro — Settings</title>
7
+ <!-- Shared utilities: theme manager (applies before first paint), error
8
+ boundary (surfaces silent script failures), i18n + api helpers. -->
9
+ <script src="/_app.js"></script>
10
+ <script>
11
+ const LANGS = { en: 'English', zh: '中文' };
12
+ const savedLang = localStorage.getItem('im-hub-lang');
13
+ const browserLang = navigator.language.startsWith('zh') ? 'zh' : 'en';
14
+ window.__lang = savedLang && LANGS[savedLang] ? savedLang : browserLang;
15
+ document.documentElement.lang = window.__lang === 'zh' ? 'zh-CN' : 'en';
16
+
17
+ const T = {
18
+ en: {
19
+ title: 'im-hub-pro — Settings',
20
+ backToChat: 'Back to Chat',
21
+ settingsTitle: 'im-hub-pro Settings',
22
+ loading: 'Loading configuration...',
23
+ loadFailed: 'Failed to load config',
24
+ h1: 'Settings',
25
+ agents: 'Agents',
26
+ defaultAgent: 'Default Agent',
27
+ saveAgents: 'Save Agents',
28
+ messengers: 'Messengers (Channels)',
29
+ wechat: 'WeChat',
30
+ wechatHint: 'iLink QR scan login',
31
+ wechatBox: 'WeChat requires QR scan login. Run <code>im-hub-pro config wechat</code> in terminal to set up.',
32
+ telegram: 'Telegram',
33
+ telegramHint: 'Bot token from @BotFather',
34
+ botToken: 'Bot Token',
35
+ channelId: 'Channel ID',
36
+ feishu: 'Feishu / Lark',
37
+ feishuHint: 'App ID and App Secret',
38
+ appId: 'App ID',
39
+ appSecret: 'App Secret',
40
+ saveMessengers: 'Save Messengers',
41
+ acpTitle: 'Remote Agents (ACP)',
42
+ acpNone: 'No remote agents configured',
43
+ name: 'Name',
44
+ endpoint: 'Endpoint',
45
+ auth: 'Auth',
46
+ enabled: 'Enabled',
47
+ test: 'Test',
48
+ del: 'Del',
49
+ addAgent: 'Add Remote Agent',
50
+ aliases: 'Aliases (comma-separated)',
51
+ endpointUrl: 'Endpoint URL',
52
+ authType: 'Auth Type',
53
+ none: 'None',
54
+ apikey: 'API Key',
55
+ bearer: 'Bearer Token',
56
+ token: 'Token',
57
+ testConn: 'Test Connection',
58
+ add: 'Add Agent',
59
+ general: 'General',
60
+ webPort: 'Web Chat Port',
61
+ save: 'Save',
62
+ saved: 'Saved',
63
+ testing: 'Testing...',
64
+ connected: 'Connected',
65
+ failed: 'Failed',
66
+ error: 'Error',
67
+ savedMsg: 'Saved',
68
+ saveFailed: 'Save failed',
69
+ enterEndpoint: 'Enter an endpoint URL',
70
+ nameRequired: 'Agent name is required',
71
+ endpointRequired: 'Endpoint URL is required',
72
+ installHint: 'npm i -g',
73
+ workspacesTitle: 'Workspaces',
74
+ workspacesNone: 'Only the default workspace exists. Add one to scope agents/rate-limits per team.',
75
+ workspaceId: 'ID',
76
+ workspaceName: 'Name',
77
+ workspaceAgents: 'Agents (comma)',
78
+ workspaceMembers: 'Members (comma userIds, blank = open)',
79
+ workspaceRateLimit: 'Rate limit',
80
+ workspaceRate: 'Rate / interval',
81
+ workspaceInterval: 'Interval (sec)',
82
+ workspaceBurst: 'Burst',
83
+ workspaceAdd: 'Add or update workspace',
84
+ workspaceEdit: 'Edit',
85
+ workspaceDelete: 'Delete',
86
+ workspaceReset: 'Reset',
87
+ workspaceConfirmDelete: 'Delete workspace "{id}"?',
88
+ workspaceSaved: 'Workspace saved',
89
+ workspaceDeleted: 'Workspace deleted',
90
+ workspaceIdHelp: 'Letters / digits / _ / - only',
91
+ workspaceDefaultLocked: 'default (locked)',
92
+ },
93
+ zh: {
94
+ title: 'im-hub-pro — 设置',
95
+ backToChat: '返回对话',
96
+ settingsTitle: 'im-hub-pro 设置',
97
+ loading: '加载配置中...',
98
+ loadFailed: '加载配置失败',
99
+ h1: '设置',
100
+ agents: 'Agent',
101
+ defaultAgent: '默认 Agent',
102
+ saveAgents: '保存 Agent',
103
+ messengers: '消息通道 (Channels)',
104
+ wechat: '微信',
105
+ wechatHint: 'iLink 扫码登录',
106
+ wechatBox: '微信需要扫码登录。在终端运行 <code>im-hub-pro config wechat</code> 进行配置。',
107
+ telegram: 'Telegram',
108
+ telegramHint: '从 @BotFather 获取 Bot Token',
109
+ botToken: 'Bot Token',
110
+ channelId: '频道 ID',
111
+ feishu: '飞书 / Lark',
112
+ feishuHint: 'App ID 和 App Secret',
113
+ appId: 'App ID',
114
+ appSecret: 'App Secret',
115
+ saveMessengers: '保存通道',
116
+ acpTitle: '远程 Agent (ACP)',
117
+ acpNone: '暂无远程 Agent',
118
+ name: '名称',
119
+ endpoint: '端点',
120
+ auth: '认证',
121
+ enabled: '启用',
122
+ test: '测试',
123
+ del: '删除',
124
+ addAgent: '添加远程 Agent',
125
+ aliases: '别名(逗号分隔)',
126
+ endpointUrl: '端点 URL',
127
+ authType: '认证方式',
128
+ none: '无',
129
+ apikey: 'API Key',
130
+ bearer: 'Bearer Token',
131
+ token: '令牌',
132
+ testConn: '测试连接',
133
+ add: '添加 Agent',
134
+ general: '通用',
135
+ webPort: 'Web 对话端口',
136
+ save: '保存',
137
+ saved: '已保存',
138
+ testing: '测试中...',
139
+ connected: '已连接',
140
+ failed: '失败',
141
+ error: '错误',
142
+ savedMsg: '已保存',
143
+ saveFailed: '保存失败',
144
+ enterEndpoint: '请输入端点 URL',
145
+ nameRequired: 'Agent 名称为必填项',
146
+ endpointRequired: '端点 URL 为必填项',
147
+ installHint: '安装命令:npm i -g',
148
+ workspacesTitle: '工作区',
149
+ workspacesNone: '仅有默认工作区。新增工作区可以为不同团队限定 agent 白名单与限流。',
150
+ workspaceId: 'ID',
151
+ workspaceName: '名称',
152
+ workspaceAgents: 'Agent(逗号分隔)',
153
+ workspaceMembers: '成员(逗号分隔 userId;空 = 开放)',
154
+ workspaceRateLimit: '限流',
155
+ workspaceRate: '速率 / 间隔',
156
+ workspaceInterval: '间隔(秒)',
157
+ workspaceBurst: '突发上限',
158
+ workspaceAdd: '新增 / 更新工作区',
159
+ workspaceEdit: '编辑',
160
+ workspaceDelete: '删除',
161
+ workspaceReset: '清空',
162
+ workspaceConfirmDelete: '确认删除工作区 "{id}" ?',
163
+ workspaceSaved: '工作区已保存',
164
+ workspaceDeleted: '工作区已删除',
165
+ workspaceIdHelp: '仅支持字母 / 数字 / _ / -',
166
+ workspaceDefaultLocked: '默认(不可改)',
167
+ },
168
+ };
169
+ function t(key) { return T[window.__lang][key] || T.en[key] || key; }
170
+ document.addEventListener('DOMContentLoaded', () => { document.title = t('title'); });
171
+ </script>
172
+ <style>
173
+ /* Three-state theming. `:root` defaults to light; explicit
174
+ data-theme="dark" forces dark; `prefers-color-scheme: dark` only
175
+ applies when the attribute is absent (mode === 'system'). */
176
+ :root {
177
+ color-scheme: light dark;
178
+ /* light defaults */
179
+ --bg: #f8f9fb;
180
+ --surface: #ffffff;
181
+ --surface2: #f1f3f6;
182
+ --border: #e1e4e8;
183
+ --text: #1a1f2e;
184
+ --text-dim: #6b7280;
185
+ --accent: #6366f1;
186
+ --accent-dim: #818cf8;
187
+ --green: #16a34a;
188
+ --red: #dc2626;
189
+ --yellow: #ca8a04;
190
+ --radius: 8px;
191
+ }
192
+ :root[data-theme="dark"] {
193
+ --bg: #0a0a0a;
194
+ --surface: #141414;
195
+ --surface2: #1e1e1e;
196
+ --border: #2a2a2a;
197
+ --text: #e5e5e5;
198
+ --text-dim: #888;
199
+ --accent: #6366f1;
200
+ --accent-dim: #4f46e5;
201
+ --green: #22c55e;
202
+ --red: #ef4444;
203
+ --yellow: #eab308;
204
+ }
205
+ @media (prefers-color-scheme: dark) {
206
+ :root:not([data-theme]) {
207
+ --bg: #0a0a0a;
208
+ --surface: #141414;
209
+ --surface2: #1e1e1e;
210
+ --border: #2a2a2a;
211
+ --text: #e5e5e5;
212
+ --text-dim: #888;
213
+ --accent-dim: #4f46e5;
214
+ --green: #22c55e;
215
+ --red: #ef4444;
216
+ --yellow: #eab308;
217
+ }
218
+ }
219
+
220
+ * { margin: 0; padding: 0; box-sizing: border-box; }
221
+
222
+ body {
223
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
224
+ background: var(--bg);
225
+ color: var(--text);
226
+ min-height: 100vh;
227
+ }
228
+
229
+ .header {
230
+ display: flex;
231
+ align-items: center;
232
+ gap: 16px;
233
+ padding: 14px 28px;
234
+ border-bottom: 1px solid var(--border);
235
+ background: var(--surface);
236
+ position: sticky;
237
+ top: 0;
238
+ z-index: 5;
239
+ }
240
+ .header .brand {
241
+ display: flex;
242
+ align-items: baseline;
243
+ gap: 12px;
244
+ min-width: 0;
245
+ }
246
+ .header .brand a {
247
+ color: var(--text-dim);
248
+ font-size: 13px;
249
+ white-space: nowrap;
250
+ }
251
+ .header .brand a:hover { color: var(--accent); }
252
+ .header .brand .title {
253
+ font-weight: 600;
254
+ font-size: 15px;
255
+ color: var(--text);
256
+ letter-spacing: 0.2px;
257
+ white-space: nowrap;
258
+ }
259
+ .header .controls {
260
+ margin-left: auto;
261
+ display: flex;
262
+ align-items: center;
263
+ gap: 8px;
264
+ flex-shrink: 0;
265
+ }
266
+ .header .controls select,
267
+ .header .controls button {
268
+ width: auto;
269
+ min-width: 0;
270
+ margin: 0;
271
+ background: var(--surface2);
272
+ color: var(--text);
273
+ border: 1px solid var(--border);
274
+ border-radius: 6px;
275
+ padding: 6px 10px;
276
+ font-size: 12px;
277
+ cursor: pointer;
278
+ outline: none;
279
+ white-space: nowrap;
280
+ }
281
+ .header .controls select:hover,
282
+ .header .controls button:hover { border-color: var(--text-dim); }
283
+
284
+ .container {
285
+ max-width: 880px;
286
+ margin: 0 auto;
287
+ padding: 32px 28px 64px;
288
+ }
289
+ .container h1 {
290
+ font-size: 24px;
291
+ font-weight: 700;
292
+ margin-bottom: 24px;
293
+ letter-spacing: -0.2px;
294
+ }
295
+
296
+ /* Cards */
297
+ .card {
298
+ background: var(--surface);
299
+ border: 1px solid var(--border);
300
+ border-radius: 12px;
301
+ padding: 22px 24px;
302
+ margin-bottom: 18px;
303
+ box-shadow: 0 1px 2px rgba(0,0,0,0.03);
304
+ }
305
+ :root[data-theme="dark"] .card { box-shadow: none; }
306
+ @media (prefers-color-scheme: dark) {
307
+ :root:not([data-theme]) .card { box-shadow: none; }
308
+ }
309
+ .card h2 {
310
+ font-size: 15px;
311
+ font-weight: 600;
312
+ margin-bottom: 18px;
313
+ display: flex;
314
+ align-items: center;
315
+ gap: 8px;
316
+ letter-spacing: -0.1px;
317
+ }
318
+ .card h2 .badge {
319
+ font-size: 11px;
320
+ padding: 2px 8px;
321
+ border-radius: 10px;
322
+ background: var(--surface2);
323
+ color: var(--text-dim);
324
+ font-weight: 400;
325
+ }
326
+
327
+ /* Form elements */
328
+ label {
329
+ display: block;
330
+ font-size: 12px;
331
+ color: var(--text-dim);
332
+ margin-bottom: 4px;
333
+ font-weight: 600;
334
+ }
335
+ input, select, textarea {
336
+ width: 100%;
337
+ background: var(--surface2);
338
+ color: var(--text);
339
+ border: 1px solid var(--border);
340
+ border-radius: 6px;
341
+ padding: 8px 12px;
342
+ font-size: 13px;
343
+ font-family: inherit;
344
+ outline: none;
345
+ margin-bottom: 12px;
346
+ }
347
+ input:focus, select:focus, textarea:focus {
348
+ border-color: var(--accent);
349
+ }
350
+ input::placeholder { color: #555; }
351
+
352
+ .row {
353
+ display: flex;
354
+ gap: 12px;
355
+ }
356
+ .row > * { flex: 1; }
357
+
358
+ /* Buttons */
359
+ .btn {
360
+ display: inline-flex;
361
+ align-items: center;
362
+ gap: 4px;
363
+ padding: 7px 14px;
364
+ border-radius: 6px;
365
+ font-size: 13px;
366
+ cursor: pointer;
367
+ border: 1px solid var(--border);
368
+ background: var(--surface2);
369
+ color: var(--text);
370
+ transition: all 0.15s;
371
+ }
372
+ .btn:hover { border-color: var(--text-dim); }
373
+ .btn-primary {
374
+ background: var(--accent);
375
+ border-color: var(--accent);
376
+ color: #fff;
377
+ }
378
+ .btn-primary:hover { background: var(--accent-dim); }
379
+ .btn-danger { color: var(--red); }
380
+ .btn-danger:hover { background: var(--surface2); border-color: var(--red); }
381
+ .btn-sm { padding: 4px 10px; font-size: 12px; }
382
+ .btn:disabled { opacity: 0.5; cursor: not-allowed; }
383
+
384
+ .actions { display: flex; gap: 8px; margin-top: 12px; }
385
+
386
+ /* Status dots */
387
+ .status { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; }
388
+ .dot {
389
+ width: 8px; height: 8px;
390
+ border-radius: 50%;
391
+ display: inline-block;
392
+ }
393
+ .dot-on { background: var(--green); }
394
+ .dot-off { background: var(--red); }
395
+
396
+ /* Agent list */
397
+ .agent-row {
398
+ display: flex;
399
+ align-items: center;
400
+ justify-content: space-between;
401
+ padding: 10px 0;
402
+ border-bottom: 1px solid var(--border);
403
+ }
404
+ .agent-row:last-child { border-bottom: none; }
405
+ .agent-row .left { display: flex; align-items: center; gap: 10px; }
406
+ .agent-row .name { font-weight: 600; font-size: 14px; }
407
+ .agent-row .hint { font-size: 12px; color: var(--text-dim); margin-top: 2px; }
408
+ .agent-row .aliases { font-size: 11px; color: var(--text-dim); }
409
+
410
+ /* ACP table */
411
+ .acp-table { width: 100%; border-collapse: collapse; margin-bottom: 12px; }
412
+ .acp-table th {
413
+ text-align: left;
414
+ font-size: 11px;
415
+ color: var(--text-dim);
416
+ padding: 6px 8px;
417
+ border-bottom: 1px solid var(--border);
418
+ font-weight: 600;
419
+ }
420
+ .acp-table td {
421
+ padding: 8px;
422
+ font-size: 13px;
423
+ border-bottom: 1px solid var(--border);
424
+ vertical-align: middle;
425
+ }
426
+ .acp-table tr:last-child td { border-bottom: none; }
427
+
428
+ /* Toggle */
429
+ .toggle {
430
+ position: relative;
431
+ width: 36px;
432
+ height: 20px;
433
+ background: var(--border);
434
+ border-radius: 10px;
435
+ cursor: pointer;
436
+ transition: background 0.2s;
437
+ flex-shrink: 0;
438
+ }
439
+ .toggle.active { background: var(--green); }
440
+ .toggle::after {
441
+ content: '';
442
+ position: absolute;
443
+ top: 2px; left: 2px;
444
+ width: 16px; height: 16px;
445
+ background: #fff;
446
+ border-radius: 50%;
447
+ transition: transform 0.2s;
448
+ }
449
+ .toggle.active::after { transform: translateX(16px); }
450
+
451
+ /* Toast */
452
+ .toast {
453
+ position: fixed;
454
+ bottom: 24px;
455
+ right: 24px;
456
+ padding: 10px 18px;
457
+ border-radius: 8px;
458
+ font-size: 13px;
459
+ z-index: 999;
460
+ opacity: 0;
461
+ transform: translateY(10px);
462
+ transition: all 0.3s;
463
+ }
464
+ .toast.show { opacity: 1; transform: translateY(0); }
465
+ .toast.success { background: var(--surface); border: 1px solid var(--green); color: var(--green); }
466
+ .toast.error { background: var(--surface); border: 1px solid var(--red); color: var(--red); }
467
+
468
+ /* Section hint */
469
+ .hint-box {
470
+ background: var(--surface2);
471
+ border-radius: 6px;
472
+ padding: 10px 14px;
473
+ font-size: 12px;
474
+ color: var(--text-dim);
475
+ margin-bottom: 12px;
476
+ line-height: 1.5;
477
+ }
478
+ .hint-box code {
479
+ background: var(--bg);
480
+ padding: 1px 5px;
481
+ border-radius: 3px;
482
+ font-family: 'SF Mono', monospace;
483
+ font-size: 11px;
484
+ }
485
+
486
+ /* Divider */
487
+ .divider { border: none; border-top: 1px solid var(--border); margin: 16px 0; }
488
+
489
+ /* Loading */
490
+ .loading {
491
+ text-align: center;
492
+ padding: 40px;
493
+ color: var(--text-dim);
494
+ }
495
+ </style>
496
+ </head>
497
+ <body>
498
+ <div class="header">
499
+ <div class="brand">
500
+ <a href="/" id="backToChat"></a>
501
+ <span class="title" id="settingsTitle"></span>
502
+ </div>
503
+ <div class="controls">
504
+ <select id="langSelect">
505
+ <option value="en">EN</option>
506
+ <option value="zh">中文</option>
507
+ </select>
508
+ <button id="theme-toggle" type="button" aria-label="Toggle color theme"></button>
509
+ </div>
510
+ </div>
511
+
512
+ <div class="container" id="app">
513
+ <div class="loading">Loading configuration...</div>
514
+ </div>
515
+
516
+ <div class="toast" id="toast"></div>
517
+
518
+ <script>
519
+ // Apply i18n to static elements
520
+ function applyLang() {
521
+ document.title = t('title');
522
+ document.documentElement.lang = window.__lang === 'zh' ? 'zh-CN' : 'en';
523
+ document.getElementById('backToChat').textContent = t('backToChat');
524
+ document.getElementById('settingsTitle').textContent = t('settingsTitle');
525
+ }
526
+
527
+ // Theme toggle (light / dark / system). _app.js applied theme already;
528
+ // here we wire the click handler so cycling re-renders the icon/label.
529
+ if (window.imhub) imhub.theme.bindToggle(document.getElementById('theme-toggle'));
530
+
531
+ // Language selector
532
+ const langSelect = document.getElementById('langSelect');
533
+ langSelect.value = window.__lang;
534
+ langSelect.addEventListener('change', () => {
535
+ window.__lang = langSelect.value;
536
+ localStorage.setItem('im-hub-lang', window.__lang);
537
+ applyLang();
538
+ render();
539
+ });
540
+
541
+ // State
542
+ let config = null;
543
+ /** Workspace list pulled from /api/workspaces?full=1. Null until first
544
+ * fetch completes; render falls through with empty rows in that case
545
+ * and refetch happens after init() completes. */
546
+ let workspaceList = null;
547
+ let agentStatus = {};
548
+
549
+ // DOM
550
+ const app = document.getElementById('app');
551
+ const toastEl = document.getElementById('toast');
552
+
553
+ // Known built-in agents with install info
554
+ const BUILTIN_AGENTS = {
555
+ 'claude-code': { aliases: ['cc', 'claude'], pkg: '@anthropic-ai/claude-code', cmd: 'claude' },
556
+ 'codex': { aliases: ['cx'], pkg: '@openai/codex', cmd: 'codex' },
557
+ 'copilot': { aliases: ['co'], pkg: '@github/copilot', cmd: 'copilot' },
558
+ 'opencode': { aliases: ['oc'], pkg: 'opencode-ai', cmd: 'opencode' },
559
+ };
560
+
561
+ // Helper: add auth token to all API requests
562
+ function authFetch(url, init = {}) {
563
+ const token = window.IMHUB_TOKEN || '';
564
+ return fetch(url, {
565
+ ...init,
566
+ headers: {
567
+ ...(init.headers || {}),
568
+ 'X-IM-Hub-Token': token,
569
+ },
570
+ });
571
+ }
572
+
573
+ // Fetch config on load
574
+ async function init() {
575
+ try {
576
+ const res = await authFetch('/api/config');
577
+ if (!res.ok) throw new Error(t('loadFailed'));
578
+ const data = await res.json();
579
+ config = data;
580
+ agentStatus = data.agentStatus || {};
581
+ // Workspaces come from a separate endpoint so the registry's
582
+ // runtime state (which may include extra/removed entries via
583
+ // REST CRUD since process start) is the source of truth.
584
+ try {
585
+ const wres = await authFetch('/api/workspaces?full=1');
586
+ if (wres.ok) workspaceList = (await wres.json()).workspaces || [];
587
+ } catch { /* fall through with workspaceList = null */ }
588
+ render();
589
+ } catch (err) {
590
+ app.innerHTML = `<div class="loading" style="color:var(--red)">${t('loadFailed')}: ${err.message}</div>`;
591
+ }
592
+ }
593
+
594
+ /** Refetch the workspace list and re-render just the workspaces card.
595
+ * Cheaper than a full settings re-render for create/update/delete. */
596
+ async function reloadWorkspaces() {
597
+ try {
598
+ const wres = await authFetch('/api/workspaces?full=1');
599
+ if (wres.ok) workspaceList = (await wres.json()).workspaces || [];
600
+ } catch { /* keep stale list, surface via toast on next save */ }
601
+ render();
602
+ }
603
+
604
+ function render() {
605
+ app.innerHTML = `
606
+ <h1>${t('h1')}</h1>
607
+ ${renderAgentsCard()}
608
+ ${renderMessengersCard()}
609
+ ${renderAcpCard()}
610
+ ${renderWorkspacesCard()}
611
+ ${renderGeneralCard()}
612
+ `;
613
+ bindEvents();
614
+ }
615
+
616
+ // ==========================================
617
+ // Agents card
618
+ // ==========================================
619
+ function renderAgentsCard() {
620
+ const agents = Object.keys(BUILTIN_AGENTS);
621
+ const enabledAgents = config.agents || [];
622
+ const defaultAgent = config.defaultAgent || 'claude-code';
623
+
624
+ const rows = agents.map(name => {
625
+ const info = BUILTIN_AGENTS[name];
626
+ const available = agentStatus[name];
627
+ const enabled = enabledAgents.includes(name);
628
+
629
+ return `
630
+ <div class="agent-row" data-agent="${name}">
631
+ <div class="left">
632
+ <span class="dot ${available ? 'dot-on' : 'dot-off'}"></span>
633
+ <div>
634
+ <div class="name">${name}</div>
635
+ <div class="aliases">${info.aliases.map(a => '/' + a).join(', ')}</div>
636
+ ${!available ? `<div class="hint">${t('installHint')} ${info.pkg}</div>` : ''}
637
+ </div>
638
+ </div>
639
+ <div class="toggle ${enabled ? 'active' : ''}" data-toggle-agent="${name}"></div>
640
+ </div>
641
+ `;
642
+ }).join('');
643
+
644
+ return `
645
+ <div class="card">
646
+ <h2>${t('agents')} <span class="badge">${agents.length}</span></h2>
647
+ ${rows}
648
+ <hr class="divider">
649
+ <label>${t('defaultAgent')}</label>
650
+ <select id="defaultAgent">
651
+ ${agents.filter(a => agentStatus[a]).map(a =>
652
+ `<option value="${a}" ${a === defaultAgent ? 'selected' : ''}>${a}</option>`
653
+ ).join('')}
654
+ </select>
655
+ <div class="actions">
656
+ <button class="btn btn-primary" id="saveAgents">${t('saveAgents')}</button>
657
+ </div>
658
+ </div>
659
+ `;
660
+ }
661
+
662
+ // ==========================================
663
+ // Messengers card
664
+ // ==========================================
665
+ function renderMessengersCard() {
666
+ const messengers = config.messengers || [];
667
+ const tg = config.telegram || {};
668
+ const fs = config.feishu || {};
669
+
670
+ const wechatEnabled = messengers.includes('wechat-ilink');
671
+ const telegramEnabled = messengers.includes('telegram');
672
+ const feishuEnabled = messengers.includes('feishu');
673
+
674
+ return `
675
+ <div class="card">
676
+ <h2>${t('messengers')} <span class="badge">${messengers.length}</span></h2>
677
+
678
+ <!-- WeChat -->
679
+ <div style="margin-bottom:16px">
680
+ <div class="agent-row">
681
+ <div class="left">
682
+ <div>
683
+ <div class="name">${t('wechat')}</div>
684
+ <div class="hint">${t('wechatHint')}</div>
685
+ </div>
686
+ </div>
687
+ <div class="toggle ${wechatEnabled ? 'active' : ''}" data-toggle-messenger="wechat-ilink"></div>
688
+ </div>
689
+ ${wechatEnabled ? `<div class="hint-box">${t('wechatBox')}</div>` : ''}
690
+ </div>
691
+
692
+ <!-- Telegram -->
693
+ <div style="margin-bottom:16px">
694
+ <div class="agent-row">
695
+ <div class="left">
696
+ <div>
697
+ <div class="name">${t('telegram')}</div>
698
+ <div class="hint">${t('telegramHint')}</div>
699
+ </div>
700
+ </div>
701
+ <div class="toggle ${telegramEnabled ? 'active' : ''}" data-toggle-messenger="telegram"></div>
702
+ </div>
703
+ ${telegramEnabled ? `
704
+ <div class="row">
705
+ <div>
706
+ <label>${t('botToken')}</label>
707
+ <input type="password" id="tgToken" value="${tg.botToken || ''}" placeholder="123456:ABC-DEF...">
708
+ </div>
709
+ <div>
710
+ <label>${t('channelId')}</label>
711
+ <input type="text" id="tgChannel" value="${tg.channelId || ''}" placeholder="default">
712
+ </div>
713
+ </div>
714
+ ` : ''}
715
+ </div>
716
+
717
+ <!-- Feishu -->
718
+ <div>
719
+ <div class="agent-row">
720
+ <div class="left">
721
+ <div>
722
+ <div class="name">${t('feishu')}</div>
723
+ <div class="hint">${t('feishuHint')}</div>
724
+ </div>
725
+ </div>
726
+ <div class="toggle ${feishuEnabled ? 'active' : ''}" data-toggle-messenger="feishu"></div>
727
+ </div>
728
+ ${feishuEnabled ? `
729
+ <div class="row">
730
+ <div>
731
+ <label>${t('appId')}</label>
732
+ <input type="text" id="fsAppId" value="${fs.appId || ''}" placeholder="cli_xxx">
733
+ </div>
734
+ <div>
735
+ <label>${t('appSecret')}</label>
736
+ <input type="password" id="fsAppSecret" value="${fs.appSecret || ''}" placeholder="Your app secret">
737
+ </div>
738
+ </div>
739
+ ` : ''}
740
+ </div>
741
+
742
+ <div class="actions">
743
+ <button class="btn btn-primary" id="saveMessengers">${t('saveMessengers')}</button>
744
+ </div>
745
+ </div>
746
+ `;
747
+ }
748
+
749
+ // ==========================================
750
+ // ACP Remote Agents card
751
+ // ==========================================
752
+ function renderAcpCard() {
753
+ const agents = config.acpAgents || [];
754
+
755
+ const rows = agents.length > 0 ? agents.map((a, i) => `
756
+ <tr data-acp-idx="${i}">
757
+ <td><strong>${a.name}</strong><br><span style="font-size:11px;color:var(--text-dim)">${(a.aliases || []).join(', ')}</span></td>
758
+ <td style="font-family:monospace;font-size:12px">${a.endpoint}</td>
759
+ <td>${a.auth?.type || t('none')}</td>
760
+ <td><div class="toggle ${a.enabled !== false ? 'active' : ''}" data-toggle-acp="${i}"></div></td>
761
+ <td>
762
+ <button class="btn btn-sm" data-test-acp="${i}">${t('test')}</button>
763
+ <button class="btn btn-sm btn-danger" data-del-acp="${i}">${t('del')}</button>
764
+ </td>
765
+ </tr>
766
+ `).join('') : `<tr><td colspan="5" style="color:var(--text-dim);text-align:center;padding:20px">${t('acpNone')}</td></tr>`;
767
+
768
+ return `
769
+ <div class="card">
770
+ <h2>${t('acpTitle')} <span class="badge">${agents.length}</span></h2>
771
+
772
+ <table class="acp-table">
773
+ <thead>
774
+ <tr><th>${t('name')}</th><th>${t('endpoint')}</th><th>${t('auth')}</th><th>${t('enabled')}</th><th></th></tr>
775
+ </thead>
776
+ <tbody>
777
+ ${rows}
778
+ </tbody>
779
+ </table>
780
+
781
+ <hr class="divider">
782
+ <div style="font-size:13px;font-weight:600;margin-bottom:10px">${t('addAgent')}</div>
783
+ <div class="row">
784
+ <div>
785
+ <label>${t('name')}</label>
786
+ <input type="text" id="acpName" placeholder="my-agent">
787
+ </div>
788
+ <div>
789
+ <label>${t('aliases')}</label>
790
+ <input type="text" id="acpAliases" placeholder="ma, agent1">
791
+ </div>
792
+ </div>
793
+ <div class="row">
794
+ <div>
795
+ <label>${t('endpointUrl')}</label>
796
+ <input type="text" id="acpEndpoint" placeholder="http://localhost:8080">
797
+ </div>
798
+ <div>
799
+ <label>${t('authType')}</label>
800
+ <select id="acpAuthType">
801
+ <option value="none">${t('none')}</option>
802
+ <option value="apikey">${t('apikey')}</option>
803
+ <option value="bearer">${t('bearer')}</option>
804
+ </select>
805
+ </div>
806
+ </div>
807
+ <div class="row" id="acpTokenRow" style="display:none">
808
+ <div>
809
+ <label>${t('token')}</label>
810
+ <input type="password" id="acpToken" placeholder="Your auth token">
811
+ </div>
812
+ <div></div>
813
+ </div>
814
+ <div class="actions">
815
+ <button class="btn" id="testAcpNew">${t('testConn')}</button>
816
+ <button class="btn btn-primary" id="addAcp">${t('add')}</button>
817
+ </div>
818
+ </div>
819
+ `;
820
+ }
821
+
822
+ // ==========================================
823
+ // Workspaces card (PR-C)
824
+ // ==========================================
825
+ function renderWorkspacesCard() {
826
+ const list = workspaceList || [];
827
+ // Surface default at the top, locked. Custom workspaces follow.
828
+ const sorted = list.slice().sort((a, b) => {
829
+ if (a.id === 'default') return -1;
830
+ if (b.id === 'default') return 1;
831
+ return a.id.localeCompare(b.id);
832
+ });
833
+
834
+ const escHtml = (s) => String(s || '').replace(/[&<>"']/g, (c) => (
835
+ { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]
836
+ ));
837
+
838
+ const rows = sorted.length ? sorted.map((w) => {
839
+ const isDefault = w.id === 'default';
840
+ const rl = w.rateLimit
841
+ ? `${w.rateLimit.rate}/${w.rateLimit.intervalSec}s · burst ${w.rateLimit.burst}`
842
+ : '—';
843
+ const agents = (w.agents && w.agents.length) ? escHtml(w.agents.join(', ')) : '<span style="color:var(--text-dim)">*</span>';
844
+ const members = (w.members && w.members.length) ? w.members.length : `<span style="color:var(--text-dim)">*</span>`;
845
+ return `
846
+ <tr data-ws-id="${escHtml(w.id)}">
847
+ <td>
848
+ <strong>${escHtml(w.id)}</strong>
849
+ ${isDefault ? `<br><span class="badge">${t('workspaceDefaultLocked')}</span>` : ''}
850
+ <br><span style="font-size:11px;color:var(--text-dim)">${escHtml(w.name || w.id)}</span>
851
+ </td>
852
+ <td style="font-family:monospace;font-size:12px;max-width:240px;word-break:break-word">${agents}</td>
853
+ <td>${members}</td>
854
+ <td style="font-size:12px">${rl}</td>
855
+ <td>
856
+ ${isDefault ? '' : `
857
+ <button class="btn btn-sm" data-edit-ws="${escHtml(w.id)}">${t('workspaceEdit')}</button>
858
+ <button class="btn btn-sm btn-danger" data-del-ws="${escHtml(w.id)}">${t('workspaceDelete')}</button>
859
+ `}
860
+ </td>
861
+ </tr>
862
+ `;
863
+ }).join('') : `<tr><td colspan="5" style="color:var(--text-dim);text-align:center;padding:20px">${t('workspacesNone')}</td></tr>`;
864
+
865
+ return `
866
+ <div class="card">
867
+ <h2>${t('workspacesTitle')} <span class="badge">${list.length}</span></h2>
868
+ <table class="acp-table">
869
+ <thead>
870
+ <tr>
871
+ <th>${t('workspaceId')}</th>
872
+ <th>${t('workspaceAgents')}</th>
873
+ <th>${t('workspaceMembers')}</th>
874
+ <th>${t('workspaceRateLimit')}</th>
875
+ <th></th>
876
+ </tr>
877
+ </thead>
878
+ <tbody>${rows}</tbody>
879
+ </table>
880
+
881
+ <hr class="divider">
882
+ <div style="font-size:13px;font-weight:600;margin-bottom:10px">${t('workspaceAdd')}</div>
883
+ <div class="row">
884
+ <div>
885
+ <label>${t('workspaceId')} <span style="font-weight:400;color:var(--text-dim);font-size:11px">(${t('workspaceIdHelp')})</span></label>
886
+ <input type="text" id="wsId" placeholder="team-data">
887
+ </div>
888
+ <div>
889
+ <label>${t('workspaceName')}</label>
890
+ <input type="text" id="wsName" placeholder="Data team">
891
+ </div>
892
+ </div>
893
+ <div class="row">
894
+ <div>
895
+ <label>${t('workspaceAgents')}</label>
896
+ <input type="text" id="wsAgents" placeholder="opencode, claude-code">
897
+ </div>
898
+ <div>
899
+ <label>${t('workspaceMembers')}</label>
900
+ <input type="text" id="wsMembers" placeholder="user-1, user-2">
901
+ </div>
902
+ </div>
903
+ <div class="row">
904
+ <div>
905
+ <label>${t('workspaceRate')}</label>
906
+ <input type="number" id="wsRate" placeholder="10" min="1">
907
+ </div>
908
+ <div>
909
+ <label>${t('workspaceInterval')}</label>
910
+ <input type="number" id="wsInterval" placeholder="60" min="1">
911
+ </div>
912
+ <div>
913
+ <label>${t('workspaceBurst')}</label>
914
+ <input type="number" id="wsBurst" placeholder="15" min="1">
915
+ </div>
916
+ </div>
917
+ <div class="actions">
918
+ <button class="btn" id="resetWs">${t('workspaceReset')}</button>
919
+ <button class="btn btn-primary" id="saveWs">${t('save')}</button>
920
+ </div>
921
+ </div>
922
+ `;
923
+ }
924
+
925
+ // ==========================================
926
+ // General card
927
+ // ==========================================
928
+ function renderGeneralCard() {
929
+ return `
930
+ <div class="card">
931
+ <h2>${t('general')}</h2>
932
+ <label>${t('webPort')}</label>
933
+ <input type="number" id="webPort" value="${config.webPort || 3000}" min="1024" max="65535" style="max-width:200px">
934
+ <div class="actions">
935
+ <button class="btn btn-primary" id="saveGeneral">${t('save')}</button>
936
+ </div>
937
+ </div>
938
+ `;
939
+ }
940
+
941
+ // ==========================================
942
+ // Event binding
943
+ // ==========================================
944
+ function bindEvents() {
945
+ // Agent toggles
946
+ document.querySelectorAll('[data-toggle-agent]').forEach(el => {
947
+ el.addEventListener('click', () => el.classList.toggle('active'));
948
+ });
949
+
950
+ // Messenger toggles
951
+ document.querySelectorAll('[data-toggle-messenger]').forEach(el => {
952
+ el.addEventListener('click', () => {
953
+ el.classList.toggle('active');
954
+ syncMessengerToggles();
955
+ });
956
+ });
957
+
958
+ // ACP toggles
959
+ document.querySelectorAll('[data-toggle-acp]').forEach(el => {
960
+ el.addEventListener('click', () => el.classList.toggle('active'));
961
+ });
962
+
963
+ // ACP delete buttons
964
+ document.querySelectorAll('[data-del-acp]').forEach(btn => {
965
+ btn.addEventListener('click', async () => {
966
+ const idx = parseInt(btn.dataset.delAcp);
967
+ config.acpAgents.splice(idx, 1);
968
+ await saveConfig();
969
+ render();
970
+ });
971
+ });
972
+
973
+ // ACP test existing
974
+ document.querySelectorAll('[data-test-acp]').forEach(btn => {
975
+ btn.addEventListener('click', async () => {
976
+ const idx = parseInt(btn.dataset.testAcp);
977
+ const agent = config.acpAgents[idx];
978
+ btn.disabled = true;
979
+ btn.textContent = t('testing');
980
+ try {
981
+ const res = await authFetch('/api/agents/acp/test', {
982
+ method: 'POST',
983
+ headers: { 'Content-Type': 'application/json' },
984
+ body: JSON.stringify({ endpoint: agent.endpoint, auth: agent.auth }),
985
+ });
986
+ const data = await res.json();
987
+ if (data.ok) {
988
+ toast(`${t('connected')}: ${data.name}${data.description ? ' — ' + data.description : ''}`, 'success');
989
+ } else {
990
+ toast(`${t('failed')}: ${data.error}`, 'error');
991
+ }
992
+ } catch (err) {
993
+ toast(`${t('error')}: ${err.message}`, 'error');
994
+ }
995
+ btn.disabled = false;
996
+ btn.textContent = t('testConn');
997
+ });
998
+ });
999
+
1000
+ // Add ACP
1001
+ document.getElementById('addAcp')?.addEventListener('click', async () => {
1002
+ const name = document.getElementById('acpName').value.trim();
1003
+ const endpoint = document.getElementById('acpEndpoint').value.trim();
1004
+ if (!name) { toast(t('nameRequired'), 'error'); return; }
1005
+ if (!endpoint) { toast(t('endpointRequired'), 'error'); return; }
1006
+
1007
+ const aliases = document.getElementById('acpAliases').value.split(',').map(s => s.trim()).filter(Boolean);
1008
+ const authType = document.getElementById('acpAuthType').value;
1009
+ const token = document.getElementById('acpToken')?.value.trim();
1010
+ const auth = authType === 'none' ? undefined : { type: authType, token };
1011
+
1012
+ if (!config.acpAgents) config.acpAgents = [];
1013
+ config.acpAgents.push({ name, aliases, endpoint, auth, enabled: true });
1014
+ await saveConfig();
1015
+ render();
1016
+ });
1017
+
1018
+ // Save general
1019
+ document.getElementById('saveGeneral')?.addEventListener('click', async () => {
1020
+ config.webPort = parseInt(document.getElementById('webPort').value) || 3000;
1021
+ await saveConfig();
1022
+ });
1023
+
1024
+ // ==========================================
1025
+ // Workspaces (PR-C)
1026
+ // ==========================================
1027
+
1028
+ // Click "Edit" on a row → pre-fill the form. The same form's Save
1029
+ // button always POSTs, which the server-side handler treats as
1030
+ // upsert (workspaceRegistry.add overwrites by id).
1031
+ document.querySelectorAll('[data-edit-ws]').forEach((btn) => {
1032
+ btn.addEventListener('click', () => {
1033
+ const id = btn.getAttribute('data-edit-ws');
1034
+ const w = (workspaceList || []).find((x) => x.id === id);
1035
+ if (!w) return;
1036
+ const set = (elId, v) => { const el = document.getElementById(elId); if (el) el.value = v; };
1037
+ set('wsId', w.id);
1038
+ set('wsName', w.name || w.id);
1039
+ set('wsAgents', (w.agents || []).join(', '));
1040
+ set('wsMembers', (w.members || []).join(', '));
1041
+ set('wsRate', w.rateLimit?.rate ?? '');
1042
+ set('wsInterval', w.rateLimit?.intervalSec ?? '');
1043
+ set('wsBurst', w.rateLimit?.burst ?? '');
1044
+ // Lock the id field on edit so a typo can't accidentally create
1045
+ // a parallel row instead of updating the chosen one.
1046
+ const idInput = document.getElementById('wsId');
1047
+ if (idInput) idInput.readOnly = true;
1048
+ // Scroll the form into view so the user sees what was filled.
1049
+ document.getElementById('saveWs')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
1050
+ });
1051
+ });
1052
+
1053
+ // Click "Delete" on a row → confirm + DELETE.
1054
+ document.querySelectorAll('[data-del-ws]').forEach((btn) => {
1055
+ btn.addEventListener('click', async () => {
1056
+ const id = btn.getAttribute('data-del-ws');
1057
+ if (!id) return;
1058
+ if (!confirm(t('workspaceConfirmDelete').replace('{id}', id))) return;
1059
+ try {
1060
+ const res = await authFetch(`/api/workspaces/${encodeURIComponent(id)}`, {
1061
+ method: 'DELETE',
1062
+ });
1063
+ if (!res.ok) {
1064
+ const data = await res.json().catch(() => ({}));
1065
+ throw new Error(data.error || res.statusText);
1066
+ }
1067
+ toast(t('workspaceDeleted'), 'success');
1068
+ await reloadWorkspaces();
1069
+ } catch (err) {
1070
+ toast(`${t('error')}: ${err.message}`, 'error');
1071
+ }
1072
+ });
1073
+ });
1074
+
1075
+ // Reset the form (clear inputs + unlock id field).
1076
+ document.getElementById('resetWs')?.addEventListener('click', () => {
1077
+ ['wsId', 'wsName', 'wsAgents', 'wsMembers', 'wsRate', 'wsInterval', 'wsBurst'].forEach((id) => {
1078
+ const el = document.getElementById(id);
1079
+ if (el) {
1080
+ el.value = '';
1081
+ if (id === 'wsId') el.readOnly = false;
1082
+ }
1083
+ });
1084
+ });
1085
+
1086
+ // Save (create or upsert) workspace.
1087
+ document.getElementById('saveWs')?.addEventListener('click', async () => {
1088
+ const id = document.getElementById('wsId').value.trim();
1089
+ if (!id) { toast(t('workspaceId') + ' required', 'error'); return; }
1090
+ if (id === 'default') { toast('"default" is reserved', 'error'); return; }
1091
+ const splitCsv = (s) => String(s || '').split(',').map((x) => x.trim()).filter(Boolean);
1092
+ const payload = {
1093
+ id,
1094
+ name: document.getElementById('wsName').value.trim() || id,
1095
+ agents: splitCsv(document.getElementById('wsAgents').value),
1096
+ members: splitCsv(document.getElementById('wsMembers').value),
1097
+ };
1098
+ const rate = parseInt(document.getElementById('wsRate').value, 10);
1099
+ const intervalSec = parseInt(document.getElementById('wsInterval').value, 10);
1100
+ const burst = parseInt(document.getElementById('wsBurst').value, 10);
1101
+ if (Number.isFinite(rate) || Number.isFinite(intervalSec) || Number.isFinite(burst)) {
1102
+ payload.rateLimit = {
1103
+ rate: Number.isFinite(rate) ? rate : 10,
1104
+ intervalSec: Number.isFinite(intervalSec) ? intervalSec : 60,
1105
+ burst: Number.isFinite(burst) ? burst : 15,
1106
+ };
1107
+ }
1108
+ try {
1109
+ const res = await authFetch('/api/workspaces', {
1110
+ method: 'POST',
1111
+ headers: { 'Content-Type': 'application/json' },
1112
+ body: JSON.stringify(payload),
1113
+ });
1114
+ if (!res.ok) {
1115
+ const data = await res.json().catch(() => ({}));
1116
+ throw new Error(data.error || res.statusText);
1117
+ }
1118
+ toast(t('workspaceSaved'), 'success');
1119
+ // Reset form for next use; reloadWorkspaces re-renders so the
1120
+ // new row appears immediately.
1121
+ document.getElementById('resetWs')?.click();
1122
+ await reloadWorkspaces();
1123
+ } catch (err) {
1124
+ toast(`${t('error')}: ${err.message}`, 'error');
1125
+ }
1126
+ });
1127
+ }
1128
+
1129
+ function syncMessengerToggles() {
1130
+ render();
1131
+ }
1132
+
1133
+ // ==========================================
1134
+ // API helpers
1135
+ // ==========================================
1136
+ async function saveConfig() {
1137
+ try {
1138
+ const payload = {
1139
+ messengers: config.messengers,
1140
+ agents: config.agents,
1141
+ defaultAgent: config.defaultAgent,
1142
+ telegram: config.telegram,
1143
+ feishu: config.feishu,
1144
+ acpAgents: config.acpAgents,
1145
+ webPort: config.webPort,
1146
+ };
1147
+
1148
+ const res = await authFetch('/api/config', {
1149
+ method: 'PUT',
1150
+ headers: { 'Content-Type': 'application/json' },
1151
+ body: JSON.stringify(payload),
1152
+ });
1153
+
1154
+ if (!res.ok) {
1155
+ const data = await res.json();
1156
+ throw new Error(data.error || t('saveFailed'));
1157
+ }
1158
+
1159
+ toast(t('savedMsg'), 'success');
1160
+ await init();
1161
+ } catch (err) {
1162
+ toast(`${t('saveFailed')}: ${err.message}`, 'error');
1163
+ }
1164
+ }
1165
+
1166
+ let toastTimer;
1167
+ function toast(msg, type) {
1168
+ toastEl.textContent = msg;
1169
+ toastEl.className = `toast ${type} show`;
1170
+ clearTimeout(toastTimer);
1171
+ toastTimer = setTimeout(() => {
1172
+ toastEl.classList.remove('show');
1173
+ }, 3000);
1174
+ }
1175
+
1176
+ // Init
1177
+ applyLang();
1178
+ init();
1179
+ </script>
1180
+ </body>
1181
+ </html>