agentworkshop 0.7.15 → 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 (99) hide show
  1. package/.AgentWorkShop/prompts/host-tools.json +924 -755
  2. package/.output/.AgentWorkShop/prompts/host-tools.json +924 -755
  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 +1411 -910
  22. package/.output/server/chunks/_/nitro.mjs.map +1 -1
  23. package/.output/server/chunks/build/{TownView-CunmEMzS.mjs → TownView-ql2eHZOg.mjs} +4 -4
  24. package/.output/server/chunks/build/{TownView-CunmEMzS.mjs.map → TownView-ql2eHZOg.mjs.map} +1 -1
  25. package/.output/server/chunks/build/{_id_-CVnMdSeG.mjs → _id_-CwR5oLu9.mjs} +4 -4
  26. package/.output/server/chunks/build/{_id_-CVnMdSeG.mjs.map → _id_-CwR5oLu9.mjs.map} +1 -1
  27. package/.output/server/chunks/build/{_id_-D265Wh_k.mjs → _id_-DvJZ5Qis.mjs} +168 -120
  28. package/.output/server/chunks/build/_id_-DvJZ5Qis.mjs.map +1 -0
  29. package/.output/server/chunks/build/{_wsId_-eNJmBp05.mjs → _wsId_-B3aB9bIp.mjs} +5 -5
  30. package/.output/server/chunks/build/{_wsId_-eNJmBp05.mjs.map → _wsId_-B3aB9bIp.mjs.map} +1 -1
  31. package/.output/server/chunks/build/{daq-DxaXk_8N.mjs → daq-epWL_sC7.mjs} +5 -5
  32. package/.output/server/chunks/build/{daq-DxaXk_8N.mjs.map → daq-epWL_sC7.mjs.map} +1 -1
  33. package/.output/server/chunks/build/{dcw-B0dtiHFG.mjs → dcw-CBPaFj2f.mjs} +2 -2
  34. package/.output/server/chunks/build/{dcw-B0dtiHFG.mjs.map → dcw-CBPaFj2f.mjs.map} +1 -1
  35. package/.output/server/chunks/build/{logs-UWXeNKy_.mjs → logs-Bz1p6x_5.mjs} +3 -3
  36. package/.output/server/chunks/build/{logs-UWXeNKy_.mjs.map → logs-Bz1p6x_5.mjs.map} +1 -1
  37. package/.output/server/chunks/build/{pages-Aexs6LjD.mjs → pages-Dq7_H4FK.mjs} +3 -3
  38. package/.output/server/chunks/build/{pages-Aexs6LjD.mjs.map → pages-Dq7_H4FK.mjs.map} +1 -1
  39. package/.output/server/chunks/build/{town-BbaR-8io.mjs → town-CstiKxf_.mjs} +5 -5
  40. package/.output/server/chunks/build/{town-BbaR-8io.mjs.map → town-CstiKxf_.mjs.map} +1 -1
  41. package/.output/server/chunks/build/{useDaqStream-BkmyQT06.mjs → useDaqStream-BOOZ-p-l.mjs} +2 -2
  42. package/.output/server/chunks/build/{useDaqStream-BkmyQT06.mjs.map → useDaqStream-BOOZ-p-l.mjs.map} +1 -1
  43. package/.output/server/chunks/build/{useDcwStream-DWVoU96J.mjs → useDcwStream-DyB2ufQI.mjs} +15 -1
  44. package/.output/server/chunks/build/{useDcwStream-DWVoU96J.mjs.map → useDcwStream-DyB2ufQI.mjs.map} +1 -1
  45. package/.output/server/chunks/build/{useDeviceTwins-34WGk9aN.mjs → useDeviceTwins-ZbT86L9O.mjs} +2 -2
  46. package/.output/server/chunks/build/{useDeviceTwins-34WGk9aN.mjs.map → useDeviceTwins-ZbT86L9O.mjs.map} +1 -1
  47. package/.output/server/chunks/build/{useOpsLog-C9zTzYof.mjs → useOpsLog-8HCYFb6n.mjs} +2 -2
  48. package/.output/server/chunks/build/{useOpsLog-C9zTzYof.mjs.map → useOpsLog-8HCYFb6n.mjs.map} +1 -1
  49. package/.output/server/chunks/routes/api/workshop/dcw/index.post.mjs +1 -1
  50. package/.output/server/chunks/routes/api/workshop/dcw/index.post.mjs.map +1 -1
  51. package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id/revert.post.mjs +42 -0
  52. package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id/revert.post.mjs.map +1 -0
  53. package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id/versions.get.mjs +29 -0
  54. package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id/versions.get.mjs.map +1 -0
  55. package/.output/server/chunks/routes/api/workshop/dcw/recipes/{_id_.patch.mjs → index.patch.mjs} +11 -6
  56. package/.output/server/chunks/routes/api/workshop/dcw/recipes/index.patch.mjs.map +1 -0
  57. package/.output/server/chunks/routes/api/workshop/index3.post.mjs +1 -1
  58. package/.output/server/chunks/routes/api/workshop/index3.post.mjs.map +1 -1
  59. package/.output/server/chunks/routes/renderer.mjs +1 -1
  60. package/.output/server/chunks/virtual/entry.mjs +34 -8
  61. package/.output/server/chunks/virtual/precomputed.mjs +1 -1
  62. package/.output/server/package.json +1 -1
  63. package/app/composables/workshop/useDcwStream.ts +12 -0
  64. package/app/pages/dcw/[id].vue +165 -1
  65. package/i18n/locales/en.ts +13 -0
  66. package/i18n/locales/zh-CN.ts +13 -0
  67. package/package.json +1 -1
  68. package/scripts/_dbg-opslog-agent-e2e.mjs +191 -0
  69. package/scripts/_dbg-recipe-e2e.mjs +176 -0
  70. package/server/api/workshop/daq/index.post.ts +1 -1
  71. package/server/api/workshop/dcw/lines/index.post.ts +1 -1
  72. package/server/api/workshop/dcw/recipes/{[id].patch.ts → [id]/index.patch.ts} +9 -3
  73. package/server/api/workshop/dcw/recipes/[id]/revert.post.ts +28 -0
  74. package/server/api/workshop/dcw/recipes/[id]/versions.get.ts +14 -0
  75. package/server/data/daqs.json +471 -61
  76. package/server/data/dcw-lines.json +49 -0
  77. package/server/data/dcw-products.json +56 -0
  78. package/server/data/dcw-recipes.json +64 -0
  79. package/server/data/dcw-rollback.json +239 -6
  80. package/server/data/dcw-runs.json +162 -142
  81. package/server/data/dcw-writes.json +132 -0
  82. package/server/data/dcws.json +344 -198
  83. package/server/data/line-runs.json +41 -11
  84. package/server/data/scene-layouts.json +11 -0
  85. package/server/plugins/workshop.ts +9 -0
  86. package/server/services/workshop/agents/agent-badge.ts +38 -0
  87. package/server/services/workshop/agents/host-tool-bridge.ts +642 -612
  88. package/server/services/workshop/agents/industrial-tools.ts +357 -4
  89. package/server/services/workshop/daq/drivers.ts +41 -8
  90. package/server/services/workshop/dcw/dcw-controller.ts +50 -2
  91. package/server/services/workshop/dcw/dcw-recipe.repo.ts +81 -3
  92. package/server/services/workshop/dcw/drivers.ts +2 -2
  93. package/server/services/workshop/dcw/recipe-rollback-manager.ts +8 -6
  94. package/shared/dcw-protocol.ts +14 -2
  95. package/.output/public/_nuxt/Bc3PiGGr.js +0 -1
  96. package/.output/public/_nuxt/_id_.DTvjd6VN.css +0 -1
  97. package/.output/public/_nuxt/builds/meta/65f6bed9-b794-44a4-805a-e22700c10817.json +0 -1
  98. package/.output/server/chunks/build/_id_-D265Wh_k.mjs.map +0 -1
  99. 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.15",
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.15",
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,191 @@
1
+ /**
2
+ * OpsLog Agent 归属 + ops_log/recipe_log 工具 E2E(隔离实例 :3021)
3
+ *
4
+ * 验证:
5
+ * 1) Agent dcw_control 下发 → audit: actorKind=agent, actorName=「Channel名/成员名」(非 UUID)
6
+ * 2) ops_log / recipe_log 注入全部 harness worker 的工具面(agent-tools/list)
7
+ * 3) REST 直调 ops_log/recipe_log:能看到 来源=Agent 条目 + 配方下发/优化开窗
8
+ * 4) 负向:未绑定节点查询被拒
9
+ * 5) 真实 LLM 任务 ×4 引擎(omp/codex/dsh/opencode):Agent 自主调用两工具并在交付中
10
+ * 回带 LOGCHECK-OK / RECIPECHECK-OK 标记(mock 引擎无 LLM 回合,由 2/3 覆盖)
11
+ * 用法:NO_PROXY='127.0.0.1,localhost' node scripts/_dbg-opslog-agent-e2e.mjs [base]
12
+ */
13
+ const BASE = process.argv[2] ?? 'http://127.0.0.1:3021'
14
+ const TAG = `ol${Math.random().toString(36).slice(2, 6)}`
15
+ let pass = 0
16
+ let fail = 0
17
+ const ok = (cond, name, extra = '') => {
18
+ if (cond) { pass++; console.log(` ✓ ${name}`) }
19
+ else { fail++; console.log(` ✗ ${name} ${extra}`) }
20
+ }
21
+ const sleep = ms => new Promise(r => setTimeout(r, ms))
22
+ const api = async (method, path, { body, token } = {}) => {
23
+ const res = await fetch(`${BASE}${path}`, {
24
+ method,
25
+ headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}) },
26
+ body: body !== undefined ? JSON.stringify(body) : undefined,
27
+ signal: AbortSignal.timeout(30_000),
28
+ })
29
+ return { status: res.status, ...(await res.json().catch(() => ({}))) }
30
+ }
31
+
32
+ const ENGINES = [
33
+ { key: 'omp', harness: 'omp', cfg: { provider: 'zhipu-coding-plan', model: 'glm-5.3-flash' } },
34
+ { key: 'codex', harness: 'codex', cfg: { model: 'glm-5.3-flash', approvalPolicy: 'never' } },
35
+ { key: 'dsh', harness: 'dsh', cfg: { provider: 'ustc', model: 'glm-5.3-flash' } },
36
+ { key: 'opencode', harness: 'opencode', cfg: { model: 'zhipuai-coding-plan/glm-5.3-flash' } },
37
+ ]
38
+
39
+ async function main() {
40
+ console.log(`━━━ OpsLog/Recipe 工具 E2E @ ${BASE} (tag=${TAG}) ━━━`)
41
+
42
+ // ── 0. admin 登录(用户名) ──
43
+ const login = await api('POST', '/api/users/login', { body: { email: 'admin', password: 'admin123' } })
44
+ const token = login.data?.token
45
+ ok(Boolean(token), 'admin 用户名登录')
46
+ if (!token) process.exit(1)
47
+
48
+ // ── 1. 产线 + mock 节点 + 配方(+下发) ──
49
+ const line = (await api('POST', '/api/workshop/dcw/lines', { body: { name: `opsline-${TAG}` }, token })).data?.line
50
+ ok(Boolean(line?.id), `产线创建 ${line?.id ?? ''}`)
51
+ const node = (await api('POST', '/api/workshop/dcw', {
52
+ body: { name: `opsnode-${TAG}`, templateRef: 'temp-sp', driver: 'mock', driverConfig: { key: `opsnode-${TAG}` }, min: 0, max: 100, unit: '℃', lineId: line.id },
53
+ token,
54
+ })).data?.node
55
+ ok(Boolean(node?.id), `mock 数控节点创建 ${node?.id ?? ''}`)
56
+ const product = (await api('POST', '/api/workshop/dcw/products', {
57
+ body: { name: `opsproduct-${TAG}`, lineId: line.id },
58
+ token,
59
+ })).data?.product
60
+ ok(Boolean(product?.id), `产品创建 ${product?.id ?? ''}`)
61
+ const recipe = (await api('POST', '/api/workshop/dcw/recipes', {
62
+ body: { name: `opsrecipe-${TAG}`, productId: product?.id, params: [{ nodeId: node.id, value: 42, min: 0, max: 100 }] },
63
+ token,
64
+ })).data?.recipe ?? (await api('GET', '/api/workshop/dcw/recipes', { token })).data?.recipes?.find(r => r.name === `opsrecipe-${TAG}`)
65
+ ok(Boolean(recipe?.id), `配方创建 ${recipe?.id ?? ''}`)
66
+ const applied = await api('POST', `/api/workshop/dcw/recipes/${recipe.id}/apply`, { body: {}, token })
67
+ ok(applied.status === 200 || applied.pending === true, `配方一键下发(status=${applied.status} ${applied.message ?? ''})`)
68
+
69
+ // ── 2. 每 harness:channel(mock lead)+ worker + 绑定 ──
70
+ const members = {}
71
+ for (const e of [...ENGINES, { key: 'mock', harness: 'mock', cfg: { delayMs: 50 } }]) {
72
+ const ch = (await api('POST', '/api/workshop/channels', {
73
+ body: { name: `opslog-${e.key}-${TAG}`, leadAgent: { name: `lead-${TAG}`, harness: 'mock', config: { delayMs: 60 } } },
74
+ token,
75
+ })).data
76
+ const tpl = await api('POST', '/api/workshop/agents', {
77
+ body: { name: `ops-${e.key}-${TAG}`, harness: e.harness, config: { ...e.cfg, systemPromptPrefix: '你是产线操作员:严格按任务执行,只做任务要求的事,完成后立即调用 complete_task。' } },
78
+ token,
79
+ })
80
+ const join = await api('POST', `/api/workshop/channels/${ch.channelId}/agents`, { body: { agentId: tpl.data.id, role: 'worker' }, token })
81
+ const instId = join.data?.id
82
+ const bind = await api('POST', '/api/workshop/agent-tools/bindings', { body: { agentId: instId, nodeId: node.id, kind: 'dcw', mode: 'auto' }, token })
83
+ members[e.key] = { channelId: ch.channelId, instId, name: `ops-${e.key}-${TAG}` }
84
+ ok(Boolean(instId) && bind.code === 0, `${e.harness} worker 建号+绑定(bind=${bind.code === 0 ? 'ok' : bind.message})`)
85
+ }
86
+
87
+ // ── 3. 工具面注入断言(全 harness 一致) ──
88
+ for (const [k, m] of Object.entries(members)) {
89
+ const list = await api('GET', `/api/workshop/agent-tools/list?agentId=${m.instId}`, { token })
90
+ const names = ((list.data?.tools) ?? []).map(t => t.name)
91
+ ok(names.includes('ops_log') && names.includes('recipe_log'), `[${k}] 工具面含 ops_log+recipe_log`)
92
+ }
93
+
94
+ // ── 4. Agent 下发(mock 直调:mock 驱动确定性 ACK,无需 PLC)→ 审计归属 ──
95
+ const wr = await api('POST', '/api/workshop/agent-tools/invoke', {
96
+ body: { agentId: members.omp.instId, tool: 'dcw_control', args: { node_id: node.id, value: 55, task_id: 'e2e-opslog' } },
97
+ token,
98
+ })
99
+ ok(wr.data?.result?.text?.includes('下发成功'), '[omp 直调] dcw_control 下发成功', wr.data?.result?.text?.slice(0, 80))
100
+ const audit = await api('GET', '/api/workshop/audit?limit=100&action=dcw.write.agent', { token })
101
+ const entry = ((audit.data?.entries) ?? []).find(x => x.targetId === node.id)
102
+ ok(Boolean(entry), 'audit 出现 dcw.write.agent 条目')
103
+ ok(entry?.actorKind === 'agent', `条目来源=agent(实际 ${entry?.actorKind})`)
104
+ ok(entry?.actorName === `opslog-omp-${TAG}/ops-omp-${TAG}`, `操作者=Channel/成员(实际 ${entry?.actorName})`)
105
+
106
+ // ── 5. 工具直调查询 ──
107
+ const q1 = await api('POST', '/api/workshop/agent-tools/invoke', {
108
+ body: { agentId: members.omp.instId, tool: 'ops_log', args: { minutes: 120 } },
109
+ token,
110
+ })
111
+ ok(q1.data?.result?.text?.includes('来源=Agent') && q1.data?.result?.text?.includes(`opslog-omp-${TAG}/ops-omp-${TAG}`), '[omp] ops_log 看到来源=Agent + 人话操作者', q1.data?.result?.text?.slice(0, 100))
112
+ const q2 = await api('POST', '/api/workshop/agent-tools/invoke', {
113
+ body: { agentId: members.omp.instId, tool: 'recipe_log', args: { minutes: 120 } },
114
+ token,
115
+ })
116
+ ok(q2.data?.result?.text?.includes('配方下发') || q2.data?.result?.text?.includes('优化开窗'), '[omp] recipe_log 看到配方下发/优化开窗', q2.data?.result?.text?.slice(0, 100))
117
+ const q3 = await api('POST', '/api/workshop/agent-tools/invoke', {
118
+ body: { agentId: members.omp.instId, tool: 'ops_log', args: { node_id: 'not-bound-node' } },
119
+ token,
120
+ })
121
+ ok(q3.data?.result?.isError === true, '负向:未绑定节点查询被拒')
122
+
123
+ // ── 6. 真实 LLM 任务 ×4:Agent 自主调用两工具并回带标记 ──
124
+ const tasks = {}
125
+ for (const e of ENGINES) {
126
+ const m = members[e.key]
127
+ const t = await api('POST', `/api/workshop/channels/${m.channelId}/tasks`, {
128
+ body: {
129
+ title: `opslog-check-${e.key}-${TAG}`,
130
+ parts: [{ text: `执行自查(严格按步骤,不要下发任何控制指令):
131
+ 1. 调用工具 ops_log(minutes=180)查看你负责产线的运维日志;
132
+ 2. 调用工具 recipe_log(minutes=180)查看配方下发与回退历史;
133
+ 3. 在最终交付中原样包含两行标记(各占一行):LOGCHECK-OK 与 RECIPECHECK-OK,并各引用一条你看到的日志摘要(一句话)。
134
+ 完成后调用 complete_task。` }],
135
+ assigneeId: m.instId,
136
+ },
137
+ token,
138
+ })
139
+ tasks[e.key] = t.data?.task?.id ?? t.data?.id
140
+ ok(Boolean(tasks[e.key]), `[${e.key}] 任务下发`)
141
+ }
142
+ const states = {}
143
+ const pollTask = async (id) => {
144
+ for (let i = 0; i < 3; i++) {
145
+ try {
146
+ const me = await api('GET', `/api/workshop/tasks/${id}`, { token })
147
+ return me.data?.state
148
+ }
149
+ catch { await sleep(3000) }
150
+ }
151
+ return undefined
152
+ }
153
+ const deadline = Date.now() + 20 * 60_000
154
+ while (Date.now() < deadline && Object.keys(states).length < ENGINES.length) {
155
+ for (const e of ENGINES) {
156
+ if (states[e.key]) continue
157
+ const st = await pollTask(tasks[e.key])
158
+ if (st && ['COMPLETED', 'FAILED', 'CANCELED'].includes(st)) states[e.key] = st
159
+ }
160
+ if (Object.keys(states).length < ENGINES.length) await sleep(6000)
161
+ }
162
+ for (const e of ENGINES) {
163
+ ok(states[e.key] === 'COMPLETED', `[${e.key}] 引擎任务 COMPLETED(state=${states[e.key] ?? 'RUNNING'})`)
164
+ // 交付标记在 任务 artifacts / channel messages / events 三处任一出现即可
165
+ const taskBlob = JSON.stringify((await api('GET', `/api/workshop/tasks/${tasks[e.key]}`, { token })).data ?? {})
166
+ const msgBlob = JSON.stringify((await api('GET', `/api/workshop/channels/${members[e.key].channelId}/messages?limit=200`, { token })).data ?? {})
167
+ const evBlob = JSON.stringify((await api('GET', `/api/workshop/channels/${members[e.key].channelId}/events?limit=400`, { token })).data ?? {})
168
+ const blob = taskBlob + msgBlob + evBlob
169
+ ok(blob.includes('LOGCHECK-OK'), `[${e.key}] 交付含 LOGCHECK-OK(Agent 真读到日志)`)
170
+ ok(blob.includes('RECIPECHECK-OK'), `[${e.key}] 交付含 RECIPECHECK-OK(Agent 真读到 Recipe 历史)`)
171
+ }
172
+
173
+ // ── 清理 ──
174
+ for (const m of Object.values(members)) await api('DELETE', `/api/workshop/channels/${m.channelId}?purge=1`, { token }).catch(() => {})
175
+ for (const e of [...ENGINES, { key: 'mock' }]) {
176
+ const list = await api('GET', '/api/workshop/agents', { token })
177
+ const tpl = (list.data ?? []).find(x => x.name === `ops-${e.key}-${TAG}`)
178
+ if (tpl) await api('DELETE', `/api/workshop/agents/${tpl.id}`, { token }).catch(() => {})
179
+ }
180
+ await api('DELETE', `/api/workshop/dcw/${node.id}`, { token }).catch(() => {})
181
+ await api('DELETE', `/api/workshop/dcw/recipes/${recipe.id}`, { token }).catch(() => {})
182
+ await api('DELETE', `/api/workshop/dcw/lines/${line.id}`, { token }).catch(() => {})
183
+
184
+ console.log(`\n━━━ OpsLog E2E: ${pass} passed / ${fail} failed ━━━`)
185
+ process.exit(fail === 0 ? 0 : 1)
186
+ }
187
+
188
+ main().catch((err) => {
189
+ console.error('E2E 异常:', err)
190
+ process.exit(1)
191
+ })
@@ -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
+ })