agim-cli 1.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 (453) hide show
  1. package/CHANGELOG.md +1234 -0
  2. package/LICENSE +21 -0
  3. package/README.md +422 -0
  4. package/README.zh-CN.md +414 -0
  5. package/dist/cli-ui/cmd-handlers.d.ts +11 -0
  6. package/dist/cli-ui/cmd-handlers.d.ts.map +1 -0
  7. package/dist/cli-ui/cmd-handlers.js +240 -0
  8. package/dist/cli-ui/cmd-handlers.js.map +1 -0
  9. package/dist/cli-ui/config-wizard.d.ts +3 -0
  10. package/dist/cli-ui/config-wizard.d.ts.map +1 -0
  11. package/dist/cli-ui/config-wizard.js +851 -0
  12. package/dist/cli-ui/config-wizard.js.map +1 -0
  13. package/dist/cli-ui/entry-menu.d.ts +28 -0
  14. package/dist/cli-ui/entry-menu.d.ts.map +1 -0
  15. package/dist/cli-ui/entry-menu.js +50 -0
  16. package/dist/cli-ui/entry-menu.js.map +1 -0
  17. package/dist/cli-ui/env-file.d.ts +35 -0
  18. package/dist/cli-ui/env-file.d.ts.map +1 -0
  19. package/dist/cli-ui/env-file.js +163 -0
  20. package/dist/cli-ui/env-file.js.map +1 -0
  21. package/dist/cli-ui/i18n.d.ts +204 -0
  22. package/dist/cli-ui/i18n.d.ts.map +1 -0
  23. package/dist/cli-ui/i18n.js +455 -0
  24. package/dist/cli-ui/i18n.js.map +1 -0
  25. package/dist/cli-ui/lang-picker.d.ts +10 -0
  26. package/dist/cli-ui/lang-picker.d.ts.map +1 -0
  27. package/dist/cli-ui/lang-picker.js +33 -0
  28. package/dist/cli-ui/lang-picker.js.map +1 -0
  29. package/dist/cli-ui/paths.d.ts +4 -0
  30. package/dist/cli-ui/paths.d.ts.map +1 -0
  31. package/dist/cli-ui/paths.js +11 -0
  32. package/dist/cli-ui/paths.js.map +1 -0
  33. package/dist/cli-ui/prompts.d.ts +65 -0
  34. package/dist/cli-ui/prompts.d.ts.map +1 -0
  35. package/dist/cli-ui/prompts.js +125 -0
  36. package/dist/cli-ui/prompts.js.map +1 -0
  37. package/dist/cli-ui/service.d.ts +41 -0
  38. package/dist/cli-ui/service.d.ts.map +1 -0
  39. package/dist/cli-ui/service.js +241 -0
  40. package/dist/cli-ui/service.js.map +1 -0
  41. package/dist/cli.d.ts +3 -0
  42. package/dist/cli.d.ts.map +1 -0
  43. package/dist/cli.js +1143 -0
  44. package/dist/cli.js.map +1 -0
  45. package/dist/core/acp-server.d.ts +8 -0
  46. package/dist/core/acp-server.d.ts.map +1 -0
  47. package/dist/core/acp-server.js +266 -0
  48. package/dist/core/acp-server.js.map +1 -0
  49. package/dist/core/agent-base.d.ts +94 -0
  50. package/dist/core/agent-base.d.ts.map +1 -0
  51. package/dist/core/agent-base.js +373 -0
  52. package/dist/core/agent-base.js.map +1 -0
  53. package/dist/core/agent-cwd.d.ts +48 -0
  54. package/dist/core/agent-cwd.d.ts.map +1 -0
  55. package/dist/core/agent-cwd.js +181 -0
  56. package/dist/core/agent-cwd.js.map +1 -0
  57. package/dist/core/agent-helper.d.ts +65 -0
  58. package/dist/core/agent-helper.d.ts.map +1 -0
  59. package/dist/core/agent-helper.js +150 -0
  60. package/dist/core/agent-helper.js.map +1 -0
  61. package/dist/core/agim-paths.d.ts +10 -0
  62. package/dist/core/agim-paths.d.ts.map +1 -0
  63. package/dist/core/agim-paths.js +64 -0
  64. package/dist/core/agim-paths.js.map +1 -0
  65. package/dist/core/approval-bus.d.ts +300 -0
  66. package/dist/core/approval-bus.d.ts.map +1 -0
  67. package/dist/core/approval-bus.js +990 -0
  68. package/dist/core/approval-bus.js.map +1 -0
  69. package/dist/core/approval-router.d.ts +101 -0
  70. package/dist/core/approval-router.d.ts.map +1 -0
  71. package/dist/core/approval-router.js +540 -0
  72. package/dist/core/approval-router.js.map +1 -0
  73. package/dist/core/audit-log.d.ts +55 -0
  74. package/dist/core/audit-log.d.ts.map +1 -0
  75. package/dist/core/audit-log.js +203 -0
  76. package/dist/core/audit-log.js.map +1 -0
  77. package/dist/core/bgjob-reader.d.ts +65 -0
  78. package/dist/core/bgjob-reader.d.ts.map +1 -0
  79. package/dist/core/bgjob-reader.js +212 -0
  80. package/dist/core/bgjob-reader.js.map +1 -0
  81. package/dist/core/circuit-breaker.d.ts +37 -0
  82. package/dist/core/circuit-breaker.d.ts.map +1 -0
  83. package/dist/core/circuit-breaker.js +115 -0
  84. package/dist/core/circuit-breaker.js.map +1 -0
  85. package/dist/core/commands/agent.d.ts +4 -0
  86. package/dist/core/commands/agent.d.ts.map +1 -0
  87. package/dist/core/commands/agent.js +40 -0
  88. package/dist/core/commands/agent.js.map +1 -0
  89. package/dist/core/commands/approval.d.ts +3 -0
  90. package/dist/core/commands/approval.d.ts.map +1 -0
  91. package/dist/core/commands/approval.js +85 -0
  92. package/dist/core/commands/approval.js.map +1 -0
  93. package/dist/core/commands/audit.d.ts +3 -0
  94. package/dist/core/commands/audit.d.ts.map +1 -0
  95. package/dist/core/commands/audit.js +84 -0
  96. package/dist/core/commands/audit.js.map +1 -0
  97. package/dist/core/commands/builtin.d.ts +3 -0
  98. package/dist/core/commands/builtin.d.ts.map +1 -0
  99. package/dist/core/commands/builtin.js +304 -0
  100. package/dist/core/commands/builtin.js.map +1 -0
  101. package/dist/core/commands/cron.d.ts +3 -0
  102. package/dist/core/commands/cron.d.ts.map +1 -0
  103. package/dist/core/commands/cron.js +128 -0
  104. package/dist/core/commands/cron.js.map +1 -0
  105. package/dist/core/commands/job.d.ts +3 -0
  106. package/dist/core/commands/job.d.ts.map +1 -0
  107. package/dist/core/commands/job.js +195 -0
  108. package/dist/core/commands/job.js.map +1 -0
  109. package/dist/core/commands/memo.d.ts +3 -0
  110. package/dist/core/commands/memo.d.ts.map +1 -0
  111. package/dist/core/commands/memo.js +151 -0
  112. package/dist/core/commands/memo.js.map +1 -0
  113. package/dist/core/commands/model.d.ts +9 -0
  114. package/dist/core/commands/model.d.ts.map +1 -0
  115. package/dist/core/commands/model.js +183 -0
  116. package/dist/core/commands/model.js.map +1 -0
  117. package/dist/core/commands/plan.d.ts +3 -0
  118. package/dist/core/commands/plan.d.ts.map +1 -0
  119. package/dist/core/commands/plan.js +75 -0
  120. package/dist/core/commands/plan.js.map +1 -0
  121. package/dist/core/commands/remind.d.ts +3 -0
  122. package/dist/core/commands/remind.d.ts.map +1 -0
  123. package/dist/core/commands/remind.js +271 -0
  124. package/dist/core/commands/remind.js.map +1 -0
  125. package/dist/core/commands/router.d.ts +3 -0
  126. package/dist/core/commands/router.d.ts.map +1 -0
  127. package/dist/core/commands/router.js +71 -0
  128. package/dist/core/commands/router.js.map +1 -0
  129. package/dist/core/commands/sessions.d.ts +3 -0
  130. package/dist/core/commands/sessions.d.ts.map +1 -0
  131. package/dist/core/commands/sessions.js +88 -0
  132. package/dist/core/commands/sessions.js.map +1 -0
  133. package/dist/core/commands/stats.d.ts +3 -0
  134. package/dist/core/commands/stats.d.ts.map +1 -0
  135. package/dist/core/commands/stats.js +73 -0
  136. package/dist/core/commands/stats.js.map +1 -0
  137. package/dist/core/commands/think.d.ts +3 -0
  138. package/dist/core/commands/think.d.ts.map +1 -0
  139. package/dist/core/commands/think.js +28 -0
  140. package/dist/core/commands/think.js.map +1 -0
  141. package/dist/core/commands/workspaces.d.ts +3 -0
  142. package/dist/core/commands/workspaces.d.ts.map +1 -0
  143. package/dist/core/commands/workspaces.js +47 -0
  144. package/dist/core/commands/workspaces.js.map +1 -0
  145. package/dist/core/config-schema.d.ts +60 -0
  146. package/dist/core/config-schema.d.ts.map +1 -0
  147. package/dist/core/config-schema.js +75 -0
  148. package/dist/core/config-schema.js.map +1 -0
  149. package/dist/core/coord-systems.d.ts +65 -0
  150. package/dist/core/coord-systems.d.ts.map +1 -0
  151. package/dist/core/coord-systems.js +229 -0
  152. package/dist/core/coord-systems.js.map +1 -0
  153. package/dist/core/cron.d.ts +29 -0
  154. package/dist/core/cron.d.ts.map +1 -0
  155. package/dist/core/cron.js +184 -0
  156. package/dist/core/cron.js.map +1 -0
  157. package/dist/core/event-bus.d.ts +80 -0
  158. package/dist/core/event-bus.d.ts.map +1 -0
  159. package/dist/core/event-bus.js +62 -0
  160. package/dist/core/event-bus.js.map +1 -0
  161. package/dist/core/intent-llm.d.ts +27 -0
  162. package/dist/core/intent-llm.d.ts.map +1 -0
  163. package/dist/core/intent-llm.js +170 -0
  164. package/dist/core/intent-llm.js.map +1 -0
  165. package/dist/core/intent.d.ts +12 -0
  166. package/dist/core/intent.d.ts.map +1 -0
  167. package/dist/core/intent.js +187 -0
  168. package/dist/core/intent.js.map +1 -0
  169. package/dist/core/job-board.d.ts +82 -0
  170. package/dist/core/job-board.d.ts.map +1 -0
  171. package/dist/core/job-board.js +379 -0
  172. package/dist/core/job-board.js.map +1 -0
  173. package/dist/core/location-context.d.ts +32 -0
  174. package/dist/core/location-context.d.ts.map +1 -0
  175. package/dist/core/location-context.js +69 -0
  176. package/dist/core/location-context.js.map +1 -0
  177. package/dist/core/location-token.d.ts +57 -0
  178. package/dist/core/location-token.d.ts.map +1 -0
  179. package/dist/core/location-token.js +128 -0
  180. package/dist/core/location-token.js.map +1 -0
  181. package/dist/core/logger.d.ts +6 -0
  182. package/dist/core/logger.d.ts.map +1 -0
  183. package/dist/core/logger.js +54 -0
  184. package/dist/core/logger.js.map +1 -0
  185. package/dist/core/memo-rpc.d.ts +13 -0
  186. package/dist/core/memo-rpc.d.ts.map +1 -0
  187. package/dist/core/memo-rpc.js +288 -0
  188. package/dist/core/memo-rpc.js.map +1 -0
  189. package/dist/core/memos.d.ts +163 -0
  190. package/dist/core/memos.d.ts.map +1 -0
  191. package/dist/core/memos.js +502 -0
  192. package/dist/core/memos.js.map +1 -0
  193. package/dist/core/metrics.d.ts +55 -0
  194. package/dist/core/metrics.d.ts.map +1 -0
  195. package/dist/core/metrics.js +291 -0
  196. package/dist/core/metrics.js.map +1 -0
  197. package/dist/core/onboarding.d.ts +99 -0
  198. package/dist/core/onboarding.d.ts.map +1 -0
  199. package/dist/core/onboarding.js +426 -0
  200. package/dist/core/onboarding.js.map +1 -0
  201. package/dist/core/pending-reminder.d.ts +25 -0
  202. package/dist/core/pending-reminder.d.ts.map +1 -0
  203. package/dist/core/pending-reminder.js +53 -0
  204. package/dist/core/pending-reminder.js.map +1 -0
  205. package/dist/core/rate-limiter.d.ts +44 -0
  206. package/dist/core/rate-limiter.d.ts.map +1 -0
  207. package/dist/core/rate-limiter.js +115 -0
  208. package/dist/core/rate-limiter.js.map +1 -0
  209. package/dist/core/registry.d.ts +32 -0
  210. package/dist/core/registry.d.ts.map +1 -0
  211. package/dist/core/registry.js +126 -0
  212. package/dist/core/registry.js.map +1 -0
  213. package/dist/core/remind-intent.d.ts +25 -0
  214. package/dist/core/remind-intent.d.ts.map +1 -0
  215. package/dist/core/remind-intent.js +196 -0
  216. package/dist/core/remind-intent.js.map +1 -0
  217. package/dist/core/reminder-rpc.d.ts +17 -0
  218. package/dist/core/reminder-rpc.d.ts.map +1 -0
  219. package/dist/core/reminder-rpc.js +169 -0
  220. package/dist/core/reminder-rpc.js.map +1 -0
  221. package/dist/core/reminders.d.ts +159 -0
  222. package/dist/core/reminders.d.ts.map +1 -0
  223. package/dist/core/reminders.js +977 -0
  224. package/dist/core/reminders.js.map +1 -0
  225. package/dist/core/router.d.ts +55 -0
  226. package/dist/core/router.d.ts.map +1 -0
  227. package/dist/core/router.js +497 -0
  228. package/dist/core/router.js.map +1 -0
  229. package/dist/core/schedule.d.ts +65 -0
  230. package/dist/core/schedule.d.ts.map +1 -0
  231. package/dist/core/schedule.js +323 -0
  232. package/dist/core/schedule.js.map +1 -0
  233. package/dist/core/session.d.ts +182 -0
  234. package/dist/core/session.d.ts.map +1 -0
  235. package/dist/core/session.js +807 -0
  236. package/dist/core/session.js.map +1 -0
  237. package/dist/core/sqlite-helper.d.ts +37 -0
  238. package/dist/core/sqlite-helper.d.ts.map +1 -0
  239. package/dist/core/sqlite-helper.js +79 -0
  240. package/dist/core/sqlite-helper.js.map +1 -0
  241. package/dist/core/transcribe.d.ts +25 -0
  242. package/dist/core/transcribe.d.ts.map +1 -0
  243. package/dist/core/transcribe.js +217 -0
  244. package/dist/core/transcribe.js.map +1 -0
  245. package/dist/core/types.d.ts +360 -0
  246. package/dist/core/types.d.ts.map +1 -0
  247. package/dist/core/types.js +3 -0
  248. package/dist/core/types.js.map +1 -0
  249. package/dist/core/workspace.d.ts +67 -0
  250. package/dist/core/workspace.d.ts.map +1 -0
  251. package/dist/core/workspace.js +113 -0
  252. package/dist/core/workspace.js.map +1 -0
  253. package/dist/index.d.ts +5 -0
  254. package/dist/index.d.ts.map +1 -0
  255. package/dist/index.js +6 -0
  256. package/dist/index.js.map +1 -0
  257. package/dist/plugins/agents/acp/acp-adapter.d.ts +16 -0
  258. package/dist/plugins/agents/acp/acp-adapter.d.ts.map +1 -0
  259. package/dist/plugins/agents/acp/acp-adapter.js +49 -0
  260. package/dist/plugins/agents/acp/acp-adapter.js.map +1 -0
  261. package/dist/plugins/agents/acp/acp-client.d.ts +32 -0
  262. package/dist/plugins/agents/acp/acp-client.d.ts.map +1 -0
  263. package/dist/plugins/agents/acp/acp-client.js +177 -0
  264. package/dist/plugins/agents/acp/acp-client.js.map +1 -0
  265. package/dist/plugins/agents/acp/discovery.d.ts +19 -0
  266. package/dist/plugins/agents/acp/discovery.d.ts.map +1 -0
  267. package/dist/plugins/agents/acp/discovery.js +111 -0
  268. package/dist/plugins/agents/acp/discovery.js.map +1 -0
  269. package/dist/plugins/agents/acp/index.d.ts +4 -0
  270. package/dist/plugins/agents/acp/index.d.ts.map +1 -0
  271. package/dist/plugins/agents/acp/index.js +4 -0
  272. package/dist/plugins/agents/acp/index.js.map +1 -0
  273. package/dist/plugins/agents/acp/types.d.ts +62 -0
  274. package/dist/plugins/agents/acp/types.d.ts.map +1 -0
  275. package/dist/plugins/agents/acp/types.js +5 -0
  276. package/dist/plugins/agents/acp/types.js.map +1 -0
  277. package/dist/plugins/agents/claude-code/index.d.ts +25 -0
  278. package/dist/plugins/agents/claude-code/index.d.ts.map +1 -0
  279. package/dist/plugins/agents/claude-code/index.js +184 -0
  280. package/dist/plugins/agents/claude-code/index.js.map +1 -0
  281. package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts +59 -0
  282. package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts.map +1 -0
  283. package/dist/plugins/agents/claude-code/mcp-approval-server.js +645 -0
  284. package/dist/plugins/agents/claude-code/mcp-approval-server.js.map +1 -0
  285. package/dist/plugins/agents/codex/build-mcp-cli-args.d.ts +28 -0
  286. package/dist/plugins/agents/codex/build-mcp-cli-args.d.ts.map +1 -0
  287. package/dist/plugins/agents/codex/build-mcp-cli-args.js +74 -0
  288. package/dist/plugins/agents/codex/build-mcp-cli-args.js.map +1 -0
  289. package/dist/plugins/agents/codex/index.d.ts +53 -0
  290. package/dist/plugins/agents/codex/index.d.ts.map +1 -0
  291. package/dist/plugins/agents/codex/index.js +341 -0
  292. package/dist/plugins/agents/codex/index.js.map +1 -0
  293. package/dist/plugins/agents/copilot/index.d.ts +35 -0
  294. package/dist/plugins/agents/copilot/index.d.ts.map +1 -0
  295. package/dist/plugins/agents/copilot/index.js +182 -0
  296. package/dist/plugins/agents/copilot/index.js.map +1 -0
  297. package/dist/plugins/agents/opencode/ensure-mcp-config.d.ts +11 -0
  298. package/dist/plugins/agents/opencode/ensure-mcp-config.d.ts.map +1 -0
  299. package/dist/plugins/agents/opencode/ensure-mcp-config.js +100 -0
  300. package/dist/plugins/agents/opencode/ensure-mcp-config.js.map +1 -0
  301. package/dist/plugins/agents/opencode/index.d.ts +5 -0
  302. package/dist/plugins/agents/opencode/index.d.ts.map +1 -0
  303. package/dist/plugins/agents/opencode/index.js +30 -0
  304. package/dist/plugins/agents/opencode/index.js.map +1 -0
  305. package/dist/plugins/agents/opencode/opencode-http-adapter.d.ts +166 -0
  306. package/dist/plugins/agents/opencode/opencode-http-adapter.d.ts.map +1 -0
  307. package/dist/plugins/agents/opencode/opencode-http-adapter.js +682 -0
  308. package/dist/plugins/agents/opencode/opencode-http-adapter.js.map +1 -0
  309. package/dist/plugins/agents/opencode/opencode-stdio-adapter.d.ts +32 -0
  310. package/dist/plugins/agents/opencode/opencode-stdio-adapter.d.ts.map +1 -0
  311. package/dist/plugins/agents/opencode/opencode-stdio-adapter.js +137 -0
  312. package/dist/plugins/agents/opencode/opencode-stdio-adapter.js.map +1 -0
  313. package/dist/plugins/agents/opencode/serve-manager.d.ts +27 -0
  314. package/dist/plugins/agents/opencode/serve-manager.d.ts.map +1 -0
  315. package/dist/plugins/agents/opencode/serve-manager.js +194 -0
  316. package/dist/plugins/agents/opencode/serve-manager.js.map +1 -0
  317. package/dist/plugins/messengers/dingtalk/dingtalk-adapter.d.ts +57 -0
  318. package/dist/plugins/messengers/dingtalk/dingtalk-adapter.d.ts.map +1 -0
  319. package/dist/plugins/messengers/dingtalk/dingtalk-adapter.js +409 -0
  320. package/dist/plugins/messengers/dingtalk/dingtalk-adapter.js.map +1 -0
  321. package/dist/plugins/messengers/dingtalk/dingtalk-client.d.ts +48 -0
  322. package/dist/plugins/messengers/dingtalk/dingtalk-client.d.ts.map +1 -0
  323. package/dist/plugins/messengers/dingtalk/dingtalk-client.js +236 -0
  324. package/dist/plugins/messengers/dingtalk/dingtalk-client.js.map +1 -0
  325. package/dist/plugins/messengers/dingtalk/index.d.ts +3 -0
  326. package/dist/plugins/messengers/dingtalk/index.d.ts.map +1 -0
  327. package/dist/plugins/messengers/dingtalk/index.js +3 -0
  328. package/dist/plugins/messengers/dingtalk/index.js.map +1 -0
  329. package/dist/plugins/messengers/dingtalk/link-coords.d.ts +23 -0
  330. package/dist/plugins/messengers/dingtalk/link-coords.d.ts.map +1 -0
  331. package/dist/plugins/messengers/dingtalk/link-coords.js +89 -0
  332. package/dist/plugins/messengers/dingtalk/link-coords.js.map +1 -0
  333. package/dist/plugins/messengers/dingtalk/media-store.d.ts +16 -0
  334. package/dist/plugins/messengers/dingtalk/media-store.d.ts.map +1 -0
  335. package/dist/plugins/messengers/dingtalk/media-store.js +77 -0
  336. package/dist/plugins/messengers/dingtalk/media-store.js.map +1 -0
  337. package/dist/plugins/messengers/dingtalk/types.d.ts +82 -0
  338. package/dist/plugins/messengers/dingtalk/types.d.ts.map +1 -0
  339. package/dist/plugins/messengers/dingtalk/types.js +14 -0
  340. package/dist/plugins/messengers/dingtalk/types.js.map +1 -0
  341. package/dist/plugins/messengers/discord/discord-adapter.d.ts +21 -0
  342. package/dist/plugins/messengers/discord/discord-adapter.d.ts.map +1 -0
  343. package/dist/plugins/messengers/discord/discord-adapter.js +238 -0
  344. package/dist/plugins/messengers/discord/discord-adapter.js.map +1 -0
  345. package/dist/plugins/messengers/discord/index.d.ts +4 -0
  346. package/dist/plugins/messengers/discord/index.d.ts.map +1 -0
  347. package/dist/plugins/messengers/discord/index.js +4 -0
  348. package/dist/plugins/messengers/discord/index.js.map +1 -0
  349. package/dist/plugins/messengers/discord/markdown-to-discord.d.ts +11 -0
  350. package/dist/plugins/messengers/discord/markdown-to-discord.d.ts.map +1 -0
  351. package/dist/plugins/messengers/discord/markdown-to-discord.js +59 -0
  352. package/dist/plugins/messengers/discord/markdown-to-discord.js.map +1 -0
  353. package/dist/plugins/messengers/discord/types.d.ts +9 -0
  354. package/dist/plugins/messengers/discord/types.d.ts.map +1 -0
  355. package/dist/plugins/messengers/discord/types.js +3 -0
  356. package/dist/plugins/messengers/discord/types.js.map +1 -0
  357. package/dist/plugins/messengers/email/email-adapter.d.ts +33 -0
  358. package/dist/plugins/messengers/email/email-adapter.d.ts.map +1 -0
  359. package/dist/plugins/messengers/email/email-adapter.js +137 -0
  360. package/dist/plugins/messengers/email/email-adapter.js.map +1 -0
  361. package/dist/plugins/messengers/feishu/card-builder.d.ts +23 -0
  362. package/dist/plugins/messengers/feishu/card-builder.d.ts.map +1 -0
  363. package/dist/plugins/messengers/feishu/card-builder.js +89 -0
  364. package/dist/plugins/messengers/feishu/card-builder.js.map +1 -0
  365. package/dist/plugins/messengers/feishu/feishu-adapter.d.ts +23 -0
  366. package/dist/plugins/messengers/feishu/feishu-adapter.d.ts.map +1 -0
  367. package/dist/plugins/messengers/feishu/feishu-adapter.js +250 -0
  368. package/dist/plugins/messengers/feishu/feishu-adapter.js.map +1 -0
  369. package/dist/plugins/messengers/feishu/feishu-client.d.ts +43 -0
  370. package/dist/plugins/messengers/feishu/feishu-client.d.ts.map +1 -0
  371. package/dist/plugins/messengers/feishu/feishu-client.js +118 -0
  372. package/dist/plugins/messengers/feishu/feishu-client.js.map +1 -0
  373. package/dist/plugins/messengers/feishu/index.d.ts +4 -0
  374. package/dist/plugins/messengers/feishu/index.d.ts.map +1 -0
  375. package/dist/plugins/messengers/feishu/index.js +4 -0
  376. package/dist/plugins/messengers/feishu/index.js.map +1 -0
  377. package/dist/plugins/messengers/feishu/types.d.ts +113 -0
  378. package/dist/plugins/messengers/feishu/types.d.ts.map +1 -0
  379. package/dist/plugins/messengers/feishu/types.js +4 -0
  380. package/dist/plugins/messengers/feishu/types.js.map +1 -0
  381. package/dist/plugins/messengers/telegram/index.d.ts +4 -0
  382. package/dist/plugins/messengers/telegram/index.d.ts.map +1 -0
  383. package/dist/plugins/messengers/telegram/index.js +4 -0
  384. package/dist/plugins/messengers/telegram/index.js.map +1 -0
  385. package/dist/plugins/messengers/telegram/markdown-to-html.d.ts +5 -0
  386. package/dist/plugins/messengers/telegram/markdown-to-html.d.ts.map +1 -0
  387. package/dist/plugins/messengers/telegram/markdown-to-html.js +186 -0
  388. package/dist/plugins/messengers/telegram/markdown-to-html.js.map +1 -0
  389. package/dist/plugins/messengers/telegram/media-download.d.ts +59 -0
  390. package/dist/plugins/messengers/telegram/media-download.d.ts.map +1 -0
  391. package/dist/plugins/messengers/telegram/media-download.js +228 -0
  392. package/dist/plugins/messengers/telegram/media-download.js.map +1 -0
  393. package/dist/plugins/messengers/telegram/telegram-adapter.d.ts +77 -0
  394. package/dist/plugins/messengers/telegram/telegram-adapter.d.ts.map +1 -0
  395. package/dist/plugins/messengers/telegram/telegram-adapter.js +880 -0
  396. package/dist/plugins/messengers/telegram/telegram-adapter.js.map +1 -0
  397. package/dist/plugins/messengers/telegram/types.d.ts +47 -0
  398. package/dist/plugins/messengers/telegram/types.d.ts.map +1 -0
  399. package/dist/plugins/messengers/telegram/types.js +3 -0
  400. package/dist/plugins/messengers/telegram/types.js.map +1 -0
  401. package/dist/plugins/messengers/wechat/context-store.d.ts +18 -0
  402. package/dist/plugins/messengers/wechat/context-store.d.ts.map +1 -0
  403. package/dist/plugins/messengers/wechat/context-store.js +105 -0
  404. package/dist/plugins/messengers/wechat/context-store.js.map +1 -0
  405. package/dist/plugins/messengers/wechat/ilink-adapter.d.ts +71 -0
  406. package/dist/plugins/messengers/wechat/ilink-adapter.d.ts.map +1 -0
  407. package/dist/plugins/messengers/wechat/ilink-adapter.js +664 -0
  408. package/dist/plugins/messengers/wechat/ilink-adapter.js.map +1 -0
  409. package/dist/plugins/messengers/wechat/ilink-client.d.ts +75 -0
  410. package/dist/plugins/messengers/wechat/ilink-client.d.ts.map +1 -0
  411. package/dist/plugins/messengers/wechat/ilink-client.js +331 -0
  412. package/dist/plugins/messengers/wechat/ilink-client.js.map +1 -0
  413. package/dist/plugins/messengers/wechat/ilink-types.d.ts +181 -0
  414. package/dist/plugins/messengers/wechat/ilink-types.d.ts.map +1 -0
  415. package/dist/plugins/messengers/wechat/ilink-types.js +22 -0
  416. package/dist/plugins/messengers/wechat/ilink-types.js.map +1 -0
  417. package/dist/plugins/messengers/wechat/media-download.d.ts +32 -0
  418. package/dist/plugins/messengers/wechat/media-download.d.ts.map +1 -0
  419. package/dist/plugins/messengers/wechat/media-download.js +78 -0
  420. package/dist/plugins/messengers/wechat/media-download.js.map +1 -0
  421. package/dist/scripts/migrate-gcj02-to-wgs84.d.ts +3 -0
  422. package/dist/scripts/migrate-gcj02-to-wgs84.d.ts.map +1 -0
  423. package/dist/scripts/migrate-gcj02-to-wgs84.js +52 -0
  424. package/dist/scripts/migrate-gcj02-to-wgs84.js.map +1 -0
  425. package/dist/utils/backoff.d.ts +35 -0
  426. package/dist/utils/backoff.d.ts.map +1 -0
  427. package/dist/utils/backoff.js +59 -0
  428. package/dist/utils/backoff.js.map +1 -0
  429. package/dist/utils/cross-platform.d.ts +26 -0
  430. package/dist/utils/cross-platform.d.ts.map +1 -0
  431. package/dist/utils/cross-platform.js +58 -0
  432. package/dist/utils/cross-platform.js.map +1 -0
  433. package/dist/utils/message-split.d.ts +14 -0
  434. package/dist/utils/message-split.d.ts.map +1 -0
  435. package/dist/utils/message-split.js +65 -0
  436. package/dist/utils/message-split.js.map +1 -0
  437. package/dist/utils/safe-equal.d.ts +2 -0
  438. package/dist/utils/safe-equal.d.ts.map +1 -0
  439. package/dist/utils/safe-equal.js +11 -0
  440. package/dist/utils/safe-equal.js.map +1 -0
  441. package/dist/web/public/_app.js +196 -0
  442. package/dist/web/public/index.html +936 -0
  443. package/dist/web/public/loc.html +305 -0
  444. package/dist/web/public/login.html +106 -0
  445. package/dist/web/public/memos.html +271 -0
  446. package/dist/web/public/reminders.html +234 -0
  447. package/dist/web/public/settings.html +1355 -0
  448. package/dist/web/public/tasks.html +1835 -0
  449. package/dist/web/server.d.ts +12 -0
  450. package/dist/web/server.d.ts.map +1 -0
  451. package/dist/web/server.js +2399 -0
  452. package/dist/web/server.js.map +1 -0
  453. package/package.json +92 -0
