agentworkshop 0.8.3 → 0.8.4

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 (196) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_nuxt/B4iJT5PX2.js +2 -0
  3. package/.output/public/_nuxt/BNUaS1jv.js +20 -0
  4. package/.output/public/_nuxt/BhJA-E372.js +1 -0
  5. package/.output/public/_nuxt/Bq8YzXzs2.js +1 -0
  6. package/.output/public/_nuxt/Cf7ER7e2.js +1 -0
  7. package/.output/public/_nuxt/{DrlnrdvY2.js → DPnjnDtg.js} +1 -1
  8. package/.output/public/_nuxt/DgTqKyYF2.js +1 -0
  9. package/.output/public/_nuxt/MmlCf-EZ.js +1 -0
  10. package/.output/public/_nuxt/builds/latest.json +1 -1
  11. package/.output/public/_nuxt/builds/meta/8a151630-3285-41b8-a1e5-063fee2002ac.json +1 -0
  12. package/.output/public/_nuxt/{entry.C7koBPom.css → entry.D6nBdXet.css} +1 -1
  13. package/.output/public/_nuxt/{ontology.JqJ6PjV1.css → ontology.D4P756Tc.css} +1 -1
  14. package/.output/public/_nuxt/pipeline.NFs5z5V-.css +1 -0
  15. package/.output/server/chunks/_/character-asset.repo.mjs +1 -1
  16. package/.output/server/chunks/_/chart-repo.mjs +1 -1
  17. package/.output/server/chunks/_/chart-repo.mjs.map +1 -1
  18. package/.output/server/chunks/_/eda-db.mjs +12 -0
  19. package/.output/server/chunks/_/eda-db.mjs.map +1 -1
  20. package/.output/server/chunks/_/host-bindings.mjs +1 -1
  21. package/.output/server/chunks/_/nitro.mjs +1977 -1951
  22. package/.output/server/chunks/_/scene-layout.repo.mjs +1 -1
  23. package/.output/server/chunks/_/template-repo.mjs +97 -0
  24. package/.output/server/chunks/_/template-repo.mjs.map +1 -0
  25. package/.output/server/chunks/build/{TownView-CUbzOT_M.mjs → TownView-CcUPLqF1.mjs} +3 -3
  26. package/.output/server/chunks/build/{TownView-CUbzOT_M.mjs.map → TownView-CcUPLqF1.mjs.map} +1 -1
  27. package/.output/server/chunks/build/{_id_-JaGx0yBE.mjs → _id_-AgACEyL6.mjs} +13 -13
  28. package/.output/server/chunks/build/{_id_-JaGx0yBE.mjs.map → _id_-AgACEyL6.mjs.map} +1 -1
  29. package/.output/server/chunks/build/{_id_-BHWZiRQZ.mjs → _id_-B3yCK2lD.mjs} +8 -8
  30. package/.output/server/chunks/build/{_id_-BHWZiRQZ.mjs.map → _id_-B3yCK2lD.mjs.map} +1 -1
  31. package/.output/server/chunks/build/{_wsId_-C4DKkEqv.mjs → _wsId_-kcDriA4i.mjs} +4 -4
  32. package/.output/server/chunks/build/{_wsId_-C4DKkEqv.mjs.map → _wsId_-kcDriA4i.mjs.map} +1 -1
  33. package/.output/server/chunks/build/{analysis-tNpFLtMt.mjs → analysis-CMsMTMCg.mjs} +3 -3
  34. package/.output/server/chunks/build/{analysis-tNpFLtMt.mjs.map → analysis-CMsMTMCg.mjs.map} +1 -1
  35. package/.output/server/chunks/build/{daq-DHsYtWC-.mjs → daq-DFQQOtcZ.mjs} +7 -7
  36. package/.output/server/chunks/build/{daq-DHsYtWC-.mjs.map → daq-DFQQOtcZ.mjs.map} +1 -1
  37. package/.output/server/chunks/build/{dcw-DnjOM6p1.mjs → dcw-8415S3Km.mjs} +7 -7
  38. package/.output/server/chunks/build/{dcw-DnjOM6p1.mjs.map → dcw-8415S3Km.mjs.map} +1 -1
  39. package/.output/server/chunks/build/{logs-C4vKFkHQ.mjs → logs-C-dWpJ80.mjs} +3 -3
  40. package/.output/server/chunks/build/{logs-C4vKFkHQ.mjs.map → logs-C-dWpJ80.mjs.map} +1 -1
  41. package/.output/server/chunks/build/{ontology-B_0Vm6j-.mjs → ontology-DoXAsa2y.mjs} +62 -62
  42. package/.output/server/chunks/build/ontology-DoXAsa2y.mjs.map +1 -0
  43. package/.output/server/chunks/build/{operations-D8D6-Jm-.mjs → operations-Ch6tZIKo.mjs} +3 -3
  44. package/.output/server/chunks/build/{operations-D8D6-Jm-.mjs.map → operations-Ch6tZIKo.mjs.map} +1 -1
  45. package/.output/server/chunks/build/{pages-Dac4icQC.mjs → pages-CQOfwKB4.mjs} +4 -4
  46. package/.output/server/chunks/build/{pages-Dac4icQC.mjs.map → pages-CQOfwKB4.mjs.map} +1 -1
  47. package/.output/server/chunks/build/pipeline-eo2RWN3f.mjs +226 -0
  48. package/.output/server/chunks/build/pipeline-eo2RWN3f.mjs.map +1 -0
  49. package/.output/server/chunks/build/{town-LkDwWnKA.mjs → town-DLFB4SZl.mjs} +4 -4
  50. package/.output/server/chunks/build/{town-LkDwWnKA.mjs.map → town-DLFB4SZl.mjs.map} +1 -1
  51. package/.output/server/chunks/build/{useDaqAddNode-CRorxgOY.mjs → useDaqAddNode-D_1EBuR3.mjs} +2 -2
  52. package/.output/server/chunks/build/{useDaqAddNode-CRorxgOY.mjs.map → useDaqAddNode-D_1EBuR3.mjs.map} +1 -1
  53. package/.output/server/chunks/build/{useDaqDetailBinding-Ck-YUW9J.mjs → useDaqDetailBinding-j05iSApd.mjs} +2 -2
  54. package/.output/server/chunks/build/{useDaqDetailBinding-Ck-YUW9J.mjs.map → useDaqDetailBinding-j05iSApd.mjs.map} +1 -1
  55. package/.output/server/chunks/build/{useDaqDetailFrames-CLMoreaz.mjs → useDaqDetailFrames-BKciVJ78.mjs} +2 -2
  56. package/.output/server/chunks/build/{useDaqDetailFrames-CLMoreaz.mjs.map → useDaqDetailFrames-BKciVJ78.mjs.map} +1 -1
  57. package/.output/server/chunks/build/{useDaqDetailHistory-BVNQXwC_.mjs → useDaqDetailHistory-DgemICkr.mjs} +2 -2
  58. package/.output/server/chunks/build/{useDaqDetailHistory-BVNQXwC_.mjs.map → useDaqDetailHistory-DgemICkr.mjs.map} +1 -1
  59. package/.output/server/chunks/build/{useDaqDetailNode-BC0e9OfX.mjs → useDaqDetailNode-Cn7_og6t.mjs} +3 -3
  60. package/.output/server/chunks/build/{useDaqDetailNode-BC0e9OfX.mjs.map → useDaqDetailNode-Cn7_og6t.mjs.map} +1 -1
  61. package/.output/server/chunks/build/{useDaqDetailParams-D173iwFg.mjs → useDaqDetailParams-DX_vjFyK.mjs} +2 -2
  62. package/.output/server/chunks/build/{useDaqDetailParams-D173iwFg.mjs.map → useDaqDetailParams-DX_vjFyK.mjs.map} +1 -1
  63. package/.output/server/chunks/build/{useDaqFilters-CC-o_21d.mjs → useDaqFilters-DMS4GN20.mjs} +3 -3
  64. package/.output/server/chunks/build/{useDaqFilters-CC-o_21d.mjs.map → useDaqFilters-DMS4GN20.mjs.map} +1 -1
  65. package/.output/server/chunks/build/{useDaqOptimizations-4vK2w1yn.mjs → useDaqOptimizations-OPbdy5YB.mjs} +2 -2
  66. package/.output/server/chunks/build/{useDaqOptimizations-4vK2w1yn.mjs.map → useDaqOptimizations-OPbdy5YB.mjs.map} +1 -1
  67. package/.output/server/chunks/build/{useDaqStream-BHKpSJFd.mjs → useDaqStream-CS1G6iro.mjs} +1 -1
  68. package/.output/server/chunks/build/{useDaqStream-BHKpSJFd.mjs.map → useDaqStream-CS1G6iro.mjs.map} +1 -1
  69. package/.output/server/chunks/build/{useDaqTemplates-CsXSc7yI.mjs → useDaqTemplates-DqhRYo88.mjs} +2 -2
  70. package/.output/server/chunks/build/{useDaqTemplates-CsXSc7yI.mjs.map → useDaqTemplates-DqhRYo88.mjs.map} +1 -1
  71. package/.output/server/chunks/build/{useDashboardData-DuKjuJTz.mjs → useDashboardData-CeaMjEA9.mjs} +3 -3
  72. package/.output/server/chunks/build/{useDashboardData-DuKjuJTz.mjs.map → useDashboardData-CeaMjEA9.mjs.map} +1 -1
  73. package/.output/server/chunks/build/{useDcwAddNode-C5DcxsHF.mjs → useDcwAddNode-Cytz_odQ.mjs} +2 -2
  74. package/.output/server/chunks/build/{useDcwAddNode-C5DcxsHF.mjs.map → useDcwAddNode-Cytz_odQ.mjs.map} +1 -1
  75. package/.output/server/chunks/build/{useDcwCards-0bAVo6uB.mjs → useDcwCards-BKCFqkRd.mjs} +2 -2
  76. package/.output/server/chunks/build/{useDcwCards-0bAVo6uB.mjs.map → useDcwCards-BKCFqkRd.mjs.map} +1 -1
  77. package/.output/server/chunks/build/{useDcwDetailScope-BNFOlt2v.mjs → useDcwDetailScope-CvdvcKAv.mjs} +3 -3
  78. package/.output/server/chunks/build/{useDcwDetailScope-BNFOlt2v.mjs.map → useDcwDetailScope-CvdvcKAv.mjs.map} +1 -1
  79. package/.output/server/chunks/build/{useDcwDetailTemplates-DSH-o0tE.mjs → useDcwDetailTemplates-CC2FcEge.mjs} +2 -2
  80. package/.output/server/chunks/build/{useDcwDetailTemplates-DSH-o0tE.mjs.map → useDcwDetailTemplates-CC2FcEge.mjs.map} +1 -1
  81. package/.output/server/chunks/build/{useDcwFilter-W61BFUcT.mjs → useDcwFilter-CVoiXbZh.mjs} +2 -2
  82. package/.output/server/chunks/build/{useDcwFilter-W61BFUcT.mjs.map → useDcwFilter-CVoiXbZh.mjs.map} +1 -1
  83. package/.output/server/chunks/build/{useDcwLedger-DsColLdc.mjs → useDcwLedger-Cd-ckHoJ.mjs} +2 -2
  84. package/.output/server/chunks/build/{useDcwLedger-DsColLdc.mjs.map → useDcwLedger-Cd-ckHoJ.mjs.map} +1 -1
  85. package/.output/server/chunks/build/{useDcwLineMutations-Cx4Ir7g3.mjs → useDcwLineMutations-BOQs-fw3.mjs} +2 -2
  86. package/.output/server/chunks/build/{useDcwLineMutations-Cx4Ir7g3.mjs.map → useDcwLineMutations-BOQs-fw3.mjs.map} +1 -1
  87. package/.output/server/chunks/build/{useDcwLineRun-DQ0vB08z.mjs → useDcwLineRun-D9qvcyTM.mjs} +2 -2
  88. package/.output/server/chunks/build/{useDcwLineRun-DQ0vB08z.mjs.map → useDcwLineRun-D9qvcyTM.mjs.map} +1 -1
  89. package/.output/server/chunks/build/{useDcwParamApprovals-CYspRuDk.mjs → useDcwParamApprovals-D36q4can.mjs} +2 -2
  90. package/.output/server/chunks/build/{useDcwParamApprovals-CYspRuDk.mjs.map → useDcwParamApprovals-D36q4can.mjs.map} +1 -1
  91. package/.output/server/chunks/build/{useDcwProducts-CTbm_5S_.mjs → useDcwProducts-BozctrwT.mjs} +2 -2
  92. package/.output/server/chunks/build/{useDcwProducts-CTbm_5S_.mjs.map → useDcwProducts-BozctrwT.mjs.map} +1 -1
  93. package/.output/server/chunks/build/{useDcwQuery-E0yjZs7G.mjs → useDcwQuery-C8FikKgu.mjs} +2 -2
  94. package/.output/server/chunks/build/{useDcwQuery-E0yjZs7G.mjs.map → useDcwQuery-C8FikKgu.mjs.map} +1 -1
  95. package/.output/server/chunks/build/{useDcwQuickActions-BvT5VFOY.mjs → useDcwQuickActions-IyQ8Rkpe.mjs} +2 -2
  96. package/.output/server/chunks/build/{useDcwQuickActions-BvT5VFOY.mjs.map → useDcwQuickActions-IyQ8Rkpe.mjs.map} +1 -1
  97. package/.output/server/chunks/build/{useDcwRecipes-zngShyk0.mjs → useDcwRecipes-k9HGhMwF.mjs} +2 -2
  98. package/.output/server/chunks/build/{useDcwRecipes-zngShyk0.mjs.map → useDcwRecipes-k9HGhMwF.mjs.map} +1 -1
  99. package/.output/server/chunks/build/{useDcwStream-82IdjJKk.mjs → useDcwStream-BnBOiawo.mjs} +1 -1
  100. package/.output/server/chunks/build/{useDcwStream-82IdjJKk.mjs.map → useDcwStream-BnBOiawo.mjs.map} +1 -1
  101. package/.output/server/chunks/build/{useDcwTemplates-CpHrcnvJ.mjs → useDcwTemplates-CnyYOOy0.mjs} +2 -2
  102. package/.output/server/chunks/build/{useDcwTemplates-CpHrcnvJ.mjs.map → useDcwTemplates-CnyYOOy0.mjs.map} +1 -1
  103. package/.output/server/chunks/build/{useDcwWrites-BlOhljp9.mjs → useDcwWrites-Cco590iv.mjs} +2 -2
  104. package/.output/server/chunks/build/{useDcwWrites-BlOhljp9.mjs.map → useDcwWrites-Cco590iv.mjs.map} +1 -1
  105. package/.output/server/chunks/build/{useLogsScope-BRyy550Y.mjs → useLogsScope-C-yrta-v.mjs} +2 -2
  106. package/.output/server/chunks/build/{useLogsScope-BRyy550Y.mjs.map → useLogsScope-C-yrta-v.mjs.map} +1 -1
  107. package/.output/server/chunks/routes/api/workshop/assets/index.post.mjs +1 -1
  108. package/.output/server/chunks/routes/api/workshop/assets/index2.post.mjs +1 -1
  109. package/.output/server/chunks/routes/api/workshop/channel-templates/_id/instantiate.post.mjs +1 -1
  110. package/.output/server/chunks/routes/api/workshop/channels/_id/activate.post.mjs +1 -1
  111. package/.output/server/chunks/routes/api/workshop/channels/_id/chat/index.post.mjs +1 -1
  112. package/.output/server/chunks/routes/api/workshop/channels/_id/index.post.mjs +1 -1
  113. package/.output/server/chunks/routes/api/workshop/channels/_id/index3.post.mjs +1 -1
  114. package/.output/server/chunks/routes/api/workshop/channels/_id/index4.post.mjs +1 -1
  115. package/.output/server/chunks/routes/api/workshop/channels/_id/line.put.mjs +1 -1
  116. package/.output/server/chunks/routes/api/workshop/channels/index.get.mjs +1 -1
  117. package/.output/server/chunks/routes/api/workshop/channels.post.mjs +1 -1
  118. package/.output/server/chunks/routes/api/workshop/dcw/lines/_id_.delete.mjs +1 -1
  119. package/.output/server/chunks/routes/api/workshop/eda/index8.get.mjs +33 -0
  120. package/.output/server/chunks/routes/api/workshop/eda/index8.get.mjs.map +1 -0
  121. package/.output/server/chunks/routes/api/workshop/eda/index8.post.mjs +41 -0
  122. package/.output/server/chunks/routes/api/workshop/eda/index8.post.mjs.map +1 -0
  123. package/.output/server/chunks/routes/api/workshop/eda/templates/_id_.delete.mjs +36 -0
  124. package/.output/server/chunks/routes/api/workshop/eda/templates/_id_.delete.mjs.map +1 -0
  125. package/.output/server/chunks/routes/api/workshop/eda/templates/_id_.get.mjs +37 -0
  126. package/.output/server/chunks/routes/api/workshop/eda/templates/_id_.get.mjs.map +1 -0
  127. package/.output/server/chunks/routes/api/workshop/eda/templates/_id_.patch.mjs +42 -0
  128. package/.output/server/chunks/routes/api/workshop/eda/templates/_id_.patch.mjs.map +1 -0
  129. package/.output/server/chunks/routes/api/workshop/index10.post.mjs +33 -0
  130. package/.output/server/chunks/routes/api/workshop/index10.post.mjs.map +1 -0
  131. package/.output/server/chunks/routes/api/workshop/index2.post.mjs +45 -46
  132. package/.output/server/chunks/routes/api/workshop/index2.post.mjs.map +1 -1
  133. package/.output/server/chunks/routes/api/workshop/index3.post.mjs +46 -23
  134. package/.output/server/chunks/routes/api/workshop/index3.post.mjs.map +1 -1
  135. package/.output/server/chunks/routes/api/workshop/index4.post.mjs +10 -9
  136. package/.output/server/chunks/routes/api/workshop/index4.post.mjs.map +1 -1
  137. package/.output/server/chunks/routes/api/workshop/index5.post.mjs +22 -19
  138. package/.output/server/chunks/routes/api/workshop/index5.post.mjs.map +1 -1
  139. package/.output/server/chunks/routes/api/workshop/index6.post.mjs +18 -42
  140. package/.output/server/chunks/routes/api/workshop/index6.post.mjs.map +1 -1
  141. package/.output/server/chunks/routes/api/workshop/index7.post.mjs +43 -17
  142. package/.output/server/chunks/routes/api/workshop/index7.post.mjs.map +1 -1
  143. package/.output/server/chunks/routes/api/workshop/index8.post.mjs +13 -15
  144. package/.output/server/chunks/routes/api/workshop/index8.post.mjs.map +1 -1
  145. package/.output/server/chunks/routes/api/workshop/index9.post.mjs +18 -4
  146. package/.output/server/chunks/routes/api/workshop/index9.post.mjs.map +1 -1
  147. package/.output/server/chunks/routes/api/workshop/process-model/datasets.post.mjs +1 -1
  148. package/.output/server/chunks/routes/api/workshop/process-model/eligibility.mjs +1 -1
  149. package/.output/server/chunks/routes/api/workshop/process-model/experiments.mjs +1 -1
  150. package/.output/server/chunks/routes/api/workshop/teams/_id/deploy.post.mjs +1 -1
  151. package/.output/server/chunks/routes/api/workshop/workspaces/_id/channel-templates/_tplId_.post.mjs +1 -1
  152. package/.output/server/chunks/routes/renderer.mjs +1 -1
  153. package/.output/server/chunks/virtual/entry.mjs +140 -43
  154. package/.output/server/chunks/virtual/entry.mjs.map +1 -1
  155. package/.output/server/chunks/virtual/precomputed.mjs +1 -1
  156. package/.output/server/chunks/virtual/precomputed.mjs.map +1 -1
  157. package/.output/server/package.json +1 -1
  158. package/README-zh.md +8 -5
  159. package/README.md +8 -5
  160. package/app/components/AwChart.vue +14 -0
  161. package/app/pages/ontology/index.vue +1 -1
  162. package/app/pages/studio/pipeline.vue +812 -0
  163. package/app/utils/chart-pipeline.ts +193 -0
  164. package/config.yml +1 -1
  165. package/docs/cli.en.md +2 -2
  166. package/docs/cli.md +2 -2
  167. package/docs/plugins.en.md +1 -1
  168. package/docs/plugins.md +1 -1
  169. package/docs/sdk.en.md +1 -1
  170. package/docs/sdk.md +1 -1
  171. package/i18n/locales/en.ts +46 -0
  172. package/i18n/locales/zh-CN.ts +46 -0
  173. package/package.json +1 -1
  174. package/scripts/check-docs-sync.mjs +1 -1
  175. package/scripts/e2e-eda-pipeline.mjs +286 -0
  176. package/scripts/e2e-resume-crash.mjs +20 -6
  177. package/scripts/e2e-timezone-settings.mjs +4 -2
  178. package/server/api/workshop/analysis/index.post.ts +54 -0
  179. package/server/api/workshop/eda/templates/[id].delete.ts +14 -0
  180. package/server/api/workshop/eda/templates/[id].get.ts +15 -0
  181. package/server/api/workshop/eda/templates/[id].patch.ts +20 -0
  182. package/server/api/workshop/eda/templates/index.get.ts +11 -0
  183. package/server/api/workshop/eda/templates/index.post.ts +20 -0
  184. package/server/services/workshop/eda/chart-repo.ts +1 -1
  185. package/server/services/workshop/eda/eda-db.ts +13 -0
  186. package/server/services/workshop/eda/template-repo.ts +101 -0
  187. package/.output/public/_nuxt/Bbk1XCCM.js +0 -14
  188. package/.output/public/_nuxt/CMzExk8F.js +0 -1
  189. package/.output/public/_nuxt/D6FhDrYe2.js +0 -1
  190. package/.output/public/_nuxt/DF9ETYJ32.js +0 -1
  191. package/.output/public/_nuxt/DoChWkff.js +0 -1
  192. package/.output/public/_nuxt/YNhAoK-R2.js +0 -1
  193. package/.output/public/_nuxt/builds/meta/fd0033e4-1eaa-42f8-9a04-3132717a703b.json +0 -1
  194. package/.output/server/chunks/build/ontology-B_0Vm6j-.mjs.map +0 -1
  195. /package/.output/public/_nuxt/{DgrK8UCQ.js → BvRk9kiK2.js} +0 -0
  196. /package/.output/public/_nuxt/{DxH5focQ2.js → DxH5focQ.js} +0 -0
