agentworkshop 0.7.14 → 0.7.16

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 (35) hide show
  1. package/.AgentWorkShop/prompts/host-tools.json +819 -755
  2. package/.output/.AgentWorkShop/prompts/host-tools.json +819 -755
  3. package/.output/nitro.json +1 -1
  4. package/.output/public/_nuxt/builds/latest.json +1 -1
  5. package/.output/public/_nuxt/builds/meta/cf0a8831-4514-493d-ad5a-4e371a089ef9.json +1 -0
  6. package/.output/server/chunks/_/nitro.mjs +885 -725
  7. package/.output/server/chunks/_/nitro.mjs.map +1 -1
  8. package/.output/server/chunks/routes/api/workshop/dcw/index.post.mjs +1 -1
  9. package/.output/server/chunks/routes/api/workshop/dcw/index.post.mjs.map +1 -1
  10. package/.output/server/chunks/routes/api/workshop/index3.post.mjs +1 -1
  11. package/.output/server/chunks/routes/api/workshop/index3.post.mjs.map +1 -1
  12. package/.output/server/package.json +1 -1
  13. package/package.json +1 -1
  14. package/scripts/_dbg-harness-ui-e2e.py +23 -13
  15. package/scripts/_dbg-opslog-agent-e2e.mjs +191 -0
  16. package/scripts/aw-expose.mjs +159 -0
  17. package/server/api/workshop/daq/index.post.ts +1 -1
  18. package/server/api/workshop/dcw/lines/index.post.ts +1 -1
  19. package/server/data/daqs.json +471 -61
  20. package/server/data/dcw-lines.json +49 -0
  21. package/server/data/dcw-products.json +49 -0
  22. package/server/data/dcw-recipes.json +64 -0
  23. package/server/data/dcw-rollback.json +239 -6
  24. package/server/data/dcw-runs.json +162 -142
  25. package/server/data/dcw-writes.json +156 -0
  26. package/server/data/dcws.json +363 -217
  27. package/server/data/line-runs.json +41 -11
  28. package/server/data/scene-layouts.json +11 -0
  29. package/server/plugins/workshop.ts +9 -0
  30. package/server/services/workshop/agents/agent-badge.ts +38 -0
  31. package/server/services/workshop/agents/host-tool-bridge.ts +622 -612
  32. package/server/services/workshop/agents/industrial-tools.ts +139 -4
  33. package/server/services/workshop/daq/drivers.ts +25 -5
  34. package/server/services/workshop/dcw/recipe-rollback-manager.ts +8 -6
  35. package/.output/public/_nuxt/builds/meta/23c30854-f9e1-4651-bc3a-b695ef5faca4.json +0 -1
@@ -11,6 +11,7 @@ import type { AgentNodeBinding } from './node-bindings.repo'
11
11
  import { getAgentNodeBindingRepo } from './node-bindings.repo'
12
12
  import { getToolApprovals } from './tool-approvals'
13
13
  import { nodeSemanticCards } from './industrial-context'
14
+ import { agentBadgeLabel } from './agent-badge'
14
15
  import { getDcwController } from '../dcw/dcw-controller'
15
16
  import { getActiveLineRun } from '../dcw/line-run'
16
17
  import { findDcwTemplate } from '../dcw/dcw-templates'
@@ -18,6 +19,7 @@ import { daqRuntimeSettings } from '../settings'
18
19
  import { getDaqNodeRepo } from '../daq/daq-node.repo'
19
20
  import { findDaqTemplate } from '../daq/daq-templates'
20
21
  import { getRecipeRollBackManager } from '../dcw/recipe-rollback-manager'
22
+ import { getOps } from '../ops/ops'
21
23
 
