skill-calorie 0.1.0

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 (373) hide show
  1. package/dist/analysis/anomaly/common.d.ts +53 -0
  2. package/dist/analysis/anomaly/common.d.ts.map +1 -0
  3. package/dist/analysis/anomaly/common.js +69 -0
  4. package/dist/analysis/anomaly/common.js.map +1 -0
  5. package/dist/analysis/anomaly/diet.d.ts +8 -0
  6. package/dist/analysis/anomaly/diet.d.ts.map +1 -0
  7. package/dist/analysis/anomaly/diet.js +95 -0
  8. package/dist/analysis/anomaly/diet.js.map +1 -0
  9. package/dist/analysis/anomaly/exercise.d.ts +9 -0
  10. package/dist/analysis/anomaly/exercise.d.ts.map +1 -0
  11. package/dist/analysis/anomaly/exercise.js +116 -0
  12. package/dist/analysis/anomaly/exercise.js.map +1 -0
  13. package/dist/analysis/anomaly/index.d.ts +19 -0
  14. package/dist/analysis/anomaly/index.d.ts.map +1 -0
  15. package/dist/analysis/anomaly/index.js +51 -0
  16. package/dist/analysis/anomaly/index.js.map +1 -0
  17. package/dist/analysis/anomaly/overall.d.ts +13 -0
  18. package/dist/analysis/anomaly/overall.d.ts.map +1 -0
  19. package/dist/analysis/anomaly/overall.js +128 -0
  20. package/dist/analysis/anomaly/overall.js.map +1 -0
  21. package/dist/analysis/anomaly/overall2.d.ts +5 -0
  22. package/dist/analysis/anomaly/overall2.d.ts.map +1 -0
  23. package/dist/analysis/anomaly/overall2.js +94 -0
  24. package/dist/analysis/anomaly/overall2.js.map +1 -0
  25. package/dist/analysis/anomaly/weight.d.ts +8 -0
  26. package/dist/analysis/anomaly/weight.d.ts.map +1 -0
  27. package/dist/analysis/anomaly/weight.js +106 -0
  28. package/dist/analysis/anomaly/weight.js.map +1 -0
  29. package/dist/analysis/anomaly/weight2.d.ts +4 -0
  30. package/dist/analysis/anomaly/weight2.d.ts.map +1 -0
  31. package/dist/analysis/anomaly/weight2.js +59 -0
  32. package/dist/analysis/anomaly/weight2.js.map +1 -0
  33. package/dist/analysis/contraindications.d.ts +63 -0
  34. package/dist/analysis/contraindications.d.ts.map +1 -0
  35. package/dist/analysis/contraindications.js +121 -0
  36. package/dist/analysis/contraindications.js.map +1 -0
  37. package/dist/analysis/cross.d.ts +75 -0
  38. package/dist/analysis/cross.d.ts.map +1 -0
  39. package/dist/analysis/cross.js +283 -0
  40. package/dist/analysis/cross.js.map +1 -0
  41. package/dist/analysis/dashboard.d.ts +18 -0
  42. package/dist/analysis/dashboard.d.ts.map +1 -0
  43. package/dist/analysis/dashboard.js +48 -0
  44. package/dist/analysis/dashboard.js.map +1 -0
  45. package/dist/analysis/deficit.d.ts +44 -0
  46. package/dist/analysis/deficit.d.ts.map +1 -0
  47. package/dist/analysis/deficit.js +54 -0
  48. package/dist/analysis/deficit.js.map +1 -0
  49. package/dist/analysis/diet.d.ts +76 -0
  50. package/dist/analysis/diet.d.ts.map +1 -0
  51. package/dist/analysis/diet.js +164 -0
  52. package/dist/analysis/diet.js.map +1 -0
  53. package/dist/analysis/exercise.d.ts +98 -0
  54. package/dist/analysis/exercise.d.ts.map +1 -0
  55. package/dist/analysis/exercise.js +298 -0
  56. package/dist/analysis/exercise.js.map +1 -0
  57. package/dist/analysis/exerciseReview.d.ts +36 -0
  58. package/dist/analysis/exerciseReview.d.ts.map +1 -0
  59. package/dist/analysis/exerciseReview.js +50 -0
  60. package/dist/analysis/exerciseReview.js.map +1 -0
  61. package/dist/analysis/precision.d.ts +15 -0
  62. package/dist/analysis/precision.d.ts.map +1 -0
  63. package/dist/analysis/precision.js +38 -0
  64. package/dist/analysis/precision.js.map +1 -0
  65. package/dist/analysis/result.d.ts +15 -0
  66. package/dist/analysis/result.d.ts.map +1 -0
  67. package/dist/analysis/result.js +13 -0
  68. package/dist/analysis/result.js.map +1 -0
  69. package/dist/analysis/review.d.ts +118 -0
  70. package/dist/analysis/review.d.ts.map +1 -0
  71. package/dist/analysis/review.js +195 -0
  72. package/dist/analysis/review.js.map +1 -0
  73. package/dist/analysis/series.d.ts +42 -0
  74. package/dist/analysis/series.d.ts.map +1 -0
  75. package/dist/analysis/series.js +127 -0
  76. package/dist/analysis/series.js.map +1 -0
  77. package/dist/analysis/simulate.d.ts +43 -0
  78. package/dist/analysis/simulate.d.ts.map +1 -0
  79. package/dist/analysis/simulate.js +109 -0
  80. package/dist/analysis/simulate.js.map +1 -0
  81. package/dist/analysis/simulate2.d.ts +65 -0
  82. package/dist/analysis/simulate2.d.ts.map +1 -0
  83. package/dist/analysis/simulate2.js +173 -0
  84. package/dist/analysis/simulate2.js.map +1 -0
  85. package/dist/analysis/trend.d.ts +39 -0
  86. package/dist/analysis/trend.d.ts.map +1 -0
  87. package/dist/analysis/trend.js +48 -0
  88. package/dist/analysis/trend.js.map +1 -0
  89. package/dist/analysis/utils.d.ts +17 -0
  90. package/dist/analysis/utils.d.ts.map +1 -0
  91. package/dist/analysis/utils.js +52 -0
  92. package/dist/analysis/utils.js.map +1 -0
  93. package/dist/analysis/volatility.d.ts +43 -0
  94. package/dist/analysis/volatility.d.ts.map +1 -0
  95. package/dist/analysis/volatility.js +96 -0
  96. package/dist/analysis/volatility.js.map +1 -0
  97. package/dist/analysis/weight.d.ts +86 -0
  98. package/dist/analysis/weight.d.ts.map +1 -0
  99. package/dist/analysis/weight.js +219 -0
  100. package/dist/analysis/weight.js.map +1 -0
  101. package/dist/analysis/weightCompare.d.ts +63 -0
  102. package/dist/analysis/weightCompare.d.ts.map +1 -0
  103. package/dist/analysis/weightCompare.js +212 -0
  104. package/dist/analysis/weightCompare.js.map +1 -0
  105. package/dist/analysis/weightCompare2.d.ts +63 -0
  106. package/dist/analysis/weightCompare2.d.ts.map +1 -0
  107. package/dist/analysis/weightCompare2.js +211 -0
  108. package/dist/analysis/weightCompare2.js.map +1 -0
  109. package/dist/analysis/weightCompare3.d.ts +13 -0
  110. package/dist/analysis/weightCompare3.d.ts.map +1 -0
  111. package/dist/analysis/weightCompare3.js +199 -0
  112. package/dist/analysis/weightCompare3.js.map +1 -0
  113. package/dist/cli/cmd_read.d.ts +8 -0
  114. package/dist/cli/cmd_read.d.ts.map +1 -0
  115. package/dist/cli/cmd_read.js +719 -0
  116. package/dist/cli/cmd_read.js.map +1 -0
  117. package/dist/cli/keys.d.ts +333 -0
  118. package/dist/cli/keys.d.ts.map +1 -0
  119. package/dist/cli/keys.js +103 -0
  120. package/dist/cli/keys.js.map +1 -0
  121. package/dist/cli/write.d.ts +24 -0
  122. package/dist/cli/write.d.ts.map +1 -0
  123. package/dist/cli/write.js +832 -0
  124. package/dist/cli/write.js.map +1 -0
  125. package/dist/fetch/audit.d.ts +25 -0
  126. package/dist/fetch/audit.d.ts.map +1 -0
  127. package/dist/fetch/audit.js +46 -0
  128. package/dist/fetch/audit.js.map +1 -0
  129. package/dist/fetch/batch.d.ts +68 -0
  130. package/dist/fetch/batch.d.ts.map +1 -0
  131. package/dist/fetch/batch.js +183 -0
  132. package/dist/fetch/batch.js.map +1 -0
  133. package/dist/fetch/body.d.ts +89 -0
  134. package/dist/fetch/body.d.ts.map +1 -0
  135. package/dist/fetch/body.js +217 -0
  136. package/dist/fetch/body.js.map +1 -0
  137. package/dist/fetch/cli.d.ts +3 -0
  138. package/dist/fetch/cli.d.ts.map +1 -0
  139. package/dist/fetch/cli.js +195 -0
  140. package/dist/fetch/cli.js.map +1 -0
  141. package/dist/fetch/cross-skill.d.ts +29 -0
  142. package/dist/fetch/cross-skill.d.ts.map +1 -0
  143. package/dist/fetch/cross-skill.js +42 -0
  144. package/dist/fetch/cross-skill.js.map +1 -0
  145. package/dist/fetch/db.d.ts +5 -0
  146. package/dist/fetch/db.d.ts.map +1 -0
  147. package/dist/fetch/db.js +4 -0
  148. package/dist/fetch/db.js.map +1 -0
  149. package/dist/fetch/diet.d.ts +174 -0
  150. package/dist/fetch/diet.d.ts.map +1 -0
  151. package/dist/fetch/diet.js +239 -0
  152. package/dist/fetch/diet.js.map +1 -0
  153. package/dist/fetch/errors.d.ts +5 -0
  154. package/dist/fetch/errors.d.ts.map +1 -0
  155. package/dist/fetch/errors.js +8 -0
  156. package/dist/fetch/errors.js.map +1 -0
  157. package/dist/fetch/exercise.d.ts +72 -0
  158. package/dist/fetch/exercise.d.ts.map +1 -0
  159. package/dist/fetch/exercise.js +270 -0
  160. package/dist/fetch/exercise.js.map +1 -0
  161. package/dist/fetch/goal.d.ts +48 -0
  162. package/dist/fetch/goal.d.ts.map +1 -0
  163. package/dist/fetch/goal.js +73 -0
  164. package/dist/fetch/goal.js.map +1 -0
  165. package/dist/fetch/goalHistory.d.ts +24 -0
  166. package/dist/fetch/goalHistory.d.ts.map +1 -0
  167. package/dist/fetch/goalHistory.js +51 -0
  168. package/dist/fetch/goalHistory.js.map +1 -0
  169. package/dist/fetch/history.d.ts +28 -0
  170. package/dist/fetch/history.d.ts.map +1 -0
  171. package/dist/fetch/history.js +29 -0
  172. package/dist/fetch/history.js.map +1 -0
  173. package/dist/fetch/index.d.ts +23 -0
  174. package/dist/fetch/index.d.ts.map +1 -0
  175. package/dist/fetch/index.js +16 -0
  176. package/dist/fetch/index.js.map +1 -0
  177. package/dist/fetch/nutritionGoal.d.ts +115 -0
  178. package/dist/fetch/nutritionGoal.d.ts.map +1 -0
  179. package/dist/fetch/nutritionGoal.js +154 -0
  180. package/dist/fetch/nutritionGoal.js.map +1 -0
  181. package/dist/fetch/photos.d.ts +76 -0
  182. package/dist/fetch/photos.d.ts.map +1 -0
  183. package/dist/fetch/photos.js +193 -0
  184. package/dist/fetch/photos.js.map +1 -0
  185. package/dist/fetch/plan.d.ts +145 -0
  186. package/dist/fetch/plan.d.ts.map +1 -0
  187. package/dist/fetch/plan.js +258 -0
  188. package/dist/fetch/plan.js.map +1 -0
  189. package/dist/fetch/products.d.ts +62 -0
  190. package/dist/fetch/products.d.ts.map +1 -0
  191. package/dist/fetch/products.js +73 -0
  192. package/dist/fetch/products.js.map +1 -0
  193. package/dist/fetch/profile.d.ts +46 -0
  194. package/dist/fetch/profile.d.ts.map +1 -0
  195. package/dist/fetch/profile.js +105 -0
  196. package/dist/fetch/profile.js.map +1 -0
  197. package/dist/fetch/shapes.d.ts +41 -0
  198. package/dist/fetch/shapes.d.ts.map +1 -0
  199. package/dist/fetch/shapes.js +38 -0
  200. package/dist/fetch/shapes.js.map +1 -0
  201. package/dist/fetch/validate.d.ts +11 -0
  202. package/dist/fetch/validate.d.ts.map +1 -0
  203. package/dist/fetch/validate.js +64 -0
  204. package/dist/fetch/validate.js.map +1 -0
  205. package/dist/fetch/weight.d.ts +109 -0
  206. package/dist/fetch/weight.d.ts.map +1 -0
  207. package/dist/fetch/weight.js +210 -0
  208. package/dist/fetch/weight.js.map +1 -0
  209. package/dist/fetch/xunji-catalog.d.ts +17 -0
  210. package/dist/fetch/xunji-catalog.d.ts.map +1 -0
  211. package/dist/fetch/xunji-catalog.js +57 -0
  212. package/dist/fetch/xunji-catalog.js.map +1 -0
  213. package/dist/index.d.ts +17 -0
  214. package/dist/index.d.ts.map +1 -0
  215. package/dist/index.js +12 -0
  216. package/dist/index.js.map +1 -0
  217. package/dist/kcal.d.ts +21 -0
  218. package/dist/kcal.d.ts.map +1 -0
  219. package/dist/kcal.js +27 -0
  220. package/dist/kcal.js.map +1 -0
  221. package/dist/migrate/migrate.d.ts +71 -0
  222. package/dist/migrate/migrate.d.ts.map +1 -0
  223. package/dist/migrate/migrate.js +450 -0
  224. package/dist/migrate/migrate.js.map +1 -0
  225. package/dist/paths.d.ts +6 -0
  226. package/dist/paths.d.ts.map +1 -0
  227. package/dist/paths.js +34 -0
  228. package/dist/paths.js.map +1 -0
  229. package/dist/render/analysisPlate.d.ts +49 -0
  230. package/dist/render/analysisPlate.d.ts.map +1 -0
  231. package/dist/render/analysisPlate.js +149 -0
  232. package/dist/render/analysisPlate.js.map +1 -0
  233. package/dist/render/bodyPlate.d.ts +55 -0
  234. package/dist/render/bodyPlate.d.ts.map +1 -0
  235. package/dist/render/bodyPlate.js +97 -0
  236. package/dist/render/bodyPlate.js.map +1 -0
  237. package/dist/render/diet.d.ts +47 -0
  238. package/dist/render/diet.d.ts.map +1 -0
  239. package/dist/render/diet.js +94 -0
  240. package/dist/render/diet.js.map +1 -0
  241. package/dist/render/envelope.d.ts +60 -0
  242. package/dist/render/envelope.d.ts.map +1 -0
  243. package/dist/render/envelope.js +111 -0
  244. package/dist/render/envelope.js.map +1 -0
  245. package/dist/render/errors.d.ts +10 -0
  246. package/dist/render/errors.d.ts.map +1 -0
  247. package/dist/render/errors.js +9 -0
  248. package/dist/render/errors.js.map +1 -0
  249. package/dist/render/exercise.d.ts +20 -0
  250. package/dist/render/exercise.d.ts.map +1 -0
  251. package/dist/render/exercise.js +32 -0
  252. package/dist/render/exercise.js.map +1 -0
  253. package/dist/render/goal.d.ts +24 -0
  254. package/dist/render/goal.d.ts.map +1 -0
  255. package/dist/render/goal.js +23 -0
  256. package/dist/render/goal.js.map +1 -0
  257. package/dist/render/goalExtra.d.ts +45 -0
  258. package/dist/render/goalExtra.d.ts.map +1 -0
  259. package/dist/render/goalExtra.js +94 -0
  260. package/dist/render/goalExtra.js.map +1 -0
  261. package/dist/render/goalPlate.d.ts +66 -0
  262. package/dist/render/goalPlate.d.ts.map +1 -0
  263. package/dist/render/goalPlate.js +110 -0
  264. package/dist/render/goalPlate.js.map +1 -0
  265. package/dist/render/health.d.ts +21 -0
  266. package/dist/render/health.d.ts.map +1 -0
  267. package/dist/render/health.js +41 -0
  268. package/dist/render/health.js.map +1 -0
  269. package/dist/render/help.d.ts +18 -0
  270. package/dist/render/help.d.ts.map +1 -0
  271. package/dist/render/help.js +58 -0
  272. package/dist/render/help.js.map +1 -0
  273. package/dist/render/home.d.ts +30 -0
  274. package/dist/render/home.d.ts.map +1 -0
  275. package/dist/render/home.js +73 -0
  276. package/dist/render/home.js.map +1 -0
  277. package/dist/render/html.d.ts +66 -0
  278. package/dist/render/html.d.ts.map +1 -0
  279. package/dist/render/html.js +510 -0
  280. package/dist/render/html.js.map +1 -0
  281. package/dist/render/index.d.ts +47 -0
  282. package/dist/render/index.d.ts.map +1 -0
  283. package/dist/render/index.js +26 -0
  284. package/dist/render/index.js.map +1 -0
  285. package/dist/render/insightPlate.d.ts +55 -0
  286. package/dist/render/insightPlate.d.ts.map +1 -0
  287. package/dist/render/insightPlate.js +97 -0
  288. package/dist/render/insightPlate.js.map +1 -0
  289. package/dist/render/library.d.ts +32 -0
  290. package/dist/render/library.d.ts.map +1 -0
  291. package/dist/render/library.js +54 -0
  292. package/dist/render/library.js.map +1 -0
  293. package/dist/render/photo.d.ts +101 -0
  294. package/dist/render/photo.d.ts.map +1 -0
  295. package/dist/render/photo.js +241 -0
  296. package/dist/render/photo.js.map +1 -0
  297. package/dist/render/planPlate.d.ts +40 -0
  298. package/dist/render/planPlate.d.ts.map +1 -0
  299. package/dist/render/planPlate.js +72 -0
  300. package/dist/render/planPlate.js.map +1 -0
  301. package/dist/render/profilePlate.d.ts +17 -0
  302. package/dist/render/profilePlate.d.ts.map +1 -0
  303. package/dist/render/profilePlate.js +18 -0
  304. package/dist/render/profilePlate.js.map +1 -0
  305. package/dist/render/ranking.d.ts +22 -0
  306. package/dist/render/ranking.d.ts.map +1 -0
  307. package/dist/render/ranking.js +63 -0
  308. package/dist/render/ranking.js.map +1 -0
  309. package/dist/render/receipt.d.ts +71 -0
  310. package/dist/render/receipt.d.ts.map +1 -0
  311. package/dist/render/receipt.js +65 -0
  312. package/dist/render/receipt.js.map +1 -0
  313. package/dist/render/weightPlate.d.ts +54 -0
  314. package/dist/render/weightPlate.d.ts.map +1 -0
  315. package/dist/render/weightPlate.js +110 -0
  316. package/dist/render/weightPlate.js.map +1 -0
  317. package/dist/schema.d.ts +25 -0
  318. package/dist/schema.d.ts.map +1 -0
  319. package/dist/schema.js +281 -0
  320. package/dist/schema.js.map +1 -0
  321. package/dist/triggers/help-lookup.d.ts +28 -0
  322. package/dist/triggers/help-lookup.d.ts.map +1 -0
  323. package/dist/triggers/help-lookup.js +163 -0
  324. package/dist/triggers/help-lookup.js.map +1 -0
  325. package/dist/triggers/index.d.ts +24 -0
  326. package/dist/triggers/index.d.ts.map +1 -0
  327. package/dist/triggers/index.js +78 -0
  328. package/dist/triggers/index.js.map +1 -0
  329. package/dist/triggers/scene-01-home.d.ts +4 -0
  330. package/dist/triggers/scene-01-home.d.ts.map +1 -0
  331. package/dist/triggers/scene-01-home.js +13 -0
  332. package/dist/triggers/scene-01-home.js.map +1 -0
  333. package/dist/triggers/scene-02-diet.d.ts +4 -0
  334. package/dist/triggers/scene-02-diet.d.ts.map +1 -0
  335. package/dist/triggers/scene-02-diet.js +74 -0
  336. package/dist/triggers/scene-02-diet.js.map +1 -0
  337. package/dist/triggers/scene-03-weight.d.ts +4 -0
  338. package/dist/triggers/scene-03-weight.d.ts.map +1 -0
  339. package/dist/triggers/scene-03-weight.js +62 -0
  340. package/dist/triggers/scene-03-weight.js.map +1 -0
  341. package/dist/triggers/scene-04-exercise.d.ts +4 -0
  342. package/dist/triggers/scene-04-exercise.d.ts.map +1 -0
  343. package/dist/triggers/scene-04-exercise.js +43 -0
  344. package/dist/triggers/scene-04-exercise.js.map +1 -0
  345. package/dist/triggers/scene-05-workout.d.ts +4 -0
  346. package/dist/triggers/scene-05-workout.d.ts.map +1 -0
  347. package/dist/triggers/scene-05-workout.js +36 -0
  348. package/dist/triggers/scene-05-workout.js.map +1 -0
  349. package/dist/triggers/scene-06-goal.d.ts +4 -0
  350. package/dist/triggers/scene-06-goal.d.ts.map +1 -0
  351. package/dist/triggers/scene-06-goal.js +29 -0
  352. package/dist/triggers/scene-06-goal.js.map +1 -0
  353. package/dist/triggers/scene-07-profile.d.ts +4 -0
  354. package/dist/triggers/scene-07-profile.d.ts.map +1 -0
  355. package/dist/triggers/scene-07-profile.js +8 -0
  356. package/dist/triggers/scene-07-profile.js.map +1 -0
  357. package/dist/triggers/scene-08-body.d.ts +4 -0
  358. package/dist/triggers/scene-08-body.d.ts.map +1 -0
  359. package/dist/triggers/scene-08-body.js +17 -0
  360. package/dist/triggers/scene-08-body.js.map +1 -0
  361. package/dist/triggers/scene-09-photo.d.ts +4 -0
  362. package/dist/triggers/scene-09-photo.d.ts.map +1 -0
  363. package/dist/triggers/scene-09-photo.js +14 -0
  364. package/dist/triggers/scene-09-photo.js.map +1 -0
  365. package/dist/triggers/scene-10-analysis.d.ts +4 -0
  366. package/dist/triggers/scene-10-analysis.d.ts.map +1 -0
  367. package/dist/triggers/scene-10-analysis.js +180 -0
  368. package/dist/triggers/scene-10-analysis.js.map +1 -0
  369. package/dist/triggers/types.d.ts +66 -0
  370. package/dist/triggers/types.d.ts.map +1 -0
  371. package/dist/triggers/types.js +3 -0
  372. package/dist/triggers/types.js.map +1 -0
  373. package/package.json +35 -0