@@ -0,0 +1,286 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * e2e-eda-pipeline —— EDA 分析管道全链验收(隔离实例 + 真浏览器)。
4
+ *
5
+ * REST:模板 CRUD/校验 → analysis 用户面 POST → charts 挂模板。
6
+ * UI :拉数 → 表格 → 代码后处理 → 绘图 → 模板保存/重载持久化/应用 → 在线查看器 → 保存结果。
7
+ *
8
+ * 运行:node scripts/e2e-eda-pipeline.mjs
9
+ * 证据:docs/audit/evidence/2026-10-26-eda-pipeline/
10
+ */
11
+ import { spawn, spawnSync } from 'node:child_process'
12
+ import { mkdirSync, mkdtempSync, openSync, rmSync } from 'node:fs'
13
+ import { tmpdir } from 'node:os'
14
+ import { join, resolve, dirname } from 'node:path'
15
+ import { fileURLToPath } from 'node:url'
16
+
17
+ const REPO = resolve(dirname(fileURLToPath(import.meta.url)), '..')
18
+ const PORT = 3040
19
+ const BASE = `http://127.0.0.1:${PORT}`
20
+ const HOME = process.env.AW_E2E_HOME ?? mkdtempSync(join(tmpdir(), 'aw-eda-pipeline-'))
21
+ const EV = resolve(REPO, 'docs/audit/evidence/2026-10-26-eda-pipeline')
22
+ mkdirSync(EV, { recursive: true })
23
+ process.env.NO_PROXY = '127.0.0.1,localhost'
24
+ process.env.NO_PROXY ??= ''
25
+
26
+ let pass = 0
27
+ const fails = []
28
+ const ok = (id, cond, detail = '') => {
29
+ if (cond) {
30
+ pass++
31
+ console.log(`✔ [${id}] ${detail}`)
32
+ }
33
+ else {
34
+ fails.push(id)
35
+ console.log(`✖ [${id}] ${detail}`)
36
+ }
37
+ }
38
+ const sleep = ms => new Promise(r => setTimeout(r, ms))
39
+
40
+ async function waitHealthy(timeoutMs = 150_000) {
41
+ const t0 = Date.now()
42
+ while (Date.now() - t0 < timeoutMs) {
43
+ try {
44
+ const r = await fetch(`${BASE}/api/health`, { signal: AbortSignal.timeout(2500) })
45
+ if (r.ok) return true
46
+ }
47
+ catch { /* not yet */ }
48
+ await sleep(2000)
49
+ }
50
+ return false
51
+ }
52
+
53
+ // ── 起隔离实例 ──
54
+ rmSync(HOME, { recursive: true, force: true })
55
+ mkdirSync(HOME, { recursive: true })
56
+ const out = openSync(join(HOME, 'instance.log'), 'a')
57
+ const inst = spawn(process.execPath, [join(REPO, 'bin', 'aw.mjs'), 'dev', '--port', String(PORT)], {
58
+ cwd: REPO,
59
+ env: { ...process.env, AW_MODE: 'home', AW_HOME: HOME, NUXT_SESSION_PASSWORD: 'EdaPipeline-Session-3040-abcdefghijklmnop' },
60
+ detached: true,
61
+ stdio: ['ignore', out, out],
62
+ })
63
+ inst.unref()
64
+ if (!await waitHealthy()) {
65
+ console.error('✖ 隔离实例未就绪')
66
+ process.exit(1)
67
+ }
68
+ console.log(`· 实例就绪 ${BASE}(home=${HOME})`)
69
+
70
+ const reg = await (await fetch(`${BASE}/api/users/register`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: 'eda-admin', email: 'eda@awshop.local', password: 'EdaPipeline#2026' }) })).json()
71
+ const TOK = reg?.data?.token
72
+ if (!TOK) {
73
+ console.error('✖ 注册失败')
74
+ process.exit(1)
75
+ }
76
+ const H = { 'authorization': `Bearer ${TOK}`, 'content-type': 'application/json' }
77
+ const api = async (method, path, body) => {
78
+ const r = await fetch(BASE + path, { method, headers: H, body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(60_000) })
79
+ return { status: r.status, json: await r.json().catch(() => null) }
80
+ }
81
+ const d = j => j?.data ?? j
82
+
83
+ // ── 造数:线 + mock DAQ 节点 + ingest 样本 ──
84
+ const lineR = await api('POST', '/api/workshop/dcw/lines', { name: `EDA管道验收线-${Date.now() % 100000}` })
85
+ const lineId = d(lineR.json)?.line?.id ?? d(lineR.json)?.id
86
+ ok('E0.1', lineR.status === 200 && Boolean(lineId), `建线 ${lineId}`)
87
+ const daqR = await api('POST', '/api/workshop/daq', { lineId, name: '克重', driver: 'mock', templateRef: 'daq-temp-tc', unit: 'g', min: 25, max: 40, decimals: 2 })
88
+ ok('E0.2', daqR.status === 200, `建数采节点(daq/query 实时路径由演示实例与产线套件覆盖;本 e2e 数据面用 CSV 导入,见 UI 段)`)
89
+
90
+ // ── REST:模板 CRUD ──
91
+ const badType = await api('POST', '/api/workshop/eda/templates', { name: 'x', chart_type: 'hologram', config: {} })
92
+ ok('E1.1', badType.status === 400, `非法图型 400(${badType.status})`)
93
+ const emptyName = await api('POST', '/api/workshop/eda/templates', { name: ' ', chart_type: 'line', config: {} })
94
+ ok('E1.2', emptyName.status === 400, `空名 400(${emptyName.status})`)
95
+ const tplR = await api('POST', '/api/workshop/eda/templates', { name: '克重趋势-达标过滤', description: 'line · x=at · y=克重', chart_type: 'line', config: { xField: 'at', yFields: ['克重'], agg: 'avg', title: '克重趋势', transform_code: 'function transform(rows) { return rows.filter(r => (r["克重"] ?? 99) <= 32.85) }' } })
96
+ const tplId = d(tplR.json)?.template?.id
97
+ ok('E1.3', tplR.status === 200 && Boolean(tplId), `保存模板 ${tplId}`)
98
+ const tplList = await api('GET', '/api/workshop/eda/templates')
99
+ ok('E1.4', (d(tplList.json)?.templates ?? []).some(t => t.id === tplId), '列表含新模板')
100
+ const tplGet = await api('GET', `/api/workshop/eda/templates/${tplId}`)
101
+ ok('E1.5', d(tplGet.json)?.template?.config?.yFields?.[0] === '克重', '详情 config 回读')
102
+ const tplPatch = await api('PATCH', `/api/workshop/eda/templates/${tplId}`, { description: '改过描述' })
103
+ ok('E1.6', d(tplPatch.json)?.template?.description === '改过描述', 'PATCH 描述落库')
104
+
105
+ // ── REST:analysis 用户面 POST ──
106
+ const arR = await api('POST', '/api/workshop/analysis', { line_id: lineId, title: '管道验收-REST', category: 'eda-pipeline', summary: 'REST 保存验证', conclusions: ['c1'], metrics: { rows: 33 }, context: { pipeline: true } })
107
+ const arId = d(arR.json)?.id
108
+ ok('E2.1', arR.status === 200 && Boolean(arId), `analysis POST ${arId}`)
109
+ const arList = await api('GET', `/api/workshop/analysis?line_id=${lineId}`)
110
+ ok('E2.2', (d(arList.json)?.reports ?? d(arList.json) ?? []).some?.(x => x.id === arId) === true, '台账列表含新结果')
111
+ const arGet = await api('GET', `/api/workshop/analysis/${arId}`)
112
+ ok('E2.3', d(arGet.json)?.report?.title === '管道验收-REST', '台账详情回读')
113
+
114
+ // ── REST:charts 挂模板 ──
115
+ const chR = await api('POST', '/api/workshop/eda/charts', { title: '管道验收图', viz_type: 'pipeline', datasource: { line_id: lineId, node_ids: [] }, encoding: { xField: 'at', yFields: ['克重'] }, options: { template: tplId }, line_id: lineId })
116
+ const chartId = d(chR.json)?.chart?.id
117
+ ok('E3.1', chR.status === 200 && Boolean(chartId), `保存图表 ${chartId}`)
118
+ const chGet = await api('GET', `/api/workshop/eda/charts/${chartId}`)
119
+ ok('E3.2', d(chGet.json)?.chart?.options?.template === tplId, '图表回读含模板链接')
120
+
121
+ // ── UI 走查(Puppeteer) ──
122
+ const uiFails = []
123
+ async function ui() {
124
+ const core = await import('file:///D:/codes/ABO/AgentWorkShop/node_modules/puppeteer-core/lib/puppeteer/puppeteer-core.js')
125
+ const puppeteer = core.default ?? core
126
+ const browser = await puppeteer.launch({ executablePath: 'C:/Program Files/Google/Chrome/Application/chrome.exe', headless: true, args: ['--no-sandbox', '--disable-dev-shm-usage'] })
127
+ const page = await browser.newPage()
128
+ await page.setViewport({ width: 1440, height: 960 })
129
+ page.on('pageerror', (e) => {
130
+ uiFails.push(`pageerror: ${e.message}`)
131
+ })
132
+ // 会话:未登录时页面内联登录门(水合后出现输入框),原生 setter 填值后点「登录」
133
+ await page.goto(`${BASE}/studio/pipeline`, { waitUntil: 'domcontentloaded', timeout: 120_000 })
134
+ let hasPwd = await page.evaluate(() => Boolean(document.querySelector('input[type="password"]')))
135
+ for (let i = 0; i < 25 && !hasPwd; i++) {
136
+ await sleep(1500)
137
+ hasPwd = await page.evaluate(() => Boolean(document.querySelector('input[type="password"]')))
138
+ }
139
+ if (hasPwd) {
140
+ await page.evaluate(() => {
141
+ const inputs = [...document.querySelectorAll('input')]
142
+ const textInput = inputs.find(i => i.type === 'email' || /邮箱|用户名/.test(i.placeholder ?? ''))
143
+ const pwd = inputs.find(i => i.type === 'password')
144
+ const set = (el, v) => {
145
+ const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set
146
+ setter.call(el, v)
147
+ el.dispatchEvent(new Event('input', { bubbles: true }))
148
+ }
149
+ set(textInput, 'eda@awshop.local')
150
+ set(pwd, 'EdaPipeline#2026')
151
+ })
152
+ await sleep(300)
153
+ await page.evaluate(() => {
154
+ const btn = [...document.querySelectorAll('button')].find(b => /登\s*录/i.test(b.textContent ?? '') && !b.disabled)
155
+ btn?.click()
156
+ })
157
+ await sleep(3500)
158
+ }
159
+ await page.goto(`${BASE}/studio/pipeline`, { waitUntil: 'domcontentloaded', timeout: 120_000 })
160
+ await page.waitForSelector('[data-test="pipeline-page"]', { timeout: 90_000 })
161
+ ok('U1', true, '/studio/pipeline 打开')
162
+ await sleep(1500)
163
+
164
+ // 数据面:产线选择(为保存面提供 line_id)+ CSV 导入(40 行,含 6 行越界)
165
+ await page.evaluate((lid) => {
166
+ const lineSel = document.querySelector('[data-test="pipeline-lines"]')
167
+ lineSel.value = lid
168
+ lineSel.dispatchEvent(new Event('change', { bubbles: true }))
169
+ }, lineId)
170
+ await sleep(800)
171
+ const csvLines = ['批次,克重,飞边率']
172
+ for (let i = 1; i <= 40; i++) csvLines.push(`B${String(i).padStart(2, '0')},${i % 7 === 0 ? 33.1 : (32.2 + Math.sin(i / 3) * 0.4).toFixed(2)},${(0.01 + i * 0.001).toFixed(3)}`)
173
+ await page.evaluate((csv) => {
174
+ const ta = document.querySelector('[data-test="pipeline-import"]')
175
+ ta.value = csv
176
+ ta.dispatchEvent(new Event('input', { bubbles: true }))
177
+ }, csvLines.join('\n'))
178
+ await page.click('[data-test="pipeline-import-btn"]')
179
+ await page.waitForFunction(() => Boolean(document.querySelector('[data-test="pipeline-table"]')), { timeout: 15_000 })
180
+ const rowCount = await page.evaluate(() => document.querySelector('[data-test="pipeline-table"] .hint')?.textContent ?? '')
181
+ ok('U2', /40/.test(rowCount), `CSV 导入 40 行(${String(rowCount).trim()})`)
182
+ await page.screenshot({ path: join(EV, 'pipeline-table.png') })
183
+
184
+ // 后处理:过滤越界行
185
+ await page.evaluate(() => {
186
+ const ta = document.querySelector('[data-test="pipeline-code"]')
187
+ ta.value = 'function transform(rows) { return rows.filter(r => (r["克重"] ?? 99) <= 32.85) }'
188
+ ta.dispatchEvent(new Event('input', { bubbles: true }))
189
+ })
190
+ await page.click('[data-test="pipeline-apply"]')
191
+ await page.waitForFunction(() => document.querySelector('[data-test="pipeline-page"]')?.textContent?.includes('后处理完成'), { timeout: 15_000 })
192
+ ok('U3', true, '代码后处理生效(越界行被过滤)')
193
+
194
+ // 配置 + 绘图
195
+ await page.evaluate(() => {
196
+ const setSel = (test, val) => {
197
+ const el = document.querySelector(`[data-test="${test}"]`)
198
+ if (!el) return
199
+ el.value = val
200
+ el.dispatchEvent(new Event('change', { bubbles: true }))
201
+ }
202
+ setSel('pipeline-type', 'line')
203
+ setSel('pipeline-xfield', '批次')
204
+ const y = document.querySelector('[data-test="pipeline-yfields"]')
205
+ if (y) {
206
+ y.value = '克重'
207
+ y.dispatchEvent(new Event('change', { bubbles: true }))
208
+ }
209
+ const t = document.querySelector('[data-test="pipeline-title"]')
210
+ t.value = '克重趋势(管道)'
211
+ t.dispatchEvent(new Event('input', { bubbles: true }))
212
+ })
213
+ await page.click('[data-test="pipeline-draw"]')
214
+ await page.waitForFunction(() => Boolean(document.querySelector('[data-test="pipeline-chart"] canvas')), { timeout: 20_000 })
215
+ ok('U4', true, '绘图渲染出 canvas')
216
+ await page.screenshot({ path: join(EV, 'pipeline-drawn.png') })
217
+
218
+ // 模板保存 + 重载持久化 + 应用
219
+ await page.type('[data-test="pipeline-template-name"]', 'UI 克重趋势')
220
+ await page.click('[data-test="pipeline-template-save"]')
221
+ await page.waitForFunction(() => Boolean(document.querySelector('[data-test="pipeline-template-item"]')), { timeout: 10_000 }).catch(async () => {
222
+ const dump = await page.evaluate(() => document.querySelector('[data-test="pipeline-page"]')?.textContent?.replace(/\s+/g, ' ').match(/配置模板.{0,220}/)?.[0] ?? '')
223
+ console.log('TPL-AREA:', dump)
224
+ })
225
+ await page.reload({ waitUntil: 'domcontentloaded' })
226
+ await page.waitForSelector('[data-test="pipeline-page"]', { timeout: 60_000 })
227
+ await sleep(1200)
228
+ const tplPersisted = await page.evaluate(() => Boolean(document.querySelector('[data-test="pipeline-template-item"]')))
229
+ ok('U5', tplPersisted, '模板重载后仍在(持久化)')
230
+ // 换数据 + 模板 → 一键复绘:重选产线(保存面用)+ 重导 CSV + 应用模板(自动 draw)
231
+ await page.evaluate((lid) => {
232
+ const lineSel = document.querySelector('[data-test="pipeline-lines"]')
233
+ lineSel.value = lid
234
+ lineSel.dispatchEvent(new Event('change', { bubbles: true }))
235
+ }, lineId)
236
+ await sleep(800)
237
+ await page.click('[data-test="pipeline-import"]', { clickCount: 3 })
238
+ await page.type('[data-test="pipeline-import"]', csvLines.join('\n'), { delay: 1 })
239
+ await page.click('[data-test="pipeline-import-btn"]')
240
+ await page.waitForFunction(() => Boolean(document.querySelector('[data-test="pipeline-table"]')), { timeout: 15_000 }).catch(async () => {
241
+ const dump = await page.evaluate(() => ({ url: location.href, hasImport: Boolean(document.querySelector('[data-test="pipeline-import"]')), body: document.body.textContent.replace(/\s+/g, ' ').slice(0, 200) }))
242
+ console.log('REIMPORT-DUMP:', JSON.stringify(dump))
243
+ })
244
+ await page.click('[data-test="pipeline-template-item"]')
245
+ await page.waitForFunction(() => Boolean(document.querySelector('[data-test="pipeline-chart"] canvas')), { timeout: 15_000 }).catch(async () => {
246
+ const dump = await page.evaluate(() => ({
247
+ tplMsg: document.querySelector('[data-test="pipeline-page"]')?.textContent?.match(/已应用模板[^。]*。|模板已保存。/)?.[0] ?? '(no msg)',
248
+ hasCanvas: Boolean(document.querySelector('[data-test="pipeline-chart"] canvas')),
249
+ viewerDisabled: document.querySelector('[data-test="pipeline-viewer"]')?.disabled,
250
+ rows: document.querySelector('[data-test="pipeline-table"] .hint')?.textContent ?? '',
251
+ }))
252
+ console.log('APPLY-DUMP:', JSON.stringify(dump))
253
+ })
254
+
255
+ // 在线查看器
256
+ await page.click('[data-test="pipeline-viewer"]')
257
+ await page.waitForSelector('[data-test="pipeline-viewer-shell"]', { timeout: 10_000 })
258
+ ok('U6', true, '在线查看器外壳打开')
259
+ await page.screenshot({ path: join(EV, 'pipeline-viewer.png') })
260
+ await page.click('[data-test="pipeline-viewer-close"]')
261
+
262
+ // 保存为分析结果(UI 面)
263
+ await page.click('[data-test="pipeline-save-result"]')
264
+ await page.waitForFunction(() => document.querySelector('[data-test="pipeline-page"]')?.textContent?.includes('分析结果已保存'), { timeout: 15_000 })
265
+ const arAfter = await api('GET', `/api/workshop/analysis?line_id=${lineId}`)
266
+ ok('U7', (JSON.stringify(arAfter.json).match(/管道验收-REST/g) ?? []).length >= 1, '台账含 REST 结果;UI 结果已提示保存')
267
+
268
+ await browser.close()
269
+ }
270
+ try {
271
+ await ui()
272
+ }
273
+ catch (e) {
274
+ console.error('UI EX:', e.message)
275
+ ok('UI', false, `UI 走查异常: ${String(e.message).slice(0, 160)}`)
276
+ }
277
+
278
+ // ── 清理:删模板(回读删后不存在) ──
279
+ const delR = await api('DELETE', `/api/workshop/eda/templates/${tplId}`)
280
+ ok('E4.1', delR.status === 200, '模板删除')
281
+ const gone = await api('GET', `/api/workshop/eda/templates/${tplId}`)
282
+ ok('E4.2', d(gone.json)?.template === null, '删后详情为 null')
283
+
284
+ console.log(`\nEDA-PIPELINE E2E: ${pass}/${pass + fails.length} passed${fails.length ? `, failed: ${fails.join(',')}` : ''}`)
285
+ spawnSync('taskkill', ['/PID', String(inst.pid), '/T', '/F'], { shell: true })
286
+ process.exit(fails.length || uiFails.length ? 1 : 0)
@@ -17,6 +17,7 @@
17
17
  * node scripts/e2e-resume-crash.mjs verify-gap # 场景 C(重启后验证重投恢复)