22
24
  export async function toolMyIndustrialNodes(agentId: string): Promise<{ text: string }> {
23
25
  const repo = getAgentNodeBindingRepo()
@@ -65,7 +67,11 @@ export async function toolMyIndustrialNodes(agentId: string): Promise<{ text: st
65
67
  4. 改动设定后等待工艺响应(热惯性/传动惯量)再评估,避免连续大幅调整。
66
68
  5. 调控闭环:每次下发自动开一条优化记录(open);观察数采后用 dcw_judge 落判定(keep/rollback/uncertain);
67
69
  判 rollback 后用 dcw_rollback 执行回退;dcw_journal 可查节点参数变更史。未判定前再下发,旧记录会被标记 superseded。
68
- 若节点被他人的 open 记录阻塞(对方已消失),超时(30 分钟)后你可接管:dcw_judge 会带接管标记入册。`,
70
+ 若节点被他人的 open 记录阻塞(对方已消失),超时(30 分钟)后你可接管:dcw_judge 会带接管标记入册。
71
+ 6. 自查面:ops_log 查你负责产线的运维日志(谁/何时做了什么,来源区分 Agent/用户/系统;
72
+ 参数 line_id/node_id/kind/actor_kind/minutes/limit/mine);recipe_log 查配方下发与回退历史
73
+ (recipe.apply/优化开窗/判定/回退,参数 line_id/recipe_id/minutes/limit)。
74
+ 作业前后各查一次,即可完整知道自己与团队对当前节点做过哪些操作、Recipe 有过哪些变更。`,
69
75
  }
70
76
  }
71
77
 
