skill-calorie 0.1.1 → 0.2.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 (144) hide show
  1. package/SKILL.md +207 -0
  2. package/dist/analysis/anomaly/common.d.ts.map +1 -1
  3. package/dist/analysis/anomaly/common.js +2 -1
  4. package/dist/analysis/anomaly/common.js.map +1 -1
  5. package/dist/analysis/cross.d.ts.map +1 -1
  6. package/dist/analysis/cross.js +3 -2
  7. package/dist/analysis/cross.js.map +1 -1
  8. package/dist/analysis/deficit.d.ts.map +1 -1
  9. package/dist/analysis/deficit.js +4 -0
  10. package/dist/analysis/deficit.js.map +1 -1
  11. package/dist/analysis/diet.js +2 -2
  12. package/dist/analysis/diet.js.map +1 -1
  13. package/dist/analysis/exercise.js +5 -5
  14. package/dist/analysis/exercise.js.map +1 -1
  15. package/dist/analysis/review.js +2 -2
  16. package/dist/analysis/review.js.map +1 -1
  17. package/dist/analysis/series.d.ts +7 -1
  18. package/dist/analysis/series.d.ts.map +1 -1
  19. package/dist/analysis/series.js +22 -6
  20. package/dist/analysis/series.js.map +1 -1
  21. package/dist/analysis/utils.d.ts +7 -0
  22. package/dist/analysis/utils.d.ts.map +1 -1
  23. package/dist/analysis/utils.js +7 -0
  24. package/dist/analysis/utils.js.map +1 -1
  25. package/dist/analysis/weightCompare3.js +2 -2
  26. package/dist/analysis/weightCompare3.js.map +1 -1
  27. package/dist/cli/cmd_read.d.ts +21 -3
  28. package/dist/cli/cmd_read.d.ts.map +1 -1
  29. package/dist/cli/cmd_read.js +480 -53
  30. package/dist/cli/cmd_read.js.map +1 -1
  31. package/dist/cli/keys.d.ts +89 -1
  32. package/dist/cli/keys.d.ts.map +1 -1
  33. package/dist/cli/keys.js +30 -0
  34. package/dist/cli/keys.js.map +1 -1
  35. package/dist/cli/write.d.ts +19 -1
  36. package/dist/cli/write.d.ts.map +1 -1
  37. package/dist/cli/write.js +148 -52
  38. package/dist/cli/write.js.map +1 -1
  39. package/dist/db/readonly.d.ts +3 -0
  40. package/dist/db/readonly.d.ts.map +1 -0
  41. package/dist/db/readonly.js +22 -0
  42. package/dist/db/readonly.js.map +1 -0
  43. package/dist/output.d.ts +82 -0
  44. package/dist/output.d.ts.map +1 -0
  45. package/dist/output.js +305 -0
  46. package/dist/output.js.map +1 -0
  47. package/dist/render/copy.d.ts +20 -0
  48. package/dist/render/copy.d.ts.map +1 -0
  49. package/dist/render/copy.js +36 -0
  50. package/dist/render/copy.js.map +1 -0
  51. package/dist/render/dietDocs.d.ts +45 -0
  52. package/dist/render/dietDocs.d.ts.map +1 -0
  53. package/dist/render/dietDocs.js +561 -0
  54. package/dist/render/dietDocs.js.map +1 -0
  55. package/dist/render/envelope.d.ts +32 -6
  56. package/dist/render/envelope.d.ts.map +1 -1
  57. package/dist/render/envelope.js +71 -0
  58. package/dist/render/envelope.js.map +1 -1
  59. package/dist/render/exercisePort.d.ts +161 -0
  60. package/dist/render/exercisePort.d.ts.map +1 -0
  61. package/dist/render/exercisePort.js +395 -0
  62. package/dist/render/exercisePort.js.map +1 -0
  63. package/dist/render/goal.d.ts.map +1 -1
  64. package/dist/render/goal.js +13 -2
  65. package/dist/render/goal.js.map +1 -1
  66. package/dist/render/goalPlate.d.ts +1 -1
  67. package/dist/render/goalPlate.d.ts.map +1 -1
  68. package/dist/render/goalPlate.js +5 -1
  69. package/dist/render/goalPlate.js.map +1 -1
  70. package/dist/render/helpCenter.d.ts +104 -0
  71. package/dist/render/helpCenter.d.ts.map +1 -0
  72. package/dist/render/helpCenter.js +386 -0
  73. package/dist/render/helpCenter.js.map +1 -0
  74. package/dist/render/html.d.ts +12 -0
  75. package/dist/render/html.d.ts.map +1 -1
  76. package/dist/render/html.js +21 -5
  77. package/dist/render/html.js.map +1 -1
  78. package/dist/render/index.d.ts +8 -1
  79. package/dist/render/index.d.ts.map +1 -1
  80. package/dist/render/index.js +5 -1
  81. package/dist/render/index.js.map +1 -1
  82. package/dist/render/nutritionPort.d.ts +89 -0
  83. package/dist/render/nutritionPort.d.ts.map +1 -0
  84. package/dist/render/nutritionPort.js +183 -0
  85. package/dist/render/nutritionPort.js.map +1 -0
  86. package/dist/render/nutritionPortDocs.d.ts +6 -0
  87. package/dist/render/nutritionPortDocs.d.ts.map +1 -0
  88. package/dist/render/nutritionPortDocs.js +317 -0
  89. package/dist/render/nutritionPortDocs.js.map +1 -0
  90. package/dist/render/photo.d.ts +6 -1
  91. package/dist/render/photo.d.ts.map +1 -1
  92. package/dist/render/photo.js +13 -4
  93. package/dist/render/photo.js.map +1 -1
  94. package/dist/render/planPlate.d.ts +2 -1
  95. package/dist/render/planPlate.d.ts.map +1 -1
  96. package/dist/render/planPlate.js +6 -1
  97. package/dist/render/planPlate.js.map +1 -1
  98. package/dist/render/receipt.d.ts +47 -1
  99. package/dist/render/receipt.d.ts.map +1 -1
  100. package/dist/render/receipt.js +67 -3
  101. package/dist/render/receipt.js.map +1 -1
  102. package/dist/render/sportDocs.d.ts +14 -0
  103. package/dist/render/sportDocs.d.ts.map +1 -0
  104. package/dist/render/sportDocs.js +540 -0
  105. package/dist/render/sportDocs.js.map +1 -0
  106. package/dist/render/sportPortDocs.d.ts +8 -0
  107. package/dist/render/sportPortDocs.d.ts.map +1 -0
  108. package/dist/render/sportPortDocs.js +468 -0
  109. package/dist/render/sportPortDocs.js.map +1 -0
  110. package/dist/render/templates.d.ts +4 -0
  111. package/dist/render/templates.d.ts.map +1 -0
  112. package/dist/render/templates.js +29 -0
  113. package/dist/render/templates.js.map +1 -0
  114. package/dist/render/trendDocs.d.ts +11 -0
  115. package/dist/render/trendDocs.d.ts.map +1 -0
  116. package/dist/render/trendDocs.js +445 -0
  117. package/dist/render/trendDocs.js.map +1 -0
  118. package/dist/render/trendMiscPort.d.ts +127 -0
  119. package/dist/render/trendMiscPort.d.ts.map +1 -0
  120. package/dist/render/trendMiscPort.js +277 -0
  121. package/dist/render/trendMiscPort.js.map +1 -0
  122. package/dist/render/trendMiscPortDocs.d.ts +10 -0
  123. package/dist/render/trendMiscPortDocs.d.ts.map +1 -0
  124. package/dist/render/trendMiscPortDocs.js +397 -0
  125. package/dist/render/trendMiscPortDocs.js.map +1 -0
  126. package/dist/render/wizardPort.d.ts +125 -0
  127. package/dist/render/wizardPort.d.ts.map +1 -0
  128. package/dist/render/wizardPort.js +393 -0
  129. package/dist/render/wizardPort.js.map +1 -0
  130. package/dist/render/wizardPortDocs.d.ts +6 -0
  131. package/dist/render/wizardPortDocs.d.ts.map +1 -0
  132. package/dist/render/wizardPortDocs.js +274 -0
  133. package/dist/render/wizardPortDocs.js.map +1 -0
  134. package/dist/triggers/routing.d.ts +141 -0
  135. package/dist/triggers/routing.d.ts.map +1 -0
  136. package/dist/triggers/routing.js +678 -0
  137. package/dist/triggers/routing.js.map +1 -0
  138. package/package.json +5 -3
  139. package/templates/diet.html +40 -0
  140. package/templates/exercise.html +40 -0
  141. package/templates/goal.html +40 -0
  142. package/templates/help.html +40 -0
  143. package/templates/home.html +40 -0
  144. package/templates/photo-gallery.html +40 -0
