@ai-content-space/loopx 0.1.2 → 0.1.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 (69) hide show
  1. package/README.md +422 -57
  2. package/README.zh-CN.md +485 -0
  3. package/assets/logo.svg +89 -0
  4. package/package.json +5 -1
  5. package/plugins/loopx/.codex-plugin/plugin.json +1 -1
  6. package/plugins/loopx/scripts/plugin-install.test.mjs +14 -0
  7. package/plugins/loopx/skills/archive/SKILL.md +49 -0
  8. package/plugins/loopx/skills/build/SKILL.md +111 -9
  9. package/plugins/loopx/skills/clarify/SKILL.md +129 -8
  10. package/plugins/loopx/skills/debug/SKILL.md +296 -0
  11. package/plugins/loopx/skills/debug/condition-based-waiting.md +115 -0
  12. package/plugins/loopx/skills/debug/defense-in-depth.md +122 -0
  13. package/plugins/loopx/skills/debug/find-polluter.sh +63 -0
  14. package/plugins/loopx/skills/debug/root-cause-tracing.md +169 -0
  15. package/plugins/loopx/skills/go-style/SKILL.md +71 -0
  16. package/plugins/loopx/skills/kratos/SKILL.md +74 -0
  17. package/plugins/loopx/skills/kratos/references/advanced-features.md +314 -0
  18. package/plugins/loopx/skills/kratos/references/architecture.md +488 -0
  19. package/plugins/loopx/skills/kratos/references/configuration.md +399 -0
  20. package/plugins/loopx/skills/kratos/references/http-customization.md +512 -0
  21. package/plugins/loopx/skills/kratos/references/middleware-logging.md +400 -0
  22. package/plugins/loopx/skills/kratos/references/proto-api-design.md +432 -0
  23. package/plugins/loopx/skills/kratos/references/security-auth.md +411 -0
  24. package/plugins/loopx/skills/kratos/references/troubleshooting.md +385 -0
  25. package/plugins/loopx/skills/plan/SKILL.md +24 -3
  26. package/plugins/loopx/skills/review/SKILL.md +98 -1
  27. package/plugins/loopx/skills/tdd/SKILL.md +371 -0
  28. package/plugins/loopx/skills/tdd/testing-anti-patterns.md +299 -0
  29. package/plugins/loopx/skills/verify/SKILL.md +139 -0
  30. package/scripts/codex-stop-hook.mjs +71 -0
  31. package/scripts/codex-workflow-hook.mjs +248 -0
  32. package/skills/archive/SKILL.md +49 -0
  33. package/skills/build/SKILL.md +111 -9
  34. package/skills/clarify/SKILL.md +129 -8
  35. package/skills/debug/SKILL.md +296 -0
  36. package/skills/debug/condition-based-waiting.md +115 -0
  37. package/skills/debug/defense-in-depth.md +122 -0
  38. package/skills/debug/find-polluter.sh +63 -0
  39. package/skills/debug/root-cause-tracing.md +169 -0
  40. package/skills/go-style/SKILL.md +71 -0
  41. package/skills/kratos/SKILL.md +74 -0
  42. package/skills/kratos/references/advanced-features.md +314 -0
  43. package/skills/kratos/references/architecture.md +488 -0
  44. package/skills/kratos/references/configuration.md +399 -0
  45. package/skills/kratos/references/http-customization.md +512 -0
  46. package/skills/kratos/references/middleware-logging.md +400 -0
  47. package/skills/kratos/references/proto-api-design.md +432 -0
  48. package/skills/kratos/references/security-auth.md +411 -0
  49. package/skills/kratos/references/troubleshooting.md +385 -0
  50. package/skills/plan/SKILL.md +20 -3
  51. package/skills/review/SKILL.md +98 -1
  52. package/skills/tdd/SKILL.md +371 -0
  53. package/skills/tdd/testing-anti-patterns.md +299 -0
  54. package/skills/verify/SKILL.md +139 -0
  55. package/src/build-runtime.mjs +311 -26
  56. package/src/build-stop-gate.mjs +94 -0
  57. package/src/cli.mjs +57 -5
  58. package/src/codex-exec-runtime.mjs +105 -5
  59. package/src/context-manifest.mjs +172 -0
  60. package/src/html-views.mjs +316 -0
  61. package/src/install-discovery.mjs +352 -5
  62. package/src/next-skill.mjs +57 -5
  63. package/src/plan-runtime.mjs +102 -122
  64. package/src/review-runtime.mjs +558 -0
  65. package/src/runtime-maintenance.mjs +429 -14
  66. package/src/template-governance.mjs +223 -0
  67. package/src/workflow.mjs +2341 -120
  68. package/src/workspace-context.mjs +166 -0
  69. package/src/workspace-memory.mjs +69 -0