18
18
  */
19
19
  import { DatabaseSync } from 'node:sqlite'
20
+ import { readFileSync } from 'node:fs'
20
21
 
21
22
  const BASE = (process.env.AW_RESUME_BASE ?? 'http://127.0.0.1:3000') + '/api/workshop'
22
23
  const DB_PATH = process.env.AW_RESUME_DB ?? 'data/workshop.sqlite'
@@ -39,10 +40,23 @@ if (process.env.AW_RESUME_TOKEN) {
39
40
  __user = { data: { token: process.env.AW_RESUME_TOKEN } }
40
41
  }
41
42
  else {
42
- __user = await fetch(BASE + '/users/register', {
43
- method: 'POST', headers: { 'content-type': 'application/json' },
44
- body: JSON.stringify({ name: 'e2e-' + Math.random().toString(36).slice(2, 10) }),
45
- }).then(r => r.json()).catch(() => null)
43
+ // 跨阶段接续:watch/crash/gap 已把 token 写进 .resume-test.json;注册是一次性的
44
+ // (首用户注册后 allowRegistration 关闭),后续阶段必须沿用同一 token,不得重注册。
45
+ try {
46
+ const mark = JSON.parse(readFileSync('.resume-test.json', 'utf8'))
47
+ if (mark?.token) __user = { data: { token: mark.token } }
48
+ }
49
+ catch { /* 首阶段尚无标记 → 走注册 */ }
50
+ if (!__user) {
51
+ __user = await fetch(BASE + '/users/register', {
52
+ method: 'POST', headers: { 'content-type': 'application/json' },
53
+ body: JSON.stringify({ name: 'e2e-' + Math.random().toString(36).slice(2, 10) }),
54
+ }).then(r => r.json()).catch(() => null)
55
+ if (__user?.data?.token) {
56
+ const { writeFileSync } = await import('node:fs')
57
+ writeFileSync('.resume-test.json', JSON.stringify({ token: __user.data.token }))
58
+ }
59
+ }
46
60
  }
