agentworkshop 0.7.16 → 0.7.17

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 (81) hide show
  1. package/.AgentWorkShop/prompts/host-tools.json +105 -0
  2. package/.output/.AgentWorkShop/prompts/host-tools.json +105 -0
  3. package/.output/nitro.json +1 -1
  4. package/.output/public/_nuxt/{DMdVWIGz.js → B-E8fXAO.js} +1 -1
  5. package/.output/public/_nuxt/{DyisHHzD.js → BA3XzwiD.js} +1 -1
  6. package/.output/public/_nuxt/{CD1SlCoI.js → BJem8_v5.js} +4 -4
  7. package/.output/public/_nuxt/{6vYobj4F.js → BWC4HQHQ.js} +1 -1
  8. package/.output/public/_nuxt/{kkEthL84.js → Ba7YofCb.js} +1 -1
  9. package/.output/public/_nuxt/{DGVAuScD.js → C_2bTK0C.js} +1 -1
  10. package/.output/public/_nuxt/{B53Mp7lD.js → CgT5wMfz.js} +1 -1
  11. package/.output/public/_nuxt/{DT-Wg8r1.js → D4CLp1hc.js} +1 -1
  12. package/.output/public/_nuxt/{C6m-kenn.js → DAR70Lf5.js} +1 -1
  13. package/.output/public/_nuxt/{B3Psw_sL.js → DBXhDLgl.js} +1 -1
  14. package/.output/public/_nuxt/{Cj6139IM.js → DsOkkMX3.js} +1 -1
  15. package/.output/public/_nuxt/DtM4RX5T.js +1 -0
  16. package/.output/public/_nuxt/{DaE5saZg.js → QT54MQV9.js} +1 -1
  17. package/.output/public/_nuxt/{Bmpvki7M.js → UtviM1bg.js} +1 -1
  18. package/.output/public/_nuxt/_id_.pF-B9O6u.css +1 -0
  19. package/.output/public/_nuxt/builds/latest.json +1 -1
  20. package/.output/public/_nuxt/builds/meta/6a0a88de-639e-4e7b-b7c1-7f4034e04787.json +1 -0
  21. package/.output/server/chunks/_/nitro.mjs +1087 -746
  22. package/.output/server/chunks/build/{TownView-CunmEMzS.mjs → TownView-ql2eHZOg.mjs} +4 -4
  23. package/.output/server/chunks/build/{TownView-CunmEMzS.mjs.map → TownView-ql2eHZOg.mjs.map} +1 -1
  24. package/.output/server/chunks/build/{_id_-CVnMdSeG.mjs → _id_-CwR5oLu9.mjs} +4 -4
  25. package/.output/server/chunks/build/{_id_-CVnMdSeG.mjs.map → _id_-CwR5oLu9.mjs.map} +1 -1
  26. package/.output/server/chunks/build/{_id_-D265Wh_k.mjs → _id_-DvJZ5Qis.mjs} +168 -120
  27. package/.output/server/chunks/build/_id_-DvJZ5Qis.mjs.map +1 -0
  28. package/.output/server/chunks/build/{_wsId_-eNJmBp05.mjs → _wsId_-B3aB9bIp.mjs} +5 -5
  29. package/.output/server/chunks/build/{_wsId_-eNJmBp05.mjs.map → _wsId_-B3aB9bIp.mjs.map} +1 -1
  30. package/.output/server/chunks/build/{daq-DxaXk_8N.mjs → daq-epWL_sC7.mjs} +5 -5
  31. package/.output/server/chunks/build/{daq-DxaXk_8N.mjs.map → daq-epWL_sC7.mjs.map} +1 -1
  32. package/.output/server/chunks/build/{dcw-B0dtiHFG.mjs → dcw-CBPaFj2f.mjs} +2 -2
  33. package/.output/server/chunks/build/{dcw-B0dtiHFG.mjs.map → dcw-CBPaFj2f.mjs.map} +1 -1
  34. package/.output/server/chunks/build/{logs-UWXeNKy_.mjs → logs-Bz1p6x_5.mjs} +3 -3
  35. package/.output/server/chunks/build/{logs-UWXeNKy_.mjs.map → logs-Bz1p6x_5.mjs.map} +1 -1
  36. package/.output/server/chunks/build/{pages-Aexs6LjD.mjs → pages-Dq7_H4FK.mjs} +3 -3
  37. package/.output/server/chunks/build/{pages-Aexs6LjD.mjs.map → pages-Dq7_H4FK.mjs.map} +1 -1
  38. package/.output/server/chunks/build/{town-BbaR-8io.mjs → town-CstiKxf_.mjs} +5 -5
  39. package/.output/server/chunks/build/{town-BbaR-8io.mjs.map → town-CstiKxf_.mjs.map} +1 -1
  40. package/.output/server/chunks/build/{useDaqStream-BkmyQT06.mjs → useDaqStream-BOOZ-p-l.mjs} +2 -2
  41. package/.output/server/chunks/build/{useDaqStream-BkmyQT06.mjs.map → useDaqStream-BOOZ-p-l.mjs.map} +1 -1
  42. package/.output/server/chunks/build/{useDcwStream-DWVoU96J.mjs → useDcwStream-DyB2ufQI.mjs} +15 -1
  43. package/.output/server/chunks/build/{useDcwStream-DWVoU96J.mjs.map → useDcwStream-DyB2ufQI.mjs.map} +1 -1
  44. package/.output/server/chunks/build/{useDeviceTwins-34WGk9aN.mjs → useDeviceTwins-ZbT86L9O.mjs} +2 -2
  45. package/.output/server/chunks/build/{useDeviceTwins-34WGk9aN.mjs.map → useDeviceTwins-ZbT86L9O.mjs.map} +1 -1
  46. package/.output/server/chunks/build/{useOpsLog-C9zTzYof.mjs → useOpsLog-8HCYFb6n.mjs} +2 -2
  47. package/.output/server/chunks/build/{useOpsLog-C9zTzYof.mjs.map → useOpsLog-8HCYFb6n.mjs.map} +1 -1
  48. package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id/revert.post.mjs +42 -0
  49. package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id/revert.post.mjs.map +1 -0
  50. package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id/versions.get.mjs +29 -0
  51. package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id/versions.get.mjs.map +1 -0
  52. package/.output/server/chunks/routes/api/workshop/dcw/recipes/{_id_.patch.mjs → index.patch.mjs} +11 -6
  53. package/.output/server/chunks/routes/api/workshop/dcw/recipes/index.patch.mjs.map +1 -0
  54. package/.output/server/chunks/routes/renderer.mjs +1 -1
  55. package/.output/server/chunks/virtual/entry.mjs +34 -8
  56. package/.output/server/chunks/virtual/precomputed.mjs +1 -1
  57. package/.output/server/package.json +1 -1
  58. package/app/composables/workshop/useDcwStream.ts +12 -0
  59. package/app/pages/dcw/[id].vue +165 -1
  60. package/i18n/locales/en.ts +13 -0
  61. package/i18n/locales/zh-CN.ts +13 -0
  62. package/package.json +1 -1
  63. package/scripts/_dbg-recipe-e2e.mjs +176 -0
  64. package/server/api/workshop/dcw/recipes/{[id].patch.ts → [id]/index.patch.ts} +9 -3
  65. package/server/api/workshop/dcw/recipes/[id]/revert.post.ts +28 -0
  66. package/server/api/workshop/dcw/recipes/[id]/versions.get.ts +14 -0
  67. package/server/data/dcw-products.json +7 -0
  68. package/server/data/dcw-rollback.json +9 -9
  69. package/server/data/dcws.json +222 -222
  70. package/server/services/workshop/agents/host-tool-bridge.ts +21 -1
  71. package/server/services/workshop/agents/industrial-tools.ts +222 -4
  72. package/server/services/workshop/daq/drivers.ts +16 -3
  73. package/server/services/workshop/dcw/dcw-controller.ts +50 -2
  74. package/server/services/workshop/dcw/dcw-recipe.repo.ts +81 -3
  75. package/server/services/workshop/dcw/drivers.ts +2 -2
  76. package/shared/dcw-protocol.ts +14 -2
  77. package/.output/public/_nuxt/Bc3PiGGr.js +0 -1
  78. package/.output/public/_nuxt/_id_.DTvjd6VN.css +0 -1
  79. package/.output/public/_nuxt/builds/meta/cf0a8831-4514-493d-ad5a-4e371a089ef9.json +0 -1
  80. package/.output/server/chunks/build/_id_-D265Wh_k.mjs.map +0 -1
  81. package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id_.patch.mjs.map +0 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentworkshop-prod",
