agentworkshop 0.7.37 → 0.7.39

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 (91) hide show
  1. package/.AgentWorkShop/prompts/aml-mission-default.md +20 -20
  2. package/.AgentWorkShop/prompts/aml-playbook.md +49 -49
  3. package/.AgentWorkShop/prompts/host-tools.json +1238 -1238
  4. package/.AgentWorkShop/prompts/peer-reply-cross-channel.md +10 -10
  5. package/.output/.AgentWorkShop/prompts/aml-mission-default.md +20 -20
  6. package/.output/.AgentWorkShop/prompts/aml-playbook.md +49 -49
  7. package/.output/.AgentWorkShop/prompts/host-tools.json +1238 -1238
  8. package/.output/.AgentWorkShop/prompts/peer-reply-cross-channel.md +10 -10
  9. package/.output/nitro.json +1 -1
  10. package/.output/public/_nuxt/{BG8sCcwx.js → CUDwzgLV.js} +4 -4
  11. package/.output/public/_nuxt/CmsOtOAA2.js +1 -0
  12. package/.output/public/_nuxt/builds/latest.json +1 -1
  13. package/.output/public/_nuxt/builds/meta/4457d63f-0866-4cea-b320-04a321f24795.json +1 -0
  14. package/.output/public/_nuxt/default.BXI_fZUi.css +1 -0
  15. package/.output/server/chunks/_/nitro.mjs +703 -704
  16. package/.output/server/chunks/_/nitro.mjs.map +1 -1
  17. package/.output/server/chunks/build/{default-hseyOqBT.mjs → default-CZmZFpqH.mjs} +27 -42
  18. package/.output/server/chunks/build/{default-hseyOqBT.mjs.map → default-CZmZFpqH.mjs.map} +1 -1
  19. package/.output/server/chunks/routes/renderer.mjs +1 -1
  20. package/.output/server/chunks/virtual/entry.mjs +3 -1
  21. package/.output/server/chunks/virtual/precomputed.mjs +1 -1
  22. package/.output/server/package.json +1 -1
  23. package/README-zh.md +6 -6
  24. package/README.md +6 -6
  25. package/app/components/AppHeader.vue +0 -17
  26. package/app/components/AppSidebar.vue +1 -0
  27. package/docs/cli.en.md +2 -2
  28. package/docs/cli.md +2 -2
  29. package/docs/full-test-plan.md +5 -1
  30. package/docs/plugins.en.md +2 -1
  31. package/docs/plugins.md +2 -1
  32. package/docs/sdk.en.md +1 -1
  33. package/docs/sdk.md +1 -1
  34. package/i18n/locales/en.ts +1 -0
  35. package/i18n/locales/zh-CN.ts +1 -0
  36. package/package.json +1 -1
  37. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.continue.json +1 -0
  38. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.json +1 -0
  39. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/054bb4bc4e4217a2ab0b0c6b663747214a9af464f5568ea60e784704e340bb0f.source +153 -0
  40. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/0554197c3d2bac203a1f5c48a5d52495d1336db1bd7aa4f4c19875810363e684.source +153 -0
  41. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/05c203d12a954fffba942e1d626e3f5fec05b18a5aef2bac003b7884a4b77406.source +431 -0
  42. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/145a6e6ba811af35aed31e0cc3211f53706a14980fa069ee9e3c1591139f96f0.source +180 -0
  43. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/1ae0fb281f97899c025e368ca73f41349b01fb5f85b19e08d7ebccff49095ed5.source +112 -0
  44. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/20ee9967aa0541c516db7575bdd4a1a745939bc7016bd10d7635903113edb8ec.source +227 -0
  45. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/233751843aeba9d4c5263f275b8102de00443c06f286918cbd23e0b6729e01aa.source +131 -0
  46. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/263c2e05b425e5c322e60c6475279f963bab3a873eb521b21df7fec935a46597.source +160 -0
  47. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/2ac5eaac28eaaee2178c4b32014df8e732f9dd432a285a872f5b7f31c1395fa4.source +157 -0
  48. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/33a0db0d2d95d2c11d0720090458e5fba1425376f5ceb5b422d1d3471be3226b.source +348 -0
  49. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/359e474ae83db35e4615d3728de6bf694b64d4dce9b536730a3b6518f9c0494a.source +296 -0
  50. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/37a4c3c31c4b70f6b719c53d1e9c4fcba603f31adfdd11bb6ef9f0d094923c56.source +285 -0
  51. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/461feca1c14a695eb8b0f17a6f95fe97eeceabc0510faaea4ba6d25bde147d8d.source +217 -0
  52. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/4cfadbda8591f29d5e1e230dc000e701b756e850441b3900fb596a4d2f6e98c2.source +301 -0
  53. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/50ca2810cf0b248c96948994c0c404531bbad6e553166b6816b3b03bfa0f75d2.source +151 -0
  54. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/52d9e8cb591a9ae86e238c3620a610cd386717bee222c0f37b25b2ac7e523dc4.source +135 -0
  55. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/6c87daafa4c006560a954636a97333f6123153c9d34d96203f7ee3a14e556357.source +156 -0
  56. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/77bc47854b577900621cacf1697f81fa2bc005af129d37614a97a3441c08f99f.source +72 -0
  57. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/7bbb789d3a527817118cf8399ed1861d6a962ade10878fd07223435d333a7089.source +469 -0
  58. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/86b7c1daafc8b60ba9982dde5bf1bcc74265709483c176e71f23ec6fc267451f.source +259 -0
  59. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/8df66d62bd41984d0970e8236882080c5085ac937d07a29779463d6a03b37c76.source +97 -0
  60. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/9962f2d1b3a85137bd40cc835725600b39f89ee02a27c06b2e0402e1eafde115.source +277 -0
  61. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a1090ea066b792cb201739f4994e37bf7c12f52e805239a1eab87f18a537f266.source +171 -0
  62. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a5241091650134d0919897e086a606a3846c8b2b54f8b4c84a8696c145ac3e9e.source +425 -0
  63. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/af19fa4c6e34d5540028be4b17600161283af3a2a2dfc61a3fdc0834436a0c14.source +99 -0
  64. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3713d78e2b2f93905ff6be8d58a8d0a2f42dea322eb67c987fda22ff6a084d5.source +325 -0
  65. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3849943c3aff102d1af815631fc843a2a52c17646a4b04696ece323e483bbec.source +332 -0
  66. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/bac735a3803a6650b7f1eb8f587e6dc5c68ea055000c3c99a11789438b78e837.source +43 -0
  67. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d32c24d61eb8b33c73e5a8fc7e4dc7ef437aed238ee4371c2b00617e6104090a.source +154 -0
  68. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d5170dc9a9879fda04fdf91d8a110d4efb6787ee38dda596d4da899883d36a72.source +531 -0
  69. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/dc59bcd7bb397be63ba2f2c3fec23f3782dcb17d2d75a9981eeced761f33eb32.source +439 -0
  70. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/eb5ff8cf66b92aec7bf6726781d8df74faa2b030c3aaac17d6b00c1fc04c9b32.source +323 -0
  71. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f10615a6fae18436e70d1201b2d44fdae3174a3aa7bb4db97dea1b3ed291d721.source +433 -0
  72. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f36d3e271668bc1260c2823542d22dbf1d0f7041ff16b1ac175bce2256e582ca.source +280 -0
  73. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f94449380af09f31ac352152007146a75cc80bae8d6411ffe57b2c56f933b0ff.source +117 -0
  74. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/fe4dfd81178fd6b648335f6405d9ede71319aebde796008e50db21f8adf44c99.source +160 -0
  75. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/ffb4e3aa548cfdadabaa44ab599f9b6a3ca6288176a2cc7a4b2fc2a12b5fb848.source +101 -0
  76. package/scripts/_audit/detached-start.mjs +103 -0
  77. package/scripts/_audit/guard-upstream-liveness.mjs +104 -0
  78. package/scripts/_dbg-perms-e2e.mjs +10 -3
  79. package/scripts/_dbg-protocol-matrix.mjs +453 -441
  80. package/scripts/_dbg-render-regression.mjs +151 -130
  81. package/scripts/_dbg-town-agents-probe.mjs +60 -0
  82. package/scripts/_dbg-visual-acceptance.mjs +63 -0
  83. package/scripts/e2e-agent-team.mjs +5 -3
  84. package/scripts/test-plugin-hardening.mjs +32 -0
  85. package/scripts/three-system-e2e.mjs +6 -6
  86. package/server/plugins/dev-stability-guard.ts +40 -63
  87. package/server/plugins-builtin/rag-bridge/index.mjs +9 -2
  88. package/server/utils/stability-severity.ts +66 -0
  89. package/.output/public/_nuxt/DVsjtsvZ2.js +0 -1
  90. package/.output/public/_nuxt/builds/meta/a66fdef7-dcfd-453b-b1f6-543c7dfd98d5.json +0 -1
  91. package/.output/public/_nuxt/default.DE_vT238.css +0 -1