@@ -0,0 +1,512 @@
1
+ # HTTP Customization
2
+
3
+ Guide for customizing HTTP responses, file handling, WebSocket, and CORS.
4
+
5
+ ## When to Use
6
+
7
+ - Customizing response format
8
+ - Handling file upload/download
9
+ - WebSocket integration
10
+ - CORS configuration
11
+ - Static file serving
12
+
13
+ ---
14
+
15
+ ## ResponseEncoder
16
+
17
+ Customize how successful responses are serialized:
18
+
19
+ ### Standard Pattern
20
+
21
+ ```go
22
+ import (
23
+ "github.com/go-kratos/kratos/v2/encoding"
24
+ "github.com/go-kratos/kratos/v2/transport/http"
25
+ "google.golang.org/protobuf/proto"
26
+ "google.golang.org/protobuf/types/known/anypb"
27
+ )
28
+
29
+ func CustomResponseEncoder() http.ServerOption {
30
+ return http.ResponseEncoder(func(w http.ResponseWriter, r *http.Request, i interface{}) error {
31
+ // Handle redirect first
32
+ if rd, ok := i.(http.Redirector); ok {
33
+ url, code := rd.Redirect()
34
+ http.Redirect(w, r, url, code)
35
+ return nil
36
+ }
37
+
38
+ // Wrap in BaseResponse
39
+ reply := &v1.BaseResponse{Code: 0}
40
+ if m, ok := i.(proto.Message); ok {
41
+ payload, err := anypb.New(m)
42
+ if err != nil {
43
+ return err
44
+ }
45
+ reply.Data = payload
46
+ }
47
+
48
+ codec := encoding.GetCodec("json")
49
+ data, err := codec.Marshal(reply)
50
+ if err != nil {
51
+ return err
52
+ }
53
+ w.Header().Set("Content-Type", "application/json")
54
+ if _, err := w.Write(data); err != nil {
55
+ return err
56
+ }
57
+ return nil
58
+ })
59
+ }
60
+ ```
61
+
62
+ ### Proto Definition for BaseResponse
63
+
64
+ ```protobuf
65
+ import "google/protobuf/any.proto";
66
+
67
+ message BaseResponse {
68
+ int32 code = 1 [json_name = "code"];
69
+ google.protobuf.Any data = 2 [json_name = "data"];
70
+ string message = 3 [json_name = "message"];
71
+ }
72
+ ```
73
+
74
+ ### Registration
75
+
76
+ ```go
77
+ srv := http.NewServer(
78
+ http.Address(":8000"),
79
+ CustomResponseEncoder(),
80
+ )
81
+ ```
82
+
83
+ ---
84
+
85
+ ## ErrorEncoder
86
+
87
+ Customize how errors are serialized:
88
+
89
+ ```go
90
+ func CustomErrorEncoder() http.ServerOption {
91
+ return http.ErrorEncoder(func(w http.ResponseWriter, r *http.Request, err error) {
92
+ // Convert Kratos error to custom format
93
+ se := errors.FromError(err)
94
+
95
+ reply := &v1.BaseResponse{
96
+ Code: se.Code,
97
+ Message: se.Message,
98
+ }
99
+
100
+ codec := encoding.GetCodec("json")
101
+ data, err := codec.Marshal(reply)
102
+ if err != nil {
103
+ w.WriteHeader(http.StatusInternalServerError)
104
+ return
105
+ }
106
+
107
+ w.Header().Set("Content-Type", "application/json")
108
+ w.WriteHeader(se.StatusCode)
109
+ if _, err := w.Write(data); err != nil {
110
+ log.Errorf("write error response failed: %v", err)
111
+ }
112
+ })
113
+ }
114
+ ```
115
+
116
+ ---
117
+
118
+ ## Zero-Value Field Handling
119
+
120
+ Protobuf by default omits zero-value fields. Solutions:
121
+
122
+ ### Option 1: EmitUnpopulated
123
+
124
+ ```go
125
+ // Custom json codec with EmitUnpopulated
126
+ import "google.golang.org/protobuf/encoding/protojson"
127
+
128
+ var MarshalOptions = protojson.MarshalOptions{
129
+ EmitUnpopulated: true, // Include zero values
130
+ UseEnumNumbers: true, // Enums as numbers, not strings
131
+ }
132
+
133
+ type codec struct{}
134
+
135
+ func (codec) Marshal(v interface{}) ([]byte, error) {
136
+ if m, ok := v.(proto.Message); ok {
137
+ return MarshalOptions.Marshal(m)
138
+ }
139
+ return json.Marshal(v)
140
+ }
141
+
142
+ func init() {
143
+ encoding.RegisterCodec(codec{})
144
+ }
145
+ ```
146
+
147
+ ### Option 2: anypb.New Wrapper
148
+
149
+ ```go
150
+ // In ResponseEncoder, wrap with anypb.New
151
+ payload, err := anypb.New(m)
152
+ if err != nil {
153
+ return err
154
+ }
155
+ reply.Data = payload
156
+ ```
157
+
158
+ **Note:** This adds `@type` field to response.
159
+
160
+ ### Option 3: Remove omitempty from pb.go
161
+
162
+ ```bash
163
+ # Makefile
164
+ ifeq ($(GOHOSTOS), darwin)
165
+ find ./api -name '*.pb.go' -exec sed -i "" -e "s/,omitempty/,optional/g" {} \;
166
+ else
167
+ find ./api -name '*.pb.go' -exec sed -i -e "s/,omitempty/,optional/g" {} \;
168
+ endif
169
+ ```
170
+
171
+ ---
172
+
173
+ ## File Upload
174
+
175
+ Proto doesn't support file upload. Use custom route:
176
+
177
+ ### Handler Pattern
178
+
179
+ ```go
180
+ import (
181
+ "bytes"
182
+ "github.com/gorilla/schema"
183
+ "io"
184
+ "net/http"
185
+ )
186
+
187
+ func UploadHandlerWithMiddleware[T comparable](ctx http.Context, fileFormKey string) (
188
+ chain middleware.Middleware,
189
+ request T,
190
+ reader io.Reader,
191
+ filename string,
192
+ err error,
193
+ ) {
194
+ if fileFormKey == "" {
195
+ fileFormKey = "file"
196
+ }
197
+
198
+ // Read file
199
+ file, fileHeader, err := ctx.Request().FormFile(fileFormKey)
200
+ defer file.Close()
201
+ if err != nil {
202
+ return nil, request, nil, "", err
203
+ }
204
+
205
+ // Buffer file content
206
+ buf := new(bytes.Buffer)
207
+ if _, err := io.Copy(buf, file); err != nil {
208
+ return nil, request, nil, fileHeader.Filename, err
209
+ }
210
+
211
+ // Parse form parameters
212
+ if err := ctx.Request().ParseForm(); err != nil {
213
+ return nil, request, nil, "", err
214
+ }
215
+
216
+ // Decode form to struct
217
+ var decoder = schema.NewDecoder()
218
+ t := new(T)
219
+ if err := decoder.Decode(t, ctx.Request().Form); err == nil {
220
+ request = *t
221
+ }
222
+
223
+ h := ctx.Middleware
224
+ return h, request, bytes.NewReader(buf.Bytes()), fileHeader.Filename, nil
225
+ }
226
+ ```
227
+
228
+ ### Service Implementation
229
+
230
+ ```go
231
+ func (s *UploadService) RegisterUploadServiceHttpServer(svr *http.Server) {
232
+ route := svr.Route("/")
233
+ route.POST("/v1/upload", s.uploadFile)
234
+ }
235
+
236
+ func (s *UploadService) uploadFile(ctx http.Context) error {
237
+ http.SetOperation(ctx, "/upload.v1.UploadService/Upload")
238
+
239
+ h, opt, reader, filename, err := UploadHandlerWithMiddleware[biz.UploadOption](ctx, "file")
240
+ if err != nil {
241
+ return v1.ErrorInvalidUploadRequest("invalid request: %v", err)
242
+ }
243
+
244
+ handler := s.uc.UploadFile(filename, reader, opt)
245
+ resp, err := h(ctx, opt)
246
+ if err != nil {
247
+ return err
248
+ }
249
+
250
+ return ctx.JSON(200, resp)
251
+ }
252
+ ```
253
+
254
+ ---
255
+
256
+ ## File Download / Redirect
257
+
258
+ ### Redirector Interface
259
+
260
+ For redirects, implement `http.Redirector`:
261
+
262
+ ```protobuf
263
+ message LuckySearchResponse {
264
+ string redirect_to = 1 [(buf.validate.field).string.uri = true];
265
+ int32 status_code = 2;
266
+ }
267
+ ```
268
+
269
+ ```go
270
+ // api/helloworld/v1/lucky_search_redirect_impl.go
271
+ package v1
272
+
273
+ import "github.com/go-kratos/kratos/v2/transport/http"
274
+
275
+ var _ http.Redirector = (*LuckySearchResponse)(nil)
276
+
277
+ func (s *LuckySearchResponse) Redirect() (string, int) {
278
+ return s.RedirectTo, int(s.StatusCode)
279
+ }
280
+ ```
281
+
282
+ In ResponseEncoder, handle Redirector first:
283
+
284
+ ```go
285
+ if rd, ok := i.(http.Redirector); ok {
286
+ url, code := rd.Redirect()
287
+ http.Redirect(w, r, url, code)
288
+ return nil
289
+ }
290
+ ```
291
+
292
+ ### File Download
293
+
294
+ ```go
295
+ // In ResponseEncoder
296
+ if asset, ok := i.(*attachment.Attachment); ok {
297
+ w.Header().Set("Content-Disposition", asset.FileName)
298
+ w.Header().Set("Content-Length", strconv.FormatInt(asset.ContentLength, 10))
299
+ w.Header().Set("Content-Type", "application/octet-stream")
300
+ w.Write(asset.Payload)
301
+ return nil
302
+ }
303
+ ```
304
+
305
+ Proto for attachment:
306
+ ```protobuf
307
+ message Attachment {
308
+ string file_name = 1;
309
+ int64 content_length = 2;
310
+ bytes payload = 3;
311
+ }
312
+ ```
313
+
314
+ ---
315
+
316
+ ## WebSocket
317
+
318
+ ### Server Setup
319
+
320
+ ```go
321
+ import (
322
+ "github.com/go-kratos/kratos/v2"
323
+ "github.com/go-kratos/kratos/v2/transport/http"
324
+ "github.com/gorilla/mux"
325
+ "github.com/gorilla/websocket"
326
+ )
327
+
328
+ var upgrader = websocket.Upgrader{
329
+ CheckOrigin: func(r *http.Request) bool {
330
+ return true // Allow all origins (configure for production)
331
+ },
332
+ }
333
+
334
+ func main() {
335
+ router := mux.NewRouter()
336
+ router.HandleFunc("/ws", WsHandler)
337
+
338
+ httpSrv := http.NewServer(http.Address(":8000"))
339
+ httpSrv.HandlePrefix("/", router)
340
+
341
+ app := kratos.New(
342
+ kratos.Name("ws"),
343
+ kratos.Server(httpSrv),
344
+ )
345
+ app.Run()
346
+ }
347
+
348
+ func WsHandler(w http.ResponseWriter, r *http.Request) {
349
+ conn, err := upgrader.Upgrade(w, r, nil)
350
+ if err != nil {
351
+ log.Print("upgrade:", err)
352
+ return
353
+ }
354
+ defer conn.Close()
355
+
356
+ for {
357
+ mt, message, err := conn.ReadMessage()
358
+ if err != nil {
359
+ log.Println("read:", err)
360
+ break
361
+ }
362
+ log.Printf("recv: %s", message)
363
+ err = conn.WriteMessage(mt, message)
364
+ if err != nil {
365
+ log.Println("write:", err)
366
+ break
367
+ }
368
+ }
369
+ }
370
+ ```
371
+
372
+ ### From Service Layer
373
+
374
+ ```go
375
+ func (s *MyService) HandleWebSocket(ctx context.Context, req *v1.WsRequest) error {
376
+ if httpCtx, ok := ctx.(http.Context); ok {
377
+ return s.uc.HandleWebSocket(req.Id, httpCtx)
378
+ }
379
+ return errors.New("not http context")
380
+ }
381
+
382
+ // UseCase
383
+ func (uc *MyUseCase) HandleWebSocket(id string, httpCtx http.Context) error {
384
+ conn, err := upgrader.Upgrade(httpCtx.Response(), httpCtx.Request(), nil)
385
+ if err != nil {
386
+ return err
387
+ }
388
+ go handleWsMessage(ctx, id, conn) // Pass context for cancellation
389
+ return nil
390
+ }
391
+
392
+ func handleWsMessage(ctx context.Context, id string, conn *websocket.Conn) {
393
+ defer conn.Close()
394
+ for {
395
+ select {
396
+ case <-ctx.Done():
397
+ return // Graceful shutdown
398
+ default:
399
+ mt, message, err := conn.ReadMessage()
400
+ if err != nil {
401
+ return // Connection closed
402
+ }
403
+ conn.WriteMessage(mt, message)
404
+ }
405
+ }
406
+ }
407
+ ```
408
+
409
+ ---
410
+
411
+ ## CORS Configuration
412
+
413
+ ### Using gorilla/handlers
414
+
415
+ ```go
416
+ import "github.com/gorilla/handlers"
417
+
418
+ srv := http.NewServer(
419
+ http.Address(":8000"),
420
+ http.Filter(handlers.CORS(
421
+ handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization"}),
422
+ handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}),
423
+ handlers.AllowedOrigins([]string{"*"}),
424
+ )),
425
+ )
426
+ ```
427
+
428
+ ### Using rs/cors
429
+
430
+ ```go
431
+ import "github.com/rs/cors"
432
+
433
+ func CorsHandler() func(http.Handler) http.Handler {
434
+ c := cors.New(cors.Options{
435
+ AllowedOrigins: []string{"https://example.com"},
436
+ AllowCredentials: true,
437
+ AllowedHeaders: []string{"Content-Type", "Authorization"},
438
+ AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
439
+ })
440
+ return c.Handler
441
+ }
442
+
443
+ srv := http.NewServer(
444
+ http.Address(":8000"),
445
+ http.Filter(CorsHandler()),
446
+ )
447
+ ```
448
+
449
+ ---
450
+
451
+ ## Static File Serving
452
+
453
+ ### Using embed.FS
454
+
455
+ ```go
456
+ import (
457
+ "embed"
458
+ "net/http"
459
+ "github.com/gorilla/mux"
460
+ )
461
+
462
+ //go:embed assets/*
463
+ var f embed.FS
464
+
465
+ func main() {
466
+ router := mux.NewRouter()
467
+ router.PathPrefix("/assets").Handler(http.FileServer(http.FS(f)))
468
+
469
+ httpSrv := http.NewServer(http.Address(":8000"))
470
+ httpSrv.HandlePrefix("/", router)
471
+
472
+ app := kratos.New(
473
+ kratos.Name("static"),
474
+ kratos.Server(httpSrv),
475
+ )
476
+ app.Run()
477
+ }
478
+ ```
479
+
480
+ ---
481
+
482
+ ## TLS Configuration
483
+
484
+ ### Manual TLS
485
+
486
+ ```go
487
+ import "crypto/tls"
488
+
489
+ func LoadTLSConfig(certFile, keyFile string) (*tls.Config, error) {
490
+ cer, err := tls.LoadX509KeyPair(certFile, keyFile)
491
+ if err != nil {
492
+ return nil, err
493
+ }
494
+ return &tls.Config{Certificates: []tls.Certificate{cer}}, nil
495
+ }
496
+
497
+ srv := http.NewServer(
498
+ http.Address(":443"),
499
+ http.TLSConfig(LoadTLSConfig("cert.pem", "key.pem")),
500
+ )
501
+ ```
502
+
503
+ ### Auto TLS (Let's Encrypt)
504
+
505
+ ```go
506
+ import "github.com/go-kratos/kratos/v2/transport/http/auto"
507
+
508
+ // Requires port 443
509
+ srv := http.NewServer(
510
+ http.Address(":443"),
511
+ auto.TLSConfig("example.com"),
512
+ )