3
- "version": "0.7.16",
3
+ "version": "0.7.17",
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "dependencies": {
@@ -239,6 +239,18 @@ function createStore() {
239
239
  runs.push(data.run)
240
240
  return data.run
241
241
  },
242
+ /** 配方参数版本历史(旧→新,尾行=当前版;含来源/操作者/描述) */
243
+ recipeVersions: async (id: string): Promise<Array<{ version: number, at: string, by?: string, actorName?: string, description?: string, params: RecipeParam[], current?: boolean }>> => {
244
+ const data = await api<{ versions: Array<{ version: number, at: string, by?: string, actorName?: string, description?: string, params: RecipeParam[], current?: boolean }> }>(`/recipes/${id}/versions`)
245
+ return data.versions ?? []
246
+ },
247
+ /** 回退配方参数到历史版本/lastGood(生成新版本) */
248
+ revertRecipe: async (id: string, body: { version?: number, toLastGood?: boolean, reason: string }): Promise<RecipeView> => {
249
+ const data = await api<{ recipe: RecipeView }>(`/recipes/${id}/revert`, { method: 'POST', body: JSON.stringify(body) })
250
+ const i = recipes.findIndex(r => r.id === id)
251
+ if (i >= 0) recipes[i] = data.recipe
252
+ return data.recipe
253
+ },
242
254
  closeRun: async (id: string): Promise<RecipeRunView> => {
243
255
  const data = await api<{ run: RecipeRunView }>(`/runs/${id}/close`, { method: 'POST' })
244
256
  const i = runs.findIndex(r => r.id === id)
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, onUnmounted, reactive, ref } from 'vue'
3
3
  import { useRoute } from 'vue-router'
4
- import { DCW_DRIVERS, type DcwParamLedger, type DcwTemplateIcon, type LineQueryResult, type RecipeRunData } from '#shared/dcw-protocol'
4
+ import { DCW_DRIVERS, type DcwParamLedger, type DcwTemplateIcon, type LineQueryResult, type RecipeRunData, type RecipeView } from '#shared/dcw-protocol'
5
5
  import type { DriverConfigField } from '#shared/daq-protocol'
6
6
  import { useDcwStream } from '~/composables/workshop/useDcwStream'
7
7
  import { useDaqStream } from '~/composables/workshop/useDaqStream'
@@ -389,6 +389,63 @@ const visibleRecipes = computed(() =>
389
389
  )
390
390
  const productName = (id: string): string => dcw.products.find(p => p.id === id)?.name ?? id
391
391
 
392
+ // ---------- 配方版本历史(整体修改变更记录;来源=用户/Agent/系统 + 操作者 + 回退) ----------
393
+ interface RecipeVersionRow {
394
+ version: number
395
+ at: string
396
+ by?: string
397
+ actorName?: string
398
+ description?: string
399
+ params: Array<{ nodeId: string, value: number, min?: number, max?: number }>
400
+ current?: boolean
401
+ }
402
+ const verOpen = ref(false)
403
+ const verRecipe = ref<RecipeView | null>(null)
404
+ const verRows = ref<RecipeVersionRow[]>([])
405
+ const verLoading = ref(false)
406
+ const verMsg = ref('')
407
+
408
+ async function openRecipeHistory(id: string): Promise<void> {
409
+ verRecipe.value = dcw.recipes.find(r => r.id === id) ?? null
410
+ verOpen.value = true
411
+ verMsg.value = ''
412
+ verLoading.value = true
413
+ try {
414
+ verRows.value = await dcw.recipeVersions(id)
415
+ }
416
+ catch (err) {
417
+ verMsg.value = apiErrorMessage(err)
418
+ }
419
+ finally {
420
+ verLoading.value = false
421
+ }
422
+ }
423
+
424
+ const verSrcLabel = (by?: string): string => (by === 'agent' ? 'Agent' : by === 'system' ? t('dcwDetail.srcSystem') : t('dcwDetail.srcUser'))
425
+
426
+ function verDiff(row: RecipeVersionRow, idx: number): string {
427
+ if (idx === 0) return t('dcwDetail.histInit')
428
+ const prev = verRows.value[idx - 1]!
429
+ const changed = row.params
430
+ .map(p => ({ p, old: prev.params.find(x => x.nodeId === p.nodeId) }))
431
+ .filter(({ p, old }) => !old || old.value !== p.value)
432
+ .map(({ p, old }) => `${dcw.nodes.find(n => n.id === p.nodeId)?.name ?? p.nodeId}: ${old?.value ?? t('dcwDetail.histAdded')} → ${p.value}`)
433
+ return changed.length > 0 ? changed.join(';') : t('dcwDetail.histNoChange')
434
+ }
435
+
436
+ async function doRevert(version: number): Promise<void> {
437
+ if (!verRecipe.value) return
438
+ verMsg.value = ''
439
+ try {
440
+ const recipe = await dcw.revertRecipe(verRecipe.value.id, { version, reason: t('dcwDetail.revertReasonUi') })
441
+ verMsg.value = t('dcwDetail.revertOk', { p0: recipe.version ?? 0 })
442
+ verRows.value = await dcw.recipeVersions(verRecipe.value.id)
443
+ }
444
+ catch (err) {
445
+ verMsg.value = apiErrorMessage(err)
446
+ }
447
+ }
448
+
392
449
  // ---------- 产线数据查询(产品/配方/参数/时间/间隔;限定本产线通道) ----------
393
450
  const query = reactive({
394
451
  productId: '',
@@ -1352,6 +1409,11 @@ function fmtPoint(p: { value?: number, avg?: number } | undefined): string {
1352
1409
  >
1353
1410
  <div class="recipe-name">
1354
1411
  <b>{{ r.name }}</b>
1412
+ <span
1413
+ v-if="r.version"
1414
+ class="param-chip mono"
1415
+ :title="t('dcwDetail.histTitle')"
1416
+ >v{{ r.version }}</span>
1355
1417
  <small class="dim">{{ productName(r.productId) }}</small>
1356
1418
  <small class="mono dim">{{ r.id }}</small>
1357
1419
  </div>
@@ -1391,6 +1453,15 @@ function fmtPoint(p: { value?: number, avg?: number } | undefined): string {
1391
1453
  >
1392
1454
  {{ $t('dcwDetail.k45eb0072') }}
1393
1455
  </button>
1456
+ <button
1457
+ class="mini-btn"
1458
+ @click="openRecipeHistory(r.id)"
1459
+ >
1460
+ {{ t('dcwDetail.histBtn') }}<span
1461
+ v-if="r.version"
1462
+ class="mono"
1463
+ >·v{{ r.version }}</span>
1464
+ </button>
1394
1465
  <button
1395
1466
  class="mini-btn danger"
1396
1467
  @click="dcw.removeRecipe(r.id)"
@@ -1785,6 +1856,86 @@ function fmtPoint(p: { value?: number, avg?: number } | undefined): string {
1785
1856
  </div>
1786
1857
  </div>
1787
1858
 
1859
+ <!-- 配方版本历史(整体修改变更记录:来源/操作者/原因/参数 diff + 回退) -->
1860
+ <div
1861
+ v-if="verOpen"
1862
+ class="modal-mask"
1863
+ @click.self="verOpen = false"
1864
+ >
1865
+ <div
1866
+ class="modal"
1867
+ style="max-width: 760px;"
1868
+ >
1869
+ <div class="modal-head">
1870
+ <h3>
1871
+ {{ t('dcwDetail.histTitle') }}<span
1872
+ v-if="verRecipe"
1873
+ class="dim"
1874
+ style="margin-left: 8px; font-size: 12px;"
1875
+ >{{ verRecipe.name }} · v{{ verRecipe.version }}</span>
1876
+ </h3>
1877
+ <button
1878
+ class="mini-btn"
1879
+ @click="verOpen = false"
1880
+ >
1881
+ ✕
1882
+ </button>
1883
+ </div>
1884
+ <p
1885
+ v-if="verMsg"
1886
+ class="banner good"
1887
+ style="margin: 6px 0;"
1888
+ >
1889
+ {{ verMsg }}
1890
+ </p>
1891
+ <p
1892
+ v-if="verLoading"
1893
+ class="dim"
1894
+ >
1895
+ {{ t('dcwDetail.histLoading') }}
1896
+ </p>
1897
+ <div
1898
+ v-else-if="verRows.length === 0"
1899
+ class="pane-empty"
1900
+ >
1901
+ {{ t('dcwDetail.histEmpty') }}
1902
+ </div>
1903
+ <div
1904
+ v-for="(row, i) in verRows"
1905
+ v-else
1906
+ :key="row.version"
1907
+ class="ver-row"
1908
+ :class="{ cur: row.current }"
1909
+ >
1910
+ <div class="ver-head">
1911
+ <span class="mono ver-v">v{{ row.version }}</span>
1912
+ <span
1913
+ class="ver-src"
1914
+ :class="row.by ?? (row.current ? 'cur' : '')"
1915
+ >{{ row.current ? t('dcwDetail.histCurrent') : verSrcLabel(row.by) }}</span>
1916
+ <span class="mono dim">{{ row.at.slice(0, 19).replace('T', ' ') }}</span>
1917
+ <span class="ver-actor">{{ row.actorName || '—' }}</span>
1918
+ <button
1919
+ v-if="!row.current"
1920
+ class="mini-btn"
1921
+ @click="doRevert(row.version)"
1922
+ >
1923
+ {{ t('dcwDetail.revertTo') }}
1924
+ </button>
1925
+ </div>
1926
+ <p
1927
+ v-if="row.description"
1928
+ class="ver-desc"
1929
+ >
1930
+ {{ row.description }}
1931
+ </p>
1932
+ <p class="ver-diff mono">
1933
+ {{ verDiff(row, i) }}
1934
+ </p>
1935
+ </div>
1936
+ </div>
1937
+ </div>
1938
+
1788
1939
  <!-- 产线数据查询(产品/配方/参数/时间/间隔) -->
1789
1940
  <section class="aw-tile query-card">
1790
1941
  <p class="sec-label">
@@ -2282,6 +2433,19 @@ h1 { margin: 2px 0 4px; font-size: 30px; font-weight: 400; letter-spacing: -0.01
2282
2433
  .recipe-params { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0; }
2283
2434
  .param-chip { padding: 2px 8px; font-size: 11px; color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, transparent); border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent); border-radius: var(--radius-chip); }
2284
2435
  .recipe-actions { display: flex; gap: 6px; align-items: center; margin-top: 8px; }
2436
+ /* ---------- 配方版本历史 ---------- */
2437
+ .ver-row { padding: 10px 12px; margin-bottom: 8px; border: 1px solid var(--divider-hair); border-radius: 10px; }
2438
+ .ver-row.cur { background: color-mix(in srgb, var(--accent) 6%, transparent); border-color: color-mix(in srgb, var(--accent) 30%, transparent); }
2439
+ .ver-head { display: flex; gap: 10px; align-items: center; }
2440
+ .ver-v { font-weight: 700; font-size: 13px; }
2441
+ .ver-src { padding: 1px 8px; font-size: 10.5px; border: 1px solid var(--line-strong); border-radius: var(--radius-pill); }
2442
+ .ver-src.agent { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 50%, transparent); }
2443
+ .ver-src.user { color: var(--ink-soft); }
2444
+ .ver-src.system { color: var(--ink-faint); }
2445
+ .ver-src.cur { color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, transparent); border-color: transparent; }
2446
+ .ver-actor { font-family: var(--font-mono); font-size: 12px; color: var(--ink-soft); }
2447
+ .ver-desc { margin: 6px 0 0; font-size: 12px; color: var(--ink); }
2448
+ .ver-diff { margin: 4px 0 0; font-size: 11px; color: var(--ink-faint); }
2285
2449
  .sec-label { margin: 14px 0 6px; font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-faint); }
2286
2450
  .runs { margin-top: 6px; }
2287
2451
  .pill-btn { padding: 6px 14px; font-size: 12.5px; cursor: pointer; color: var(--paper-raised); background: var(--accent); border: 1px solid var(--accent); border-radius: var(--radius-pill); }
@@ -840,6 +840,19 @@ export default {
840
840
  k1bphrb3048: 'Delete failed',
841
841
  },
842
842
  dcwDetail: {
843
+ histBtn: 'History',
844
+ histTitle: 'Recipe Version History',
845
+ histLoading: 'Loading…',
846
+ histEmpty: 'No version history yet (param edits are recorded automatically)',
847
+ histCurrent: 'Current',
848
+ histInit: 'Initial version',
849
+ histAdded: 'added',
850
+ histNoChange: 'No param changes',
851
+ revertTo: 'Revert to',
852
+ revertOk: 'Reverted; new version v{p0} created',
853
+ revertReasonUi: 'UI revert to history version',
854
+ srcUser: 'User',
855
+ srcSystem: 'System',
843
856
  k9r7d4e016: 'Poll read (ms)',
844
857
  k9r7d4e017: 'Default 5000ms; 0 = manual read only',
845
858
  k9r7d4e018: 'PLC reading',
@@ -839,6 +839,19 @@ export default {
839
839
  k1bphrb3048: '删除失败',
840
840
  },
841
841
  dcwDetail: {
842
+ histBtn: '版本历史',
843
+ histTitle: '配方版本历史',
844
+ histLoading: '加载中…',
845
+ histEmpty: '暂无版本历史(配方参数修改后自动入史)',
846
+ histCurrent: '当前',
847
+ histInit: '初始版本',
848
+ histAdded: '新增',
849
+ histNoChange: '参数未变',
850
+ revertTo: '回退到此版',
851
+ revertOk: '已回退并生成新版本 v{p0}',
852
+ revertReasonUi: '界面回退到历史版本',
853
+ srcUser: '用户',
854
+ srcSystem: '系统',
842
855
  k9r7d4e016: '周期读(ms)',
843
856
  k9r7d4e017: '缺省 5000ms;0 = 仅手动读取',
844
857
  k9r7d4e018: 'PLC 读数',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentworkshop",
3
- "version": "0.7.16",
3
+ "version": "0.7.17",
4
4
  "type": "module",
5
5
  "description": "AgentWorkShop 软件开发系统 - 基于 Nuxt 4 的配置驱动运行范式",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Recipe 管理 + Agent 操控 E2E(隔离实例 :3021)
3
+ *
4
+ * 验证:
5
+ * 1) REST 版本流:PATCH 参数 → v2(归因 user/admin);GET versions 历史完整
6
+ * 2) Agent recipe_update(部分合并)→ v3(归因 agent/Channel名-成员名 + reason)
7
+ * 3) Agent recipe_versions 查看版本史(谁/何时/为什么/diff)
8
+ * 4) REST revert → v4(user);Agent recipe_rollback version=3 → v5(agent)
9
+ * 5) line_context:产线/产品/配方/版本/PLC 当前值/我的绑定 全景
10
+ * 6) 负向:无 reason 拒绝;未绑定节点拒绝;回退到未来版本拒绝
11
+ * 7) 工具面注入:四新工具在 omp worker 工具清单
12
+ * 8) 真实 LLM 任务(omp):line_context → recipe_update → recipe_versions,交付回带标记
13
+ * 用法:NO_PROXY='127.0.0.1,localhost' node scripts/_dbg-recipe-e2e.mjs [base]
14
+ */
15
+ const BASE = process.argv[2] ?? 'http://127.0.0.1:3021'
16
+ const TAG = `rc${Math.random().toString(36).slice(2, 6)}`
17
+ let pass = 0
18
+ let fail = 0
19
+ const ok = (cond, name, extra = '') => {
20
+ if (cond) { pass++; console.log(` ✓ ${name}`) }
21
+ else { fail++; console.log(` ✗ ${name} ${extra}`) }
22
+ }
23
+ const sleep = ms => new Promise(r => setTimeout(r, ms))
24
+ const api = async (method, path, { body, token } = {}) => {
25
+ const res = await fetch(`${BASE}${path}`, {
26
+ method,
27
+ headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}) },
28
+ body: body !== undefined ? JSON.stringify(body) : undefined,
29
+ signal: AbortSignal.timeout(30_000),
30
+ })
31
+ return { status: res.status, ...(await res.json().catch(() => ({}))) }
32
+ }
33
+ const invoke = (token, agentId, tool, args) => api('POST', '/api/workshop/agent-tools/invoke', { body: { agentId, tool, args }, token })
34
+
35
+ async function main() {
36
+ console.log(`━━━ Recipe 管理/Agent 操控 E2E @ ${BASE} (tag=${TAG}) ━━━`)
37
+
38
+ // ── 0. 登录 + 夹具 ──
39
+ const login = await api('POST', '/api/users/login', { body: { email: 'admin', password: 'admin123' } })
40
+ const token = login.data?.token
41
+ ok(Boolean(token), 'admin 登录')
42
+ const line = (await api('POST', '/api/workshop/dcw/lines', { body: { name: `rcline-${TAG}` }, token })).data?.line
43
+ const node = (await api('POST', '/api/workshop/dcw', {
44
+ body: { name: `rcnode-${TAG}`, templateRef: 'temp-sp', driver: 'mock', driverConfig: { key: `rcnode-${TAG}` }, min: 0, max: 100, unit: '℃', lineId: line.id },
45
+ token,
46
+ })).data?.node
47
+ const product = (await api('POST', '/api/workshop/dcw/products', { body: { name: `rcproduct-${TAG}`, lineId: line.id }, token })).data?.product
48
+ const recipe = (await api('POST', '/api/workshop/dcw/recipes', {
49
+ body: { name: `rcrecipe-${TAG}`, productId: product.id, params: [{ nodeId: node.id, value: 42, min: 0, max: 100 }] },
50
+ token,
51
+ })).data?.recipe
52
+ ok(Boolean(line?.id && node?.id && product?.id && recipe?.id), `夹具:产线/节点/产品/配方(${recipe?.id} v${recipe?.version})`)
53
+
54
+ // ── 1. Agent 组建(omp worker + 绑定)──
55
+ const ch = (await api('POST', '/api/workshop/channels', {
56
+ body: { name: `rc-${TAG}`, leadAgent: { name: `lead-${TAG}`, harness: 'mock', config: { delayMs: 60 } } },
57
+ token,
58
+ })).data
59
+ const tpl = await api('POST', '/api/workshop/agents', {
60
+ body: { name: `rcw-${TAG}`, harness: 'omp', config: { provider: 'zhipu-coding-plan', model: 'glm-5.3-flash', systemPromptPrefix: '你是产线操作员:严格按任务执行,完成后立即调用 complete_task。' } },
61
+ token,
62
+ })
63
+ const join = await api('POST', `/api/workshop/channels/${ch.channelId}/agents`, { body: { agentId: tpl.data.id, role: 'worker' }, token })
64
+ const instId = join.data?.id
65
+ const bind = await api('POST', '/api/workshop/agent-tools/bindings', { body: { agentId: instId, nodeId: node.id, kind: 'dcw', mode: 'auto' }, token })
66
+ ok(Boolean(instId) && bind.code === 0, 'omp worker 建号+绑定')
67
+
68
+ // ── 2. 工具面注入 ──
69
+ const list = await api('GET', `/api/workshop/agent-tools/list?agentId=${instId}`, { token })
70
+ const names = (list.data?.tools ?? []).map(x => x.name)
71
+ ok(['line_context', 'recipe_versions', 'recipe_update', 'recipe_rollback'].every(n => names.includes(n)), '工具面含 4 个新工具')
72
+
73
+ // ── 3. REST 版本流:PATCH → v2(user/admin 归因)──
74
+ const patched = await api('PATCH', `/api/workshop/dcw/recipes/${recipe.id}`, {
75
+ body: { params: [{ nodeId: node.id, value: 50, min: 0, max: 100 }] },
76
+ token,
77
+ })
78
+ ok(patched.data?.recipe?.version === 2, `PATCH → v2(实际 v${patched.data?.recipe?.version})`)
79
+ const vs1 = (await api('GET', `/api/workshop/dcw/recipes/${recipe.id}/versions`, { token })).data?.versions ?? []
80
+ const v1entry = vs1.find(v => v.version === 1)
81
+ ok(vs1.length === 2 && v1entry?.by === 'user' && v1entry?.actorName === 'admin', `versions 历史完整(by=${v1entry?.by}, actor=${v1entry?.actorName})`)
82
+
83
+ // ── 4. Agent recipe_update → v3(agent 归因)──
84
+ const upd = await invoke(token, instId, 'recipe_update', {
85
+ recipe_id: recipe.id,
86
+ params: [{ node_id: node.id, value: 88 }],
87
+ reason: '数采证据:窗口均值稳定在 88 且无越限',
88
+ })
89
+ ok(upd.data?.result?.text?.includes('v3'), `[agent] recipe_update → v3`, upd.data?.result?.text?.slice(0, 100))
90
+ const vs2 = (await api('GET', `/api/workshop/dcw/recipes/${recipe.id}/versions`, { token })).data?.versions ?? []
91
+ const v2entry = vs2.find(v => v.version === 2)
92
+ ok(v2entry?.by === 'agent' && v2entry?.actorName?.includes('/'), `v3 归因 agent + Channel/成员(by=${v2entry?.by}, actor=${v2entry?.actorName})`)
93
+ const cur = (await api('GET', '/api/workshop/dcw', { token })).data?.recipes?.find(r => r.id === recipe.id)
94
+ ok(cur?.params?.[0]?.value === 88, '当前参数值=88(部分合并成功)')
95
+
96
+ // ── 5. Agent recipe_versions 查看 ──
97
+ const vh = await invoke(token, instId, 'recipe_versions', { recipe_id: recipe.id })
98
+ ok(vh.data?.result?.text?.includes('来源=Agent') && vh.data?.result?.text?.includes('来源=用户'), '[agent] recipe_versions 含双来源归因', vh.data?.result?.text?.slice(0, 100))
99
+ ok(vh.data?.result?.text?.includes('42→50') || vh.data?.result?.text?.includes('50→88'), '[agent] 版本 diff 可读')
100
+
101
+ // ── 6. REST revert → v4(user);Agent rollback version=3 → v5 ──
102
+ const rv = await api('POST', `/api/workshop/dcw/recipes/${recipe.id}/revert`, { body: { version: 2, reason: 'E2E 界面回退' }, token })
103
+ ok(rv.data?.recipe?.version === 4 && rv.data?.recipe?.params?.[0]?.value === 50, `REST revert → v4 参数=50(实际 v${rv.data?.recipe?.version} 值 ${rv.data?.recipe?.params?.[0]?.value})`)
104
+ const rb = await invoke(token, instId, 'recipe_rollback', { recipe_id: recipe.id, version: 3, reason: '优化翻车,回退到 88 的版本' })
105
+ ok(rb.data?.result?.text?.includes('v5'), '[agent] recipe_rollback → v5', rb.data?.result?.text?.slice(0, 100))
106
+ const cur2 = (await api('GET', '/api/workshop/dcw', { token })).data?.recipes?.find(r => r.id === recipe.id)
107
+ ok(cur2?.params?.[0]?.value === 88, '回退后参数=88(非破坏版本化)')
108
+
109
+ // ── 7. line_context 全景 ──
110
+ const lc = await invoke(token, instId, 'line_context', {})
111
+ ok(lc.data?.result?.text?.includes(`rcline-${TAG}`) && lc.data?.result?.text?.includes(`rcproduct-${TAG}`) && lc.data?.result?.text?.includes(`rcrecipe-${TAG}`), '[agent] line_context 含产线/产品/配方名')
112
+ ok(lc.data?.result?.text?.includes('v5') && lc.data?.result?.text?.includes('PLC 当前'), '[agent] line_context 含版本与 PLC 当前值')
113
+
114
+ // ── 8. 负向 ──
115
+ const n1 = await invoke(token, instId, 'recipe_update', { recipe_id: recipe.id, params: [{ node_id: node.id, value: 60 }] })
116
+ ok(n1.data?.result?.isError === true, '负向:无 reason 被拒')
117
+ const n2 = await invoke(token, instId, 'recipe_update', { recipe_id: recipe.id, params: [{ node_id: 'not-bound', value: 60 }], reason: 'x' })
118
+ ok(n2.data?.result?.isError === true, '负向:未绑定节点被拒')
119
+ const n3 = await invoke(token, instId, 'recipe_rollback', { recipe_id: recipe.id, version: 99, reason: 'x' })
120
+ ok(n3.data?.result?.isError === true, '负向:不存在的版本被拒')
121
+
122
+ // ── 9. 真实 LLM 任务(omp):上下文→保存→查史,回带标记 ──
123
+ const t = await api('POST', `/api/workshop/channels/${ch.channelId}/tasks`, {
124
+ body: {
125
+ title: `recipe-loop-${TAG}`,
126
+ parts: [{ text: `执行配方闭环自查(不要下发 PLC 控制指令):
127
+ 1. 调用 line_context 确认你控制的产线/产品/配方与版本;
128
+ 2. 调用 recipe_update,recipe_id 用 line_context 看到的,params=[{node_id: "${node.id}", value: 92}],reason 写"闭环验证:数采窗口稳定";
129
+ 3. 调用 recipe_versions 复核;
130
+ 4. 最终交付原样包含三行标记:CONTEXT-OK、SAVED-OK、HIST-OK,并各附一句你看到的信息。
131
+ 完成后调用 complete_task。` }],
132
+ assigneeId: instId,
133
+ },
134
+ token,
135
+ })
136
+ const taskId = t.data?.task?.id ?? t.data?.id
137
+ ok(Boolean(taskId), '[omp] 闭环任务下发')
138
+ let state = ''
139
+ const deadline = Date.now() + 14 * 60_000
140
+ while (Date.now() < deadline) {
141
+ for (let i = 0; i < 3; i++) {
142
+ try {
143
+ const me = await api('GET', `/api/workshop/tasks/${taskId}`, { token })
144
+ state = me.data?.state ?? ''
145
+ break
146
+ }
147
+ catch { await sleep(3000) }
148
+ }
149
+ if (['COMPLETED', 'FAILED', 'CANCELED'].includes(state)) break
150
+ await sleep(6000)
151
+ }
152
+ ok(state === 'COMPLETED', `[omp] 闭环任务 COMPLETED(state=${state})`)
153
+ const taskBlob = JSON.stringify((await api('GET', `/api/workshop/tasks/${taskId}`, { token })).data ?? {})
154
+ const msgBlob = JSON.stringify((await api('GET', `/api/workshop/channels/${ch.channelId}/messages?limit=200`, { token })).data ?? {})
155
+ const blob = taskBlob + msgBlob
156
+ ok(blob.includes('CONTEXT-OK'), '[omp] 交付含 CONTEXT-OK(Agent 知道自己操控的产线/产品/配方)')
157
+ const finalVer = (await api('GET', '/api/workshop/dcw', { token })).data?.recipes?.find(r => r.id === recipe.id)
158
+ ok(finalVer?.params?.[0]?.value === 92 && (finalVer?.version ?? 0) >= 6, `[omp] LLM 保存参数生效(v${finalVer?.version} 值 ${finalVer?.params?.[0]?.value})`)
159
+ ok(blob.includes('SAVED-OK') && blob.includes('HIST-OK'), '[omp] 交付含 SAVED-OK/HIST-OK')
160
+
161
+ // ── 清理 ──
162
+ await api('DELETE', `/api/workshop/channels/${ch.channelId}?purge=1`, { token }).catch(() => {})
163
+ await api('DELETE', `/api/workshop/agents/${tpl.data.id}`, { token }).catch(() => {})
164
+ await api('DELETE', `/api/workshop/dcw/${node.id}`, { token }).catch(() => {})
165
+ await api('DELETE', `/api/workshop/dcw/recipes/${recipe.id}`, { token }).catch(() => {})
166
+ await api('DELETE', `/api/workshop/dcw/products/${product.id}`, { token }).catch(() => {})
167
+ await api('DELETE', `/api/workshop/dcw/lines/${line.id}`, { token }).catch(() => {})
168
+
169
+ console.log(`\n━━━ Recipe E2E: ${pass} passed / ${fail} failed ━━━`)
170
+ process.exit(fail === 0 ? 0 : 1)
171
+ }
172
+
173
+ main().catch((err) => {
174
+ console.error('E2E 异常:', err)
175
+ process.exit(1)
176
+ })
@@ -1,16 +1,22 @@
1
1
  /**
2
2
  * PATCH /api/workshop/dcw/recipes/:id —— 编辑配方。
3
+ * params 变更自动版本化(paramsHistory 入史,归因=当前用户)。
3
4
  */