@@ -0,0 +1,101 @@
1
+ /**
2
+ * 动态记忆感知测试:按需抓取(recallRows scope 过滤/原文还原/touch)、
3
+ * 主动沉淀(save 私有/共享分流 + 共享域 dedupKey 命名空间)、
4
+ * 记忆引子(小预算 primer + 工具提示行)。
5
+ * 运行: npx tsx scripts/test-memory-dynamic.ts
6
+ */
7
+ import type { DatabaseSync } from 'node:sqlite'
8
+ import { openWorkshopDb } from '../server/services/workshop/db/database'
9
+ import { createMemoryRepo, TEAM_AGENT_ID } from '../server/services/workshop/db/memory.repo'
10
+ import { AgentMemory } from '../server/services/workshop/runtime/memory'
11
+
12
+ let failures = 0
13
+ function check(name: string, ok: boolean, detail = ''): void {
14
+ console.log(` ${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
15
+ if (!ok) failures += 1
16
+ }
17
+
18
+ const db: DatabaseSync = openWorkshopDb(':memory:')
19
+ const repo = createMemoryRepo(db)
20
+ const seg = (s: string): string => s.replace(/([\u4e00-\u9fff])/g, ' $1 ')
21
+
22
+ // 播种:a1 私有 × 2、a2 私有 × 1、team 公共 × 1、他 channel 的 team 行 × 1(隔离守卫)
23
+ repo.upsert({ channelId: 'ch1', agentId: 'a1', kind: 'episodic-task', title: '网关限流方案', titleFts: seg('网关限流方案'), content: seg('最终采用令牌桶算法限流,阈值100qps'), importance: 0.8, taskId: 't1', dedupKey: 'task:t1' })
24
+ repo.upsert({ channelId: 'ch1', agentId: 'a1', kind: 'episodic-task', title: '部署脚本草稿', titleFts: seg('部署脚本草稿'), content: seg('deploy.sh 需要先source env'), importance: 0.4, taskId: 't2', dedupKey: 'task:t2' })
25
+ repo.upsert({ channelId: 'ch1', agentId: 'a2', kind: 'episodic-task', title: '他人限流笔记', titleFts: seg('他人限流笔记'), content: seg('a2自己的限流杂记'), importance: 0.8, taskId: 't3', dedupKey: 'task:t3' })
26
+ repo.upsert({ channelId: 'ch1', agentId: TEAM_AGENT_ID, kind: 'semantic', title: 'channel 限流约定', titleFts: seg('channel 限流约定'), content: seg('全channel统一用令牌桶,禁用计数器'), importance: 0.9, taskId: null, dedupKey: 'conv:ratelimit' })
27
+ repo.upsert({ channelId: 'ch2', agentId: TEAM_AGENT_ID, kind: 'semantic', title: '他channel限流约定', titleFts: seg('他channel限流约定'), content: seg('ch2专属公共记忆不应被ch1看到'), importance: 0.9, taskId: null, dedupKey: 'conv:other' })
28
+
29
+ const memA1 = new AgentMemory(repo, { channelId: 'ch1', agentId: 'a1' })
30
+
31
+ console.log('--- 按需抓取 recallRows ---')
32
+ {
33
+ const auto = await memA1.recallRows('限流 令牌桶')
34
+ check('auto 域命中本人 + 公共记忆', auto.some(s => s.title === '网关限流方案') && auto.some(s => s.title === 'channel 限流约定'), JSON.stringify(auto.map(s => s.title)))
35
+ check('auto 域不含他人私有记忆', auto.every(s => s.title !== '他人限流笔记'))
36
+ check('auto 域不含他 channel 公共记忆', auto.every(s => s.title !== '他channel限流约定'))
37
+ check('source 标注 private/shared', auto.some(s => s.source === 'private') && auto.some(s => s.source === 'shared'))
38
+ const own = auto.find(s => s.title === '网关限流方案')
39
+ check('content 还原为未切分原文', own !== undefined && !/\s/.test(own.content.slice(0, 4)) && own.content.includes('令牌桶'), own?.content.slice(0, 20))
40
+ check('返回综合分 score', auto.every(s => typeof s.score === 'number' && s.score > 0))
41
+
42
+ const sharedOnly = await memA1.recallRows('限流', { scope: 'shared' })
43
+ check('shared 域仅公共记忆', sharedOnly.length > 0 && sharedOnly.every(s => s.source === 'shared'), JSON.stringify(sharedOnly.map(s => s.title)))
44
+ check('shared 域严格本 channel', sharedOnly.every(s => s.title !== '他channel限流约定'))
45
+
46
+ const privOnly = await memA1.recallRows('限流', { scope: 'private' })
47
+ check('private 域仅本人记忆', privOnly.length > 0 && privOnly.every(s => s.source === 'private'), JSON.stringify(privOnly.map(s => s.title)))
48
+
49
+ const before = repo.listByAgent('a1', 10).find(r => r.title === '网关限流方案')!.accessCount
50
+ await memA1.recallRows('网关 限流')
51
+ const after = repo.listByAgent('a1', 10).find(r => r.title === '网关限流方案')!.accessCount
52
+ check('recallRows 触发 touch(强化后续排序)', after === before + 1, `before=${before} after=${after}`)
53
+
54
+ // scope 域内完全无记忆才真正返回空(全新 channel;有记忆时弱命中/时近兜底会返回行,by design)
55
+ const none = await new AgentMemory(repo, { channelId: 'ch-empty', agentId: 'nobody' }).recallRows('完全不存在的关键词xyzq')
56
+ check('无命中返回空数组(无记忆 agent)', Array.isArray(none) && none.length === 0)
57
+ const fallback = await memA1.recallRows('完全不存在的关键词xyzq')
58
+ check('FTS 无命中走时近兜底(有记忆 agent 返回最近行)', fallback.length > 0, JSON.stringify(fallback.map(s => s.title)))
59
+ }
60
+
61
+ console.log('\n--- 主动沉淀 save(私有/共享分流)---')
62
+ {
63
+ const priv = await memA1.save({ title: 'SQLite 并发写坑', content: '并发写会 SQLITE_BUSY,须串行化写入', scope: 'private', dedupKey: 'note:sqlite' })
64
+ check('私有沉淀落入本人域', repo.findByAgentDedup('ch1', 'a1', 'note:sqlite') !== null && priv.scope === 'private')
65
+
66
+ const shared = await memA1.save({ title: '限流阈值结论', content: '全队统一令牌桶阈值100qps', scope: 'shared', dedupKey: 'conv:threshold' })
67
+ const teamRow = repo.findByAgentDedup('ch1', TEAM_AGENT_ID, `agent:a1:conv:threshold`)
68
+ check('共享沉淀落入 team 公共域(来源命名空间 dedupKey)', teamRow !== null && shared.dedupKey === 'agent:a1:conv:threshold')
69
+
70
+ // 同 raw key 不同 agent:命名空间隔离互不覆盖
71
+ const memA2 = new AgentMemory(repo, { channelId: 'ch1', agentId: 'a2' })
72
+ await memA2.save({ title: 'a2 的阈值结论', content: 'a2认为阈值应按服务分级', scope: 'shared', dedupKey: 'conv:threshold' })
73
+ const teamRows = repo.listByAgentChannel('ch1', TEAM_AGENT_ID, 100).filter(r => r.content.includes('阈值') || r.title.includes('阈值'))
74
+ check('多 agent 同 raw key 共享沉淀互不覆盖', repo.findByAgentDedup('ch1', TEAM_AGENT_ID, 'agent:a1:conv:threshold') !== null && repo.findByAgentDedup('ch1', TEAM_AGENT_ID, 'agent:a2:conv:threshold') !== null, `rows=${teamRows.length}`)
75
+
76
+ // 共享沉淀即时对全员可检索
77
+ const a2sees = await memA2.recallRows('阈值 令牌桶', { scope: 'shared' })
78
+ check('共享沉淀对其他 agent 即时可检索', a2sees.some(s => s.title === '限流阈值结论'), JSON.stringify(a2sees.map(s => s.title)))
79
+
80
+ // 稳定 dedupKey 幂等刷新不重复
81
+ await memA1.save({ title: '限流阈值结论', content: '修订:阈值升到200qps', scope: 'shared', dedupKey: 'conv:threshold' })
82
+ const count = repo.listByAgentChannel('ch1', TEAM_AGENT_ID, 100).filter(r => r.dedupKey === undefined && r.title === '限流阈值结论').length
83
+ const refreshed = repo.listByAgentChannel('ch1', TEAM_AGENT_ID, 100).some(r => r.title === '限流阈值结论')
84
+ check('稳定 dedupKey 幂等刷新', refreshed && count <= 1)
85
+ }
86
+
87
+ console.log('\n--- 记忆引子 primer(小预算注入)---')
88
+ {
89
+ const block = await memA1.recall('限流 令牌桶 部署', { touch: false })
90
+ check('primer 返回记忆块', block !== null)
91
+ check('primer 含按需抓取工具提示', block !== null && block.includes('search_memory'), block?.slice(-80))
92
+ // v2 三层注入:L0 简报(save(shared) 后自动存在)+ L1 引子 + 提示,同在 300 tok 引子预算 + 500 tok 总预算内
93
+ check('primer 含 L0 会话简报', block !== null && block.includes('会话简报'))
94
+ check('primer 小预算(行数受限)', block !== null && block.split('\n').length < 16, `lines=${block?.split('\n').length}`)
95
+
96
+ const memTiny = new AgentMemory(repo, { channelId: 'ch1', agentId: 'a1', budgetTokens: 5 })
97
+ check('极小预算 primer 无行则 null', (await memTiny.recall('限流')) === null)
98
+ }
99
+
100
+ console.log(failures === 0 ? '\nALL PASS' : `\n${failures} FAILED`)
101
+ process.exit(failures === 0 ? 0 : 1)
@@ -0,0 +1,103 @@
1
+ /**
2
+ * 把 `aw start` 以**真分离**方式拉起(脱离 agent 作业的进程组),日志落文件。
3
+ *
4
+ * 为什么需要它:agent 工具的后台作业在回合结束/被回收时会连子进程一起终止,
5
+ * 而 `aw start` 的 stdout 缓冲区随之丢失 —— 表现为「日志停在半路、无 fatal 行、
6
+ * 端口释放、退出码 1」,极易误判成服务自己崩了(实测踩过两次)。
7
+ *
8
+ * 做法:node 的 spawn + detached + stdio 重定向到文件。
9
+ * Windows 上还要 unref 并用 windowsHide,否则父进程退出时子进程一起走。
10
+ *
11
+ * 用法:
12
+ * node scripts/_audit/detached-start.mjs [--log <path>] [--cwd <dir>] [--port 3001] [--wait 45]
13
+ * node scripts/_audit/detached-start.mjs --status # 只看健康门与日志尾
14
+ * node scripts/_audit/detached-start.mjs --stop # 停掉分离实例
15
+ */
16
+ import { spawn, spawnSync } from 'node:child_process'
17
+ import { closeSync, existsSync, openSync, readFileSync, statSync } from 'node:fs'
18
+ import { dirname, join, resolve } from 'node:path'
19
+ import { fileURLToPath } from 'node:url'
20
+
21
+ const arg = (k, d) => {
22
+ const i = process.argv.indexOf(k)
23
+ return i >= 0 ? process.argv[i + 1] : d
24
+ }
25
+ const repo = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..')
26
+ const CWD = resolve(arg('--cwd', repo))
27
+ const PORT = Number(arg('--port', '3001'))
28
+ const LOG = resolve(arg('--log', join(CWD, '..', 'aw-detached.log')))
29
+ const PIDFILE = LOG + '.pid'
30
+ const BASE = `http://127.0.0.1:${PORT}`
31
+
32
+ const health = async () => {
33
+ try {
34
+ const r = await fetch(`${BASE}/api/health`, { signal: AbortSignal.timeout(6000) })
35
+ const j = await r.json()
36
+ return j?.data?.status === 'ok' ? { ok: true, version: j.data.version, uptimeMs: j.data.uptimeMs } : { ok: false }
37
+ }
38
+ catch { return { ok: false } }
39
+ }
40
+
41
+ if (process.argv.includes('--status')) {
42
+ const h = await health()
43
+ console.log(h.ok ? `✔ 健康 version=${h.version} uptime=${Math.round(h.uptimeMs / 1000)}s` : '✖ 不可达')
44
+ const pid = existsSync(PIDFILE) ? readFileSync(PIDFILE, 'utf8').trim() : '(无 pid 文件)'
45
+ console.log(` 分离 pid 文件: ${pid}`)
46
+ if (existsSync(LOG)) {
47
+ const size = statSync(LOG).size
48
+ const tail = readFileSync(LOG, 'utf8').split(/\r?\n/).slice(-6)
49
+ console.log(` 日志 ${LOG} (${(size / 1024).toFixed(1)} KB) 尾部:`)
50
+ for (const l of tail) if (l.trim()) console.log(' ' + l.slice(0, 150))
51
+ }
52
+ process.exitCode = h.ok ? 0 : 1
53
+ }
54
+
55
+ else if (process.argv.includes('--stop')) {
56
+ const pid = existsSync(PIDFILE) ? Number(readFileSync(PIDFILE, 'utf8').trim()) : NaN
57
+ if (Number.isInteger(pid)) {
58
+ const r = spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore' })
59
+ console.log(r.status === 0 ? `✔ 已终止分离实例 pid=${pid}` : `· taskkill 退出码 ${r.status}(进程可能已退出)`)
60
+ }
61
+ else console.log('· 无 pid 文件,改用 aw stop')
62
+ if (!Number.isInteger(pid)) spawnSync('aw', ['stop'], { cwd: CWD, stdio: 'inherit', shell: true })
63
+ }
64
+
65
+ else {
66
+ const pre = await health()
67
+ if (pre.ok) {
68
+ console.log(`· ${BASE} 已有健康实例(version=${pre.version}),不重复启动`)
69
+ process.exitCode = 0
70
+ }
71
+ else {
72
+ const out = openSync(LOG, 'a')
73
+ // 只清大小写重复的代理变量:7890 会拦 localhost
74
+ const env = { ...process.env, NO_PROXY: '127.0.0.1,localhost', HTTP_PROXY: '', HTTPS_PROXY: '' }
75
+ const child = spawn(process.execPath, [join(repo, 'bin', 'aw.mjs'), 'start', '--port', String(PORT)], {
76
+ cwd: CWD,
77
+ env,
78
+ detached: true,
79
+ windowsHide: true,
80
+ stdio: ['ignore', out, out],
81
+ })
82
+ closeSync(out)
83
+ child.unref()
84
+ const { writeFileSync } = await import('node:fs')
85
+ writeFileSync(PIDFILE, String(child.pid), 'utf8')
86
+ console.log(`✔ 已分离启动 pid=${child.pid} → ${BASE},日志 ${LOG}`)
87
+
88
+ const waitS = Number(arg('--wait', '45'))
89
+ for (let i = 0; i < waitS; i += 3) {
90
+ await new Promise(r => setTimeout(r, 3000))
91
+ const h = await health()
92
+ if (h.ok) {
93
+ console.log(`✔ 健康门通过 version=${h.version}(${(i + 3)}s)`)
94
+ process.exitCode = 0
95
+ break
96
+ }
97
+ }
98
+ if (process.exitCode === undefined) {
99
+ console.error(`✖ ${waitS}s 内未通过健康门,看日志:${LOG}`)
100
+ process.exitCode = 1
101
+ }
102
+ }
103
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * 稳定性护栏端到端验证:外部设备不可达时,服务端**必须存活**。
3
+ *
4
+ * 复现的真实事故:一台 Modbus TCP 设备未启动(connect ECONNREFUSED 127.0.0.1:1502)
5
+ * 触发 unhandledRejection 走到 fatal 分支 → 整个生产实例 exit 1 →
6
+ * 数采/数控/数字孪生全平台被一台设备带走。
7
+ *
8
+ * 做法:
9
+ * 1. 建立指向**必然拒绝连接**的端口的 Modbus TCP 数采节点(默认 1502,可 --port 覆盖)
10
+ * 2. 等采集轮询真正发起连接(默认 25s)
11
+ * 3. 断言:健康门仍 200、进程未死、日志出现 upstream 分级记录
12
+ *
13
+ * 用法:node scripts/_audit/guard-upstream-liveness.mjs [--base http://127.0.0.1:3001] [--port 1502] [--wait 25] [--keep]
14
+ */
15
+ const arg = (k, d) => {
16
+ const i = process.argv.indexOf(k)
17
+ return i >= 0 ? process.argv[i + 1] : d
18
+ }
19
+ const BASE = arg('--base', 'http://127.0.0.1:3001')
20
+ const DEAD_PORT = Number(arg('--port', '1502'))
21
+ const WAIT_S = Number(arg('--wait', '25'))
22
+ const KEEP = process.argv.includes('--keep')
23
+ const TOKEN = process.env.AW_E2E_TOKEN
24
+ ?? (await (await fetch(`${BASE}/api/users/login`, {
25
+ method: 'POST',
26
+ headers: { 'content-type': 'application/json' },
27
+ body: JSON.stringify({ email: 'admin@awshop.local', password: 'admin123' }),
28
+ })).json()).data.token
29
+
30
+ const sleep = ms => new Promise(r => setTimeout(r, ms))
31
+ const api = async (method, path, body) => {
32
+ const r = await fetch(BASE + path, {
33
+ method,
34
+ headers: { 'content-type': 'application/json', 'authorization': `Bearer ${TOKEN}` },
35
+ body: body === undefined ? undefined : JSON.stringify(body),
36
+ })
37
+ return r.json().catch(() => ({}))
38
+ }
39
+
40
+ let pass = 0
41
+ let fail = 0
42
+ const check = (name, ok, detail = '') => {
43
+ console.log(` ${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
44
+ ok ? pass++ : fail++
45
+ }
46
+
47
+ const health = async () => {
48
+ try {
49
+ const r = await fetch(`${BASE}/api/health`, { signal: AbortSignal.timeout(5000) })
50
+ const j = await r.json()
51
+ return { ok: r.ok && j?.data?.status === 'ok', version: j?.data?.version }
52
+ }
53
+ catch (e) {
54
+ return { ok: false, error: String(e?.message ?? e) }
55
+ }
56
+ }
57
+
58
+ console.log(`\n━━━ 稳定性护栏:外部设备不可达不应带走服务(设备端口 :${DEAD_PORT}) ━━━`)
59
+ const before = await health()
60
+ check('前置:服务健康', before.ok, `version=${before.version}`)
61
+
62
+ // 1) 建一条指向死端口的 Modbus TCP 节点。字段形状对齐既有节点:
63
+ // templateRef 必填(信号模板)、地址走 driverConfig.register。
64
+ const ts = Date.now()
65
+ const line = await api('POST', '/api/workshop/dcw/lines', { name: `护栏验证线-${ts}`, productId: null })
66
+ const lineId = line?.data?.line?.id ?? line?.data?.id
67
+ check('创建验证产线', !!lineId, lineId ?? JSON.stringify(line).slice(0, 120))
68
+
69
+ const node = await api('POST', '/api/workshop/daq', {
70
+ lineId,
71
+ name: `死端口节点-${ts}`,
72
+ templateRef: arg('--template', 'daq-temp-tc'),
73
+ driver: 'modbus-tcp',
74
+ unit: '℃',
75
+ intervalMs: 500,
76
+ enabled: true,
77
+ driverConfig: { host: '127.0.0.1', port: DEAD_PORT, unitId: 1, register: 40001, dataType: 'float32', byteOrder: 'big' },
78
+ })
79
+ const nodeId = node?.data?.node?.id ?? node?.data?.id
80
+ check('创建指向必然拒绝连接端口的数采节点', !!nodeId, nodeId ?? JSON.stringify(node).slice(0, 200))
81
+
82
+ // 2) 开跑,让轮询真正去连那个端口
83
+ if (lineId) await api('POST', `/api/workshop/dcw/lines/${lineId}/start`, {})
84
+ console.log(` · 等待 ${WAIT_S}s 让采集轮询发起连接 ...`)
85
+ await sleep(WAIT_S * 1000)
86
+
87
+ // 3) 核心断言:服务还活着
88
+ const after = await health()
89
+ check('设备不可达后服务仍健康(修复前此处 exit 1)', after.ok, after.ok ? `version=${after.version}` : `unreachable: ${after.error}`)
90
+
91
+ const opened = await api('GET', `/api/workshop/daq/${nodeId}/samples?from=${Date.now() - WAIT_S * 1000}&bucketMs=1000&limit=5`).catch(() => null)
92
+ check('节点面仍可查询(平台未被带走)', opened !== null)
93
+
94
+ // 4) 清理
95
+ if (!KEEP && lineId) {
96
+ await api('POST', `/api/workshop/dcw/lines/${lineId}/stop`, {}).catch(() => null)
97
+ await api('DELETE', `/api/workshop/daq/${nodeId}`).catch(() => null)
98
+ await api('DELETE', `/api/workshop/dcw/lines/${lineId}`).catch(() => null)
99
+ console.log(' · 验证用产线/节点已清理')
100
+ }
101
+ else if (KEEP) console.log(` · KEEP=1,保留现场:line=${lineId} node=${nodeId}`)
102
+
103
+ console.log(`\n━━━ 结果:${pass} PASS / ${fail} FAIL ━━━\n`)
104
+ process.exitCode = fail === 0 ? 0 : 1
@@ -70,13 +70,20 @@ const wr2 = await api(`/api/workshop/dcw/${w2.id}/write`, { method: 'POST', body
70
70
  ok('operate 产线写控放行', wr2.status === 200 && wr2.body?.data?.outcome != null, `ack=${JSON.stringify(wr2.body?.data?.outcome)?.slice(0, 60)}`)
71
71
 
72
72
  // ---- 绑定校验(daq 需 readonly+,dcw 需 operate) ----
73
+ // 绑定主体是频道成员实例 id(0.7.37 绑定主体校验):plain 用户自建频道+成员,取实例 id
74
+ const bindCh = await api('/api/workshop/channels', { method: 'POST', body: JSON.stringify({ name: `e2e-perms-ch-${Date.now()}`, leadAgent: { name: 'e2e-perms-lead', harness: 'mock' } }) }, plainTok)
75
+ const bindTpl = await api('/api/workshop/agents', { method: 'POST', body: JSON.stringify({ name: 'e2e-perms-worker', harness: 'mock' }) }, plainTok)
76
+ const bindJoin = await api(`/api/workshop/channels/${bindCh.body?.data?.channelId}/agents`, { method: 'POST', body: JSON.stringify({ agentId: bindTpl.body?.data?.id, role: 'worker' }) }, plainTok)
77
+ const bindAgentId = bindJoin.body?.data?.id
78
+ ok('plain 自建频道成员(绑定主体)', Boolean(bindAgentId), bindJoin.body?.message ?? '')
79
+
73
80
  const daqNodes = q1.body.data.nodes
74
81
  const dq1 = daqNodes.find(n => n.lineId === LINE1)
75
- const bindD = await api('/api/workshop/agent-tools/bindings', { method: 'POST', body: JSON.stringify({ agentId: 'e2e-perms-agent', nodeId: dq1.id, kind: 'daq' }) }, plainTok)
82
+ const bindD = await api('/api/workshop/agent-tools/bindings', { method: 'POST', body: JSON.stringify({ agentId: bindAgentId, nodeId: dq1.id, kind: 'daq' }) }, plainTok)
76
83
  ok('绑定 daq 节点(readonly 线)放行', bindD.status === 200, JSON.stringify(bindD.body?.message ?? ''))
77
- const bindW1 = await api('/api/workshop/agent-tools/bindings', { method: 'POST', body: JSON.stringify({ agentId: 'e2e-perms-agent', nodeId: w1.id, kind: 'dcw' }) }, plainTok)
84
+ const bindW1 = await api('/api/workshop/agent-tools/bindings', { method: 'POST', body: JSON.stringify({ agentId: bindAgentId, nodeId: w1.id, kind: 'dcw' }) }, plainTok)
78
85
  ok('绑定 dcw 节点(readonly 线)被拒', bindW1.status === 403, bindW1.body?.message)
79
- const bindW2 = await api('/api/workshop/agent-tools/bindings', { method: 'POST', body: JSON.stringify({ agentId: 'e2e-perms-agent', nodeId: w2.id, kind: 'dcw' }) }, plainTok)
86
+ const bindW2 = await api('/api/workshop/agent-tools/bindings', { method: 'POST', body: JSON.stringify({ agentId: bindAgentId, nodeId: w2.id, kind: 'dcw' }) }, plainTok)
80
87
  ok('绑定 dcw 节点(operate 线)放行', bindW2.status === 200)
81
88
 
82
89
  // ---- 越权访问无权产线节点(直接按 id 打,数据面不因列表隐藏而放行) ----