@@ -0,0 +1,1355 @@
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" aria-label="Back to chat"></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
+ function esc(s) {
520
+ return String(s || '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
521
+ }
522
+
523
+ // Apply i18n to static elements
524
+ function applyLang() {
525
+ document.title = t('title');
526
+ document.documentElement.lang = window.__lang === 'zh' ? 'zh-CN' : 'en';
527
+ document.getElementById('backToChat').textContent = t('backToChat');
528
+ document.getElementById('settingsTitle').textContent = t('settingsTitle');
529
+ }
530
+
531
+ // Theme toggle (light / dark / system). _app.js applied theme already;
532
+ // here we wire the click handler so cycling re-renders the icon/label.
533
+ if (window.imhub) imhub.theme.bindToggle(document.getElementById('theme-toggle'));
534
+
535
+ // Language selector
536
+ const langSelect = document.getElementById('langSelect');
537
+ langSelect.value = window.__lang;
538
+ langSelect.addEventListener('change', () => {
539
+ window.__lang = langSelect.value;
540
+ localStorage.setItem('im-hub-lang', window.__lang);
541
+ applyLang();
542
+ render();
543
+ });
544
+
545
+ // State
546
+ let config = null;
547
+ /** Workspace list pulled from /api/workspaces?full=1. Null until first
548
+ * fetch completes; render falls through with empty rows in that case
549
+ * and refetch happens after init() completes. */
550
+ let workspaceList = null;
551
+ let agentStatus = {};
552
+
553
+ // DOM
554
+ const app = document.getElementById('app');
555
+ const toastEl = document.getElementById('toast');
556
+
557
+ // Known built-in agents with install info
558
+ const BUILTIN_AGENTS = {
559
+ 'claude-code': { aliases: ['cc', 'claude'], pkg: '@anthropic-ai/claude-code', cmd: 'claude' },
560
+ 'codex': { aliases: ['cx'], pkg: '@openai/codex', cmd: 'codex' },
561
+ 'copilot': { aliases: ['co'], pkg: '@github/copilot', cmd: 'copilot' },
562
+ 'opencode': { aliases: ['oc'], pkg: 'opencode-ai', cmd: 'opencode' },
563
+ };
564
+
565
+ // Helper: add auth token to all API requests (uses session cookie;
566
+ // falls back to IMHUB_TOKEN header if set for backward compat).
567
+ function authFetch(url, init = {}) {
568
+ const token = window.IMHUB_TOKEN || '';
569
+ const headers = { ...(init.headers || {}) };
570
+ if (token) headers['X-IM-Hub-Token'] = token;
571
+ return fetch(url, { ...init, headers, credentials: 'same-origin' });
572
+ }
573
+
574
+ // Fetch config on load
575
+ async function init() {
576
+ try {
577
+ const res = await authFetch('/api/config');
578
+ if (!res.ok) throw new Error(t('loadFailed'));
579
+ const data = await res.json();
580
+ config = data;
581
+ agentStatus = data.agentStatus || {};
582
+ // Workspaces come from a separate endpoint so the registry's
583
+ // runtime state (which may include extra/removed entries via
584
+ // REST CRUD since process start) is the source of truth.
585
+ try {
586
+ const wres = await authFetch('/api/workspaces?full=1');
587
+ if (wres.ok) workspaceList = (await wres.json()).workspaces || [];
588
+ } catch { /* fall through with workspaceList = null */ }
589
+ render();
590
+ } catch (err) {
591
+ app.innerHTML = `<div class="loading" style="color:var(--red)">${t('loadFailed')}: ${esc(err.message)}</div>`;
592
+ }
593
+ }
594
+
595
+ /** Refetch the workspace list and re-render just the workspaces card.
596
+ * Cheaper than a full settings re-render for create/update/delete. */
597
+ async function reloadWorkspaces() {
598
+ try {
599
+ const wres = await authFetch('/api/workspaces?full=1');
600
+ if (wres.ok) workspaceList = (await wres.json()).workspaces || [];
601
+ } catch { /* keep stale list, surface via toast on next save */ }
602
+ render();
603
+ }
604
+
605
+ function render() {
606
+ app.innerHTML = `
607
+ <h1>${t('h1')}</h1>
608
+ ${renderAgentsCard()}
609
+ ${renderMessengersCard()}
610
+ ${renderAcpCard()}
611
+ ${renderWorkspacesCard()}
612
+ ${renderGeneralCard()}
613
+ `;
614
+ bindEvents();
615
+ }
616
+
617
+ // ==========================================
618
+ // Agents card
619
+ // ==========================================
620
+ function renderAgentsCard() {
621
+ const agents = Object.keys(BUILTIN_AGENTS);
622
+ const enabledAgents = config.agents || [];
623
+ const defaultAgent = config.defaultAgent || 'claude-code';
624
+
625
+ const rows = agents.map(name => {
626
+ const info = BUILTIN_AGENTS[name];
627
+ const available = agentStatus[name];
628
+ const enabled = enabledAgents.includes(name);
629
+
630
+ return `
631
+ <div class="agent-row" data-agent="${name}">
632
+ <div class="left">
633
+ <span class="dot ${available ? 'dot-on' : 'dot-off'}"></span>
634
+ <div>
635
+ <div class="name">${name}</div>
636
+ <div class="aliases">${info.aliases.map(a => `/${a}`).join(', ')}</div>
637
+ ${!available ? `<div class="hint">${t('installHint')} ${info.pkg}</div>` : ''}
638
+ </div>
639
+ </div>
640
+ <div class="toggle ${enabled ? 'active' : ''}" data-toggle-agent="${name}"></div>
641
+ </div>
642
+ `;
643
+ }).join('');
644
+
645
+ return `
646
+ <div class="card">
647
+ <h2>${t('agents')} <span class="badge">${agents.length}</span></h2>
648
+ ${rows}
649
+ <hr class="divider">
650
+ <label>${t('defaultAgent')}</label>
651
+ <select id="defaultAgent">
652
+ ${agents.filter(a => agentStatus[a]).map(a =>
653
+ `<option value="${esc(a)}" ${a === defaultAgent ? 'selected' : ''}>${esc(a)}</option>`
654
+ ).join('')}
655
+ </select>
656
+ <div class="actions">
657
+ <button type="button" class="btn btn-primary" id="saveAgents">${t('saveAgents')}</button>
658
+ </div>
659
+ </div>
660
+ `;
661
+ }
662
+
663
+ // ==========================================
664
+ // Messengers card
665
+ // ==========================================
666
+ function renderMessengersCard() {
667
+ const messengers = config.messengers || [];
668
+ const tg = config.telegram || {};
669
+ const fs = config.feishu || {};
670
+
671
+ const wechatEnabled = messengers.includes('wechat-ilink');
672
+ const telegramEnabled = messengers.includes('telegram');
673
+ const feishuEnabled = messengers.includes('feishu');
674
+
675
+ return `
676
+ <div class="card">
677
+ <h2>${t('messengers')} <span class="badge">${messengers.length}</span></h2>
678
+
679
+ <!-- WeChat -->
680
+ <div style="margin-bottom:16px">
681
+ <div class="agent-row">
682
+ <div class="left">
683
+ <div>
684
+ <div class="name">${t('wechat')}</div>
685
+ <div class="hint">${t('wechatHint')}</div>
686
+ </div>
687
+ </div>
688
+ <div class="toggle ${wechatEnabled ? 'active' : ''}" data-toggle-messenger="wechat-ilink"></div>
689
+ </div>
690
+ ${wechatEnabled ? `<div class="hint-box">${t('wechatBox')}</div>` : ''}
691
+ </div>
692
+
693
+ <!-- Telegram -->
694
+ <div style="margin-bottom:16px">
695
+ <div class="agent-row">
696
+ <div class="left">
697
+ <div>
698
+ <div class="name">${t('telegram')}</div>
699
+ <div class="hint">${t('telegramHint')}</div>
700
+ </div>
701
+ </div>
702
+ <div class="toggle ${telegramEnabled ? 'active' : ''}" data-toggle-messenger="telegram"></div>
703
+ </div>
704
+ ${telegramEnabled ? `
705
+ <div class="row">
706
+ <div>
707
+ <label>${t('botToken')}</label>
708
+ <input type="password" id="tgToken" value="${esc(tg.botToken || '')}" placeholder="123456:ABC-DEF...">
709
+ </div>
710
+ <div>
711
+ <label>${t('channelId')}</label>
712
+ <input type="text" id="tgChannel" value="${esc(tg.channelId || '')}" placeholder="default">
713
+ </div>
714
+ </div>
715
+ ` : ''}
716
+ </div>
717
+
718
+ <!-- Feishu -->
719
+ <div>
720
+ <div class="agent-row">
721
+ <div class="left">
722
+ <div>
723
+ <div class="name">${t('feishu')}</div>
724
+ <div class="hint">${t('feishuHint')}</div>
725
+ </div>
726
+ </div>
727
+ <div class="toggle ${feishuEnabled ? 'active' : ''}" data-toggle-messenger="feishu"></div>
728
+ </div>
729
+ ${feishuEnabled ? `
730
+ <div class="row">
731
+ <div>
732
+ <label>${t('appId')}</label>
733
+ <input type="text" id="fsAppId" value="${esc(fs.appId || '')}" placeholder="cli_xxx">
734
+ </div>
735
+ <div>
736
+ <label>${t('appSecret')}</label>
737
+ <input type="password" id="fsAppSecret" value="${esc(fs.appSecret || '')}" placeholder="Your app secret">
738
+ </div>
739
+ </div>
740
+ ` : ''}
741
+ </div>
742
+
743
+ <div class="actions">
744
+ <button type="button" class="btn btn-primary" id="saveMessengers">${t('saveMessengers')}</button>
745
+ </div>
746
+ </div>
747
+ `;
748
+ }
749
+
750
+ // ==========================================
751
+ // ACP Remote Agents card
752
+ // ==========================================
753
+ function renderAcpCard() {
754
+ const agents = config.acpAgents || [];
755
+
756
+ const rows = agents.length > 0 ? agents.map((a, i) => `
757
+ <tr data-acp-idx="${i}">
758
+ <td><strong>${esc(a.name)}</strong><br><span style="font-size:11px;color:var(--text-dim)">${esc((a.aliases || []).join(', '))}</span></td>
759
+ <td style="font-family:monospace;font-size:12px">${esc(a.endpoint)}</td>
760
+ <td>${a.auth?.type || t('none')}</td>
761
+ <td><div class="toggle ${a.enabled !== false ? 'active' : ''}" data-toggle-acp="${i}"></div></td>
762
+ <td>
763
+ <button type="button" class="btn btn-sm" data-test-acp="${i}">${t('test')}</button>
764
+ <button type="button" class="btn btn-sm btn-danger" data-del-acp="${i}">${t('del')}</button>
765
+ </td>
766
+ </tr>
767
+ `).join('') : `<tr><td colspan="5" style="color:var(--text-dim);text-align:center;padding:20px">${t('acpNone')}</td></tr>`;
768
+
769
+ return `
770
+ <div class="card">
771
+ <h2>${t('acpTitle')} <span class="badge">${agents.length}</span></h2>
772
+
773
+ <table class="acp-table">
774
+ <thead>
775
+ <tr><th>${t('name')}</th><th>${t('endpoint')}</th><th>${t('auth')}</th><th>${t('enabled')}</th><th></th></tr>
776
+ </thead>
777
+ <tbody>
778
+ ${rows}
779
+ </tbody>
780
+ </table>
781
+
782
+ <hr class="divider">
783
+ <div style="font-size:13px;font-weight:600;margin-bottom:10px">${t('addAgent')}</div>
784
+ <div class="row">
785
+ <div>
786
+ <label>${t('name')}</label>
787
+ <input type="text" id="acpName" placeholder="my-agent">
788
+ </div>
789
+ <div>
790
+ <label>${t('aliases')}</label>
791
+ <input type="text" id="acpAliases" placeholder="ma, agent1">
792
+ </div>
793
+ </div>
794
+ <div class="row">
795
+ <div>
796
+ <label>${t('endpointUrl')}</label>
797
+ <input type="text" id="acpEndpoint" placeholder="http://localhost:8080">
798
+ </div>
799
+ <div>
800
+ <label>${t('authType')}</label>
801
+ <select id="acpAuthType">
802
+ <option value="none">${t('none')}</option>
803
+ <option value="apikey">${t('apikey')}</option>
804
+ <option value="bearer">${t('bearer')}</option>
805
+ </select>
806
+ </div>
807
+ </div>
808
+ <div class="row" id="acpTokenRow" style="display:none">
809
+ <div>
810
+ <label>${t('token')}</label>
811
+ <input type="password" id="acpToken" placeholder="Your auth token">
812
+ </div>
813
+ <div></div>
814
+ </div>
815
+ <div class="actions">
816
+ <button type="button" class="btn" id="testAcpNew">${t('testConn')}</button>
817
+ <button type="button" class="btn btn-primary" id="addAcp">${t('add')}</button>
818
+ </div>
819
+ </div>
820
+ `;
821
+ }
822
+
823
+ // ==========================================
824
+ // Workspaces card (PR-C)
825
+ // ==========================================
826
+ function renderWorkspacesCard() {
827
+ const list = workspaceList || [];
828
+ // Surface default at the top, locked. Custom workspaces follow.
829
+ const sorted = list.slice().sort((a, b) => {
830
+ if (a.id === 'default') return -1;
831
+ if (b.id === 'default') return 1;
832
+ return a.id.localeCompare(b.id);
833
+ });
834
+
835
+ const escHtml = (s) => String(s || '').replace(/[&<>"']/g, (c) => (
836
+ { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]
837
+ ));
838
+
839
+ const rows = sorted.length ? sorted.map((w) => {
840
+ const isDefault = w.id === 'default';
841
+ const rl = w.rateLimit
842
+ ? `${w.rateLimit.rate}/${w.rateLimit.intervalSec}s · burst ${w.rateLimit.burst}`
843
+ : '—';
844
+ const agents = (w.agents?.length) ? escHtml(w.agents.join(', ')) : '<span style="color:var(--text-dim)">*</span>';
845
+ const members = (w.members?.length) ? w.members.length : `<span style="color:var(--text-dim)">*</span>`;
846
+ return `
847
+ <tr data-ws-id="${escHtml(w.id)}">
848
+ <td>
849
+ <strong>${escHtml(w.id)}</strong>
850
+ ${isDefault ? `<br><span class="badge">${t('workspaceDefaultLocked')}</span>` : ''}
851
+ <br><span style="font-size:11px;color:var(--text-dim)">${escHtml(w.name || w.id)}</span>
852
+ </td>
853
+ <td style="font-family:monospace;font-size:12px;max-width:240px;word-break:break-word">${agents}</td>
854
+ <td>${members}</td>
855
+ <td style="font-size:12px">${rl}</td>
856
+ <td>
857
+ ${isDefault ? '' : `
858
+ <button type="button" class="btn btn-sm" data-edit-ws="${escHtml(w.id)}">${t('workspaceEdit')}</button>
859
+ <button type="button" class="btn btn-sm btn-danger" data-del-ws="${escHtml(w.id)}">${t('workspaceDelete')}</button>
860
+ `}
861
+ </td>
862
+ </tr>
863
+ `;
864
+ }).join('') : `<tr><td colspan="5" style="color:var(--text-dim);text-align:center;padding:20px">${t('workspacesNone')}</td></tr>`;
865
+
866
+ return `
867
+ <div class="card">
868
+ <h2>${t('workspacesTitle')} <span class="badge">${list.length}</span></h2>
869
+ <table class="acp-table">
870
+ <thead>
871
+ <tr>
872
+ <th>${t('workspaceId')}</th>
873
+ <th>${t('workspaceAgents')}</th>
874
+ <th>${t('workspaceMembers')}</th>
875
+ <th>${t('workspaceRateLimit')}</th>
876
+ <th></th>
877
+ </tr>
878
+ </thead>
879
+ <tbody>${rows}</tbody>
880
+ </table>
881
+
882
+ <hr class="divider">
883
+ <div style="font-size:13px;font-weight:600;margin-bottom:10px">${t('workspaceAdd')}</div>
884
+ <div class="row">
885
+ <div>
886
+ <label>${t('workspaceId')} <span style="font-weight:400;color:var(--text-dim);font-size:11px">(${t('workspaceIdHelp')})</span></label>
887
+ <input type="text" id="wsId" placeholder="team-data">
888
+ </div>
889
+ <div>
890
+ <label>${t('workspaceName')}</label>
891
+ <input type="text" id="wsName" placeholder="Data team">
892
+ </div>
893
+ </div>
894
+ <div class="row">
895
+ <div>
896
+ <label>${t('workspaceAgents')}</label>
897
+ <input type="text" id="wsAgents" placeholder="opencode, claude-code">
898
+ </div>
899
+ <div>
900
+ <label>${t('workspaceMembers')}</label>
901
+ <input type="text" id="wsMembers" placeholder="user-1, user-2">
902
+ </div>
903
+ </div>
904
+ <div class="row">
905
+ <div>
906
+ <label>${t('workspaceRate')}</label>
907
+ <input type="number" id="wsRate" placeholder="10" min="1">
908
+ </div>
909
+ <div>
910
+ <label>${t('workspaceInterval')}</label>
911
+ <input type="number" id="wsInterval" placeholder="60" min="1">
912
+ </div>
913
+ <div>
914
+ <label>${t('workspaceBurst')}</label>
915
+ <input type="number" id="wsBurst" placeholder="15" min="1">
916
+ </div>
917
+ </div>
918
+ <div class="actions">
919
+ <button type="button" class="btn" id="resetWs">${t('workspaceReset')}</button>
920
+ <button type="button" class="btn btn-primary" id="saveWs">${t('save')}</button>
921
+ </div>
922
+ </div>
923
+ `;
924
+ }
925
+
926
+ // ==========================================
927
+ // General card
928
+ // ==========================================
929
+ function renderGeneralCard() {
930
+ return `
931
+ <div class="card">
932
+ <h2>${t('general')}</h2>
933
+ <label>${t('webPort')}</label>
934
+ <input type="number" id="webPort" value="${config.webPort || 3000}" min="1024" max="65535" style="max-width:200px">
935
+ <div class="actions">
936
+ <button type="button" class="btn btn-primary" id="saveGeneral">${t('save')}</button>
937
+ </div>
938
+ </div>
939
+
940
+ <!-- SMTP (env-file) — feeds /remind email delivery -->
941
+ <div class="card">
942
+ <h2>📨 SMTP</h2>
943
+ <p class="muted" style="margin-top:-4px;margin-bottom:12px;font-size:13px">
944
+ Email delivery for <code>/remind email me@x.com …</code>. Credentials live in <code>~/.im-hub/env</code> (chmod 600). Leave blank to disable.
945
+ </p>
946
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
947
+ <div>
948
+ <label>Host</label>
949
+ <input type="text" id="smtpHost" placeholder="smtp.gmail.com" />
950
+ </div>
951
+ <div>
952
+ <label>Port</label>
953
+ <input type="number" id="smtpPort" placeholder="465" min="1" max="65535" />
954
+ </div>
955
+ <div>
956
+ <label>User (login email)</label>
957
+ <input type="text" id="smtpUser" placeholder="you@example.com" />
958
+ </div>
959
+ <div>
960
+ <label>Password / app token</label>
961
+ <input type="password" id="smtpPass" placeholder="••••••••" autocomplete="off" />
962
+ </div>
963
+ <div>
964
+ <label>From (defaults to user)</label>
965
+ <input type="text" id="smtpFrom" placeholder="" />
966
+ </div>
967
+ <div>
968
+ <label>TLS mode</label>
969
+ <select id="smtpSecure">
970
+ <option value="auto">auto</option>
971
+ <option value="true">true (SSL on connect, port 465)</option>
972
+ <option value="false">false (STARTTLS, port 587)</option>
973
+ </select>
974
+ </div>
975
+ </div>
976
+ <div class="actions" style="margin-top:12px">
977
+ <button type="button" class="btn btn-primary" id="saveSmtp">${t('save')}</button>
978
+ <button type="button" class="btn" id="clearSmtp">Disable SMTP</button>
979
+ <button type="button" class="btn" id="revealSmtp" title="Show the stored password">Reveal</button>
980
+ </div>
981
+ <p class="muted" id="smtpStatus" style="margin-top:8px;font-size:12px"></p>
982
+ </div>
983
+
984
+ <!-- Baidu Maps AK (env-file) — feeds /memo address geocoding -->
985
+ <div class="card">
986
+ <h2>🗺 Baidu Maps AK</h2>
987
+ <p class="muted" style="margin-top:-4px;margin-bottom:12px;font-size:13px">
988
+ Enables <code>/memo</code> address-to-coords lookup. Without it, /memo still works via raw coords or browser GPS share. Free AK: <a href="https://lbsyun.baidu.com/" target="_blank">lbsyun.baidu.com</a> → Console → 应用管理 → 创建应用 → 服务端 API.
989
+ </p>
990
+ <label>IMHUB_BAIDU_MAP_AK</label>
991
+ <input type="text" id="baiduAk" placeholder="32-char AK starting with non-digit" style="max-width:480px" />
992
+ <div class="actions" style="margin-top:12px">
993
+ <button type="button" class="btn btn-primary" id="saveBaidu">${t('save')}</button>
994
+ <button type="button" class="btn" id="clearBaidu">Clear</button>
995
+ <button type="button" class="btn" id="revealBaidu" title="Show the stored AK">Reveal</button>
996
+ </div>
997
+ <p class="muted" id="baiduStatus" style="margin-top:8px;font-size:12px"></p>
998
+ </div>
999
+ `;
1000
+ }
1001
+
1002
+ async function loadEnvSection(reveal) {
1003
+ try {
1004
+ const url = reveal ? '/api/env?reveal=1' : '/api/env';
1005
+ const data = await authFetch(url).then(r => r.json());
1006
+ const env = data.env || {};
1007
+ const set = (id, key) => {
1008
+ const el = document.getElementById(id);
1009
+ if (el) el.value = env[key] != null ? env[key] : '';
1010
+ };
1011
+ set('smtpHost', 'IMHUB_SMTP_HOST');
1012
+ set('smtpPort', 'IMHUB_SMTP_PORT');
1013
+ set('smtpUser', 'IMHUB_SMTP_USER');
1014
+ set('smtpPass', 'IMHUB_SMTP_PASS');
1015
+ set('smtpFrom', 'IMHUB_SMTP_FROM');
1016
+ const sec = document.getElementById('smtpSecure');
1017
+ if (sec) sec.value = env['IMHUB_SMTP_SECURE'] || 'auto';
1018
+ set('baiduAk', 'IMHUB_BAIDU_MAP_AK');
1019
+ const smtpStatus = document.getElementById('smtpStatus');
1020
+ const baiduStatus = document.getElementById('baiduStatus');
1021
+ if (smtpStatus) smtpStatus.textContent = env['IMHUB_SMTP_HOST']
1022
+ ? `Loaded${reveal ? ' (raw)' : ' (password masked)'}`
1023
+ : '(no SMTP credentials set)';
1024
+ if (baiduStatus) baiduStatus.textContent = env['IMHUB_BAIDU_MAP_AK']
1025
+ ? `Loaded${reveal ? ' (raw)' : ' (masked)'}`
1026
+ : '(not set)';
1027
+ } catch (err) {
1028
+ const smtpStatus = document.getElementById('smtpStatus');
1029
+ if (smtpStatus) smtpStatus.textContent = 'Failed to load: ' + (err && err.message ? err.message : err);
1030
+ }
1031
+ }
1032
+
1033
+
1034
+ // ==========================================
1035
+ // Event binding
1036
+ // ==========================================
1037
+ function bindEvents() {
1038
+ // Populate SMTP / Baidu env-backed fields. Values come in masked;
1039
+ // the Reveal buttons re-fetch with ?reveal=1 on demand.
1040
+ void loadEnvSection();
1041
+
1042
+ // Agent toggles
1043
+ document.querySelectorAll('[data-toggle-agent]').forEach(el => {
1044
+ el.addEventListener('click', () => el.classList.toggle('active'));
1045
+ });
1046
+
1047
+ // Messenger toggles
1048
+ document.querySelectorAll('[data-toggle-messenger]').forEach(el => {
1049
+ el.addEventListener('click', () => {
1050
+ el.classList.toggle('active');
1051
+ syncMessengerToggles();
1052
+ });
1053
+ });
1054
+
1055
+ // ACP toggles
1056
+ document.querySelectorAll('[data-toggle-acp]').forEach(el => {
1057
+ el.addEventListener('click', () => el.classList.toggle('active'));
1058
+ });
1059
+
1060
+ // ACP delete buttons
1061
+ document.querySelectorAll('[data-del-acp]').forEach(btn => {
1062
+ btn.addEventListener('click', async () => {
1063
+ const idx = parseInt(btn.dataset.delAcp, 10);
1064
+ config.acpAgents.splice(idx, 1);
1065
+ await saveConfig();
1066
+ render();
1067
+ });
1068
+ });
1069
+
1070
+ // ACP test existing
1071
+ document.querySelectorAll('[data-test-acp]').forEach(btn => {
1072
+ btn.addEventListener('click', async () => {
1073
+ const idx = parseInt(btn.dataset.testAcp, 10);
1074
+ const agent = config.acpAgents[idx];
1075
+ btn.disabled = true;
1076
+ btn.textContent = t('testing');
1077
+ try {
1078
+ const res = await authFetch('/api/agents/acp/test', {
1079
+ method: 'POST',
1080
+ headers: { 'Content-Type': 'application/json' },
1081
+ body: JSON.stringify({ endpoint: agent.endpoint, auth: agent.auth }),
1082
+ });
1083
+ const data = await res.json();
1084
+ if (data.ok) {
1085
+ toast(`${t('connected')}: ${data.name}${data.description ? ` — ${data.description}` : ''}`, 'success');
1086
+ } else {
1087
+ toast(`${t('failed')}: ${data.error}`, 'error');
1088
+ }
1089
+ } catch (err) {
1090
+ toast(`${t('error')}: ${err.message}`, 'error');
1091
+ }
1092
+ btn.disabled = false;
1093
+ btn.textContent = t('testConn');
1094
+ });
1095
+ });
1096
+
1097
+ // Add ACP
1098
+ document.getElementById('addAcp')?.addEventListener('click', async () => {
1099
+ const name = document.getElementById('acpName').value.trim();
1100
+ const endpoint = document.getElementById('acpEndpoint').value.trim();
1101
+ if (!name) { toast(t('nameRequired'), 'error'); return; }
1102
+ if (!endpoint) { toast(t('endpointRequired'), 'error'); return; }
1103
+
1104
+ const aliases = document.getElementById('acpAliases').value.split(',').map(s => s.trim()).filter(Boolean);
1105
+ const authType = document.getElementById('acpAuthType').value;
1106
+ const token = document.getElementById('acpToken')?.value.trim();
1107
+ const auth = authType === 'none' ? undefined : { type: authType, token };
1108
+
1109
+ if (!config.acpAgents) config.acpAgents = [];
1110
+ config.acpAgents.push({ name, aliases, endpoint, auth, enabled: true });
1111
+ await saveConfig();
1112
+ render();
1113
+ });
1114
+
1115
+ // Save general
1116
+ document.getElementById('saveGeneral')?.addEventListener('click', async () => {
1117
+ config.webPort = parseInt(document.getElementById('webPort').value, 10) || 3000;
1118
+ await saveConfig();
1119
+ });
1120
+
1121
+ // SMTP save: bundle all 6 fields into one /api/env PUT. Empty
1122
+ // host treats it as "skip" — caller can also use the Disable button
1123
+ // for an explicit clear of all 6 keys.
1124
+ document.getElementById('saveSmtp')?.addEventListener('click', async () => {
1125
+ const get = (id) => document.getElementById(id)?.value?.trim() ?? '';
1126
+ const host = get('smtpHost'); const port = get('smtpPort');
1127
+ const user = get('smtpUser'); const pass = get('smtpPass');
1128
+ const from = get('smtpFrom'); const secure = get('smtpSecure');
1129
+ if (!host) {
1130
+ alert('Host is required (use Disable button to clear instead).');
1131
+ return;
1132
+ }
1133
+ // pass === '' AND we have an existing masked value would erase the
1134
+ // password. Detect: if pass starts with masked chars (4 plain + ****
1135
+ // shape) we treat as "keep existing" by not sending the key.
1136
+ const updates = {
1137
+ IMHUB_SMTP_HOST: host,
1138
+ IMHUB_SMTP_PORT: port || '465',
1139
+ IMHUB_SMTP_USER: user,
1140
+ IMHUB_SMTP_FROM: from || user,
1141
+ IMHUB_SMTP_SECURE: secure || 'auto',
1142
+ };
1143
+ const looksMasked = pass && /\*{2,}/.test(pass);
1144
+ if (pass && !looksMasked) updates.IMHUB_SMTP_PASS = pass;
1145
+ else if (!pass) {
1146
+ // Explicit blank — refuse, unless user clicks Disable.
1147
+ alert('Password required (use Disable button to clear instead).');
1148
+ return;
1149
+ }
1150
+ try {
1151
+ await authFetch('/api/env', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ updates }) });
1152
+ alert('SMTP saved.');
1153
+ await loadEnvSection();
1154
+ } catch (err) {
1155
+ alert('Save failed: ' + (err && err.message ? err.message : err));
1156
+ }
1157
+ });
1158
+ document.getElementById('clearSmtp')?.addEventListener('click', async () => {
1159
+ if (!confirm('Disable SMTP? Email reminder delivery will stop working.')) return;
1160
+ const updates = {
1161
+ IMHUB_SMTP_HOST: null, IMHUB_SMTP_PORT: null, IMHUB_SMTP_USER: null,
1162
+ IMHUB_SMTP_PASS: null, IMHUB_SMTP_FROM: null, IMHUB_SMTP_SECURE: null,
1163
+ };
1164
+ try {
1165
+ await authFetch('/api/env', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ updates }) });
1166
+ await loadEnvSection();
1167
+ } catch (err) {
1168
+ alert('Disable failed: ' + (err && err.message ? err.message : err));
1169
+ }
1170
+ });
1171
+ document.getElementById('revealSmtp')?.addEventListener('click', () => loadEnvSection(true));
1172
+
1173
+ // Baidu AK
1174
+ document.getElementById('saveBaidu')?.addEventListener('click', async () => {
1175
+ const ak = document.getElementById('baiduAk')?.value?.trim() || '';
1176
+ if (!ak) { alert('AK required (use Clear button to remove instead).'); return; }
1177
+ const looksMasked = /\*{2,}/.test(ak);
1178
+ if (looksMasked) { alert('Looks like the masked placeholder — enter the real AK or Reveal first.'); return; }
1179
+ try {
1180
+ await authFetch('/api/env', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ updates: { IMHUB_BAIDU_MAP_AK: ak } }) });
1181
+ alert('Baidu AK saved.');
1182
+ await loadEnvSection();
1183
+ } catch (err) {
1184
+ alert('Save failed: ' + (err && err.message ? err.message : err));
1185
+ }
1186
+ });
1187
+ document.getElementById('clearBaidu')?.addEventListener('click', async () => {
1188
+ if (!confirm('Clear Baidu Maps AK? Address-based /memo lookups will stop.')) return;
1189
+ try {
1190
+ await authFetch('/api/env', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ updates: { IMHUB_BAIDU_MAP_AK: null } }) });
1191
+ await loadEnvSection();
1192
+ } catch (err) {
1193
+ alert('Clear failed: ' + (err && err.message ? err.message : err));
1194
+ }
1195
+ });
1196
+ document.getElementById('revealBaidu')?.addEventListener('click', () => loadEnvSection(true));
1197
+
1198
+ // ==========================================
1199
+ // Workspaces (PR-C)
1200
+ // ==========================================
1201
+
1202
+ // Click "Edit" on a row → pre-fill the form. The same form's Save
1203
+ // button always POSTs, which the server-side handler treats as
1204
+ // upsert (workspaceRegistry.add overwrites by id).
1205
+ document.querySelectorAll('[data-edit-ws]').forEach((btn) => {
1206
+ btn.addEventListener('click', () => {
1207
+ const id = btn.getAttribute('data-edit-ws');
1208
+ const w = (workspaceList || []).find((x) => x.id === id);
1209
+ if (!w) return;
1210
+ const set = (elId, v) => { const el = document.getElementById(elId); if (el) el.value = v; };
1211
+ set('wsId', w.id);
1212
+ set('wsName', w.name || w.id);
1213
+ set('wsAgents', (w.agents || []).join(', '));
1214
+ set('wsMembers', (w.members || []).join(', '));
1215
+ set('wsRate', w.rateLimit?.rate ?? '');
1216
+ set('wsInterval', w.rateLimit?.intervalSec ?? '');
1217
+ set('wsBurst', w.rateLimit?.burst ?? '');
1218
+ // Lock the id field on edit so a typo can't accidentally create
1219
+ // a parallel row instead of updating the chosen one.
1220
+ const idInput = document.getElementById('wsId');
1221
+ if (idInput) idInput.readOnly = true;
1222
+ // Scroll the form into view so the user sees what was filled.
1223
+ document.getElementById('saveWs')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
1224
+ });
1225
+ });
1226
+
1227
+ // Click "Delete" on a row → confirm + DELETE.
1228
+ document.querySelectorAll('[data-del-ws]').forEach((btn) => {
1229
+ btn.addEventListener('click', async () => {
1230
+ const id = btn.getAttribute('data-del-ws');
1231
+ if (!id) return;
1232
+ if (!confirm(t('workspaceConfirmDelete').replace('{id}', id))) return;
1233
+ try {
1234
+ const res = await authFetch(`/api/workspaces/${encodeURIComponent(id)}`, {
1235
+ method: 'DELETE',
1236
+ });
1237
+ if (!res.ok) {
1238
+ const data = await res.json().catch(() => ({}));
1239
+ throw new Error(data.error || res.statusText);
1240
+ }
1241
+ toast(t('workspaceDeleted'), 'success');
1242
+ await reloadWorkspaces();
1243
+ } catch (err) {
1244
+ toast(`${t('error')}: ${err.message}`, 'error');
1245
+ }
1246
+ });
1247
+ });
1248
+
1249
+ // Reset the form (clear inputs + unlock id field).
1250
+ document.getElementById('resetWs')?.addEventListener('click', () => {
1251
+ ['wsId', 'wsName', 'wsAgents', 'wsMembers', 'wsRate', 'wsInterval', 'wsBurst'].forEach((id) => {
1252
+ const el = document.getElementById(id);
1253
+ if (el) {
1254
+ el.value = '';
1255
+ if (id === 'wsId') el.readOnly = false;
1256
+ }
1257
+ });
1258
+ });
1259
+
1260
+ // Save (create or upsert) workspace.
1261
+ document.getElementById('saveWs')?.addEventListener('click', async () => {
1262
+ const id = document.getElementById('wsId').value.trim();
1263
+ if (!id) { toast(`${t('workspaceId')} required`, 'error'); return; }
1264
+ if (id === 'default') { toast('"default" is reserved', 'error'); return; }
1265
+ const splitCsv = (s) => String(s || '').split(',').map((x) => x.trim()).filter(Boolean);
1266
+ const payload = {
1267
+ id,
1268
+ name: document.getElementById('wsName').value.trim() || id,
1269
+ agents: splitCsv(document.getElementById('wsAgents').value),
1270
+ members: splitCsv(document.getElementById('wsMembers').value),
1271
+ };
1272
+ const rate = parseInt(document.getElementById('wsRate').value, 10);
1273
+ const intervalSec = parseInt(document.getElementById('wsInterval').value, 10);
1274
+ const burst = parseInt(document.getElementById('wsBurst').value, 10);
1275
+ if (Number.isFinite(rate) || Number.isFinite(intervalSec) || Number.isFinite(burst)) {
1276
+ payload.rateLimit = {
1277
+ rate: Number.isFinite(rate) ? rate : 10,
1278
+ intervalSec: Number.isFinite(intervalSec) ? intervalSec : 60,
1279
+ burst: Number.isFinite(burst) ? burst : 15,
1280
+ };
1281
+ }
1282
+ try {
1283
+ const res = await authFetch('/api/workspaces', {
1284
+ method: 'POST',
1285
+ headers: { 'Content-Type': 'application/json' },
1286
+ body: JSON.stringify(payload),
1287
+ });
1288
+ if (!res.ok) {
1289
+ const data = await res.json().catch(() => ({}));
1290
+ throw new Error(data.error || res.statusText);
1291
+ }
1292
+ toast(t('workspaceSaved'), 'success');
1293
+ // Reset form for next use; reloadWorkspaces re-renders so the
1294
+ // new row appears immediately.
1295
+ document.getElementById('resetWs')?.click();
1296
+ await reloadWorkspaces();
1297
+ } catch (err) {
1298
+ toast(`${t('error')}: ${err.message}`, 'error');
1299
+ }
1300
+ });
1301
+ }
1302
+
1303
+ function syncMessengerToggles() {
1304
+ render();
1305
+ }
1306
+
1307
+ // ==========================================
1308
+ // API helpers
1309
+ // ==========================================
1310
+ async function saveConfig() {
1311
+ try {
1312
+ const payload = {
1313
+ messengers: config.messengers,
1314
+ agents: config.agents,
1315
+ defaultAgent: config.defaultAgent,
1316
+ telegram: config.telegram,
1317
+ feishu: config.feishu,
1318
+ acpAgents: config.acpAgents,
1319
+ webPort: config.webPort,
1320
+ };
1321
+
1322
+ const res = await authFetch('/api/config', {
1323
+ method: 'PUT',
1324
+ headers: { 'Content-Type': 'application/json' },
1325
+ body: JSON.stringify(payload),
1326
+ });
1327
+
1328
+ if (!res.ok) {
1329
+ const data = await res.json();
1330
+ throw new Error(data.error || t('saveFailed'));
1331
+ }
1332
+
1333
+ toast(t('savedMsg'), 'success');
1334
+ await init();
1335
+ } catch (err) {
1336
+ toast(`${t('saveFailed')}: ${err.message}`, 'error');
1337
+ }
1338
+ }
1339
+
1340
+ let toastTimer;
1341
+ function toast(msg, type) {
1342
+ toastEl.textContent = msg;
1343
+ toastEl.className = `toast ${type} show`;
1344
+ clearTimeout(toastTimer);
1345
+ toastTimer = setTimeout(() => {
1346
+ toastEl.classList.remove('show');
1347
+ }, 3000);
1348
+ }
1349
+
1350
+ // Init
1351
+ applyLang();
1352
+ init();
1353
+ </script>
1354
+ </body>
1355
+ </html>