4
5
  import { getRouterParam, readBody } from 'h3'
5
6
  import type { RecipeInput } from '#shared/dcw-protocol'
6
7
  import { resolveUser } from '@/server/api/workshop/caller'
7
8
  import { defineApiHandler } from '@/server/utils/response'
8
- import { getDcwRecipeRepo } from '@/server/services/workshop/dcw/dcw-recipe.repo'
9
+ import { getDcwController } from '@/server/services/workshop/dcw/dcw-controller'
9
10
 
10
11
  export default defineApiHandler(async (event) => {
11
- resolveUser(event)
12
+ const user = resolveUser(event)
12
13
  const id = getRouterParam(event, 'id') ?? ''
13
14
  const body = await readBody<Partial<RecipeInput>>(event) ?? {}
14
- const recipe = getDcwRecipeRepo().update(id, body)
15
+ const recipe = getDcwController().updateRecipe(id, body, {
16
+ by: 'user',
17
+ actorName: user.name,
18
+ actor: user.id,
19
+ description: '界面编辑配方',
20
+ })
15
21
  return { recipe }
16
22
  })
@@ -0,0 +1,28 @@
1
+ /**
2
+ * POST /api/workshop/dcw/recipes/:id/revert —— 回退配方参数到指定历史版本/已知良好批次。
3
+ * 生成新版本(非破坏;历史完整保留)。body: { version?: number, toLastGood?: boolean, reason: string }。
4
+ */
5
+ import { getRouterParam, readBody } from 'h3'
6
+ import { resolveUser } from '@/server/api/workshop/caller'
7
+ import { defineApiHandler } from '@/server/utils/response'
8
+ import { getDcwController } from '@/server/services/workshop/dcw/dcw-controller'
9
+ import { AppError, ErrorCodes } from '@/server/utils/errors'
10
+
11
+ export default defineApiHandler(async (event) => {
12
+ const user = resolveUser(event)
13
+ const id = getRouterParam(event, 'id') ?? ''
14
+ const body = await readBody<{ version?: number, toLastGood?: boolean, reason?: string }> (event) ?? {}
15
+ if (!body.toLastGood && body.version == null) {
16
+ throw new AppError(400, ErrorCodes.VALIDATION_ERROR, '需提供 version(历史版本号)或 toLastGood=true')
17
+ }
18
+ const recipe = getDcwController().revertRecipe(id, {
19
+ version: body.version,
20
+ toLastGood: body.toLastGood === true,
21
+ }, {
22
+ by: 'user',
23
+ actorName: user.name,
24
+ actor: user.id,
25
+ description: body.reason?.trim() || '界面回退',
26
+ })
27
+ return { recipe }
28
+ })
@@ -0,0 +1,14 @@
1
+ /**
2
+ * GET /api/workshop/dcw/recipes/:id/versions —— 配方参数版本历史(旧→新,尾行=当前版)。
3
+ * 每条:version/at/by(来源)/actorName(人话操作者)/description(变更原因)/params。
4
+ */
5
+ import { getRouterParam } from 'h3'
6
+ import { resolveUser } from '@/server/api/workshop/caller'
7
+ import { defineApiHandler } from '@/server/utils/response'
8
+ import { getDcwController } from '@/server/services/workshop/dcw/dcw-controller'
9
+
10
+ export default defineApiHandler(async (event) => {
11
+ resolveUser(event)
12
+ const id = getRouterParam(event, 'id') ?? ''
13
+ return { versions: getDcwController().recipeVersions(id) }
14
+ })
@@ -1433,5 +1433,12 @@
1433
1433
  "name": "opsproduct-olxa06",