package/dist/cli/write.js CHANGED
@@ -9,7 +9,7 @@ import { getNutritionGoal, setNutritionGoal, updateWaterGoal } from '../fetch/nu
9
9
  import { pauseAllGoals, resumeAllGoals, setWeightGoal } from '../fetch/goal.js';
10
10
  import { CALIPER_FIELDS, ValidationError, addComposition, addMeasurement, deleteComposition, deleteMeasurement, } from '../fetch/body.js';
11
11
  import { SOURCE_CHOICES, SOURCE_LABELS } from '../kcal.js';
12
- import { buildCrudReceipt } from '../render/receipt.js';
12
+ import { buildCrudReceipt, withM5 } from '../render/receipt.js';
13
13
  import { CalorieRenderError } from '../render/errors.js';
14
14
  import { shiftISODate, todayISO } from '../analysis/utils.js';
15
15
  import { isCalorieWriteKey } from './keys.js';
@@ -72,6 +72,68 @@ function assertISO(v, field) {
72
72
  function esc(s) {
73
73
  return String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
74
74
  }
75
+ /** #101 · 删除口径单一来源(prose 词条 + `items[].status` 状态串同源派生)。
76
+ * `recoverable` 恒 false:全仓 77 键 0 个 restore/undo/recover 入口,故不得出现「可恢复」承诺。 */
77
+ const SOFT_EXCLUDED_INNER = '软删除:行保留,已从查询与统计中排除;暂无恢复入口';
78
+ const SOFT_EXCLUDED = '(' + SOFT_EXCLUDED_INNER + ')';
79
+ const HARD_INNER = '硬删除,不可恢复';
80
+ const HARD_WORDING = '(' + HARD_INNER + ')';
81
+ /** 状态串与 prose 同源:kind 决定「软/硬」,两者一律带「不可恢复」。 */
82
+ function deleteStatus(kind, base = '已删除') {
83
+ return base + (kind === 'soft' ? '(软,不可恢复)' : '(硬,不可恢复)');
84
+ }
85
+ /* ------------------------------------------------ #97 · M5 回执契约(追加字段,正本 t97-m5-contract.md) */
86
+ /** 影响行数来源:SQLite `total_changes()` 在本键写库前后的增量。
87
+ * 真实库行数(INSERT/UPDATE/DELETE/软删标记一律计;`INSERT OR REPLACE` 命中已有行=1,
88
+ * `INSERT OR IGNORE` 命中已有行=0——实测 node:sqlite v24),非自报。 */
89
+ function totalChanges(db) {
90
+ const row = db.prepare('SELECT total_changes() AS n').get();
91
+ return row && typeof row.n === 'number' ? Number(row.n) : 0;
92
+ }
93
+ /** 写入字段摘要(CLI 参数名口径):create 键=该键写入字段全集(缺省值也算写入);
94
+ * update 键=本次实际变更字段;delete 键=空数组(无写入字段,旧版同样只印 id/日期/影响)。 */
95
+ const F = {
96
+ diet: ['foodName', 'calories', 'protein', 'carbs', 'fat', 'grams', 'note', 'date', 'time'],
97
+ water: ['ml', 'note', 'date', 'time'],
98
+ weight: ['kg', 'note', 'date', 'time'],
99
+ weightBatch: ['kg', 'date'],
100
+ exercise: ['type', 'calories', 'minutes', 'date', 'time', 'note', 'reps', 'category', 'difficulty', 'distance', 'heartRate', 'maxHeartRate', 'steps', 'setIndex', 'loadKg', 'backfill'],
101
+ photo: ['srcPaths', 'tag', 'note', 'date', 'time'],
102
+ product: ['productName', 'brand', 'calories', 'protein', 'fat', 'saturatedFat', 'carbohydrates', 'sugar', 'dietaryFiber', 'sodium', 'note'],
103
+ };
104
+ /** `calorie.goal.set` 本次**实际被 SET 的列** → CLI 参数名(正本 §3.4「update 键=本次实际变更字段」)。
105
+ * 与 `fetch/nutritionGoal.ts:68-78` 的两条 SQL 同源:传 `water` 走 6 列 `INSERT OR REPLACE`
106
+ * (含 `water_goal`),不传则 SQL 里**没有** `water_goal` 列——该列的值只因 REPLACE 落列默认
107
+ * (`schema.ts:32` `water_goal INTEGER DEFAULT 2000`),**不属本次 SET 的字段**,故不得报 `water`。
108
+ * 记账列 `updated_at` 无 CLI 参数,按 §3.4 不计入摘要。
109
+ * 注意:`INSERT OR REPLACE` 连带把 `weight_goal`/`goal_deadline`/`goal_paused`/`start_weight`/
110
+ * `start_date` 重置为默认/NULL——那是**实际行为缺陷**(红队 D-4),已转 #127;本票只让回执如实
111
+ * 反映「本次 SET 了哪些列」,**不改写语义**(不做 UPSERT 化)。 */
112
+ const goalSetWrittenFields = (hasWater) => ['calorie', 'protein', 'carbs', 'fat', ...(hasWater ? ['water'] : [])];
113
+ /** 库列名 → CLI 参数名(update 键按实际变更列回报写入字段摘要)。 */
114
+ const COL_CLI = {
115
+ food_name: 'foodName', grams: 'grams', calories: 'calories', protein: 'protein', carbs: 'carbs',
116
+ fat: 'fat', note: 'note', date: 'date', time: 'time',
117
+ exercise_type: 'type', calories_burned: 'calories', duration_minutes: 'minutes', category: 'category',
118
+ difficulty: 'difficulty', distance_km: 'distance', avg_heart_rate: 'heartRate',
119
+ max_heart_rate: 'maxHeartRate', steps: 'steps', reps: 'reps', load_kg: 'loadKg',
120
+ set_index: 'setIndex', is_backfill: 'backfill',
121
+ product_name: 'productName', brand: 'brand', saturated_fat: 'saturatedFat', carbohydrates: 'carbohydrates',
122
+ sugar: 'sugar', dietary_fiber: 'dietaryFiber', sodium: 'sodium',
123
+ };
124
+ const cliNames = (cols) => cols.map((c) => COL_CLI[c] ?? c);
125
+ /** 只取「实际提供」的参数名(写入字段摘要;`undefined` 不算写入)。 */
126
+ function provided(params, names) {
127
+ return names.filter((n) => params[n] !== undefined);
128
+ }
129
+ /** `input` 里实际有值的键(体脂/围度等动态字段表)。 */
130
+ function definedKeys(input) {
131
+ return Object.keys(input).filter((k) => input[k] !== undefined);
132
+ }
133
+ /** 围度库列名 → CLI 参数名(`MEASURE_CAMEL` 反向;写入字段摘要统一走 CLI 名口径)。 */
134
+ function measureCliNames(keys) {
135
+ return keys.map((k) => Object.keys(MEASURE_CAMEL).find((c) => MEASURE_CAMEL[c] === k) ?? k);
136
+ }
75
137
  /** C6 #43 · 写收据 HTML 结构化分项:摘要 + 操作元 + items 逐条(id/日期/状态/原因/明细)。 */
76
138
  function receiptHtml(scene, summary, op, recordId, items) {
77
139
  const list = (items ?? []).length > 0
@@ -166,12 +228,17 @@ function oneExercise(item, i) {
166
228
  isBackfill: (g('backfill') ?? g('isBackfill')) === true,
167
229
  };
168
230
  }
169
- /** 写分发(唯一出口 cmd_read 内调用;未知键上游已拦,此处再拦一道)。 */
231
+ /** 写分发(唯一出口 cmd_read 内调用;未知键上游已拦,此处再拦一道)。
232
+ * #97 · M5:`affectedRows` 在此统一注入——写库前后各取一次 SQLite `total_changes()`,
233
+ * 增量即「影响 N 行」(35 键单一来源,逐键不各自自报)。 */
170
234
  export function dispatchWrite(key, params, db) {
171
235
  if (!isCalorieWriteKey(key))
172
236
  fail(3, '未知 calorie 写键:' + key);
237
+ const before = totalChanges(db);
173
238
  try {
174
- return dispatchInner(key, params, db);
239
+ const res = dispatchInner(key, params, db);
240
+ const receipt = withM5(res.data.receipt, { affectedRows: totalChanges(db) - before });
241
+ return { data: { ...res.data, receipt }, html: res.html };
175
242
  }
176
243
  catch (e) {
177
244
  if (e instanceof ValidationError)
@@ -214,11 +281,13 @@ function dispatchInner(key, params, db) {
214
281
  if (r.duplicate) {
215
282
  return out(R('记一餐', 'create', String(r.message ?? '重复记录已跳过'), '记一餐', 'food_log (写库回执)', {
216
283
  recordId: r.dupId ?? null, noChange: true,
284
+ ids: r.dupId ? [r.dupId] : [], idSource: r.dupId ? 'record' : 'none', writtenFields: [],
217
285
  }));
218
286
  }
219
287
  const remain = r.remainingCal === null || r.remainingCal === undefined ? '' : ' · 今日剩 ' + r.remainingCal + ' 卡';
220
288
  return out(R('记一餐', 'create', '已记一餐:' + r.food_name + ' ' + r.date + ' ' + r.time + '(' + r.meal + ')' + remain, '记一餐', 'food_log (写库回执)', {
221
- recordId: r.id, items: [{ id: r.id ?? undefined, date: r.date, status: '成功', reason: '', detail: r.food_name }],
289
+ recordId: r.id, ids: r.id === null ? [] : [r.id], writtenFields: [...F.diet],
290
+ items: [{ id: r.id ?? undefined, date: r.date, status: '成功', reason: '', detail: r.food_name }],
222
291
  }));
223
292
  }
224
293
  case 'calorie.diet.update': {
@@ -243,15 +312,15 @@ function dispatchInner(key, params, db) {
243
312
  assertISO(fields['date'], 'date');
244
313
  const r = updateMeal(db, id, fields);
245
314
  return out(R('改饮食记录', 'update', '已更新饮食 #' + id + '(' + (r.changed.length ? r.changed.join('、') : '无实际变化') + ')', '改饮食记录', 'food_log (写库回执)', {
246
- recordId: id, noChange: r.changed.length === 0,
315
+ recordId: id, ids: [id], writtenFields: cliNames(Object.keys(fields)), noChange: r.changed.length === 0,
247
316
  items: [{ id, status: '已更新', reason: '', detail: r.changed.join(',') || '无变化' }],
248
317
  }));
249
318
  }
250
319
  case 'calorie.diet.remove': {
251
320
  const id = needId(params);
252
321
  const r = deleteMeal(db, id);
253
- return out(R('删饮食记录', 'delete', '已删除饮食 #' + id + '(' + r.food_name + ' ' + r.calories + ' 卡)', '删饮食记录', 'food_log (写库回执)', {
254
- recordId: id, items: [{ id, status: '已删除', reason: '', detail: r.food_name }],
322
+ return out(R('删饮食记录', 'delete', '已删除饮食 #' + id + '(' + r.food_name + ' ' + r.calories + ' 卡 · ' + HARD_INNER + ')', '删饮食记录', 'food_log (写库回执)', {
323
+ recordId: id, ids: [id], writtenFields: [], items: [{ id, status: deleteStatus('hard'), reason: '', detail: r.food_name }],
255
324
  }));
256
325
  }
257
326
  case 'calorie.diet.batch': {
@@ -271,7 +340,8 @@ function dispatchInner(key, params, db) {
271
340
  };
272
341
  }));
273
342
  return out(R('批量补记饮食', 'create', '批量记饮食:新增 ' + r.added + ',跳过 ' + r.skipped + ',失败 ' + r.failed, '批量补记饮食', 'food_log (写库回执)', {
274
- noChange: r.added === 0,
343
+ noChange: r.added === 0, ids: [], idSource: 'condition',
344
+ writtenFields: r.added > 0 ? [...F.diet] : [],
275
345
  items: r.failures.slice(0, 20).map(([idx, reason]) => ({ status: '失败', reason, detail: '第' + idx + '条' })),
276
346
  }));
277
347
  }
@@ -285,7 +355,8 @@ function dispatchInner(key, params, db) {
285
355
  if (r.copied + r.skipped === 0)
286
356
  throw new CalorieRenderError('missing-data', '来源无饮食记录(' + from + ')');
287
357
  return out(R('复制昨日饮食', 'create', '已复制饮食 ' + from + '→' + to + ':复制 ' + r.copied + ',跳过 ' + r.skipped, '复制昨日饮食', 'food_log (写库回执)', {
288
- noChange: r.copied === 0,
358
+ noChange: r.copied === 0, ids: [], idSource: 'condition',
359
+ writtenFields: r.copied > 0 ? [...F.diet] : [],
289
360
  }));
290
361
  }
291
362
  case 'calorie.diet.update-by-date': {
@@ -305,7 +376,9 @@ function dispatchInner(key, params, db) {
305
376
  const r = updateMealsByDate(db, date, fields);
306
377
  if (r.matched === 0)
307
378
  throw new CalorieRenderError('missing-data', '无饮食记录(' + date + ')');
308
- return out(R('改某日饮食', 'update', '已更新 ' + date + ' 饮食 ' + r.updated + ' 条(' + r.changedFields.join('、') + ')', '改某日饮食', 'food_log (写库回执)', {}));
379
+ return out(R('改某日饮食', 'update', '已更新 ' + date + ' 饮食 ' + r.updated + ' 条(' + r.changedFields.join('、') + ')', '改某日饮食', 'food_log (写库回执)', {
380
+ ids: [], idSource: 'condition', writtenFields: cliNames(r.changedFields),
381
+ }));
309
382
  }
310
383
  case 'calorie.diet.remove-by-date': {
311
384
  const date = needStr(params, 'date');
@@ -313,7 +386,7 @@ function dispatchInner(key, params, db) {
313
386
  const r = deleteMealsByDate(db, date);
314
387
  if (r.deleted === 0)
315
388
  throw new CalorieRenderError('missing-data', '无饮食记录(' + date + ')');
316
- return out(R('删某日饮食', 'delete', '已删除 ' + date + ' 饮食 ' + r.deleted + ' 条', '删某日饮食', 'food_log (写库回执)', {}));
389
+ return out(R('删某日饮食', 'delete', '已删除 ' + date + ' 饮食 ' + r.deleted + ' 条' + HARD_WORDING, '删某日饮食', 'food_log (写库回执)', { ids: [], idSource: 'condition', writtenFields: [] }));
317
390
  }
318
391
  case 'calorie.diet.remove-by-range': {
319
392
  const start = needStr(params, 'start');
@@ -325,7 +398,7 @@ function dispatchInner(key, params, db) {
325
398
  const r = deleteMealsByRange(db, start, end);
326
399
  if (r.deleted === 0)
327
400
  throw new CalorieRenderError('missing-data', '无饮食记录(' + start + '~' + end + ')');
328
- return out(R('批量删饮食', 'delete', '已删除 ' + start + '~' + end + ' 饮食 ' + r.deleted + ' 条', '批量删饮食', 'food_log (写库回执)', {}));
401
+ return out(R('批量删饮食', 'delete', '已删除 ' + start + '~' + end + ' 饮食 ' + r.deleted + ' 条' + HARD_WORDING, '批量删饮食', 'food_log (写库回执)', { ids: [], idSource: 'condition', writtenFields: [] }));
329
402
  }
330
403
  case 'calorie.diet.remove-by-type': {
331
404
  const date = needStr(params, 'date');
@@ -336,7 +409,7 @@ function dispatchInner(key, params, db) {
336
409
  const r = deleteMealsByType(db, date, mealType);
337
410
  if (r.deleted === 0)
338
411
  throw new CalorieRenderError('missing-data', date + ' 无' + mealType + '记录');
339
- return out(R('删一餐', 'delete', '已删除 ' + date + ' ' + mealType + ' ' + r.deleted + ' 条', '删一餐', 'food_log (写库回执)', {}));
412
+ return out(R('删一餐', 'delete', '已删除 ' + date + ' ' + mealType + ' ' + r.deleted + ' 条' + HARD_WORDING, '删一餐', 'food_log (写库回执)', { ids: [], idSource: 'condition', writtenFields: [] }));
340
413
  }
341
414
  case 'calorie.water.log': {
342
415
  const ml = needNum(params, 'ml');
@@ -350,12 +423,17 @@ function dispatchInner(key, params, db) {
350
423
  note: optStr(params, 'note'), date, time: optStr(params, 'time'),
351
424
  });
352
425
  if (r.duplicate) {
353
- return out(R('记喝水', 'create', String(r.message ?? '重复记录已跳过'), '记喝水', 'food_log (写库回执)', { noChange: true }));
426
+ // 与 `calorie.diet.add` 同口径(§3.3 `record`):重复跳过的原 id 由 fetch 回传(`fetch/diet.ts:96`),
427
+ // 拿得到就报 `record`+`ids=[dupId]`,拿不到才退 `none`(返修 R-3/蓝队 D-3 统一)。
428
+ return out(R('记喝水', 'create', String(r.message ?? '重复记录已跳过'), '记喝水', 'food_log (写库回执)', {
429
+ recordId: r.dupId ?? null, noChange: true,
430
+ ids: r.dupId ? [r.dupId] : [], idSource: r.dupId ? 'record' : 'none', writtenFields: [],
431
+ }));
354
432
  }
355
433
  const day = date ?? todayISO();
356
434
  const sum = getDailySummary(db, day);
357
435
  return out(R('记喝水', 'create', '已记喝水 ' + ml + ' ml(' + day + ' 累计 ' + sum.waterMl + ' ml)', '记喝水', 'food_log (写库回执)', {
358
- recordId: r.id,
436
+ recordId: r.id, ids: r.id === null ? [] : [r.id], writtenFields: [...F.water],
359
437
  items: [{ id: r.id ?? undefined, date: r.date, status: '成功', reason: '', detail: ml + 'ml' }],
360
438
  }));
361
439
  }
@@ -369,7 +447,8 @@ function dispatchInner(key, params, db) {
369
447
  const r = logWeight(db, kg, optStr(params, 'note') ?? '', date, optStr(params, 'time'));
370
448
  const bmiText = r.bmi === null ? 'BMI 待补身高(补档案:calorie-cmd-read calorie.profile.set)' : 'BMI ' + r.bmi;
371
449
  return out(R('记体重', 'create', '已记体重 ' + r.kg + ' kg(' + bmiText + ' · ' + r.date + ' ' + r.time + ')', '记体重', 'weight_log (写库回执)', {
372
- recordId: r.id, items: [{ id: r.id, date: r.date, status: '成功', reason: '', detail: r.kg + 'kg' }],
450
+ recordId: r.id, ids: [r.id], writtenFields: [...F.weight],
451
+ items: [{ id: r.id, date: r.date, status: '成功', reason: '', detail: r.kg + 'kg' }],
373
452
  }));
374
453
  }
375
454
  case 'calorie.weight.update': {
@@ -385,13 +464,16 @@ function dispatchInner(key, params, db) {
385
464
  const r = updateWeight(db, id, kg, note);
386
465
  const bmiTextU = r.bmi === null ? 'BMI 待补身高(补档案:calorie-cmd-read calorie.profile.set)' : 'BMI ' + r.bmi;
387
466
  return out(R('改体重记录', 'update', '已更新体重 #' + id + ':' + r.oldWeight + '→' + r.newWeight + ' kg(' + bmiTextU + ')', '改体重记录', 'weight_log (写库回执)', {
388
- recordId: id, items: [{ id, status: '已更新', reason: '' }],
467
+ recordId: id, ids: [id], writtenFields: [...(kg !== undefined ? ['kg'] : []), ...(note !== undefined ? ['note'] : [])],
468
+ items: [{ id, status: '已更新', reason: '' }],
389
469
  }));
390
470
  }
391
471
  if (date !== undefined) {
392
472
  assertISO(date, 'date');
393
473
  const r = updateWeightByDate(db, date, kg, note);
394
- return out(R('改某日体重', 'update', '已更新 ' + date + ' 体重 ' + r.hitCount + ' 条', '改某日体重', 'weight_log (写库回执)', {}));
474
+ return out(R('改某日体重', 'update', '已更新 ' + date + ' 体重 ' + r.hitCount + ' 条', '改某日体重', 'weight_log (写库回执)', {
475
+ ids: [], idSource: 'condition', writtenFields: [...(kg !== undefined ? ['kg'] : []), ...(note !== undefined ? ['note'] : [])],
476
+ }));
395
477
  }
396
478
  fail(2, '缺参数 id 或 date(二选一)');
397
479
  throw new Error('unreachable');
@@ -405,14 +487,14 @@ function dispatchInner(key, params, db) {
405
487
  if (!Number.isInteger(id) || id <= 0)
406
488
  fail(2, 'id 须为正整数');
407
489
  const r = deleteWeight(db, id);
408
- return out(R('删体重记录', 'delete', '已删除体重 #' + id + '(' + r.date + ' ' + r.weight_kg + ' kg)', '删体重记录', 'weight_log (写库回执)', {
409
- recordId: id, items: [{ id, status: '已删除', reason: '' }],
490
+ return out(R('删体重记录', 'delete', '已删除体重 #' + id + '(' + r.date + ' ' + r.weight_kg + ' kg · ' + HARD_INNER + ')', '删体重记录', 'weight_log (写库回执)', {
491
+ recordId: id, ids: [id], writtenFields: [], items: [{ id, status: deleteStatus('hard'), reason: '' }],
410
492
  }));
411
493
  }
412
494
  if (date !== undefined) {
413
495
  assertISO(date, 'date');
414
496
  const r = deleteWeightByDate(db, date);
415
- return out(R('删某日体重', 'delete', '已删除 ' + date + ' 体重 ' + r.deletedCount + ' 条', '删某日体重', 'weight_log (写库回执)', {}));
497
+ return out(R('删某日体重', 'delete', '已删除 ' + date + ' 体重 ' + r.deletedCount + ' 条' + HARD_WORDING, '删某日体重', 'weight_log (写库回执)', { ids: [], idSource: 'condition', writtenFields: [] }));
416
498
  }
417
499
  if (start !== undefined || end !== undefined) {
418
500
  if (start === undefined || end === undefined)
@@ -422,7 +504,7 @@ function dispatchInner(key, params, db) {
422
504
  if (start > end)
423
505
  fail(2, 'start 不得晚于 end');
424
506
  const r = deleteWeightRange(db, start, end);
425
- return out(R('批量删体重', 'delete', '已删除 ' + start + '~' + end + ' 体重 ' + r.deletedCount + ' 条', '批量删体重', 'weight_log (写库回执)', {}));
507
+ return out(R('批量删体重', 'delete', '已删除 ' + start + '~' + end + ' 体重 ' + r.deletedCount + ' 条' + HARD_WORDING, '批量删体重', 'weight_log (写库回执)', { ids: [], idSource: 'condition', writtenFields: [] }));
426
508
  }
427
509
  fail(2, '缺参数 id/date/start+end(三选一)');
428
510
  throw new Error('unreachable');
@@ -436,7 +518,8 @@ function dispatchInner(key, params, db) {
436
518
  return { date: o['date'] === undefined ? undefined : String(o['date']), kg: o['kg'] };
437
519
  }));
438
520
  return out(R('批量补录体重', 'create', '批量记体重:写入 ' + r.wrote + ',跳过 ' + r.skipped + ',失败 ' + r.failed, '批量补录体重', 'weight_log (写库回执)', {
439
- noChange: r.wrote === 0,
521
+ noChange: r.wrote === 0, ids: [], idSource: 'condition',
522
+ writtenFields: r.wrote > 0 ? [...F.weightBatch] : [],
440
523
  items: r.items.filter((x) => x.status === '失败').slice(0, 20).map((x) => ({ status: '失败', reason: x.reason, detail: String(x.date) })),
441
524
  }));
442
525
  }
@@ -450,7 +533,8 @@ function dispatchInner(key, params, db) {
450
533
  if (r.copied + r.skipped === 0)
451
534
  throw new CalorieRenderError('missing-data', '昨日无运动记录可复制');
452
535
  return out(R('复制昨日运动', 'create', '已复制昨日运动→' + target + ':复制 ' + r.copied + ',跳过 ' + r.skipped, '复制昨日运动', 'exercise_log (写库回执)', {
453
- noChange: r.copied === 0,
536
+ noChange: r.copied === 0, ids: [], idSource: 'condition',
537
+ writtenFields: r.copied > 0 ? [...F.exercise] : [],
454
538
  }));
455
539
  }
456
540
  if (params['items'] !== undefined) {
@@ -459,13 +543,15 @@ function dispatchInner(key, params, db) {
459
543
  fail(2, 'items 至多 200 条');
460
544
  const r = batchAdd(db, items.map((e, i) => oneExercise((e ?? {}), '(第' + i + '条)')));
461
545
  return out(R('记运动', 'create', '批量记运动:新增 ' + r.added + ' 条', '批量补记运动', 'exercise_log (写库回执)', {
462
- recordId: r.ids[0] ?? null,
546
+ recordId: r.ids[0] ?? null, ids: r.ids, idSource: r.ids.length > 0 ? 'record' : 'condition',
547
+ writtenFields: r.added > 0 ? [...F.exercise] : [],
463
548
  }));
464
549
  }
465
550
  const input = oneExercise(params, '');
466
551
  const r = addRecord(db, input);
467
552
  return out(R('记运动', 'create', '已记运动:' + input.exerciseType + ' ' + input.caloriesBurned + ' 卡' + (input.minutes ? ' · ' + input.minutes + ' 分钟' : '') + '(' + input.date + ')', '记运动', 'exercise_log (写库回执)', {
468
- recordId: r.id, items: [{ id: r.id, date: input.date, status: '成功', reason: '', detail: input.exerciseType }],
553
+ recordId: r.id, ids: [r.id], writtenFields: [...F.exercise],
554
+ items: [{ id: r.id, date: input.date, status: '成功', reason: '', detail: input.exerciseType }],
469
555
  }));
470
556
  }
471
557
  case 'calorie.exercise.update': {
@@ -503,7 +589,8 @@ function dispatchInner(key, params, db) {
503
589
  fail(2, 'id 须为正整数');
504
590
  updateRecord(db, id, fields);
505
591
  return out(R('改运动记录', 'update', '已更新运动 #' + id + '(' + Object.keys(fields).join('、') + ')', '改运动记录', 'exercise_log (写库回执)', {
506
- recordId: id, items: [{ id, status: '已更新', reason: '' }],
592
+ recordId: id, ids: [id], writtenFields: cliNames(Object.keys(fields)),
593
+ items: [{ id, status: '已更新', reason: '' }],
507
594
  }));
508
595
  }
509
596
  if (date !== undefined) {
@@ -511,7 +598,9 @@ function dispatchInner(key, params, db) {
511
598
  const r = updateDay(db, date, fields);
512
599
  if (r.matched === 0)
513
600
  throw new CalorieRenderError('missing-data', '无运动记录(' + date + ')');
514
- return out(R('改某日运动', 'update', '已更新 ' + date + ' 运动 ' + r.matched + ' 条', '改某日运动', 'exercise_log (写库回执)', {}));
601
+ return out(R('改某日运动', 'update', '已更新 ' + date + ' 运动 ' + r.matched + ' 条', '改某日运动', 'exercise_log (写库回执)', {
602
+ ids: [], idSource: 'condition', writtenFields: cliNames(Object.keys(fields)),
603
+ }));
515
604
  }
516
605
  fail(2, '缺参数 id 或 date(二选一)');
517
606
  throw new Error('unreachable');
@@ -525,8 +614,8 @@ function dispatchInner(key, params, db) {
525
614
  if (!Number.isInteger(id) || id <= 0)
526
615
  fail(2, 'id 须为正整数');
527
616
  deleteRecord(db, id);
528
- return out(R('删运动记录', 'delete', '已删除运动 #' + id, '删运动记录', 'exercise_log (写库回执)', {
529
- recordId: id, items: [{ id, status: '已删除', reason: '' }],
617
+ return out(R('删运动记录', 'delete', '已删除运动 #' + id + SOFT_EXCLUDED, '删运动记录', 'exercise_log (写库回执)', {
618
+ recordId: id, ids: [id], writtenFields: ['is_deleted'], items: [{ id, status: deleteStatus('soft'), reason: '' }],
530
619
  }));
531
620
  }
532
621
  if (date !== undefined) {
@@ -534,7 +623,7 @@ function dispatchInner(key, params, db) {
534
623
  const n = deleteDay(db, date);
535
624
  if (n === 0)
536
625
  throw new CalorieRenderError('missing-data', '无运动记录(' + date + ')');
537
- return out(R('删某日运动', 'delete', '已删除 ' + date + ' 运动 ' + n + ' 条', '删某日运动', 'exercise_log (写库回执)', {}));
626
+ return out(R('删某日运动', 'delete', '已删除 ' + date + ' 运动 ' + n + ' 条' + SOFT_EXCLUDED, '删某日运动', 'exercise_log (写库回执)', { ids: [], idSource: 'condition', writtenFields: ['is_deleted'] }));
538
627
  }
539
628
  if (from !== undefined || to !== undefined) {
540
629
  if (from === undefined || to === undefined)
@@ -546,7 +635,7 @@ function dispatchInner(key, params, db) {
546
635
  const n = deleteRange(db, from, to);
547
636
  if (n === 0)
548
637
  throw new CalorieRenderError('missing-data', '无运动记录(' + from + '~' + to + ')');
549
- return out(R('批量删运动', 'delete', '已删除 ' + from + '~' + to + ' 运动 ' + n + ' 条', '批量删运动', 'exercise_log (写库回执)', {}));
638
+ return out(R('批量删运动', 'delete', '已删除 ' + from + '~' + to + ' 运动 ' + n + ' 条' + SOFT_EXCLUDED, '批量删运动', 'exercise_log (写库回执)', { ids: [], idSource: 'condition', writtenFields: ['is_deleted'] }));
550
639
  }
551
640
  fail(2, '缺参数 id/date/from+to(三选一)');
552
641
  throw new Error('unreachable');
@@ -573,7 +662,9 @@ function dispatchInner(key, params, db) {
573
662
  catch {
574
663
  distance = null;
575
664
  }
576
- const receipt = buildAddReceipt(added, { tag, note: optStr(params, 'note'), distance, failedCount: srcPaths.length - added.length || undefined });
665
+ const receipt = withM5(buildAddReceipt(added, { tag, note: optStr(params, 'note'), distance, failedCount: srcPaths.length - added.length || undefined }), {
666
+ ids: added.map((a) => a.id), writtenFields: [...F.photo],
667
+ });
577
668
  return { data: { ok: true, message: receipt.summary, receipt }, html: receiptHtml(receipt.scene, receipt.summary, receipt.op, receipt.recordId, receipt.items) };
578
669
  }
579
670
  case 'calorie.photo.remove': {
@@ -583,7 +674,7 @@ function dispatchInner(key, params, db) {
583
674
  if (!snap)
584
675
  throw new CalorieRenderError('missing-data', '身材照 #' + id + ' 不存在');
585
676
  deletePhoto(db, dir, id);
586
- const receipt = buildDeleteReceipt(snap);
677
+ const receipt = withM5(buildDeleteReceipt(snap), { ids: [id], writtenFields: [] });
587
678
  return { data: { ok: true, message: receipt.summary, receipt }, html: receiptHtml(receipt.scene, receipt.summary, receipt.op, receipt.recordId, receipt.items) };
588
679
  }
589
680
  case 'calorie.photo.tag': {
@@ -617,7 +708,7 @@ function dispatchInner(key, params, db) {
617
708
  scene = '删照片标签';
618
709
  }
619
710
  const after = getPhotoRow(db, id)?.tag_list ?? before;
620
- const receipt = buildTagReceipt(id, before, after, scene);
711
+ const receipt = withM5(buildTagReceipt(id, before, after, scene), { ids: [id], writtenFields: op === 'set' ? ['tags'] : ['tag'] });
621
712
  return { data: { ok: true, message: receipt.summary, receipt }, html: receiptHtml(receipt.scene, receipt.summary, receipt.op, receipt.recordId, receipt.items) };
622
713
  }
623
714
  case 'calorie.product.add': {
@@ -632,7 +723,8 @@ function dispatchInner(key, params, db) {
632
723
  sodium: needNum(params, 'sodium'), note: optStr(params, 'note'),
633
724
  });
634
725
  return out(R('存食品', 'create', '已存食品 #' + r.id + '(' + productName.trim() + ')', '存食品', 'nutrition_products (写库回执)', {
635
- recordId: r.id, items: [{ id: r.id, status: '成功', reason: '', detail: productName.trim() }],
726
+ recordId: r.id, ids: [r.id], writtenFields: [...F.product],
727
+ items: [{ id: r.id, status: '成功', reason: '', detail: productName.trim() }],
636
728
  }));
637
729
  }
638
730
  case 'calorie.product.update': {
@@ -657,7 +749,8 @@ function dispatchInner(key, params, db) {
657
749
  if (!r.updated)
658
750
  throw new CalorieRenderError('missing-data', '食品 #' + id + ' 不存在');
659
751
  return out(R('改食品', 'update', '已更新食品 #' + id + '(' + Object.keys(fields).join('、') + ')', '改食品', 'nutrition_products (写库回执)', {
660
- recordId: id, items: [{ id, status: '已更新', reason: '' }],
752
+ recordId: id, ids: [id], writtenFields: cliNames(Object.keys(fields)),
753
+ items: [{ id, status: '已更新', reason: '' }],
661
754
  }));
662
755
  }
663
756
  case 'calorie.product.deprecate': {
@@ -668,8 +761,8 @@ function dispatchInner(key, params, db) {
668
761
  throw new CalorieRenderError('missing-data', '食品 #' + id + ' 不存在');
669
762
  fail(2, String(r.error ?? '废弃失败'));
670
763
  }
671
- return out(R('下架食品', 'update', '已下架食品 #' + id + '(' + (r.name ?? '') + ',行保留可恢复)', '下架食品', 'nutrition_products (写库回执)', {
672
- recordId: id, items: [{ id, status: '已下架', reason: '' }],
764
+ return out(R('下架食品', 'update', '已下架食品 #' + id + '(' + (r.name ?? '') + ' · ' + SOFT_EXCLUDED_INNER + ')', '下架食品', 'nutrition_products (写库回执)', {
765
+ recordId: id, ids: [id], writtenFields: ['is_deprecated'], items: [{ id, status: deleteStatus('soft', '已下架'), reason: '' }],
673
766
  }));
674
767
  }
675
768
  case 'calorie.profile.set':
@@ -702,7 +795,7 @@ function dispatchInner(key, params, db) {
702
795
  const r = updateProfile(db, picked);
703
796
  const after = r.after;
704
797
  return out(R(scene, 'update', '已' + scene + '(身高 ' + (after.height_cm ?? '—') + ' · 年龄 ' + (after.age ?? '—') + ' · 活动量 ' + (after.activity_level ?? '—') + ')', wake, 'user_profile (写库回执)', {
705
- recordId: 1, noChange: r.changed.length === 0,
798
+ recordId: 1, ids: [1], idSource: 'singleton', writtenFields: Object.keys(picked), noChange: r.changed.length === 0,
706
799
  }));
707
800
  }
708
801
  case 'calorie.profile.activity': {
@@ -711,7 +804,7 @@ function dispatchInner(key, params, db) {
711
804
  fail(2, '缺参数 activityLevel');
712
805
  const r = setActivityLevel(db, level);
713
806
  return out(R('设活动量', 'update', '已设活动量:' + (r.before ?? '—') + '→' + r.after, '设活动量', 'user_profile (写库回执)', {
714
- recordId: 1, noChange: r.before === r.after,
807
+ recordId: 1, ids: [1], idSource: 'singleton', writtenFields: ['activityLevel'], noChange: r.before === r.after,
715
808
  }));
716
809
  }
717
810
  case 'calorie.goal.set': {
@@ -732,7 +825,7 @@ function dispatchInner(key, params, db) {
732
825
  const r = setNutritionGoal(db, { calorie, protein, carbs, fat, water });
733
826
  const tail = r.consistent ? ' · 宏量自洽' : ' · ⚠宏量换算差 ' + r.diffKcal + ' 卡(>50 建议复核)';
734
827
  return out(R('定营养目标', had ? 'update' : 'create', '已定营养目标:' + r.calorieGoal + ' 卡·蛋白 ' + r.proteinGoal + '·碳水 ' + r.carbsGoal + '·脂肪 ' + r.fatGoal + (r.waterGoal === null ? '' : '·饮水 ' + r.waterGoal) + tail, '定营养目标', 'daily_goal (写库回执)', {
735
- recordId: 1,
828
+ recordId: 1, ids: [1], idSource: 'singleton', writtenFields: goalSetWrittenFields(water !== undefined),
736
829
  }));
737
830
  }
738
831
  case 'calorie.goal.water': {
@@ -741,7 +834,7 @@ function dispatchInner(key, params, db) {
741
834
  throw new CalorieRenderError('missing-data', '尚无营养目标行(先定营养目标)');
742
835
  const r = updateWaterGoal(db, water);
743
836
  return out(R('定饮水目标', 'update', '已定饮水目标:' + (r.oldWaterGoal ?? '—') + '→' + r.newWaterGoal + ' ml', '定饮水目标', 'daily_goal (写库回执)', {
744
- recordId: 1, noChange: r.oldWaterGoal === r.newWaterGoal,
837
+ recordId: 1, ids: [1], idSource: 'singleton', writtenFields: ['water'], noChange: r.oldWaterGoal === r.newWaterGoal,
745
838
  }));
746
839
  }
747
840
  case 'calorie.goal.weight': {
@@ -752,19 +845,20 @@ function dispatchInner(key, params, db) {
752
845
  kg, deadline: params['deadline'], startKg: params['startKg'], startDate: params['startDate'],
753
846
  });
754
847
  return out(R('定体重目标', 'update', '已定体重目标 ' + r.weightGoal + ' kg' + (r.deadline ? '(截止 ' + r.deadline + ')' : '') + (r.startKg !== null ? ' · 起点 ' + r.startKg + ' kg' : ''), '定体重目标', 'daily_goal (写库回执)', {
755
- recordId: 1,
848
+ recordId: 1, ids: [1], idSource: 'singleton',
849
+ writtenFields: provided(params, ['kg', 'deadline', 'startKg', 'startDate']),
756
850
  }));
757
851
  }
758
852
  case 'calorie.goal.pause': {
759
853
  const r = pauseAllGoals(db);
760
854
  return out(R('暂停所有目标', 'update', '已暂停所有目标(记录照常,仅目标暂停)', '暂停所有目标', 'daily_goal (写库回执)', {
761
- recordId: r.id,
855
+ recordId: r.id, ids: [r.id], idSource: 'singleton', writtenFields: ['goal_paused'],
762
856
  }));
763
857
  }
764
858
  case 'calorie.goal.resume': {
765
859
  const r = resumeAllGoals(db);
766
860
  return out(R('重启所有目标', 'update', '已重启所有目标(恢复正常)', '重启所有目标', 'daily_goal (写库回执)', {
767
- recordId: r.id,
861
+ recordId: r.id, ids: [r.id], idSource: 'singleton', writtenFields: ['goal_paused'],
768
862
  }));
769
863
  }
770
864
  case 'calorie.body.composition-add': {
@@ -785,14 +879,15 @@ function dispatchInner(key, params, db) {
785
879
  const r = addComposition(db, input);
786
880
  const label = SOURCE_LABELS[source] ?? source;
787
881
  return out(R('记体脂', 'create', '已记体脂:' + date + ' ' + label + ' ' + r.bodyFatPct + '%', '记体脂', 'body_composition (写库回执)', {
788
- recordId: r.id, items: [{ id: r.id, date, status: '成功', reason: '', detail: r.bodyFatPct + '%' }],
882
+ recordId: r.id, ids: [r.id], writtenFields: definedKeys(input),
883
+ items: [{ id: r.id, date, status: '成功', reason: '', detail: r.bodyFatPct + '%' }],
789
884
  }));
790
885
  }
791
886
  case 'calorie.body.composition-remove': {
792
887
  const id = needId(params);
793
888
  deleteComposition(db, id);
794
- return out(R('删体脂', 'delete', '已删除体脂记录 #' + id + '(软删除,可恢复)', '删体脂', 'body_composition (写库回执)', {
795
- recordId: id, items: [{ id, status: '已删除', reason: '' }],
889
+ return out(R('删体脂', 'delete', '已删除体脂记录 #' + id + SOFT_EXCLUDED, '删体脂', 'body_composition (写库回执)', {
890
+ recordId: id, ids: [id], writtenFields: ['is_deprecated'], items: [{ id, status: deleteStatus('soft'), reason: '' }],
796
891
  }));
797
892
  }
798
893
  case 'calorie.body.measure-add': {
@@ -814,14 +909,15 @@ function dispatchInner(key, params, db) {
814
909
  return camel + ' ' + String(input[f]);
815
910
  }).join('、');
816
911
  return out(R('记围度', 'create', '已记围度:' + date + '(' + filledCn + ')', '记围度', 'body_measurements (写库回执)', {
817
- recordId: r.id, items: [{ id: r.id, date, status: '成功', reason: '', detail: filledCn }],
912
+ recordId: r.id, ids: [r.id], writtenFields: measureCliNames(definedKeys(input)),
913
+ items: [{ id: r.id, date, status: '成功', reason: '', detail: filledCn }],
818
914
  }));
819
915
  }
820
916
  case 'calorie.body.measure-remove': {
821
917
  const id = needId(params);
822
918
  deleteMeasurement(db, id);
823
- return out(R('删围度', 'delete', '已删除围度记录 #' + id + '(软删除,可恢复)', '删围度', 'body_measurements (写库回执)', {
824
- recordId: id, items: [{ id, status: '已删除', reason: '' }],
919
+ return out(R('删围度', 'delete', '已删除围度记录 #' + id + SOFT_EXCLUDED, '删围度', 'body_measurements (写库回执)', {
920
+ recordId: id, ids: [id], writtenFields: ['is_deprecated'], items: [{ id, status: deleteStatus('soft'), reason: '' }],
825
921
  }));
826
922
  }
827
923
  default: