draftgo-cli 4.0.1 → 4.0.23

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 (79) hide show
  1. package/README.md +87 -11
  2. package/package.json +9 -4
  3. package/resources/custom-service-sdk/ai.go +520 -0
  4. package/resources/custom-service-sdk/ai_test.go +156 -0
  5. package/resources/custom-service-sdk/auth_test.go +56 -0
  6. package/resources/custom-service-sdk/billing.go +596 -0
  7. package/resources/custom-service-sdk/billing_test.go +150 -0
  8. package/resources/custom-service-sdk/go.mod +3 -0
  9. package/resources/custom-service-sdk/manifest.json +77 -0
  10. package/resources/custom-service-sdk/platform.go +345 -0
  11. package/resources/custom-service-sdk/platform_logger_test.go +24 -0
  12. package/resources/custom-service-sdk/registration_test.go +39 -0
  13. package/resources/custom-service-sdk/resources.go +246 -0
  14. package/resources/custom-service-sdk/resources_billing_test.go +115 -0
  15. package/resources/custom-service-sdk/resources_files_test.go +57 -0
  16. package/resources/custom-service-sdk/resources_scope_test.go +87 -0
  17. package/resources/custom-service-sdk/sdk.go +208 -0
  18. package/resources/skill/SKILL.md +36 -88
  19. package/resources/skill/init/SKILL.md +4 -4
  20. package/resources/skill/manifest.json +5 -1
  21. package/resources/skill/references/aihub.md +25 -2
  22. package/resources/skill/references/app-api.md +56 -6
  23. package/resources/skill/references/architecture.md +2 -2
  24. package/resources/skill/references/chat-sdk.md +4 -2
  25. package/resources/skill/references/checkout.md +17 -3
  26. package/resources/skill/references/custom-services.md +112 -47
  27. package/resources/skill/references/data.md +19 -4
  28. package/resources/skill/references/delivery.md +33 -0
  29. package/resources/skill/references/diagnostics.md +51 -0
  30. package/resources/skill/references/frontend.md +34 -46
  31. package/resources/skill/references/mcp.md +33 -5
  32. package/resources/skill/references/methods.md +189 -0
  33. package/resources/skill/references/modules.md +37 -9
  34. package/resources/skill/references/runtime.md +23 -1
  35. package/src/cli.js +24 -0
  36. package/src/commandRegistry.js +9 -1
  37. package/src/commands/api.js +21 -10
  38. package/src/commands/apiKey.js +34 -0
  39. package/src/commands/capabilities.js +93 -0
  40. package/src/commands/checkout.js +1 -1
  41. package/src/commands/commit.js +1 -1
  42. package/src/commands/components.js +550 -0
  43. package/src/commands/conflict.js +1 -1
  44. package/src/commands/connect.js +18 -8
  45. package/src/commands/customService.js +20 -4
  46. package/src/commands/dataRange.js +33 -0
  47. package/src/commands/delete.js +12 -1
  48. package/src/commands/diff.js +18 -2
  49. package/src/commands/grant.js +29 -0
  50. package/src/commands/group.js +38 -0
  51. package/src/commands/help.js +64 -20
  52. package/src/commands/init.js +3 -3
  53. package/src/commands/map.js +145 -17
  54. package/src/commands/mcp.js +2 -2
  55. package/src/commands/reconcile.js +1 -1
  56. package/src/commands/role.js +32 -0
  57. package/src/commands/space.js +41 -0
  58. package/src/commands/status.js +110 -7
  59. package/src/commands/update.js +23 -11
  60. package/src/commands/verify.js +75 -0
  61. package/src/commands/worklog.js +6 -2
  62. package/src/consoleEncoding.js +34 -0
  63. package/src/contractCompatibility.js +57 -0
  64. package/src/customServices.js +138 -18
  65. package/src/diffReport.js +106 -0
  66. package/src/index.js +2 -0
  67. package/src/localRuntime/compose.js +14 -17
  68. package/src/localRuntime/index.js +22 -23
  69. package/src/localRuntime/services.js +27 -36
  70. package/src/mcp/client.js +11 -2
  71. package/src/mcp/protocol.js +22 -2
  72. package/src/mcp/tools.js +14 -1
  73. package/src/platforms.js +9 -0
  74. package/src/projectConfig.js +6 -4
  75. package/src/releaseInstall.js +105 -0
  76. package/src/updateCheck.js +48 -28
  77. package/src/worklog.js +2 -1
  78. package/src/worktree/backend.js +1 -1
  79. package/src/worktree/index.js +7 -2