1434
1434
  "description": "",
1435
1435
  "createdAt": "2026-09-06T21:25:39.947+08:00"
1436
+ },
1437
+ {
1438
+ "id": "pd-b61755e6",
1439
+ "lineId": "",
1440
+ "name": "vis-product-rc",
1441
+ "description": "",
1442
+ "createdAt": "2026-09-06T22:49:03.508+08:00"
1436
1443
  }
1437
1444
  ]
@@ -5236,7 +5236,7 @@
5236
5236
  "channels": [],
5237
5237
  "degraded": true
5238
5238
  },
5239
- "evaluatedAt": "2026-09-06T21:50:04.887+08:00",
5239
+ "evaluatedAt": "2026-09-06T22:54:48.800+08:00",
5240
5240
  "windowAgg": {
5241
5241
  "at": "2026-08-31T16:25:17.398Z",
5242
5242
  "fromMs": 1788193396897,
@@ -11685,7 +11685,7 @@
11685
11685
  }
11686
11686
  ]
11687
11687
  },
11688
- "evaluatedAt": "2026-09-06T21:50:04.887+08:00",
11688
+ "evaluatedAt": "2026-09-06T22:54:48.800+08:00",
11689
11689
  "windowAgg": {
11690
11690
  "at": "2026-09-05T02:07:03.383Z",
11691
11691
  "fromMs": 1788573903195,
@@ -11746,7 +11746,7 @@
11746
11746
  }