@@ -0,0 +1,719 @@
1
+ #!/usr/bin/env node
2
+ /** T11 #30 · 卡路里唯一出口 cmd_read:argv+JSON(stdout)+exit;非 0 走 stderr;超时 terminate+TOAST 降级标记。
3
+ * #40 · 写键同出口:35 写键(diet/water/weight/exercise/photo/product/profile/goal/body 各域,一律 receipt)
4
+ * 走 cli/write.ts 分发(memo.create/update/remove 范式:先调 fetch 库函数写库,再用 T10 receipt 组装回执)。
5
+ * 退出码对齐 skilllink 冻结(P9):0 ok;1 预检;2 用法/参数;3 key;4 取数/超时;5 envelope/渲染/落盘。
6
+ * stdout 纯净:成功只打 envelope JSON 一行(version/skill/shape/key/data 全字段,对齐 link-core 0.1.0)。
7
+ * 组合键为 registry 合法点式(见 cli/keys.ts;内部 VIEW_KEYS 下划线键仅渲染层复用,不直接登记)。
8
+ * 缺失阻断不返空:空库/空窗/无目标一律抛(CalorieRenderError missing-data / FetchError),exit 4,不返空数组冒充正常。
9
+ * 仅 type-only 消费 link-core(零运行时依赖);envelope 手工装配,形状校验本地镜像 link-core。
10
+ * HTML 用 --html 显式落盘(utf8):视图键走 render/html.ts 专属模板(与 T8/T9/T10 快照同源),其余走通用 section。
11
+ */
12
+ import { writeFileSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import { openDb } from '../schema.js';
15
+ import { DB_FILENAME } from '../paths.js';
16
+ import { listMeals } from '../fetch/diet.js';
17
+ import { getCalorieHistory } from '../fetch/history.js';
18
+ import { FetchError } from '../fetch/errors.js';
19
+ import { buildHomeData } from '../render/home.js';
20
+ import { buildDietOverview, buildMealDistribution, zeroMealDistribution } from '../render/diet.js';
21
+ import { buildExerciseView } from '../render/exercise.js';
22
+ import { buildGoalView } from '../render/goal.js';
23
+ import { buildGoalConfig, buildGoalRecommend, buildGoalWeight, buildGoalProgress, buildGoalStatus } from '../render/goalPlate.js';
24
+ import { buildWeightDashboard, buildWeightHistoryView, buildWeightCompareView, buildWeightReviewView, buildVolatilityView } from '../render/weightPlate.js';
25
+ import { buildBodyCompositionView, buildBodyMeasureView } from '../render/bodyPlate.js';
26
+ import { buildPlanView, buildPlanWizardView, buildExerciseGoalView } from '../render/planPlate.js';
27
+ import { buildGoalExpiringView, buildGoalPredictView, buildGoalVsActualView } from '../render/goalExtra.js';
28
+ import { buildPredictView, buildAnomalyView, buildContraView, buildDedupeView } from '../render/insightPlate.js';
29
+ import { buildProfileView } from '../render/profilePlate.js';
30
+ import { buildCombinedAnalysis, buildDeficitPlate, buildDietReview } from '../render/analysisPlate.js';
31
+ import { buildHealthPlate } from '../render/health.js';
32
+ import { buildAllRankings, buildFoodRankingPlate } from '../render/ranking.js';
33
+ import { buildProductLibrary, buildProductSearch, buildProductStats } from '../render/library.js';
34
+ import { buildCompareData, buildGalleryData, buildGifTask, buildViewerData } from '../render/photo.js';
35
+ import { buildPhotoHelp, lookupPhotoHelp } from '../render/help.js';
36
+ import { renderCombinedHtml, renderDeficitHtml, renderDietHtml, renderDietReviewHtml, renderExerciseHtml, renderGalleryHtml, renderCompareHtml, renderViewerHtml, renderGifHtml, renderPhotoHelpHtml, renderGoalConfigHtml, renderGoalRecommendHtml, renderGoalWeightHtml, renderGoalProgressHtml, renderGoalStatusHtml, renderGoalHtml, renderHealthHtml, renderHomeHtml, renderProductLibraryHtml, renderProductSearchHtml, renderRankingHtml, renderAllRankingsHtml, renderWeightHtml, renderWeightHistoryHtml, renderWeightCompareHtml, renderWeightReviewHtml, renderVolatilityHtml, renderBodyCompositionHtml, renderBodyMeasureHtml, renderPlanHtml, renderPlanWizardHtml, renderExerciseGoalHtml, renderGoalExpiringHtml, renderGoalPredictHtml, renderGoalVsActualHtml, renderPredictHtml, renderAnomalyHtml, renderContraHtml, renderDedupeHtml, renderProfileHtml, } from '../render/html.js';
37
+ import { assertStatMetrics } from '../render/envelope.js';
38
+ import { CalorieRenderError } from '../render/errors.js';
39
+ import { TRIGGERS, searchHelp } from '../triggers/index.js';
40
+ import { shiftISODate, todayISO } from '../analysis/utils.js';
41
+ import { CALORIE_COMBOS, ENVELOPE_VERSION, CALORIE_SKILL, calorieShapeFor, isCalorieWriteKey } from './keys.js';
42
+ import { dispatchWrite } from './write.js';
43
+ const DEFAULT_TIMEOUT_MS = 30000;
44
+ function fail(code, msg) {
45
+ console.error('ERR ' + code + ': ' + msg);
46
+ process.exit(code);
47
+ }
48
+ function toast(msg) {
49
+ console.error('TOAST: ' + msg);
50
+ }
51
+ function preflight() {
52
+ const v = process.versions.node.split('.').map(Number);
53
+ const major = v[0];
54
+ const minor = v[1];
55
+ if (!(major > 22 || (major === 22 && minor >= 13)))
56
+ fail(1, 'node 低于 22.13:' + process.versions.node);
57
+ const p = process.env.SKILLS_DB_PATH;
58
+ if (!p)
59
+ fail(1, 'SKILLS_DB_PATH 未设置(无默认值,必设)');
60
+ return p;
61
+ }
62
+ function parseArgs(a) {
63
+ const o = {
64
+ key: a[0], params: undefined, html: undefined, timeout: DEFAULT_TIMEOUT_MS,
65
+ };
66
+ for (let i = 1; i < a.length; i++) {
67
+ if (a[i] === '--params' && i + 1 < a.length)
68
+ o.params = a[++i];
69
+ else if (a[i] === '--html' && i + 1 < a.length)
70
+ o.html = a[++i];
71
+ else if (a[i] === '--timeout' && i + 1 < a.length) {
72
+ o.timeout = Number(a[++i]);
73
+ if (!Number.isFinite(o.timeout) || o.timeout <= 0)
74
+ fail(2, '--timeout 须为正数毫秒');
75
+ }
76
+ else
77
+ fail(2, '未知参数:' + a[i] + '(用法:cmd_read <calorie.key> [--params JSON对象] [--html 输出路径] [--timeout 毫秒])');
78
+ }
79
+ return o;
80
+ }
81
+ function needStr(params, name) {
82
+ const v = params[name];
83
+ if (typeof v !== 'string' || v.length === 0)
84
+ fail(2, '缺参数 ' + name);
85
+ return v;
86
+ }
87
+ function optStr(params, name) {
88
+ const v = params[name];
89
+ if (v === undefined || v === null)
90
+ return undefined;
91
+ if (typeof v !== 'string')
92
+ fail(2, '参数 ' + name + ' 须为字符串');
93
+ return v;
94
+ }
95
+ function optNum(params, name) {
96
+ const v = params[name];
97
+ if (v === undefined || v === null)
98
+ return undefined;
99
+ if (typeof v !== 'number' || !Number.isFinite(v))
100
+ fail(2, '参数 ' + name + ' 须为有限 number');
101
+ return v;
102
+ }
103
+ function assertISO(v, field) {
104
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(v))
105
+ fail(2, field + ' 非法(须 YYYY-MM-DD):' + v);
106
+ }
107
+ function latestFoodDate(db) {
108
+ try {
109
+ const row = db.prepare('SELECT MAX(date) AS m FROM food_log').get();
110
+ return row?.m ?? null;
111
+ }
112
+ catch {
113
+ return null;
114
+ }
115
+ }
116
+ function defaultRange(db, params) {
117
+ let end = optStr(params, 'end') ?? optStr(params, 'date') ?? optStr(params, 'today') ?? undefined;
118
+ let start = optStr(params, 'start') ?? undefined;
119
+ if (end)
120
+ assertISO(end, 'end');
121
+ if (start)
122
+ assertISO(start, 'start');
123
+ if (start && end) {
124
+ if (start > end)
125
+ fail(2, 'start 不得晚于 end');
126
+ return { start, end };
127
+ }
128
+ const latest = latestFoodDate(db) ?? todayISO();
129
+ end = end ?? latest;
130
+ assertISO(end, 'end');
131
+ if (start) {
132
+ if (start > end)
133
+ fail(2, 'start 不得晚于 end');
134
+ return { start, end: end };
135
+ }
136
+ return { start: shiftISODate(end, -6), end: end };
137
+ }
138
+ /** 本地 envelope 形状校验(镜像 link-core assertShapeData,不运行时 import)。 */
139
+ function assertEnvelopeData(shape, data) {
140
+ switch (shape) {
141
+ case 'list':
142
+ if (!Array.isArray(data['items']))
143
+ throw new CalorieRenderError('bad-input', 'list 形缺 items 数组');
144
+ if (data['total'] !== undefined && typeof data['total'] !== 'number')
145
+ throw new CalorieRenderError('bad-input', 'list 形 total 须为 number');
146
+ break;
147
+ case 'detail':
148
+ if (typeof data['item'] !== 'object' || data['item'] === null || Array.isArray(data['item'])) {
149
+ throw new CalorieRenderError('bad-input', 'detail 形缺 item 对象');
150
+ }
151
+ break;
152
+ case 'stat':
153
+ if (typeof data['metrics'] !== 'object' || data['metrics'] === null || Array.isArray(data['metrics'])) {
154
+ throw new CalorieRenderError('bad-input', 'stat 形缺 metrics 对象');
155
+ }
156
+ assertStatMetrics(data['metrics']);
157
+ break;
158
+ case 'receipt':
159
+ if (typeof data['ok'] !== 'boolean' || typeof data['message'] !== 'string') {
160
+ throw new CalorieRenderError('bad-input', 'receipt 形缺 ok/message 全字段');
161
+ }
162
+ break;
163
+ case 'analysis':
164
+ if (typeof data['summary'] !== 'string' || data['summary'].length === 0) {
165
+ throw new CalorieRenderError('bad-input', 'analysis 形缺 summary 全字段');
166
+ }
167
+ break;
168
+ case 'fallback':
169
+ if (typeof data['reason'] !== 'string' || data['reason'].length === 0 || data['degraded'] !== true) {
170
+ throw new CalorieRenderError('bad-input', 'fallback 形缺 reason/degraded:true 全字段');
171
+ }
172
+ break;
173
+ default:
174
+ throw new CalorieRenderError('bad-input', '未知 shape:' + String(shape));
175
+ }
176
+ }
177
+ function buildEnvelope(key, shape, data) {
178
+ assertEnvelopeData(shape, data);
179
+ return { version: ENVELOPE_VERSION, skill: CALORIE_SKILL, shape, key, data };
180
+ }
181
+ /** 非空数挑拣:null/undefined/NaN/Infinity 一律丢弃(stat.metrics 须全有限 number)。 */
182
+ function nums(input) {
183
+ const out = {};
184
+ for (const [k, v] of Object.entries(input)) {
185
+ if (typeof v === 'number' && Number.isFinite(v))
186
+ out[k] = v;
187
+ }
188
+ return out;
189
+ }
190
+ function photosDirOf(params) {
191
+ const p = optStr(params, 'photosDir');
192
+ if (p)
193
+ return p;
194
+ const e = process.env.CALORIE_PHOTOS_DIR;
195
+ return e ? e : undefined;
196
+ }
197
+ // 全键分发:读走 render/fetch 读,HELP 走触发词现找;未知键上游已拦,此处再拦一道。
198
+ /** #41 · 测试直调出口(纯 CLI 同逻辑,不经过 argv/spawn;CLI 唯一出口仍为 main)。 */
199
+ export function dispatch(key, params, db) {
200
+ switch (key) {
201
+ case 'calorie.today': {
202
+ const date = optStr(params, 'date') ?? latestFoodDate(db) ?? todayISO();
203
+ assertISO(date, 'date');
204
+ const rows = listMeals(db, date).filter((r) => r.food_name !== '💧水');
205
+ if (rows.length === 0)
206
+ throw new CalorieRenderError('missing-data', '无饮食记录(' + date + ')');
207
+ const items = rows.map((r) => ({ id: r.id, date: r.date, time: r.time, food_name: r.food_name, grams: r.grams, calories: r.calories, protein: r.protein, carbs: r.carbs, fat: r.fat }));
208
+ const o = buildDietOverview(db, date, date);
209
+ const dist = buildMealDistribution(db, date);
210
+ return { data: { items, total: items.length }, html: renderDietHtml(o, dist) };
211
+ }
212
+ case 'calorie.view.home': {
213
+ const date = optStr(params, 'date') ?? optStr(params, 'today') ?? latestFoodDate(db) ?? todayISO();
214
+ assertISO(date, 'date');
215
+ const windowDays = optNum(params, 'windowDays') ?? 7;
216
+ if (!Number.isInteger(windowDays) || windowDays < 1 || windowDays > 90)
217
+ fail(2, 'windowDays 须为 1..90 整数');
218
+ const h = buildHomeData(db, date, windowDays);
219
+ const metrics = nums({
220
+ calorieGoal: h.calorieGoal, waterGoal: h.waterGoal, caloriePct: h.caloriePct, proteinPct: h.proteinPct,
221
+ waterPct: h.waterPct, deficitToday: h.deficitToday, streakDays: h.streakDays,
222
+ intakeCal: h.daily.totals.cal, proteinG: h.daily.totals.pro, carbsG: h.daily.totals.carbs, fatG: h.daily.totals.fat,
223
+ waterMl: h.daily.waterMl, entryCount: h.daily.entryCount, avgIntake: h.week.avgIntake, avgDeficit: h.week.avgDeficit,
224
+ loggedDays: h.week.loggedDays,
225
+ });
226
+ return { data: { metrics }, html: renderHomeHtml(h) };
227
+ }
228
+ case 'calorie.view.diet': {
229
+ const { start, end } = defaultRange(db, params);
230
+ const date = optStr(params, 'date') ?? end;
231
+ assertISO(date, 'date');
232
+ const o = buildDietOverview(db, start, end);
233
+ // C4 #43 · 尾日空回零(窗内有数不掀整窗 missing;窗全空由上行 overview 抛 missing-data)。
234
+ let dist;
235
+ try {
236
+ dist = buildMealDistribution(db, date);
237
+ }
238
+ catch (e) {
239
+ if (e instanceof CalorieRenderError && e.code === 'missing-data')
240
+ dist = zeroMealDistribution(date);
241
+ else
242
+ throw e;
243
+ }
244
+ const metrics = nums({
245
+ totalCalories: o.totalCalories, avgCalories: o.avgCalories, calorieGoal: o.calorieGoal,
246
+ loggedDays: o.loggedDays, days: o.days, distTotal: dist.totalCalories,
247
+ 'meal.早餐': dist.slices.find((s) => s.meal === '早餐')?.calories,
248
+ 'meal.午餐': dist.slices.find((s) => s.meal === '午餐')?.calories,
249
+ 'meal.晚餐': dist.slices.find((s) => s.meal === '晚餐')?.calories,
250
+ 'meal.加餐': dist.slices.find((s) => s.meal === '加餐')?.calories,
251
+ });
252
+ return { data: { metrics }, html: renderDietHtml(o, dist) };
253
+ }
254
+ case 'calorie.view.exercise': {
255
+ const { start, end } = defaultRange(db, params);
256
+ const v = buildExerciseView(db, start, end);
257
+ const metrics = nums({
258
+ totalBurned: v.review.totalBurned, totalMinutes: v.review.totalMinutes, sessions: v.review.sessions,
259
+ activeDays: v.review.activeDays, totalBurnedSeries: v.totalBurnedSeries,
260
+ avgBurnedPerLoggedDay: v.avgBurnedPerLoggedDay, seriesActiveDays: v.activeDays,
261
+ });
262
+ return { data: { metrics }, html: renderExerciseHtml(v) };
263
+ }
264
+ case 'calorie.view.goal': {
265
+ const { start, end } = defaultRange(db, params);
266
+ const v = buildGoalView(db, start, end);
267
+ const metrics = nums({
268
+ calorie_goal: v.nutrition.calorie_goal, protein_goal: v.nutrition.protein_goal,
269
+ carbs_goal: v.nutrition.carbs_goal, fat_goal: v.nutrition.fat_goal, water_goal: v.nutrition.water_goal,
270
+ completionPct: v.completionPct, weeklyDeficit: v.deficit.summary.weeklyDeficit,
271
+ predictedLossKg: v.deficit.summary.predictedLossKg, avgDeficit: v.deficit.summary.avgDeficit,
272
+ avgIntake: v.deficit.summary.avgIntake, trendAvg: v.trend.summary.avg,
273
+ completedCount: v.history.completedCount, incompleteCount: v.history.incompleteCount,
274
+ });
275
+ return { data: { metrics }, html: renderGoalHtml(v) };
276
+ }
277
+ case 'calorie.view.goal-config': {
278
+ const g = buildGoalConfig(db);
279
+ const metrics = nums({
280
+ calorie_goal: g.nutrition.calorie_goal, protein_goal: g.nutrition.protein_goal,
281
+ carbs_goal: g.nutrition.carbs_goal, fat_goal: g.nutrition.fat_goal, water_goal: g.nutrition.water_goal,
282
+ diffKcal: g.diffKcal, consistent: g.consistent ? 1 : 0, paused: g.paused ? 1 : 0,
283
+ });
284
+ return { data: { metrics }, html: renderGoalConfigHtml(g) };
285
+ }
286
+ case 'calorie.view.goal-recommend': {
287
+ const profile = optStr(params, 'profile') ?? 'cut';
288
+ if (!['cut', 'maintain', 'bulk'].includes(profile))
289
+ fail(2, 'profile 非法(cut/maintain/bulk):' + profile);
290
+ const g = buildGoalRecommend(db, profile);
291
+ const metrics = nums({
292
+ calorieGoal: g.recommend.calorieGoal, proteinGoal: g.recommend.proteinGoal, carbsGoal: g.recommend.carbsGoal,
293
+ fatGoal: g.recommend.fatGoal, waterGoal: g.recommend.waterGoal, tdee: g.recommend.tdee, bmr: g.recommend.bmr,
294
+ weeklyRateKg: g.recommend.weeklyRateKg, weightKg: g.recommend.basis.weightKg,
295
+ recommendedWaterMl: g.water.recommendedWaterMl, mlPerKg: g.water.mlPerKg,
296
+ });
297
+ return { data: { metrics }, html: renderGoalRecommendHtml(g) };
298
+ }
299
+ case 'calorie.view.goal-weight': {
300
+ const { start, end } = defaultRange(db, params);
301
+ const g = buildGoalWeight(db, start, end);
302
+ const metrics = nums({ weightGoal: g.weightGoal, latestKg: g.latestKg, deltaKg: g.deltaKg, loggedDays: g.loggedDays });
303
+ return { data: { metrics }, html: renderGoalWeightHtml(g) };
304
+ }
305
+ case 'calorie.view.goal-progress': {
306
+ const { start, end } = defaultRange(db, params);
307
+ const historyDays = optNum(params, 'historyDays') ?? 30;
308
+ if (!Number.isInteger(historyDays) || historyDays < 1 || historyDays > 365)
309
+ fail(2, 'historyDays 须为 1..365 整数');
310
+ const g = buildGoalProgress(db, start, end, historyDays);
311
+ const metrics = nums({
312
+ calorie_goal: g.nutrition.calorie_goal, completionPct: g.completionPct,
313
+ weeklyDeficit: g.deficit.summary.weeklyDeficit, predictedLossKg: g.deficit.summary.predictedLossKg,
314
+ avgDeficit: g.deficit.summary.avgDeficit, trendAvg: g.trend.summary.avg,
315
+ completedCount: g.history.completedCount, incompleteCount: g.history.incompleteCount,
316
+ });
317
+ return { data: { metrics }, html: renderGoalProgressHtml(g) };
318
+ }
319
+ case 'calorie.view.goal-status': {
320
+ const g = buildGoalStatus(db);
321
+ const metrics = nums({ paused: g.paused ? 1 : 0, calorie_goal: g.nutrition.calorie_goal, water_goal: g.nutrition.water_goal });
322
+ return { data: { metrics }, html: renderGoalStatusHtml(g) };
323
+ }
324
+ case 'calorie.view.combined': {
325
+ const pair = optStr(params, 'pair') ?? 'weight_calorie';
326
+ const window = optStr(params, 'window') ?? '7d';
327
+ const start = optStr(params, 'start') ?? null;
328
+ const end = optStr(params, 'end') ?? null;
329
+ const today = optStr(params, 'today') ?? latestFoodDate(db) ?? todayISO();
330
+ const c = buildCombinedAnalysis(db, pair, window, start, end, today);
331
+ const metrics = nums({
332
+ aAvg: c.analysis.aAvg, bAvg: c.analysis.bAvg, aDelta: c.analysis.aDelta, bDelta: c.analysis.bDelta,
333
+ aCount: c.analysis.aCount, bCount: c.analysis.bCount,
334
+ correlationR: c.analysis.correlation.r, correlationN: c.analysis.correlation.n, days: c.analysis.days,
335
+ seriesDays: c.series.length,
336
+ });
337
+ return { data: { metrics }, html: renderCombinedHtml(c) };
338
+ }
339
+ case 'calorie.view.deficit': {
340
+ const { start, end } = defaultRange(db, params);
341
+ const d = buildDeficitPlate(db, start, end);
342
+ const metrics = nums({
343
+ avgIntake: d.summary.avgIntake, avgBurn: d.summary.avgBurn, avgExerciseBurn: d.summary.avgExerciseBurn,
344
+ avgDeficit: d.summary.avgDeficit, weeklyDeficit: d.summary.weeklyDeficit, predictedLossKg: d.summary.predictedLossKg,
345
+ days: d.meta.days, weekdayCount: d.meta.weekdayCount, weekendCount: d.meta.weekendCount,
346
+ targetIntake: d.target.intake, targetTdee: d.target.tdee,
347
+ });
348
+ return { data: { metrics }, html: renderDeficitHtml(d) };
349
+ }
350
+ case 'calorie.view.diet-review': {
351
+ const { start, end } = defaultRange(db, params);
352
+ const r = buildDietReview(db, start, end);
353
+ const metrics = nums({
354
+ loggedDays: r.loggedDays,
355
+ 'meal.早餐': r.byMeal.find((s) => s.meal === '早餐')?.totalCalories,
356
+ 'meal.午餐': r.byMeal.find((s) => s.meal === '午餐')?.totalCalories,
357
+ 'meal.晚餐': r.byMeal.find((s) => s.meal === '晚餐')?.totalCalories,
358
+ 'meal.加餐': r.byMeal.find((s) => s.meal === '加餐')?.totalCalories,
359
+ });
360
+ return { data: { metrics }, html: renderDietReviewHtml(r) };
361
+ }
362
+ case 'calorie.view.health': {
363
+ const { start, end } = defaultRange(db, params);
364
+ const h = buildHealthPlate(db, start, end);
365
+ const metrics = nums({ loggedDays: h.loggedDays, avgIntake: h.avgIntake, avgDeficit: h.avgDeficit });
366
+ return { data: { metrics }, html: renderHealthHtml(h) };
367
+ }
368
+ case 'calorie.view.ranking': {
369
+ const { start, end } = defaultRange(db, params);
370
+ const category = optStr(params, 'category');
371
+ const topN = optNum(params, 'topN') ?? 5;
372
+ if (!Number.isInteger(topN) || topN < 1 || topN > 50)
373
+ fail(2, 'topN 须为 1..50 整数');
374
+ if (category) {
375
+ const one = buildFoodRankingPlate(db, start, end, category, topN);
376
+ const top = one.items[0];
377
+ const metrics = nums({ total: one.items.length, topN: one.topN, topCal: top?.totalCal, topCnt: top?.cnt, topRank: top?.rank });
378
+ return { data: { metrics }, html: renderRankingHtml(one) };
379
+ }
380
+ const all = buildAllRankings(db, start, end, topN);
381
+ const metrics = nums({ okCount: all.okCount, topN: all.topN });
382
+ return { data: { metrics }, html: renderAllRankingsHtml(all) };
383
+ }
384
+ case 'calorie.view.library': {
385
+ const category = optStr(params, 'category') ?? null;
386
+ const limit = optNum(params, 'limit') ?? 50;
387
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100)
388
+ fail(2, 'limit 须为 1..100 整数');
389
+ const lib = buildProductLibrary(db, category, limit);
390
+ const stats = buildProductStats(db);
391
+ const metrics = nums({ total: lib.total, statsTotal: stats.total });
392
+ return { data: { metrics }, html: renderProductLibraryHtml(lib) };
393
+ }
394
+ case 'calorie.view.search': {
395
+ const keyword = needStr(params, 'keyword');
396
+ const limit = optNum(params, 'limit') ?? 20;
397
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100)
398
+ fail(2, 'limit 须为 1..100 整数');
399
+ const s = buildProductSearch(db, keyword, limit);
400
+ const metrics = nums({ total: s.total, limit: limit });
401
+ return { data: { metrics }, html: renderProductSearchHtml(s) };
402
+ }
403
+ case 'calorie.photo.list': {
404
+ const dir = photosDirOf(params);
405
+ const filter = {};
406
+ for (const k of ['tag', 'dateFrom', 'dateTo', 'days', 'limit', 'today']) {
407
+ if (params[k] !== undefined)
408
+ filter[k] = params[k];
409
+ }
410
+ const g = buildGalleryData(db, filter, dir ?? null);
411
+ const items = g.photos.map((p) => ({ id: p.id, date: p.date, photoPath: p.photoPath, tagList: p.tagList, fileExists: p.fileExists }));
412
+ return { data: { items, total: g.totalCount }, html: renderGalleryHtml(g) };
413
+ }
414
+ case 'calorie.photo.detail': {
415
+ const id = params['id'];
416
+ if (typeof id !== 'number' || !Number.isInteger(id))
417
+ fail(2, '缺参数 id(整数照片 id)');
418
+ const dir = photosDirOf(params);
419
+ const v = buildViewerData(db, id, dir ?? null);
420
+ return { data: { item: { id: v.photo.id, date: v.photo.date, photoPath: v.photo.photoPath, tagList: v.photo.tagList } }, html: renderViewerHtml(v) };
421
+ }
422
+ case 'calorie.photo.compare': {
423
+ const id1 = params['id1'];
424
+ const id2 = params['id2'];
425
+ if (typeof id1 !== 'number' || !Number.isInteger(id1))
426
+ fail(2, '缺参数 id1(整数)');
427
+ if (typeof id2 !== 'number' || !Number.isInteger(id2))
428
+ fail(2, '缺参数 id2(整数)');
429
+ const dir = photosDirOf(params);
430
+ const c = buildCompareData(db, id1, id2, dir ?? null);
431
+ const items = [c.photo1, c.photo2].map((p) => ({ id: p.id, date: p.date, photoPath: p.photoPath, tagList: p.tagList }));
432
+ return { data: { items, total: 2 }, html: renderCompareHtml(c) };
433
+ }
434
+ case 'calorie.photo.gif': {
435
+ const tag = needStr(params, 'tag');
436
+ const gif = buildGifTask(db, {
437
+ tag, dateFrom: (optStr(params, 'dateFrom') ?? null),
438
+ dateTo: (optStr(params, 'dateTo') ?? null),
439
+ days: (optNum(params, 'days') ?? 90),
440
+ });
441
+ return { data: { summary: 'GIF 任务:标签 ' + gif.tag + ' 共 ' + gif.photoCount + ' 张(' + (gif.firstDate ?? '—') + ' ~ ' + (gif.lastDate ?? '—') + ')· ' + gif.note }, html: renderGifHtml(gif) };
442
+ }
443
+ case 'calorie.help.center': {
444
+ const q = optStr(params, 'q') ?? optStr(params, 'keyword') ?? undefined;
445
+ const hits = q ? lookupPhotoHelp(q) : buildPhotoHelp();
446
+ if (q && hits.length === 0)
447
+ throw new CalorieRenderError('missing-data', 'HELP 无命中:' + q);
448
+ const items = hits.map((h) => ({ wakeWord: h.wakeWord, key: h.key, desc: h.desc, exec: h.exec }));
449
+ return { data: { items, total: items.length }, html: renderPhotoHelpHtml(hits, q) };
450
+ }
451
+ case 'calorie.help.lookup': {
452
+ const q = needStr(params, 'q');
453
+ // C2/C3 #43 · 唯一搜索入口 searchHelp:别名感知 + 可执行排前 + 高频词合成首条(去legacy首命中)。
454
+ const found = searchHelp(TRIGGERS, q);
455
+ if (found.length === 0)
456
+ throw new CalorieRenderError('missing-data', '唤醒词无命中:' + q);
457
+ const sceneToCategory = { '01': '主页', '02': '饮食', '03': '体重', '04': '运动', '05': '健身计划', '06': '目标管理', '07': '基础信息', '08': '身体细节', '09': '身材照片', '10': '分析' };
458
+ const hits = found.map((h) => {
459
+ const src = TRIGGERS.find((t) => t.wake_word === h.wake_word && ('key' in t ? String(t.key ?? '') : '') === String(h.key ?? ''));
460
+ const category = src ? src.category : (sceneToCategory[h.scene] ?? h.scene);
461
+ return { wake_word: h.wake_word, category, key: String(h.key ?? ''), cli: h.cli, desc: h.desc };
462
+ });
463
+ const html = '<section class="ilife-page" data-skill="calorie" data-slot="ilife:calorie:help"><h1>唤醒词 HELP 速查 · ' + q.replace(/&/g, '&amp;').replace(/</g, '&lt;') + '(' + hits.length + ' 条)</h1>' +
464
+ hits.map((h) => '<div class="ilife-item"><b>' + String(h.wake_word).replace(/&/g, '&amp;').replace(/</g, '&lt;') + '</b> <span>' + String(h.key).replace(/&/g, '&amp;') + '</span><div>' + String(h.desc).replace(/&/g, '&amp;') + '</div><pre>' + String(h.cli).replace(/&/g, '&amp;').replace(/</g, '&lt;') + '</pre></div>').join('') + '</section>';
465
+ return { data: { items: hits, total: hits.length }, html };
466
+ }
467
+ case 'calorie.history': {
468
+ const days = optNum(params, 'days') ?? 7;
469
+ if (!Number.isInteger(days) || days < 1 || days > 365)
470
+ fail(2, 'days 须为 1..365 整数');
471
+ const h = getCalorieHistory(db, days);
472
+ if (h.rows.length === 0)
473
+ throw new CalorieRenderError('missing-data', '最近' + String(days) + '天无记录');
474
+ const items = h.rows.map((r) => ({ date: r.date, calories: r.calories, protein: r.protein, status: r.status }));
475
+ const html = '<section class="ilife-page" data-skill="calorie" data-slot="ilife:calorie:history"><h1>热量历史(最近' + h.days + '天)</h1>' +
476
+ items.map((r) => '<div class="ilife-item"><b>' + r.date + '</b> ' + r.calories + ' 卡 · ' + String(r.status).replace(/&/g, '&amp;') + '</div>').join('') + '</section>';
477
+ return { data: { items, total: items.length }, html };
478
+ }
479
+ case 'calorie.view.weight': {
480
+ const { start, end } = defaultRange(db, params);
481
+ const w = buildWeightDashboard(db, start, end);
482
+ const metrics = nums({
483
+ recordCount: w.trend.recordCount, avgWeight: w.trend.avgWeight,
484
+ maxWeight: w.trend.maxWeight, minWeight: w.trend.minWeight,
485
+ firstWeight: w.trend.firstWeight, lastWeight: w.trend.lastWeight,
486
+ changeKg: w.trend.changeKg, dailyChangeG: w.trend.dailyChangeG,
487
+ weightGoal: w.weightGoal, gapKg: w.gapKg,
488
+ });
489
+ return { data: { metrics }, html: renderWeightHtml(w) };
490
+ }
491
+ case 'calorie.view.weight-history': {
492
+ const startDate = optStr(params, 'startDate') ?? optStr(params, 'start');
493
+ const endDate = optStr(params, 'endDate') ?? optStr(params, 'end');
494
+ const days = optNum(params, 'days');
495
+ let h;
496
+ if (startDate && endDate)
497
+ h = buildWeightHistoryView(db, { startDate, endDate });
498
+ else if (startDate && !endDate)
499
+ h = buildWeightHistoryView(db, { startDate });
500
+ else if (days !== undefined)
501
+ h = buildWeightHistoryView(db, { days });
502
+ else
503
+ h = buildWeightHistoryView(db, {});
504
+ const metrics = nums({
505
+ rows: h.rows.length,
506
+ spanDays: h.change?.spanDays, first: h.change?.first, last: h.change?.last,
507
+ delta: h.change?.delta, dailyAvg: h.change?.dailyAvg,
508
+ });
509
+ return { data: { metrics }, html: renderWeightHistoryHtml(h) };
510
+ }
511
+ case 'calorie.view.weight-compare': {
512
+ const start = needStr(params, 'start');
513
+ const end = needStr(params, 'end');
514
+ const compareStart = needStr(params, 'compareStart');
515
+ const compareEnd = needStr(params, 'compareEnd');
516
+ const v = buildWeightCompareView(db, start, end, compareStart, compareEnd);
517
+ const metrics = nums({
518
+ avgDiff: v.compare.avgDiff,
519
+ currentAvg: v.compare.currentPeriod.avgWeight, compareAvg: v.compare.comparePeriod.avgWeight,
520
+ currentChange: v.compare.currentPeriod.changeKg, compareChange: v.compare.comparePeriod.changeKg,
521
+ });
522
+ return { data: { metrics }, html: renderWeightCompareHtml(v) };
523
+ }
524
+ case 'calorie.view.weight-review': {
525
+ const today = optStr(params, 'today') ?? optStr(params, 'date');
526
+ const v = buildWeightReviewView(db, today ?? undefined);
527
+ const metrics = nums({
528
+ currentWeight: v.milestone.currentWeight, weightGoal: v.milestone.weightGoal,
529
+ gapKg: v.milestone.gapKg, actualDailyChangeKg: v.milestone.actualDailyChangeKg,
530
+ estDays: v.milestone.estDays, calorieAdjustment: v.milestone.calorieAdjustment,
531
+ });
532
+ return { data: { metrics }, html: renderWeightReviewHtml(v) };
533
+ }
534
+ case 'calorie.view.volatility': {
535
+ const { start, end } = defaultRange(db, params);
536
+ const mode = (optStr(params, 'baselineMode') ?? optStr(params, 'mode') ?? 'rolling');
537
+ const v = buildVolatilityView(db, start, end, mode);
538
+ const metrics = nums({
539
+ baselineValue: v.volatility.baselineValue, baselineSigma: v.volatility.baselineSigma,
540
+ yellow: v.volatility.thresholds.yellow, red: v.volatility.thresholds.red,
541
+ points: v.volatility.points.length, anomalies: v.volatility.recentAnomalies.length,
542
+ deviationKg: v.volatility.earlyWarning.deviationKg,
543
+ });
544
+ return { data: { metrics }, html: renderVolatilityHtml(v) };
545
+ }
546
+ case 'calorie.view.body-composition': {
547
+ const days = optNum(params, 'days') ?? 90;
548
+ const source = optStr(params, 'source');
549
+ const limit = optNum(params, 'limit') ?? 20;
550
+ const v = buildBodyCompositionView(db, { days: days, source: source ?? undefined, limit: limit });
551
+ const metrics = nums({ total: v.total, latestPct: v.latestPct, trendDays: v.trend.length });
552
+ return { data: { metrics }, html: renderBodyCompositionHtml(v) };
553
+ }
554
+ case 'calorie.view.body-measure': {
555
+ const metric = optStr(params, 'metric');
556
+ const days = optNum(params, 'days') ?? 90;
557
+ const limit = optNum(params, 'limit') ?? 20;
558
+ const dateFrom = optStr(params, 'dateFrom');
559
+ const dateTo = optStr(params, 'dateTo');
560
+ const v = buildBodyMeasureView(db, { metric: metric ?? undefined, days: days, limit: limit, dateFrom: dateFrom ?? undefined, dateTo: dateTo ?? undefined });
561
+ const metrics = nums({ total: v.total, latestVal: v.latestVal, trendDays: v.trend.length });
562
+ return { data: { metrics }, html: renderBodyMeasureHtml(v) };
563
+ }
564
+ case 'calorie.view.plan': {
565
+ const v = buildPlanView(db);
566
+ const metrics = nums({ totalSessions: v.totalSessions, totalMovements: v.totalMovements, totalWeeks: v.totalWeeks });
567
+ return { data: { metrics }, html: renderPlanHtml(v) };
568
+ }
569
+ case 'calorie.view.plan-wizard': {
570
+ const plan = params['plan'];
571
+ if (typeof plan !== 'object' || plan === null || Array.isArray(plan))
572
+ fail(2, '缺参数 plan(PlanInput 对象)');
573
+ const catalog = params['catalog'];
574
+ const v = buildPlanWizardView(plan, catalog ?? undefined);
575
+ const metrics = nums({ errorCount: v.errorCount, warningCount: v.warningCount, insertedCount: v.insertedCount });
576
+ return { data: { metrics }, html: renderPlanWizardHtml(v) };
577
+ }
578
+ case 'calorie.view.exercise-goal': {
579
+ const { start, end } = defaultRange(db, params);
580
+ const v = buildExerciseGoalView(db, start, end);
581
+ const metrics = nums({ dailyGoal: v.dailyGoal, goalTotal: v.goalTotal, actual: v.actual, pct: v.pct, gap: v.gap, achieved: v.achieved ? 1 : 0, days: v.days });
582
+ return { data: { metrics }, html: renderExerciseGoalHtml(v) };
583
+ }
584
+ case 'calorie.view.goal-expiring': {
585
+ const withinDays = optNum(params, 'withinDays') ?? optNum(params, 'days') ?? 14;
586
+ const today = optStr(params, 'today');
587
+ const v = buildGoalExpiringView(db, withinDays, today ?? undefined);
588
+ const metrics = nums({ daysLeft: v.daysLeft, withinDays: v.withinDays, expiring: v.expiring ? 1 : 0, weightGoal: v.weightGoal, calorieGoal: v.calorieGoal });
589
+ return { data: { metrics }, html: renderGoalExpiringHtml(v) };
590
+ }
591
+ case 'calorie.view.goal-predict': {
592
+ const { start, end } = defaultRange(db, params);
593
+ const v = buildGoalPredictView(db, start, end);
594
+ const metrics = nums({ targetKg: v.targetKg, current: v.current, daysLeft: v.daysLeft, ratePerWeek: v.ratePerWeek, feasible: v.feasible ? 1 : 0 });
595
+ return { data: { metrics }, html: renderGoalPredictHtml(v) };
596
+ }
597
+ case 'calorie.view.goal-vs-actual': {
598
+ const { start, end } = defaultRange(db, params);
599
+ const historyDays = optNum(params, 'historyDays') ?? 30;
600
+ const v = buildGoalVsActualView(db, start, end, historyDays);
601
+ const metrics = nums({
602
+ completedCount: v.completedCount, incompleteCount: v.incompleteCount,
603
+ completionPct: v.completionPct, trendAvg: v.trendAvg, calorieGoal: v.calorieGoal,
604
+ });
605
+ return { data: { metrics }, html: renderGoalVsActualHtml(v) };
606
+ }
607
+ case 'calorie.view.predict': {
608
+ const { start, end } = defaultRange(db, params);
609
+ const horizonDays = optNum(params, 'horizonDays') ?? optNum(params, 'days') ?? 30;
610
+ const v = buildPredictView(db, start, end, horizonDays);
611
+ const metrics = nums({ current: v.current, ratePerWeek: v.ratePerWeek, forecastValue: v.forecastValue, forecastLo: v.forecastLo, forecastHi: v.forecastHi, horizonDays: v.horizonDays });
612
+ return { data: { metrics }, html: renderPredictHtml(v) };
613
+ }
614
+ case 'calorie.view.anomaly': {
615
+ const kind = needStr(params, 'kind');
616
+ const { start, end } = defaultRange(db, params);
617
+ const v = buildAnomalyView(db, kind, start, end);
618
+ const metrics = nums({ findingCount: v.findingCount, days: v.diagnosis.days, degraded: v.diagnosis.degraded ? 1 : 0 });
619
+ return { data: { metrics }, html: renderAnomalyHtml(v) };
620
+ }
621
+ case 'calorie.view.contraindication': {
622
+ const part = optStr(params, 'part') ?? 'all';
623
+ const v = buildContraView(db, part);
624
+ const metrics = nums({ scannedSessions: v.scannedSessions, scannedMovements: v.scannedMovements, errorCount: v.errorCount, warnCount: v.warnCount, infoCount: v.infoCount });
625
+ return { data: { metrics }, html: renderContraHtml(v) };
626
+ }
627
+ case 'calorie.view.dedupe': {
628
+ const v = buildDedupeView(db);
629
+ const metrics = nums({ groupCount: v.groupCount, rowCount: v.rowCount, totalProducts: v.totalProducts });
630
+ return { data: { metrics }, html: renderDedupeHtml(v) };
631
+ }
632
+ case 'calorie.view.profile': {
633
+ const v = buildProfileView(db);
634
+ const metrics = nums({
635
+ age: v.profile.age, heightCm: v.profile.height_cm,
636
+ hasGoal: v.hasGoal ? 1 : 0, latestWeightKg: v.latestWeightKg,
637
+ calorieGoal: v.nutrition?.calorie_goal,
638
+ });
639
+ return { data: { metrics }, html: renderProfileHtml(v) };
640
+ }
641
+ default:
642
+ fail(3, '未知 calorie key:' + key);
643
+ throw new Error('unreachable');
644
+ }
645
+ }
646
+ function parseReadArgs(a) {
647
+ return parseArgs(a);
648
+ }
649
+ async function main() {
650
+ const o = parseReadArgs(process.argv.slice(2));
651
+ if (!o.key)
652
+ fail(2, '用法:calorie-cmd-read <calorie.key> [--params JSON对象] [--html 输出路径] [--timeout 毫秒]');
653
+ const dbPath = preflight();
654
+ let params = {};
655
+ if (o.params !== undefined) {
656
+ try {
657
+ params = JSON.parse(o.params);
658
+ }
659
+ catch (e) {
660
+ fail(2, '--params 须为 JSON:' + e.message);
661
+ }
662
+ if (typeof params !== 'object' || params === null || Array.isArray(params))
663
+ fail(2, '--params 须为 JSON 对象');
664
+ }
665
+ let shape;
666
+ try {
667
+ shape = calorieShapeFor(o.key);
668
+ }
669
+ catch (e) {
670
+ fail(3, e.message);
671
+ throw new Error('unreachable');
672
+ }
673
+ void CALORIE_COMBOS[o.key];
674
+ const timer = setTimeout(() => {
675
+ toast('cmd_read 超时 terminate(' + String(o.timeout) + 'ms),已终止取数');
676
+ process.exit(4);
677
+ }, o.timeout);
678
+ timer.unref();
679
+ let env = null;
680
+ try {
681
+ const db = openDb(join(dbPath, DB_FILENAME));
682
+ try {
683
+ // #40 · 唯一出口:写键走 write.ts 分发(memo.create/update/remove 范式),读键走既有 dispatch。
684
+ const out = isCalorieWriteKey(o.key)
685
+ ? dispatchWrite(o.key, params, db)
686
+ : dispatch(o.key, params, db);
687
+ env = buildEnvelope(o.key, shape, out.data);
688
+ if (o.html) {
689
+ try {
690
+ writeFileSync(o.html, out.html, 'utf8');
691
+ }
692
+ catch (e) {
693
+ fail(5, 'HTML 写盘失败:' + String(o.html) + '(' + e.message + ')');
694
+ }
695
+ }
696
+ }
697
+ finally {
698
+ db.close();
699
+ }
700
+ }
701
+ catch (e) {
702
+ if (e instanceof CalorieRenderError) {
703
+ if (e.code === 'missing-data')
704
+ fail(4, '取数失败(缺失阻断):' + e.message);
705
+ fail(e.code === 'bad-input' ? 2 : 5, (e.code === 'bad-input' ? '参数失败:' : '渲染失败:') + e.message);
706
+ }
707
+ if (e instanceof FetchError)
708
+ fail(4, '取数失败:' + e.message);
709
+ fail(4, '未知失败:' + (e.message || String(e)));
710
+ }
711
+ finally {
712
+ clearTimeout(timer);
713
+ }
714
+ process.stdout.write(JSON.stringify(env) + '\n');
715
+ }
716
+ const invokedAsCli = (process.argv[1] ?? '').replace(/\\/g, '/').endsWith('cmd_read.js');
717
+ if (invokedAsCli)
718
+ await main();
719
+ //# sourceMappingURL=cmd_read.js.map