chore: scaffold http client app plan implementation

This commit is contained in:
曾志威
2026-06-07 17:52:13 +08:00
commit 9a8f91ec05
26 changed files with 8301 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
# Dependencies
frontend/node_modules/
# Build outputs
frontend/dist/
backend/bin/
# Runtime/cache data
http-client-cache/
http-client-responses/
http-client-events/
http-client-logs/
http-client-diagnostics/
http-client-backups/
# Local databases and logs
*.db
*.db-*
*.log
# OS/editor noise
.DS_Store
Thumbs.db
+23
View File
@@ -0,0 +1,23 @@
package main
import (
"errors"
"log"
"net/http"
"time"
"github.com/local/http-client-app/backend/internal/api"
)
func main() {
server := &http.Server{
Addr: "127.0.0.1:32180",
Handler: api.NewRouter(),
ReadHeaderTimeout: 5 * time.Second,
}
log.Printf("HTTP Client local service listening on http://%s", server.Addr)
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}
+39
View File
@@ -0,0 +1,39 @@
module github.com/local/http-client-app/backend
go 1.26.0
require github.com/gin-gonic/gin v1.11.0
require (
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/tools v0.34.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
)
+88
View File
@@ -0,0 +1,88 @@
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+31
View File
@@ -0,0 +1,31 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/local/http-client-app/backend/internal/model"
)
func NewRouter() http.Handler {
gin.SetMode(gin.ReleaseMode)
router := gin.New()
router.Use(gin.Recovery())
router.GET("/api/health", func(c *gin.Context) {
c.JSON(http.StatusOK, model.SuccessEnvelope(requestID(c), gin.H{
"status": "ok",
}))
})
return router
}
func requestID(c *gin.Context) string {
if requestID := c.GetHeader("X-Request-Id"); requestID != "" {
return requestID
}
return "req_local"
}
+53
View File
@@ -0,0 +1,53 @@
package api_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/local/http-client-app/backend/internal/api"
)
func TestHealthEndpointReturnsEnvelope(t *testing.T) {
router := api.NewRouter()
req := httptest.NewRequest(http.MethodGet, "/api/health", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", rec.Code)
}
var body map[string]any
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatalf("response is not JSON: %v", err)
}
if body["success"] != true {
t.Fatalf("expected success=true, got %#v", body["success"])
}
if body["error"] != nil {
t.Fatalf("expected error=null, got %#v", body["error"])
}
data, ok := body["data"].(map[string]any)
if !ok {
t.Fatalf("expected data object, got %#v", body["data"])
}
if data["status"] != "ok" {
t.Fatalf("expected data.status=ok, got %#v", data["status"])
}
if body["requestId"] == "" {
t.Fatalf("expected requestId to be set")
}
if body["timestamp"] == "" {
t.Fatalf("expected timestamp to be set")
}
}
+28
View File
@@ -0,0 +1,28 @@
package model
import "time"
type ErrorResponse struct {
Code string `json:"code"`
Message string `json:"message"`
Details any `json:"details"`
Location any `json:"location"`
}
type Envelope struct {
Success bool `json:"success"`
Data any `json:"data"`
Error *ErrorResponse `json:"error"`
RequestID string `json:"requestId"`
Timestamp string `json:"timestamp"`
}
func SuccessEnvelope(requestID string, data any) Envelope {
return Envelope{
Success: true,
Data: data,
Error: nil,
RequestID: requestID,
Timestamp: time.Now().UTC().Format(time.RFC3339),
}
}
+94
View File
@@ -0,0 +1,94 @@
openapi: 3.1.0
info:
title: HTTP Client App Internal API
version: 0.1.0
description: Internal REST API contract for the local HTTP Client application.
servers:
- url: http://127.0.0.1:32180
description: Local development server
paths:
/api/health:
get:
operationId: getHealth
summary: Check local service health
tags:
- system
responses:
"200":
description: Service is healthy.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/ApiEnvelope"
- type: object
properties:
data:
$ref: "#/components/schemas/HealthData"
examples:
healthy:
value:
success: true
data:
status: ok
version: 0.1.0
error: null
requestId: req_01HZY7R4S7S0Y73ZJ81EF1Z8Y4
timestamp: "2026-06-06T10:20:30Z"
components:
schemas:
ApiEnvelope:
type: object
required:
- success
- data
- error
- requestId
- timestamp
properties:
success:
type: boolean
description: Whether the request completed successfully.
data:
description: Response payload. Null when the request failed.
error:
oneOf:
- $ref: "#/components/schemas/ApiError"
- type: "null"
requestId:
type: string
minLength: 1
timestamp:
type: string
format: date-time
additionalProperties: false
ApiError:
type: object
required:
- code
- message
properties:
code:
type: string
minLength: 1
message:
type: string
minLength: 1
details:
description: Optional structured error details.
additionalProperties: false
HealthData:
type: object
required:
- status
properties:
status:
type: string
enum:
- ok
version:
type: string
uptimeSeconds:
type: integer
minimum: 0
additionalProperties: false
+41
View File
@@ -0,0 +1,41 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.local/http-client-app/contracts/events.schema.json",
"title": "WebSocket Event Envelope",
"description": "Base envelope for WebSocket events emitted by the local HTTP Client application.",
"type": "object",
"required": [
"type",
"executionId",
"seq",
"timestamp",
"payload"
],
"properties": {
"type": {
"type": "string",
"minLength": 1,
"examples": [
"execution.response.chunk"
]
},
"executionId": {
"type": "string",
"minLength": 1
},
"seq": {
"type": "integer",
"minimum": 0,
"description": "Monotonically increasing sequence number within an execution."
},
"timestamp": {
"type": "string",
"format": "date-time"
},
"payload": {
"type": "object",
"description": "Event-specific payload. Specialized schemas can narrow this object by event type."
}
},
"additionalProperties": false
}
@@ -0,0 +1,63 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.local/http-client-app/contracts/workspace-json-schemas/environments.schema.json",
"title": "HTTP Client Environments",
"type": "object",
"required": [
"schemaVersion",
"environments"
],
"properties": {
"schemaVersion": {
"type": "integer",
"const": 1
},
"defaultEnvironment": {
"type": [
"string",
"null"
]
},
"environments": {
"type": "array",
"items": {
"type": "object",
"required": [
"name",
"variables"
],
"properties": {
"name": {
"type": "string",
"minLength": 1
},
"variables": {
"type": "object",
"additionalProperties": {
"type": [
"string",
"number",
"boolean",
"null"
]
}
}
},
"additionalProperties": false
}
},
"globals": {
"type": "object",
"additionalProperties": {
"type": [
"string",
"number",
"boolean",
"null"
]
},
"default": {}
}
},
"additionalProperties": false
}
@@ -0,0 +1,156 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.local/http-client-app/contracts/workspace-json-schemas/mock-file.schema.json",
"title": "HTTP Client Mock Rule File",
"type": "object",
"required": [
"schemaVersion",
"updatedAt",
"rules"
],
"properties": {
"schemaVersion": {
"type": "integer",
"const": 1
},
"updatedAt": {
"type": "string",
"format": "date-time"
},
"rules": {
"type": "array",
"items": {
"$ref": "#/$defs/mockRule"
}
}
},
"additionalProperties": false,
"$defs": {
"mockRule": {
"type": "object",
"required": [
"id",
"name",
"enabled",
"priority",
"match",
"response"
],
"properties": {
"id": {
"type": "string",
"minLength": 1
},
"name": {
"type": "string",
"minLength": 1
},
"enabled": {
"type": "boolean"
},
"priority": {
"type": "integer"
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
},
"description": {
"type": "string"
},
"match": {
"type": "object",
"required": [
"method",
"path"
],
"properties": {
"method": {
"type": "string",
"minLength": 1
},
"path": {
"type": "string",
"minLength": 1
},
"pathMode": {
"type": "string",
"enum": [
"exact",
"prefix",
"regex"
],
"default": "exact"
},
"headers": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"query": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"additionalProperties": false
},
"response": {
"type": "object",
"required": [
"statusCode"
],
"properties": {
"statusCode": {
"type": "integer",
"minimum": 100,
"maximum": 599
},
"headers": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"bodyType": {
"type": "string",
"enum": [
"json",
"text",
"binary",
"file"
]
},
"body": {
"type": [
"string",
"object",
"array",
"number",
"boolean",
"null"
]
},
"bodyFilePath": {
"type": "string"
},
"delayMs": {
"type": "integer",
"minimum": 0
},
"templateEnabled": {
"type": "boolean",
"default": false
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
}
}
@@ -0,0 +1,113 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.local/http-client-app/contracts/workspace-json-schemas/openapi-sources.schema.json",
"title": "HTTP Client OpenAPI Sources",
"type": "object",
"required": [
"schemaVersion",
"sources"
],
"properties": {
"schemaVersion": {
"type": "integer",
"const": 1
},
"sources": {
"type": "array",
"items": {
"type": "object",
"required": [
"id",
"name",
"sourcePath",
"format"
],
"properties": {
"id": {
"type": "string",
"minLength": 1
},
"name": {
"type": "string",
"minLength": 1
},
"sourcePath": {
"type": "string",
"minLength": 1
},
"format": {
"type": "string",
"enum": [
"json",
"yaml"
]
},
"openapiVersion": {
"type": "string"
},
"title": {
"type": "string"
},
"version": {
"type": "string"
},
"contentHash": {
"type": "string"
},
"lastSyncedAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"targets": {
"type": "object",
"properties": {
"requestFilePath": {
"type": "string"
},
"mockFilePath": {
"type": "string"
}
},
"additionalProperties": false
},
"sync": {
"type": "object",
"properties": {
"strategy": {
"type": "string",
"enum": [
"skip-local-modified",
"overwrite-generated",
"preview-only"
],
"default": "skip-local-modified"
},
"generateRequests": {
"type": "boolean",
"default": true
},
"generateMocks": {
"type": "boolean",
"default": false
},
"enableSchemaValidation": {
"type": "boolean",
"default": false
},
"baseUrlVariable": {
"type": "string",
"default": "baseUrl"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
}
},
"additionalProperties": false
}
@@ -0,0 +1,72 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.local/http-client-app/contracts/workspace-json-schemas/settings.schema.json",
"title": "HTTP Client Workspace Settings",
"type": "object",
"required": [
"schemaVersion"
],
"properties": {
"schemaVersion": {
"type": "integer",
"const": 1
},
"defaultEnvironment": {
"type": [
"string",
"null"
]
},
"history": {
"type": "object",
"properties": {
"retentionMaxCount": {
"type": "integer",
"minimum": 0,
"default": 1000
}
},
"additionalProperties": false
},
"mock": {
"type": "object",
"properties": {
"proxyFallback": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"default": false
},
"upstreamBaseUrl": {
"type": [
"string",
"null"
],
"format": "uri"
}
},
"additionalProperties": false
},
"hitLog": {
"type": "object",
"properties": {
"retentionMaxCount": {
"type": "integer",
"minimum": 0,
"default": 10000
},
"retentionDays": {
"type": "integer",
"minimum": 0,
"default": 7
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>HTTP Client App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+4324
View File
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
{
"name": "http-client-app-frontend",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"test": "vitest run"
},
"dependencies": {
"vue": "^3.5.16"
},
"devDependencies": {
"@types/node": "^22.15.29",
"@vitejs/plugin-vue": "^5.2.4",
"@vue/test-utils": "^2.4.6",
"jsdom": "^25.0.1",
"vite": "^6.3.5",
"vitest": "^2.1.9",
"vue-tsc": "^2.2.10"
}
}
+12
View File
@@ -0,0 +1,12 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import App from './App.vue'
describe('App', () => {
it('renders the application title', () => {
const wrapper = mount(App)
expect(wrapper.get('h1').text()).toBe('HTTP Client App')
})
})
+28
View File
@@ -0,0 +1,28 @@
<template>
<main class="app-shell">
<section class="hero" aria-labelledby="app-title">
<p class="eyebrow">HTTP Client MVP</p>
<h1 id="app-title">HTTP Client App</h1>
<p class="subtitle">用于管理工作区打开 .http 文件并预览请求调试流程的前端骨架</p>
</section>
<section class="status-card" aria-labelledby="backend-health-title">
<div>
<p class="section-label">后端健康状态</p>
<h2 id="backend-health-title">等待接入健康检查</h2>
</div>
<span class="status-pill">占位</span>
</section>
<section class="action-grid" aria-label="工作区入口">
<button type="button" class="action-card">
<span>打开工作区</span>
<small>选择包含请求集合的目录</small>
</button>
<button type="button" class="action-card">
<span>打开 .http 文件</span>
<small>从单个请求文件开始</small>
</button>
</section>
</main>
</template>
+6
View File
@@ -0,0 +1,6 @@
import { createApp } from 'vue'
import App from './App.vue'
import './styles.css'
createApp(App).mount('#app')
+144
View File
@@ -0,0 +1,144 @@
:root {
color: #1f2a37;
background: #f3efe6;
font-family: "Avenir Next", "Segoe UI", sans-serif;
font-synthesis: none;
text-rendering: optimizeLegibility;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
background:
radial-gradient(circle at top left, rgba(235, 176, 104, 0.32), transparent 34rem),
linear-gradient(135deg, #f8f4eb 0%, #e8dcc9 100%);
}
button {
font: inherit;
}
.app-shell {
width: min(960px, calc(100% - 32px));
margin: 0 auto;
padding: 56px 0;
}
.hero {
padding: 48px;
border: 1px solid rgba(71, 51, 34, 0.14);
border-radius: 28px;
background: rgba(255, 252, 246, 0.76);
box-shadow: 0 24px 80px rgba(65, 47, 29, 0.14);
}
.eyebrow,
.section-label {
margin: 0 0 12px;
color: #9b4b24;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
}
h1,
h2,
p {
margin-top: 0;
}
h1 {
margin-bottom: 16px;
color: #263326;
font-size: clamp(2.5rem, 8vw, 5.8rem);
line-height: 0.94;
}
.subtitle {
max-width: 660px;
margin-bottom: 0;
color: #5f6a59;
font-size: 1.1rem;
line-height: 1.7;
}
.status-card,
.action-card {
border: 1px solid rgba(71, 51, 34, 0.12);
border-radius: 22px;
background: rgba(255, 252, 246, 0.7);
}
.status-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
margin: 24px 0;
padding: 24px;
}
.status-card h2 {
margin-bottom: 0;
font-size: 1.25rem;
}
.status-pill {
padding: 8px 14px;
border-radius: 999px;
color: #6d3b19;
background: #f3d9b8;
font-weight: 700;
}
.action-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.action-card {
min-height: 132px;
padding: 24px;
color: #263326;
text-align: left;
cursor: not-allowed;
}
.action-card span {
display: block;
margin-bottom: 12px;
font-size: 1.25rem;
font-weight: 800;
}
.action-card small {
color: #66705f;
font-size: 0.95rem;
}
@media (max-width: 640px) {
.app-shell {
padding: 24px 0;
}
.hero {
padding: 28px;
}
.status-card,
.action-grid {
grid-template-columns: 1fr;
}
.status-card {
align-items: flex-start;
flex-direction: column;
}
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+17
View File
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"]
},
"include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"],
"exclude": ["src/**/*.test.ts", "src/**/*.spec.ts"],
"references": []
}
+13
View File
@@ -0,0 +1,13 @@
import { fileURLToPath, URL } from 'node:url'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
})
+17
View File
@@ -0,0 +1,17 @@
import { fileURLToPath, URL } from 'node:url'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vitest/config'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
test: {
environment: 'jsdom',
globals: true,
},
})
File diff suppressed because it is too large Load Diff
+171
View File
@@ -0,0 +1,171 @@
param(
[string]$DocumentPath = (Join-Path $PSScriptRoot "..\http-client-app-plan.md"),
[string[]]$DisabledKeywords = @(
"<<<<<<<",
">>>>>>>",
"TODO_BLOCKER",
"FIXME_BLOCKER"
)
)
$ErrorActionPreference = "Stop"
$resolvedDocument = Resolve-Path -LiteralPath $DocumentPath
$python = Get-Command python -ErrorAction SilentlyContinue
$usePyLauncher = $false
if (-not $python) {
$python = Get-Command py -ErrorAction SilentlyContinue
$usePyLauncher = $true
}
if (-not $python) {
Write-Error "Python was not found. Install Python 3 or make the 'python'/'py' command available on PATH."
}
$checker = @'
import argparse
import json
import re
import sys
from pathlib import Path
def line_col(text, offset):
line = text.count("\n", 0, offset) + 1
last_newline = text.rfind("\n", 0, offset)
column = offset + 1 if last_newline == -1 else offset - last_newline
return line, column
def check_code_fences(text):
errors = []
fence_pattern = re.compile(r"^[ \t]*```", re.MULTILINE)
matches = list(fence_pattern.finditer(text))
if len(matches) % 2 != 0:
line, _ = line_col(text, matches[-1].start())
errors.append(f"Unbalanced fenced code block near line {line}.")
return errors
def check_json_blocks(text):
errors = []
block_pattern = re.compile(r"^[ \t]*```json[^\n]*\n(.*?)(?:\n^[ \t]*```[ \t]*$)", re.MULTILINE | re.DOTALL)
for index, match in enumerate(block_pattern.finditer(text), start=1):
raw_json = match.group(1)
try:
json.loads(raw_json)
except json.JSONDecodeError as exc:
start_line, _ = line_col(text, match.start(1))
errors.append(
f"Invalid JSON code block #{index} at line {start_line + exc.lineno - 1}, "
f"column {exc.colno}: {exc.msg}."
)
return errors
def is_table_separator(line):
stripped = line.strip()
if "|" not in stripped:
return False
cells = [cell.strip() for cell in stripped.strip("|").split("|")]
return bool(cells) and all(re.fullmatch(r":?-{3,}:?", cell or "") for cell in cells)
def pipe_count(line):
stripped = line.strip()
return stripped.count("|")
def check_tables(text):
errors = []
lines = text.splitlines()
in_fence = False
for index, line in enumerate(lines):
if re.match(r"^[ \t]*```", line):
in_fence = not in_fence
continue
if in_fence or not is_table_separator(line):
continue
table_lines = []
cursor = index - 1
while cursor >= 0 and "|" in lines[cursor]:
table_lines.insert(0, (cursor + 1, lines[cursor]))
cursor -= 1
table_lines.append((index + 1, line))
cursor = index + 1
while cursor < len(lines) and "|" in lines[cursor]:
table_lines.append((cursor + 1, lines[cursor]))
cursor += 1
counts = {pipe_count(table_line) for _, table_line in table_lines}
if len(counts) > 1:
first_line = table_lines[0][0]
errors.append(f"Markdown table pipe count mismatch near line {first_line}.")
return errors
def check_disabled_keywords(text, keywords):
errors = []
for keyword in keywords:
if not keyword:
continue
offset = text.find(keyword)
if offset != -1:
line, column = line_col(text, offset)
errors.append(f"Disabled keyword '{keyword}' found at line {line}, column {column}.")
return errors
def main():
parser = argparse.ArgumentParser(description="Verify Markdown planning document structure.")
parser.add_argument("document")
parser.add_argument("--keyword", action="append", default=[])
args = parser.parse_args()
document = Path(args.document)
text = document.read_text(encoding="utf-8-sig")
errors = []
errors.extend(check_code_fences(text))
errors.extend(check_json_blocks(text))
errors.extend(check_tables(text))
errors.extend(check_disabled_keywords(text, args.keyword))
if errors:
print(f"Documentation verification failed for {document}:")
for error in errors:
print(f" - {error}")
return 1
print(f"Documentation verification passed for {document}.")
return 0
if __name__ == "__main__":
sys.exit(main())
'@
$tempChecker = Join-Path ([System.IO.Path]::GetTempPath()) ("verify-docs-{0}.py" -f ([System.Guid]::NewGuid().ToString("N")))
try {
Set-Content -LiteralPath $tempChecker -Value $checker -Encoding UTF8
$arguments = @()
if ($usePyLauncher) {
$arguments += "-3"
}
$arguments += @($tempChecker, $resolvedDocument.Path)
foreach ($keyword in $DisabledKeywords) {
$arguments += @("--keyword", $keyword)
}
& $python.Source @arguments
exit $LASTEXITCODE
}
finally {
if (Test-Path -LiteralPath $tempChecker) {
Remove-Item -LiteralPath $tempChecker -Force
}
}