11747
11747
  ]
11748
11748
  },
11749
- "evaluatedAt": "2026-09-06T21:50:04.887+08:00",
11749
+ "evaluatedAt": "2026-09-06T22:54:48.800+08:00",
11750
11750
  "windowAgg": {
11751
11751
  "at": "2026-09-05T02:44:52.815Z",
11752
11752
  "fromMs": 1788576172595,
@@ -11806,7 +11806,7 @@
11806
11806
  }
11807
11807
  ]
11808
11808
  },
11809
- "evaluatedAt": "2026-09-06T21:50:04.887+08:00",
11809
+ "evaluatedAt": "2026-09-06T22:54:48.800+08:00",
11810
11810
  "windowAgg": {
11811
11811
  "at": "2026-09-05T03:09:49.742Z",
11812
11812
  "fromMs": 1788577669697,
@@ -11866,7 +11866,7 @@
11866
11866
  }
11867
11867
  ]
11868
11868
  },
11869
- "evaluatedAt": "2026-09-06T21:50:04.887+08:00",
11869
+ "evaluatedAt": "2026-09-06T22:54:48.800+08:00",
11870
11870
  "windowAgg": {
11871
11871
  "at": "2026-09-05T03:34:32.268Z",
11872
11872
  "fromMs": 1788579151974,
@@ -11915,7 +11915,7 @@
11915
11915
  "channels": [],
11916
11916
  "degraded": true
11917
11917
  },
