agentworkshop 0.5.3 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.output/nitro.json +1 -1
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/f2f649c5-fd3b-4507-a3a1-8b4a194de72d.json +1 -0
- package/.output/server/chunks/_/nitro.mjs +798 -679
- package/.output/server/chunks/_/nitro.mjs.map +1 -1
- package/.output/server/package.json +1 -1
- package/cli/commands/start.mjs +2 -2
- package/package.json +1 -1
- package/scripts/_dbg-prompts-resolve-test.ts +39 -0
- package/scripts/_dbg-retention-test.ts +25 -0
- package/scripts/dev-guard.mjs +33 -0
- package/scripts/start.mjs +13 -0
- package/server/data/daqs.json +75 -75
- package/server/data/dcw-lines.json +7 -0
- package/server/data/dcw-products.json +7 -0
- package/server/data/dcw-rollback.json +139 -1
- package/server/data/dcw-runs.json +27 -18
- package/server/data/dcw-writes.json +48 -0
- package/server/data/dcws.json +21 -0
- package/server/data/line-runs.json +22 -22
- package/server/plugins/backup.ts +21 -11
- package/server/plugins/dev-stability-guard.ts +13 -1
- package/server/plugins/retention.ts +57 -0
- package/server/services/workshop/daq/daq-controller.ts +4 -2
- package/server/services/workshop/db/retention.ts +50 -0
- package/server/services/workshop/dcw/dcw-controller.ts +4 -2
- package/server/services/workshop/prompts/loader.ts +63 -10
- package/shared/config/single-instance.mjs +86 -0
- package/.output/public/_nuxt/builds/meta/fb769eda-b1db-437b-adc7-ecf71eb3b30b.json +0 -1
|
@@ -1,22 +1,4 @@
|
|
|
1
1
|
[
|
|
2
|
-
{
|
|
3
|
-
"id": "rr-57bca111",
|
|
4
|
-
"recipeId": "rc-ee30a7a0",
|
|
5
|
-
"recipeName": "终审配方A",
|
|
6
|
-
"productId": "pd-99159b45",
|
|
7
|
-
"lineId": "ln-c6713e4d",
|
|
8
|
-
"startedAt": "2026-08-29T15:13:03.707Z",
|
|
9
|
-
"endedAt": "2026-08-29T15:14:14.609Z",
|
|
10
|
-
"results": [
|
|
11
|
-
{
|
|
12
|
-
"templateRef": "dcw-temp-sp",
|
|
13
|
-
"nodeId": "dw-c4da380a",
|
|
14
|
-
"ok": true,
|
|
15
|
-
"message": "写入并回读一致:182 → raw 1280,回读 182(标定后物理值 182)",
|
|
16
|
-
"value": 182
|
|
17
|
-
}
|
|
18
|
-
]
|
|
19
|
-
},
|
|
20
2
|
{
|
|
21
3
|
"id": "rr-c8ccb1f5",
|
|
22
4
|
"recipeId": "rc-3b094c50",
|
|
@@ -4731,5 +4713,32 @@
|
|
|
4731
4713
|
"max": 188
|
|
4732
4714
|
}
|
|
4733
4715
|
]
|
|
4716
|
+
},
|
|
4717
|
+
{
|
|
4718
|
+
"id": "rr-09acb43b",
|
|
4719
|
+
"recipeId": "rc-4d460065",
|
|
4720
|
+
"recipeName": "全功能配方-41bdm2",
|
|
4721
|
+
"productId": "pd-33f3607e",
|
|
4722
|
+
"lineId": "ln-69eaecb9",
|
|
4723
|
+
"startedAt": "2026-09-02T10:37:44.029Z",
|
|
4724
|
+
"endedAt": "2026-09-02T10:43:20.233Z",
|
|
4725
|
+
"results": [
|
|
4726
|
+
{
|
|
4727
|
+
"templateRef": "dcw-temp-sp",
|
|
4728
|
+
"nodeId": "dw-eeedb292",
|
|
4729
|
+
"ok": true,
|
|
4730
|
+
"message": "Mock PLC 写入成功:180 → raw 180,回读一致(标定后物理值 180)",
|
|
4731
|
+
"value": 180
|
|
4732
|
+
}
|
|
4733
|
+
],
|
|
4734
|
+
"paramsSnapshot": [
|
|
4735
|
+
{
|
|
4736
|
+
"nodeId": "dw-eeedb292",
|
|
4737
|
+
"templateRef": "dcw-temp-sp",
|
|
4738
|
+
"value": 180,
|
|
4739
|
+
"min": 176,
|
|
4740
|
+
"max": 188
|
|
4741
|
+
}
|
|
4742
|
+
]
|
|
4734
4743
|
}
|
|
4735
4744
|
]
|
|
@@ -9862,5 +9862,53 @@
|
|
|
9862
9862
|
"message": "Mock PLC 写入成功:182 → raw 182,回读一致(标定后物理值 182)",
|
|
9863
9863
|
"recipeRunId": null,
|
|
9864
9864
|
"at": "2026-09-02T08:46:51.079Z"
|
|
9865
|
+
},
|
|
9866
|
+
{
|
|
9867
|
+
"id": "wh-4b83484b",
|
|
9868
|
+
"nodeId": "dw-eeedb292",
|
|
9869
|
+
"nodeName": "FFX-温度设定",
|
|
9870
|
+
"param": "烘箱温度设定",
|
|
9871
|
+
"eng": 180,
|
|
9872
|
+
"raw": 180,
|
|
9873
|
+
"ok": true,
|
|
9874
|
+
"message": "Mock PLC 写入成功:180 → raw 180,回读一致(标定后物理值 180)",
|
|
9875
|
+
"recipeRunId": "rr-09acb43b",
|
|
9876
|
+
"at": "2026-09-02T10:37:44.035Z"
|
|
9877
|
+
},
|
|
9878
|
+
{
|
|
9879
|
+
"id": "wh-515c6f27",
|
|
9880
|
+
"nodeId": "dw-eeedb292",
|
|
9881
|
+
"nodeName": "FFX-温度设定",
|
|
9882
|
+
"param": "烘箱温度设定",
|
|
9883
|
+
"eng": 182,
|
|
9884
|
+
"raw": 182,
|
|
9885
|
+
"ok": true,
|
|
9886
|
+
"message": "Mock PLC 写入成功:182 → raw 182,回读一致(标定后物理值 182)",
|
|
9887
|
+
"recipeRunId": null,
|
|
9888
|
+
"at": "2026-09-02T10:37:51.026Z"
|
|
9889
|
+
},
|
|
9890
|
+
{
|
|
9891
|
+
"id": "wh-f337d292",
|
|
9892
|
+
"nodeId": "dw-eeedb292",
|
|
9893
|
+
"nodeName": "FFX-温度设定",
|
|
9894
|
+
"param": "烘箱温度设定",
|
|
9895
|
+
"eng": 180,
|
|
9896
|
+
"raw": 180,
|
|
9897
|
+
"ok": true,
|
|
9898
|
+
"message": "Mock PLC 写入成功:180 → raw 180,回读一致(标定后物理值 180)",
|
|
9899
|
+
"recipeRunId": null,
|
|
9900
|
+
"at": "2026-09-02T10:37:51.168Z"
|
|
9901
|
+
},
|
|
9902
|
+
{
|
|
9903
|
+
"id": "wh-f7919063",
|
|
9904
|
+
"nodeId": "dw-eeedb292",
|
|
9905
|
+
"nodeName": "FFX-温度设定",
|
|
9906
|
+
"param": "烘箱温度设定",
|
|
9907
|
+
"eng": 182,
|
|
9908
|
+
"raw": 182,
|
|
9909
|
+
"ok": true,
|
|
9910
|
+
"message": "Mock PLC 写入成功:182 → raw 182,回读一致(标定后物理值 182)",
|
|
9911
|
+
"recipeRunId": null,
|
|
9912
|
+
"at": "2026-09-02T10:39:20.083Z"
|
|
9865
9913
|
}
|
|
9866
9914
|
]
|
package/server/data/dcws.json
CHANGED
|
@@ -2026,5 +2026,26 @@
|
|
|
2026
2026
|
"state": "ok",
|
|
2027
2027
|
"lastError": null,
|
|
2028
2028
|
"createdAt": "2026-09-02T08:44:01.557Z"
|
|
2029
|
+
},
|
|
2030
|
+
{
|
|
2031
|
+
"id": "dw-eeedb292",
|
|
2032
|
+
"templateRef": "dcw-temp-sp",
|
|
2033
|
+
"name": "FFX-温度设定",
|
|
2034
|
+
"driver": "mock",
|
|
2035
|
+
"enabled": true,
|
|
2036
|
+
"holdIntervalMs": null,
|
|
2037
|
+
"unit": "℃",
|
|
2038
|
+
"decimals": 1,
|
|
2039
|
+
"min": 150,
|
|
2040
|
+
"max": 200,
|
|
2041
|
+
"deviceBindingId": null,
|
|
2042
|
+
"driverConfig": {},
|
|
2043
|
+
"lineId": "ln-69eaecb9",
|
|
2044
|
+
"value": 182,
|
|
2045
|
+
"lastAckAt": "2026-09-02T10:39:20.083Z",
|
|
2046
|
+
"lastWriteAt": "2026-09-02T10:39:20.083Z",
|
|
2047
|
+
"state": "ok",
|
|
2048
|
+
"lastError": null,
|
|
2049
|
+
"createdAt": "2026-09-02T10:37:43.697Z"
|
|
2029
2050
|
}
|
|
2030
2051
|
]
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"productId": "pd-3ad54846",
|
|
8
8
|
"productName": "光学膜 BOPP",
|
|
9
9
|
"startedAt": "2026-08-31T16:20:13.707Z",
|
|
10
|
-
"taggedSamples":
|
|
10
|
+
"taggedSamples": 34566
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
"lineId": "ln-c377a6bf",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"productId": "pd-7e69e0ee",
|
|
18
18
|
"productName": "警告验证产品-k2xh",
|
|
19
19
|
"startedAt": "2026-09-01T03:23:18.233Z",
|
|
20
|
-
"taggedSamples":
|
|
20
|
+
"taggedSamples": 14092
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"lineId": "ln-9224d3d7",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"productId": "pd-910c0fa8",
|
|
28
28
|
"productName": "新协议产品-9xhn",
|
|
29
29
|
"startedAt": "2026-09-01T05:59:24.062Z",
|
|
30
|
-
"taggedSamples":
|
|
30
|
+
"taggedSamples": 17074
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
"lineId": "ln-386c53ed",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"productId": "pd-2d4f9357",
|
|
38
38
|
"productName": "新协议产品-6ofk",
|
|
39
39
|
"startedAt": "2026-09-01T06:05:53.063Z",
|
|
40
|
-
"taggedSamples":
|
|
40
|
+
"taggedSamples": 17363
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
"lineId": "ln-5957f6db",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"productId": "pd-a3ed5047",
|
|
48
48
|
"productName": "新协议产品-x4hd",
|
|
49
49
|
"startedAt": "2026-09-01T06:09:02.148Z",
|
|
50
|
-
"taggedSamples":
|
|
50
|
+
"taggedSamples": 16949
|
|
51
51
|
},
|
|
52
52
|
{
|
|
53
53
|
"lineId": "ln-9ec42f08",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"productId": "pd-5de729c0",
|
|
58
58
|
"productName": "新协议产品-teti",
|
|
59
59
|
"startedAt": "2026-09-01T06:13:44.315Z",
|
|
60
|
-
"taggedSamples":
|
|
60
|
+
"taggedSamples": 16928
|
|
61
61
|
},
|
|
62
62
|
{
|
|
63
63
|
"lineId": "ln-69e37924",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"productId": "pd-45eb8d43",
|
|
68
68
|
"productName": "新协议产品-fg8t",
|
|
69
69
|
"startedAt": "2026-09-01T06:16:47.469Z",
|
|
70
|
-
"taggedSamples":
|
|
70
|
+
"taggedSamples": 16961
|
|
71
71
|
},
|
|
72
72
|
{
|
|
73
73
|
"lineId": "ln-c269e692",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"productId": "pd-b3bd0191",
|
|
78
78
|
"productName": "新协议产品-w2sp",
|
|
79
79
|
"startedAt": "2026-09-01T06:56:05.893Z",
|
|
80
|
-
"taggedSamples":
|
|
80
|
+
"taggedSamples": 17012
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
"lineId": "ln-229d1637",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"productId": "pd-06b896f9",
|
|
88
88
|
"productName": "新协议产品-lp4h",
|
|
89
89
|
"startedAt": "2026-09-01T06:57:29.288Z",
|
|
90
|
-
"taggedSamples":
|
|
90
|
+
"taggedSamples": 16884
|
|
91
91
|
},
|
|
92
92
|
{
|
|
93
93
|
"lineId": "ln-9546f91f",
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"productId": "pd-53e55078",
|
|
98
98
|
"productName": "新协议产品-jri6",
|
|
99
99
|
"startedAt": "2026-09-01T07:00:25.059Z",
|
|
100
|
-
"taggedSamples":
|
|
100
|
+
"taggedSamples": 16307
|
|
101
101
|
},
|
|
102
102
|
{
|
|
103
103
|
"lineId": "ln-087aad5b",
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
"productId": "pd-e7e969af",
|
|
108
108
|
"productName": "新协议产品-4c2d",
|
|
109
109
|
"startedAt": "2026-09-01T07:09:07.307Z",
|
|
110
|
-
"taggedSamples":
|
|
110
|
+
"taggedSamples": 15299
|
|
111
111
|
},
|
|
112
112
|
{
|
|
113
113
|
"lineId": "ln-97d44f8a",
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
"productId": "pd-28a3ab2c",
|
|
118
118
|
"productName": "新协议产品-b2ni",
|
|
119
119
|
"startedAt": "2026-09-01T07:13:44.912Z",
|
|
120
|
-
"taggedSamples":
|
|
120
|
+
"taggedSamples": 15148
|
|
121
121
|
},
|
|
122
122
|
{
|
|
123
123
|
"lineId": "ln-a9551770",
|
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
"productId": "pd-0c6a64c8",
|
|
128
128
|
"productName": "新协议产品-uwkf",
|
|
129
129
|
"startedAt": "2026-09-01T07:15:40.113Z",
|
|
130
|
-
"taggedSamples":
|
|
130
|
+
"taggedSamples": 14793
|
|
131
131
|
},
|
|
132
132
|
{
|
|
133
133
|
"lineId": "ln-844ad82f",
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
"productId": "pd-6932b24f",
|
|
138
138
|
"productName": "新协议产品-kk6l",
|
|
139
139
|
"startedAt": "2026-09-01T09:06:55.728Z",
|
|
140
|
-
"taggedSamples":
|
|
140
|
+
"taggedSamples": 12948
|
|
141
141
|
},
|
|
142
142
|
{
|
|
143
143
|
"lineId": "ln-af002514",
|
|
@@ -147,7 +147,7 @@
|
|
|
147
147
|
"productId": "pd-6348a595",
|
|
148
148
|
"productName": "0.8mm 光学膜",
|
|
149
149
|
"startedAt": "2026-09-01T14:06:28.766Z",
|
|
150
|
-
"taggedSamples":
|
|
150
|
+
"taggedSamples": 5772
|
|
151
151
|
},
|
|
152
152
|
{
|
|
153
153
|
"lineId": "ln-0eccf6ba",
|
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
"productId": "pd-4b71be77",
|
|
158
158
|
"productName": "哨兵产品-mtjku36w",
|
|
159
159
|
"startedAt": "2026-09-02T04:09:11.482Z",
|
|
160
|
-
"taggedSamples":
|
|
160
|
+
"taggedSamples": 1213
|
|
161
161
|
},
|
|
162
162
|
{
|
|
163
163
|
"lineId": "ln-e984246e",
|
|
@@ -167,7 +167,7 @@
|
|
|
167
167
|
"productId": "pd-bb1e0bed",
|
|
168
168
|
"productName": "产品C-mtjkwyhu",
|
|
169
169
|
"startedAt": "2026-09-02T04:11:26.583Z",
|
|
170
|
-
"taggedSamples":
|
|
170
|
+
"taggedSamples": 1212
|
|
171
171
|
},
|
|
172
172
|
{
|
|
173
173
|
"lineId": "ln-1fe0b5bc",
|
|
@@ -177,7 +177,7 @@
|
|
|
177
177
|
"productId": "pd-9f2d9507",
|
|
178
178
|
"productName": "产品D-mtjkzcfr",
|
|
179
179
|
"startedAt": "2026-09-02T04:13:17.902Z",
|
|
180
|
-
"taggedSamples":
|
|
180
|
+
"taggedSamples": 1211
|
|
181
181
|
},
|
|
182
182
|
{
|
|
183
183
|
"lineId": "ln-d826473e",
|
|
@@ -187,7 +187,7 @@
|
|
|
187
187
|
"productId": "pd-d230255e",
|
|
188
188
|
"productName": "产品E-mtjl43pv",
|
|
189
189
|
"startedAt": "2026-09-02T04:16:59.891Z",
|
|
190
|
-
"taggedSamples":
|
|
190
|
+
"taggedSamples": 1210
|
|
191
191
|
},
|
|
192
192
|
{
|
|
193
193
|
"lineId": "ln-156abb71",
|
|
@@ -197,7 +197,7 @@
|
|
|
197
197
|
"productId": "pd-6065b4ba",
|
|
198
198
|
"productName": "产品F-mtjl60qf",
|
|
199
199
|
"startedAt": "2026-09-02T04:18:29.340Z",
|
|
200
|
-
"taggedSamples":
|
|
200
|
+
"taggedSamples": 1209
|
|
201
201
|
},
|
|
202
202
|
{
|
|
203
203
|
"lineId": "ln-bac43927",
|
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
"productId": "pd-52f54e33",
|
|
208
208
|
"productName": "产品G-mtjl98nl",
|
|
209
209
|
"startedAt": "2026-09-02T04:20:59.682Z",
|
|
210
|
-
"taggedSamples":
|
|
210
|
+
"taggedSamples": 1208
|
|
211
211
|
},
|
|
212
212
|
{
|
|
213
213
|
"lineId": "ln-f223008a",
|
|
@@ -217,6 +217,6 @@
|
|
|
217
217
|
"productId": "pd-d919d4bf",
|
|
218
218
|
"productName": "产品-mtjo000v",
|
|
219
219
|
"startedAt": "2026-09-02T05:37:50.743Z",
|
|
220
|
-
"taggedSamples":
|
|
220
|
+
"taggedSamples": 1207
|
|
221
221
|
}
|
|
222
222
|
]
|
package/server/plugins/backup.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 备份定时插件(S6,production-readiness-plan)。
|
|
3
3
|
*
|
|
4
|
-
* 对 data/ 下三库(workshop/users/daq-timeseries)每日
|
|
4
|
+
* 对 data/ 下三库(workshop/users/daq-timeseries)每日 serialize 镜像快照到
|
|
5
5
|
* data/backups/,按 BACKUP_KEEP(默认 7)轮转;调度模式复用 ws.ts 保留期清理的
|
|
6
6
|
* setInterval + globalThis key 防 HMR 重复 + unref()。dev 与生产均生效
|
|
7
7
|
* (备份无副作用,始终开启;BACKUP_DISABLED=1 可关)。
|
|
8
8
|
*/
|
|
9
9
|
import { DatabaseSync } from 'node:sqlite'
|
|
10
|
-
import { existsSync, mkdirSync, readdirSync, rmSync } from 'node:fs'
|
|
10
|
+
import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
11
11
|
import { resolve } from 'node:path'
|
|
12
12
|
import { ensureDataDir } from '@/shared/config/home.mjs'
|
|
13
13
|
|
|
@@ -15,7 +15,23 @@ const g = globalThis as typeof globalThis & { __awBackupTimer?: NodeJS.Timeout,
|
|
|
15
15
|
|
|
16
16
|
const DB_FILES = ['workshop.sqlite', 'users.sqlite', 'daq-timeseries.sqlite'] as const
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* 单库在线快照(hardening ST-2):node:sqlite serialize() 一致性数据库镜像 → 落盘。
|
|
20
|
+
* 纯读操作,不与主写者抢锁(VACUUM INTO 会与写事务竞争 SQLITE_BUSY,已实测);
|
|
21
|
+
* 产物为标准 SQLite 文件,与历史 .bak 结构兼容。失败只记录不抛出。
|
|
22
|
+
*/
|
|
23
|
+
async function backupOne(src: string, target: string): Promise<void> {
|
|
24
|
+
const db = new DatabaseSync(src)
|
|
25
|
+
try {
|
|
26
|
+
const image = db.serialize()
|
|
27
|
+
writeFileSync(target, image)
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
db.close()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function backupOnce(dataDir: string): Promise<void> {
|
|
19
35
|
const backupDir = resolve(dataDir, 'backups')
|
|
20
36
|
mkdirSync(backupDir, { recursive: true })
|
|
21
37
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
|
|
@@ -24,14 +40,8 @@ function backupOnce(dataDir: string): void {
|
|
|
24
40
|
if (!existsSync(src)) continue
|
|
25
41
|
const target = resolve(backupDir, `${file}.${stamp}.bak`)
|
|
26
42
|
try {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// VACUUM INTO:在线一致性快照(WAL 下安全),不阻塞长事务
|
|
30
|
-
db.exec(`VACUUM INTO '${target.replace(/\\/g, '/')}'`)
|
|
31
|
-
}
|
|
32
|
-
finally {
|
|
33
|
-
db.close()
|
|
34
|
-
}
|
|
43
|
+
await backupOne(src, target)
|
|
44
|
+
console.log('[backup]', file, '快照完成(serialize)')
|
|
35
45
|
}
|
|
36
46
|
catch (err) {
|
|
37
47
|
console.error(`[backup] ${file} 快照失败:`, err instanceof Error ? err.message : err)
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* 依赖异常中止进程,绝不能被护栏吞掉。
|
|
8
8
|
*/
|
|
9
9
|
const SOCKET_NOISE = /ECONNRESET|EPIPE|ECONNABORTED|ETIMEDOUT|ERR_STREAM_PREMATURE_CLOSE/
|
|
10
|
+
// SQLite 忙/锁(hardening ST-1):多写竞争的瞬态错误,记录并继续;
|
|
11
|
+
// 真正的库损坏/磁盘错误不经此路径,仍走致命语义。
|
|
12
|
+
const SQLITE_BUSY = /SQLITE_BUSY|SQLITE_LOCKED|database is locked/i
|
|
10
13
|
|
|
11
14
|
function isSocketNoise(reason: unknown): boolean {
|
|
12
15
|
if (reason == null) return false
|
|
@@ -22,11 +25,16 @@ function describe(reason: unknown): string {
|
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
export default defineNitroPlugin(() => {
|
|
28
|
+
const isTransientDbBusy = (reason: unknown): boolean => SQLITE_BUSY.test(describe(reason))
|
|
25
29
|
process.on('unhandledRejection', (reason) => {
|
|
26
30
|
if (isSocketNoise(reason)) {
|
|
27
31
|
console.warn(`[stability-guard] socket noise suppressed: ${describe(reason).slice(0, 120)}`)
|
|
28
32
|
return
|
|
29
33
|
}
|
|
34
|
+
if (isTransientDbBusy(reason)) {
|
|
35
|
+
console.warn(`[stability-guard] sqlite busy(瞬态,不退出): ${describe(reason).slice(0, 120)}`)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
30
38
|
console.error(`[stability-guard] fatal unhandledRejection, exiting:\n${describe(reason)}`)
|
|
31
39
|
process.exit(1)
|
|
32
40
|
})
|
|
@@ -35,8 +43,12 @@ export default defineNitroPlugin(() => {
|
|
|
35
43
|
console.warn(`[stability-guard] socket noise suppressed: ${describe(err).slice(0, 120)}`)
|
|
36
44
|
return
|
|
37
45
|
}
|
|
46
|
+
if (isTransientDbBusy(err)) {
|
|
47
|
+
console.warn(`[stability-guard] sqlite busy(瞬态,不退出): ${describe(err).slice(0, 120)}`)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
38
50
|
console.error(`[stability-guard] fatal uncaughtException, exiting:\n${describe(err)}`)
|
|
39
51
|
process.exit(1)
|
|
40
52
|
})
|
|
41
|
-
console.warn('[stability-guard] armed (socket
|
|
53
|
+
console.warn('[stability-guard] armed (socket 断连与 sqlite 瞬态忙 → 记录继续;真实错误 → exit 1)')
|
|
42
54
|
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 保留期定时插件(hardening GROW-1)。
|
|
3
|
+
*
|
|
4
|
+
* 每 24h 对 workshop.sqlite 的 messages/audit_log/approval_history 做分批保留
|
|
5
|
+
* 清理(策略见 db/retention.ts;RETENTION_DISABLED=1 可整体关闭)。
|
|
6
|
+
* 调度模式同 backup.ts:setInterval + globalThis key 防 HMR 重复 + unref()。
|
|
7
|
+
* 启动 90s 后首跑(避开启动风暴,且错峰于 backup 的 30s 首备)。
|
|
8
|
+
*/
|
|
9
|
+
import { DatabaseSync } from 'node:sqlite'
|
|
10
|
+
import { existsSync } from 'node:fs'
|
|
11
|
+
import { resolve } from 'node:path'
|
|
12
|
+
import { runRetentionSweep } from '@/server/services/workshop/db/retention'
|
|
13
|
+
import { ensureDataDir } from '@/shared/config/home.mjs'
|
|
14
|
+
|
|
15
|
+
const g = globalThis as typeof globalThis & { __awRetentionTimer?: NodeJS.Timeout }
|
|
16
|
+
|
|
17
|
+
function sweepOnce(dataDir: string): void {
|
|
18
|
+
const dbPath = resolve(dataDir, 'workshop.sqlite')
|
|
19
|
+
if (!existsSync(dbPath)) return
|
|
20
|
+
const db = new DatabaseSync(dbPath)
|
|
21
|
+
try {
|
|
22
|
+
const results = runRetentionSweep(db)
|
|
23
|
+
for (const r of results) {
|
|
24
|
+
if (r.removed > 0) console.log(`[retention] ${r.table} 清理 ${r.removed} 行(>${r.days}d)`)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
db.close()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default function retentionPlugin() {
|
|
33
|
+
if (process.env.RETENTION_DISABLED === '1' || process.env.AW_RETENTION_DISABLED === '1') return
|
|
34
|
+
if (g.__awRetentionTimer) return
|
|
35
|
+
const dataDir = ensureDataDir()
|
|
36
|
+
|
|
37
|
+
const first = setTimeout(() => {
|
|
38
|
+
try {
|
|
39
|
+
sweepOnce(dataDir)
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
console.error('[retention] 首轮清理失败:', err instanceof Error ? err.message : err)
|
|
43
|
+
}
|
|
44
|
+
}, 90_000)
|
|
45
|
+
first.unref?.()
|
|
46
|
+
|
|
47
|
+
const timer = setInterval(() => {
|
|
48
|
+
try {
|
|
49
|
+
sweepOnce(dataDir)
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
console.error('[retention] 定时清理失败:', err instanceof Error ? err.message : err)
|
|
53
|
+
}
|
|
54
|
+
}, 24 * 3_600_000)
|
|
55
|
+
timer.unref?.()
|
|
56
|
+
g.__awRetentionTimer = timer
|
|
57
|
+
}
|
|
@@ -476,12 +476,14 @@ class DaqController {
|
|
|
476
476
|
controllerState(): AepDaqControllerState & { produced?: number, consumed?: number, dropped?: number, samplesStored?: number, tsdbDropped?: number } {
|
|
477
477
|
// 丢弃 = 队列层真实丢弃(inproc 拥塞/mqtt 断连)+ 消费侧乱序迟到帧(诚实可见)
|
|
478
478
|
const queueLost = g_queueLost()
|
|
479
|
+
// 单次遍历(hardening PERF-1):status 高频路径不再对全表多次 all()
|
|
480
|
+
const nodes = this.repo.all()
|
|
479
481
|
return {
|
|
480
482
|
running: this.running,
|
|
481
483
|
defaultIntervalMs: this.defaultIntervalMs,
|
|
482
484
|
defaultPublishIntervalMs: this.defaultPublishIntervalMs,
|
|
483
|
-
nodesTotal:
|
|
484
|
-
nodesOnline: this.
|
|
485
|
+
nodesTotal: nodes.length,
|
|
486
|
+
nodesOnline: this.running ? nodes.filter(n => n.enabled).length : 0,
|
|
485
487
|
produced: this.producedCount,
|
|
486
488
|
consumed: this.consumedCount,
|
|
487
489
|
dropped: Math.max(0, this.producedCount - this.consumedCount) + queueLost + this.lateDropped,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 增长治理保留策略(hardening GROW-1)。
|
|
3
|
+
*
|
|
4
|
+
* 为无保留机制的只增表做分批清理(每批 ≤5000,循环到 0,避免长事务持锁):
|
|
5
|
+
* messages 默认 30d (AW_MESSAGES_RETENTION_D)
|
|
6
|
+
* audit_log 默认 90d (AW_AUDIT_RETENTION_D;合规观察期,从严保留)
|
|
7
|
+
* approval_history 默认 180d (AW_APPROVAL_RETENTION_D;历史裁决主张不可变,放宽)
|
|
8
|
+
* AW_RETENTION_DISABLED=1 整体逃生门。时间列均为 TEXT ISO,可字典序比较。
|
|
9
|
+
* channel_events 与 daq_samples 已有各自保留机制,此处不重复覆盖。
|
|
10
|
+
*/
|
|
11
|
+
type DatabaseSyncLike = {
|
|
12
|
+
prepare: (sql: string) => { run: (...args: unknown[]) => { changes: number | bigint } }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const DAY_MS = 86_400_000
|
|
16
|
+
const BATCH = 5000
|
|
17
|
+
|
|
18
|
+
const TARGETS = [
|
|
19
|
+
{ table: 'messages', column: 'created_at', days: 30, envKey: 'AW_MESSAGES_RETENTION_D' },
|
|
20
|
+
{ table: 'audit_log', column: 'at', days: 90, envKey: 'AW_AUDIT_RETENTION_D' },
|
|
21
|
+
{ table: 'approval_history', column: 'created_at', days: 180, envKey: 'AW_APPROVAL_RETENTION_D' },
|
|
22
|
+
] as const
|
|
23
|
+
|
|
24
|
+
function retentionDays(envKey: string, fallback: number): number {
|
|
25
|
+
const n = Number(process.env[envKey])
|
|
26
|
+
return Number.isFinite(n) && n > 0 ? n : fallback
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface RetentionResult { table: string, removed: number, days: number }
|
|
30
|
+
|
|
31
|
+
/** 对单库执行一轮保留清理;返回各表删除行数(调用方负责 try/catch 与日志)。 */
|
|
32
|
+
export function runRetentionSweep(db: DatabaseSyncLike): RetentionResult[] {
|
|
33
|
+
if (process.env.AW_RETENTION_DISABLED === '1') return []
|
|
34
|
+
const out: RetentionResult[] = []
|
|
35
|
+
for (const t of TARGETS) {
|
|
36
|
+
const days = retentionDays(t.envKey, t.days)
|
|
37
|
+
const before = new Date(Date.now() - days * DAY_MS).toISOString()
|
|
38
|
+
let removed = 0
|
|
39
|
+
for (;;) {
|
|
40
|
+
const res = db.prepare(
|
|
41
|
+
`DELETE FROM ${t.table} WHERE rowid IN (SELECT rowid FROM ${t.table} WHERE ${t.column} < ? LIMIT ${BATCH})`,
|
|
42
|
+
).run(before)
|
|
43
|
+
const n = Number(res.changes)
|
|
44
|
+
removed += n
|
|
45
|
+
if (n < BATCH) break
|
|
46
|
+
}
|
|
47
|
+
out.push({ table: t.table, removed, days })
|
|
48
|
+
}
|
|
49
|
+
return out
|
|
50
|
+
}
|
|
@@ -218,10 +218,12 @@ class DcwController {
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
controllerState() {
|
|
221
|
+
// 单次遍历(hardening PERF-1):status 高频路径不再对全表多次 all()
|
|
222
|
+
const nodes = this.repo.all()
|
|
221
223
|
return {
|
|
222
224
|
running: this.running,
|
|
223
|
-
nodesTotal:
|
|
224
|
-
nodesOnline: this.running ?
|
|
225
|
+
nodesTotal: nodes.length,
|
|
226
|
+
nodesOnline: this.running ? nodes.filter(n => n.enabled).length : 0,
|
|
225
227
|
writesTotal: this.writesTotal,
|
|
226
228
|
writesFailed: this.writesFailed,
|
|
227
229
|
}
|
|
@@ -7,11 +7,18 @@
|
|
|
7
7
|
* - 缓存:mtime 感知(文件修改即刻生效,dev/HMR 友好;stat 开销可忽略)
|
|
8
8
|
* - host-tools.json:结构化工具定义(name/label/description/parameters),
|
|
9
9
|
* JSON 承载 schema,与叙事型 md 并置同一目录统一管理
|
|
10
|
-
* -
|
|
10
|
+
* - 目录解析(hardening 后的规约,与配置根优先级同族):
|
|
11
|
+
* ① AW_PROMPTS_DIR 环境变量(显式覆盖,兼容旧约定)
|
|
12
|
+
* ② 项目内 <cwd 向上>/.AgentWorkShop/prompts 真实存在 → 项目内优先
|
|
13
|
+
* ③ 否则 ~/.AgentWorkShop/prompts —— 首次解析时从包内资产播种初始文件
|
|
14
|
+
* (只补缺失文件,绝不覆盖已有内容 → 用户在 Home 的定制即权威版本);
|
|
15
|
+
* 播种源 = AW_PACKAGE_ROOT(启动器注入的包根)/.AgentWorkShop/prompts
|
|
11
16
|
* 启动即校验(缺文件 fail-fast,防静默降级为无 prompt 裸跑)
|
|
12
17
|
*/
|
|
13
|
-
import { readFileSync, statSync,
|
|
14
|
-
import { join, resolve } from 'node:path'
|
|
18
|
+
import { copyFileSync, existsSync, readdirSync, readFileSync, statSync, mkdirSync } from 'node:fs'
|
|
19
|
+
import { dirname, join, resolve } from 'node:path'
|
|
20
|
+
import { fileURLToPath } from 'node:url'
|
|
21
|
+
import { awHome, findLocalConfigRoot } from '../../../../shared/config/home.mjs'
|
|
15
22
|
import { AppError } from '../../../utils/errors'
|
|
16
23
|
|
|
17
24
|
/** 模板变量值(字符串化注入) */
|
|
@@ -24,15 +31,61 @@ interface CacheEntry {
|
|
|
24
31
|
|
|
25
32
|
const cache = new Map<string, CacheEntry>()
|
|
26
33
|
|
|
27
|
-
/** prompts 目录(
|
|
34
|
+
/** 包内资产 prompts 目录(播种源;不存在返回 null) */
|
|
35
|
+
function packagedPromptsDir(): string | null {
|
|
36
|
+
const packageRoot = process.env.AW_PACKAGE_ROOT
|
|
37
|
+
? resolve(process.env.AW_PACKAGE_ROOT)
|
|
38
|
+
: resolve(dirname(fileURLToPath(import.meta.url)), '../../../..')
|
|
39
|
+
const candidate = join(packageRoot, '.AgentWorkShop', 'prompts')
|
|
40
|
+
return existsSync(candidate) ? candidate : null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 首次解析时把包内初始 prompts 播种到 ~/.AgentWorkShop/prompts(只补缺,不覆盖) */
|
|
44
|
+
function seedHomePrompts(homePrompts: string): void {
|
|
45
|
+
const src = packagedPromptsDir()
|
|
46
|
+
if (!src || resolve(src) === resolve(homePrompts)) return
|
|
47
|
+
mkdirSync(homePrompts, { recursive: true })
|
|
48
|
+
let seeded = 0
|
|
49
|
+
for (const f of readdirSync(src)) {
|
|
50
|
+
const dst = join(homePrompts, f)
|
|
51
|
+
if (existsSync(dst)) continue
|
|
52
|
+
try {
|
|
53
|
+
copyFileSync(join(src, f), dst)
|
|
54
|
+
seeded++
|
|
55
|
+
}
|
|
56
|
+
catch { /* 单文件失败不阻断播种 */ }
|
|
57
|
+
}
|
|
58
|
+
if (seeded > 0) console.log(`[prompts] 已向 ${homePrompts} 播种 ${seeded} 个初始提示词文件`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* prompts 目录解析:显式覆盖 > 项目内(存在即用) > ~/.AgentWorkShop/prompts(自动播种)。
|
|
63
|
+
* 失败 fail-fast —— 外置 prompt 为必需,防静默降级为无 prompt 裸跑。
|
|
64
|
+
*/
|
|
28
65
|
export function promptsDir(): string {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
66
|
+
// ① 显式覆盖(兼容既有部署约定)
|
|
67
|
+
if (process.env.AW_PROMPTS_DIR) {
|
|
68
|
+
const base = resolve(process.env.AW_PROMPTS_DIR)
|
|
69
|
+
if (!existsSync(base)) {
|
|
70
|
+
throw new AppError(500, 'PROMPTS_DIR_MISSING', `prompts 目录不存在: ${base}(AW_PROMPTS_DIR 指定)`)
|
|
71
|
+
}
|
|
72
|
+
return base
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ~/.AgentWorkShop/prompts:默认兜底目录,首次解析自动播种初始文件(只补缺,不覆盖用户定制)
|
|
76
|
+
const homePrompts = join(awHome(process.env), 'prompts')
|
|
77
|
+
if (!existsSync(homePrompts)) seedHomePrompts(homePrompts)
|
|
78
|
+
|
|
79
|
+
// ② 项目内 ./.AgentWorkShop/prompts 真实存在 → 项目内优先
|
|
80
|
+
const localConfigRoot = findLocalConfigRoot(process.cwd())
|
|
81
|
+
if (localConfigRoot) {
|
|
82
|
+
const candidate = join(localConfigRoot, 'prompts')
|
|
83
|
+
if (existsSync(candidate)) return candidate
|
|
34
84
|
}
|
|
35
|
-
|
|
85
|
+
|
|
86
|
+
// ③ ~/.AgentWorkShop/prompts
|
|
87
|
+
if (existsSync(homePrompts)) return homePrompts
|
|
88
|
+
throw new AppError(500, 'PROMPTS_DIR_MISSING', `prompts 目录不存在(已尝试自动播种): ${homePrompts};可用 AW_PROMPTS_DIR 指定`)
|
|
36
89
|
}
|
|
37
90
|
|
|
38
91
|
/** 读取 md 原文(mtime 缓存;文件缺失 fail-fast) */
|