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.
- 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 → BJem8_v5.js} +4 -4
- package/.output/public/_nuxt/{6vYobj4F.js → BWC4HQHQ.js} +1 -1
- 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/DtM4RX5T.js +1 -0
- 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_.pF-B9O6u.css +1 -0
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/6a0a88de-639e-4e7b-b7c1-7f4034e04787.json +1 -0
- package/.output/server/chunks/_/nitro.mjs +1087 -746
- 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_-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/{_id_-D265Wh_k.mjs → _id_-DvJZ5Qis.mjs} +168 -120
- package/.output/server/chunks/build/_id_-DvJZ5Qis.mjs.map +1 -0
- 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 +34 -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 +165 -1
- package/i18n/locales/en.ts +13 -0
- package/i18n/locales/zh-CN.ts +13 -0
- package/package.json +1 -1
- package/scripts/_dbg-recipe-e2e.mjs +176 -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/dcw-products.json +7 -0
- package/server/data/dcw-rollback.json +9 -9
- package/server/data/dcws.json +222 -222
- package/server/services/workshop/agents/host-tool-bridge.ts +21 -1
- package/server/services/workshop/agents/industrial-tools.ts +222 -4
- package/server/services/workshop/daq/drivers.ts +16 -3
- package/server/services/workshop/dcw/dcw-controller.ts +50 -2
- package/server/services/workshop/dcw/dcw-recipe.repo.ts +81 -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
|
@@ -21,7 +21,7 @@ import type { WorkspaceTask } from '../types/task'
|
|
|
21
21
|
import type { RpcHostToolDefinition } from './adapters/omp-rpc-client'
|
|
22
22
|
import { loadHostToolDefs } from '../prompts/loader'
|
|
23
23
|
import { daqRuntimeSettings } from '../settings'
|
|
24
|
-
import { toolDaqFrames, toolDaqQuery, toolDcwControl, toolDcwJudge, toolDcwJournal, toolDcwRead, toolDcwRollback, toolMyIndustrialNodes, toolOpsLog, toolRecipeLog } from './industrial-tools'
|
|
24
|
+
import { toolDaqFrames, toolDaqQuery, toolDcwControl, toolDcwJudge, toolDcwJournal, toolDcwRead, toolDcwRollback, toolLineContext, toolMyIndustrialNodes, toolOpsLog, toolRecipeLog, toolRecipeRollback, toolRecipeUpdate, toolRecipeVersions } from './industrial-tools'
|
|
25
25
|
import { listPluginTools } from './plugin-tools'
|
|
26
26
|
import { extractTaskMode } from '../runtime/execution-mode'
|
|
27
27
|
|
|
@@ -162,6 +162,14 @@ export async function dispatchHostTool(ctx: HostToolBridgeContext, req: HostTool
|
|
|
162
162
|
return toolOpsLog(identity.agentId, args as Parameters<typeof toolOpsLog>[1])
|
|
163
163
|
case 'recipe_log':
|
|
164
164
|
return toolRecipeLog(identity.agentId, args as Parameters<typeof toolRecipeLog>[1])
|
|
165
|
+
case 'line_context':
|
|
166
|
+
return toolLineContext(identity.agentId, args as { line_id?: string })
|
|
167
|
+
case 'recipe_versions':
|
|
168
|
+
return toolRecipeVersions(identity.agentId, args as { recipe_id?: string, limit?: number | string })
|
|
169
|
+
case 'recipe_update':
|
|
170
|
+
return toolRecipeUpdate(identity.agentId, args as Parameters<typeof toolRecipeUpdate>[1])
|
|
171
|
+
case 'recipe_rollback':
|
|
172
|
+
return toolRecipeRollback(identity.agentId, args as Parameters<typeof toolRecipeRollback>[1])
|
|
165
173
|
}
|
|
166
174
|
|
|
167
175
|
const ws = ctx.getWorkspace()
|
|
@@ -610,6 +618,18 @@ export async function dispatchHostTool(ctx: HostToolBridgeContext, req: HostTool
|
|
|
610
618
|
case 'recipe_log': {
|
|
611
619
|
return toolRecipeLog(identity.agentId, args as Parameters<typeof toolRecipeLog>[1])
|
|
612
620
|
}
|
|
621
|
+
case 'line_context': {
|
|
622
|
+
return toolLineContext(identity.agentId, args as { line_id?: string })
|
|
623
|
+
}
|
|
624
|
+
case 'recipe_versions': {
|
|
625
|
+
return toolRecipeVersions(identity.agentId, args as { recipe_id?: string, limit?: number | string })
|
|
626
|
+
}
|
|
627
|
+
case 'recipe_update': {
|
|
628
|
+
return toolRecipeUpdate(identity.agentId, args as Parameters<typeof toolRecipeUpdate>[1])
|
|
629
|
+
}
|
|
630
|
+
case 'recipe_rollback': {
|
|
631
|
+
return toolRecipeRollback(identity.agentId, args as Parameters<typeof toolRecipeRollback>[1])
|
|
632
|
+
}
|
|
613
633
|
}
|
|
614
634
|
return { text: `未知工具: ${req.toolName}`, isError: true }
|
|
615
635
|
}
|
|
@@ -20,6 +20,9 @@ import { getDaqNodeRepo } from '../daq/daq-node.repo'
|
|
|
20
20
|
import { findDaqTemplate } from '../daq/daq-templates'
|
|
21
21
|
import { getRecipeRollBackManager } from '../dcw/recipe-rollback-manager'
|
|
22
22
|
import { getOps } from '../ops/ops'
|
|
23
|
+
import { getDcwLineRepo } from '../dcw/dcw-line.repo'
|
|
24
|
+
import { getDcwProductRepo } from '../dcw/dcw-product.repo'
|
|
25
|
+
import { getDcwRecipeRepo } from '../dcw/dcw-recipe.repo'
|
|
23
26
|
|
|
24
27
|
export async function toolMyIndustrialNodes(agentId: string): Promise<{ text: string }> {
|
|
25
28
|
const repo = getAgentNodeBindingRepo()
|
|
@@ -68,10 +71,13 @@ export async function toolMyIndustrialNodes(agentId: string): Promise<{ text: st
|
|
|
68
71
|
5. 调控闭环:每次下发自动开一条优化记录(open);观察数采后用 dcw_judge 落判定(keep/rollback/uncertain);
|
|
69
72
|
判 rollback 后用 dcw_rollback 执行回退;dcw_journal 可查节点参数变更史。未判定前再下发,旧记录会被标记 superseded。
|
|
70
73
|
若节点被他人的 open 记录阻塞(对方已消失),超时(30 分钟)后你可接管:dcw_judge 会带接管标记入册。
|
|
71
|
-
6. 自查面:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
6. 自查面:line_context 看你控制的产线/产品/配方全景(动手前必读,确认归属);
|
|
75
|
+
ops_log 查你负责产线的运维日志(谁/何时做了什么,来源区分 Agent/用户/系统;
|
|
76
|
+
参数 line_id/node_id/kind/actor_kind/minutes/limit/mine);recipe_log 查配方下发与回退事件;
|
|
77
|
+
recipe_versions 查配方参数版本史(谁改的/为什么/参数 diff)。
|
|
78
|
+
7. 配方操控闭环(在线优化框架):验证过参数更优 → recipe_update 保存进配方(记录你的名字与原因,
|
|
79
|
+
生成新版本);发现优化有问题 → recipe_rollback 回退到稳定版本(version)或已知良好批次
|
|
80
|
+
(to_last_good=true);运行中 PLC 当前值用 dcw_control 下发,配方定义供后续批次生效。`,
|
|
75
81
|
}
|
|
76
82
|
}
|
|
77
83
|
|
|
@@ -616,3 +622,215 @@ export async function toolRecipeLog(agentId: string, args: {
|
|
|
616
622
|
text: `Recipe 变更史(${lineId ? `产线 ${lineId}` : `负责产线 ${scope.lineIds.length} 条`},近 ${minutes} 分钟,${rows.length} 条,新→旧):\n${body.join('\n')}\n\n说明:配方下发=整批参数写命令;优化开窗=单次设定变更(含 Agent 假设);回退执行=已恢复基线值。节点级参数值逐笔历史用 dcw_journal(node_id)。`,
|
|
617
623
|
}
|
|
618
624
|
}
|
|
625
|
+
|
|
626
|
+
// ================================================================
|
|
627
|
+
// 产线上下文 + Recipe 版本管理(Agent 操控闭环:知道改什么 → 保存 → 可回退)
|
|
628
|
+
// ================================================================
|
|
629
|
+
|
|
630
|
+
/** 工具:line_context —— 我控制的产线/产品/Recipe 全景(归属认知 + 完整参数面)。
|
|
631
|
+
* 逐产线输出:产线名/运行态、活动批次(产品/配方/版本/参数)、我的节点绑定、
|
|
632
|
+
* 配方目标 vs PLC 当前值对照、lastGood 批次。 */
|
|
633
|
+
export async function toolLineContext(agentId: string, args: { line_id?: string } = {}): Promise<{ text: string, isError?: boolean }> {
|
|
634
|
+
const scope = agentOpsScope(agentId)
|
|
635
|
+
if (!scope) return { text: '你尚未绑定任何工业节点,暂无产线上下文(请先在数字孪生界面绑定节点)。' }
|
|
636
|
+
const wanted = String(args.line_id ?? '').trim()
|
|
637
|
+
const lineIds = wanted ? [wanted] : scope.lineIds
|
|
638
|
+
if (wanted && !scope.lineIds.includes(wanted)) {
|
|
639
|
+
return { text: `产线 ${wanted} 不在你的负责范围(你绑定节点覆盖的产线:${scope.lineIds.join(', ') || '无'})。`, isError: true }
|
|
640
|
+
}
|
|
641
|
+
if (lineIds.length === 0) return { text: '你绑定的节点均未分配产线(未挂线的节点没有产线/产品/配方上下文)。' }
|
|
642
|
+
|
|
643
|
+
const sections: string[] = []
|
|
644
|
+
for (const lid of lineIds) {
|
|
645
|
+
const line = getDcwLineRepo().byId(lid)
|
|
646
|
+
const run = getActiveLineRun(lid)
|
|
647
|
+
const recipe = run?.recipeId ? getDcwController().listRecipes().find(r => r.id === run.recipeId) : undefined
|
|
648
|
+
const parts: string[] = []
|
|
649
|
+
parts.push(`■ 产线 ${line?.name ?? lid}(${lid})· 状态:${run ? '运行中' : '待机/停线'}`)
|
|
650
|
+
if (run) {
|
|
651
|
+
parts.push(` 活动批次 ${run.runId.slice(0, 8)} · 产品「${run.productName}」(${run.productId}) · 开跑 ${run.startedAt.slice(11, 19)} · 已打标 ${run.taggedSamples} 样本`)
|
|
652
|
+
}
|
|
653
|
+
const myDcw = getAgentNodeBindingRepo().byAgent(agentId)
|
|
654
|
+
.filter(b => b.kind === 'dcw')
|
|
655
|
+
.map(b => getDcwController().byId(b.nodeId))
|
|
656
|
+
.filter(n => n?.lineId === lid)
|
|
657
|
+
const myDaq = getAgentNodeBindingRepo().byAgent(agentId)
|
|
658
|
+
.filter(b => b.kind === 'daq')
|
|
659
|
+
.map(b => getDaqNodeRepo().byId(b.nodeId))
|
|
660
|
+
.filter(n => n?.lineId === lid)
|
|
661
|
+
parts.push(` 我绑定的节点:数控 [${myDcw.map(n => n!.name).join(', ') || '无'}];数采 [${myDaq.map(n => n!.name).join(', ') || '无'}]`)
|
|
662
|
+
if (recipe) {
|
|
663
|
+
parts.push(` 配方「${recipe.name}」(${recipe.id}) v${recipe.version ?? 1}${recipe.description ? ` — ${recipe.description}` : ''}`)
|
|
664
|
+
for (const p of recipe.params) {
|
|
665
|
+
const node = getDcwController().byId(p.nodeId)
|
|
666
|
+
const mine = myDcw.some(n => n!.id === p.nodeId)
|
|
667
|
+
const cur = node?.value != null ? node.value : '?'
|
|
668
|
+
const win = p.min != null || p.max != null ? `(窗口 ${p.min ?? '-∞'}~${p.max ?? '+∞'})` : ''
|
|
669
|
+
parts.push(` · ${node?.name ?? p.nodeId} = ${p.value}${node?.unit ?? ''}${win} | PLC 当前 ${cur}${node?.unit ?? ''}${mine ? ' [我负责]' : ''}`)
|
|
670
|
+
}
|
|
671
|
+
const good = recipe.lastGoodRunId ? getDcwRecipeRepo().runById(recipe.lastGoodRunId) : undefined
|
|
672
|
+
parts.push(` 已知良好批次:${good ? `${good.id.slice(0, 8)}(${good.startedAt.slice(0, 19).replace('T', ' ')})` : '未标记'};当前参数版本 v${recipe.version ?? 1}`)
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
// 无活动批次:列出该产线的配方定义(产线未开跑也有配方上下文)
|
|
676
|
+
const lineRecipes = getDcwController().listRecipes().filter(r => r.lineId === lid).slice(0, 3)
|
|
677
|
+
if (lineRecipes.length === 0) {
|
|
678
|
+
parts.push(' 当前无活动配方(产线未开跑;该产线也暂无配方定义)')
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
parts.push(` 当前无活动批次;产线已有 ${lineRecipes.length} 个配方定义(开跑时绑定):`)
|
|
682
|
+
for (const r of lineRecipes) {
|
|
683
|
+
const product = r.productId ? getDcwProductRepo().byId(r.productId) : undefined
|
|
684
|
+
const pp = r.params.map((p) => {
|
|
685
|
+
const node = getDcwController().byId(p.nodeId)
|
|
686
|
+
const cur = node?.value != null ? node.value : '?'
|
|
687
|
+
return `${node?.name ?? p.nodeId}=${p.value}${node?.unit ?? ''}(PLC 当前 ${cur})`
|
|
688
|
+
}).join(', ')
|
|
689
|
+
parts.push(` · 配方「${r.name}」(${r.id}) v${r.version ?? 1} · 产品「${product?.name ?? r.productId}」: ${pp}${r.lastGoodRunId ? ` [良好批次 ${r.lastGoodRunId.slice(0, 8)}]` : ''}`)
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
sections.push(parts.join('\n'))
|
|
694
|
+
}
|
|
695
|
+
return {
|
|
696
|
+
text: `你控制的产线全景(${sections.length} 条):\n\n${sections.join('\n\n')}\n\n说明:配方目标=开跑批次冻结的工艺窗口;PLC 当前值=实时读数。把验证过的最佳参数固化到配方用 recipe_update;回退配方到稳定版本用 recipe_rollback;逐节点参数变更史用 dcw_journal;配方版本史用 recipe_versions。`,
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/** 工具:recipe_versions —— 配方参数版本史(谁/何时/为什么改了什么;旧→新)。
|
|
701
|
+
* 每条含 by(user/agent/system)、操作者人话名、变更描述、参数 diff。 */
|
|
702
|
+
export async function toolRecipeVersions(agentId: string, args: { recipe_id?: string, limit?: number | string }): Promise<{ text: string, isError?: boolean }> {
|
|
703
|
+
const recipeId = String(args.recipe_id ?? '').trim()
|
|
704
|
+
if (!recipeId) return { text: 'recipe_id 必填(line_context 可看到当前配方的 id)。', isError: true }
|
|
705
|
+
const recipe = getDcwController().listRecipes().find(r => r.id === recipeId)
|
|
706
|
+
if (!recipe) return { text: `配方 ${recipeId} 不存在。`, isError: true }
|
|
707
|
+
const scope = agentOpsScope(agentId)
|
|
708
|
+
if (!scope || !recipe.lineId || !scope.lineIds.includes(recipe.lineId)) {
|
|
709
|
+
return { text: `无权查看配方 ${recipeId} 的版本史(该配方不在你负责的产线上)。`, isError: true }
|
|
710
|
+
}
|
|
711
|
+
const rows = getDcwController().recipeVersions(recipeId)
|
|
712
|
+
const limit = Math.min(Number(args.limit) || 20, 50)
|
|
713
|
+
const shown = rows.slice(-limit)
|
|
714
|
+
const srcLabel: Record<string, string> = { user: '用户', agent: 'Agent', system: '系统' }
|
|
715
|
+
const lines = shown.map((v, i) => {
|
|
716
|
+
const prev = shown[i - 1]
|
|
717
|
+
let diff = '初始版本'
|
|
718
|
+
if (prev) {
|
|
719
|
+
const changed = v.params
|
|
720
|
+
.map(p => ({ p, old: prev.params.find(x => x.nodeId === p.nodeId) }))
|
|
721
|
+
.filter(({ p, old }) => !old || old.value !== p.value)
|
|
722
|
+
.map(({ p, old }) => `${getDcwController().byId(p.nodeId)?.name ?? p.nodeId} ${old?.value ?? '新增'}→${p.value}`)
|
|
723
|
+
const removed = prev.params.filter(o => !v.params.some(x => x.nodeId === o.nodeId)).map(o => `${o.nodeId} 移除`)
|
|
724
|
+
const all = [...changed, ...removed]
|
|
725
|
+
diff = all.length > 0 ? `变更:${all.join(';')}` : '参数未变'
|
|
726
|
+
}
|
|
727
|
+
const src = v.by ? (srcLabel[v.by] ?? v.by) : '—'
|
|
728
|
+
return `- v${v.version} [${v.at.slice(0, 19).replace('T', ' ')}] 来源=${src} 操作者=${v.actorName ?? '—'}${v.description ? ` | ${v.description}` : ''}\n ${diff}`
|
|
729
|
+
})
|
|
730
|
+
return {
|
|
731
|
+
text: `配方「${recipe.name}」版本史(当前 v${recipe.version ?? 1},共 ${rows.length} 条,旧→新):\n${lines.join('\n')}\n\n回退用 recipe_rollback(recipe_id + version 或 to_last_good=true);保存新参数用 recipe_update。`,
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/** 工具:recipe_update —— 把验证过的最佳参数保存进配方(生成新版本,带归因与原因)。
|
|
736
|
+
* 只改配方定义(下一批次生效);不改运行中 PLC 当前值(那用 dcw_control 逐节点下发)。 */
|
|
737
|
+
export async function toolRecipeUpdate(agentId: string, args: {
|
|
738
|
+
recipe_id?: string
|
|
739
|
+
params?: Array<{ node_id?: string, value?: number | string, min?: number | string, max?: number | string }>
|
|
740
|
+
reason?: string
|
|
741
|
+
task_id?: string
|
|
742
|
+
}): Promise<{ text: string, isError?: boolean }> {
|
|
743
|
+
const recipeId = String(args.recipe_id ?? '').trim()
|
|
744
|
+
const reason = String(args.reason ?? '').trim()
|
|
745
|
+
if (!recipeId) return { text: 'recipe_id 必填(line_context 可看到当前配方的 id)。', isError: true }
|
|
746
|
+
if (!reason) return { text: 'reason 必填:保存参数必须说明依据(如数采证据/判定结论),便于版本史追溯。', isError: true }
|
|
747
|
+
const list = (args.params ?? []).filter(p => p && String(p.node_id ?? '').trim() && Number.isFinite(Number(p.value)))
|
|
748
|
+
if (list.length === 0) return { text: 'params 至少提供一条 {node_id, value}(value 必须为数字)。', isError: true }
|
|
749
|
+
const recipe = getDcwController().listRecipes().find(r => r.id === recipeId)
|
|
750
|
+
if (!recipe) return { text: `配方 ${recipeId} 不存在。`, isError: true }
|
|
751
|
+
const repo = getAgentNodeBindingRepo()
|
|
752
|
+
for (const p of list) {
|
|
753
|
+
const nodeId = String(p.node_id).trim()
|
|
754
|
+
const node = getDcwController().byId(nodeId)
|
|
755
|
+
if (!node) return { text: `节点 ${nodeId} 不存在。`, isError: true }
|
|
756
|
+
if (node.lineId !== recipe.lineId) return { text: `节点「${node.name}」不属于配方「${recipe.name}」所在产线,不可混入。`, isError: true }
|
|
757
|
+
if (!repo.find(agentId, nodeId, 'dcw')) {
|
|
758
|
+
return { text: `无权修改节点「${node.name}」的配方参数(仅可改自己绑定 dcw 的节点)。`, isError: true }
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
// 部分合并:只更新提供的节点,其余参数保持不变
|
|
762
|
+
const merged = recipe.params.map((p) => {
|
|
763
|
+
const patch = list.find(x => String(x.node_id).trim() === p.nodeId)
|
|
764
|
+
if (!patch) return { nodeId: p.nodeId, templateRef: p.templateRef, value: p.value, min: p.min, max: p.max }
|
|
765
|
+
const out: { nodeId: string, templateRef?: string, value: number, min?: number, max?: number } = { nodeId: p.nodeId, templateRef: p.templateRef, value: Number(patch.value) }
|
|
766
|
+
if (p.min != null) out.min = p.min
|
|
767
|
+
if (patch.min != null && Number.isFinite(Number(patch.min))) out.min = Number(patch.min)
|
|
768
|
+
if (p.max != null) out.max = p.max
|
|
769
|
+
if (patch.max != null && Number.isFinite(Number(patch.max))) out.max = Number(patch.max)
|
|
770
|
+
return out
|
|
771
|
+
})
|
|
772
|
+
const extra = list.filter(p => !recipe.params.some(x => x.nodeId === String(p.node_id).trim()))
|
|
773
|
+
for (const p of extra) merged.push({ nodeId: String(p.node_id).trim(), value: Number(p.value) })
|
|
774
|
+
try {
|
|
775
|
+
const updated = getDcwController().updateRecipe(recipeId, { params: merged }, {
|
|
776
|
+
by: 'agent',
|
|
777
|
+
actorName: agentBadgeLabel(agentId),
|
|
778
|
+
actor: agentId,
|
|
779
|
+
description: reason,
|
|
780
|
+
})
|
|
781
|
+
const changedNodes = list.map((p) => {
|
|
782
|
+
const node = getDcwController().byId(String(p.node_id).trim())
|
|
783
|
+
const before = recipe.params.find(x => x.nodeId === String(p.node_id).trim())?.value
|
|
784
|
+
return `${node?.name ?? p.node_id} ${before ?? '?'}→${Number(p.value)}`
|
|
785
|
+
}).join(';')
|
|
786
|
+
return {
|
|
787
|
+
text: `已保存为 v${updated.version ?? 1}:「${updated.name}」参数 ${changedNodes};原因:${reason}。\n注意:配方定义已更新,运行中批次仍按开跑时冻结的参数生产,新参数从下次开跑/一键下发生效;要把新值写入运行中的 PLC,用 dcw_control 逐节点下发(走安全联锁)。回退用 recipe_rollback。`,
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
catch (err) {
|
|
791
|
+
return { text: `保存失败:${err instanceof Error ? err.message : String(err)}`, isError: true }
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/** 工具:recipe_rollback —— 回退配方参数到稳定版本(指定历史版本或已知良好批次快照)。
|
|
796
|
+
* 生成新版本(非破坏,历史保留);不改运行中 PLC 当前值。 */
|
|
797
|
+
export async function toolRecipeRollback(agentId: string, args: {
|
|
798
|
+
recipe_id?: string
|
|
799
|
+
version?: number | string
|
|
800
|
+
to_last_good?: boolean | string
|
|
801
|
+
reason?: string
|
|
802
|
+
}): Promise<{ text: string, isError?: boolean }> {
|
|
803
|
+
const recipeId = String(args.recipe_id ?? '').trim()
|
|
804
|
+
const reason = String(args.reason ?? '').trim()
|
|
805
|
+
const toLastGood = args.to_last_good === true || args.to_last_good === 'true'
|
|
806
|
+
const version = Number(args.version)
|
|
807
|
+
if (!recipeId) return { text: 'recipe_id 必填。', isError: true }
|
|
808
|
+
if (!reason) return { text: 'reason 必填:回退必须说明原因(如优化翻车/越限),便于版本史追溯。', isError: true }
|
|
809
|
+
if (!toLastGood && !Number.isFinite(version)) return { text: '需提供 version(历史版本号)或 to_last_good=true。', isError: true }
|
|
810
|
+
const recipe = getDcwController().listRecipes().find(r => r.id === recipeId)
|
|
811
|
+
if (!recipe) return { text: `配方 ${recipeId} 不存在。`, isError: true }
|
|
812
|
+
const scope = agentOpsScope(agentId)
|
|
813
|
+
if (!scope || !recipe.lineId || !scope.lineIds.includes(recipe.lineId)) {
|
|
814
|
+
return { text: `无权回退配方 ${recipeId}(该配方不在你负责的产线上)。`, isError: true }
|
|
815
|
+
}
|
|
816
|
+
if (!toLastGood && Number.isFinite(version) && version >= (recipe.version ?? 1)) {
|
|
817
|
+
return { text: `不能回退到 v${version}(当前已是 v${recipe.version ?? 1};回退目标是更早的版本)。`, isError: true }
|
|
818
|
+
}
|
|
819
|
+
try {
|
|
820
|
+
const updated = getDcwController().revertRecipe(recipeId, {
|
|
821
|
+
version: toLastGood ? undefined : version,
|
|
822
|
+
toLastGood,
|
|
823
|
+
}, {
|
|
824
|
+
by: 'agent',
|
|
825
|
+
actorName: agentBadgeLabel(agentId),
|
|
826
|
+
actor: agentId,
|
|
827
|
+
description: reason,
|
|
828
|
+
})
|
|
829
|
+
return {
|
|
830
|
+
text: `回退完成:配方「${updated.name}」已生成 v${updated.version ?? 1},参数恢复为目标版本(${toLastGood ? '已知良好批次冻结' : `v${version}`});原因:${reason}。\n运行中批次不受影响(仍按开跑冻结参数);新参数下次开跑生效。版本史用 recipe_versions 复核。`,
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
catch (err) {
|
|
834
|
+
return { text: `回退失败:${err instanceof Error ? err.message : String(err)}`, isError: true }
|
|
835
|
+
}
|
|
836
|
+
}
|
|
@@ -228,6 +228,19 @@ export function evictModbusConn(cfg: Record<string, unknown>, transport: ModbusT
|
|
|
228
228
|
modbusPool.delete(key)
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
/**
|
|
232
|
+
* 安全关闭:半开 socket(对端断电)时 modbus-serial close() 等待 FIN 永不返回,
|
|
233
|
+
* 一律 500ms 超时保护(fire-and-forget 底层 close,调用方不阻塞)。
|
|
234
|
+
*/
|
|
235
|
+
export async function closeModbusSafely(client: import('modbus-serial').ModbusRTU): Promise<void> {
|
|
236
|
+
await Promise.race([
|
|
237
|
+
Promise.resolve()
|
|
238
|
+
.then(() => client.close())
|
|
239
|
+
.catch(() => { /* 已断 */ }),
|
|
240
|
+
new Promise((r) => setTimeout(r, 500)),
|
|
241
|
+
])
|
|
242
|
+
}
|
|
243
|
+
|
|
231
244
|
/**
|
|
232
245
|
* 连接级排队执行:操作串行入队,完成后链路归还。
|
|
233
246
|
* 数控写入等当前采样读完成再执行(不再 409 快速失败);数采采样忙时仍跳帧让路防堆积。
|
|
@@ -272,7 +285,7 @@ export async function getModbusConn(cfg: Record<string, unknown>, transport: Mod
|
|
|
272
285
|
if (existing && existing.client.isOpen) return existing
|
|
273
286
|
if (existing) {
|
|
274
287
|
try {
|
|
275
|
-
await existing.client
|
|
288
|
+
await closeModbusSafely(existing.client)
|
|
276
289
|
}
|
|
277
290
|
catch { /* 已断 */ }
|
|
278
291
|
modbusPool.delete(key)
|
|
@@ -425,7 +438,7 @@ export const modbusTcpDriver: DaqDriver = {
|
|
|
425
438
|
const key = modbusKey(driverConfig)
|
|
426
439
|
const conn = modbusPool.get(key)
|
|
427
440
|
if (conn) {
|
|
428
|
-
await conn.client
|
|
441
|
+
await closeModbusSafely(conn.client)
|
|
429
442
|
modbusPool.delete(key)
|
|
430
443
|
}
|
|
431
444
|
}
|
|
@@ -663,7 +676,7 @@ export const modbusRtuDriver: DaqDriver = {
|
|
|
663
676
|
try {
|
|
664
677
|
const conn = modbusPool.get(modbusKey(driverConfig, 'rtu-tcp'))
|
|
665
678
|
if (conn) {
|
|
666
|
-
await conn.client
|
|
679
|
+
await closeModbusSafely(conn.client)
|
|
667
680
|
modbusPool.delete(modbusKey(driverConfig, 'rtu-tcp'))
|
|
668
681
|
}
|
|
669
682
|
}
|
|
@@ -556,8 +556,56 @@ class DcwController {
|
|
|
556
556
|
return getDcwRecipeRepo().create(input)
|
|
557
557
|
}
|
|
558
558
|
|
|
559
|
-
updateRecipe(id: string, patch: Partial<RecipeInput
|
|
560
|
-
|
|
559
|
+
updateRecipe(id: string, patch: Partial<RecipeInput>, meta?: { by: 'user' | 'agent' | 'system', actorName: string, actor: string, description: string }) {
|
|
560
|
+
const recipe = getDcwRecipeRepo().update(id, patch, meta)
|
|
561
|
+
// 配方参数版本变更 → 运维日志(版本号 + 归因;与版本历史同点入册)
|
|
562
|
+
if (patch.params !== undefined) {
|
|
563
|
+
try {
|
|
564
|
+
recordOps({
|
|
565
|
+
actor: meta?.actor ?? 'user',
|
|
566
|
+
actorName: meta?.actorName ?? meta?.actor ?? 'user',
|
|
567
|
+
actorKind: meta?.by ?? 'user',
|
|
568
|
+
action: meta?.by === 'agent' ? 'recipe.update.agent' : 'recipe.update',
|
|
569
|
+
kind: 'recipe',
|
|
570
|
+
targetKind: 'recipe',
|
|
571
|
+
targetId: id,
|
|
572
|
+
summary: `更新配方「${recipe.name}」→ v${recipe.version ?? 1}${meta?.description ? `:${meta.description.slice(0, 80)}` : ''}`,
|
|
573
|
+
lineId: recipe.lineId ?? '',
|
|
574
|
+
productId: recipe.productId ?? '',
|
|
575
|
+
recipeId: id,
|
|
576
|
+
detail: { version: recipe.version ?? 1, description: meta?.description ?? '' },
|
|
577
|
+
})
|
|
578
|
+
}
|
|
579
|
+
catch { /* 日志失败不影响更新 */ }
|
|
580
|
+
}
|
|
581
|
+
return recipe
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** 回退配方参数到历史版本/lastGood(生成新版本;非破坏) */
|
|
585
|
+
revertRecipe(id: string, target: { version?: number, toLastGood?: boolean }, meta: { by: 'user' | 'agent' | 'system', actorName: string, actor: string, description: string }) {
|
|
586
|
+
const recipe = getDcwRecipeRepo().revertToVersion(id, target, meta)
|
|
587
|
+
try {
|
|
588
|
+
recordOps({
|
|
589
|
+
actor: meta.actor,
|
|
590
|
+
actorName: meta.actorName,
|
|
591
|
+
actorKind: meta.by,
|
|
592
|
+
action: 'recipe.revert',
|
|
593
|
+
kind: 'recipe',
|
|
594
|
+
targetKind: 'recipe',
|
|
595
|
+
targetId: id,
|
|
596
|
+
summary: `回退配方「${recipe.name}」→ v${recipe.version ?? 1}${meta.description ? `:${meta.description.slice(0, 80)}` : ''}`,
|
|
597
|
+
lineId: recipe.lineId ?? '',
|
|
598
|
+
productId: recipe.productId ?? '',
|
|
599
|
+
recipeId: id,
|
|
600
|
+
detail: { version: recipe.version ?? 1, description: meta.description },
|
|
601
|
+
})
|
|
602
|
+
}
|
|
603
|
+
catch { /* 日志失败不影响回退 */ }
|
|
604
|
+
return recipe
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
recipeVersions(id: string) {
|
|
608
|
+
return getDcwRecipeRepo().versions(id)
|
|
561
609
|
}
|
|
562
610
|
|
|
563
611
|
removeRecipe(id: string): void {
|
|
@@ -29,6 +29,14 @@ function loadJson<T>(file: string, fallback: T): T {
|
|
|
29
29
|
return loadJsonFile(file, fallback) as T
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/** 配方变更归因(版本历史/运维日志共用):by=来源,actorName=人话操作者 */
|
|
33
|
+
export interface RecipeUpdateMeta {
|
|
34
|
+
by: 'user' | 'agent' | 'system'
|
|
35
|
+
actorName: string
|
|
36
|
+
actor: string
|
|
37
|
+
description: string
|
|
38
|
+
}
|
|
39
|
+
|
|
32
40
|
function saveJson(file: string, data: unknown): void {
|
|
33
41
|
saveJsonFileAtomic(file, data)
|
|
34
42
|
}
|
|
@@ -175,7 +183,7 @@ class DcwRecipeRepo {
|
|
|
175
183
|
return recipe
|
|
176
184
|
}
|
|
177
185
|
|
|
178
|
-
update(id: string, patch: Partial<RecipeInput
|
|
186
|
+
update(id: string, patch: Partial<RecipeInput>, meta?: RecipeUpdateMeta): RecipeView {
|
|
179
187
|
const r = this.byId(id)
|
|
180
188
|
if (!r) throw new AppError(404, ErrorCodes.NOT_FOUND, `Recipe 不存在: ${id}`)
|
|
181
189
|
if (patch.productId !== undefined && patch.productId !== r.productId) {
|
|
@@ -191,11 +199,20 @@ class DcwRecipeRepo {
|
|
|
191
199
|
if (patch.description !== undefined) r.description = String(patch.description).trim()
|
|
192
200
|
if (patch.params !== undefined) {
|
|
193
201
|
const fresh = normParams(patch.params, r.lineId)
|
|
194
|
-
// 参数版本化(调控闭环):活动批次外的参数修改 → 版本自增 + 旧版入史(cap 20)
|
|
202
|
+
// 参数版本化(调控闭环):活动批次外的参数修改 → 版本自增 + 旧版入史(cap 20);
|
|
203
|
+
// 归因元数据(by/actorName/actor/description)随版本入史,前端/Agent 可查谁改的为什么改
|
|
195
204
|
const changed = JSON.stringify(fresh) !== JSON.stringify(r.params)
|
|
196
205
|
if (changed) {
|
|
197
206
|
r.paramsHistory ??= []
|
|
198
|
-
r.paramsHistory.push({
|
|
207
|
+
r.paramsHistory.push({
|
|
208
|
+
version: r.version ?? 1,
|
|
209
|
+
params: r.params,
|
|
210
|
+
at: new Date().toISOString(),
|
|
211
|
+
by: meta?.by ?? 'user',
|
|
212
|
+
actorName: meta?.actorName ?? meta?.actor ?? 'user',
|
|
213
|
+
actor: meta?.actor ?? '',
|
|
214
|
+
description: meta?.description ?? '',
|
|
215
|
+
})
|
|
199
216
|
if (r.paramsHistory.length > 20)
|
|
200
217
|
r.paramsHistory.splice(0, r.paramsHistory.length - 20)
|
|
201
218
|
r.version = (r.version ?? 1) + 1
|
|
@@ -208,6 +225,67 @@ class DcwRecipeRepo {
|
|
|
208
225
|
return r
|
|
209
226
|
}
|
|
210
227
|
|
|
228
|
+
/**
|
|
229
|
+
* 回退配方参数到指定历史版本(或已知良好批次快照),生成新版本(非破坏,历史完整保留)。
|
|
230
|
+
* target: { version: N } = 回到 vN 的参数;{ toLastGood: true } = 回到 lastGood 批次的参数冻结。
|
|
231
|
+
*/
|
|
232
|
+
revertToVersion(id: string, target: { version?: number, toLastGood?: boolean }, meta: RecipeUpdateMeta): RecipeView {
|
|
233
|
+
const r = this.byId(id)
|
|
234
|
+
if (!r) throw new AppError(404, ErrorCodes.NOT_FOUND, `Recipe 不存在: ${id}`)
|
|
235
|
+
let snapshot: RecipeParam[] | undefined
|
|
236
|
+
let desc: string
|
|
237
|
+
if (target.toLastGood) {
|
|
238
|
+
const run = r.lastGoodRunId ? this.runById(r.lastGoodRunId) : undefined
|
|
239
|
+
if (!run?.paramsSnapshot?.length) {
|
|
240
|
+
throw new AppError(409, ErrorCodes.CONFLICT, `配方「${r.name}」无可回退的良好批次(先标记 lastGood 或指定 version)`)
|
|
241
|
+
}
|
|
242
|
+
snapshot = normParams(run.paramsSnapshot, r.lineId)
|
|
243
|
+
desc = `回退到已知良好批次 ${run.id.slice(0, 8)} 的参数冻结`
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
const v = Number(target.version)
|
|
247
|
+
if (!Number.isFinite(v)) throw new AppError(400, ErrorCodes.VALIDATION_ERROR, 'version 必须为数字版本号')
|
|
248
|
+
const hit = (r.paramsHistory ?? []).find(h => h.version === v)
|
|
249
|
+
if (!hit) {
|
|
250
|
+
const avail = (r.paramsHistory ?? []).map(h => h.version).sort((a, b) => b - a)
|
|
251
|
+
throw new AppError(404, ErrorCodes.NOT_FOUND, `版本 v${v} 不存在(可用历史版本:${avail.length ? avail.map(n => `v${n}`).join(', ') : '无'};当前 v${r.version ?? 1})`)
|
|
252
|
+
}
|
|
253
|
+
snapshot = hit.params
|
|
254
|
+
desc = `回退到 v${v}(该版变更于 ${hit.at.slice(0, 19).replace('T', ' ')}${hit.actorName ? `,原操作者 ${hit.actorName}` : ''})`
|
|
255
|
+
}
|
|
256
|
+
if (JSON.stringify(snapshot) === JSON.stringify(r.params)) {
|
|
257
|
+
throw new AppError(409, ErrorCodes.CONFLICT, `目标版本参数与当前 v${r.version ?? 1} 完全一致,无需回退`)
|
|
258
|
+
}
|
|
259
|
+
const out = this.update(id, {
|
|
260
|
+
params: snapshot.map(p => ({ nodeId: p.nodeId, templateRef: p.templateRef, value: p.value, min: p.min, max: p.max })),
|
|
261
|
+
}, { ...meta, description: `${meta.description ? `${meta.description};` : ''}${desc}` })
|
|
262
|
+
return out
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** 参数版本历史(旧→新;含当前版尾部) */
|
|
266
|
+
versions(id: string): Array<{ version: number, at: string, by?: string, actorName?: string, description?: string, params: RecipeParam[], current?: boolean }> {
|
|
267
|
+
const r = this.byId(id)
|
|
268
|
+
if (!r) throw new AppError(404, ErrorCodes.NOT_FOUND, `Recipe 不存在: ${id}`)
|
|
269
|
+
const rows: Array<{ version: number, at: string, by?: string, actorName?: string, description?: string, params: RecipeParam[], current?: boolean }> = (r.paramsHistory ?? []).map(h => ({
|
|
270
|
+
version: h.version,
|
|
271
|
+
at: h.at,
|
|
272
|
+
by: h.by,
|
|
273
|
+
actorName: h.actorName,
|
|
274
|
+
description: h.description,
|
|
275
|
+
params: h.params,
|
|
276
|
+
}))
|
|
277
|
+
rows.push({
|
|
278
|
+
version: r.version ?? 1,
|
|
279
|
+
at: r.updatedAt,
|
|
280
|
+
by: undefined,
|
|
281
|
+
actorName: undefined,
|
|
282
|
+
description: '当前版本',
|
|
283
|
+
params: r.params,
|
|
284
|
+
current: true,
|
|
285
|
+
})
|
|
286
|
+
return rows
|
|
287
|
+
}
|
|
288
|
+
|
|
211
289
|
/** 标记已知良好批次(基准恢复的目标;Trial keep / 手动均可调) */
|
|
212
290
|
markGood(recipeId: string, runId: string): RecipeView {
|
|
213
291
|
const r = this.byId(recipeId)
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { createRequire } from 'node:module'
|
|
11
11
|
import type { DcwDriverKind } from '../../../../shared/dcw-protocol'
|
|
12
12
|
import { AppError } from '../../../utils/errors'
|
|
13
|
-
import { classifyCommError, decodeRegisters, evictModbusConn, evictOpcUaConn, getModbusConn, getOpcUaConn, modbusKey, registerOffset, withModbusConn } from '../daq/drivers'
|
|
13
|
+
import { classifyCommError, closeModbusSafely, decodeRegisters, evictModbusConn, evictOpcUaConn, getModbusConn, getOpcUaConn, modbusKey, registerOffset, withModbusConn } from '../daq/drivers'
|
|
14
14
|
|
|
15
15
|
const reqNative = createRequire(import.meta.url)
|
|
16
16
|
|
|
@@ -268,7 +268,7 @@ export const modbusTcpDcwDriver: DcwWriteDriver = {
|
|
|
268
268
|
try {
|
|
269
269
|
const key = modbusKey(driverConfig)
|
|
270
270
|
const conn = await getModbusConn(driverConfig).catch(() => null)
|
|
271
|
-
if (conn) await conn.client
|
|
271
|
+
if (conn) await closeModbusSafely(conn.client)
|
|
272
272
|
void key
|
|
273
273
|
}
|
|
274
274
|
catch { /* ignore */ }
|
package/shared/dcw-protocol.ts
CHANGED
|
@@ -377,8 +377,20 @@ export interface RecipeView {
|
|
|
377
377
|
daqWindows: RecipeDaqWindow[]
|
|
378
378
|
/** 参数版本(活动批次外的 params 修改自增;回退/审计定位用) */
|
|
379
379
|
version?: number
|
|
380
|
-
/** 参数版本历史(cap 20:每次活动批次外的 params
|
|
381
|
-
paramsHistory?: Array<{
|
|
380
|
+
/** 参数版本历史(cap 20:每次活动批次外的 params 修改存旧版;by/actorName/actor/description 为变更归因) */
|
|
381
|
+
paramsHistory?: Array<{
|
|
382
|
+
version: number
|
|
383
|
+
params: RecipeParam[]
|
|
384
|
+
at: string
|
|
385
|
+
/** 变更来源:user/agent/system */
|
|
386
|
+
by?: string
|
|
387
|
+
/** 人话操作者(用户名或「Channel名/成员名」) */
|
|
388
|
+
actorName?: string
|
|
389
|
+
/** 原始 id(userId/agentId/'system') */
|
|
390
|
+
actor?: string
|
|
391
|
+
/** 变更描述/原因 */
|
|
392
|
+
description?: string
|
|
393
|
+
}>
|
|
382
394
|
/** 已知良好批次(判定 keep / 手动标记;基准恢复的目标) */
|
|
383
395
|
lastGoodRunId?: string | null
|
|
384
396
|
createdAt: string
|