47
61
  let __userToken = __user?.data?.token
48
62
  if (!__userToken) {
@@ -172,7 +186,7 @@ async function crash() {
172
186
 
173
187
  async function verify() {
174
188
  console.log('━━━ 场景 B:重启 → 断线重连自动恢复执行 ━━━')
175
- const { readFileSync, unlinkSync } = await import('node:fs')
189
+ const { readFileSync } = await import('node:fs')
176
190
  const mark = JSON.parse(readFileSync('.resume-test.json', 'utf8'))
177
191
  __userToken = resumeTokenFrom(mark)
178
192
  console.log(` 恢复目标: task=${mark.taskId.slice(0, 8)}… (workerTask=${mark.workerTaskId.slice(0, 8)}…);沿用 crash 阶段用户 token`)
@@ -199,7 +213,7 @@ async function verify() {
199
213
  check('恢复执行后全员 idle', (queue.json.data ?? []).every(a => a.state === 'idle'), JSON.stringify(states))
200
214
 
201
215
  await req('DELETE', `/channels/${mark.channelId}`)
202
- unlinkSync('.resume-test.json')
216
+ // 标记保留:gap/verify-gap 阶段还要沿用同一 token 与 workerTaskId;最终由 verify-gap 清理
203
217
  console.log(`\n结果: PASS=${pass} FAIL=${fail}`)
204
218
  process.exit(fail === 0 ? 0 : 1)
205
219
  }
@@ -10,6 +10,8 @@ import { join } from 'node:path'
10
10
  const BASE = process.argv[2] ?? 'http://127.0.0.1:3005'
11
11
  const ROOT = process.cwd()
12
12
  process.env.NO_PROXY = '127.0.0.1,localhost'
13
+ // 数据根可移植:打隔离 home 实例时用 AW_TZ_DATA 指向该 home 的 data 目录(缺省仓库配置根)
14
+ const DATA_DIR = process.env.AW_TZ_DATA ?? join(ROOT, '.AgentWorkShop', 'data')
13
15
 
14
16
  async function api(method, path, body, token) {
15
17
  const r = await fetch(BASE + path, {
@@ -24,11 +26,11 @@ async function api(method, path, body, token) {
24
26
  const reg = await api('POST', '/api/workshop/users/register', { name: `tz-e2e-${Date.now()}` })
25
27
  const token = reg.json?.data?.token
26
28
  assert.ok(token, 'register token missing')
27
- const users = new DatabaseSync(join(ROOT, '.AgentWorkShop', 'data', 'users.sqlite'))
29
+ const users = new DatabaseSync(join(DATA_DIR, 'users.sqlite'))
28
30
  users.prepare('UPDATE users SET role=\'admin\' WHERE email LIKE \'tz-e2e-%\' AND role=\'user\'').run()
29
31
  users.close()
30
32
 
31
- const db = new DatabaseSync(join(ROOT, '.AgentWorkShop', 'data', 'workshop.sqlite'))
33
+ const db = new DatabaseSync(join(DATA_DIR, 'workshop.sqlite'))
32
34
  const before = db.prepare('SELECT id, created_at AS createdAt FROM chat_messages ORDER BY rowid DESC LIMIT 1').get()
33
35
  const written = []
34
36
  try {
@@ -0,0 +1,54 @@
1
+ /**
2
+ * POST /api/workshop/analysis —— 人工/界面保存分析结果(管道页「保存为分析结果」)。
3
+ * 此前 analysis_reports 只有 Agent 面(analysis_save)可写;本路由补齐用户面,
4
+ * body: { line_id, title, category?, summary?, conclusions?, report?, metrics?, context? }
5
+ */
6
+ import { randomUUID } from 'node:crypto'
7
+ import { readBody } from 'h3'
8
+ import { defineApiHandler } from '@/server/utils/response'
9
+ import { AppError } from '@/server/utils/errors'
10
+ import { resolveUser } from '@/server/api/workshop/caller'
11
+ import { getDcwLineRepo } from '@/server/services/workshop/dcw/dcw-line.repo'
12
+ import { getAnalysisRepo } from '@/server/services/workshop/analysis/analysis.repo'
13
+ import { recordOps } from '@/server/services/workshop/ops/ops'
14
+
15
+ export default defineApiHandler(async (event) => {
16
+ const user = resolveUser(event)
17
+ const body = await readBody<Record<string, unknown>>(event) ?? {}
18
+ const lineId = String(body.line_id ?? '').trim()
19
+ const title = String(body.title ?? '').trim()
20
+ if (!lineId) throw new AppError(400, 'VALIDATION_ERROR' as never, 'line_id 必填。')
21
+ if (!title) throw new AppError(400, 'VALIDATION_ERROR' as never, 'title 必填。')
22
+ const repo = getAnalysisRepo()
23
+ const id = `ar-${randomUUID().slice(0, 8)}`
24
+ const now = new Date().toISOString()
25
+ const context = (body.context ?? {}) as Record<string, unknown>
26
+ repo.insert({
27
+ id,
28
+ lineId,
29
+ lineName: getDcwLineRepo().byId(lineId)?.name ?? '',
30
+ title: title.slice(0, 200),
31
+ category: String(body.category ?? 'eda-pipeline').slice(0, 60),
32
+ summary: String(body.summary ?? '').slice(0, 500),
33
+ conclusions: Array.isArray(body.conclusions) ? (body.conclusions as unknown[]).map(c => String(c)) : [],
34
+ report: String(body.report ?? ''),
35
+ metrics: (body.metrics ?? {}) as Record<string, unknown>,
36
+ recipeId: String(body.recipe_id ?? ''),
37
+ recipeVersion: String(body.recipe_version ?? ''),
38
+ windowFrom: body.window_from ? String(body.window_from) : null,
39
+ windowTo: body.window_to ? String(body.window_to) : null,
40
+ dataSources: String(body.data_sources ?? ''),
41
+ context,
42
+ channelId: '',
43
+ channelName: '',
44
+ actor: user.id,
45
+ actorName: user.name,
46
+ createdAt: now,
47
+ })
48
+ recordOps({
49
+ actor: user.id, actorName: user.name, actorKind: 'user',
50
+ action: 'analysis.save', kind: 'system', targetKind: 'analysis-report', targetId: id,
51
+ summary: `保存分析结果「${title.slice(0, 60)}」`,
52
+ })
53
+ return { id, report: repo.get(id) ?? null }
54
+ })
@@ -0,0 +1,14 @@
1
+ /**
2
+ * DELETE /api/workshop/eda/templates/:id —— 删除模板(owner/admin)。
3
+ */
4
+ import { getRouterParam } from 'h3'
5
+ import { defineApiHandler } from '@/server/utils/response'
6
+ import { resolveUser } from '@/server/api/workshop/caller'
7
+ import { deleteTemplate } from '@/server/services/workshop/eda/template-repo'
8
+
9
+ export default defineApiHandler(async (event) => {
10
+ const user = resolveUser(event)
11
+ const id = String(getRouterParam(event, 'id') ?? '')
12
+ deleteTemplate(user, id)
13
+ return { deleted: id }
14
+ })
@@ -0,0 +1,15 @@
1
+ /**
2
+ * GET /api/workshop/eda/templates/:id —— 模板详情(owner/admin)。
3
+ */
4
+ import { getRouterParam } from 'h3'
5
+ import { defineApiHandler } from '@/server/utils/response'
6
+ import { resolveUser } from '@/server/api/workshop/caller'
7
+ import { templateById } from '@/server/services/workshop/eda/template-repo'
8
+
9
+ export default defineApiHandler(async (event) => {
10
+ const user = resolveUser(event)
11
+ const id = String(getRouterParam(event, 'id') ?? '')
12
+ const template = templateById(user, id)
13
+ if (!template) return { template: null }
14
+ return { template }
15
+ })
@@ -0,0 +1,20 @@
1
+ /**
2
+ * PATCH /api/workshop/eda/templates/:id —— 更新模板(owner/admin)。
3
+ */
4
+ import { getRouterParam, readBody } from 'h3'
5
+ import { defineApiHandler } from '@/server/utils/response'
6
+ import { resolveUser } from '@/server/api/workshop/caller'
7
+ import { updateTemplate } from '@/server/services/workshop/eda/template-repo'
8
+
9
+ export default defineApiHandler(async (event) => {
10
+ const user = resolveUser(event)
11
+ const id = String(getRouterParam(event, 'id') ?? '')
12
+ const body = await readBody<Record<string, unknown>>(event) ?? {}
13
+ const template = updateTemplate(user, id, {
14
+ name: body.name === undefined ? undefined : String(body.name),
15
+ description: body.description === undefined ? undefined : String(body.description),
16
+ chartType: body.chart_type === undefined ? undefined : String(body.chart_type),
17
+ config: (body.config ?? undefined) as Record<string, unknown> | undefined,
18
+ })
19
+ return { template }
20
+ })
@@ -0,0 +1,11 @@
1
+ /**
2
+ * GET /api/workshop/eda/templates —— 我的图表配置模板列表(admin 可见全部)。
3
+ */
4
+ import { defineApiHandler } from '@/server/utils/response'
5
+ import { resolveUser } from '@/server/api/workshop/caller'
6
+ import { listTemplates } from '@/server/services/workshop/eda/template-repo'
7
+
8
+ export default defineApiHandler(async (event) => {
9
+ const user = resolveUser(event)
10
+ return { templates: listTemplates(user) }
11
+ })
@@ -0,0 +1,20 @@
1
+ /**
2
+ * POST /api/workshop/eda/templates —— 保存图表配置模板。
3
+ * body: { name, description?, chart_type, config }
4
+ */
5
+ import { readBody } from 'h3'
6
+ import { defineApiHandler } from '@/server/utils/response'
7
+ import { resolveUser } from '@/server/api/workshop/caller'
8
+ import { createTemplate } from '@/server/services/workshop/eda/template-repo'
9
+
10
+ export default defineApiHandler(async (event) => {
11
+ const user = resolveUser(event)
12
+ const body = await readBody<Record<string, unknown>>(event) ?? {}
13
+ const template = createTemplate(user, {
14
+ name: String(body.name ?? ''),
15
+ description: body.description === undefined ? undefined : String(body.description),
16
+ chartType: String(body.chart_type ?? ''),
17
+ config: (body.config ?? {}) as Record<string, unknown>,
18
+ })
19
+ return { template }
20
+ })
@@ -17,7 +17,7 @@ import { lineMode } from '../permissions'
17
17
  import { recordOps } from '../ops/ops'
18
18
  import { edaDb } from './eda-db'
19
19
 
20
- export const CHART_VIZ_TYPES = ['trend', 'bar', 'scatter', 'table', 'kpi', 'histogram'] as const
20
+ export const CHART_VIZ_TYPES = ['trend', 'bar', 'scatter', 'table', 'kpi', 'histogram', 'pipeline'] as const
21
21
  export type ChartVizType = typeof CHART_VIZ_TYPES[number]
22
22
  const VISIBILITIES = ['private', 'line'] as const
23
23
 
@@ -74,6 +74,19 @@ const SCHEMA: readonly string[] = [
74
74
  'CREATE INDEX IF NOT EXISTS idx_eda_charts_owner ON eda_charts(owner_user_id, updated_at DESC)',
75
75
  'CREATE INDEX IF NOT EXISTS idx_eda_charts_line ON eda_charts(line_id, visibility)',
76
76
 
77
+ // EDA 管道:图表配置模板(用户把「图型+编码字段+后处理代码」存成命名模板,换数据快速复绘)
78
+ `CREATE TABLE IF NOT EXISTS eda_chart_templates (
79
+ id TEXT PRIMARY KEY,
80
+ name TEXT NOT NULL,
81
+ description TEXT NOT NULL DEFAULT '',
82
+ chart_type TEXT NOT NULL,
83
+ config_json TEXT NOT NULL DEFAULT '{}',
84
+ owner_user_id TEXT NOT NULL,
85
+ created_at TEXT NOT NULL,
86
+ updated_at TEXT NOT NULL
87
+ )`,
88
+ 'CREATE INDEX IF NOT EXISTS idx_eda_chart_templates_owner ON eda_chart_templates(owner_user_id, updated_at DESC)',
89
+
77
90
  // plan1-M3 看板 + 关联(Metabase report_dashboardcard 式)
78
91
  `CREATE TABLE IF NOT EXISTS eda_dashboards (
79
92
  id TEXT PRIMARY KEY,
@@ -0,0 +1,101 @@
1
+ /**
2
+ * EDA 图表配置模板 repo —— 用户把「图型 + 编码字段 + 后处理代码」存成命名模板,
3
+ * 换一份数据选模板即可快速复绘(管道页 Step4)。owner 私有,CRUD 全走本模块。
4
+ */
5
+ import { randomUUID } from 'node:crypto'
6
+ import { edaDb } from './eda-db'
7
+ import { AppError } from '@/server/utils/errors'
8
+ import { recordOps } from '../ops/ops'
9
+ import type { ResolvedUser } from '@/server/api/workshop/caller'
10
+
11
+ export interface ChartTemplateInput {
12
+ name: string
13
+ description?: string
14
+ chartType: string
15
+ config: Record<string, unknown>
16
+ }
17
+
18
+ export interface ChartTemplateRow {
19
+ id: string
20
+ name: string
21
+ description: string
22
+ chartType: string
23
+ config: Record<string, unknown>
24
+ ownerId: string
25
+ createdAt: string
26
+ updatedAt: string
27
+ }
28
+
29
+ export const CHART_TEMPLATE_TYPES = new Set(['line', 'bar', 'scatter', 'pie', 'histogram'])
30
+
31
+ function rowToTemplate(r: Record<string, unknown>): ChartTemplateRow {
32
+ return {
33
+ id: String(r.id),
34
+ name: String(r.name),
35
+ description: String(r.description ?? ''),
36
+ chartType: String(r.chart_type),
37
+ config: JSON.parse(String(r.config_json ?? '{}')) as Record<string, unknown>,
38
+ ownerId: String(r.owner_user_id),
39
+ createdAt: String(r.created_at),
40
+ updatedAt: String(r.updated_at),
41
+ }
42
+ }
43
+
44
+ export function createTemplate(user: ResolvedUser, input: ChartTemplateInput): ChartTemplateRow {
45
+ const name = input.name.trim()
46
+ if (!name) throw new AppError(400, 'VALIDATION_ERROR' as never, '模板名必填。')
47
+ if (!CHART_TEMPLATE_TYPES.has(input.chartType)) throw new AppError(400, 'VALIDATION_ERROR' as never, `不支持的图型: ${input.chartType}`)
48
+ const id = `et-${randomUUID().slice(0, 8)}`
49
+ const now = new Date().toISOString()
50
+ edaDb().prepare(`INSERT INTO eda_chart_templates (id, name, description, chart_type, config_json, owner_user_id, created_at, updated_at)
51
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
52
+ .run(id, name.slice(0, 120), (input.description ?? '').slice(0, 500), input.chartType,
53
+ JSON.stringify(input.config ?? {}), user.id, now, now)
54
+ recordOps({
55
+ actor: user.id, actorName: user.name, actorKind: 'user',
56
+ action: 'eda.template.save', kind: 'system', targetKind: 'eda-chart-template', targetId: id,
57
+ summary: `保存图表模板「${name}」(${input.chartType})`,
58
+ })
59
+ return templateById(user, id)!
60
+ }
61
+
62
+ export function templateById(user: ResolvedUser, id: string): ChartTemplateRow | undefined {
63
+ const r = edaDb().prepare('SELECT * FROM eda_chart_templates WHERE id = ?').get(id) as Record<string, unknown> | undefined
64
+ if (!r) return undefined
65
+ const row = rowToTemplate(r)
66
+ if (row.ownerId !== user.id && user.role !== 'admin') throw new AppError(403, 'FORBIDDEN' as never, '仅 owner 或管理员可读取该模板。')
67
+ return row
68
+ }
69
+
70
+ export function listTemplates(user: ResolvedUser): ChartTemplateRow[] {
71
+ const rows = user.role === 'admin'
72
+ ? edaDb().prepare('SELECT * FROM eda_chart_templates ORDER BY updated_at DESC').all() as Record<string, unknown>[]
73
+ : edaDb().prepare('SELECT * FROM eda_chart_templates WHERE owner_user_id = ? ORDER BY updated_at DESC').all(user.id) as Record<string, unknown>[]
74
+ return rows.map(rowToTemplate)
75
+ }
76
+
77
+ export function updateTemplate(user: ResolvedUser, id: string, patch: Partial<ChartTemplateInput>): ChartTemplateRow {
78
+ const existing = templateById(user, id)
79
+ if (!existing) throw new AppError(404, 'NOT_FOUND' as never, `模板不存在: ${id}`)
80
+ if (existing.ownerId !== user.id && user.role !== 'admin') throw new AppError(403, 'FORBIDDEN' as never, '仅 owner 或管理员可修改该模板。')
81
+ const name = (patch.name ?? existing.name).trim().slice(0, 120)
82
+ if (!name) throw new AppError(400, 'VALIDATION_ERROR' as never, '模板名必填。')
83
+ const chartType = patch.chartType ?? existing.chartType
84
+ if (!CHART_TEMPLATE_TYPES.has(chartType)) throw new AppError(400, 'VALIDATION_ERROR' as never, `不支持的图型: ${chartType}`)
85
+ edaDb().prepare(`UPDATE eda_chart_templates SET name = ?, description = ?, chart_type = ?, config_json = ?, updated_at = ? WHERE id = ?`)
86
+ .run(name, (patch.description ?? existing.description).slice(0, 500), chartType,
87
+ JSON.stringify(patch.config ?? existing.config), new Date().toISOString(), id)
88
+ return templateById(user, id)!
89
+ }
90
+
91
+ export function deleteTemplate(user: ResolvedUser, id: string): void {
92
+ const existing = templateById(user, id)
93
+ if (!existing) throw new AppError(404, 'NOT_FOUND' as never, `模板不存在: ${id}`)
94
+ if (existing.ownerId !== user.id && user.role !== 'admin') throw new AppError(403, 'FORBIDDEN' as never, '仅 owner 或管理员可删除该模板。')
95
+ edaDb().prepare('DELETE FROM eda_chart_templates WHERE id = ?').run(id)
96
+ recordOps({
97
+ actor: user.id, actorName: user.name, actorKind: 'user',
98
+ action: 'eda.template.delete', kind: 'system', targetKind: 'eda-chart-template', targetId: id,
99
+ summary: `删除图表模板「${existing.name}」`,
100
+ })
101
+ }