agentworkshop 0.7.16 → 0.7.18
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.
- package/.AgentWorkShop/prompts/host-tools.json +105 -0
- package/.output/.AgentWorkShop/prompts/host-tools.json +105 -0
- package/.output/nitro.json +1 -1
- package/.output/public/_nuxt/{DMdVWIGz.js → B-E8fXAO.js} +1 -1
- package/.output/public/_nuxt/{DyisHHzD.js → BA3XzwiD.js} +1 -1
- package/.output/public/_nuxt/{CD1SlCoI.js → BOJaHO5V.js} +4 -4
- package/.output/public/_nuxt/{6vYobj4F.js → BWC4HQHQ.js} +1 -1
- package/.output/public/_nuxt/B_C3GLsD.js +1 -0
- package/.output/public/_nuxt/{kkEthL84.js → Ba7YofCb.js} +1 -1
- package/.output/public/_nuxt/{DGVAuScD.js → C_2bTK0C.js} +1 -1
- package/.output/public/_nuxt/{B53Mp7lD.js → CgT5wMfz.js} +1 -1
- package/.output/public/_nuxt/{DT-Wg8r1.js → D4CLp1hc.js} +1 -1
- package/.output/public/_nuxt/{C6m-kenn.js → DAR70Lf5.js} +1 -1
- package/.output/public/_nuxt/{B3Psw_sL.js → DBXhDLgl.js} +1 -1
- package/.output/public/_nuxt/{Cj6139IM.js → DsOkkMX3.js} +1 -1
- package/.output/public/_nuxt/{DaE5saZg.js → QT54MQV9.js} +1 -1
- package/.output/public/_nuxt/{Bmpvki7M.js → UtviM1bg.js} +1 -1
- package/.output/public/_nuxt/_id_.IbmpkhH4.css +1 -0
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/253c093e-3336-4451-9f11-08061bf22f01.json +1 -0
- package/.output/public/_nuxt/{entry.BNApuuEW.css → entry.DRUzDwmX.css} +1 -1
- package/.output/server/chunks/_/nitro.mjs +1130 -734
- package/.output/server/chunks/build/{TownView-CunmEMzS.mjs → TownView-ql2eHZOg.mjs} +4 -4
- package/.output/server/chunks/build/{TownView-CunmEMzS.mjs.map → TownView-ql2eHZOg.mjs.map} +1 -1
- package/.output/server/chunks/build/{_id_-D265Wh_k.mjs → _id_-Bs-euJPu.mjs} +215 -120
- package/.output/server/chunks/build/_id_-Bs-euJPu.mjs.map +1 -0
- package/.output/server/chunks/build/{_id_-CVnMdSeG.mjs → _id_-CwR5oLu9.mjs} +4 -4
- package/.output/server/chunks/build/{_id_-CVnMdSeG.mjs.map → _id_-CwR5oLu9.mjs.map} +1 -1
- package/.output/server/chunks/build/{_wsId_-eNJmBp05.mjs → _wsId_-B3aB9bIp.mjs} +5 -5
- package/.output/server/chunks/build/{_wsId_-eNJmBp05.mjs.map → _wsId_-B3aB9bIp.mjs.map} +1 -1
- package/.output/server/chunks/build/{daq-DxaXk_8N.mjs → daq-epWL_sC7.mjs} +5 -5
- package/.output/server/chunks/build/{daq-DxaXk_8N.mjs.map → daq-epWL_sC7.mjs.map} +1 -1
- package/.output/server/chunks/build/{dcw-B0dtiHFG.mjs → dcw-CBPaFj2f.mjs} +2 -2
- package/.output/server/chunks/build/{dcw-B0dtiHFG.mjs.map → dcw-CBPaFj2f.mjs.map} +1 -1
- package/.output/server/chunks/build/{logs-UWXeNKy_.mjs → logs-Bz1p6x_5.mjs} +3 -3
- package/.output/server/chunks/build/{logs-UWXeNKy_.mjs.map → logs-Bz1p6x_5.mjs.map} +1 -1
- package/.output/server/chunks/build/{pages-Aexs6LjD.mjs → pages-Dq7_H4FK.mjs} +3 -3
- package/.output/server/chunks/build/{pages-Aexs6LjD.mjs.map → pages-Dq7_H4FK.mjs.map} +1 -1
- package/.output/server/chunks/build/{town-BbaR-8io.mjs → town-CstiKxf_.mjs} +5 -5
- package/.output/server/chunks/build/{town-BbaR-8io.mjs.map → town-CstiKxf_.mjs.map} +1 -1
- package/.output/server/chunks/build/{useDaqStream-BkmyQT06.mjs → useDaqStream-BOOZ-p-l.mjs} +2 -2
- package/.output/server/chunks/build/{useDaqStream-BkmyQT06.mjs.map → useDaqStream-BOOZ-p-l.mjs.map} +1 -1
- package/.output/server/chunks/build/{useDcwStream-DWVoU96J.mjs → useDcwStream-DyB2ufQI.mjs} +15 -1
- package/.output/server/chunks/build/{useDcwStream-DWVoU96J.mjs.map → useDcwStream-DyB2ufQI.mjs.map} +1 -1
- package/.output/server/chunks/build/{useDeviceTwins-34WGk9aN.mjs → useDeviceTwins-ZbT86L9O.mjs} +2 -2
- package/.output/server/chunks/build/{useDeviceTwins-34WGk9aN.mjs.map → useDeviceTwins-ZbT86L9O.mjs.map} +1 -1
- package/.output/server/chunks/build/{useOpsLog-C9zTzYof.mjs → useOpsLog-8HCYFb6n.mjs} +2 -2
- package/.output/server/chunks/build/{useOpsLog-C9zTzYof.mjs.map → useOpsLog-8HCYFb6n.mjs.map} +1 -1
- package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id/revert.post.mjs +42 -0
- package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id/revert.post.mjs.map +1 -0
- package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id/versions.get.mjs +29 -0
- package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id/versions.get.mjs.map +1 -0
- package/.output/server/chunks/routes/api/workshop/dcw/recipes/{_id_.patch.mjs → index.patch.mjs} +11 -6
- package/.output/server/chunks/routes/api/workshop/dcw/recipes/index.patch.mjs.map +1 -0
- package/.output/server/chunks/routes/renderer.mjs +1 -1
- package/.output/server/chunks/virtual/entry.mjs +42 -8
- package/.output/server/chunks/virtual/precomputed.mjs +1 -1
- package/.output/server/package.json +1 -1
- package/app/composables/workshop/useDcwStream.ts +12 -0
- package/app/pages/dcw/[id].vue +219 -5
- package/i18n/locales/en.ts +17 -0
- package/i18n/locales/zh-CN.ts +17 -0
- package/package.json +1 -1
- package/scripts/_dbg-recipe-e2e.mjs +176 -0
- package/scripts/_dbg-recipe-stale-e2e.mjs +135 -0
- package/server/api/workshop/dcw/recipes/{[id].patch.ts → [id]/index.patch.ts} +9 -3
- package/server/api/workshop/dcw/recipes/[id]/revert.post.ts +28 -0
- package/server/api/workshop/dcw/recipes/[id]/versions.get.ts +14 -0
- package/server/data/daqs.json +203 -66
- package/server/data/dcw-lines.json +7 -28
- package/server/data/dcw-products.json +14 -7
- package/server/data/dcw-recipes.json +28 -14
- package/server/data/dcw-rollback.json +108 -9
- package/server/data/dcw-runs.json +430 -230
- package/server/data/dcw-writes.json +48 -0
- package/server/data/dcws.json +464 -372
- package/server/data/line-runs.json +23 -13
- package/server/services/workshop/agents/host-tool-bridge.ts +21 -1
- package/server/services/workshop/agents/industrial-tools.ts +256 -4
- package/server/services/workshop/daq/drivers.ts +16 -3
- package/server/services/workshop/dcw/dcw-controller.ts +61 -3
- package/server/services/workshop/dcw/dcw-recipe.repo.ts +96 -3
- package/server/services/workshop/dcw/drivers.ts +2 -2
- package/shared/dcw-protocol.ts +14 -2
- package/.output/public/_nuxt/Bc3PiGGr.js +0 -1
- package/.output/public/_nuxt/_id_.DTvjd6VN.css +0 -1
- package/.output/public/_nuxt/builds/meta/cf0a8831-4514-493d-ad5a-4e371a089ef9.json +0 -1
- package/.output/server/chunks/build/_id_-D265Wh_k.mjs.map +0 -1
- package/.output/server/chunks/routes/api/workshop/dcw/recipes/_id_.patch.mjs.map +0 -1
|
@@ -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)
|
package/app/pages/dcw/[id].vue
CHANGED
|
@@ -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,83 @@ const visibleRecipes = computed(() =>
|
|
|
389
389
|
)
|
|
390
390
|
const productName = (id: string): string => dcw.products.find(p => p.id === id)?.name ?? id
|
|
391
391
|
|
|
392
|
+
// ---------- 配方参数节点失效态(已删除/已停用/已取消绑定;灰化 + 徽标) ----------
|
|
393
|
+
interface StaleRef { code: 'deleted' | 'disabled' | 'unbound', label: string }
|
|
394
|
+
function paramStatus(nodeId: string, lineId: string): StaleRef | null {
|
|
395
|
+
const node = dcw.nodes.find(n => n.id === nodeId)
|
|
396
|
+
if (!node) return { code: 'deleted', label: t('dcwDetail.staleDeleted') }
|
|
397
|
+
if (!node.enabled) return { code: 'disabled', label: t('dcwDetail.staleDisabled') }
|
|
398
|
+
if (lineId && node.lineId !== lineId) return { code: 'unbound', label: t('dcwDetail.staleUnbound') }
|
|
399
|
+
return null
|
|
400
|
+
}
|
|
401
|
+
function paramNodeName(nodeId: string): string {
|
|
402
|
+
return dcw.nodes.find(n => n.id === nodeId)?.name ?? nodeId
|
|
403
|
+
}
|
|
404
|
+
function daqWindowStatus(nodeId: string, lineId: string): StaleRef | null {
|
|
405
|
+
const node = daq.nodes.find(n => n.id === nodeId)
|
|
406
|
+
if (!node) return { code: 'deleted', label: t('dcwDetail.staleDeleted') }
|
|
407
|
+
if (!node.enabled) return { code: 'disabled', label: t('dcwDetail.staleDisabled') }
|
|
408
|
+
if (lineId && node.lineId !== lineId) return { code: 'unbound', label: t('dcwDetail.staleUnbound') }
|
|
409
|
+
return null
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ---------- 配方版本历史(整体修改变更记录;来源=用户/Agent/系统 + 操作者 + 回退) ----------
|
|
413
|
+
interface RecipeVersionRow {
|
|
414
|
+
version: number
|
|
415
|
+
at: string
|
|
416
|
+
by?: string
|
|
417
|
+
actorName?: string
|
|
418
|
+
description?: string
|
|
419
|
+
params: Array<{ nodeId: string, value: number, min?: number, max?: number }>
|
|
420
|
+
current?: boolean
|
|
421
|
+
}
|
|
422
|
+
const verOpen = ref(false)
|
|
423
|
+
const verRecipe = ref<RecipeView | null>(null)
|
|
424
|
+
const verRows = ref<RecipeVersionRow[]>([])
|
|
425
|
+
const verLoading = ref(false)
|
|
426
|
+
const verMsg = ref('')
|
|
427
|
+
|
|
428
|
+
async function openRecipeHistory(id: string): Promise<void> {
|
|
429
|
+
verRecipe.value = dcw.recipes.find(r => r.id === id) ?? null
|
|
430
|
+
verOpen.value = true
|
|
431
|
+
verMsg.value = ''
|
|
432
|
+
verLoading.value = true
|
|
433
|
+
try {
|
|
434
|
+
verRows.value = await dcw.recipeVersions(id)
|
|
435
|
+
}
|
|
436
|
+
catch (err) {
|
|
437
|
+
verMsg.value = apiErrorMessage(err)
|
|
438
|
+
}
|
|
439
|
+
finally {
|
|
440
|
+
verLoading.value = false
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const verSrcLabel = (by?: string): string => (by === 'agent' ? 'Agent' : by === 'system' ? t('dcwDetail.srcSystem') : t('dcwDetail.srcUser'))
|
|
445
|
+
|
|
446
|
+
function verDiff(row: RecipeVersionRow, idx: number): string {
|
|
447
|
+
if (idx === 0) return t('dcwDetail.histInit')
|
|
448
|
+
const prev = verRows.value[idx - 1]!
|
|
449
|
+
const changed = row.params
|
|
450
|
+
.map(p => ({ p, old: prev.params.find(x => x.nodeId === p.nodeId) }))
|
|
451
|
+
.filter(({ p, old }) => !old || old.value !== p.value)
|
|
452
|
+
.map(({ p, old }) => `${dcw.nodes.find(n => n.id === p.nodeId)?.name ?? p.nodeId}: ${old?.value ?? t('dcwDetail.histAdded')} → ${p.value}`)
|
|
453
|
+
return changed.length > 0 ? changed.join(';') : t('dcwDetail.histNoChange')
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function doRevert(version: number): Promise<void> {
|
|
457
|
+
if (!verRecipe.value) return
|
|
458
|
+
verMsg.value = ''
|
|
459
|
+
try {
|
|
460
|
+
const recipe = await dcw.revertRecipe(verRecipe.value.id, { version, reason: t('dcwDetail.revertReasonUi') })
|
|
461
|
+
verMsg.value = t('dcwDetail.revertOk', { p0: recipe.version ?? 0 })
|
|
462
|
+
verRows.value = await dcw.recipeVersions(verRecipe.value.id)
|
|
463
|
+
}
|
|
464
|
+
catch (err) {
|
|
465
|
+
verMsg.value = apiErrorMessage(err)
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
392
469
|
// ---------- 产线数据查询(产品/配方/参数/时间/间隔;限定本产线通道) ----------
|
|
393
470
|
const query = reactive({
|
|
394
471
|
productId: '',
|
|
@@ -436,6 +513,7 @@ const recipeOpen = ref(false)
|
|
|
436
513
|
const recipeEditing = ref<string | null>(null)
|
|
437
514
|
const recipeSaving = ref(false)
|
|
438
515
|
const recipeError = ref('')
|
|
516
|
+
const recipeStaleNote = ref('')
|
|
439
517
|
const recipeForm = reactive({
|
|
440
518
|
productId: '',
|
|
441
519
|
name: '',
|
|
@@ -463,10 +541,15 @@ function openRecipeEdit(id: string): void {
|
|
|
463
541
|
if (!r) return
|
|
464
542
|
recipeEditing.value = id
|
|
465
543
|
recipeError.value = ''
|
|
544
|
+
// 失效参数自动清理:节点已删除的参数行直接剔除(保存会被后端归一化拒绝);
|
|
545
|
+
// 停用/解绑节点参数保留可编辑,状态由 select 旁徽标标示
|
|
546
|
+
const live = r.params.filter(p => dcw.nodes.some(n => n.id === p.nodeId))
|
|
547
|
+
const dropped = r.params.length - live.length
|
|
548
|
+
recipeStaleNote.value = dropped > 0 ? t('dcwDetail.staleDropped', { p0: dropped }) : ''
|
|
466
549
|
recipeForm.productId = r.productId
|
|
467
550
|
recipeForm.name = r.name
|
|
468
551
|
recipeForm.description = r.description
|
|
469
|
-
recipeForm.params =
|
|
552
|
+
recipeForm.params = live.map(p => ({
|
|
470
553
|
nodeId: p.nodeId,
|
|
471
554
|
value: p.value,
|
|
472
555
|
min: p.min ?? '',
|
|
@@ -1352,6 +1435,11 @@ function fmtPoint(p: { value?: number, avg?: number } | undefined): string {
|
|
|
1352
1435
|
>
|
|
1353
1436
|
<div class="recipe-name">
|
|
1354
1437
|
<b>{{ r.name }}</b>
|
|
1438
|
+
<span
|
|
1439
|
+
v-if="r.version"
|
|
1440
|
+
class="param-chip mono"
|
|
1441
|
+
:title="t('dcwDetail.histTitle')"
|
|
1442
|
+
>v{{ r.version }}</span>
|
|
1355
1443
|
<small class="dim">{{ productName(r.productId) }}</small>
|
|
1356
1444
|
<small class="mono dim">{{ r.id }}</small>
|
|
1357
1445
|
</div>
|
|
@@ -1366,16 +1454,25 @@ function fmtPoint(p: { value?: number, avg?: number } | undefined): string {
|
|
|
1366
1454
|
v-for="(p, i) in r.params"
|
|
1367
1455
|
:key="i"
|
|
1368
1456
|
class="param-chip"
|
|
1457
|
+
:class="{ stale: paramStatus(p.nodeId, r.lineId) }"
|
|
1458
|
+
:title="paramStatus(p.nodeId, r.lineId)?.label ?? ''"
|
|
1369
1459
|
>
|
|
1370
|
-
{{ (
|
|
1460
|
+
{{ paramNodeName(p.nodeId) }} = {{ p.value }}<span
|
|
1461
|
+
v-if="paramStatus(p.nodeId, r.lineId)"
|
|
1462
|
+
class="chip-stale-tag"
|
|
1463
|
+
>{{ paramStatus(p.nodeId, r.lineId)!.label }}</span>
|
|
1371
1464
|
</span>
|
|
1372
1465
|
<span
|
|
1373
1466
|
v-for="(w, i) in r.daqWindows"
|
|
1374
1467
|
:key="`w-${i}`"
|
|
1375
1468
|
class="param-chip daqwin"
|
|
1376
|
-
:
|
|
1469
|
+
:class="{ stale: daqWindowStatus(w.nodeId, r.lineId) }"
|
|
1470
|
+
:title="daqWindowStatus(w.nodeId, r.lineId)?.label ?? $t('dcwDetail.k1l11api013')"
|
|
1377
1471
|
>
|
|
1378
|
-
◎ {{ daqNodeCh(w.nodeId) }} ∈ [{{ w.min ?? '-∞' }}, {{ w.max ?? '+∞' }}]
|
|
1472
|
+
◎ {{ daqNodeCh(w.nodeId) }} ∈ [{{ w.min ?? '-∞' }}, {{ w.max ?? '+∞' }}]<span
|
|
1473
|
+
v-if="daqWindowStatus(w.nodeId, r.lineId)"
|
|
1474
|
+
class="chip-stale-tag"
|
|
1475
|
+
>{{ daqWindowStatus(w.nodeId, r.lineId)!.label }}</span>
|
|
1379
1476
|
</span>
|
|
1380
1477
|
</div>
|
|
1381
1478
|
<div class="recipe-actions">
|
|
@@ -1391,6 +1488,15 @@ function fmtPoint(p: { value?: number, avg?: number } | undefined): string {
|
|
|
1391
1488
|
>
|
|
1392
1489
|
{{ $t('dcwDetail.k45eb0072') }}
|
|
1393
1490
|
</button>
|
|
1491
|
+
<button
|
|
1492
|
+
class="mini-btn"
|
|
1493
|
+
@click="openRecipeHistory(r.id)"
|
|
1494
|
+
>
|
|
1495
|
+
{{ t('dcwDetail.histBtn') }}<span
|
|
1496
|
+
v-if="r.version"
|
|
1497
|
+
class="mono"
|
|
1498
|
+
>·v{{ r.version }}</span>
|
|
1499
|
+
</button>
|
|
1394
1500
|
<button
|
|
1395
1501
|
class="mini-btn danger"
|
|
1396
1502
|
@click="dcw.removeRecipe(r.id)"
|
|
@@ -1662,6 +1768,10 @@ function fmtPoint(p: { value?: number, avg?: number } | undefined): string {
|
|
|
1662
1768
|
{{ n.name }}({{ dcwTemplateRefCh(n.templateRef) }} · {{ n.min }}~{{ n.max }} {{ n.unit }})
|
|
1663
1769
|
</option>
|
|
1664
1770
|
</select>
|
|
1771
|
+
<span
|
|
1772
|
+
v-if="paramStatus(p.nodeId, lineId)"
|
|
1773
|
+
class="row-stale"
|
|
1774
|
+
>{{ paramStatus(p.nodeId, lineId)!.label }}</span>
|
|
1665
1775
|
</label>
|
|
1666
1776
|
<label class="f">
|
|
1667
1777
|
<span>{{ $t('dcwDetail.k3renp2098') }}<em>*</em></span>
|
|
@@ -1761,6 +1871,12 @@ function fmtPoint(p: { value?: number, avg?: number } | undefined): string {
|
|
|
1761
1871
|
>
|
|
1762
1872
|
{{ $t('dcwDetail.kv1de1p105') }}
|
|
1763
1873
|
</button>
|
|
1874
|
+
<p
|
|
1875
|
+
v-if="recipeStaleNote"
|
|
1876
|
+
class="m-note"
|
|
1877
|
+
>
|
|
1878
|
+
{{ recipeStaleNote }}
|
|
1879
|
+
</p>
|
|
1764
1880
|
<p
|
|
1765
1881
|
v-if="recipeError"
|
|
1766
1882
|
class="m-err"
|
|
@@ -1785,6 +1901,86 @@ function fmtPoint(p: { value?: number, avg?: number } | undefined): string {
|
|
|
1785
1901
|
</div>
|
|
1786
1902
|
</div>
|
|
1787
1903
|
|
|
1904
|
+
<!-- 配方版本历史(整体修改变更记录:来源/操作者/原因/参数 diff + 回退) -->
|
|
1905
|
+
<div
|
|
1906
|
+
v-if="verOpen"
|
|
1907
|
+
class="modal-mask"
|
|
1908
|
+
@click.self="verOpen = false"
|
|
1909
|
+
>
|
|
1910
|
+
<div
|
|
1911
|
+
class="modal"
|
|
1912
|
+
style="max-width: 760px;"
|
|
1913
|
+
>
|
|
1914
|
+
<div class="modal-head">
|
|
1915
|
+
<h3>
|
|
1916
|
+
{{ t('dcwDetail.histTitle') }}<span
|
|
1917
|
+
v-if="verRecipe"
|
|
1918
|
+
class="dim"
|
|
1919
|
+
style="margin-left: 8px; font-size: 12px;"
|
|
1920
|
+
>{{ verRecipe.name }} · v{{ verRecipe.version }}</span>
|
|
1921
|
+
</h3>
|
|
1922
|
+
<button
|
|
1923
|
+
class="mini-btn"
|
|
1924
|
+
@click="verOpen = false"
|
|
1925
|
+
>
|
|
1926
|
+
✕
|
|
1927
|
+
</button>
|
|
1928
|
+
</div>
|
|
1929
|
+
<p
|
|
1930
|
+
v-if="verMsg"
|
|
1931
|
+
class="banner good"
|
|
1932
|
+
style="margin: 6px 0;"
|
|
1933
|
+
>
|
|
1934
|
+
{{ verMsg }}
|
|
1935
|
+
</p>
|
|
1936
|
+
<p
|
|
1937
|
+
v-if="verLoading"
|
|
1938
|
+
class="dim"
|
|
1939
|
+
>
|
|
1940
|
+
{{ t('dcwDetail.histLoading') }}
|
|
1941
|
+
</p>
|
|
1942
|
+
<div
|
|
1943
|
+
v-else-if="verRows.length === 0"
|
|
1944
|
+
class="pane-empty"
|
|
1945
|
+
>
|
|
1946
|
+
{{ t('dcwDetail.histEmpty') }}
|
|
1947
|
+
</div>
|
|
1948
|
+
<div
|
|
1949
|
+
v-for="(row, i) in verRows"
|
|
1950
|
+
v-else
|
|
1951
|
+
:key="row.version"
|
|
1952
|
+
class="ver-row"
|
|
1953
|
+
:class="{ cur: row.current }"
|
|
1954
|
+
>
|
|
1955
|
+
<div class="ver-head">
|
|
1956
|
+
<span class="mono ver-v">v{{ row.version }}</span>
|
|
1957
|
+
<span
|
|
1958
|
+
class="ver-src"
|
|
1959
|
+
:class="row.by ?? (row.current ? 'cur' : '')"
|
|
1960
|
+
>{{ row.current ? t('dcwDetail.histCurrent') : verSrcLabel(row.by) }}</span>
|
|
1961
|
+
<span class="mono dim">{{ row.at.slice(0, 19).replace('T', ' ') }}</span>
|
|
1962
|
+
<span class="ver-actor">{{ row.actorName || '—' }}</span>
|
|
1963
|
+
<button
|
|
1964
|
+
v-if="!row.current"
|
|
1965
|
+
class="mini-btn"
|
|
1966
|
+
@click="doRevert(row.version)"
|
|
1967
|
+
>
|
|
1968
|
+
{{ t('dcwDetail.revertTo') }}
|
|
1969
|
+
</button>
|
|
1970
|
+
</div>
|
|
1971
|
+
<p
|
|
1972
|
+
v-if="row.description"
|
|
1973
|
+
class="ver-desc"
|
|
1974
|
+
>
|
|
1975
|
+
{{ row.description }}
|
|
1976
|
+
</p>
|
|
1977
|
+
<p class="ver-diff mono">
|
|
1978
|
+
{{ verDiff(row, i) }}
|
|
1979
|
+
</p>
|
|
1980
|
+
</div>
|
|
1981
|
+
</div>
|
|
1982
|
+
</div>
|
|
1983
|
+
|
|
1788
1984
|
<!-- 产线数据查询(产品/配方/参数/时间/间隔) -->
|
|
1789
1985
|
<section class="aw-tile query-card">
|
|
1790
1986
|
<p class="sec-label">
|
|
@@ -2282,6 +2478,19 @@ h1 { margin: 2px 0 4px; font-size: 30px; font-weight: 400; letter-spacing: -0.01
|
|
|
2282
2478
|
.recipe-params { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0; }
|
|
2283
2479
|
.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
2480
|
.recipe-actions { display: flex; gap: 6px; align-items: center; margin-top: 8px; }
|
|
2481
|
+
/* ---------- 配方版本历史 ---------- */
|
|
2482
|
+
.ver-row { padding: 10px 12px; margin-bottom: 8px; border: 1px solid var(--divider-hair); border-radius: 10px; }
|
|
2483
|
+
.ver-row.cur { background: color-mix(in srgb, var(--accent) 6%, transparent); border-color: color-mix(in srgb, var(--accent) 30%, transparent); }
|
|
2484
|
+
.ver-head { display: flex; gap: 10px; align-items: center; }
|
|
2485
|
+
.ver-v { font-weight: 700; font-size: 13px; }
|
|
2486
|
+
.ver-src { padding: 1px 8px; font-size: 10.5px; border: 1px solid var(--line-strong); border-radius: var(--radius-pill); }
|
|
2487
|
+
.ver-src.agent { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 50%, transparent); }
|
|
2488
|
+
.ver-src.user { color: var(--ink-soft); }
|
|
2489
|
+
.ver-src.system { color: var(--ink-faint); }
|
|
2490
|
+
.ver-src.cur { color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, transparent); border-color: transparent; }
|
|
2491
|
+
.ver-actor { font-family: var(--font-mono); font-size: 12px; color: var(--ink-soft); }
|
|
2492
|
+
.ver-desc { margin: 6px 0 0; font-size: 12px; color: var(--ink); }
|
|
2493
|
+
.ver-diff { margin: 4px 0 0; font-size: 11px; color: var(--ink-faint); }
|
|
2285
2494
|
.sec-label { margin: 14px 0 6px; font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-faint); }
|
|
2286
2495
|
.runs { margin-top: 6px; }
|
|
2287
2496
|
.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); }
|
|
@@ -2307,6 +2516,11 @@ h1 { margin: 2px 0 4px; font-size: 30px; font-weight: 400; letter-spacing: -0.01
|
|
|
2307
2516
|
.test-result.good { color: var(--tone-success-dot); }
|
|
2308
2517
|
.test-result.bad { color: var(--tone-danger-dot); }
|
|
2309
2518
|
.m-err { margin: 8px 0 0; font-size: 12px; color: var(--tone-danger-dot); }
|
|
2519
|
+
.m-note { margin: 8px 0 0; font-size: 12px; color: var(--ink-faint); }
|
|
2520
|
+
/* 失效配方参数:灰化 + 徽标 */
|
|
2521
|
+
.param-chip.stale { color: var(--ink-faint); background: color-mix(in srgb, var(--ink) 5%, transparent); border-color: var(--divider-hair); text-decoration: line-through; text-decoration-color: color-mix(in srgb, var(--ink-faint) 60%, transparent); }
|
|
2522
|
+
.chip-stale-tag { margin-left: 5px; padding: 0 5px; font-size: 9.5px; font-style: normal; color: var(--tone-danger-dot); background: color-mix(in srgb, var(--tone-danger-dot) 8%, transparent); border: 1px solid color-mix(in srgb, var(--tone-danger-dot) 30%, transparent); border-radius: var(--radius-chip); text-decoration: none; }
|
|
2523
|
+
.row-stale { flex: none; padding: 1px 7px; font-size: 10.5px; color: var(--tone-danger-dot); border: 1px solid color-mix(in srgb, var(--tone-danger-dot) 30%, transparent); border-radius: var(--radius-chip); }
|
|
2310
2524
|
.m-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 16px; }
|
|
2311
2525
|
.aw-pill.outline { color: var(--ink); background: var(--paper-raised); border-color: var(--line-strong); }
|
|
2312
2526
|
.err { margin-top: 14px; font-size: 13px; color: var(--tone-danger-dot); }
|
package/i18n/locales/en.ts
CHANGED
|
@@ -840,6 +840,23 @@ export default {
|
|
|
840
840
|
k1bphrb3048: 'Delete failed',
|
|
841
841
|
},
|
|
842
842
|
dcwDetail: {
|
|
843
|
+
staleDeleted: 'deleted',
|
|
844
|
+
staleDisabled: 'disabled',
|
|
845
|
+
staleUnbound: 'unbound',
|
|
846
|
+
staleDropped: 'Removed {p0} stale param(s) (node deleted); re-add with a live node if needed',
|
|
847
|
+
histBtn: 'History',
|
|
848
|
+
histTitle: 'Recipe Version History',
|
|
849
|
+
histLoading: 'Loading…',
|
|
850
|
+
histEmpty: 'No version history yet (param edits are recorded automatically)',
|
|
851
|
+
histCurrent: 'Current',
|
|
852
|
+
histInit: 'Initial version',
|
|
853
|
+
histAdded: 'added',
|
|
854
|
+
histNoChange: 'No param changes',
|
|
855
|
+
revertTo: 'Revert to',
|
|
856
|
+
revertOk: 'Reverted; new version v{p0} created',
|
|
857
|
+
revertReasonUi: 'UI revert to history version',
|
|
858
|
+
srcUser: 'User',
|
|
859
|
+
srcSystem: 'System',
|
|
843
860
|
k9r7d4e016: 'Poll read (ms)',
|
|
844
861
|
k9r7d4e017: 'Default 5000ms; 0 = manual read only',
|
|
845
862
|
k9r7d4e018: 'PLC reading',
|
package/i18n/locales/zh-CN.ts
CHANGED
|
@@ -839,6 +839,23 @@ export default {
|
|
|
839
839
|
k1bphrb3048: '删除失败',
|
|
840
840
|
},
|
|
841
841
|
dcwDetail: {
|
|
842
|
+
staleDeleted: '已删除',
|
|
843
|
+
staleDisabled: '已停用',
|
|
844
|
+
staleUnbound: '已取消绑定',
|
|
845
|
+
staleDropped: '已自动移除 {p0} 条失效参数(节点已删除);如需恢复请重新添加有效节点',
|
|
846
|
+
histBtn: '版本历史',
|
|
847
|
+
histTitle: '配方版本历史',
|
|
848
|
+
histLoading: '加载中…',
|
|
849
|
+
histEmpty: '暂无版本历史(配方参数修改后自动入史)',
|
|
850
|
+
histCurrent: '当前',
|
|
851
|
+
histInit: '初始版本',
|
|
852
|
+
histAdded: '新增',
|
|
853
|
+
histNoChange: '参数未变',
|
|
854
|
+
revertTo: '回退到此版',
|
|
855
|
+
revertOk: '已回退并生成新版本 v{p0}',
|
|
856
|
+
revertReasonUi: '界面回退到历史版本',
|
|
857
|
+
srcUser: '用户',
|
|
858
|
+
srcSystem: '系统',
|
|
842
859
|
k9r7d4e016: '周期读(ms)',
|
|
843
860
|
k9r7d4e017: '缺省 5000ms;0 = 仅手动读取',
|
|
844
861
|
k9r7d4e018: 'PLC 读数',
|
package/package.json
CHANGED
|
@@ -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
|
+
})
|