@@ -0,0 +1,150 @@
1
+ package sdk
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "testing"
7
+ )
8
+
9
+ func TestBillingSDKCarriesStableOperationsAndIdempotency(t *testing.T) {
10
+ client := &recordingClient{}
11
+ draftgo := NewContext(context.Background(), nil, nil, nil, client)
12
+
13
+ _, err := draftgo.Billing.CreatePaymentOrder(draftgo.Context(), PaymentOrderRequest{
14
+ BusinessOrderID: "biz-1", AmountMinor: 100, Provider: "wechat", IdempotencyKey: "pay-1",
15
+ })
16
+ if err != nil || client.op != "billing.payment.create" {
17
+ t.Fatalf("create operation=%q err=%v", client.op, err)
18
+ }
19
+ if client.args["idempotency_key"] != "pay-1" || client.args["business_order_id"] != "biz-1" {
20
+ t.Fatalf("payment contract lost stable fields: %#v", client.args)
21
+ }
22
+ _, err = draftgo.Billing.RefundPaymentOrder(draftgo.Context(), RefundRequest{PaymentOrderID: "42", AmountMinor: 50, IdempotencyKey: "refund-42"})
23
+ if err != nil || client.op != "billing.payment.refund" || client.args["payment_order_id"] != "42" || client.args["idempotency_key"] != "refund-42" {
24
+ t.Fatalf("refund operation=%q args=%#v err=%v", client.op, client.args, err)
25
+ }
26
+ _, err = draftgo.Billing.ListPaymentRefunds(draftgo.Context(), "42")
27
+ if err != nil || client.op != "billing.payment.refunds.list" || client.args["payment_order_id"] != "42" {
28
+ t.Fatalf("refund list operation=%q err=%v", client.op, err)
29
+ }
30
+
31
+ _, err = draftgo.Billing.StartAIBilling(draftgo.Context(), AIBillingStartRequest{
32
+ IdempotencyKey: "ai-1", RequestID: "request-1", Model: "model-a", EstimatedMinor: 20,
33
+ })
34
+ if err != nil || client.op != "billing.ai.session.start" {
35
+ t.Fatalf("AI start operation=%q err=%v", client.op, err)
36
+ }
37
+ if client.args["idempotency_key"] != "ai-1" || client.args["request_id"] != "request-1" {
38
+ t.Fatalf("AI billing contract lost idempotency/request fields: %#v", client.args)
39
+ }
40
+
41
+ _, err = draftgo.Billing.SettleAIBilling(draftgo.Context(), AISettlementRequest{
42
+ SessionID: "session-1", Usage: map[string]any{"requests": 1}, IdempotencyKey: "settle-1",
43
+ })
44
+ if err != nil || client.op != "billing.ai.session.settle" {
45
+ t.Fatalf("AI settle operation=%q err=%v", client.op, err)
46
+ }
47
+ if client.args["session_id"] != "session-1" || client.args["idempotency_key"] != "settle-1" {
48
+ t.Fatalf("AI settlement contract lost stable fields: %#v", client.args)
49
+ }
50
+ }
51
+
52
+ // Keep this test tied to JSON-compatible SDK payloads: custom services cross
53
+ // an RPC boundary and must not depend on Go-only values.
54
+ func TestBillingSDKRequestsAreJSONCompatible(t *testing.T) {
55
+ payload := PaymentOrderRequest{BusinessOrderID: "biz-1", AmountMinor: 1, IdempotencyKey: "k"}
56
+ if _, err := json.Marshal(payload); err != nil {
57
+ t.Fatalf("payment request is not JSON compatible: %v", err)
58
+ }
59
+ settlement := AISettlementRequest{SessionID: "s-1", Usage: map[string]any{"input_tokens": int64(2)}, IdempotencyKey: "k"}
60
+ if _, err := json.Marshal(settlement); err != nil {
61
+ t.Fatalf("AI settlement is not JSON compatible: %v", err)
62
+ }
63
+ }
64
+
65
+ func TestPaymentOrderParserPreservesFulfillmentMetadata(t *testing.T) {
66
+ order := paymentOrder(map[string]any{
67
+ "id": "42", "business_order_id": "biz-42", "ownership_type": "space", "workspace_id": float64(2), "space_id": float64(8), "owner_user_id": float64(7),
68
+ "payer_user_id": float64(9), "created_by": float64(7), "provider": "wechat", "provider_trade_no": "trade-42", "status": "paid",
69
+ "amount_minor": float64(1288), "product_ref": "product:pro", "fulfillment_ref": "subscription:3",
70
+ "metadata": map[string]any{"sku": "pro"}, "created_at": "2026-08-14T10:00:00Z", "paid_at": "2026-08-14T10:01:00Z",
71
+ })
72
+ if order.Ownership.ScopeType != ScopeSpace || order.Ownership.WorkspaceID != 2 || order.Ownership.SpaceID != 8 || order.Ownership.OwnerUserID != 7 || order.PayerUserID != 9 || order.ProductRef != "product:pro" || order.FulfillmentRef != "subscription:3" || order.Metadata["sku"] != "pro" || order.ProviderTradeNo != "trade-42" {
73
+ t.Fatalf("payment order=%#v", order)
74
+ }
75
+ }
76
+
77
+ func TestBillingSDKPriceVersionOperations(t *testing.T) {
78
+ client := &recordingClient{}
79
+ draftgo := NewContext(context.Background(), nil, nil, nil, client)
80
+ created, err := draftgo.Admin.Billing.CreateAIPriceVersion(draftgo.Context(), AIPriceVersionRequest{Model: "model-a", Version: "v1", BillingMode: "per_call", BillingEnabled: true, RequestPriceMinor: 8})
81
+ if err != nil || client.op != "billing.ai.pricing.create" {
82
+ t.Fatalf("create = %+v, operation=%q, err=%v", created, client.op, err)
83
+ }
84
+ if client.args["__draftgo_admin"] != true || client.args["request_price_minor"] != float64(8) {
85
+ t.Fatalf("create args = %#v", client.args)
86
+ }
87
+ }
88
+
89
+ func TestBillingSDKAICostsAndProviderCost(t *testing.T) {
90
+ client := &recordingClient{}
91
+ draftgo := NewContext(context.Background(), nil, nil, nil, client)
92
+ _, err := draftgo.Admin.Billing.GetAICosts(draftgo.Context(), map[string]any{"model_key": "model-a"})
93
+ if err != nil || client.op != "billing.ai.costs.list" || client.args["__draftgo_admin"] != true {
94
+ t.Fatalf("cost operation=%q args=%#v err=%v", client.op, client.args, err)
95
+ }
96
+ _, err = draftgo.Admin.Billing.CreateAIPriceVersion(draftgo.Context(), AIPriceVersionRequest{Model: "model-a", Version: "v1", ProviderCostMode: "per_call", ProviderRequestCostMinor: 3})
97
+ if err != nil || client.args["provider_cost_mode"] != "per_call" || client.args["provider_request_cost_minor"] != float64(3) {
98
+ t.Fatalf("provider cost args=%#v err=%v", client.args, err)
99
+ }
100
+ }
101
+
102
+ func TestBillingSDKPlanAndSubscriptionOperations(t *testing.T) {
103
+ client := &recordingClient{}
104
+ draftgo := NewContext(context.Background(), nil, nil, nil, client)
105
+
106
+ _, err := draftgo.Admin.Billing.CreatePlan(draftgo.Context(), PlanRequest{
107
+ Code: "pro", Name: "Pro", PriceMinor: 9900, BillingCycle: "monthly",
108
+ Entitlements: map[string]any{"ai.requests": 1000},
109
+ })
110
+ if err != nil || client.op != "billing.plan.create" || client.args["__draftgo_admin"] != true {
111
+ t.Fatalf("create plan operation=%q args=%#v err=%v", client.op, client.args, err)
112
+ }
113
+ if client.args["code"] != "pro" || client.args["price_minor"] != float64(9900) {
114
+ t.Fatalf("create plan args=%#v", client.args)
115
+ }
116
+
117
+ _, err = draftgo.Billing.ListPlans(draftgo.Context(), PlanQuery{Status: "active"})
118
+ if err != nil || client.op != "billing.plan.list" {
119
+ t.Fatalf("list plans operation=%q err=%v", client.op, err)
120
+ }
121
+ query, _ := client.args["query"].(map[string]any)
122
+ if query["status"] != "active" {
123
+ t.Fatalf("list plan query=%#v", query)
124
+ }
125
+
126
+ _, err = draftgo.Admin.Billing.CreateSubscription(draftgo.Context(), SubscriptionRequest{
127
+ PlanID: 3, Subject: BillingSubject{Type: "space", ID: "8"}, SourceType: "payment_order", SourceID: "88", IdempotencyKey: "subscription-88",
128
+ })
129
+ if err != nil || client.op != "billing.subscription.create" || client.args["idempotency_key"] != "subscription-88" {
130
+ t.Fatalf("create subscription operation=%q args=%#v err=%v", client.op, client.args, err)
131
+ }
132
+
133
+ _, err = draftgo.Billing.ListSubscriptions(draftgo.Context(), SubscriptionQuery{Status: "active"})
134
+ if err != nil || client.op != "billing.subscription.list" {
135
+ t.Fatalf("list subscriptions operation=%q err=%v", client.op, err)
136
+ }
137
+
138
+ _, err = draftgo.Admin.Billing.ChangeSubscriptionPlan(draftgo.Context(), 9, SubscriptionChangeRequest{PlanID: 4})
139
+ if err != nil || client.op != "billing.subscription.change_plan" || client.args["id"] != float64(9) {
140
+ t.Fatalf("change subscription operation=%q args=%#v err=%v", client.op, client.args, err)
141
+ }
142
+ _, err = draftgo.Admin.Billing.CancelSubscription(draftgo.Context(), 9)
143
+ if err != nil || client.op != "billing.subscription.cancel" {
144
+ t.Fatalf("cancel subscription operation=%q err=%v", client.op, err)
145
+ }
146
+ _, err = draftgo.Admin.Billing.FulfillSubscriptionPayment(draftgo.Context(), SubscriptionPaymentFulfillmentRequest{PaymentOrderID: "88", PlanID: 3})
147
+ if err != nil || client.op != "billing.subscription.fulfill_payment" || client.args["payment_order_id"] != "88" || client.args["plan_id"] != float64(3) {
148
+ t.Fatalf("fulfill subscription operation=%q args=%#v err=%v", client.op, client.args, err)
149
+ }
150
+ }
@@ -0,0 +1,3 @@
1
+ module draftgo/sdk
2
+
3
+ go 1.26.0
@@ -0,0 +1,77 @@
1
+ {
2
+ "schema_version": 1,
3
+ "module": "draftgo/sdk",
4
+ "fingerprint": "94d0b5064c3d5c3d742ddf2856ed6e37196ff81bd16696aa88cd8267c3f57889",
5
+ "files": [
6
+ {
7
+ "path": "ai.go",
8
+ "sha256": "0d45e652f96769c1f2d371a1c25b03903d09356c600fbd0e6d67190466259248",
9
+ "bytes": 26627
10
+ },
11
+ {
12
+ "path": "ai_test.go",
13
+ "sha256": "d5a906ea3d9d0451f116d2acac54bfb83669e5e471d1d5ada7c0e1fdabef13b6",
14
+ "bytes": 6531
15
+ },
16
+ {
17
+ "path": "auth_test.go",
18
+ "sha256": "f0a02db739c8c5fa9d2856c59bd7ec823194cae50c1445f7b1340ce3dc0a616f",
19
+ "bytes": 1604
20
+ },
21
+ {
22
+ "path": "billing.go",
23
+ "sha256": "9ce87c80885aa8b8996710622ec0f14cc7a74d3ca78e5ed4168b6c135994d583",
24
+ "bytes": 30976
25
+ },
26
+ {
27
+ "path": "billing_test.go",
28
+ "sha256": "d6632b31d385fe3658dd1fbc74e31a87cc69b23bf97422d8339c5a46bf2d1aa2",
29
+ "bytes": 8009
30
+ },
31
+ {
32
+ "path": "go.mod",
33
+ "sha256": "b89c04833ac41ea06dfbc8c60ebdc1c2e3e628243cf5adbb36b7320acfe2a3ef",
34
+ "bytes": 30
35
+ },
36
+ {
37
+ "path": "platform.go",
38
+ "sha256": "deb0ab4a91dbb9a1f07974982ffb218d89f719b3691cf5e9a955abcc86a6d2ba",
39
+ "bytes": 11923
40
+ },
41
+ {
42
+ "path": "platform_logger_test.go",
43
+ "sha256": "e8eb02e668c62bb317506f0197cfab3e520fd53534f3bb1f99f55155fa24218d",
44
+ "bytes": 540
45
+ },
46
+ {
47
+ "path": "registration_test.go",
48
+ "sha256": "e9fd4ab60a2b6b28c997019ce76b224f7cec52b934d080ebfd94298448acd4f0",
49
+ "bytes": 1031
50
+ },
51
+ {
52
+ "path": "resources.go",
53
+ "sha256": "e633aa1431beddd25b2c999370a770f77c3de7a2d7123687eca75c9ed0d3838f",
54
+ "bytes": 9708
55
+ },
56
+ {
57
+ "path": "resources_billing_test.go",
58
+ "sha256": "3fadeb8b7af0fad43f34e2479e4331a18f022bcdabdcb3b4ba921768c6b6a7ec",
59
+ "bytes": 4859
60
+ },
61
+ {
62
+ "path": "resources_files_test.go",
63
+ "sha256": "f77a3b6e421e20b528a1bfca5e2b164b63b3599bf3566b8f3998fdf72c4c0aa6",
64
+ "bytes": 2551
65
+ },
66
+ {
67
+ "path": "resources_scope_test.go",
68
+ "sha256": "bba3243f348e9418888f37ee2d5b487fc26a31e4a646e75bef46d5d807938cee",
69
+ "bytes": 3592
70
+ },
71
+ {
72
+ "path": "sdk.go",
73
+ "sha256": "b67db621642c389a2000e52b1640bacccf23a6d94f1fdcc1989e6a4244821924",
74
+ "bytes": 8224
75
+ }
76
+ ]
77
+ }
@@ -0,0 +1,345 @@
1
+ package sdk
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "errors"
7
+ "fmt"
8
+ "net/http"
9
+ "strings"
10
+ "sync"
11
+ "time"
12
+ )
13
+
14
+ // Client is implemented by the isolated runner protocol. It is deliberately
15
+ // generic so new platform capabilities do not require exposing host handles.
16
+ type Client interface {
17
+ Call(context.Context, string, any) (any, error)
18
+ }
19
+
20
+ type Database interface {
21
+ Create(string, map[string]any, ...int64) (map[string]any, error)
22
+ CreateMany(string, []map[string]any) ([]map[string]any, error)
23
+ Get(string, int64) (map[string]any, error)
24
+ Update(string, int64, map[string]any) (map[string]any, error)
25
+ UpdateMany(string, []map[string]any) ([]map[string]any, error)
26
+ Delete(string, int64) (bool, error)
27
+ Query(string, QueryOptions) (QueryResult, error)
28
+ }
29
+
30
+ type QueryOptions struct {
31
+ Filters map[string]any
32
+ Page int
33
+ PageSize int
34
+ OrderBy string
35
+ Order string
36
+ }
37
+
38
+ type QueryResult struct {
39
+ Items []map[string]any `json:"items"`
40
+ Total int `json:"total"`
41
+ Page int `json:"page"`
42
+ PageSize int `json:"page_size"`
43
+ }
44
+
45
+ type Users interface {
46
+ Get(int64) (map[string]any, error)
47
+ List(QueryOptions) (QueryResult, error)
48
+ Update(int64, map[string]any) (map[string]any, error)
49
+ }
50
+
51
+ type Auth interface {
52
+ RequireLogin() error
53
+ RequireAdmin() error
54
+ RequireRole(string) error
55
+ CurrentUser() map[string]any
56
+ }
57
+
58
+ type Notifier interface {
59
+ Send(int64, string, string, string) error
60
+ }
61
+
62
+ type Cache interface {
63
+ Get(string) (any, error)
64
+ Set(string, any, time.Duration) error
65
+ Delete(string) (bool, error)
66
+ }
67
+
68
+ type Config interface {
69
+ Get(string, any) (any, error)
70
+ }
71
+
72
+ type HTTPClient interface {
73
+ Get(context.Context, string, http.Header, time.Duration) (HTTPResponse, error)
74
+ Post(context.Context, string, any, http.Header, time.Duration) (HTTPResponse, error)
75
+ Put(context.Context, string, any, http.Header, time.Duration) (HTTPResponse, error)
76
+ Patch(context.Context, string, any, http.Header, time.Duration) (HTTPResponse, error)
77
+ Delete(context.Context, string, http.Header, time.Duration) (HTTPResponse, error)
78
+ }
79
+
80
+ type HTTPResponse struct {
81
+ StatusCode int `json:"status_code"`
82
+ Headers http.Header `json:"headers,omitempty"`
83
+ Data any `json:"data,omitempty"`
84
+ }
85
+
86
+ type LogEntry struct {
87
+ Level string `json:"level"`
88
+ Message string `json:"message"`
89
+ Timestamp time.Time `json:"timestamp"`
90
+ }
91
+
92
+ // Logger is local to one invocation and becomes part of the audited result.
93
+ type Logger struct {
94
+ mu sync.Mutex
95
+ entries []LogEntry
96
+ truncated bool
97
+ }
98
+
99
+ func (log *Logger) append(level, message string) {
100
+ if log == nil {
101
+ return
102
+ }
103
+ log.mu.Lock()
104
+ defer log.mu.Unlock()
105
+ if len(log.entries) < 500 {
106
+ if characters := []rune(message); len(characters) > 4096 {
107
+ message = string(characters[:4096])
108
+ log.truncated = true
109
+ }
110
+ log.entries = append(log.entries, LogEntry{Level: level, Message: message, Timestamp: time.Now()})
111
+ } else {
112
+ log.truncated = true
113
+ }
114
+ }
115
+
116
+ func (log *Logger) Truncated() bool {
117
+ if log == nil {
118
+ return false
119
+ }
120
+ log.mu.Lock()
121
+ defer log.mu.Unlock()
122
+ return log.truncated
123
+ }
124
+ func (log *Logger) Debug(message string) { log.append("debug", message) }
125
+ func (log *Logger) Info(message string) { log.append("info", message) }
126
+ func (log *Logger) Warn(message string) { log.append("warn", message) }
127
+ func (log *Logger) Error(message string) { log.append("error", message) }
128
+ func (log *Logger) Entries() []LogEntry {
129
+ if log == nil {
130
+ return nil
131
+ }
132
+ log.mu.Lock()
133
+ defer log.mu.Unlock()
134
+ out := make([]LogEntry, len(log.entries))
135
+ copy(out, log.entries)
136
+ return out
137
+ }
138
+
139
+ type platformClient struct {
140
+ client Client
141
+ context context.Context
142
+ admin bool
143
+ }
144
+ type dbClient struct{ platformClient }
145
+ type usersClient struct{ platformClient }
146
+ type notifierClient struct{ platformClient }
147
+ type cacheClient struct{ platformClient }
148
+ type configClient struct{ platformClient }
149
+ type httpClient struct{ platformClient }
150
+ type aiHubClient struct{ platformClient }
151
+ type knowledgeClient struct{ platformClient }
152
+ type memoryClient struct{ platformClient }
153
+
154
+ func (p platformClient) call(draftgo context.Context, operation string, args any) (any, error) {
155
+ if p.client == nil {
156
+ return nil, errors.New("DraftGo platform RPC is unavailable")
157
+ }
158
+ if draftgo == nil {
159
+ draftgo = p.context
160
+ }
161
+ if p.admin {
162
+ values, ok := args.(map[string]any)
163
+ if !ok {
164
+ raw, err := json.Marshal(args)
165
+ if err == nil {
166
+ ok = json.Unmarshal(raw, &values) == nil
167
+ }
168
+ }
169
+ if ok {
170
+ cloned := make(map[string]any, len(values)+1)
171
+ for key, value := range values {
172
+ cloned[key] = value
173
+ }
174
+ cloned["__draftgo_admin"] = true
175
+ args = cloned
176
+ }
177
+ }
178
+ return p.client.Call(draftgo, operation, args)
179
+ }
180
+ func mapResult(value any) map[string]any { result, _ := value.(map[string]any); return result }
181
+ func mapSlice(value any) []map[string]any {
182
+ raw, _ := value.([]any)
183
+ result := make([]map[string]any, 0, len(raw))
184
+ for _, item := range raw {
185
+ if object, ok := item.(map[string]any); ok {
186
+ result = append(result, object)
187
+ }
188
+ }
189
+ return result
190
+ }
191
+ func (p dbClient) Create(kind string, data map[string]any, userID ...int64) (map[string]any, error) {
192
+ args := map[string]any{"db_type": kind, "data": data}
193
+ if len(userID) > 0 {
194
+ args["owner_user_id"] = userID[0]
195
+ }
196
+ value, err := p.call(p.context, "db.create", args)
197
+ return mapResult(value), err
198
+ }
199
+ func (p dbClient) CreateMany(kind string, items []map[string]any) ([]map[string]any, error) {
200
+ value, err := p.call(p.context, "db.create_many", map[string]any{"db_type": kind, "items": items})
201
+ return mapSlice(value), err
202
+ }
203
+ func (p dbClient) Get(kind string, id int64) (map[string]any, error) {
204
+ value, err := p.call(p.context, "db.get", map[string]any{"db_type": kind, "record_id": id})
205
+ return mapResult(value), err
206
+ }
207
+ func (p dbClient) Update(kind string, id int64, data map[string]any) (map[string]any, error) {
208
+ value, err := p.call(p.context, "db.update", map[string]any{"db_type": kind, "record_id": id, "data": data})
209
+ return mapResult(value), err
210
+ }
211
+ func (p dbClient) UpdateMany(kind string, items []map[string]any) ([]map[string]any, error) {
212
+ value, err := p.call(p.context, "db.update_many", map[string]any{"db_type": kind, "items": items})
213
+ return mapSlice(value), err
214
+ }
215
+ func (p dbClient) Delete(kind string, id int64) (bool, error) {
216
+ value, err := p.call(p.context, "db.delete", map[string]any{"db_type": kind, "record_id": id})
217
+ ok, _ := value.(bool)
218
+ return ok, err
219
+ }
220
+ func (p dbClient) Query(kind string, options QueryOptions) (QueryResult, error) {
221
+ value, err := p.call(p.context, "db.query", map[string]any{"db_type": kind, "filters": options.Filters, "page": nullablePositive(options.Page), "page_size": nullablePositive(options.PageSize), "order_by": options.OrderBy, "order": options.Order})
222
+ return queryResult(value), err
223
+ }
224
+ func (p usersClient) GetUser(id int64) (map[string]any, error) {
225
+ value, err := p.call(p.context, "users.get", map[string]any{"user_id": id})
226
+ return mapResult(value), err
227
+ }
228
+ func (p usersClient) List(options QueryOptions) (QueryResult, error) {
229
+ value, err := p.call(p.context, "users.list", map[string]any{"filters": options.Filters, "page": nullablePositive(options.Page), "page_size": nullablePositive(options.PageSize)})
230
+ return queryResult(value), err
231
+ }
232
+ func (p usersClient) UpdateUser(id int64, data map[string]any) (map[string]any, error) {
233
+ value, err := p.call(p.context, "users.update", map[string]any{"user_id": id, "data": data})
234
+ return mapResult(value), err
235
+ }
236
+ func (p usersClient) Get(id int64) (map[string]any, error) { return p.GetUser(id) }
237
+ func (p usersClient) Update(id int64, data map[string]any) (map[string]any, error) {
238
+ return p.UpdateUser(id, data)
239
+ }
240
+ func (p notifierClient) Send(userID int64, title, content, noticeType string) error {
241
+ _, err := p.call(p.context, "notify.send", map[string]any{"user_id": userID, "title": title, "content": content, "notice_type": noticeType})
242
+ return err
243
+ }
244
+ func (p cacheClient) GetCache(key string) (any, error) {
245
+ return p.call(p.context, "cache.get", map[string]any{"key": key})
246
+ }
247
+ func (p cacheClient) SetCache(key string, value any, ttl time.Duration) error {
248
+ _, err := p.call(p.context, "cache.set", map[string]any{"key": key, "value": value, "ttl": int(ttl.Seconds())})
249
+ return err
250
+ }
251
+ func (p cacheClient) DeleteCache(key string) (bool, error) {
252
+ value, err := p.call(p.context, "cache.delete", map[string]any{"key": key})
253
+ ok, _ := value.(bool)
254
+ return ok, err
255
+ }
256
+ func (p configClient) GetConfig(key string, defaultValue any) (any, error) {
257
+ return p.call(p.context, "config.get", map[string]any{"key": key, "default": defaultValue})
258
+ }
259
+ func (p cacheClient) Get(key string) (any, error) { return p.GetCache(key) }
260
+ func (p cacheClient) Set(key string, value any, ttl time.Duration) error {
261
+ return p.SetCache(key, value, ttl)
262
+ }
263
+ func (p cacheClient) Delete(key string) (bool, error) { return p.DeleteCache(key) }
264
+ func (p configClient) Get(key string, defaultValue any) (any, error) {
265
+ return p.GetConfig(key, defaultValue)
266
+ }
267
+ func (p httpClient) request(draftgo context.Context, method, url string, body any, headers http.Header, timeout time.Duration) (HTTPResponse, error) {
268
+ value, err := p.call(draftgo, "http.request", map[string]any{"method": method, "url": url, "body": body, "headers": headers, "timeout_ms": int(timeout.Milliseconds())})
269
+ response := HTTPResponse{}
270
+ if raw, ok := value.(map[string]any); ok {
271
+ if status, ok := raw["status_code"].(float64); ok {
272
+ response.StatusCode = int(status)
273
+ }
274
+ response.Data = raw["data"]
275
+ if rawHeaders, ok := raw["headers"].(map[string]any); ok {
276
+ response.Headers = http.Header{}
277
+ for key, value := range rawHeaders {
278
+ response.Headers.Set(key, fmt.Sprint(value))
279
+ }
280
+ }
281
+ }
282
+ return response, err
283
+ }
284
+ func (p httpClient) Get(draftgo context.Context, url string, headers http.Header, timeout time.Duration) (HTTPResponse, error) {
285
+ return p.request(draftgo, http.MethodGet, url, nil, headers, timeout)
286
+ }
287
+ func (p httpClient) Post(draftgo context.Context, url string, body any, headers http.Header, timeout time.Duration) (HTTPResponse, error) {
288
+ return p.request(draftgo, http.MethodPost, url, body, headers, timeout)
289
+ }
290
+ func (p httpClient) Put(draftgo context.Context, url string, body any, headers http.Header, timeout time.Duration) (HTTPResponse, error) {
291
+ return p.request(draftgo, http.MethodPut, url, body, headers, timeout)
292
+ }
293
+ func (p httpClient) Patch(draftgo context.Context, url string, body any, headers http.Header, timeout time.Duration) (HTTPResponse, error) {
294
+ return p.request(draftgo, http.MethodPatch, url, body, headers, timeout)
295
+ }
296
+ func (p httpClient) Delete(draftgo context.Context, url string, headers http.Header, timeout time.Duration) (HTTPResponse, error) {
297
+ return p.request(draftgo, http.MethodDelete, url, nil, headers, timeout)
298
+ }
299
+ func nullablePositive(value int) any {
300
+ if value <= 0 {
301
+ return nil
302
+ }
303
+ return value
304
+ }
305
+ func queryResult(value any) QueryResult {
306
+ object := mapResult(value)
307
+ result := QueryResult{Total: asInt(object["total"]), Page: asInt(object["page"]), PageSize: asInt(object["page_size"])}
308
+ result.Items = mapSlice(object["items"])
309
+ return result
310
+ }
311
+ func asInt(value any) int {
312
+ switch typed := value.(type) {
313
+ case int:
314
+ return typed
315
+ case int64:
316
+ return int(typed)
317
+ case float64:
318
+ return int(typed)
319
+ default:
320
+ return 0
321
+ }
322
+ }
323
+
324
+ type authClient struct {
325
+ platformClient
326
+ user map[string]any
327
+ }
328
+
329
+ func (a authClient) CurrentUser() map[string]any { return a.user }
330
+ func (a authClient) RequireLogin() error {
331
+ _, err := a.call(a.context, "auth.require_login", map[string]any{})
332
+ return err
333
+ }
334
+ func (a authClient) RequireAdmin() error {
335
+ _, err := a.call(a.context, "auth.require_admin", map[string]any{})
336
+ return err
337
+ }
338
+ func (a authClient) RequireRole(role string) error {
339
+ role = strings.TrimSpace(role)
340
+ if role == "" {
341
+ return errors.New("require_role failed: role is required")
342
+ }
343
+ _, err := a.call(a.context, "auth.require_role", map[string]any{"role": role})
344
+ return err
345
+ }
@@ -0,0 +1,24 @@
1
+ package sdk
2
+
3
+ import (
4
+ "strings"
5
+ "testing"
6
+ )
7
+
8
+ func TestLoggerReportsEntryAndMessageTruncation(t *testing.T) {
9
+ logger := &Logger{}
10
+ logger.Info(strings.Repeat("界", 4097))
11
+ for index := 1; index <= 500; index++ {
12
+ logger.Info("entry")
13
+ }
14
+ entries := logger.Entries()
15
+ if len(entries) != 500 {
16
+ t.Fatalf("entries = %d, want 500", len(entries))
17
+ }
18
+ if got := len([]rune(entries[0].Message)); got != 4096 {
19
+ t.Fatalf("message characters = %d, want 4096", got)
20
+ }
21
+ if !logger.Truncated() {
22
+ t.Fatal("Truncated() = false, want true")
23
+ }
24
+ }
@@ -0,0 +1,39 @@
1
+ package sdk
2
+
3
+ import (
4
+ "strings"
5
+ "testing"
6
+ )
7
+
8
+ func TestRouteCanonicalizesRuntimePath(t *testing.T) {
9
+ app := NewApp()
10
+ app.Route(" get ", "//health//", func(*Context) (any, error) { return nil, nil })
11
+
12
+ registrations := app.Registrations()
13
+ if len(registrations) != 1 || registrations[0].Method != "GET" || registrations[0].Path != "/health" {
14
+ t.Fatalf("canonical route registration = %#v", registrations)
15
+ }
16
+ }
17
+
18
+ func TestRouteRejectsCanonicalDuplicate(t *testing.T) {
19
+ app := NewApp()
20
+ app.Route("GET", "/health", func(*Context) (any, error) { return nil, nil })
21
+
22
+ deferred := recoverValue(func() {
23
+ app.Route(" get ", "/health/", func(*Context) (any, error) { return nil, nil })
24
+ })
25
+ if message := strings.TrimSpace(asPanicString(deferred)); message != "duplicate route GET /health" {
26
+ t.Fatalf("duplicate route panic = %q", message)
27
+ }
28
+ }
29
+
30
+ func recoverValue(run func()) (value any) {
31
+ defer func() { value = recover() }()
32
+ run()
33
+ return nil
34
+ }
35
+
36
+ func asPanicString(value any) string {
37
+ text, _ := value.(string)
38
+ return text
39
+ }