@@ -123,6 +129,8 @@ export async function toolDcwControl(agentId: string, args: { node_id?: string,
123
129
  const meta = {
124
130
  source: 'agent' as const,
125
131
  actor: agentId,
132
+ // 人话操作者:「Channel名/成员名」——/logs 来源=Agent 与用户/系统操作可区分追溯
133
+ actorName: agentBadgeLabel(agentId),
126
134
  taskId: args.task_id ? String(args.task_id) : undefined,
127
135
  hypothesis: args.hypothesis ? String(args.hypothesis) : '',
128
136
  }
@@ -355,7 +363,7 @@ export async function toolDcwJudge(agentId: string, args: { record_id?: string,
355
363
  return { text: `记录 ${recordId} 不是你发起的优化(发起者:${record.agentId ?? '用户'}),Agent 仅可判定自己的记录;他人记录请请用户在界面判定。`, isError: true }
356
364
  try {
357
365
  const finalReason = takeover ? `[接管孤儿记录,原属主 ${record.agentId ?? '?'} 超时未判定] ${reason}` : reason
358
- const updated = rb.judge(recordId, verdict, finalReason, 'agent', agentId, { takeover })
366
+ const updated = rb.judge(recordId, verdict, finalReason, 'agent', agentId, { takeover, actorName: agentBadgeLabel(agentId) })
359
367
  const next = verdict === 'rollback'
360
368
  ? '判定已入册;请立即用 dcw_rollback(record_id) 执行回退(判定不自动改 PLC)。'
361
369
  : verdict === 'keep'
@@ -385,10 +393,10 @@ export async function toolDcwRollback(agentId: string, args: { record_id?: strin
385
393
  const takeover = record.agentId !== agentId && rb.isStale(record)
386
394
  if (record.agentId !== agentId && !takeover)
387
395
  return { text: `记录 ${recordId} 不是你发起的优化(发起者:${record.agentId ?? '用户'}),回退他人记录请请用户在数采中心/产线详情执行;若原属主已消失(超时未判定),可先 dcw_judge 接管后再回退。`, isError: true }
388
- const fresh = await rb.rollbackRecord(recordId, agentId, 'agent')
396
+ const fresh = await rb.rollbackRecord(recordId, agentId, 'agent', undefined, { actorName: agentBadgeLabel(agentId) })
389
397
  return { text: `回退已执行:记录 ${recordId} 标记 rolled-back;下发恢复值 ${fresh?.params[0]?.to}${'(以回读为准)'};新回退记录 ${fresh?.id} 已入册。请 daq_query 复测确认恢复。` }
390
398
  }
391
- const fresh = await rb.rollbackNode(nodeId, agentId, 'agent', to)
399
+ const fresh = await rb.rollbackNode(nodeId, agentId, 'agent', to, { actorName: agentBadgeLabel(agentId) })
392
400
  return { text: `节点单步回退已执行:恢复到最近稳定锚值;新回退记录 ${fresh?.id} 已入册。请 daq_query 复测确认恢复。` }
393
401
  }
394
402
  catch (err) {
@@ -481,3 +489,130 @@ export async function toolDaqFrames(agentId: string, args: {
481
489
  }
482
490
  return { text: `数采帧查询结果(${targets.length} 个节点):\n\n${sections.join('\n\n')}\n\n向量=多点工程量轮廓(完整点列前端可查);图像=像素在对象存储,指标供判读(brightness 过低=曝光不足/遮挡)。` }
483
491
  }
492
+
493
+ // ================================================================
494
+ // 运维日志 / Recipe 变更史查询(Agent 自查面:负责产线 scoped)
495
+ // ================================================================
496
+
497
+ /** Agent 负责范围 = 绑定节点的全集(节点 id 集 + 这些节点所属产线集);无绑定 = 无范围 */
498
+ function agentOpsScope(agentId: string): { lineIds: string[], nodeIds: Set<string> } | null {
499
+ const bindings = getAgentNodeBindingRepo().byAgent(agentId)
500
+ if (bindings.length === 0) return null
501
+ const lineIds = new Set<string>()
502
+ const nodeIds = new Set<string>()
503
+ for (const b of bindings) {
504
+ nodeIds.add(b.nodeId)
505
+ try {
506
+ const n = b.kind === 'dcw' ? getDcwController().byId(b.nodeId) : getDaqNodeRepo().byId(b.nodeId)
507
+ if (n?.lineId) lineIds.add(n.lineId)
508
+ }
509
+ catch { /* 节点刚被删等情况忽略 */ }
510
+ }
511
+ return { lineIds: [...lineIds], nodeIds }
512
+ }
513
+
514
+ const OPS_ACTOR_LABEL: Record<string, string> = { agent: 'Agent', user: '用户', system: '系统' }
515
+
516
+ function fmtAuditAt(iso: string): string {
517
+ return iso.length >= 19 ? iso.slice(5, 19).replace('T', ' ') : iso
518
+ }
519
+
520
+ /** 工具:ops_log —— 负责产线的运维/审计日志查询(全部下发/判定/回退/配方/产线事件)。
521
+ * 权限:仅自己绑定节点所覆盖的产线;node_id 须为绑定节点。来源(actor_kind)区分 Agent/用户/系统。 */
522
+ export async function toolOpsLog(agentId: string, args: {
523
+ line_id?: string
524
+ node_id?: string
525
+ kind?: string
526
+ actor_kind?: string
527
+ minutes?: number | string
528
+ limit?: number | string
529
+ mine?: boolean | string
530
+ }): Promise<{ text: string, isError?: boolean }> {
531
+ const audit = getOps()?.audit
532
+ if (!audit) return { text: '审计仓储未装配(服务未就绪),请稍后重试。', isError: true }
533
+ const scope = agentOpsScope(agentId)
534
+ if (!scope) return { text: '你尚未绑定任何工业节点,无日志可查(日志权限跟随节点绑定)。', isError: true }
535
+ const nodeId = String(args.node_id ?? '').trim()
536
+ const lineId = String(args.line_id ?? '').trim()
537
+ if (nodeId && !scope.nodeIds.has(nodeId))
538
+ return { text: `无权查询节点 ${nodeId} 的日志(仅可查自己绑定的节点;用 my_industrial_nodes 查看绑定)。`, isError: true }
539
+ if (lineId && !scope.lineIds.includes(lineId))
540
+ return { text: `无权查询产线 ${lineId} 的日志(你的负责产线:${scope.lineIds.join(', ') || '(绑定节点均未分配产线)'})。`, isError: true }
541
+ const kind = String(args.kind ?? '').trim() || undefined
542
+ const actorKind = String(args.actor_kind ?? '').trim() || undefined
543
+ const mine = args.mine === true || args.mine === 'true'
544
+ const minutes = Number(args.minutes) || 1440
545
+ const limit = Math.min(Number(args.limit) || 20, 100)
546
+ const from = new Date(Date.now() - minutes * 60_000).toISOString()
547
+
548
+ const lines = lineId ? [lineId] : scope.lineIds
549
+ const byId = new Map<string, Record<string, unknown>>()
550
+ for (const lid of lines) {
551
+ for (const r of audit.query({ lineId: lid, kind, actorKind, actor: mine ? agentId : undefined, from, limit }))
552
+ byId.set(String(r.id), r)
553
+ }
554
+ let rows = [...byId.values()]
555
+ if (nodeId) rows = rows.filter(r => r.targetId === nodeId)
556
+ rows.sort((a, b) => String(b.at).localeCompare(String(a.at)))
557
+ rows = rows.slice(0, limit)
558
+ if (rows.length === 0) return { text: `窗口(近 ${minutes} 分钟)内无匹配日志。可调大 minutes,或放宽 kind/actor_kind/node_id 过滤。` }
559
+
560
+ const body = rows.map((r) => {
561
+ const src = OPS_ACTOR_LABEL[String(r.actorKind)] ?? String(r.actorKind)
562
+ return `- [${fmtAuditAt(String(r.at))}] 来源=${src} 操作者=${String(r.actorName) || String(r.actor)} | ${String(r.action)} | ${String(r.summary)}`
563
+ })
564
+ const scopeNote = lineId ? `产线 ${lineId}` : `负责产线 ${scope.lineIds.length} 条`
565
+ return {
566
+ text: `运维日志(${scopeNote},近 ${minutes} 分钟,${rows.length} 条,新→旧):\n${body.join('\n')}\n\n说明:来源=Agent 的操作者格式为「Channel名/成员名」;需要节点级参数值变更史用 dcw_journal,配方下发/回退专门视图用 recipe_log。`,
567
+ }
568
+ }
569
+
570
+ /** 工具:recipe_log —— 负责产线的配方下发与回退历史(recipe.apply + 优化开窗/判定/回退)。
571
+ * Agent 据此知道 Recipe 层面发生过哪些下发变更、谁做的、是否已回退。 */
572
+ export async function toolRecipeLog(agentId: string, args: {
573
+ line_id?: string
574
+ recipe_id?: string
575
+ minutes?: number | string
576
+ limit?: number | string
577
+ }): Promise<{ text: string, isError?: boolean }> {
578
+ const audit = getOps()?.audit
579
+ if (!audit) return { text: '审计仓储未装配(服务未就绪),请稍后重试。', isError: true }
580
+ const scope = agentOpsScope(agentId)
581
+ if (!scope) return { text: '你尚未绑定任何工业节点,无 Recipe 历史可查(权限跟随节点绑定)。', isError: true }
582
+ const lineId = String(args.line_id ?? '').trim()
583
+ if (lineId && !scope.lineIds.includes(lineId))
584
+ return { text: `无权查询产线 ${lineId} 的 Recipe 历史(你的负责产线:${scope.lineIds.join(', ') || '(无)'})。`, isError: true }
585
+ const recipeId = String(args.recipe_id ?? '').trim() || undefined
586
+ const minutes = Number(args.minutes) || 1440
587
+ const limit = Math.min(Number(args.limit) || 20, 100)
588
+ const from = new Date(Date.now() - minutes * 60_000).toISOString()
589
+
590
+ const lines = lineId ? [lineId] : scope.lineIds
591
+ const byId = new Map<string, Record<string, unknown>>()
592
+ for (const lid of lines) {
593
+ for (const kind of ['recipe', 'rollback']) {
594
+ for (const r of audit.query({ lineId: lid, kind, recipeId, from, limit }))
595
+ byId.set(String(r.id), r)
596
+ }
597
+ }
598
+ const rows = [...byId.values()].sort((a, b) => String(b.at).localeCompare(String(a.at))).slice(0, limit)
599
+ if (rows.length === 0) return { text: `窗口(近 ${minutes} 分钟)内无配方下发/回退记录。可调大 minutes 或换 line_id。` }
600
+
601
+ const body = rows.map((r) => {
602
+ const src = OPS_ACTOR_LABEL[String(r.actorKind)] ?? String(r.actorKind)
603
+ const action = String(r.action)
604
+ const tag = action === 'recipe.apply'
605
+ ? '配方下发'
606
+ : action === 'optimization.open'
607
+ ? '优化开窗'
608
+ : action === 'optimization.judge'
609
+ ? '优化判定'
610
+ : action === 'optimization.rollback'
611
+ ? '回退执行'
612
+ : action
613
+ return `- [${fmtAuditAt(String(r.at))}] ${tag} 来源=${src} 操作者=${String(r.actorName) || String(r.actor)} | ${String(r.summary)}${r.recipeId ? `(配方 ${String(r.recipeId).slice(0, 8)})` : ''}`
614
+ })
615
+ return {
616
+ text: `Recipe 变更史(${lineId ? `产线 ${lineId}` : `负责产线 ${scope.lineIds.length} 条`},近 ${minutes} 分钟,${rows.length} 条,新→旧):\n${body.join('\n')}\n\n说明:配方下发=整批参数写命令;优化开窗=单次设定变更(含 Agent 假设);回退执行=已恢复基线值。节点级参数值逐笔历史用 dcw_journal(node_id)。`,
617
+ }
618
+ }
@@ -231,15 +231,35 @@ export function evictModbusConn(cfg: Record<string, unknown>, transport: ModbusT
231
231
  /**
232
232
  * 连接级排队执行:操作串行入队,完成后链路归还。
233
233
  * 数控写入等当前采样读完成再执行(不再 409 快速失败);数采采样忙时仍跳帧让路防堆积。
234
+ * per-request 3s 硬超时:半开连接(对端断电/SYN 黑洞)时底层请求永不 settle,
235
+ * 超时后作废连接并重置 tail 链,防止单条死连接堵死该 host:port 的全部后续请求。
234
236
  */
235
237
  export function withModbusConn<R>(conn: ModbusConn, fn: () => Promise<R>): Promise<R> {
236
238
  conn.pending++
237
239
  const run = conn.tail.then(() => fn(), () => fn())
238
- conn.tail = run.then(
239
- () => { conn.pending-- },
240
- () => { conn.pending-- },
241
- )
242
- return run
240
+ let timer: NodeJS.Timeout | undefined
241
+ let dead = false
242
+ const guarded = Promise.race([
243
+ run,
244
+ new Promise<never>((_, rej) => {
245
+ timer = setTimeout(() => {
246
+ dead = true
247
+ try {
248
+ void conn.client.close()
249
+ }
250
+ catch { /* 已断 */ }
251
+ rej(new Error('Modbus 响应超时(3s)——连接可能半开,已作废重连'))
252
+ }, 3000)
253
+ }),
254
+ ] as Promise<R>)
255
+ void guarded.catch(() => { /* 超时分支先 reject,防 unhandled */ })
256
+ conn.tail = guarded.then(
257
+ () => { clearTimeout(timer); conn.pending-- },
258
+ () => { clearTimeout(timer); conn.pending-- },
259
+ ).then(() => {
260
+ if (dead) conn.tail = Promise.resolve()
261
+ })
262
+ return guarded
243
263
  }
244
264
 
245
265
  export function modbusKey(cfg: Record<string, unknown>, transport: ModbusTransport = 'tcp'): string {
@@ -200,7 +200,7 @@ export class RecipeRollBackManager {
200
200
  return record.status === 'open' && Date.now() - Date.parse(record.setAt) > OPEN_RECORD_STALE_MS()
201
201
  }
202
202
 
203
- judge(recordId: string, verdict: OptimizationVerdict, reason: string, by: 'agent' | 'system' | 'user', actor: string, opts?: { takeover?: boolean }): OptimizationRecord {
203
+ judge(recordId: string, verdict: OptimizationVerdict, reason: string, by: 'agent' | 'system' | 'user', actor: string, opts?: { takeover?: boolean, actorName?: string }): OptimizationRecord {
204
204
  const record = this.repo.byId(recordId)
205
205
  if (!record)
206
206
  throw new AppError(404, ErrorCodes.NOT_FOUND, `优化记录不存在: ${recordId}`)
@@ -218,7 +218,7 @@ export class RecipeRollBackManager {
218
218
  this.emit('judged', record)
219
219
  recordOps({
220
220
  actor,
221
- actorName: actor,
221
+ actorName: opts?.actorName ?? actor,
222
222
  actorKind: by,
223
223
  action: 'optimization.judge',
224
224
  kind: 'rollback',
@@ -237,7 +237,7 @@ export class RecipeRollBackManager {
237
237
  // ================================================================
238
238
 
239
239
  /** 回退一条优化记录:目标 = 该记录的 from 值(设定前基线) */
240
- async rollbackRecord(recordId: string, actor: string, by: 'agent' | 'user' | 'system', approvalId?: string): Promise<OptimizationRecord> {
240
+ async rollbackRecord(recordId: string, actor: string, by: 'agent' | 'user' | 'system', approvalId?: string, opts?: { actorName?: string }): Promise<OptimizationRecord> {
241
241
  const record = this.repo.byId(recordId)
242
242
  if (!record)
243
243
  throw new AppError(404, ErrorCodes.NOT_FOUND, `优化记录不存在: ${recordId}`)
@@ -246,11 +246,11 @@ export class RecipeRollBackManager {
246
246
  throw new AppError(409, ErrorCodes.CONFLICT, `记录 ${recordId} 无基线值(首写无 prevValue),无法回退`)
247
247
  if (by === 'agent')
248
248
  this.checkRollbackAllowed(record.nodeId)
249
- return this.executeRollbackWrite(record, from, actor, approvalId, undefined, by)
249
+ return this.executeRollbackWrite(record, from, actor, approvalId, undefined, by, opts?.actorName)
250
250
  }
251
251
 
252
252
  /** 节点级单步回退:目标 = 最近稳定锚的 prevValue(撤销栈栈顶) */
253
- async rollbackNode(nodeId: string, actor: string, by: 'agent' | 'user' | 'system', toAnchorId?: string): Promise<OptimizationRecord> {
253
+ async rollbackNode(nodeId: string, actor: string, by: 'agent' | 'user' | 'system', toAnchorId?: string, opts?: { actorName?: string }): Promise<OptimizationRecord> {
254
254
  const node = getDcwNodeRepo().byId(nodeId)
255
255
  if (!node)
256
256
  throw new AppError(404, ErrorCodes.NOT_FOUND, `控制节点不存在: ${nodeId}`)
@@ -274,6 +274,7 @@ export class RecipeRollBackManager {
274
274
  undefined,
275
275
  toAnchorId,
276
276
  by,
277
+ opts?.actorName,
277
278
  )
278
279
  }
279
280
 
@@ -336,6 +337,7 @@ export class RecipeRollBackManager {
336
337
  approvalId?: string,
337
338
  toAnchorId?: string,
338
339
  by: 'agent' | 'user' | 'system' = 'system',
340
+ actorName?: string,
339
341
  ): Promise<OptimizationRecord> {
340
342
  // 先关原记录(防回退写自身的 afterWrite 把它当 open supersede)
341
343
  const orig = this.repo.byId(record.id)
@@ -368,7 +370,7 @@ export class RecipeRollBackManager {
368
370
  this.emit('rolled-back', orig)
369
371
  recordOps({
370
372
  actor,
371
- actorName: actor,
373
+ actorName: actorName ?? actor,
372
374
  actorKind: by === 'agent' ? 'agent' : by === 'user' ? 'user' : 'system',
373
375
  action: 'optimization.rollback',
374
376
  kind: 'rollback',
@@ -1 +0,0 @@
1
- {"id":"23c30854-f9e1-4651-bc3a-b695ef5faca4","timestamp":1788693267652,"prerendered":[]}