11918
- "evaluatedAt": "2026-09-06T21:50:04.887+08:00",
11918
+ "evaluatedAt": "2026-09-06T22:54:48.800+08:00",
11919
11919
  "windowAgg": {
11920
11920
  "at": "2026-09-05T09:01:07.414Z",
11921
11921
  "fromMs": 1788598747074,
@@ -11954,7 +11954,7 @@
11954
11954
  "channels": [],
11955
11955
  "degraded": true
11956
11956
  },
11957
- "evaluatedAt": "2026-09-06T21:50:04.887+08:00",
11957
+ "evaluatedAt": "2026-09-06T22:54:48.800+08:00",
11958
11958
  "windowAgg": {
11959
11959
  "at": "2026-09-06T21:04:02.391+08:00",
11960
11960
  "fromMs": 1788699722063,
@@ -11993,7 +11993,7 @@
11993
11993
  "channels": [],
11994
11994
  "degraded": true
11995
11995
  },
11996
- "evaluatedAt": "2026-09-06T21:50:04.887+08:00",
11996
+ "evaluatedAt": "2026-09-06T22:54:48.800+08:00",
11997
11997
  "windowAgg": {
11998
11998
  "at": "2026-09-06T21:18:40.623+08:00",
11999
11999
  "fromMs": 1788700600508,
@@ -12032,7 +12032,7 @@
12032
12032
  "channels": [],
12033
12033
  "degraded": true
12034
12034
  },
12035
- "evaluatedAt": "2026-09-06T21:50:04.887+08:00",
12035
+ "evaluatedAt": "2026-09-06T22:54:48.800+08:00",
12036
12036
  "windowAgg": {
12037
12037
  "at": "2026-09-06T21:27:40.730+08:00",
12038
12038
  "fromMs": 1788701140666,