llzai commited on
Commit
9853396
·
verified ·
1 Parent(s): bf86ba9

Upload 1793 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .air.toml +47 -0
  2. .dockerignore +19 -0
  3. .gitattributes +10 -0
  4. .golangci.yml +393 -0
  5. .goreleaser.yml +89 -0
  6. .mockery.yml +17 -0
  7. .pre-commit-config.yaml +22 -0
  8. AGENTS.md +3 -0
  9. CHANGELOG.md +20 -0
  10. CLAUDE.md +428 -0
  11. LICENSE +210 -0
  12. Makefile +123 -0
  13. cmd/axonhub/main.go +281 -0
  14. conf/conf.go +217 -0
  15. config.example.yml +142 -0
  16. deploy/axonhub.service +14 -0
  17. deploy/helm/.helmignore +23 -0
  18. deploy/helm/Chart.yaml +12 -0
  19. deploy/helm/README.md +210 -0
  20. deploy/helm/install.bat +38 -0
  21. deploy/helm/install.sh +38 -0
  22. deploy/helm/templates/NOTES.txt +43 -0
  23. deploy/helm/templates/_helpers.tpl +76 -0
  24. deploy/helm/templates/data-pvc.yaml +18 -0
  25. deploy/helm/templates/deployment.yaml +82 -0
  26. deploy/helm/templates/hpa.yaml +33 -0
  27. deploy/helm/templates/ingress.yaml +62 -0
  28. deploy/helm/templates/postgresql-service.yaml +19 -0
  29. deploy/helm/templates/postgresql-statefulset.yaml +79 -0
  30. deploy/helm/templates/service.yaml +17 -0
  31. deploy/helm/templates/serviceaccount.yaml +12 -0
  32. deploy/helm/templates/tests/test-connection.yaml +16 -0
  33. deploy/helm/values-production.yaml +171 -0
  34. deploy/helm/values.yaml +165 -0
  35. deploy/install.bat +15 -0
  36. deploy/install.ps1 +217 -0
  37. deploy/install.sh +550 -0
  38. deploy/nginx.conf +238 -0
  39. deploy/start.bat +14 -0
  40. deploy/start.ps1 +155 -0
  41. deploy/start.sh +200 -0
  42. deploy/stop.bat +14 -0
  43. deploy/stop.ps1 +163 -0
  44. deploy/stop.sh +226 -0
  45. docker-compose.yml +172 -0
  46. docs/axonhub-architecture-light.svg +156 -0
  47. docs/axonhub-architecture.svg +141 -0
  48. docs/en/api-reference/anthropic-api.md +203 -0
  49. docs/en/api-reference/embedding-api.md +176 -0
  50. docs/en/api-reference/gemini-api.md +214 -0
.air.toml ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Config file for [Air](https://github.com/cosmtrek/air) in TOML format
2
+
3
+ # Working directory
4
+ # . or absolute path, please note that the directories following must be under root.
5
+ root = "."
6
+ tmp_dir = "tmp"
7
+
8
+ [build]
9
+ # Just plain old shell command. You could use `make` as well.
10
+ cmd = "go build -o ./tmp/axonhub cmd/axonhub/main.go"
11
+ # Binary file yields from `cmd`.
12
+ bin = "./tmp/axonhub"
13
+ # Customize binary.
14
+ full_bin = "APP_USER=air ./tmp/axonhub"
15
+ # Watch these filename extensions.
16
+ include_ext = ["go","yml"]
17
+ # Ignore these filename extensions or directories.
18
+ exclude_dir = ["assets", "tmp", "vendor", "frontend", "integration_test", "website", "scripts", "openspec", "llm/tools","tools" ,"examples"]
19
+ # Watch these directories if you specified.
20
+ include_dir = []
21
+ # Exclude files.
22
+ exclude_file = []
23
+ # This log file places in your tmp_dir.
24
+ log = "air.log"
25
+ # It's not necessary to trigger build each time file changes if it's too frequent.
26
+ delay = 10000 # ms
27
+ # Stop running old binary when build errors occur.
28
+ stop_on_error = true
29
+ # Send Interrupt signal before killing process (windows does not support this feature)
30
+ send_interrupt = false
31
+ # Delay after sending Interrupt signal
32
+ kill_delay = 1000 # ms
33
+
34
+ [log]
35
+ # Show log time
36
+ time = false
37
+
38
+ [color]
39
+ # Customize each part's color. If no color found, use the raw app log.
40
+ main = "magenta"
41
+ watcher = "cyan"
42
+ build = "yellow"
43
+ runner = "green"
44
+
45
+ [misc]
46
+ # Delete tmp directory on exit
47
+ clean_on_exit = true
.dockerignore ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .git
2
+ .gitignore
3
+ .dockerignore
4
+ node_modules
5
+ frontend/node_modules
6
+ frontend/dist
7
+ .axonhub
8
+ axonhub
9
+ axonhub.db
10
+ .DS_Store
11
+ *.log
12
+ coverage
13
+ dist
14
+ build
15
+ .vscode
16
+ .idea
17
+ .env
18
+ .env.local
19
+ .env.*.local
.gitattributes CHANGED
@@ -33,3 +33,13 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ docs/screenshots/axonhub-channels.png filter=lfs diff=lfs merge=lfs -text
37
+ docs/screenshots/axonhub-dashboard.png filter=lfs diff=lfs merge=lfs -text
38
+ docs/screenshots/axonhub-model-price.png filter=lfs diff=lfs merge=lfs -text
39
+ docs/screenshots/axonhub-models.png filter=lfs diff=lfs merge=lfs -text
40
+ docs/screenshots/axonhub-profiles.png filter=lfs diff=lfs merge=lfs -text
41
+ docs/screenshots/axonhub-requests.png filter=lfs diff=lfs merge=lfs -text
42
+ docs/screenshots/axonhub-system.png filter=lfs diff=lfs merge=lfs -text
43
+ docs/screenshots/axonhub-trace.png filter=lfs diff=lfs merge=lfs -text
44
+ docs/screenshots/axonhub-usage-logs.png filter=lfs diff=lfs merge=lfs -text
45
+ docs/screenshots/axonhub-users.png filter=lfs diff=lfs merge=lfs -text
.golangci.yml ADDED
@@ -0,0 +1,393 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This file contains all available configuration options
2
+ # with their default values.
3
+
4
+ version: "2"
5
+ # options for analysis running
6
+ run:
7
+ # default concurrency is a available CPU number
8
+ concurrency: 4
9
+
10
+ # timeout for analysis, e.g. 30s, 5m, default is 1m
11
+ timeout: 10m
12
+
13
+ # exit code when at least one issue was found, default is 1
14
+ issues-exit-code: 1
15
+
16
+ # include test files or not, default is true
17
+ tests: true
18
+
19
+ # list of build tags, all linters use it. Default is empty list.
20
+ build-tags: []
21
+
22
+
23
+ linters:
24
+ default: all
25
+ disable:
26
+ - prealloc
27
+ - errchkjson
28
+ - contextcheck
29
+ - rowserrcheck
30
+ - sqlclosecheck
31
+ - wastedassign
32
+ - forbidigo
33
+ - gocritic
34
+ - revive
35
+ - nonamedreturns
36
+ - wrapcheck
37
+ - varnamelen
38
+ - paralleltest
39
+ - nlreturn
40
+ - testpackage
41
+ - gochecknoglobals
42
+ - wsl
43
+ - ireturn
44
+ - godox
45
+ - cyclop
46
+ - whitespace
47
+ - nolintlint
48
+ - unused
49
+ - depguard
50
+ - lll
51
+ - exhaustruct
52
+ - gochecknoinits
53
+ - dupl
54
+ - tagalign
55
+ - funlen
56
+ - gomoddirectives
57
+ - nestif
58
+ - testableexamples
59
+ - gocyclo
60
+ - gosmopolitan
61
+ - perfsprint
62
+ - inamedparam
63
+ - interfacebloat
64
+ - unparam
65
+ - mnd
66
+ - recvcheck
67
+ - tagliatelle
68
+ - goconst
69
+ - gocognit
70
+ - err113
71
+ - funcorder
72
+ - noinlineerr
73
+ - nilnil
74
+ - thelper
75
+ - dogsled
76
+ - unqueryvet
77
+ - predeclared
78
+
79
+
80
+ # all available settings of specific linters
81
+ settings:
82
+ dogsled:
83
+ # checks assignments with too many blank identifiers; default is 2
84
+ max-blank-identifiers: 2
85
+ dupl:
86
+ # tokens count to trigger issue, 150 by default
87
+ threshold: 100
88
+ errcheck:
89
+ # report about not checking of errors in type assertions: `a := b.(MyStruct)`;
90
+ # default is false: such cases aren't reported by default.
91
+ check-type-assertions: false
92
+
93
+ # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
94
+ # default is false: such cases aren't reported by default.
95
+ check-blank: false
96
+
97
+ funlen:
98
+ lines: 60
99
+ statements: 40
100
+ gocognit:
101
+ # minimal code complexity to report, 30 by default (but we recommend 10-20)
102
+ min-complexity: 20
103
+ goconst:
104
+ # minimal length of string constant, 3 by default
105
+ min-len: 3
106
+ # minimal occurrences count to trigger, 3 by default
107
+ min-occurrences: 3
108
+ gocritic:
109
+ # Which checks should be enabled; can't be combined with 'disabled-checks';
110
+ # See https://go-critic.github.io/overview#checks-overview
111
+ # To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run`
112
+ # By default list of stable checks is used.
113
+ enabled-checks: []
114
+
115
+ # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks.
116
+ # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags".
117
+ enabled-tags:
118
+ - performance
119
+
120
+ settings: # settings passed to gocritic
121
+ captLocal: # must be valid enabled check name
122
+ paramsOnly: true
123
+ rangeValCopy:
124
+ sizeThreshold: 32
125
+ gocyclo:
126
+ # minimal code complexity to report, 30 by default (but we recommend 10-20)
127
+ min-complexity: 10
128
+ godox:
129
+ # report any comments starting with keywords, this is useful for TODO or FIXME comments that
130
+ # might be left in the code accidentally and should be resolved before merging
131
+ keywords: [] # default keywords are TODO, BUG, and FIXME, these can be overwritten by this setting
132
+ govet:
133
+ # settings per analyzer
134
+ settings:
135
+ printf: # analyzer name, run `go tool vet help` to see all analyzers
136
+ funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer
137
+ - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof
138
+ - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf
139
+ - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf
140
+ - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf
141
+
142
+ # enable or disable analyzers by name
143
+ enable:
144
+ - atomicalign
145
+ disable:
146
+ - shadow
147
+ disable-all: false
148
+
149
+ lll:
150
+ # max line length, lines longer will be reported. Default is 120.
151
+ # '\t' is counted as 1 character by default, and can be changed with the tab-width option
152
+ line-length: 180
153
+ # tab width in spaces. Default to 1.
154
+ tab-width: 1
155
+ misspell:
156
+ # Correct spellings using locale preferences for US or UK.
157
+ # Default is to use a neutral variety of English.
158
+ # Setting locale to US will correct the British spelling of 'colour' to 'color'.
159
+ locale: US
160
+ nakedret:
161
+ # make an issue if func has more lines of code than this setting and it has naked returns; default is 30
162
+ max-func-lines: 30
163
+ prealloc:
164
+ # XXX: we don't recommend using this linter before doing performance profiling.
165
+ # For most programs usage of prealloc will be a premature optimization.
166
+
167
+ # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them.
168
+ # True by default.
169
+ simple: true
170
+ range-loops: true # Report preallocation suggestions on range loops, true by default
171
+ for-loops: false # Report preallocation suggestions on for loops, false by default
172
+ unparam:
173
+ # Inspect exported functions, default is false. Set to true if no external program/library imports your code.
174
+ # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
175
+ # if it's called for subdir of a project it can't find external interfaces. All text editor integrations
176
+ # with golangci-lint call it on a directory with the changed file.
177
+ check-exported: false
178
+ whitespace:
179
+ multi-if: false # Enforces newlines (or comments) after every multi-line if statement
180
+ multi-func: false # Enforces newlines (or comments) after every multi-line function signature
181
+ wsl:
182
+ # If true append is only allowed to be cuddled if appending value is
183
+ # matching variables, fields or types on line above. Default is true.
184
+ strict-append: true
185
+ # Allow calls and assignments to be cuddled as long as the lines have any
186
+ # matching variables, fields or types. Default is true.
187
+ allow-assign-and-call: true
188
+ # Allow multiline assignments to be cuddled. Default is true.
189
+ allow-multiline-assign: true
190
+ # Allow declarations (var) to be cuddled.
191
+ allow-cuddle-declarations: true
192
+ # Allow trailing comments in ending of blocks
193
+ allow-trailing-comment: false
194
+ # Force newlines in end of case at this limit (0 = never).
195
+ force-case-trailing-whitespace: 0
196
+
197
+
198
+ exclusions:
199
+ # Exclude all linters from running on ent files.
200
+ paths:
201
+ - internal/ent
202
+ # Excluding configuration per-path, per-linter, per-text and per-source
203
+ rules:
204
+ # Exclude some linters from running on tools files.
205
+ - path: tools/
206
+ linters:
207
+ - gosec
208
+ - errcheck
209
+ # Exclude some linters from running on tests files.
210
+ - path: _test\.go$
211
+ linters:
212
+ - noctx
213
+ - sqlclosecheck
214
+ - gocyclo
215
+ - errcheck
216
+ - dupl
217
+ - gosec
218
+ - unused
219
+ - deadcode
220
+ - exhaustruct
221
+ - funlen
222
+ - maintidx
223
+ - cyclop
224
+ - gocognit
225
+ - unparam
226
+ - containedctx
227
+ - goconst
228
+ - ineffassign
229
+ - staticcheck
230
+ - bodyclose
231
+ - forcetypeassert
232
+ - predeclared
233
+ - testifylint
234
+ - musttag
235
+ - dupword
236
+ - intrange
237
+ - govet
238
+ - errname
239
+ - noinlineerr
240
+ - nilnil
241
+ - exhaustive
242
+
243
+ - path: _mock\.go$
244
+ linters:
245
+ - gofmt
246
+
247
+ # Exclude some staticcheck messages
248
+ - linters:
249
+ - staticcheck
250
+ text: "SA9003:"
251
+
252
+ - linters:
253
+ - staticcheck
254
+ text: "SA1019:"
255
+
256
+ - linters:
257
+ - staticcheck
258
+ text: "SA4009:"
259
+
260
+ - linters:
261
+ - staticcheck
262
+ text: "ST1001:"
263
+
264
+ - linters:
265
+ - staticcheck
266
+ text: "QF1008:"
267
+
268
+ - linters:
269
+ - staticcheck
270
+ text: "ST1005:"
271
+
272
+ - linters:
273
+ - stylecheck
274
+ text: "ST1001:"
275
+
276
+ - linters:
277
+ - stylecheck
278
+ text: "ST1003:"
279
+
280
+ - linters:
281
+ - stylecheck
282
+ text: "ST1006:"
283
+
284
+ - linters:
285
+ - stylecheck
286
+ text: "ST1016:"
287
+
288
+ # Exclude lll issues for long lines with go:generate
289
+ - linters:
290
+ - lll
291
+ source: "^//go:generate "
292
+
293
+ formatters:
294
+ # Enable specific formatter.
295
+ # Default: [] (uses standard Go formatting)
296
+ enable:
297
+ - gci
298
+ - gofmt
299
+ - gofumpt
300
+ - goimports
301
+ # - golines
302
+ settings:
303
+ gci:
304
+ # Section configuration to compare against.
305
+ # Section names are case-insensitive and may contain parameters in ().
306
+ # The default order of sections is `standard > default > custom > blank > dot > alias > localmodule`.
307
+ # If `custom-order` is `true`, it follows the order of `sections` option.
308
+ # Default: ["standard", "default"]
309
+ sections:
310
+ - standard # Standard section: captures all standard packages.
311
+ - default # Default section: contains all imports that could not be matched to another section type.
312
+ - blank # Blank section: contains all blank imports. This section is not present unless explicitly enabled.
313
+ - dot # Dot section: contains all dot imports. This section is not present unless explicitly enabled.
314
+ - alias # Alias section: contains all alias imports. This section is not present unless explicitly enabled.
315
+ - localmodule # Local module section: contains all local packages. This section is not present unless explicitly enabled.
316
+ # Checks that no inline comments are present.
317
+ # Default: false
318
+ no-inline-comments: false
319
+ # Checks that no prefix comments (comment lines above an import) are present.
320
+ # Default: false
321
+ no-prefix-comments: false
322
+ # Enable custom order of sections.
323
+ # If `true`, make the section order the same as the order of `sections`.
324
+ # Default: false
325
+ custom-order: true
326
+ # Drops lexical ordering for custom sections.
327
+ # Default: false
328
+ no-lex-order: true
329
+
330
+ golines:
331
+ # Target maximum line length.
332
+ # Default: 100
333
+ max-len: 180
334
+ # Length of a tabulation.
335
+ # Default: 4
336
+ tab-len: 4
337
+ # Shorten single-line comments.
338
+ # Default: false
339
+ shorten-comments: true
340
+ # Default: true
341
+ reformat-tags: false
342
+ # Split chained methods on the dots as opposed to the arguments.
343
+ # Default: true
344
+ chain-split-dots: false
345
+
346
+ goimports:
347
+ # Local prefixes to use with "goimports -local".
348
+ # Default: []
349
+ local-prefixes:
350
+ - "github.com/looplj/axonhub"
351
+
352
+ exclusions:
353
+ # Mode of the generated files analysis.
354
+ #
355
+ # - `strict`: sources are excluded by strictly following the Go generated file convention.
356
+ # Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$`
357
+ # This line must appear before the first non-comment, non-blank text in the file.
358
+ # https://go.dev/s/generatedcode
359
+ # - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc.
360
+ # - `disable`: disable the generated files exclusion.
361
+ #
362
+ # Default: lax
363
+ generated: lax
364
+ # Which file paths to exclude.
365
+ # This option is ignored when using `--stdin` as the path is unknown.
366
+ # Default: []
367
+ paths:
368
+ - ".resolvers.go"
369
+ - "axonhub.resolvers.go"
370
+ - "ent.resolvers.go"
371
+ - "internal/ent"
372
+ - "internal/server/gql/ent.resolvers.go"
373
+
374
+
375
+ issues:
376
+ # make issues output unique by line, default is true
377
+ uniq-by-line: true
378
+
379
+ max-issues-per-linter: 0
380
+
381
+ # Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
382
+ max-same-issues: 0
383
+
384
+ # Show only new issues: if there are unstaged changes or untracked files,
385
+ # only those changes are analyzed, else only changes in HEAD~ are analyzed.
386
+ # It's a super-useful option for integration of golangci-lint into existing
387
+ # large codebase. It's not practical to fix all existing issues at the moment
388
+ # of integration: much better don't allow issues in new code.
389
+ # Default is false.
390
+ new: true
391
+
392
+ # Auto-fix enabled issues. Default is false.
393
+ fix: true
.goreleaser.yml ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: 2
2
+
3
+ project_name: axonhub
4
+
5
+ dist: .builds
6
+
7
+ before:
8
+ hooks:
9
+ - /bin/sh -c 'cd deploy && chmod +x *.sh'
10
+ - make build-frontend
11
+ - go mod tidy
12
+
13
+ builds:
14
+ - id: build
15
+ main: ./cmd/axonhub
16
+ binary: axonhub
17
+ flags:
18
+ - -tags=nomsgpack
19
+ ldflags:
20
+ - -s -w -X github.com/looplj/axonhub/internal/build.Version={{ .Tag }}
21
+ - -s -w -X github.com/looplj/axonhub/internal/build.Commit={{ .Commit }}
22
+ - -s -w -X github.com/looplj/axonhub/internal/build.BuildTime={{ .Date }}
23
+ env:
24
+ - CGO_ENABLED=0
25
+ goos:
26
+ - linux
27
+ - windows
28
+ - darwin
29
+ goarch:
30
+ - amd64
31
+ - arm64
32
+ - loong64
33
+ goarm:
34
+ - 7
35
+ ignore:
36
+ - goos: windows
37
+ goarch: arm
38
+ - goos: windows
39
+ goarch: s390x
40
+ - goos: windows
41
+ goarch: loong64
42
+ - goos: darwin
43
+ goarch: arm
44
+ - goos: darwin
45
+ goarch: s390x
46
+ - goos: darwin
47
+ goarch: loong64
48
+
49
+ release:
50
+ draft: true
51
+
52
+ archives:
53
+ - id: archive
54
+ ids: [build]
55
+ formats: [zip]
56
+ files:
57
+ - LICENSE
58
+ - CHANGELOG.md
59
+ - src: deploy/install.sh
60
+ dst: install.sh
61
+ - src: deploy/start.sh
62
+ dst: start.sh
63
+ - src: deploy/stop.sh
64
+ dst: stop.sh
65
+ - src: deploy/install.ps1
66
+ dst: install.ps1
67
+ - src: deploy/start.ps1
68
+ dst: start.ps1
69
+ - src: deploy/stop.ps1
70
+ dst: stop.ps1
71
+ - src: deploy/install.bat
72
+ dst: install.bat
73
+ - src: deploy/start.bat
74
+ dst: start.bat
75
+ - src: deploy/stop.bat
76
+ dst: stop.bat
77
+
78
+ checksum:
79
+ name_template: 'checksums.txt'
80
+
81
+ snapshot:
82
+ version_template: '{{ incpatch .Version }}-snapshot'
83
+
84
+ changelog:
85
+ sort: asc
86
+ filters:
87
+ exclude:
88
+ - '^examples:'
89
+ - '^ui:'
.mockery.yml ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ all: false
2
+ dir: '{{.InterfaceDir}}'
3
+ filename: '{{.InterfaceName|snakecase}}.mock.gen.go'
4
+ force-file-write: true
5
+ formatter: goimports
6
+ log-level: info
7
+ structname: '{{.Mock}}{{.InterfaceName}}'
8
+ pkgname: '{{.SrcPackageName}}'
9
+ recursive: false
10
+ require-template-schema-exists: true
11
+ template: testify
12
+ template-schema: '{{.Template}}.schema.json'
13
+ packages:
14
+ github.com/looplj/axonhub/llm/pipeline:
15
+ config:
16
+ interfaces:
17
+ Executor:
.pre-commit-config.yaml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ repos:
2
+ - repo: 'https://github.com/golangci/golangci-lint'
3
+ rev: v2.8.0
4
+ hooks:
5
+ - id: golangci-lint
6
+ entry: golangci-lint run -vvv
7
+
8
+ - repo: local
9
+ hooks:
10
+ - id: run-go-test
11
+ name: Run go test
12
+ entry: bash -c 'go test ./... --short'
13
+ language: system
14
+ types: [go]
15
+ pass_filenames: false
16
+
17
+ - repo: https://github.com/dnephin/pre-commit-golang
18
+ rev: v0.5.1
19
+ hooks:
20
+ # - id: go-build
21
+ - id: go-mod-tidy
22
+
AGENTS.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Repository Guidelines
2
+
3
+ @CLAUDE.md read and follow its instructions.
CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ v0.4.0
2
+
3
+ - Introduced thread-aware tracing with zero-SDK integration and configurable trace headers
4
+ - Added trace visualization interface for following end-to-end conversations
5
+ - Added configurable data storage policies to keep or trim trace payloads based on compliance needs
6
+
7
+ v0.3.0
8
+
9
+ - Launched project workspace management with per-project API keys and dashboards
10
+ - Improved project-scoped permissions and usage insights
11
+
12
+ v0.2.0
13
+
14
+ - Added multimodal image generation via chat completions and image_generation tools
15
+ - Documented provider support and sample usage for image workflows
16
+
17
+ v0.1.1
18
+
19
+ - Add OpenRouter outbound transformer
20
+ - Support Google Gemini OpenAI compatible API
CLAUDE.md ADDED
@@ -0,0 +1,428 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ AxonHub is an all-in-one AI development platform that serves as a unified API gateway for multiple AI providers. It provides OpenAI and Anthropic-compatible API interfaces with automatic request transformation, enabling seamless communication between clients and various AI providers through a sophisticated bidirectional data transformation pipeline.
8
+
9
+ ### Core Architecture
10
+ - **Transformation Pipeline**: Bidirectional data transformation between clients and AI providers
11
+ - **Unified API Layer**: OpenAI/Anthropic-compatible interfaces with automatic translation
12
+ - **Channel Management**: Multi-provider support with configurable channels
13
+ - **Thread-aware Tracing**: Request tracing with thread linking capabilities
14
+ - **Permission System**: RBAC with fine-grained access control
15
+ - **System Management**: Web-based configuration interface
16
+
17
+ ## Development Commands
18
+
19
+ ### Backend (Go)
20
+ ```bash
21
+ # Run the main server
22
+ go run cmd/axonhub/main.go
23
+
24
+ # Generate GraphQL and Ent code (run after schema changes)
25
+ make generate
26
+
27
+ # Run tests
28
+ go test ./...
29
+
30
+ # Run linting
31
+ golangci-lint run
32
+
33
+ # Build the application
34
+ make build-backend
35
+
36
+ # Use air for hot reload (development) - server auto-restarts on changes
37
+ air
38
+ ```
39
+
40
+ ### Frontend (React)
41
+ ```bash
42
+ # Navigate to frontend directory
43
+ cd frontend
44
+
45
+ # Install dependencies
46
+ pnpm install
47
+
48
+ # Start development server (port 5173) - already configured with backend proxy
49
+ pnpm dev
50
+
51
+ # Build for production
52
+ pnpm build
53
+
54
+ # Run linting
55
+ pnpm lint
56
+
57
+ # Format code
58
+ pnpm format
59
+
60
+ # Check for unused dependencies
61
+ pnpm knip
62
+
63
+ # Tests (Playwright)
64
+ pnpm test:e2e # run E2E suite via scripts/e2e
65
+ pnpm test:e2e:headed # headed browser mode
66
+ pnpm test:e2e:ui # Playwright UI runner
67
+ pnpm test:e2e:debug # debug mode
68
+ pnpm test:e2e:report # open last HTML report
69
+ pnpm test:ui # Playwright UI (direct)
70
+ pnpm test:ui:headed # Playwright headed (direct)
71
+ ```
72
+
73
+ ### Make Commands
74
+ ```bash
75
+ # Generate GraphQL and Ent code
76
+ make generate
77
+
78
+ # Build backend only
79
+ make build-backend
80
+
81
+ # Build frontend only
82
+ make build-frontend
83
+
84
+ # Build both frontend and backend
85
+ make build
86
+
87
+ # Cleanup test database
88
+ make cleanup-db
89
+
90
+ # E2E Testing
91
+ make e2e-test # Run full E2E test suite
92
+ make e2e-backend-start # Start E2E backend service
93
+ make e2e-backend-stop # Stop E2E backend service
94
+ make e2e-backend-status # Check E2E backend status
95
+ make e2e-backend-restart # Restart E2E backend service
96
+ make e2e-backend-clean # Clean up E2E test files
97
+
98
+ # Migration Testing
99
+ make migration-test TAG=v0.1.0 # Test migration from specific tag
100
+ make migration-test-all # Test migration for all recent versions
101
+ make migration-test-all-dbs TAG=v0.1.0 # Test migration across all DB types
102
+
103
+ # Data Syncing
104
+ make sync-faq # Sync FAQ from GitHub issues
105
+ make sync-models # Sync model developers data
106
+
107
+ # Utilities
108
+ make filter-logs # Filter and analyze load balance logs
109
+ ```
110
+
111
+ ## Architecture Overview
112
+
113
+ ### Technology Stack
114
+ - **Backend**: Go 1.25.3+ with Gin HTTP framework, Ent ORM, gqlgen GraphQL, FX dependency injection
115
+ - **Frontend**: React 19 with TypeScript, TanStack Router, TanStack Query, Zustand, Tailwind CSS
116
+ - **Database**: SQLite (development), PostgreSQL/MySQL/TiDB (production)
117
+ - **Authentication**: JWT with role-based access control
118
+
119
+ ### Backend Structure
120
+ - **Server Layer** (`internal/server/`): HTTP server and route handling with Gin
121
+ - **Business Logic** (`internal/server/biz/`): Core business logic and services
122
+ - **API Layer** (`internal/server/api/`): REST and GraphQL API handlers
123
+ - **Database** (`internal/ent/`): Ent ORM for database operations with SQLite
124
+ - **LLM Integration** (`internal/llm/`): AI provider transformers and pipeline processing
125
+ - **Context Management** (`internal/contexts/`): Context handling utilities
126
+ - **Utilities** (`internal/pkg/`): Shared utilities (HTTP client, streams, errors, JSON)
127
+ - **Auth & Scopes** (`internal/scopes/`): Permission system with role-based access control
128
+
129
+ ### Frontend Structure
130
+ - **TanStack Router**: File-based routing in `frontend/src/routes/`
131
+ - **TanStack Query**: Data fetching and caching with GraphQL
132
+ - **TanStack Table**: Data tables with pagination/filtering
133
+ - **GraphQL**: API communication via `frontend/src/gql/`
134
+ - **Shadcn/ui**: Component library with Tailwind CSS
135
+ - **Zustand**: State management in `frontend/src/stores/`
136
+ - **AI SDK**: Integration for enhanced AI capabilities
137
+ - **Feature-based Organization**: Components organized by feature in `frontend/src/features/`
138
+ - **Shared Components**: Reusable components in `frontend/src/components/`
139
+ - **Custom Hooks**: Shared hooks in `frontend/src/hooks/`
140
+ - **Internationalization**: i18n support in `frontend/src/locales/` (en.json, zh.json)
141
+ - **Utilities**: Shared utilities in `frontend/src/utils/`
142
+
143
+ ### Key Components
144
+
145
+ #### LLM Transformer System
146
+ - **Pipeline Architecture**: Enhanced request processing with retry capabilities
147
+ - **Persistent Transformers**: `PersistentInboundTransformer` and `PersistentOutboundTransformer`
148
+ - **Stream Processing**: Enhanced SSE support with chunk aggregation
149
+ - **Supported Providers**: OpenAI, Anthropic, DeepSeek, AI SDK
150
+ - **Auto-save**: Configurable persistence of chat requests and responses
151
+ - **Load Balancing**: Round-robin and failover strategies
152
+
153
+ #### Database Schema
154
+ - **Users**: Authentication and role management with soft delete
155
+ - **Roles**: Permission groups with scope-based access
156
+ - **Channels**: AI provider configurations
157
+ - **API Keys**: Authentication tokens
158
+ - **Requests**: Request logging and execution tracking
159
+ - **Systems**: System-wide configuration (storeChunks, etc.)
160
+ - **Soft Delete**: Data safety across all entities
161
+
162
+ #### Permission System
163
+ - **Enhanced Scopes**: read_channels, write_channels, read_users, read_settings, write_settings
164
+ - **Owner scope**: Full system access
165
+ - **Role-based access control**: Users can have multiple roles
166
+ - **Ent privacy policies**: Database-level permission enforcement
167
+ - **Granular permissions**: Fine-grained access control
168
+
169
+ #### System Management
170
+ - **Web Interface**: Complete system settings management
171
+ - **Configuration Options**: Controllable persistence and system behavior
172
+ - **Real-time Updates**: Live configuration changes
173
+ - **GraphQL API**: System configuration endpoints
174
+
175
+
176
+ ### Development Workflow
177
+
178
+ #### General Notes
179
+ - All summary files should be stored in `.windsurf/summary` directory if available
180
+
181
+ #### Backend Development
182
+ 1. **Code Changes**: Modify Go code in `internal/` directory
183
+ 2. **Schema Changes**: If modifying Ent schemas (`internal/ent/schema/`), run `make generate`
184
+ - When changing any Ent schema or GraphQL schema, run `make generate` to regenerate models and resolvers
185
+ - The `make generate` command automatically enters the gql directory and runs go generate
186
+ 3. **GraphQL Changes**: If modifying GraphQL schemas (`internal/server/gql/`), run `make generate`
187
+ 4. **Hot Reload**: Use `air` for automatic server restart on changes (configured in `.air.toml`)
188
+ - **CRITICAL**: The server in development is managed by air - it will rebuild and start when code changes, so DO NOT restart manually
189
+ - Air watches `.go` and `.yml` files
190
+ - Excludes directories: `frontend`, `integration_test`, `scripts`, `tools`, `examples`
191
+ - Builds to `./tmp/axonhub`
192
+
193
+ **Dev vs prod builds**
194
+ - Dev hot-reload uses Air (`.air.toml`) and builds to `./tmp/axonhub`.
195
+ - Production build uses `make build-backend` and outputs `./axonhub`.
196
+
197
+ 5. **Building**: Use `make build-backend` to build the server for production deployment
198
+ 6. **Testing**: Run `go test ./...` for unit tests
199
+ 7. **Linting**: Run `golangci-lint run` before committing
200
+
201
+ #### Frontend Development
202
+ 1. **Start Dev Server**: Run `pnpm dev` in `frontend/` directory (port 5173)
203
+ - **CRITICAL**: DO NOT restart the development server - it's already started and managed
204
+ 2. **Proxy Configuration**: Frontend proxies API requests to backend on port 8090
205
+ 3. **Code Changes**: Modify React components in `frontend/src/`
206
+ 4. **Internationalization**:
207
+ - **REQUIRED**: MUST ADD i18n key in the `frontend/src/locales/*.json` files if creating a new key in the code
208
+ - **REQUIRED**: MUST KEEP THE KEY IN THE CODE AND JSON FILE THE SAME
209
+ - Support both English and Chinese translations (`en.json` and `zh.json`)
210
+ - Add i18n keys to both `frontend/src/locales/en.json` and `frontend/src/locales/zh.json`
211
+ 5. **Testing**: Use `pnpm test:e2e` for E2E tests or `pnpm test:ui` for Playwright UI tests
212
+ 6. **Linting**: Run `pnpm lint` before committing
213
+
214
+ **Additional Frontend Development Rules:**
215
+ - Use `pnpm` as the package manager exclusively
216
+ - Use GraphQL input to filter data instead of filtering in the frontend
217
+ - Search filters must use debounce to avoid too many requests
218
+ - Add sidebar data and route when adding new feature pages
219
+ - Use `extractNumberID` to extract int id from the GUID
220
+ - Follow component organization in `frontend/src/features/`
221
+
222
+ #### Database Development
223
+ 1. **Schema Changes**: Modify Ent schemas in `internal/ent/schema/`
224
+ 2. **Code Generation**: Run `make generate` to regenerate Ent code
225
+ 3. **Migrations**: Auto-applied on server start
226
+ 4. **Testing**: Use `enttest.NewEntClient(t, "sqlite3", "file:ent?mode=memory&_fk=0")` for isolated tests
227
+
228
+ #### Integration Testing
229
+ 1. **E2E Tests**: Run `make e2e-test` for full integration test suite
230
+ 2. **Migration Tests**: Run `make migration-test TAG=v0.1.0` to test database migrations
231
+ 3. **Cross-DB Tests**: Run `make migration-test-all-dbs TAG=v0.1.0` for multi-database migration testing
232
+
233
+ ## Important Files
234
+
235
+ ### Backend
236
+ - `cmd/axonhub/main.go`: Application entry point
237
+ - `internal/server/server.go`: HTTP server configuration
238
+ - `internal/llm/pipeline/`: Pipeline processing architecture
239
+ - `internal/ent/schema/`: Database schema definitions
240
+ - `internal/pkg/`: Shared utilities and helpers
241
+ - `conf/conf.go`: Configuration loading and validation
242
+ - `internal/server/gql/`: GraphQL schema and resolvers
243
+
244
+ ### Frontend
245
+ - `frontend/src/app/`: React Router v7 app directory
246
+ - `frontend/src/features/`: Feature-based component organization
247
+ - `frontend/src/features/system/`: System management interface
248
+ - `frontend/src/locales/`: Internationalization files (en.json, zh.json)
249
+
250
+ ### Configuration & Documentation
251
+ - `config.yml` / `config.example.yml`: Main configuration files
252
+ - `AGENTS.md`: Repository guidelines for contributors
253
+ - `README.md` / `README.zh-CN.md`: Project documentation
254
+ - `docs/`: Detailed documentation and architecture diagrams
255
+
256
+ ## Key Development Patterns
257
+
258
+ ### Adding a New AI Provider Channel
259
+ When introducing a new provider channel, keep backend and frontend changes aligned:
260
+
261
+ 1. **Extend the channel enum in the Ent schema** – add the provider key to the `field.Enum("type")` list in `internal/ent/schema/channel.go` and regenerate Ent artifacts
262
+ 2. **Wire the outbound transformer** – update the switch in `ChannelService.buildChannel` to construct the correct outbound transformer for the new enum
263
+ 3. **Sync the frontend schema** – update:
264
+ - Zod schema in `frontend/src/features/channels/data/schema.ts`
265
+ - Channel configuration in `frontend/src/features/channels/data/constants.ts`
266
+ - Internationalization in `frontend/src/locales/en.json` and `frontend/src/locales/zh.json`
267
+
268
+ ### Database Schema Changes
269
+ - Always run `make generate` after modifying Ent schemas
270
+ - Use `enttest.NewEntClient(t, "sqlite3", "file:ent?mode=memory&_fk=0")` for testing
271
+ - Follow soft delete patterns across all entities
272
+
273
+ ### Go Development Patterns
274
+ - **REQUIRED**: USE `github.com/samber/lo` package to handle collection, slice, map, ptr, etc.
275
+ - Follow dependency injection patterns using FX framework
276
+ - Use structured logging with zap
277
+ - Implement proper context propagation
278
+ - **Key Dependencies**:
279
+ - `entgo.io/ent` - ORM framework with code generation
280
+ - `github.com/99designs/gqlgen` - GraphQL code generation
281
+ - `github.com/gin-gonic/gin` - HTTP framework
282
+ - `go.uber.org/fx` - Dependency injection
283
+ - `github.com/google/uuid` - UUID generation
284
+ - `github.com/redis/go-redis/v9` - Redis client
285
+ - `github.com/jackc/pgx/v5` - PostgreSQL driver
286
+ - `github.com/go-sql-driver/mysql` - MySQL driver
287
+
288
+ ### Biz Service Rules
289
+ - **REQUIRED**: Ensure the dependency service not be nil, the logic code should not check the service is nil.
290
+ - **REQUIRED**: Dependency services are guaranteed initialized; business logic must not add nil checks.
291
+
292
+ ### Error Handling
293
+ - **REQUIRED**: Always handle errors using the unified error response format from `internal/pkg/errors`
294
+ - Implement proper error wrapping with context
295
+ - Follow middleware-based error recovery patterns
296
+
297
+ ### GraphQL Development
298
+ - **Method Signatures/New Fields**: Resolver method signatures and new field definitions should always be generated by modifying `*.graphql` schema files and running `make generate`
299
+ - **Implementation Content**: The implementation body of existing resolver methods can be manually modified
300
+ - Run `make generate` from the project root after schema changes
301
+ - Use GraphQL input filtering instead of frontend filtering for data queries
302
+
303
+ #### Adding New GraphQL Types
304
+ When adding new types or inputs to the GraphQL schema, you need to:
305
+
306
+ 1. **Add the type/input definition** in the appropriate `.graphql` file in `internal/server/gql/`
307
+ 2. **Add type mapping in `gqlgen.yml`** under the `models:` section - map the GraphQL type to the Go type:
308
+ ```yaml
309
+ MyNewType:
310
+ model:
311
+ - github.com/looplj/axonhub/internal/package.MyType
312
+ MyNewInput:
313
+ model:
314
+ - github.com/looplj/axonhub/internal/package.MyInput
315
+ ```
316
+ 3. **Run `make generate`** to regenerate the code
317
+ 4. **Implement the resolver** in the appropriate `*_resolvers.go` file
318
+
319
+ ## Quality gates (match CI / pre-commit)
320
+
321
+ Backend:
322
+ - `go test -v --short ./...`
323
+ - `golangci-lint run`
324
+
325
+ Pre-commit hooks (see `.pre-commit-config.yaml`) also run:
326
+ - `go mod tidy`
327
+
328
+ ## Testing
329
+
330
+ ### Backend Testing
331
+ - **Unit Tests**: Go unit tests with testify
332
+ - **Integration Tests**: Database integration tests with in-memory SQLite
333
+ - **Test Database**: Use `enttest.NewEntClient(t, "sqlite3", "file:ent?mode=memory&_fk=0")` for isolated tests
334
+ - **Code Quality**: `golangci-lint run` for Go linting
335
+ - **Error Handling**: Use unified error format from `internal/pkg/errors`
336
+
337
+ ### Frontend Testing
338
+ - **E2E Tests**: Playwright E2E tests with UI and headed modes
339
+ - **Test Commands**:
340
+ - `pnpm test:e2e` - Run full E2E test suite
341
+ - `pnpm test:e2e:headed` - Run E2E tests in headed mode
342
+ - `pnpm test:e2e:ui` - Run E2E tests in UI mode
343
+ - `pnpm test:e2e:debug` - Run E2E tests in debug mode
344
+ - `pnpm test:ui` - Run Playwright tests in UI mode
345
+ - `pnpm test:ui:headed` - Run Playwright tests in headed mode
346
+ - **Code Quality**: `pnpm lint` for TypeScript/ESLint checking
347
+ - **Test Credentials**: Frontend testing uses `my@example.com` / `pwd123456`
348
+
349
+ ### Integration Testing
350
+ - **E2E Test Suite**: Use `make e2e-test` or `bash ./scripts/e2e/e2e-test.sh` for full integration tests
351
+ - **Migration Testing**: Use `make migration-test TAG=v0.1.0` to test database migrations
352
+ - **Cross-DB Testing**: Use `make migration-test-all-dbs TAG=v0.1.0` to test migrations across all supported databases
353
+ - **Test Cleanup**: Use `make cleanup-db` to remove playwright test data from database
354
+
355
+ ## Key Features in Development
356
+ - Enhanced transformer stream aggregation
357
+ - Configurable persistence behavior
358
+ - System options for controlling data storage
359
+ - Improved error handling and recovery mechanisms
360
+ - Stream closing when client disconnects
361
+ - Real-time request tracing and monitoring
362
+
363
+
364
+ ## Configuration
365
+
366
+ ### Environment Setup
367
+ - Uses SQLite database (axonhub.db) by default
368
+ - Configuration loaded from `conf/conf.go` with YAML and env var support
369
+ - Logging with structured JSON output using zap
370
+ - FX dependency injection framework
371
+ - Go version: 1.25.3+
372
+ - Frontend development server: port 5173 (proxies to backend)
373
+ - Backend API: port 8090
374
+
375
+ ### Database Support
376
+ - **Development**: SQLite (default, auto-migration)
377
+ - **Production**: PostgreSQL 15+, MySQL 8.0+, TiDB V8.0+
378
+ - **Cloud**: TiDB Cloud, Neon DB (serverless options)
379
+ - All support automatic schema migration
380
+
381
+ ### Key Configuration Files
382
+ - `config.yml` / `config.example.yml`: Main configuration files (YAML format)
383
+ - `.air.toml`: Air hot reload configuration for backend development
384
+ - `.golangci.yml`: Go linting configuration
385
+ - `.pre-commit-config.yaml`: Git pre-commit hooks
386
+ - `frontend/vite.config.ts`: Frontend build configuration
387
+ - `docker-compose.yml`: Docker Compose configuration for containerized deployment
388
+ - `render.yaml`: Render deployment configuration
389
+
390
+ ### Configuration Management
391
+ - **Primary Config**: `config.yml` (create from `config.example.yml`)
392
+ - **Environment Variables**: All configuration options can be overridden with environment variables
393
+ - Format: `AXONHUB_{SECTION}_{KEY}` (e.g., `AXONHUB_SERVER_PORT=8090`)
394
+ - Nested keys use underscores (e.g., `AXONHUB_SERVER_CORS_ENABLED=true`)
395
+ - **Database Support**: Configurable via `db.dialect` and `db.dsn`
396
+ - SQLite (default): `dialect: "sqlite3"`, `dsn: "axonhub.db"`
397
+ - PostgreSQL: `dialect: "postgres"`, `dsn: "host=..."`
398
+ - MySQL: `dialect: "mysql"`, `dsn: "user:pass@tcp(...)/axonhub"`
399
+ - TiDB: `dialect: "tidb"`, `dsn: "user.root:pass@tcp(...)/axonhub?tls=true"`
400
+
401
+ ## Common Development Tasks
402
+
403
+ ### Running a Single Test
404
+ ```bash
405
+ # Backend: Run specific test
406
+ go test -v -run TestFunctionName ./path/to/package
407
+
408
+ # Frontend: Run specific test file
409
+ pnpm test:e2e -- path/to/test.spec.ts
410
+ ```
411
+
412
+ ### Database Migration
413
+ ```bash
414
+ # Generate new migration after schema changes
415
+ make generate
416
+
417
+ # The migration will be auto-applied on server start
418
+ ```
419
+
420
+ ### Debugging Stream Processing
421
+ - Check `internal/llm/pipeline/` for stream aggregation logic
422
+ - Monitor SSE connections in browser DevTools Network tab
423
+ - Use request tracing with `AH-Trace-Id` header for debugging
424
+
425
+ ### Performance Optimization
426
+ - Use request tracing to identify bottlenecks
427
+ - Monitor database queries with Ent's built-in logging
428
+ - Check channel load balancing configuration for optimal distribution
LICENSE ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ AxonHub Licensing Overview:
2
+ 1. Priority: If a specific file contains a different license header, the terms in that header take precedence for that file.
3
+ 2. Scope: The `llm/` directory in this project is licensed under the GNU Lesser General Public License version 3.0 (LGPL-3.0). See `llm/LICENSE` for details.
4
+ 3. Scope: The `llm/bedrock/` directory in this project is copied from anthropic-sdk-go. See `llm/bedrock/NOTICE` for details.
5
+ 4. General: All other parts of this project are licensed under the Apache License, Version 2.0, unless otherwise specified.
6
+
7
+ ---
8
+
9
+ Apache License
10
+ Version 2.0, January 2004
11
+ http://www.apache.org/licenses/
12
+
13
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
14
+
15
+ 1. Definitions.
16
+
17
+ "License" shall mean the terms and conditions for use, reproduction,
18
+ and distribution as defined by Sections 1 through 9 of this document.
19
+
20
+ "Licensor" shall mean the copyright owner or entity authorized by
21
+ the copyright owner that is granting the License.
22
+
23
+ "Legal Entity" shall mean the union of the acting entity and all
24
+ other entities that control, are controlled by, or are under common
25
+ control with that entity. For the purposes of this definition,
26
+ "control" means (i) the power, direct or indirect, to cause the
27
+ direction or management of such entity, whether by contract or
28
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
29
+ outstanding shares, or (iii) beneficial ownership of such entity.
30
+
31
+ "You" (or "Your") shall mean an individual or Legal Entity
32
+ exercising permissions granted by this License.
33
+
34
+ "Source" form shall mean the preferred form for making modifications,
35
+ including but not limited to software source code, documentation
36
+ source, and configuration files.
37
+
38
+ "Object" form shall mean any form resulting from mechanical
39
+ transformation or translation of a Source form, including but
40
+ not limited to compiled object code, generated documentation,
41
+ and conversions to other media types.
42
+
43
+ "Work" shall mean the work of authorship, whether in Source or
44
+ Object form, made available under the License, as indicated by a
45
+ copyright notice that is included in or attached to the work
46
+ (an example is provided in the Appendix below).
47
+
48
+ "Derivative Works" shall mean any work, whether in Source or Object
49
+ form, that is based on (or derived from) the Work and for which the
50
+ editorial revisions, annotations, elaborations, or other modifications
51
+ represent, as a whole, an original work of authorship. For the purposes
52
+ of this License, Derivative Works shall not include works that remain
53
+ separable from, or merely link (or bind by name) to the interfaces of,
54
+ the Work and Derivative Works thereof.
55
+
56
+ "Contribution" shall mean any work of authorship, including
57
+ the original version of the Work and any modifications or additions
58
+ to that Work or Derivative Works thereof, that is intentionally
59
+ submitted to Licensor for inclusion in the Work by the copyright owner
60
+ or by an individual or Legal Entity authorized to submit on behalf of
61
+ the copyright owner. For the purposes of this definition, "submitted"
62
+ means any form of electronic, verbal, or written communication sent
63
+ to the Licensor or its representatives, including but not limited to
64
+ communication on electronic mailing lists, source code control systems,
65
+ and issue tracking systems that are managed by, or on behalf of, the
66
+ Licensor for the purpose of discussing and improving the Work, but
67
+ excluding communication that is conspicuously marked or otherwise
68
+ designated in writing by the copyright owner as "Not a Contribution."
69
+
70
+ "Contributor" shall mean Licensor and any individual or Legal Entity
71
+ on behalf of whom a Contribution has been received by Licensor and
72
+ subsequently incorporated within the Work.
73
+
74
+ 2. Grant of Copyright License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ copyright license to reproduce, prepare Derivative Works of,
78
+ publicly display, publicly perform, sublicense, and distribute the
79
+ Work and such Derivative Works in Source or Object form.
80
+
81
+ 3. Grant of Patent License. Subject to the terms and conditions of
82
+ this License, each Contributor hereby grants to You a perpetual,
83
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
84
+ (except as stated in this section) patent license to make, have made,
85
+ use, offer to sell, sell, import, and otherwise transfer the Work,
86
+ where such license applies only to those patent claims licensable
87
+ by such Contributor that are necessarily infringed by their
88
+ Contribution(s) alone or by combination of their Contribution(s)
89
+ with the Work to which such Contribution(s) was submitted. If You
90
+ institute patent litigation against any entity (including a
91
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
92
+ or a Contribution incorporated within the Work constitutes direct
93
+ or contributory patent infringement, then any patent licenses
94
+ granted to You under this License for that Work shall terminate
95
+ as of the date such litigation is filed.
96
+
97
+ 4. Redistribution. You may reproduce and distribute copies of the
98
+ Work or Derivative Works thereof in any medium, with or without
99
+ modifications, and in Source or Object form, provided that You
100
+ meet the following conditions:
101
+
102
+ (a) You must give any other recipients of the Work or
103
+ Derivative Works a copy of this License; and
104
+
105
+ (b) You must cause any modified files to carry prominent notices
106
+ stating that You changed the files; and
107
+
108
+ (c) You must retain, in the Source form of any Derivative Works
109
+ that You distribute, all copyright, patent, trademark, and
110
+ attribution notices from the Source form of the Work,
111
+ excluding those notices that do not pertain to any part of
112
+ the Derivative Works; and
113
+
114
+ (d) If the Work includes a "NOTICE" text file as part of its
115
+ distribution, then any Derivative Works that You distribute must
116
+ include a readable copy of the attribution notices contained
117
+ within such NOTICE file, excluding those notices that do not
118
+ pertain to any part of the Derivative Works, in at least one
119
+ of the following places: within a NOTICE text file distributed
120
+ as part of the Derivative Works; within the Source form or
121
+ documentation, if provided along with the Derivative Works; or,
122
+ within a display generated by the Derivative Works, if and
123
+ wherever such third-party notices normally appear. The contents
124
+ of the NOTICE file are for informational purposes only and
125
+ do not modify the License. You may add Your own attribution
126
+ notices within Derivative Works that You distribute, alongside
127
+ or as an addendum to the NOTICE text from the Work, provided
128
+ that such additional attribution notices cannot be construed
129
+ as modifying the License.
130
+
131
+ You may add Your own copyright statement to Your modifications and
132
+ may provide additional or different license terms and conditions
133
+ for use, reproduction, or distribution of Your modifications, or
134
+ for any such Derivative Works as a whole, provided Your use,
135
+ reproduction, and distribution of the Work otherwise complies with
136
+ the conditions stated in this License.
137
+
138
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
139
+ any Contribution intentionally submitted for inclusion in the Work
140
+ by You to the Licensor shall be under the terms and conditions of
141
+ this License, without any additional terms or conditions.
142
+ Notwithstanding the above, nothing herein shall supersede or modify
143
+ the terms of any separate license agreement you may have executed
144
+ with Licensor regarding such Contributions.
145
+
146
+ 6. Trademarks. This License does not grant permission to use the trade
147
+ names, trademarks, service marks, or product names of the Licensor,
148
+ except as required for reasonable and customary use in describing the
149
+ origin of the Work and reproducing the content of the NOTICE file.
150
+
151
+ 7. Disclaimer of Warranty. Unless required by applicable law or
152
+ agreed to in writing, Licensor provides the Work (and each
153
+ Contributor provides its Contributions) on an "AS IS" BASIS,
154
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
155
+ implied, including, without limitation, any warranties or conditions
156
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
157
+ PARTICULAR PURPOSE. You are solely responsible for determining the
158
+ appropriateness of using or redistributing the Work and assume any
159
+ risks associated with Your exercise of permissions under this License.
160
+
161
+ 8. Limitation of Liability. In no event and under no legal theory,
162
+ whether in tort (including negligence), contract, or otherwise,
163
+ unless required by applicable law (such as deliberate and grossly
164
+ negligent acts) or agreed to in writing, shall any Contributor be
165
+ liable to You for damages, including any direct, indirect, special,
166
+ incidental, or consequential damages of any character arising as a
167
+ result of this License or out of the use or inability to use the
168
+ Work (including but not limited to damages for loss of goodwill,
169
+ work stoppage, computer failure or malfunction, or any and all
170
+ other commercial damages or losses), even if such Contributor
171
+ has been advised of the possibility of such damages.
172
+
173
+ 9. Accepting Warranty or Additional Liability. While redistributing
174
+ the Work or Derivative Works thereof, You may choose to offer,
175
+ and charge a fee for, acceptance of support, warranty, indemnity,
176
+ or other liability obligations and/or rights consistent with this
177
+ License. However, in accepting such obligations, You may act only
178
+ on Your own behalf and on Your sole responsibility, not on behalf
179
+ of any other Contributor, and only if You agree to indemnify,
180
+ defend, and hold each Contributor harmless for any liability
181
+ incurred by, or claims asserted against, such Contributor by reason
182
+ of your accepting any such warranty or additional liability.
183
+
184
+ END OF TERMS AND CONDITIONS
185
+
186
+ APPENDIX: How to apply the Apache License to your work.
187
+
188
+ To apply the Apache License to your work, attach the following
189
+ boilerplate notice, with the fields enclosed by brackets "[]"
190
+ replaced with your own identifying information. (Don't include
191
+ the brackets!) The text should be enclosed in the appropriate
192
+ comment syntax for the file format. We also recommend that a
193
+ file or class name and description of purpose be included on the
194
+ same "printed page" as the copyright notice for easier
195
+ identification within third-party archives.
196
+
197
+ Copyright 2026 looplj
198
+
199
+ Licensed under the Apache License, Version 2.0 (the "License");
200
+ you may not use this file except in compliance with the License.
201
+ You may obtain a copy of the License at
202
+
203
+ http://www.apache.org/licenses/LICENSE-2.0
204
+
205
+ Unless required by applicable law or agreed to in writing, software
206
+ distributed under the License is distributed on an "AS IS" BASIS,
207
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
208
+ See the License for the specific language governing permissions and
209
+ limitations under the License.
210
+
Makefile ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .PHONY: generate build backend frontend cleanup-db \
2
+ e2e-test e2e-backend-start e2e-backend-stop e2e-backend-status e2e-backend-restart e2e-backend-clean \
3
+ migration-test migration-test-all migration-test-all-dbs \
4
+ sync-faq sync-models filter-logs
5
+
6
+ # Generate GraphQL and Ent code
7
+ generate:
8
+ @echo "Generating GraphQL and Ent code..."
9
+ cd internal/server/gql && go generate
10
+ @echo "Generation completed!"
11
+
12
+ generate-openapi:
13
+ @echo "Generating GraphQL and Ent code..."
14
+ cd internal/server/gql/openapi && go generate
15
+ @echo "Generation completed!"
16
+
17
+ # Build the backend application
18
+ build-backend:
19
+ @echo "Building axonhub backend..."
20
+ go build -ldflags "-s -w" -tags=nomsgpack -o axonhub ./cmd/axonhub
21
+ @echo "Backend build completed!"
22
+
23
+ # Build the frontend application
24
+ build-frontend:
25
+ @echo "Building axonhub frontend..."
26
+ cd frontend && pnpm vite build
27
+ @echo "Copying frontend dist to server static directory..."
28
+ rm -rf internal/server/static/dist/assets
29
+ mkdir -p internal/server/static/dist
30
+ cp -r frontend/dist/* internal/server/static/dist/
31
+ @echo "Frontend build completed!"
32
+
33
+ # Build both frontend and backend
34
+ build: build-frontend build-backend
35
+ @echo "Full build completed!"
36
+
37
+ # Cleanup test database - remove all playwright test data
38
+ cleanup-db:
39
+ @echo "Cleaning up playwright test data from database..."
40
+ @sqlite3 axonhub.db "DELETE FROM user_roles WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'pw-test-%' OR first_name LIKE 'pw-test%');"
41
+ @sqlite3 axonhub.db "DELETE FROM user_projects WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'pw-test-%' OR first_name LIKE 'pw-test%');"
42
+ @sqlite3 axonhub.db "DELETE FROM user_projects WHERE project_id IN (SELECT id FROM projects WHERE slug LIKE 'pw-test-%' OR name LIKE 'pw-test-%');"
43
+ @sqlite3 axonhub.db "DELETE FROM api_keys WHERE name LIKE 'pw-test-%';"
44
+ @sqlite3 axonhub.db "DELETE FROM api_keys WHERE user_id IN (SELECT id FROM users WHERE email LIKE 'pw-test-%' OR first_name LIKE 'pw-test%');"
45
+ @sqlite3 axonhub.db "DELETE FROM api_keys WHERE project_id IN (SELECT id FROM projects WHERE slug LIKE 'pw-test-%' OR name LIKE 'pw-test-%');"
46
+ @sqlite3 axonhub.db "DELETE FROM roles WHERE code LIKE 'pw-test-%' OR name LIKE 'pw-test-%';"
47
+ @sqlite3 axonhub.db "DELETE FROM roles WHERE project_id IN (SELECT id FROM projects WHERE slug LIKE 'pw-test-%' OR name LIKE 'pw-test-%');"
48
+ @sqlite3 axonhub.db "DELETE FROM usage_logs WHERE project_id IN (SELECT id FROM projects WHERE slug LIKE 'pw-test-%' OR name LIKE 'pw-test-%');"
49
+ @sqlite3 axonhub.db "DELETE FROM requests WHERE project_id IN (SELECT id FROM projects WHERE slug LIKE 'pw-test-%' OR name LIKE 'pw-test-%');"
50
+ @sqlite3 axonhub.db "DELETE FROM users WHERE email LIKE 'pw-test-%' OR first_name LIKE 'pw-test%';"
51
+ @sqlite3 axonhub.db "DELETE FROM projects WHERE slug LIKE 'pw-test-%' OR name LIKE 'pw-test-%';"
52
+ @echo "Cleanup completed!"
53
+
54
+ # --- E2E Testing ---
55
+
56
+ # Run the full E2E test suite
57
+ e2e-test:
58
+ @echo "Running E2E tests..."
59
+ @./scripts/e2e/e2e-test.sh
60
+
61
+ # Start the E2E backend service
62
+ e2e-backend-start:
63
+ @echo "Starting E2E backend..."
64
+ @./scripts/e2e/e2e-backend.sh start
65
+
66
+ # Stop the E2E backend service
67
+ e2e-backend-stop:
68
+ @echo "Stopping E2E backend..."
69
+ @./scripts/e2e/e2e-backend.sh stop
70
+
71
+ # Check E2E backend status
72
+ e2e-backend-status:
73
+ @./scripts/e2e/e2e-backend.sh status
74
+
75
+ # Restart the E2E backend service
76
+ e2e-backend-restart:
77
+ @echo "Restarting E2E backend..."
78
+ @./scripts/e2e/e2e-backend.sh restart
79
+
80
+ # Clean up E2E test files
81
+ e2e-backend-clean:
82
+ @echo "Cleaning up E2E test files..."
83
+ @./scripts/e2e/e2e-backend.sh clean
84
+
85
+ # --- Migration Testing ---
86
+
87
+ # Test database migration from a specific tag
88
+ # Usage: make migration-test TAG=v0.1.0
89
+ migration-test:
90
+ @if [ -z "$(TAG)" ]; then echo "Error: TAG is required. Usage: make migration-test TAG=v0.1.0"; exit 1; fi
91
+ @echo "Running migration test from $(TAG)..."
92
+ @./scripts/migration/migration-test.sh $(TAG)
93
+
94
+ # Run migration tests for all recent stable versions
95
+ migration-test-all:
96
+ @echo "Running migration tests for all versions..."
97
+ @./scripts/migration/migration-test-all.sh
98
+
99
+ # Test migration across all supported database types
100
+ # Usage: make migration-test-all-dbs TAG=v0.1.0
101
+ migration-test-all-dbs:
102
+ @if [ -z "$(TAG)" ]; then echo "Error: TAG is required. Usage: make migration-test-all-dbs TAG=v0.1.0"; exit 1; fi
103
+ @echo "Running migration tests across all DBs from $(TAG)..."
104
+ @./scripts/migration/test-migration-all-dbs.sh $(TAG)
105
+
106
+ # --- Data Syncing ---
107
+
108
+ # Sync FAQ from GitHub issues
109
+ sync-faq:
110
+ @echo "Syncing FAQ from GitHub..."
111
+ @node ./scripts/sync/sync-github-faq.js
112
+
113
+ # Sync model developers data
114
+ sync-models:
115
+ @echo "Syncing model developers..."
116
+ @node ./scripts/sync/sync-model-developers.js
117
+
118
+ # --- Utilities ---
119
+
120
+ # Filter and analyze load balance logs
121
+ filter-logs:
122
+ @echo "Filtering load balance logs..."
123
+ @./scripts/utils/filter-load-balance-logs.sh
cmd/axonhub/main.go ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package main
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "fmt"
7
+ "os"
8
+
9
+ "github.com/andreazorzetto/yh/highlight"
10
+ "github.com/hokaccha/go-prettyjson"
11
+ "go.uber.org/fx"
12
+ "go.uber.org/fx/fxevent"
13
+ "gopkg.in/yaml.v3"
14
+
15
+ sdk "go.opentelemetry.io/otel/sdk/metric"
16
+
17
+ "github.com/looplj/axonhub/conf"
18
+ "github.com/looplj/axonhub/internal/build"
19
+ "github.com/looplj/axonhub/internal/ent"
20
+ "github.com/looplj/axonhub/internal/log"
21
+ "github.com/looplj/axonhub/internal/metrics"
22
+ "github.com/looplj/axonhub/internal/server"
23
+ )
24
+
25
+ func main() {
26
+ if len(os.Args) > 1 {
27
+ switch os.Args[1] {
28
+ case "config":
29
+ handleConfigCommand()
30
+ return
31
+ case "version", "--version", "-v":
32
+ showVersion()
33
+ return
34
+ case "help", "--help", "-h":
35
+ showHelp()
36
+ case "build-info":
37
+ showBuildInfo()
38
+ return
39
+ }
40
+ }
41
+
42
+ startServer()
43
+ }
44
+
45
+ func showBuildInfo() {
46
+ fmt.Println(build.GetBuildInfo())
47
+ }
48
+
49
+ type logger struct{}
50
+
51
+ func (l *logger) LogEvent(event fxevent.Event) {
52
+ log.Debug(context.Background(), "fx event", log.Any("event", event))
53
+ }
54
+
55
+ func startServer() {
56
+ server.Run(
57
+ fx.WithLogger(func() fxevent.Logger {
58
+ return &logger{}
59
+ }),
60
+ fx.Provide(conf.Load),
61
+ fx.Provide(metrics.NewProvider),
62
+ fx.Invoke(func(lc fx.Lifecycle, server *server.Server, provider *sdk.MeterProvider, ent *ent.Client) {
63
+ lc.Append(fx.Hook{
64
+ OnStart: func(ctx context.Context) error {
65
+ if provider != nil {
66
+ return metrics.SetupMetrics(provider, server.Config.Name)
67
+ }
68
+
69
+ return nil
70
+ },
71
+ OnStop: func(ctx context.Context) error {
72
+ if provider != nil {
73
+ return provider.Shutdown(ctx)
74
+ }
75
+
76
+ return nil
77
+ },
78
+ })
79
+ lc.Append(fx.Hook{
80
+ OnStart: func(ctx context.Context) error {
81
+ go func() {
82
+ err := server.Run()
83
+ if err != nil {
84
+ log.Error(context.Background(), "server run error:", log.Cause(err))
85
+ os.Exit(1)
86
+ }
87
+ }()
88
+
89
+ return nil
90
+ },
91
+ OnStop: func(ctx context.Context) error {
92
+ err := server.Shutdown(ctx)
93
+ if err != nil {
94
+ log.Error(context.Background(), "server shutdown error:", log.Cause(err))
95
+ }
96
+
97
+ err = ent.Close()
98
+ if err != nil {
99
+ log.Error(context.Background(), "ent close error:", log.Cause(err))
100
+ }
101
+
102
+ return nil
103
+ },
104
+ })
105
+ }),
106
+ )
107
+ }
108
+
109
+ func handleConfigCommand() {
110
+ if len(os.Args) < 3 {
111
+ fmt.Println("Usage: axonhub config <preview|validate|get>")
112
+ os.Exit(1)
113
+ }
114
+
115
+ switch os.Args[2] {
116
+ case "preview":
117
+ configPreview()
118
+ case "validate":
119
+ configValidate()
120
+ case "get":
121
+ configGet()
122
+ default:
123
+ fmt.Println("Usage: axonhub config <preview|validate|get>")
124
+ os.Exit(1)
125
+ }
126
+ }
127
+
128
+ func configPreview() {
129
+ format := "yml"
130
+
131
+ for i := 3; i < len(os.Args); i++ {
132
+ if os.Args[i] == "--format" || os.Args[i] == "-f" {
133
+ if i+1 < len(os.Args) {
134
+ format = os.Args[i+1]
135
+ }
136
+ }
137
+ }
138
+
139
+ config, err := conf.Load()
140
+ if err != nil {
141
+ fmt.Printf("Failed to load config: %v\n", err)
142
+ os.Exit(1)
143
+ }
144
+
145
+ var output string
146
+
147
+ switch format {
148
+ case "json":
149
+ b, err := prettyjson.Marshal(config)
150
+ if err != nil {
151
+ fmt.Printf("Failed to preview config: %v\n", err)
152
+ os.Exit(1)
153
+ }
154
+
155
+ output = string(b)
156
+ case "yml", "yaml":
157
+ b, err := yaml.Marshal(config)
158
+ if err != nil {
159
+ fmt.Printf("Failed to preview config: %v\n", err)
160
+ os.Exit(1)
161
+ }
162
+
163
+ output, err = highlight.Highlight(bytes.NewBuffer(b))
164
+ if err != nil {
165
+ fmt.Printf("Failed to preview config: %v\n", err)
166
+ os.Exit(1)
167
+ }
168
+ default:
169
+ fmt.Printf("Unsupported format: %s\n", format)
170
+ os.Exit(1)
171
+ }
172
+
173
+ fmt.Println(output)
174
+ }
175
+
176
+ func configValidate() {
177
+ config, err := conf.Load()
178
+ if err != nil {
179
+ fmt.Printf("Failed to load config: %v\n", err)
180
+ os.Exit(1)
181
+ }
182
+
183
+ errors := validateConfig(config)
184
+
185
+ if len(errors) == 0 {
186
+ fmt.Println("Configuration is valid!")
187
+ return
188
+ }
189
+
190
+ fmt.Println("Configuration validation failed:")
191
+
192
+ for _, err := range errors {
193
+ fmt.Printf(" - %s\n", err)
194
+ }
195
+
196
+ os.Exit(1)
197
+ }
198
+
199
+ func validateConfig(config conf.Config) []string {
200
+ var errors []string
201
+
202
+ if config.APIServer.Port <= 0 || config.APIServer.Port > 65535 {
203
+ errors = append(errors, "server.port must be between 1 and 65535")
204
+ }
205
+
206
+ if config.DB.DSN == "" {
207
+ errors = append(errors, "db.dsn cannot be empty")
208
+ }
209
+
210
+ if config.Log.Name == "" {
211
+ errors = append(errors, "log.name cannot be empty")
212
+ }
213
+
214
+ if config.APIServer.CORS.Enabled && len(config.APIServer.CORS.AllowedOrigins) == 0 {
215
+ errors = append(errors, "server.cors.allowed_origins cannot be empty when CORS is enabled")
216
+ }
217
+
218
+ return errors
219
+ }
220
+
221
+ func configGet() {
222
+ if len(os.Args) < 4 {
223
+ fmt.Println("Usage: axonhub config get <key>")
224
+ fmt.Println("")
225
+ fmt.Println("Available keys:")
226
+ fmt.Println(" server.port Server port number")
227
+ fmt.Println(" server.name Server name")
228
+ fmt.Println(" db.dialect Database dialect")
229
+ fmt.Println(" db.dsn Database DSN")
230
+ os.Exit(1)
231
+ }
232
+
233
+ key := os.Args[3]
234
+
235
+ config, err := conf.Load()
236
+ if err != nil {
237
+ fmt.Fprintf(os.Stderr, "Failed to load config: %v\n", err)
238
+ os.Exit(1)
239
+ }
240
+
241
+ var value any
242
+
243
+ switch key {
244
+ case "server.port":
245
+ value = config.APIServer.Port
246
+ case "server.name":
247
+ value = config.APIServer.Name
248
+ case "server.base_path":
249
+ value = config.APIServer.BasePath
250
+ case "server.debug":
251
+ value = config.APIServer.Debug
252
+ case "db.dialect":
253
+ value = config.DB.Dialect
254
+ case "db.dsn":
255
+ value = config.DB.DSN
256
+ default:
257
+ fmt.Fprintf(os.Stderr, "Unknown config key: %s\n", key)
258
+ os.Exit(1)
259
+ }
260
+
261
+ fmt.Println(value)
262
+ }
263
+
264
+ func showHelp() {
265
+ fmt.Println("AxonHub AI Gateway")
266
+ fmt.Println("")
267
+ fmt.Println("Usage:")
268
+ fmt.Println(" axonhub Start the server (default)")
269
+ fmt.Println(" axonhub config preview Preview configuration")
270
+ fmt.Println(" axonhub config validate Validate configuration")
271
+ fmt.Println(" axonhub config get <key> Get a specific config value")
272
+ fmt.Println(" axonhub version Show version")
273
+ fmt.Println(" axonhub help Show this help message")
274
+ fmt.Println("")
275
+ fmt.Println("Options:")
276
+ fmt.Println(" -f, --format FORMAT Output format for config preview (yml, json)")
277
+ }
278
+
279
+ func showVersion() {
280
+ fmt.Println(build.Version)
281
+ }
conf/conf.go ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package conf
2
+
3
+ import (
4
+ "context"
5
+ "encoding"
6
+ "errors"
7
+ "fmt"
8
+ "reflect"
9
+ "strings"
10
+ "time"
11
+
12
+ "github.com/go-viper/mapstructure/v2"
13
+ "github.com/spf13/viper"
14
+ "go.uber.org/fx"
15
+ "go.uber.org/zap/zapcore"
16
+
17
+ "github.com/looplj/axonhub/internal/log"
18
+ "github.com/looplj/axonhub/internal/metrics"
19
+ "github.com/looplj/axonhub/internal/pkg/xcache"
20
+ "github.com/looplj/axonhub/internal/server"
21
+ "github.com/looplj/axonhub/internal/server/db"
22
+ "github.com/looplj/axonhub/internal/server/gc"
23
+ )
24
+
25
+ type Config struct {
26
+ fx.Out `yaml:"-" json:"-"`
27
+
28
+ DB db.Config `conf:"db" yaml:"db" json:"db"`
29
+ Log log.Config `conf:"log" yaml:"log" json:"log"`
30
+ APIServer server.Config `conf:"server" yaml:"server" json:"server"`
31
+ Metrics metrics.Config `conf:"metrics" yaml:"metrics" json:"metrics"`
32
+ GC gc.Config `conf:"gc" yaml:"gc" json:"gc"`
33
+ Cache xcache.Config `conf:"cache" yaml:"cache" json:"cache"`
34
+ ProviderQuota providerQuotaConfig `conf:"provider_quota" yaml:"provider_quota" json:"provider_quota"`
35
+ }
36
+
37
+ type providerQuotaConfig struct {
38
+ CheckInterval time.Duration `conf:"check_interval" yaml:"check_interval" json:"check_interval"`
39
+ }
40
+
41
+ // Load loads configuration from YAML file and environment variables.
42
+ func Load() (Config, error) {
43
+ v := viper.New()
44
+
45
+ // Set config file name and paths
46
+ v.SetConfigName("config")
47
+ v.SetConfigType("yml")
48
+ v.AddConfigPath(".")
49
+ v.AddConfigPath("/etc/axonhub/")
50
+ v.AddConfigPath("$HOME/.config/axonhub/")
51
+ v.AddConfigPath("./conf")
52
+
53
+ // Enable environment variable support
54
+ v.AutomaticEnv()
55
+ v.SetEnvPrefix("AXONHUB")
56
+ v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
57
+
58
+ // Set default values
59
+ setDefaults(v)
60
+
61
+ // Read config file
62
+ if err := v.ReadInConfig(); err != nil {
63
+ var configFileNotFoundError viper.ConfigFileNotFoundError
64
+ if !errors.As(err, &configFileNotFoundError) {
65
+ return Config{}, fmt.Errorf("failed to read config file: %w", err)
66
+ }
67
+ // Config file not found, use defaults and environment variables
68
+ }
69
+
70
+ // Parse log level from string before unmarshaling
71
+ logLevelStr := v.GetString("log.level")
72
+
73
+ logLevel, err := parseLogLevel(logLevelStr)
74
+ if err != nil {
75
+ return Config{}, fmt.Errorf("invalid log level '%s': %w", logLevelStr, err)
76
+ }
77
+ // Set the parsed log level back to viper for unmarshaling
78
+ v.Set("log.level", int(logLevel))
79
+
80
+ // Unmarshal config
81
+ var config Config
82
+ if err := v.Unmarshal(&config, func(dc *mapstructure.DecoderConfig) {
83
+ dc.DecodeHook = customizedDecodeHook
84
+ dc.TagName = "conf"
85
+ }); err != nil {
86
+ return Config{}, fmt.Errorf("failed to unmarshal config: %w", err)
87
+ }
88
+
89
+ log.Debug(context.Background(), "Config loaded successfully", log.Any("config", config))
90
+
91
+ return config, nil
92
+ }
93
+
94
+ var (
95
+ _TypeTextUnmarshaler = reflect.TypeFor[encoding.TextUnmarshaler]()
96
+ _TypeDuration = reflect.TypeFor[time.Duration]()
97
+ )
98
+
99
+ func customizedDecodeHook(srcType reflect.Type, dstType reflect.Type, data any) (any, error) {
100
+ str, ok := data.(string)
101
+ if !ok {
102
+ return data, nil
103
+ }
104
+
105
+ switch {
106
+ case reflect.PointerTo(dstType).Implements(_TypeTextUnmarshaler):
107
+ value := reflect.New(dstType)
108
+
109
+ u, _ := value.Interface().(encoding.TextUnmarshaler)
110
+ if err := u.UnmarshalText([]byte(str)); err != nil {
111
+ return nil, err
112
+ }
113
+
114
+ return u, nil
115
+ case dstType == _TypeDuration:
116
+ if strings.TrimSpace(str) == "" {
117
+ return time.Duration(0), nil
118
+ }
119
+ return time.ParseDuration(str)
120
+ default:
121
+ return data, nil
122
+ }
123
+ }
124
+
125
+ // setDefaults sets default configuration values.
126
+ func setDefaults(v *viper.Viper) {
127
+ // Server defaults
128
+ v.SetDefault("server.host", "0.0.0.0")
129
+ v.SetDefault("server.port", 8090)
130
+ v.SetDefault("server.name", "AxonHub")
131
+ v.SetDefault("server.base_path", "")
132
+ v.SetDefault("server.request_timeout", "30s")
133
+ v.SetDefault("server.llm_request_timeout", "600s")
134
+ v.SetDefault("server.trace.thread_header", "AH-Thread-Id")
135
+ v.SetDefault("server.trace.trace_header", "AH-Trace-Id")
136
+ v.SetDefault("server.trace.extra_trace_headers", []string{})
137
+ v.SetDefault("server.trace.extra_trace_body_fields", []string{})
138
+ v.SetDefault("server.trace.claude_code_trace_enabled", false)
139
+ v.SetDefault("server.trace.codex_trace_enabled", false)
140
+ v.SetDefault("server.debug", false)
141
+
142
+ // CORS defaults
143
+ v.SetDefault("server.cors.enabled", false)
144
+ v.SetDefault("server.cors.debug", false)
145
+ v.SetDefault("server.cors.allowed_origins", []string{"http://localhost:8090"})
146
+ v.SetDefault("server.cors.allowed_methods", []string{"GET", "POST", "DELETE", "PATCH", "PUT", "OPTIONS", "HEAD"})
147
+ v.SetDefault("server.cors.allowed_headers", []string{"Content-Type", "Authorization", "X-API-Key", "X-Goog-Api-Key", "X-Project-ID", "X-Thread-ID", "X-Trace-ID"})
148
+ v.SetDefault("server.cors.exposed_headers", []string{})
149
+ v.SetDefault("server.cors.allow_credentials", false)
150
+ v.SetDefault("server.cors.max_age", "30m")
151
+
152
+ // Database defaults
153
+ v.SetDefault("db.dialect", "sqlite3")
154
+ v.SetDefault("db.dsn", "file:axonhub.db?cache=shared&_fk=1&journal_mode=WAL")
155
+ v.SetDefault("db.debug", false)
156
+
157
+ // Log defaults
158
+ v.SetDefault("log.name", "axonhub")
159
+ v.SetDefault("log.debug", false)
160
+ v.SetDefault("log.skip_level", 1)
161
+ v.SetDefault("log.level", "info")
162
+ v.SetDefault("log.level_key", "level")
163
+ v.SetDefault("log.time_key", "time")
164
+ v.SetDefault("log.caller_key", "label")
165
+ v.SetDefault("log.function_key", "")
166
+ v.SetDefault("log.name_key", "logger")
167
+ v.SetDefault("log.encoding", "json")
168
+ v.SetDefault("log.includes", []string{})
169
+ v.SetDefault("log.excludes", []string{})
170
+ v.SetDefault("log.output", "stdio")
171
+ v.SetDefault("log.file.path", "logs/axonhub.log")
172
+ v.SetDefault("log.file.max_size", 100) // MB
173
+ v.SetDefault("log.file.max_age", 30) // days
174
+ v.SetDefault("log.file.max_backups", 10) // files
175
+ v.SetDefault("log.file.local_time", true)
176
+
177
+ // Metrics defaults
178
+ v.SetDefault("metrics.enabled", false)
179
+
180
+ // GC defaults
181
+ v.SetDefault("gc.cron", "0 2 * * *") // Daily at 2:00 AM
182
+
183
+ // Provider quota defaults
184
+ v.SetDefault("provider_quota.check_interval", "20m") // Check every 20 minutes
185
+
186
+ // Cache defaults
187
+ v.SetDefault("cache.mode", "memory")
188
+ v.SetDefault("cache.default_expiration", "5m")
189
+ v.SetDefault("cache.cleanup_interval", "10m")
190
+ v.SetDefault("cache.redis.addr", "")
191
+ v.SetDefault("cache.redis.url", "")
192
+ v.SetDefault("cache.redis.username", "")
193
+ v.SetDefault("cache.redis.password", "")
194
+ // Note: cache.redis.db has no default value to allow explicit override to 0
195
+ v.SetDefault("cache.redis.tls", false)
196
+ v.SetDefault("cache.redis.tls_insecure_skip_verify", false)
197
+ }
198
+
199
+ // parseLogLevel converts a string log level to zapcore.Level.
200
+ func parseLogLevel(level string) (zapcore.Level, error) {
201
+ switch strings.ToLower(level) {
202
+ case "debug":
203
+ return zapcore.DebugLevel, nil
204
+ case "info":
205
+ return zapcore.InfoLevel, nil
206
+ case "warn", "warning":
207
+ return zapcore.WarnLevel, nil
208
+ case "error":
209
+ return zapcore.ErrorLevel, nil
210
+ case "panic":
211
+ return zapcore.PanicLevel, nil
212
+ case "fatal":
213
+ return zapcore.FatalLevel, nil
214
+ default:
215
+ return zapcore.InfoLevel, fmt.Errorf("unknown log level: %s", level)
216
+ }
217
+ }
config.example.yml ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AxonHub Configuration Example
2
+ # Copy this file to config.yml and modify as needed
3
+
4
+ # Server configuration
5
+ server:
6
+ host: "0.0.0.0" # Server host (env: AXONHUB_SERVER_HOST)
7
+ port: 8090 # Server port (env: AXONHUB_SERVER_PORT)
8
+ name: "AxonHub" # Server name (env: AXONHUB_SERVER_NAME)
9
+ base_path: "" # Base path for API routes (env: AXONHUB_SERVER_BASE_PATH)
10
+ request_timeout: "30s" # Request timeout duration (env: AXONHUB_SERVER_REQUEST_TIMEOUT)
11
+ llm_request_timeout: "600s" # LLM request timeout duration (env: AXONHUB_SERVER_LLM_REQUEST_TIMEOUT)
12
+ trace:
13
+ thread_header: "AH-Thread-Id" # Thread ID header name (env: AXONHUB_SERVER_TRACE_THREAD_HEADER)
14
+ trace_header: "AH-Trace-Id" # Trace ID header name (env: AXONHUB_SERVER_TRACE_TRACE_HEADER)
15
+ extra_trace_headers: [] # Extra trace headers (env: AXONHUB_SERVER_TRACE_EXTRA_TRACE_HEADERS)
16
+ extra_trace_body_fields: [] # Extra trace body fields (env: AXONHUB_SERVER_TRACE_EXTRA_TRACE_BODY_FIELDS)
17
+ claude_code_trace_enabled: false # Enable extracting trace IDs from Claude Code request metadata (env: AXONHUB_SERVER_TRACE_CLAUDE_CODE_TRACE_ENABLED)
18
+ codex_trace_enabled: false # Enable extracting trace IDs from Codex Session_id header (env: AXONHUB_SERVER_TRACE_CODEX_TRACE_ENABLED)
19
+ debug: false # Enable debug mode (env: AXONHUB_SERVER_DEBUG)
20
+ cors:
21
+ enabled: true # Enable CORS middleware (env: AXONHUB_SERVER_CORS_ENABLED)
22
+ debug: false # Enable CORS debug logging (env: AXONHUB_SERVER_CORS_DEBUG)
23
+ allowed_origins: # Allowed origins for CORS (env: AXONHUB_SERVER_CORS_ALLOWED_ORIGINS)
24
+ - "http://localhost:3000"
25
+ - "http://localhost:5173"
26
+ allowed_methods: # Allowed HTTP methods (env: AXONHUB_SERVER_CORS_ALLOWED_METHODS)
27
+ - "GET"
28
+ - "POST"
29
+ - "DELETE"
30
+ - "PATCH"
31
+ - "PUT"
32
+ - "OPTIONS"
33
+ - "HEAD"
34
+ allowed_headers: # Allowed headers (env: AXONHUB_SERVER_CORS_ALLOWED_HEADERS)
35
+ - "Content-Type"
36
+ - "Authorization"
37
+ - "X-API-Key"
38
+ - "X-Goog-Api-Key"
39
+ - "X-Project-ID"
40
+ - "AH-Thread-Id"
41
+ - "AH-Trace-Id"
42
+ # Add any other headers your frontend sends
43
+ exposed_headers: [] # Exposed headers (env: AXONHUB_SERVER_CORS_EXPOSED_HEADERS)
44
+ allow_credentials: true # Allow credentials (env: AXONHUB_SERVER_CORS_ALLOW_CREDENTIALS)
45
+ max_age: 1m # Max age for preflight cache in seconds (env: AXONHUB_SERVER_CORS_MAX_AGE)
46
+
47
+ # Database configuration
48
+ db:
49
+ dialect: "sqlite3" # Database dialect: sqlite3, postgres, mysql (env: AXONHUB_DB_DIALECT)
50
+ dsn: "file:axonhub.db?cache=shared&_fk=1" # Database connection string (env: AXONHUB_DB_DSN)
51
+ debug: false # Enable database debug logging (env: AXONHUB_DB_DEBUG)
52
+
53
+ # Cache configuration
54
+ cache:
55
+ # One of: memory, redis, two-level (memory + redis)
56
+ mode: "memory" # (env: AXONHUB_CACHE_MODE)
57
+
58
+ # Memory cache configuration
59
+ memory:
60
+ expiration: "5s" # Memory cache TTL (env: AXONHUB_CACHE_MEMORY_EXPIRATION)
61
+ cleanup_interval: "10m" # Memory cache cleanup interval (env: AXONHUB_CACHE_MEMORY_CLEANUP_INTERVAL)
62
+
63
+ # Redis cache configuration
64
+ redis:
65
+ # Simple mode: Use addr for plain host:port
66
+ addr: "" # Redis address (host:port) (env: AXONHUB_CACHE_REDIS_ADDR)
67
+
68
+ # URL mode: Use url for full redis:// or rediss:// URLs (takes priority over addr)
69
+ # Supported formats:
70
+ # - "redis://127.0.0.1:6379"
71
+ # - "rediss://127.0.0.1:6380" (auto-enables TLS)
72
+ # - "redis://user:pass@127.0.0.1:6379/0"
73
+ url: "" # Redis URL (env: AXONHUB_CACHE_REDIS_URL)
74
+
75
+ # Optional overrides (apply to both addr and url modes)
76
+ username: "" # Overrides URL username (env: AXONHUB_CACHE_REDIS_USERNAME)
77
+ password: "" # Overrides URL password (env: AXONHUB_CACHE_REDIS_PASSWORD)
78
+ db: 0 # Overrides URL /db (env: AXONHUB_CACHE_REDIS_DB)
79
+ tls: false # Enable TLS for addr mode (env: AXONHUB_CACHE_REDIS_TLS)
80
+ tls_insecure_skip_verify: false # Skip TLS cert verification (env: AXONHUB_CACHE_REDIS_TLS_INSECURE_SKIP_VERIFY)
81
+ expiration: "30m" # Redis cache TTL (env: AXONHUB_CACHE_REDIS_EXPIRATION)
82
+
83
+ # Logging configuration
84
+ log:
85
+ name: "axonhub" # Logger name (env: AXONHUB_LOG_NAME)
86
+ debug: false # Enable debug logging (env: AXONHUB_LOG_DEBUG)
87
+ level: "info" # Log level: debug, info, warn, error, panic, fatal (env: AXONHUB_LOG_LEVEL)
88
+ level_key: "level" # Key name for log level field (env: AXONHUB_LOG_LEVEL_KEY)
89
+ time_key: "time" # Key name for timestamp field (env: AXONHUB_LOG_TIME_KEY)
90
+ caller_key: "label" # Key name for caller info field (env: AXONHUB_LOG_CALLER_KEY)
91
+ function_key: "" # Key name for function field (env: AXONHUB_LOG_FUNCTION_KEY)
92
+ name_key: "logger" # Key name for logger name field (env: AXONHUB_LOG_NAME_KEY)
93
+ encoding: "json" # Log encoding: json, console, console_json (env: AXONHUB_LOG_ENCODING)
94
+ includes: [] # Logger names to include (env: AXONHUB_LOG_INCLUDES)
95
+ excludes: [] # Logger names to exclude (env: AXONHUB_LOG_EXCLUDES)
96
+ output: "stdio" # Output target: file or stdio (env: AXONHUB_LOG_OUTPUT)
97
+ file: # File-based logging configuration (env prefix: AXONHUB_LOG_FILE_*)
98
+ path: "logs/axonhub.log" # Log file path (env: AXONHUB_LOG_FILE_PATH)
99
+ max_size: 100 # Max size in MB before rotation (env: AXONHUB_LOG_FILE_MAX_SIZE)
100
+ max_age: 30 # Max age in days to retain (env: AXONHUB_LOG_FILE_MAX_AGE)
101
+ max_backups: 10 # Max number of old log files to retain (env: AXONHUB_LOG_FILE_MAX_BACKUPS)
102
+ local_time: true # Use local time for rotated files (env: AXONHUB_LOG_FILE_LOCAL_TIME)
103
+
104
+ # Metrics configuration
105
+ metrics:
106
+ enabled: false # Enable metrics collection (env: AXONHUB_METRICS_ENABLED)
107
+ exporter:
108
+ type: "oltphttp" # Metrics exporter type: prometheus, console (env: AXONHUB_METRICS_EXPORTER_TYPE)
109
+ endpoint: "localhost:8080" # Metrics exporter endpoint (env: AXONHUB_METRICS_EXPORTER_ENDPOINT)
110
+ insecure: true # Enable insecure connection (env: AXONHUB_METRICS_EXPORTER_INSECURE)
111
+
112
+
113
+ # Garbage Collection configuration
114
+ gc:
115
+ cron: "0 2 * * *" # Cron expression for GC execution (env: AXONHUB_GC_CRON)
116
+ # This example runs daily at 2:00 AM
117
+ # Format: minute hour day month day_of_week
118
+ # Examples:
119
+ # "0 2 * * *" - Daily at 2:00 AM
120
+ # "0 3 * * 0" - Weekly on Sunday at 3:00 AM
121
+ # "0 4 1 * *" - Monthly on 1st day at 4:00 AM
122
+
123
+ # Provider Quota configuration
124
+ provider_quota:
125
+ check_interval: "20m" # Interval for checking provider quota status (env: AXONHUB_PROVIDER_QUOTA_CHECK_INTERVAL)
126
+ # Supported values: 1m, 2m, 3m, 4m, 5m, 6m, 10m, 12m, 15m, 20m, 30m, 1h, 2h, etc.
127
+ # Default: 20m
128
+ # Longer intervals reduce API calls but show less fresh quota data
129
+
130
+ # Environment Variable Examples:
131
+ # export AXONHUB_SERVER_PORT=8080
132
+ # export AXONHUB_DB_DSN="postgres://user:pass@localhost/axonhub?sslmode=disable"
133
+ # export AXONHUB_LOG_LEVEL="debug"
134
+ # export AXONHUB_LOG_ENCODING="console"
135
+ # export AXONHUB_LOG_OUTPUT="stdio" # or "file"
136
+ # export AXONHUB_LOG_FILE_PATH="/var/log/axonhub/axonhub.log"
137
+ # export AXONHUB_LOG_FILE_MAX_SIZE=200
138
+ # export AXONHUB_LOG_FILE_MAX_AGE=14
139
+ # export AXONHUB_LOG_FILE_MAX_BACKUPS=7
140
+ # export AXONHUB_SERVER_TRACE_HEADER="X-Trace-ID"
141
+ # export AXONHUB_METRICS_ENABLED=false
142
+ # export AXONHUB_GC_CRON="0 3 * * 0" # Run weekly on Sunday at 3:00 AM
deploy/axonhub.service ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [Unit]
2
+ Description=AxonHub AI Gateway
3
+ After=network.target
4
+
5
+ [Service]
6
+ Type=simple
7
+ User=axonhub
8
+ WorkingDirectory=/opt/axonhub
9
+ ExecStart=/opt/axonhub/axonhub
10
+ Restart=always
11
+ RestartSec=5
12
+
13
+ [Install]
14
+ WantedBy=multi-user.target
deploy/helm/.helmignore ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Patterns to ignore when building packages.
2
+ # This supports shell glob matching, relative path matching, and
3
+ # negation (prefixed with !). Only one pattern per line.
4
+ .DS_Store
5
+ # Common VCS dirs
6
+ .git/
7
+ .gitignore
8
+ .bzr/
9
+ .bzrignore
10
+ .hg/
11
+ .hgignore
12
+ .svn/
13
+ # Common backup files
14
+ *.swp
15
+ *.bak
16
+ *.tmp
17
+ *.orig
18
+ *~
19
+ # Various IDEs
20
+ .project
21
+ .idea/
22
+ *.tmproj
23
+ .vscode/
deploy/helm/Chart.yaml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ apiVersion: v2
2
+ name: axonhub
3
+ version: 0.1.0
4
+ appVersion: "latest"
5
+ description: A Helm chart for deploying AxonHub on Kubernetes
6
+ type: application
7
+ home: https://github.com/looplj/axonhub
8
+ sources:
9
+ - https://github.com/looplj/axonhub
10
+ maintainers:
11
+ - name: AxonHub Team
12
+ email: team@axonhub.ai
deploy/helm/README.md ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AxonHub Helm Chart
2
+
3
+ This Helm chart deploys AxonHub on Kubernetes with PostgreSQL database.
4
+
5
+ ## Prerequisites
6
+
7
+ - Kubernetes 1.19+
8
+ - Helm 3.0+
9
+ - PV provisioner support in the underlying infrastructure (for persistence)
10
+
11
+ ## Installing the Chart
12
+
13
+ To install the chart with the release name `axonhub`:
14
+
15
+ ```bash
16
+ helm install axonhub ./deploy/helm
17
+ ```
18
+
19
+ ## Configuration
20
+
21
+ The following table lists the configurable parameters of the AxonHub chart and their default values.
22
+
23
+ ### Global Parameters
24
+
25
+ | Parameter | Description | Default |
26
+ |-----------|-------------|---------|
27
+ | `global.imagePullSecrets` | Global Docker registry secret names as an array | `[]` |
28
+
29
+ ### AxonHub Parameters
30
+
31
+ | Parameter | Description | Default |
32
+ |-----------|-------------|---------|
33
+ | `axonhub.replicaCount` | Number of AxonHub replicas | `1` |
34
+ | `axonhub.image.repository` | AxonHub image repository | `looplj/axonhub` |
35
+ | `axonhub.image.tag` | AxonHub image tag | `latest` |
36
+ | `axonhub.image.pullPolicy` | Image pull policy | `IfNotPresent` |
37
+ | `axonhub.dbPassword` | Database password | `axonhub_password` |
38
+ | `axonhub.service.type` | Kubernetes service type | `ClusterIP` |
39
+ | `axonhub.service.port` | Service port | `8090` |
40
+ | `axonhub.resources` | CPU/Memory resource requests/limits | `{}` |
41
+ | `axonhub.persistence.enabled` | Enable persistence using PVC | `false` |
42
+ | `axonhub.persistence.size` | PVC storage request size | `10Gi` |
43
+
44
+ ### PostgreSQL Parameters
45
+
46
+ | Parameter | Description | Default |
47
+ |-----------|-------------|---------|
48
+ | `postgresql.enabled` | Deploy PostgreSQL chart | `true` |
49
+ | `postgresql.replicaCount` | Number of PostgreSQL replicas | `1` |
50
+ | `postgresql.image.repository` | PostgreSQL image repository | `postgres` |
51
+ | `postgresql.image.tag` | PostgreSQL image tag | `16-alpine` |
52
+ | `postgresql.auth.postgresPassword` | PostgreSQL admin password | `axonhub_password` |
53
+ | `postgresql.auth.username` | PostgreSQL user name | `axonhub` |
54
+ | `postgresql.auth.password` | PostgreSQL user password | `axonhub_password` |
55
+ | `postgresql.auth.database` | PostgreSQL database name | `axonhub` |
56
+ | `postgresql.service.type` | Kubernetes service type | `ClusterIP` |
57
+ | `postgresql.service.port` | PostgreSQL service port | `5432` |
58
+ | `postgresql.primary.persistence.enabled` | Enable PostgreSQL persistence | `true` |
59
+ | `postgresql.primary.persistence.size` | PVC storage request size | `8Gi` |
60
+
61
+ ### Ingress Parameters
62
+
63
+ | Parameter | Description | Default |
64
+ |-----------|-------------|---------|
65
+ | `ingress.enabled` | Enable ingress controller resource | `false` |
66
+ | `ingress.className` | IngressClass that will be used | `""` |
67
+ | `ingress.annotations` | Ingress annotations | `{}` |
68
+ | `ingress.hosts` | Ingress hostnames | `[{host: axonhub.local, paths: [{path: /, pathType: Prefix}]}]` |
69
+ | `ingress.tls` | Ingress TLS configuration | `[]` |
70
+
71
+ ## Database Configuration Options
72
+
73
+ ### Using Internal PostgreSQL (Default)
74
+
75
+ The chart includes PostgreSQL by default. This is suitable for:
76
+ - Development environments
77
+ - Small production deployments
78
+ - When you want managed database within Kubernetes
79
+
80
+ ```yaml
81
+ postgresql:
82
+ enabled: true # Default setting
83
+ ```
84
+
85
+ ### Using External Database
86
+
87
+ Disable internal PostgreSQL and configure external database connection:
88
+
89
+ ```yaml
90
+ postgresql:
91
+ enabled: false
92
+
93
+ axonhub:
94
+ env:
95
+ AXONHUB_DB_DSN: "postgres://username:password@external-db-host:5432/database?sslmode=require"
96
+ ```
97
+
98
+ ## Production Deployment
99
+
100
+ For production deployments, you should:
101
+
102
+ 1. Change default passwords:
103
+ ```yaml
104
+ axonhub:
105
+ dbPassword: "your-secure-password"
106
+
107
+ postgresql:
108
+ auth:
109
+ postgresPassword: "your-secure-postgres-password"
110
+ password: "your-secure-password"
111
+ ```
112
+
113
+ 2. Enable persistence:
114
+ ```yaml
115
+ axonhub:
116
+ persistence:
117
+ enabled: true
118
+ size: 20Gi
119
+
120
+ postgresql:
121
+ primary:
122
+ persistence:
123
+ enabled: true
124
+ size: 20Gi
125
+ ```
126
+
127
+ 3. Configure resource limits:
128
+ ```yaml
129
+ axonhub:
130
+ resources:
131
+ limits:
132
+ cpu: 2000m
133
+ memory: 2Gi
134
+ requests:
135
+ cpu: 1000m
136
+ memory: 1Gi
137
+
138
+ postgresql:
139
+ resources:
140
+ limits:
141
+ cpu: 1000m
142
+ memory: 1Gi
143
+ requests:
144
+ cpu: 500m
145
+ memory: 512Mi
146
+ ```
147
+
148
+ 4. Enable ingress for external access:
149
+ ```yaml
150
+ ingress:
151
+ enabled: true
152
+ className: "nginx"
153
+ hosts:
154
+ - host: axonhub.yourdomain.com
155
+ paths:
156
+ - path: /
157
+ pathType: Prefix
158
+ ```
159
+
160
+ ## Upgrading
161
+
162
+ To upgrade the chart:
163
+
164
+ ```bash
165
+ helm upgrade axonhub ./deploy/helm -f values-production.yaml
166
+ ```
167
+
168
+ ## Uninstalling
169
+
170
+ To uninstall/delete the release:
171
+
172
+ ```bash
173
+ helm uninstall axonhub
174
+ ```
175
+
176
+ ## Verification
177
+
178
+ After installation, you can verify the deployment:
179
+
180
+ ```bash
181
+ # Check pods status
182
+ kubectl get pods
183
+
184
+ # Check services
185
+ kubectl get svc
186
+
187
+ # Port forward to test
188
+ kubectl port-forward svc/axonhub 8090:8090
189
+
190
+ # Test health endpoint
191
+ curl http://localhost:8090/health
192
+ ```
193
+
194
+ ## Troubleshooting
195
+
196
+ Common issues and solutions:
197
+
198
+ 1. **Database connection failed**: Check PostgreSQL pod status and logs
199
+ 2. **Insufficient resources**: Adjust resource requests/limits in values.yaml
200
+ 3. **Persistent volume issues**: Ensure your cluster has PV provisioner configured
201
+ 4. **Ingress not working**: Verify ingress controller is installed and configured
202
+
203
+ ## Architecture
204
+
205
+ The chart deploys:
206
+ - AxonHub application as a Deployment
207
+ - PostgreSQL database as a StatefulSet
208
+ - Services for both components
209
+ - Optional ingress for external access
210
+ - Persistent volumes for data persistence
deploy/helm/install.bat ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ setlocal
3
+
4
+ set RELEASE_NAME=%1
5
+ if "%RELEASE_NAME%"=="" set RELEASE_NAME=axonhub
6
+
7
+ set NAMESPACE=%2
8
+ if "%NAMESPACE%"=="" set NAMESPACE=default
9
+
10
+ echo Installing AxonHub with Helm...
11
+ echo Release name: %RELEASE_NAME%
12
+ echo Namespace: %NAMESPACE%
13
+
14
+ REM Create namespace if it doesn't exist
15
+ kubectl create namespace %NAMESPACE% 2>nul || echo Namespace already exists
16
+
17
+ REM Install the chart
18
+ helm install %RELEASE_NAME% ./deploy/helm ^
19
+ --namespace %NAMESPACE% ^
20
+ --timeout 10m0s
21
+
22
+ echo.
23
+ echo Installation completed!
24
+ echo.
25
+ echo To access AxonHub:
26
+ echo 1. Port forward the service:
27
+ echo kubectl port-forward svc/%RELEASE_NAME% 8090:8090 -n %NAMESPACE%
28
+ echo.
29
+ echo 2. Visit http://localhost:8090 in your browser
30
+ echo.
31
+ echo To check the status:
32
+ echo kubectl get pods -n %NAMESPACE%
33
+ echo.
34
+ echo To view logs:
35
+ echo kubectl logs -l app.kubernetes.io/name=axonhub -n %NAMESPACE%
36
+ echo.
37
+ echo To uninstall:
38
+ echo helm uninstall %RELEASE_NAME% -n %NAMESPACE%
deploy/helm/install.sh ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # AxonHub Helm Installation Script
4
+
5
+ set -e
6
+
7
+ RELEASE_NAME=${1:-axonhub}
8
+ NAMESPACE=${2:-default}
9
+
10
+ echo "Installing AxonHub with Helm..."
11
+ echo "Release name: $RELEASE_NAME"
12
+ echo "Namespace: $NAMESPACE"
13
+
14
+ # Create namespace if it doesn't exist
15
+ kubectl create namespace $NAMESPACE 2>/dev/null || true
16
+
17
+ # Install the chart
18
+ helm install $RELEASE_NAME ./deploy/helm \
19
+ --namespace $NAMESPACE \
20
+ --timeout 10m0s
21
+
22
+ echo ""
23
+ echo "Installation completed!"
24
+ echo ""
25
+ echo "To access AxonHub:"
26
+ echo "1. Port forward the service:"
27
+ echo " kubectl port-forward svc/$RELEASE_NAME 8090:8090 -n $NAMESPACE"
28
+ echo ""
29
+ echo "2. Visit http://localhost:8090 in your browser"
30
+ echo ""
31
+ echo "To check the status:"
32
+ echo " kubectl get pods -n $NAMESPACE"
33
+ echo ""
34
+ echo "To view logs:"
35
+ echo " kubectl logs -l app.kubernetes.io/name=axonhub -n $NAMESPACE"
36
+ echo ""
37
+ echo "To uninstall:"
38
+ echo " helm uninstall $RELEASE_NAME -n $NAMESPACE"
deploy/helm/templates/NOTES.txt ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Thank you for installing AxonHub!
2
+
3
+ 1. Get the application URL by running these commands:
4
+ {{- if .Values.ingress.enabled }}
5
+ {{- range $host := .Values.ingress.hosts }}
6
+ {{- range .paths }}
7
+ http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
8
+ {{- end }}
9
+ {{- end }}
10
+ {{- else if contains "NodePort" .Values.axonhub.service.type }}
11
+ export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "axonhub.fullname" . }})
12
+ export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
13
+ echo http://$NODE_IP:$NODE_PORT
14
+ {{- else if contains "LoadBalancer" .Values.axonhub.service.type }}
15
+ NOTE: It may take a few minutes for the LoadBalancer IP to be available.
16
+ You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "axonhub.fullname" . }}'
17
+ export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "axonhub.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
18
+ echo http://$SERVICE_IP:{{ .Values.axonhub.service.port }}
19
+ {{- else if contains "ClusterIP" .Values.axonhub.service.type }}
20
+ export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "axonhub.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
21
+ echo "Visit http://127.0.0.1:8090 to use your application"
22
+ kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8090:8090
23
+ {{- end }}
24
+
25
+ 2. Check the status of your deployment:
26
+ kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "axonhub.name" . }}"
27
+
28
+ 3. View logs:
29
+ kubectl logs --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "axonhub.name" . }}" -c axonhub
30
+
31
+ 4. Access the application health endpoint:
32
+ kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ include "axonhub.fullname" . }} 8090:8090
33
+ # Then visit http://localhost:8090/health
34
+
35
+ {{- if .Values.postgresql.enabled }}
36
+ 5. PostgreSQL database is available at:
37
+ Host: {{ include "axonhub.postgresql.serviceName" . }}.{{ .Release.Namespace }}.svc.cluster.local
38
+ Port: {{ .Values.postgresql.service.port }}
39
+ Database: {{ .Values.postgresql.auth.database }}
40
+ Username: {{ .Values.postgresql.auth.username }}
41
+ {{- end }}
42
+
43
+ WARNING: Default passwords are used. Please change them in production environments!
deploy/helm/templates/_helpers.tpl ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{/*
2
+ Expand the name of the chart.
3
+ */}}
4
+ {{- define "axonhub.name" -}}
5
+ {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
6
+ {{- end }}
7
+
8
+ {{/*
9
+ Create a default fully qualified app name.
10
+ We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
11
+ If release name contains chart name it will be used as a full name.
12
+ */}}
13
+ {{- define "axonhub.fullname" -}}
14
+ {{- if .Values.fullnameOverride }}
15
+ {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
16
+ {{- else }}
17
+ {{- $name := default .Chart.Name .Values.nameOverride }}
18
+ {{- if contains $name .Release.Name }}
19
+ {{- .Release.Name | trunc 63 | trimSuffix "-" }}
20
+ {{- else }}
21
+ {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
22
+ {{- end }}
23
+ {{- end }}
24
+ {{- end }}
25
+
26
+ {{/*
27
+ Create chart name and version as used by the chart label.
28
+ */}}
29
+ {{- define "axonhub.chart" -}}
30
+ {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
31
+ {{- end }}
32
+
33
+ {{/*
34
+ Common labels
35
+ */}}
36
+ {{- define "axonhub.labels" -}}
37
+ helm.sh/chart: {{ include "axonhub.chart" . }}
38
+ {{ include "axonhub.selectorLabels" . }}
39
+ {{- if .Chart.AppVersion }}
40
+ app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
41
+ {{- end }}
42
+ app.kubernetes.io/managed-by: {{ .Release.Service }}
43
+ {{- end }}
44
+
45
+ {{/*
46
+ Selector labels
47
+ */}}
48
+ {{- define "axonhub.selectorLabels" -}}
49
+ app.kubernetes.io/name: {{ include "axonhub.name" . }}
50
+ app.kubernetes.io/instance: {{ .Release.Name }}
51
+ {{- end }}
52
+
53
+ {{/*
54
+ Create the name of the service account to use
55
+ */}}
56
+ {{- define "axonhub.serviceAccountName" -}}
57
+ {{- if .Values.serviceAccount.create }}
58
+ {{- default (include "axonhub.fullname" .) .Values.serviceAccount.name }}
59
+ {{- else }}
60
+ {{- default "default" .Values.serviceAccount.name }}
61
+ {{- end }}
62
+ {{- end }}
63
+
64
+ {{/*
65
+ PostgreSQL fullname
66
+ */}}
67
+ {{- define "axonhub.postgresql.fullname" -}}
68
+ {{- printf "%s-postgresql" (include "axonhub.fullname" .) | trunc 63 | trimSuffix "-" }}
69
+ {{- end }}
70
+
71
+ {{/*
72
+ PostgreSQL service name
73
+ */}}
74
+ {{- define "axonhub.postgresql.serviceName" -}}
75
+ {{- include "axonhub.postgresql.fullname" . }}
76
+ {{- end }}
deploy/helm/templates/data-pvc.yaml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{- if .Values.axonhub.persistence.enabled }}
2
+ apiVersion: v1
3
+ kind: PersistentVolumeClaim
4
+ metadata:
5
+ name: {{ include "axonhub.fullname" . }}-data
6
+ labels:
7
+ {{- include "axonhub.labels" . | nindent 4 }}
8
+ app.kubernetes.io/component: axonhub
9
+ spec:
10
+ accessModes:
11
+ {{- toYaml .Values.axonhub.persistence.accessModes | nindent 4 }}
12
+ resources:
13
+ requests:
14
+ storage: {{ .Values.axonhub.persistence.size }}
15
+ {{- if .Values.axonhub.persistence.storageClass }}
16
+ storageClassName: {{ .Values.axonhub.persistence.storageClass }}
17
+ {{- end }}
18
+ {{- end }}
deploy/helm/templates/deployment.yaml ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ apiVersion: apps/v1
2
+ kind: Deployment
3
+ metadata:
4
+ name: {{ include "axonhub.fullname" . }}
5
+ labels:
6
+ {{- include "axonhub.labels" . | nindent 4 }}
7
+ app.kubernetes.io/component: axonhub
8
+ spec:
9
+ {{- if not .Values.autoscaling.enabled }}
10
+ replicas: {{ .Values.axonhub.replicaCount }}
11
+ {{- end }}
12
+ selector:
13
+ matchLabels:
14
+ {{- include "axonhub.selectorLabels" . | nindent 6 }}
15
+ app.kubernetes.io/component: axonhub
16
+ template:
17
+ metadata:
18
+ {{- with .Values.axonhub.podAnnotations }}
19
+ annotations:
20
+ {{- toYaml . | nindent 8 }}
21
+ {{- end }}
22
+ labels:
23
+ {{- include "axonhub.selectorLabels" . | nindent 8 }}
24
+ app.kubernetes.io/component: axonhub
25
+ spec:
26
+ {{- with .Values.global.imagePullSecrets }}
27
+ imagePullSecrets:
28
+ {{- toYaml . | nindent 8 }}
29
+ {{- end }}
30
+ serviceAccountName: {{ include "axonhub.serviceAccountName" . }}
31
+ securityContext:
32
+ {{- toYaml .Values.axonhub.podSecurityContext | nindent 8 }}
33
+ {{- if .Values.postgresql.enabled }}
34
+ initContainers:
35
+ - name: wait-for-postgres
36
+ image: busybox:1.36
37
+ command: ['sh', '-c', 'until nc -z {{ include "axonhub.postgresql.serviceName" . }} 5432; do echo waiting for postgresql; sleep 2; done;']
38
+ {{- end }}
39
+ containers:
40
+ - name: {{ .Chart.Name }}
41
+ securityContext:
42
+ {{- toYaml .Values.axonhub.securityContext | nindent 12 }}
43
+ image: "{{ .Values.axonhub.image.repository }}:{{ .Values.axonhub.image.tag | default .Chart.AppVersion }}"
44
+ imagePullPolicy: {{ .Values.axonhub.image.pullPolicy }}
45
+ env:
46
+ {{- range $key, $value := .Values.axonhub.env }}
47
+ - name: {{ $key }}
48
+ value: {{ $value | quote }}
49
+ {{- end }}
50
+ ports:
51
+ - name: http
52
+ containerPort: {{ .Values.axonhub.service.port }}
53
+ protocol: TCP
54
+ livenessProbe:
55
+ {{- toYaml .Values.axonhub.livenessProbe | nindent 12 }}
56
+ readinessProbe:
57
+ {{- toYaml .Values.axonhub.readinessProbe | nindent 12 }}
58
+ resources:
59
+ {{- toYaml .Values.axonhub.resources | nindent 12 }}
60
+ {{- if .Values.axonhub.persistence.enabled }}
61
+ volumeMounts:
62
+ - name: data
63
+ mountPath: /data
64
+ {{- end }}
65
+ {{- with .Values.axonhub.nodeSelector }}
66
+ nodeSelector:
67
+ {{- toYaml . | nindent 8 }}
68
+ {{- end }}
69
+ {{- with .Values.axonhub.affinity }}
70
+ affinity:
71
+ {{- toYaml . | nindent 8 }}
72
+ {{- end }}
73
+ {{- with .Values.axonhub.tolerations }}
74
+ tolerations:
75
+ {{- toYaml . | nindent 8 }}
76
+ {{- end }}
77
+ {{- if .Values.axonhub.persistence.enabled }}
78
+ volumes:
79
+ - name: data
80
+ persistentVolumeClaim:
81
+ claimName: {{ include "axonhub.fullname" . }}-data
82
+ {{- end }}
deploy/helm/templates/hpa.yaml ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{- if .Values.autoscaling.enabled }}
2
+ apiVersion: autoscaling/v2
3
+ kind: HorizontalPodAutoscaler
4
+ metadata:
5
+ name: {{ include "axonhub.fullname" . }}
6
+ labels:
7
+ {{- include "axonhub.labels" . | nindent 4 }}
8
+ app.kubernetes.io/component: axonhub
9
+ spec:
10
+ scaleTargetRef:
11
+ apiVersion: apps/v1
12
+ kind: Deployment
13
+ name: {{ include "axonhub.fullname" . }}
14
+ minReplicas: {{ .Values.autoscaling.minReplicas }}
15
+ maxReplicas: {{ .Values.autoscaling.maxReplicas }}
16
+ metrics:
17
+ {{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
18
+ - type: Resource
19
+ resource:
20
+ name: cpu
21
+ target:
22
+ type: Utilization
23
+ averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
24
+ {{- end }}
25
+ {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
26
+ - type: Resource
27
+ resource:
28
+ name: memory
29
+ target:
30
+ type: Utilization
31
+ averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
32
+ {{- end }}
33
+ {{- end }}
deploy/helm/templates/ingress.yaml ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{- if .Values.ingress.enabled -}}
2
+ {{- $fullName := include "axonhub.fullname" . -}}
3
+ {{- $svcPort := .Values.axonhub.service.port -}}
4
+ {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
5
+ {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
6
+ {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
7
+ {{- end }}
8
+ {{- end }}
9
+ {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
10
+ apiVersion: networking.k8s.io/v1
11
+ {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
12
+ apiVersion: networking.k8s.io/v1beta1
13
+ {{- else -}}
14
+ apiVersion: extensions/v1beta1
15
+ {{- end }}
16
+ kind: Ingress
17
+ metadata:
18
+ name: {{ $fullName }}
19
+ labels:
20
+ {{- include "axonhub.labels" . | nindent 4 }}
21
+ app.kubernetes.io/component: axonhub
22
+ {{- with .Values.ingress.annotations }}
23
+ annotations:
24
+ {{- toYaml . | nindent 4 }}
25
+ {{- end }}
26
+ spec:
27
+ {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
28
+ ingressClassName: {{ .Values.ingress.className }}
29
+ {{- end }}
30
+ {{- if .Values.ingress.tls }}
31
+ tls:
32
+ {{- range .Values.ingress.tls }}
33
+ - hosts:
34
+ {{- range .hosts }}
35
+ - {{ . | quote }}
36
+ {{- end }}
37
+ secretName: {{ .secretName }}
38
+ {{- end }}
39
+ {{- end }}
40
+ rules:
41
+ {{- range .Values.ingress.hosts }}
42
+ - host: {{ .host | quote }}
43
+ http:
44
+ paths:
45
+ {{- range .paths }}
46
+ - path: {{ .path }}
47
+ {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
48
+ pathType: {{ .pathType }}
49
+ {{- end }}
50
+ backend:
51
+ {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
52
+ service:
53
+ name: {{ $fullName }}
54
+ port:
55
+ number: {{ $svcPort }}
56
+ {{- else }}
57
+ serviceName: {{ $fullName }}
58
+ servicePort: {{ $svcPort }}
59
+ {{- end }}
60
+ {{- end }}
61
+ {{- end }}
62
+ {{- end }}
deploy/helm/templates/postgresql-service.yaml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{- if .Values.postgresql.enabled -}}
2
+ apiVersion: v1
3
+ kind: Service
4
+ metadata:
5
+ name: {{ include "axonhub.postgresql.serviceName" . }}
6
+ labels:
7
+ {{- include "axonhub.labels" . | nindent 4 }}
8
+ app.kubernetes.io/component: postgresql
9
+ spec:
10
+ type: {{ .Values.postgresql.service.type }}
11
+ ports:
12
+ - port: {{ .Values.postgresql.service.port }}
13
+ targetPort: postgresql
14
+ protocol: TCP
15
+ name: postgresql
16
+ selector:
17
+ {{- include "axonhub.selectorLabels" . | nindent 4 }}
18
+ app.kubernetes.io/component: postgresql
19
+ {{- end }}
deploy/helm/templates/postgresql-statefulset.yaml ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{- if .Values.postgresql.enabled -}}
2
+ apiVersion: apps/v1
3
+ kind: StatefulSet
4
+ metadata:
5
+ name: {{ include "axonhub.postgresql.fullname" . }}
6
+ labels:
7
+ {{- include "axonhub.labels" . | nindent 4 }}
8
+ app.kubernetes.io/component: postgresql
9
+ spec:
10
+ serviceName: {{ include "axonhub.postgresql.serviceName" . }}
11
+ replicas: {{ .Values.postgresql.replicaCount }}
12
+ selector:
13
+ matchLabels:
14
+ {{- include "axonhub.selectorLabels" . | nindent 6 }}
15
+ app.kubernetes.io/component: postgresql
16
+ template:
17
+ metadata:
18
+ labels:
19
+ {{- include "axonhub.selectorLabels" . | nindent 8 }}
20
+ app.kubernetes.io/component: postgresql
21
+ spec:
22
+ {{- with .Values.global.imagePullSecrets }}
23
+ imagePullSecrets:
24
+ {{- toYaml . | nindent 8 }}
25
+ {{- end }}
26
+ securityContext:
27
+ {{- toYaml .Values.postgresql.podSecurityContext | nindent 8 }}
28
+ containers:
29
+ - name: postgresql
30
+ securityContext:
31
+ {{- toYaml .Values.postgresql.securityContext | nindent 12 }}
32
+ image: "{{ .Values.postgresql.image.repository }}:{{ .Values.postgresql.image.tag }}"
33
+ imagePullPolicy: {{ .Values.postgresql.image.pullPolicy }}
34
+ env:
35
+ - name: POSTGRES_DB
36
+ value: {{ .Values.postgresql.auth.database | quote }}
37
+ - name: POSTGRES_USER
38
+ value: {{ .Values.postgresql.auth.username | quote }}
39
+ - name: POSTGRES_PASSWORD
40
+ value: {{ .Values.postgresql.auth.password | quote }}
41
+ ports:
42
+ - name: postgresql
43
+ containerPort: 5432
44
+ protocol: TCP
45
+ livenessProbe:
46
+ {{- toYaml .Values.postgresql.livenessProbe | nindent 12 }}
47
+ readinessProbe:
48
+ {{- toYaml .Values.postgresql.readinessProbe | nindent 12 }}
49
+ resources:
50
+ {{- toYaml .Values.postgresql.resources | nindent 12 }}
51
+ volumeMounts:
52
+ - name: data
53
+ mountPath: /var/lib/postgresql/data
54
+ subPath: postgresql-db
55
+ {{- with .Values.postgresql.nodeSelector }}
56
+ nodeSelector:
57
+ {{- toYaml . | nindent 8 }}
58
+ {{- end }}
59
+ {{- with .Values.postgresql.affinity }}
60
+ affinity:
61
+ {{- toYaml . | nindent 8 }}
62
+ {{- end }}
63
+ {{- with .Values.postgresql.tolerations }}
64
+ tolerations:
65
+ {{- toYaml . | nindent 8 }}
66
+ {{- end }}
67
+ volumeClaimTemplates:
68
+ - metadata:
69
+ name: data
70
+ spec:
71
+ accessModes:
72
+ {{- toYaml .Values.postgresql.primary.persistence.accessModes | nindent 10 }}
73
+ resources:
74
+ requests:
75
+ storage: {{ .Values.postgresql.primary.persistence.size }}
76
+ {{- if .Values.postgresql.primary.persistence.storageClass }}
77
+ storageClassName: {{ .Values.postgresql.primary.persistence.storageClass }}
78
+ {{- end }}
79
+ {{- end }}
deploy/helm/templates/service.yaml ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ apiVersion: v1
2
+ kind: Service
3
+ metadata:
4
+ name: {{ include "axonhub.fullname" . }}
5
+ labels:
6
+ {{- include "axonhub.labels" . | nindent 4 }}
7
+ app.kubernetes.io/component: axonhub
8
+ spec:
9
+ type: {{ .Values.axonhub.service.type }}
10
+ ports:
11
+ - port: {{ .Values.axonhub.service.port }}
12
+ targetPort: http
13
+ protocol: TCP
14
+ name: http
15
+ selector:
16
+ {{- include "axonhub.selectorLabels" . | nindent 4 }}
17
+ app.kubernetes.io/component: axonhub
deploy/helm/templates/serviceaccount.yaml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{- if .Values.serviceAccount.create -}}
2
+ apiVersion: v1
3
+ kind: ServiceAccount
4
+ metadata:
5
+ name: {{ include "axonhub.serviceAccountName" . }}
6
+ labels:
7
+ {{- include "axonhub.labels" . | nindent 4 }}
8
+ {{- with .Values.serviceAccount.annotations }}
9
+ annotations:
10
+ {{- toYaml . | nindent 4 }}
11
+ {{- end }}
12
+ {{- end }}
deploy/helm/templates/tests/test-connection.yaml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ apiVersion: v1
2
+ kind: Pod
3
+ metadata:
4
+ name: "{{ include "axonhub.fullname" . }}-test-connection"
5
+ labels:
6
+ {{- include "axonhub.labels" . | nindent 4 }}
7
+ app.kubernetes.io/component: test
8
+ annotations:
9
+ "helm.sh/hook": test
10
+ spec:
11
+ containers:
12
+ - name: wget
13
+ image: busybox
14
+ command: ['wget']
15
+ args: ['{{ include "axonhub.fullname" . }}:{{ .Values.axonhub.service.port }}']
16
+ restartPolicy: Never
deploy/helm/values-production.yaml ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Production values for AxonHub Helm chart
2
+ # Copy this file and modify according to your production requirements
3
+
4
+ # Global configuration
5
+ global:
6
+ imagePullSecrets: []
7
+ # Add your image pull secrets here if using private registry
8
+ # - name: my-registry-secret
9
+
10
+ # AxonHub application configuration
11
+ axonhub:
12
+ replicaCount: 2
13
+
14
+ image:
15
+ repository: looplj/axonhub
16
+ tag: "latest" # Consider using specific version in production
17
+ pullPolicy: IfNotPresent
18
+
19
+ # Environment variables - customize the database connection string for your environment
20
+ env:
21
+ AXONHUB_DB_DIALECT: postgres
22
+ # For external database - modify this connection string
23
+ AXONHUB_DB_DSN: postgres://axonhub:your_db_password@your-db-host:5432/axonhub?sslmode=require
24
+ # For internal PostgreSQL - uncomment below and comment above
25
+ # AXONHUB_DB_DSN: postgres://axonhub:CHANGE_ME_POSTGRES_USER_123456@axonhub-postgresql:5432/axonhub?sslmode=disable
26
+
27
+ # Environment variables are defined above in the axonhub.env section
28
+
29
+ # Service configuration
30
+ service:
31
+ type: ClusterIP
32
+ port: 8090
33
+
34
+ # Resources - adjust based on your workload
35
+ resources:
36
+ limits:
37
+ cpu: 2000m
38
+ memory: 2Gi
39
+ requests:
40
+ cpu: 1000m
41
+ memory: 1Gi
42
+
43
+ # Health checks
44
+ livenessProbe:
45
+ httpGet:
46
+ path: /health
47
+ port: 8090
48
+ initialDelaySeconds: 60
49
+ periodSeconds: 30
50
+ timeoutSeconds: 10
51
+ failureThreshold: 3
52
+
53
+ readinessProbe:
54
+ httpGet:
55
+ path: /health
56
+ port: 8090
57
+ initialDelaySeconds: 30
58
+ periodSeconds: 10
59
+ timeoutSeconds: 5
60
+ failureThreshold: 3
61
+
62
+ # Security context
63
+ podSecurityContext:
64
+ fsGroup: 1000
65
+
66
+ securityContext:
67
+ runAsNonRoot: true
68
+ runAsUser: 1000
69
+ runAsGroup: 1000
70
+
71
+ # Persistence - enable for production
72
+ persistence:
73
+ enabled: true
74
+ size: 20Gi
75
+ storageClass: "" # Specify your storage class
76
+ accessModes:
77
+ - ReadWriteOnce
78
+
79
+ # PostgreSQL configuration - set to false if using external database
80
+ postgresql:
81
+ enabled: true
82
+ replicaCount: 1
83
+
84
+ image:
85
+ repository: postgres
86
+ tag: "16-alpine"
87
+ pullPolicy: IfNotPresent
88
+
89
+ # Security - CHANGE THESE VALUES IN PRODUCTION
90
+ auth:
91
+ postgresPassword: "CHANGE_ME_POSTGRES_ADMIN_123456"
92
+ username: "axonhub"
93
+ password: "CHANGE_ME_POSTGRES_USER_123456"
94
+ database: "axonhub"
95
+
96
+ # Service configuration
97
+ service:
98
+ type: ClusterIP
99
+ port: 5432
100
+
101
+ # Resources
102
+ resources:
103
+ limits:
104
+ cpu: 1000m
105
+ memory: 1Gi
106
+ requests:
107
+ cpu: 500m
108
+ memory: 512Mi
109
+
110
+ # Persistence
111
+ primary:
112
+ persistence:
113
+ enabled: true
114
+ size: 20Gi
115
+ storageClass: "" # Specify your storage class
116
+ accessModes:
117
+ - ReadWriteOnce
118
+
119
+ # Health checks
120
+ livenessProbe:
121
+ exec:
122
+ command:
123
+ - pg_isready
124
+ - -U
125
+ - axonhub
126
+ initialDelaySeconds: 30
127
+ periodSeconds: 10
128
+ timeoutSeconds: 5
129
+ failureThreshold: 6
130
+
131
+ readinessProbe:
132
+ exec:
133
+ command:
134
+ - pg_isready
135
+ - -U
136
+ - axonhub
137
+ initialDelaySeconds: 5
138
+ periodSeconds: 10
139
+ timeoutSeconds: 5
140
+ failureThreshold: 6
141
+
142
+ # Ingress configuration - enable for external access
143
+ ingress:
144
+ enabled: true
145
+ className: "nginx" # Adjust based on your ingress controller
146
+ annotations:
147
+ # Add your ingress annotations here
148
+ # nginx.ingress.kubernetes.io/rewrite-target: /
149
+ cert-manager.io/cluster-issuer: "letsencrypt-prod" # For SSL certificates
150
+ hosts:
151
+ - host: axonhub.yourdomain.com # Change to your domain
152
+ paths:
153
+ - path: /
154
+ pathType: Prefix
155
+ tls:
156
+ - secretName: axonhub-tls
157
+ hosts:
158
+ - axonhub.yourdomain.com # Change to your domain
159
+
160
+ # Autoscaling - enable for production workloads
161
+ autoscaling:
162
+ enabled: true
163
+ minReplicas: 2
164
+ maxReplicas: 10
165
+ targetCPUUtilizationPercentage: 80
166
+
167
+ # Service account
168
+ serviceAccount:
169
+ create: true
170
+ annotations: {}
171
+ name: ""
deploy/helm/values.yaml ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Default values for axonhub.
2
+ # This is a YAML-formatted file.
3
+ # Declare variables to be passed into your templates.
4
+
5
+ global:
6
+ # Global image pull secrets
7
+ imagePullSecrets: []
8
+
9
+ # AxonHub application configuration
10
+ axonhub:
11
+ replicaCount: 1
12
+
13
+ image:
14
+ repository: looplj/axonhub
15
+ pullPolicy: IfNotPresent
16
+ tag: "latest"
17
+
18
+ # Environment variables for AxonHub
19
+ env:
20
+ AXONHUB_DB_DIALECT: postgres
21
+ # Full database connection string - customize this for your environment
22
+ AXONHUB_DB_DSN: postgres://axonhub:axonhub_password@axonhub-postgresql:5432/axonhub?sslmode=disable
23
+
24
+ # Service configuration
25
+ service:
26
+ type: ClusterIP
27
+ port: 8090
28
+
29
+ # Resource limits and requests
30
+ resources:
31
+ limits:
32
+ cpu: 1000m
33
+ memory: 1Gi
34
+ requests:
35
+ cpu: 500m
36
+ memory: 512Mi
37
+
38
+ # Health checks
39
+ livenessProbe:
40
+ httpGet:
41
+ path: /health
42
+ port: 8090
43
+ initialDelaySeconds: 60
44
+ periodSeconds: 30
45
+ timeoutSeconds: 10
46
+ failureThreshold: 3
47
+
48
+ readinessProbe:
49
+ httpGet:
50
+ path: /health
51
+ port: 8090
52
+ initialDelaySeconds: 30
53
+ periodSeconds: 10
54
+ timeoutSeconds: 5
55
+ failureThreshold: 3
56
+
57
+ # Security context
58
+ podSecurityContext:
59
+ fsGroup: 1000
60
+
61
+ securityContext:
62
+ runAsNonRoot: true
63
+ runAsUser: 1000
64
+ runAsGroup: 1000
65
+
66
+ # Node selection
67
+ nodeSelector: {}
68
+ tolerations: []
69
+ affinity: {}
70
+
71
+ # Persistent volume for data storage (optional)
72
+ persistence:
73
+ enabled: false
74
+ size: 10Gi
75
+ storageClass: ""
76
+ accessModes:
77
+ - ReadWriteOnce
78
+
79
+ # PostgreSQL database configuration
80
+ postgresql:
81
+ enabled: true
82
+ replicaCount: 1
83
+
84
+ image:
85
+ repository: postgres
86
+ tag: "16-alpine"
87
+ pullPolicy: IfNotPresent
88
+
89
+ # Database credentials
90
+ auth:
91
+ postgresPassword: "axonhub_password" # Should be overridden in production
92
+ username: "axonhub"
93
+ password: "axonhub_password" # Should be overridden in production
94
+ database: "axonhub"
95
+
96
+ # Service configuration
97
+ service:
98
+ type: ClusterIP
99
+ port: 5432
100
+
101
+ # Resource limits
102
+ resources:
103
+ limits:
104
+ cpu: 500m
105
+ memory: 512Mi
106
+ requests:
107
+ cpu: 250m
108
+ memory: 256Mi
109
+
110
+ # Persistence
111
+ primary:
112
+ persistence:
113
+ enabled: true
114
+ size: 8Gi
115
+ storageClass: ""
116
+ accessModes:
117
+ - ReadWriteOnce
118
+
119
+ # Health check
120
+ livenessProbe:
121
+ exec:
122
+ command:
123
+ - pg_isready
124
+ - -U
125
+ - axonhub
126
+ initialDelaySeconds: 30
127
+ periodSeconds: 10
128
+ timeoutSeconds: 5
129
+ failureThreshold: 6
130
+
131
+ readinessProbe:
132
+ exec:
133
+ command:
134
+ - pg_isready
135
+ - -U
136
+ - axonhub
137
+ initialDelaySeconds: 5
138
+ periodSeconds: 10
139
+ timeoutSeconds: 5
140
+ failureThreshold: 6
141
+
142
+ # Ingress configuration
143
+ ingress:
144
+ enabled: true
145
+ className: ""
146
+ annotations: {}
147
+ hosts:
148
+ - host: axonhub.local
149
+ paths:
150
+ - path: /
151
+ pathType: Prefix
152
+ tls: []
153
+
154
+ # Autoscaling configuration
155
+ autoscaling:
156
+ enabled: false
157
+ minReplicas: 1
158
+ maxReplicas: 10
159
+ targetCPUUtilizationPercentage: 80
160
+
161
+ # Service account configuration
162
+ serviceAccount:
163
+ create: true
164
+ annotations: {}
165
+ name: ""
deploy/install.bat ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ setlocal
3
+
4
+ REM AxonHub Windows installer wrapper (.bat)
5
+
6
+ where powershell >NUL 2>NUL
7
+ if %ERRORLEVEL% NEQ 0 (
8
+ echo [ERROR] PowerShell is required to run this installer.
9
+ echo Please run on Windows 7+ with PowerShell available in PATH.
10
+ exit /b 1
11
+ )
12
+
13
+ set "SCRIPT_DIR=%~dp0"
14
+ powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%install.ps1" %*
15
+ exit /b %ERRORLEVEL%
deploy/install.ps1 ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ param(
2
+ [Parameter(ValueFromRemainingArguments=$true)]
3
+ [string[]]$ArgsFromCmd
4
+ )
5
+
6
+ $ErrorActionPreference = 'Stop'
7
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
8
+
9
+ function Write-Info([string]$m){ Write-Host "[INFO] $m" -ForegroundColor Cyan }
10
+ function Write-Success([string]$m){ Write-Host "[SUCCESS] $m" -ForegroundColor Green }
11
+ function Write-Warn([string]$m){ Write-Host "[WARNING] $m" -ForegroundColor Yellow }
12
+ function Write-Err([string]$m){ Write-Host "[ERROR] $m" -ForegroundColor Red }
13
+
14
+ $Repo = 'looplj/axonhub'
15
+ $Api = "https://api.github.com/repos/$Repo"
16
+
17
+ $IncludeBeta = $false
18
+ $IncludeRC = $false
19
+ $VerboseFlag = $false
20
+ $Version = $env:AXONHUB_VERSION
21
+
22
+ function Show-Usage {
23
+ Write-Host @'
24
+ AxonHub Installer (Windows)
25
+
26
+ Usage:
27
+ install.bat [options] [version]
28
+
29
+ Options:
30
+ -b, --beta Consider beta pre-releases
31
+ -r, --rc Consider release-candidate pre-releases
32
+ -v, --verbose Print extra debug logs
33
+ -h, --help Show this help and exit
34
+ '@
35
+ }
36
+
37
+ # Parse args
38
+ foreach($a in $ArgsFromCmd){
39
+ switch -Regex ($a){
40
+ '^(--beta|-b)$' { $IncludeBeta = $true; continue }
41
+ '^(--rc|-r)$' { $IncludeRC = $true; continue }
42
+ '^(--verbose|-v)$' { $VerboseFlag = $true; continue }
43
+ '^(--help|-h)$' { Show-Usage; exit 0 }
44
+ default { if(-not $Version){ $Version = $a } else { Write-Warn "Ignoring extra argument: $a" } }
45
+ }
46
+ }
47
+
48
+ function Get-Platform {
49
+ $archEnv = $env:PROCESSOR_ARCHITECTURE
50
+ switch ($archEnv.ToLower()){
51
+ 'amd64' { return 'windows_amd64' }
52
+ 'arm64' { return 'windows_arm64' }
53
+ default { Write-Err "Unsupported architecture: $archEnv"; exit 1 }
54
+ }
55
+ }
56
+
57
+ function Invoke-GHApi([string]$url){
58
+ $headers = @{
59
+ 'Accept'='application/vnd.github+json'
60
+ 'X-GitHub-Api-Version'='2022-11-28'
61
+ 'User-Agent'='axonhub-installer'
62
+ }
63
+ if($env:GITHUB_TOKEN){ $headers['Authorization'] = "Bearer $($env:GITHUB_TOKEN)" }
64
+ return Invoke-RestMethod -Method GET -Uri $url -Headers $headers -ErrorAction Stop
65
+ }
66
+
67
+ function Get-LatestReleaseTag {
68
+ try {
69
+ $json = Invoke-GHApi "$Api/releases/latest"
70
+ return $json.tag_name
71
+ } catch {
72
+ # Fallback to HTML redirect (best-effort)
73
+ Write-Warn "API failed or rate-limited, falling back to HTML redirect..."
74
+ try {
75
+ $resp = Invoke-WebRequest -Uri "https://github.com/$Repo/releases/latest" -Headers @{ 'User-Agent'='axonhub-installer' } -MaximumRedirection 0 -ErrorAction Stop
76
+ } catch { $resp = $_.Exception.Response }
77
+ if($resp -and $resp.Headers['Location']){
78
+ $loc = $resp.Headers['Location']
79
+ if($loc -match '/tag/([^/]+)$'){ return $matches[1] }
80
+ }
81
+ Write-Err "Could not determine latest release version"
82
+ exit 1
83
+ }
84
+ }
85
+
86
+ function Get-LatestVersion([bool]$includeBeta,[bool]$includeRC){
87
+ if(-not $includeBeta -and -not $includeRC){ return Get-LatestReleaseTag }
88
+ Write-Info "Fetching releases (beta=$includeBeta, rc=$includeRC) ..."
89
+ try {
90
+ $rels = Invoke-GHApi "$Api/releases?per_page=100"
91
+ $filtered = $rels | Where-Object { -not $_.draft }
92
+ if($includeBeta -and $includeRC){
93
+ $filtered = $filtered | Where-Object { $_.tag_name -match '-beta' -or $_.tag_name -match '-rc' }
94
+ } elseif($includeBeta){
95
+ $filtered = $filtered | Where-Object { $_.tag_name -match '-beta' }
96
+ } else {
97
+ $filtered = $filtered | Where-Object { $_.tag_name -match '-rc' }
98
+ }
99
+ if(-not $filtered){
100
+ Write-Warn 'No matching pre-release found; falling back to latest stable.'
101
+ return Get-LatestReleaseTag
102
+ }
103
+ # GitHub API returns releases in reverse chronological order; take the first
104
+ return ($filtered | Select-Object -First 1).tag_name
105
+ } catch {
106
+ Write-Warn 'Failed to fetch releases; falling back to latest stable.'
107
+ return Get-LatestReleaseTag
108
+ }
109
+ }
110
+
111
+ function Get-AssetUrl([string]$version,[string]$platform){
112
+ Write-Info "Resolving asset for $version ($platform) ..."
113
+ try {
114
+ $tag = $version
115
+ $json = Invoke-GHApi "$Api/releases/tags/$tag"
116
+ $asset = $json.assets | Where-Object { $_.browser_download_url -match $platform -and $_.browser_download_url -like '*.zip' } | Select-Object -First 1
117
+ if($asset){ return $asset.browser_download_url }
118
+ } catch {}
119
+ # Fallback by pattern
120
+ $clean = $version.TrimStart('v')
121
+ $file = "axonhub_${clean}_${platform}.zip"
122
+ $candidate = "https://github.com/$Repo/releases/download/$version/$file"
123
+ try {
124
+ $head = Invoke-WebRequest -Uri $candidate -Method Head -ErrorAction Stop
125
+ return $candidate
126
+ } catch {
127
+ Write-Err "Could not find asset for platform $platform in release $version"
128
+ exit 1
129
+ }
130
+ }
131
+
132
+ function Ensure-Dirs([string]$path){ if(-not (Test-Path $path)){ New-Item -ItemType Directory -Force -Path $path | Out-Null } }
133
+
134
+ # Main
135
+ Write-Info 'Starting AxonHub installation...'
136
+
137
+ $Platform = Get-Platform
138
+ Write-Info "Detected platform: $Platform"
139
+
140
+ if(-not $Version){ $Version = Get-LatestVersion $IncludeBeta $IncludeRC }
141
+ Write-Info "Using version: $Version"
142
+
143
+ $BaseDir = Join-Path $env:LOCALAPPDATA 'AxonHub'
144
+ $ConfigDir = $BaseDir
145
+ $DataDir = $BaseDir
146
+ $LogDir = $BaseDir
147
+ Ensure-Dirs $BaseDir
148
+ Ensure-Dirs (Join-Path $BaseDir 'logs')
149
+
150
+ $AssetUrl = Get-AssetUrl $Version $Platform
151
+ $TempDir = New-Item -ItemType Directory -Path (Join-Path ([IO.Path]::GetTempPath()) ([IO.Path]::GetRandomFileName())) -Force
152
+ $ZipPath = Join-Path $TempDir 'axonhub.zip'
153
+ Write-Info "Downloading: $AssetUrl"
154
+ Invoke-WebRequest -Uri $AssetUrl -OutFile $ZipPath -UseBasicParsing
155
+
156
+ Write-Info 'Extracting archive...'
157
+ Expand-Archive -Path $ZipPath -DestinationPath $TempDir -Force
158
+
159
+ $BinaryPath = Get-ChildItem -Path $TempDir -Recurse -Filter 'axonhub.exe' -File | Select-Object -First 1 | ForEach-Object { $_.FullName }
160
+ if(-not $BinaryPath){ Write-Err 'axonhub.exe not found in archive'; exit 1 }
161
+
162
+ $TargetBinary = Join-Path $BaseDir 'axonhub.exe'
163
+ Copy-Item -Path $BinaryPath -Destination $TargetBinary -Force
164
+
165
+ # Create default config if missing
166
+ $ConfigFile = Join-Path $BaseDir 'config.yml'
167
+ if(-not (Test-Path $ConfigFile)){
168
+ Write-Info 'Creating default configuration...'
169
+ $baseForDSN = ($BaseDir -replace '\\','/')
170
+ @"
171
+ server:
172
+ port: 8090
173
+ name: "AxonHub"
174
+ debug: false
175
+
176
+ db:
177
+ dialect: "sqlite3"
178
+ dsn: "$baseForDSN/axonhub.db?cache=shared&_fk=1&journal_mode=WAL"
179
+
180
+ cache:
181
+ mode: "memory"
182
+ cache:
183
+ expiration: "5s"
184
+ cleanup_interval: "5s"
185
+
186
+ log:
187
+ level: "info"
188
+ encoding: "json"
189
+ output: "file"
190
+ file:
191
+ path: "$baseForDSN/logs/axonhub.log"
192
+ max_size: 100
193
+ max_age: 30
194
+ max_backups: 10
195
+ local_time: true
196
+ "@ | Set-Content -Path $ConfigFile -Encoding UTF8
197
+ }
198
+
199
+ Write-Success 'AxonHub installation completed!'
200
+
201
+ # Get configured port for display
202
+ $port = 8090
203
+ if(Test-Path $TargetBinary){
204
+ try {
205
+ $configPort = & $TargetBinary config get server.port 2>$null
206
+ if($configPort -match '^[0-9]+$'){
207
+ $port = $configPort
208
+ }
209
+ } catch {}
210
+ }
211
+
212
+ Write-Info "Next steps:"
213
+ Write-Host " 1. Edit configuration: $ConfigFile"
214
+ Write-Host " 2. Start AxonHub: start.bat"
215
+ Write-Host " 3. Stop AxonHub: stop.bat"
216
+ Write-Host " 4. View logs: $BaseDir\axonhub.log (or logs\axonhub.log in config)"
217
+ Write-Host " 5. Access web interface: http://localhost:$port"
deploy/install.sh ADDED
@@ -0,0 +1,550 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # AxonHub Installation Script
4
+ # This script downloads and installs the latest AxonHub release for direct start/stop usage (no systemd)
5
+
6
+ set -e
7
+
8
+ # Colors for output
9
+ RED='\033[0;31m'
10
+ GREEN='\033[0;32m'
11
+ YELLOW='\033[1;33m'
12
+ BLUE='\033[0;34m'
13
+ NC='\033[0m' # No Color
14
+
15
+ # Configuration
16
+ INSTALL_DIR="/usr/local/bin"
17
+ # Resolve non-root user's HOME when running via sudo
18
+ if [[ -n "$SUDO_USER" && "$SUDO_USER" != "root" ]]; then
19
+ USER_HOME="$(eval echo ~${SUDO_USER})"
20
+ else
21
+ USER_HOME="$HOME"
22
+ fi
23
+ BASE_DIR="${USER_HOME}/.config/axonhub"
24
+ CONFIG_DIR="${BASE_DIR}"
25
+ DATA_DIR="${BASE_DIR}"
26
+ LOG_DIR="${BASE_DIR}"
27
+ SERVICE_USER="axonhub"
28
+
29
+ # GitHub repository
30
+ REPO="looplj/axonhub"
31
+ GITHUB_API="https://api.github.com/repos/${REPO}"
32
+
33
+ # CLI options (default: exclude beta/rc)
34
+ INCLUDE_BETA="false"
35
+ INCLUDE_RC="false"
36
+ VERBOSE="false"
37
+
38
+ print_info() {
39
+ echo -e "${BLUE}[INFO]${NC} $1" 1>&2
40
+ }
41
+
42
+ curl_gh() {
43
+ # Curl helper for GitHub with proper headers and optional token
44
+ local url="$1"
45
+ local headers=(
46
+ -H "Accept: application/vnd.github+json"
47
+ -H "X-GitHub-Api-Version: 2022-11-28"
48
+ -H "User-Agent: axonhub-installer"
49
+ )
50
+ if [[ -n "$GITHUB_TOKEN" ]]; then
51
+ headers+=( -H "Authorization: Bearer $GITHUB_TOKEN" )
52
+ fi
53
+ curl -fsSL "${headers[@]}" "$url"
54
+ }
55
+
56
+ print_success() {
57
+ echo -e "${GREEN}[SUCCESS]${NC} $1" 1>&2
58
+ }
59
+
60
+ print_warning() {
61
+ echo -e "${YELLOW}[WARNING]${NC} $1" 1>&2
62
+ }
63
+
64
+ print_error() {
65
+ echo -e "${RED}[ERROR]${NC} $1" 1>&2
66
+ }
67
+
68
+ # Verbose logger
69
+ debug() {
70
+ if [[ "$VERBOSE" == "true" ]]; then
71
+ echo -e "${YELLOW}[DEBUG]${NC} $1" 1>&2
72
+ fi
73
+ }
74
+
75
+ usage() {
76
+ cat 1>&2 <<EOF
77
+ AxonHub Installer
78
+
79
+ Usage:
80
+ sudo ./install.sh [options] [version]
81
+
82
+ Options:
83
+ -b, --beta Consider beta pre-releases when resolving latest version
84
+ -r, --rc Consider release-candidate (rc) pre-releases when resolving latest version
85
+ -v, --verbose Print extra debug logs
86
+ -h, --help Show this help and exit
87
+
88
+ Notes:
89
+ - By default, beta/rc versions are filtered out and the latest stable release is used.
90
+ - If a version is provided (e.g., v1.2.3), flags are ignored and that version is used.
91
+ - If both --beta and --rc are provided, the newest matching pre-release is selected.
92
+ EOF
93
+ }
94
+
95
+ check_root() {
96
+ if [[ $EUID -ne 0 ]]; then
97
+ print_error "This script must be run as root (use sudo)"
98
+ exit 1
99
+ fi
100
+ }
101
+
102
+ detect_architecture() {
103
+ local arch=$(uname -m)
104
+ local os=$(uname -s | tr '[:upper:]' '[:lower:]')
105
+
106
+ case $arch in
107
+ x86_64|amd64)
108
+ arch="amd64"
109
+ ;;
110
+ aarch64|arm64)
111
+ arch="arm64"
112
+ ;;
113
+ *)
114
+ print_error "Unsupported architecture: $arch"
115
+ exit 1
116
+ ;;
117
+ esac
118
+
119
+ case $os in
120
+ linux)
121
+ os="linux"
122
+ ;;
123
+ darwin)
124
+ os="darwin"
125
+ ;;
126
+ *)
127
+ print_error "Unsupported operating system: $os"
128
+ exit 1
129
+ ;;
130
+ esac
131
+
132
+ echo "${os}_${arch}"
133
+ }
134
+
135
+ get_latest_release() {
136
+ print_info "Fetching latest release information..."
137
+
138
+ local tag_name
139
+ # Try GitHub API first
140
+ if json=$(curl_gh "${GITHUB_API}/releases/latest" 2>/dev/null); then
141
+ tag_name=$(echo "$json" | tr -d '\n\r\t' | sed -nE 's/.*"tag_name"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' | head -1)
142
+ fi
143
+
144
+ # Fallback: follow the HTML redirect to the latest tag
145
+ if [[ -z "$tag_name" ]]; then
146
+ print_warning "API failed or rate-limited, falling back to HTML redirect..."
147
+ local final_url
148
+ final_url=$(curl -fsSL -H "User-Agent: axonhub-installer" -o /dev/null -w "%{url_effective}" "https://github.com/${REPO}/releases/latest" || true)
149
+ tag_name=$(echo "$final_url" | sed -nE 's#.*/tag/([^/]+).*#\1#p' | head -1)
150
+ fi
151
+
152
+ if [[ -z "$tag_name" ]]; then
153
+ print_error "Could not determine latest release version"
154
+ exit 1
155
+ fi
156
+
157
+ debug "Selected tag: $tag_name"
158
+ echo "$tag_name"
159
+ }
160
+
161
+ # Get the latest version based on flags (default stable; with --beta/--rc select matching pre-releases)
162
+ get_latest_version() {
163
+ local include_beta="$1"
164
+ local include_rc="$2"
165
+
166
+ # Default path: stable-only
167
+ if [[ "$include_beta" != "true" && "$include_rc" != "true" ]]; then
168
+ get_latest_release
169
+ return
170
+ fi
171
+
172
+ print_info "Fetching releases to determine latest version (beta=${include_beta}, rc=${include_rc})..."
173
+
174
+ local json tag_name pattern
175
+ tag_name=""
176
+ if [[ "$include_beta" == "true" && "$include_rc" == "true" ]]; then
177
+ pattern='-beta|-rc'
178
+ elif [[ "$include_beta" == "true" ]]; then
179
+ pattern='-beta'
180
+ else
181
+ pattern='-rc'
182
+ fi
183
+
184
+ if json=$(curl_gh "${GITHUB_API}/releases?per_page=100" 2>/dev/null); then
185
+ local tags pairs
186
+ if command -v jq >/dev/null 2>&1; then
187
+ tags=$(echo "$json" | jq -r '.[] | select(.draft==false) | .tag_name')
188
+ else
189
+ tags=$(echo "$json" | grep -oE '"tag_name"\s*:\s*"[^"]+"' | sed -E 's/.*"tag_name"\s*:\s*"([^"]+)".*/\1/')
190
+ fi
191
+
192
+ debug "Fetched release tags (first 20): $(printf '%s\n' "$tags" | head -n20 | tr '\n' ' ')"
193
+
194
+ # Build pairs of cleaned|original, where cleaned strips any prefix before the semantic version
195
+ pairs=$(printf '%s\n' "$tags" | awk '{
196
+ orig=$0;
197
+ cleaned=orig;
198
+ if (match(cleaned,/(v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z\.\-]+)*)$/,m)) {
199
+ cleaned=m[1];
200
+ }
201
+ print cleaned"|"orig
202
+ }')
203
+
204
+ # Filter by pattern on the cleaned part and semver-sort to pick the highest; return original tag
205
+ local best
206
+ best=$(printf '%s\n' "$pairs" |
207
+ awk -F'|' -v pat="$pattern" '$1 ~ pat {print $0}' |
208
+ awk -F'|' '{ t=$1; sub(/^v/,"",t); print t"|"$2 }' |
209
+ sort -t '|' -k1,1V | tail -n1 | cut -d'|' -f2)
210
+
211
+ tag_name="$best"
212
+ fi
213
+
214
+ if [[ -z "$tag_name" ]]; then
215
+ print_warning "No matching pre-release found; falling back to latest stable release."
216
+ tag_name=$(get_latest_release)
217
+ fi
218
+
219
+ echo "$tag_name"
220
+ }
221
+
222
+ # Normalize version by removing a leading 'v'
223
+ normalize_version() {
224
+ local v="$1"
225
+ v="${v#v}"
226
+ echo "$v"
227
+ }
228
+
229
+ # Return 0 (true) if $1 < $2 in semantic version order, using sort -V
230
+ version_lt() {
231
+ local a b first
232
+ a=$(normalize_version "$1")
233
+ b=$(normalize_version "$2")
234
+ first=$(printf '%s\n' "$a" "$b" | sort -V | head -n1)
235
+ [[ "$first" == "$a" && "$a" != "$b" ]]
236
+ }
237
+
238
+ # Get asset download url for a given version and platform (e.g., darwin_arm64), prefer .zip
239
+ get_asset_download_url() {
240
+ local version=$1
241
+ local platform=$2
242
+ local url=""
243
+
244
+ print_info "Resolving asset download URL for ${version} (${platform})..."
245
+ debug "Querying ${GITHUB_API}/releases/tags/${version}"
246
+ if json=$(curl_gh "${GITHUB_API}/releases/tags/${version}" 2>/dev/null); then
247
+ if command -v jq >/dev/null 2>&1; then
248
+ debug "Assets on tag (names): $(echo "$json" | jq -r '.assets[]?.name' | tr '\n' ' ')"
249
+ url=$(echo "$json" | jq -r --arg platform "$platform" '.assets[]?.browser_download_url | select(test($platform)) | select(endswith(".zip"))' | head -n1)
250
+ else
251
+ url=$(echo "$json" \
252
+ | tr -d '\n\r\t' \
253
+ | sed -nE 's/.*("browser_download_url"[[[:space:]]]*:[[:space:]]*"[^"]+").*/\1/p' \
254
+ | sed -nE 's/.*"browser_download_url"[[[:space:]]]*:[[:space:]]*"([^"]+)".*/\1/p' \
255
+ | grep "$platform" \
256
+ | grep '\.zip$' -m 1)
257
+ fi
258
+ fi
259
+ debug "Matched asset URL from tag endpoint: ${url:-<none>}"
260
+
261
+ # Fallback to patterned URL if API failed or empty
262
+ if [[ -z "$url" ]]; then
263
+ print_warning "API failed or no asset matched; trying list endpoint..."
264
+ if json2=$(curl_gh "${GITHUB_API}/releases?per_page=100" 2>/dev/null); then
265
+ if command -v jq >/dev/null 2>&1; then
266
+ url=$(echo "$json2" | jq -r --arg tag "$version" --arg platform "$platform" '.[] | select(.tag_name==$tag) | .assets[]?.browser_download_url | select(test($platform)) | select(endswith(".zip"))' | head -n1)
267
+ else
268
+ url=$(echo "$json2" \
269
+ | tr -d '\n\r\t' \
270
+ | sed -nE 's/.*\{([^}]*)\}.*/\{\1\}/gp' \
271
+ | grep -E '"tag_name"[[:space:]]*:[[:space:]]*"'"$version"'"' \
272
+ | sed -nE 's/.*("browser_download_url"[[[:space:]]]*:[[:space:]]*"[^"]+").*/\1/p' \
273
+ | sed -nE 's/.*"browser_download_url"[[[:space:]]]*:[[:space:]]*"([^"]+)".*/\1/p' \
274
+ | grep "$platform" \
275
+ | grep '\.zip$' -m 1)
276
+ fi
277
+ fi
278
+ debug "Matched asset URL from list endpoint: ${url:-<none>}"
279
+ fi
280
+
281
+ if [[ -z "$url" ]]; then
282
+ print_warning "API failed or no asset matched; trying patterned URL..."
283
+ local clean_version="$version"
284
+ clean_version="${clean_version##*:}"
285
+ clean_version="${clean_version#v}"
286
+ local filename="axonhub_${clean_version}_${platform}.zip"
287
+ local candidate="https://github.com/${REPO}/releases/download/${version}/${filename}"
288
+ debug "Trying candidate URL: $candidate"
289
+ if curl -fsI "$candidate" >/dev/null 2>&1; then
290
+ url="$candidate"
291
+ fi
292
+ fi
293
+
294
+ if [[ -z "$url" ]]; then
295
+ print_error "Could not find a matching .zip asset for platform ${platform} in release ${version}"
296
+ exit 1
297
+ fi
298
+ echo "$url"
299
+ }
300
+
301
+ download_and_extract() {
302
+ local version=$1
303
+ local platform=$2
304
+ local temp_dir=$(mktemp -d)
305
+
306
+ # Resolve exact asset URL from GitHub API
307
+ local download_url
308
+ download_url=$(get_asset_download_url "$version" "$platform")
309
+ local filename
310
+ filename=$(basename "$download_url")
311
+
312
+ print_info "Downloading AxonHub ${version} for ${platform}..."
313
+
314
+ if ! curl -fSL -o "${temp_dir}/${filename}" "$download_url"; then
315
+ print_error "Failed to download AxonHub asset"
316
+ rm -rf "$temp_dir"
317
+ exit 1
318
+ fi
319
+
320
+ print_info "Extracting archive..."
321
+
322
+ if ! command -v unzip >/dev/null 2>&1; then
323
+ print_error "unzip command not found. Please install unzip and rerun."
324
+ rm -rf "$temp_dir"
325
+ exit 1
326
+ fi
327
+
328
+ if ! unzip -q "${temp_dir}/${filename}" -d "$temp_dir"; then
329
+ print_error "Failed to extract archive"
330
+ rm -rf "$temp_dir"
331
+ exit 1
332
+ fi
333
+
334
+ # Find the extracted binary
335
+ local binary_path
336
+ binary_path=$(find "$temp_dir" -name "axonhub" -type f | head -1)
337
+
338
+ if [[ -z "$binary_path" ]]; then
339
+ print_error "Could not find axonhub binary in archive"
340
+ rm -rf "$temp_dir"
341
+ exit 1
342
+ fi
343
+
344
+ echo "$binary_path"
345
+ }
346
+
347
+ create_user() {
348
+ # No system user management per requirements
349
+ print_info "Skipping system user creation"
350
+ }
351
+
352
+ setup_directories() {
353
+ print_info "Setting up directories..."
354
+
355
+ # Create directories
356
+ mkdir -p "$CONFIG_DIR" "$DATA_DIR" "$LOG_DIR"
357
+
358
+ # Set ownership and permissions to invoking user
359
+ local target_user="${SUDO_USER:-$USER}"
360
+ local target_group
361
+ target_group="$(id -gn "$target_user" 2>/dev/null || echo "$target_user")"
362
+ chown -R "$target_user:$target_group" "$CONFIG_DIR" "$DATA_DIR" "$LOG_DIR" 2>/dev/null || true
363
+ chmod 755 "$CONFIG_DIR" "$DATA_DIR" "$LOG_DIR"
364
+ }
365
+
366
+ install_binary() {
367
+ local binary_path=$1
368
+
369
+ print_info "Installing AxonHub binary to $INSTALL_DIR..."
370
+
371
+ # Install binary
372
+ cp "$binary_path" "$INSTALL_DIR/axonhub"
373
+ chmod +x "$INSTALL_DIR/axonhub"
374
+
375
+ # Clean up temp directory only if it looks like a system temp path
376
+ local dir
377
+ dir="$(dirname "$binary_path")"
378
+ local tmp1="${TMPDIR:-/tmp}"
379
+ if [[ "$dir" == /tmp/* || "$dir" == /var/folders/* || "$dir" == /private/var/folders/* || "$dir" == "$tmp1"* ]]; then
380
+ rm -rf "$dir" 2>/dev/null || true
381
+ fi
382
+ }
383
+
384
+ create_default_config() {
385
+ local config_file="$CONFIG_DIR/config.yml"
386
+
387
+ if [[ ! -f "$config_file" ]]; then
388
+ print_info "Creating default configuration..."
389
+
390
+ cat > "$config_file" << EOF
391
+ server:
392
+ port: 8090
393
+ name: "AxonHub"
394
+ debug: false
395
+
396
+ db:
397
+ dialect: "sqlite3"
398
+ dsn: "${BASE_DIR}/axonhub.db?cache=shared&_fk=1&journal_mode=WAL"
399
+
400
+ cache:
401
+ mode: "memory"
402
+ cache:
403
+ expiration: "5s"
404
+ cleanup_interval: "5s"
405
+
406
+ log:
407
+ level: "info"
408
+ encoding: "json"
409
+ output: "file"
410
+ file:
411
+ path: "${BASE_DIR}/logs/axonhub.log"
412
+ max_size: 100
413
+ max_age: 30
414
+ max_backups: 10
415
+ local_time: true
416
+ EOF
417
+
418
+ local target_user="${SUDO_USER:-$USER}"
419
+ local target_group
420
+ target_group="$(id -gn "$target_user" 2>/dev/null || echo "$target_user")"
421
+ chown "$target_user:$target_group" "$config_file" 2>/dev/null || true
422
+ chmod 644 "$config_file"
423
+
424
+ print_success "Default configuration created at $config_file"
425
+ else
426
+ print_info "Configuration file already exists at $config_file"
427
+ fi
428
+ }
429
+
430
+ # Note: systemd service installation removed; use deploy/start.sh and deploy/stop.sh to manage AxonHub
431
+
432
+ main() {
433
+ print_info "Starting AxonHub installation..."
434
+
435
+ # Check if running as root
436
+ check_root
437
+
438
+ # Detect system architecture
439
+ local platform
440
+ platform=$(detect_architecture)
441
+ print_info "Detected platform: $platform"
442
+
443
+ # Determine target version (env AXONHUB_VERSION, positional arg, or latest)
444
+ local version version_arg
445
+ version="${AXONHUB_VERSION:-}"
446
+
447
+ # Parse CLI flags and optional version argument
448
+ if [[ -z "$version" ]]; then
449
+ while [[ $# -gt 0 ]]; do
450
+ case "$1" in
451
+ -b|--beta)
452
+ INCLUDE_BETA="true"; shift ;;
453
+ -r|--rc)
454
+ INCLUDE_RC="true"; shift ;;
455
+ -v|--verbose)
456
+ VERBOSE="true"; shift ;;
457
+ -h|--help)
458
+ usage; exit 0 ;;
459
+ --)
460
+ shift; break ;;
461
+ -*)
462
+ print_error "Unknown option: $1"; usage; exit 1 ;;
463
+ *)
464
+ if [[ -z "${version_arg:-}" ]]; then
465
+ version_arg="$1"; shift
466
+ else
467
+ break
468
+ fi ;;
469
+ esac
470
+ done
471
+ version="${version_arg:-}"
472
+ fi
473
+
474
+ if [[ -z "$version" ]]; then
475
+ version=$(get_latest_version "$INCLUDE_BETA" "$INCLUDE_RC")
476
+ fi
477
+ print_info "Using version: $version"
478
+
479
+ # Prefer local binary near this script; offer to update if newer is available
480
+ local binary_path
481
+ local script_dir
482
+ script_dir=$(cd "$(dirname "$0")" && pwd)
483
+ if [[ -x "$script_dir/axonhub" ]]; then
484
+ print_info "Found local binary: $script_dir/axonhub"
485
+ # Try to read local version from the binary
486
+ local local_version norm_local norm_target
487
+ if local_version=$("$script_dir/axonhub" version 2>/dev/null | head -n1 | tr -d '\r'); then
488
+ print_info "Local binary version: $local_version"
489
+ norm_local=$(normalize_version "$local_version")
490
+ else
491
+ print_warning "Could not determine local binary version"
492
+ norm_local=""
493
+ fi
494
+ norm_target=$(normalize_version "$version")
495
+
496
+ if [[ -n "$norm_local" && "$norm_local" != "dev" ]] && version_lt "$norm_local" "$norm_target"; then
497
+ echo -n "A newer version is available (local ${local_version}, latest ${version}). Download the latest now? [Y/n]: " 1>&2
498
+ read -r reply
499
+ if [[ -z "$reply" || "$reply" =~ ^[Yy]$ ]]; then
500
+ binary_path=$(download_and_extract "$version" "$platform")
501
+ else
502
+ print_info "Using existing local binary as requested."
503
+ binary_path="$script_dir/axonhub"
504
+ fi
505
+ else
506
+ print_info "Local binary is up-to-date. Using existing local binary."
507
+ binary_path="$script_dir/axonhub"
508
+ fi
509
+ else
510
+ # Download and extract
511
+ binary_path=$(download_and_extract "$version" "$platform")
512
+ fi
513
+
514
+ # Create system user
515
+ create_user
516
+
517
+ # Setup directories
518
+ setup_directories
519
+
520
+ # Install binary
521
+ install_binary "$binary_path"
522
+
523
+ # Create default configuration
524
+ create_default_config
525
+
526
+ print_success "AxonHub installation completed!"
527
+ echo
528
+
529
+ # Get configured port for display
530
+ local port=8090
531
+ if [[ -x "$INSTALL_DIR/axonhub" ]]; then
532
+ local config_port
533
+ config_port=$("$INSTALL_DIR/axonhub" config get server.port 2>/dev/null) || true
534
+ if [[ -n "$config_port" && "$config_port" =~ ^[0-9]+$ ]]; then
535
+ port="$config_port"
536
+ fi
537
+ fi
538
+
539
+ print_info "Next steps:"
540
+ echo " 1. Edit configuration: nano $CONFIG_DIR/config.yml"
541
+ echo " 2. Start AxonHub: ./start.sh"
542
+ echo " 3. Stop AxonHub: ./stop.sh"
543
+ echo " 4. View logs: tail -f $LOG_DIR/axonhub.log"
544
+ echo " 5. Access web interface: http://localhost:${port}"
545
+ echo
546
+ print_info "To start AxonHub now, run: ./start.sh"
547
+ }
548
+
549
+ # Run main function
550
+ main "$@"
deploy/nginx.conf ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AxonHub Nginx 配置文件
2
+ # 用于 Docker 部署的反向代理配置
3
+
4
+ user nginx;
5
+ worker_processes auto;
6
+ error_log /var/log/nginx/error.log warn;
7
+ pid /var/run/nginx.pid;
8
+
9
+ events {
10
+ worker_connections 1024;
11
+ use epoll;
12
+ multi_accept on;
13
+ }
14
+
15
+ http {
16
+ include /etc/nginx/mime.types;
17
+ default_type application/octet-stream;
18
+
19
+ # 日志格式
20
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
21
+ '$status $body_bytes_sent "$http_referer" '
22
+ '"$http_user_agent" "$http_x_forwarded_for" '
23
+ 'rt=$request_time uct="$upstream_connect_time" '
24
+ 'uht="$upstream_header_time" urt="$upstream_response_time"';
25
+
26
+ access_log /var/log/nginx/access.log main;
27
+
28
+ # 基础配置
29
+ sendfile on;
30
+ tcp_nopush on;
31
+ tcp_nodelay on;
32
+ keepalive_timeout 65;
33
+ types_hash_max_size 2048;
34
+ server_tokens off;
35
+
36
+ # Gzip 压缩
37
+ gzip on;
38
+ gzip_vary on;
39
+ gzip_proxied any;
40
+ gzip_comp_level 6;
41
+ gzip_types
42
+ text/plain
43
+ text/css
44
+ text/xml
45
+ text/javascript
46
+ application/json
47
+ application/javascript
48
+ application/xml+rss
49
+ application/atom+xml
50
+ image/svg+xml;
51
+
52
+ # 上游服务器配置
53
+ upstream axonhub_backend {
54
+ server axonhub:8090;
55
+ keepalive 32;
56
+ }
57
+
58
+ # 限流配置
59
+ limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
60
+ limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
61
+
62
+ # HTTP 服务器(重定向到 HTTPS)
63
+ server {
64
+ listen 80;
65
+ server_name _;
66
+
67
+ # 健康检查端点
68
+ location /health {
69
+ access_log off;
70
+ return 200 "healthy\n";
71
+ add_header Content-Type text/plain;
72
+ }
73
+
74
+ # 重定向到 HTTPS
75
+ location / {
76
+ return 301 https://$host$request_uri;
77
+ }
78
+ }
79
+
80
+ # HTTPS 服务器
81
+ server {
82
+ listen 443 ssl http2;
83
+ server_name _;
84
+
85
+ # SSL 配置
86
+ ssl_certificate /etc/nginx/ssl/cert.pem;
87
+ ssl_certificate_key /etc/nginx/ssl/key.pem;
88
+
89
+ # SSL 安全配置
90
+ ssl_protocols TLSv1.2 TLSv1.3;
91
+ ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
92
+ ssl_prefer_server_ciphers off;
93
+ ssl_session_cache shared:SSL:10m;
94
+ ssl_session_timeout 10m;
95
+
96
+ # 安全头
97
+ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
98
+ add_header X-Frame-Options DENY always;
99
+ add_header X-Content-Type-Options nosniff always;
100
+ add_header X-XSS-Protection "1; mode=block" always;
101
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
102
+
103
+ # 主要代理配置
104
+ location / {
105
+ # 限流
106
+ limit_req zone=api burst=20 nodelay;
107
+
108
+ # 代理设置
109
+ proxy_pass http://axonhub_backend;
110
+ proxy_http_version 1.1;
111
+ proxy_set_header Upgrade $http_upgrade;
112
+ proxy_set_header Connection "upgrade";
113
+ proxy_set_header Host $host;
114
+ proxy_set_header X-Real-IP $remote_addr;
115
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
116
+ proxy_set_header X-Forwarded-Proto $scheme;
117
+ proxy_set_header X-Forwarded-Host $host;
118
+ proxy_set_header X-Forwarded-Port $server_port;
119
+
120
+ # 超时设置
121
+ proxy_connect_timeout 60s;
122
+ proxy_send_timeout 60s;
123
+ proxy_read_timeout 60s;
124
+
125
+ # 缓冲设置
126
+ proxy_buffering on;
127
+ proxy_buffer_size 4k;
128
+ proxy_buffers 8 4k;
129
+ proxy_busy_buffers_size 8k;
130
+ }
131
+
132
+ # API 端点特殊配置
133
+ location /v1/ {
134
+ # API 限流
135
+ limit_req zone=api burst=50 nodelay;
136
+
137
+ proxy_pass http://axonhub_backend;
138
+ proxy_http_version 1.1;
139
+ proxy_set_header Connection "";
140
+ proxy_set_header Host $host;
141
+ proxy_set_header X-Real-IP $remote_addr;
142
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
143
+ proxy_set_header X-Forwarded-Proto $scheme;
144
+
145
+ # 长时间请求支持(AI 生成可能需要更长时间)
146
+ proxy_connect_timeout 120s;
147
+ proxy_send_timeout 120s;
148
+ proxy_read_timeout 600s;
149
+
150
+ # 禁用缓冲以支持流式响应
151
+ proxy_buffering off;
152
+ proxy_cache off;
153
+ }
154
+
155
+ # GraphQL 端点
156
+ location /query {
157
+ limit_req zone=api burst=30 nodelay;
158
+
159
+ proxy_pass http://axonhub_backend;
160
+ proxy_http_version 1.1;
161
+ proxy_set_header Connection "";
162
+ proxy_set_header Host $host;
163
+ proxy_set_header X-Real-IP $remote_addr;
164
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
165
+ proxy_set_header X-Forwarded-Proto $scheme;
166
+
167
+ # GraphQL 通常需要较长的处理时间
168
+ proxy_connect_timeout 60s;
169
+ proxy_send_timeout 60s;
170
+ proxy_read_timeout 300s;
171
+ }
172
+
173
+ # 登录端点限流
174
+ location /auth/login {
175
+ limit_req zone=login burst=5 nodelay;
176
+
177
+ proxy_pass http://axonhub_backend;
178
+ proxy_http_version 1.1;
179
+ proxy_set_header Connection "";
180
+ proxy_set_header Host $host;
181
+ proxy_set_header X-Real-IP $remote_addr;
182
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
183
+ proxy_set_header X-Forwarded-Proto $scheme;
184
+ }
185
+
186
+ # 静态文件缓存
187
+ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
188
+ proxy_pass http://axonhub_backend;
189
+ proxy_set_header Host $host;
190
+
191
+ # 缓存设置
192
+ expires 1y;
193
+ add_header Cache-Control "public, immutable";
194
+ add_header Vary Accept-Encoding;
195
+
196
+ # 压缩
197
+ gzip_static on;
198
+ }
199
+
200
+ # 健康检查
201
+ location /health {
202
+ access_log off;
203
+ proxy_pass http://axonhub_backend/health;
204
+ proxy_set_header Host $host;
205
+
206
+ # 快速超时
207
+ proxy_connect_timeout 5s;
208
+ proxy_send_timeout 5s;
209
+ proxy_read_timeout 5s;
210
+ }
211
+
212
+ # 监控端点(可选,需要认证)
213
+ location /metrics {
214
+ # 限制访问
215
+ allow 127.0.0.1;
216
+ allow 10.0.0.0/8;
217
+ allow 172.16.0.0/12;
218
+ allow 192.168.0.0/16;
219
+ deny all;
220
+
221
+ proxy_pass http://axonhub_backend/metrics;
222
+ proxy_set_header Host $host;
223
+ }
224
+
225
+ # 禁止访问敏感文件
226
+ location ~ /\. {
227
+ deny all;
228
+ access_log off;
229
+ log_not_found off;
230
+ }
231
+
232
+ location ~ \.(yml|yaml|json|env)$ {
233
+ deny all;
234
+ access_log off;
235
+ log_not_found off;
236
+ }
237
+ }
238
+ }
deploy/start.bat ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ setlocal
3
+
4
+ REM AxonHub Windows start wrapper (.bat)
5
+
6
+ where powershell >NUL 2>NUL
7
+ if %ERRORLEVEL% NEQ 0 (
8
+ echo [ERROR] PowerShell is required to run this script.
9
+ exit /b 1
10
+ )
11
+
12
+ set "SCRIPT_DIR=%~dp0"
13
+ powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%start.ps1" %*
14
+ exit /b %ERRORLEVEL%
deploy/start.ps1 ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ param(
2
+ [Parameter(ValueFromRemainingArguments=$true)]
3
+ [string[]]$ArgsFromCmd
4
+ )
5
+
6
+ $ErrorActionPreference = 'Stop'
7
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
8
+
9
+ function Write-Info([string]$m){ Write-Host "[INFO] $m" -ForegroundColor Cyan }
10
+ function Write-Success([string]$m){ Write-Host "[SUCCESS] $m" -ForegroundColor Green }
11
+ function Write-Warn([string]$m){ Write-Host "[WARNING] $m" -ForegroundColor Yellow }
12
+ function Write-Err([string]$m){ Write-Host "[ERROR] $m" -ForegroundColor Red }
13
+
14
+ $ServiceName = 'axonhub'
15
+ $BaseDir = Join-Path $env:LOCALAPPDATA 'AxonHub'
16
+ $ConfigFile = Join-Path $BaseDir 'config.yml'
17
+ $BinaryPath = Join-Path $BaseDir 'axonhub.exe'
18
+ $PidFile = Join-Path $BaseDir 'axonhub.pid'
19
+ $LogFile = Join-Path $BaseDir 'axonhub.log'
20
+ $DefaultPort = 8090
21
+
22
+ function Show-Usage {
23
+ Write-Host @"
24
+ Usage: start.bat
25
+
26
+ This script starts AxonHub directly (no service manager).
27
+ Logs: $LogFile
28
+ PID file: $PidFile
29
+ "@
30
+ }
31
+
32
+ foreach($a in $ArgsFromCmd){
33
+ switch -Regex ($a){
34
+ '^(--help|-h)$' { Show-Usage; exit 0 }
35
+ default { Write-Warn "Unknown option: $a" }
36
+ }
37
+ }
38
+
39
+ function Ensure-Dirs([string]$path){ if(-not (Test-Path $path)){ New-Item -ItemType Directory -Force -Path $path | Out-Null } }
40
+
41
+ function Get-ConfiguredPort {
42
+ $port = $DefaultPort
43
+ if(Test-Path $BinaryPath){
44
+ try {
45
+ $configPort = $null
46
+ $configPort = & $BinaryPath config get server.port 2>$null
47
+ if($configPort -match '^[0-9]+$'){
48
+ $port = [int]$configPort
49
+ }
50
+ } catch {}
51
+ }
52
+ return $port
53
+ }
54
+
55
+ function Check-Port([int]$port){
56
+ try {
57
+ $lines = netstat -ano | Select-String -Pattern "^\s*TCP\s+[^:]+:$port\s+" -ErrorAction SilentlyContinue
58
+ if($lines){
59
+ Write-Warn "Port $port is already in use"
60
+ Write-Info "Processes using port ${port}:"
61
+ $lines | ForEach-Object { $_.Line } | Write-Host
62
+ return $false
63
+ }
64
+ } catch {}
65
+ return $true
66
+ }
67
+
68
+ $stdoutTempFile = Join-Path $env:TEMP ("axonhub-" + [guid]::NewGuid().ToString() + '-stdout.log')
69
+ $stderrTempFile = Join-Path $env:TEMP ("axonhub-" + [guid]::NewGuid().ToString() + '-stderr.log')
70
+
71
+ Write-Info 'Starting AxonHub...'
72
+
73
+ if(-not (Test-Path $BinaryPath)){
74
+ Write-Err "AxonHub binary not found at $BinaryPath"
75
+ Write-Info 'Please run the installer first: install.bat'
76
+ exit 1
77
+ }
78
+
79
+ Ensure-Dirs $BaseDir
80
+
81
+ # Already running?
82
+ if(Test-Path $PidFile){
83
+ try {
84
+ $pid = Get-Content -Path $PidFile -ErrorAction Stop
85
+ if($pid -and (Get-Process -Id $pid -ErrorAction SilentlyContinue)){
86
+ Write-Warn "AxonHub is already running (PID: $pid)"
87
+ exit 0
88
+ } else {
89
+ Write-Info 'Removing stale PID file'
90
+ Remove-Item -Force $PidFile -ErrorAction SilentlyContinue
91
+ }
92
+ } catch {
93
+ Remove-Item -Force $PidFile -ErrorAction SilentlyContinue
94
+ }
95
+ }
96
+
97
+ # Check configured port
98
+ $port = Get-ConfiguredPort
99
+ if(-not (Check-Port $port)){
100
+ Write-Err "Cannot start AxonHub: port $port is already in use"
101
+ exit 1
102
+ }
103
+
104
+ $ConfigArgs = @()
105
+ if(Test-Path $ConfigFile){
106
+ # Config exists, binary will auto-detect it from $HOME/.config/axonhub/
107
+ Write-Info "Configuration found at $ConfigFile, binary will auto-detect it"
108
+ } else {
109
+ Write-Warn "Configuration not found at $ConfigFile, starting with defaults"
110
+ }
111
+
112
+ Write-Info 'Starting AxonHub process...'
113
+ try {
114
+ if($ConfigArgs.Count -gt 0){
115
+ $p = Start-Process -FilePath $BinaryPath -ArgumentList $ConfigArgs -RedirectStandardOutput $stdoutTempFile -RedirectStandardError $stderrTempFile -PassThru -WindowStyle Hidden
116
+ } else {
117
+ $p = Start-Process -FilePath $BinaryPath -RedirectStandardOutput $stdoutTempFile -RedirectStandardError $stderrTempFile -PassThru -WindowStyle Hidden
118
+ }
119
+ Start-Sleep -Seconds 2
120
+ if($p -and (Get-Process -Id $p.Id -ErrorAction SilentlyContinue)){
121
+ $p.Id | Out-File -FilePath $PidFile -Encoding ascii -Force
122
+ Remove-Item -Force $stdoutTempFile,$stderrTempFile -ErrorAction SilentlyContinue
123
+ Write-Success "AxonHub started successfully (PID: $($p.Id))"
124
+ Write-Info 'Process information:'
125
+ Write-Host " • PID: $($p.Id)"
126
+ Write-Host " • Log file: $LogFile"
127
+ Write-Host " • Config: " -NoNewline; if(Test-Path $ConfigFile){ Write-Host $ConfigFile } else { Write-Host 'default' }
128
+ Write-Host " • Web interface: http://localhost:$port"
129
+ Write-Host ''
130
+ Write-Info 'To stop AxonHub: stop.bat'
131
+ Write-Info "To view logs: Get-Content -Path '$LogFile' -Tail 100 -Wait"
132
+ } else {
133
+ Write-Err 'AxonHub failed to start'
134
+ if(Test-Path $LogFile){
135
+ Write-Info 'Last few log lines:'
136
+ Get-Content -Path $LogFile -Tail 20
137
+ }
138
+ if(Test-Path $stdoutTempFile){
139
+ Write-Info 'Captured stdout:'
140
+ Get-Content -Path $stdoutTempFile -Tail 20
141
+ }
142
+ if(Test-Path $stderrTempFile){
143
+ Write-Info 'Captured stderr:'
144
+ Get-Content -Path $stderrTempFile -Tail 20
145
+ }
146
+ if(Test-Path $PidFile){ Remove-Item -Force $PidFile -ErrorAction SilentlyContinue }
147
+ Remove-Item -Force $stdoutTempFile,$stderrTempFile -ErrorAction SilentlyContinue
148
+ exit 1
149
+ }
150
+ } catch {
151
+ Write-Err $_.Exception.Message
152
+ if(Test-Path $PidFile){ Remove-Item -Force $PidFile -ErrorAction SilentlyContinue }
153
+ Remove-Item -Force $stdoutTempFile,$stderrTempFile -ErrorAction SilentlyContinue
154
+ exit 1
155
+ }
deploy/start.sh ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # AxonHub Start Script
4
+ # This script starts AxonHub directly (no systemd), with proper error handling and logging
5
+
6
+ set -e
7
+
8
+ # Colors for output
9
+ RED='\033[0;31m'
10
+ GREEN='\033[0;32m'
11
+ YELLOW='\033[1;33m'
12
+ BLUE='\033[0;34m'
13
+ NC='\033[0m' # No Color
14
+
15
+ # Configuration
16
+ SERVICE_NAME="axonhub"
17
+ # Resolve non-root user's HOME when running via sudo
18
+ if [[ -n "$SUDO_USER" && "$SUDO_USER" != "root" ]]; then
19
+ USER_HOME="$(eval echo ~${SUDO_USER})"
20
+ TARGET_USER="$SUDO_USER"
21
+ else
22
+ USER_HOME="$HOME"
23
+ TARGET_USER="$USER"
24
+ fi
25
+ TARGET_GROUP="$(id -gn "$TARGET_USER" 2>/dev/null || echo "$TARGET_USER")"
26
+ BASE_DIR="${USER_HOME}/.config/axonhub"
27
+ CONFIG_FILE="${BASE_DIR}/config.yml"
28
+ BINARY_PATH="/usr/local/bin/axonhub"
29
+ DEFAULT_PORT=8090
30
+ PID_FILE="${BASE_DIR}/axonhub.pid"
31
+ LOG_FILE="${BASE_DIR}/axonhub.log"
32
+
33
+ print_info() {
34
+ echo -e "${BLUE}[INFO]${NC} $1"
35
+ }
36
+
37
+ print_success() {
38
+ echo -e "${GREEN}[SUCCESS]${NC} $1"
39
+ }
40
+
41
+ print_warning() {
42
+ echo -e "${YELLOW}[WARNING]${NC} $1"
43
+ }
44
+
45
+ print_error() {
46
+ echo -e "${RED}[ERROR]${NC} $1"
47
+ }
48
+
49
+ # Note: systemd-related logic removed for simplicity; this script always starts directly
50
+
51
+ start_directly() {
52
+ print_info "Starting AxonHub directly..."
53
+
54
+ # Check if already running
55
+ if [[ -f "$PID_FILE" ]]; then
56
+ local pid=$(cat "$PID_FILE")
57
+ if kill -0 "$pid" 2>/dev/null; then
58
+ print_warning "AxonHub is already running (PID: $pid)"
59
+ return 0
60
+ else
61
+ print_info "Removing stale PID file"
62
+ rm -f "$PID_FILE"
63
+ fi
64
+ fi
65
+
66
+ # Check if binary exists
67
+ if [[ ! -x "$BINARY_PATH" ]]; then
68
+ print_error "AxonHub binary not found at $BINARY_PATH"
69
+ print_info "Please run the install script first: ./deploy/install.sh"
70
+ return 1
71
+ fi
72
+
73
+ # Check if config exists
74
+ if [[ ! -f "$CONFIG_FILE" ]]; then
75
+ print_warning "Configuration file not found at $CONFIG_FILE"
76
+ print_info "Starting with default configuration..."
77
+ CONFIG_ARGS=""
78
+ else
79
+ # Config exists, binary will auto-detect it from $HOME/.config/axonhub/
80
+ CONFIG_ARGS=""
81
+ fi
82
+
83
+ # Ensure base directory exists and is owned by target user
84
+ mkdir -p "$BASE_DIR"
85
+ chown "$TARGET_USER:$TARGET_GROUP" "$BASE_DIR" 2>/dev/null || true
86
+
87
+ # Start AxonHub in background
88
+ print_info "Starting AxonHub process..."
89
+
90
+ if [[ $EUID -eq 0 ]]; then
91
+ # Running with sudo/root; start as invoking user so files live under their HOME
92
+ print_info "Running as user: $TARGET_USER"
93
+ sudo -u "$TARGET_USER" bash -c "mkdir -p '$BASE_DIR'; \"$BINARY_PATH\" $CONFIG_ARGS >> '$LOG_FILE' 2>&1 & echo \$! > '$PID_FILE'"
94
+ local pid
95
+ pid=$(cat "$PID_FILE" 2>/dev/null || true)
96
+ else
97
+ "$BINARY_PATH" $CONFIG_ARGS > "$LOG_FILE" 2>&1 &
98
+ local pid=$!
99
+ echo "$pid" > "$PID_FILE"
100
+ fi
101
+
102
+ # Wait a moment and check if process is still running
103
+ sleep 2
104
+
105
+ if kill -0 "$pid" 2>/dev/null; then
106
+ print_success "AxonHub started successfully (PID: $pid)"
107
+ print_info "Process information:"
108
+ echo " • PID: $pid"
109
+ echo " • Log file: $LOG_FILE"
110
+ echo " • Config: ${CONFIG_FILE:-"default"}"
111
+ local port
112
+ port=$(get_configured_port)
113
+ echo " • Web interface: http://localhost:${port}"
114
+ echo
115
+ print_info "To stop AxonHub: ./stop.sh"
116
+ print_info "To view logs: tail -f $LOG_FILE"
117
+ else
118
+ print_error "AxonHub failed to start"
119
+ if [[ -f "$LOG_FILE" ]]; then
120
+ print_info "Last few log lines:"
121
+ tail -n 10 "$LOG_FILE"
122
+ fi
123
+ rm -f "$PID_FILE"
124
+ return 1
125
+ fi
126
+ }
127
+
128
+ get_configured_port() {
129
+ local port="$DEFAULT_PORT"
130
+
131
+ # Try to get port from config using axonhub binary
132
+ if [[ -x "$BINARY_PATH" ]]; then
133
+ local config_port
134
+ config_port=$("$BINARY_PATH" config get server.port 2>/dev/null) || true
135
+ if [[ -n "$config_port" && "$config_port" =~ ^[0-9]+$ ]]; then
136
+ port="$config_port"
137
+ fi
138
+ fi
139
+
140
+ echo "$port"
141
+ }
142
+
143
+ check_port() {
144
+ local port=${1:-$DEFAULT_PORT}
145
+
146
+ if command -v netstat >/dev/null 2>&1; then
147
+ if netstat -tuln | grep -q ":$port "; then
148
+ print_warning "Port $port is already in use"
149
+ print_info "Processes using port $port:"
150
+ netstat -tulnp | grep ":$port " || true
151
+ return 1
152
+ fi
153
+ elif command -v ss >/dev/null 2>&1; then
154
+ if ss -tuln | grep -q ":$port "; then
155
+ print_warning "Port $port is already in use"
156
+ print_info "Processes using port $port:"
157
+ ss -tulnp | grep ":$port " || true
158
+ return 1
159
+ fi
160
+ fi
161
+
162
+ return 0
163
+ }
164
+
165
+ main() {
166
+ print_info "Starting AxonHub..."
167
+
168
+ # Get configured port
169
+ local port
170
+ port=$(get_configured_port)
171
+
172
+ # Check if port is available
173
+ if ! check_port "$port"; then
174
+ print_error "Cannot start AxonHub: port $port is already in use"
175
+ return 1
176
+ fi
177
+
178
+ # Always start directly
179
+ start_directly
180
+ }
181
+
182
+ # Handle script arguments
183
+ case "${1:-}" in
184
+ --help|-h)
185
+ echo "Usage: $0"
186
+ echo
187
+ echo "This script starts AxonHub directly (no systemd)."
188
+ echo "Logs: $LOG_FILE"
189
+ echo "PID file: $PID_FILE"
190
+ exit 0
191
+ ;;
192
+ "")
193
+ main
194
+ ;;
195
+ *)
196
+ print_error "Unknown option: $1"
197
+ print_info "Use --help for usage information"
198
+ exit 1
199
+ ;;
200
+ esac
deploy/stop.bat ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ setlocal
3
+
4
+ REM AxonHub Windows stop wrapper (.bat)
5
+
6
+ where powershell >NUL 2>NUL
7
+ if %ERRORLEVEL% NEQ 0 (
8
+ echo [ERROR] PowerShell is required to run this script.
9
+ exit /b 1
10
+ )
11
+
12
+ set "SCRIPT_DIR=%~dp0"
13
+ powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%stop.ps1" %*
14
+ exit /b %ERRORLEVEL%
deploy/stop.ps1 ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ param(
2
+ [Parameter(ValueFromRemainingArguments=$true)]
3
+ [string[]]$ArgsFromCmd
4
+ )
5
+
6
+ $ErrorActionPreference = 'Stop'
7
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
8
+
9
+ function Write-Info([string]$m){ Write-Host "[INFO] $m" -ForegroundColor Cyan }
10
+ function Write-Success([string]$m){ Write-Host "[SUCCESS] $m" -ForegroundColor Green }
11
+ function Write-Warn([string]$m){ Write-Host "[WARNING] $m" -ForegroundColor Yellow }
12
+ function Write-Err([string]$m){ Write-Host "[ERROR] $m" -ForegroundColor Red }
13
+
14
+ $ServiceName = 'axonhub'
15
+ $BaseDir = Join-Path $env:LOCALAPPDATA 'AxonHub'
16
+ $PidFile = Join-Path $BaseDir 'axonhub.pid'
17
+ $ProcessName = 'axonhub'
18
+
19
+ function Show-Usage {
20
+ Write-Host @"
21
+ Usage: stop.bat [--force]
22
+
23
+ This script stops AxonHub directly (no service manager).
24
+ Options:
25
+ --force Force kill all AxonHub processes
26
+ --help, -h Show this help message
27
+ "@
28
+ }
29
+
30
+ $Force = $false
31
+ foreach($a in $ArgsFromCmd){
32
+ switch -Regex ($a){
33
+ '^(--force)$' { $Force = $true; continue }
34
+ '^(--help|-h)$' { Show-Usage; exit 0 }
35
+ default { Write-Warn "Unknown option: $a" }
36
+ }
37
+ }
38
+
39
+ function Stop-ByPid(){
40
+ Write-Info 'Stopping AxonHub using PID file...'
41
+ if(-not (Test-Path $PidFile)){
42
+ Write-Warn "PID file not found at $PidFile"
43
+ return $false
44
+ }
45
+ try {
46
+ $pid = Get-Content -Path $PidFile -ErrorAction Stop
47
+ } catch {
48
+ Write-Warn 'Unable to read PID file'
49
+ Remove-Item -Force $PidFile -ErrorAction SilentlyContinue
50
+ return $false
51
+ }
52
+ if(-not ($pid -match '^[0-9]+$')){
53
+ Write-Err "Invalid PID in file: $pid"
54
+ Remove-Item -Force $PidFile -ErrorAction SilentlyContinue
55
+ return $false
56
+ }
57
+ $proc = Get-Process -Id $pid -ErrorAction SilentlyContinue
58
+ if(-not $proc){
59
+ Write-Warn "Process with PID $pid is not running"
60
+ Remove-Item -Force $PidFile -ErrorAction SilentlyContinue
61
+ return $false
62
+ }
63
+ Write-Info "Stopping process $pid ..."
64
+ try { Stop-Process -Id $pid -ErrorAction SilentlyContinue } catch {}
65
+ # Wait up to 10 seconds
66
+ $timeout = 10
67
+ for($i=0; $i -lt $timeout; $i++){
68
+ Start-Sleep -Seconds 1
69
+ if(-not (Get-Process -Id $pid -ErrorAction SilentlyContinue)){ break }
70
+ }
71
+ if(Get-Process -Id $pid -ErrorAction SilentlyContinue){
72
+ Write-Warn 'Process did not stop gracefully, forcing termination...'
73
+ try { Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue } catch {}
74
+ Start-Sleep -Seconds 2
75
+ }
76
+ if(-not (Get-Process -Id $pid -ErrorAction SilentlyContinue)){
77
+ Write-Success "AxonHub stopped successfully (PID: $pid)"
78
+ Remove-Item -Force $PidFile -ErrorAction SilentlyContinue
79
+ return $true
80
+ } else {
81
+ Write-Err 'Failed to stop AxonHub process'
82
+ return $false
83
+ }
84
+ }
85
+
86
+ function Stop-ByProcessName(){
87
+ Write-Info 'Stopping AxonHub by process name...'
88
+ $procs = Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.ProcessName -match '^axonhub' }
89
+ if(-not $procs){
90
+ Write-Warn 'No AxonHub processes found'
91
+ return $false
92
+ }
93
+ Write-Info ("Found AxonHub processes: " + ($procs.Id -join ' '))
94
+ foreach($p in $procs){
95
+ Write-Info ("Stopping process " + $p.Id + ' ...')
96
+ try { Stop-Process -Id $p.Id -ErrorAction SilentlyContinue } catch {}
97
+ # Wait up to 10 seconds each
98
+ $timeout = 10
99
+ for($i=0; $i -lt $timeout; $i++){
100
+ Start-Sleep -Seconds 1
101
+ if(-not (Get-Process -Id $p.Id -ErrorAction SilentlyContinue)){ break }
102
+ }
103
+ if(Get-Process -Id $p.Id -ErrorAction SilentlyContinue){
104
+ Write-Warn ("Process " + $p.Id + ' did not stop gracefully, forcing...')
105
+ try { Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue } catch {}
106
+ }
107
+ }
108
+ Start-Sleep -Seconds 2
109
+ $remaining = Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.ProcessName -match '^axonhub' }
110
+ if(-not $remaining){
111
+ Write-Success 'All AxonHub processes stopped successfully'
112
+ Remove-Item -Force $PidFile -ErrorAction SilentlyContinue
113
+ return $true
114
+ } else {
115
+ Write-Err ("Some AxonHub processes are still running: " + ($remaining.Id -join ' '))
116
+ return $false
117
+ }
118
+ }
119
+
120
+ function Check-Running(){
121
+ $procs = Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.ProcessName -match '^axonhub' }
122
+ if($procs){
123
+ Write-Info 'Running AxonHub processes:'
124
+ $procs | Select-Object Id,ProcessName,Path | Format-Table -AutoSize | Out-String | Write-Host
125
+ return $true
126
+ }
127
+ return $false
128
+ }
129
+
130
+ if($Force){
131
+ Write-Info 'Force stopping all AxonHub processes...'
132
+ $procs = Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.ProcessName -match '^axonhub' }
133
+ if($procs){
134
+ foreach($p in $procs){ try { Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue } catch {} }
135
+ Start-Sleep -Seconds 2
136
+ $still = Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.ProcessName -match '^axonhub' }
137
+ if(-not $still){
138
+ Write-Success 'All AxonHub processes force-stopped'
139
+ Remove-Item -Force $PidFile -ErrorAction SilentlyContinue
140
+ } else {
141
+ Write-Err 'Failed to force-stop some processes'
142
+ exit 1
143
+ }
144
+ } else {
145
+ Write-Info 'No AxonHub processes found'
146
+ }
147
+ exit 0
148
+ }
149
+
150
+ Write-Info 'Stopping AxonHub...'
151
+ $stopped = $false
152
+ if(Stop-ByPid){ $stopped = $true }
153
+ if(-not $stopped){ if(Stop-ByProcessName){ $stopped = $true } }
154
+ if(-not $stopped){
155
+ if(Check-Running){
156
+ Write-Err 'Failed to stop all AxonHub processes'
157
+ exit 1
158
+ } else {
159
+ Write-Info 'No AxonHub processes were running'
160
+ }
161
+ }
162
+ Remove-Item -Force $PidFile -ErrorAction SilentlyContinue
163
+ Write-Success 'AxonHub has been stopped'
deploy/stop.sh ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # AxonHub Stop Script
4
+ # This script stops AxonHub directly (no systemd), with proper error handling
5
+
6
+ set -e
7
+
8
+ # Colors for output
9
+ RED='\033[0;31m'
10
+ GREEN='\033[0;32m'
11
+ YELLOW='\033[1;33m'
12
+ BLUE='\033[0;34m'
13
+ NC='\033[0m' # No Color
14
+
15
+ # Configuration
16
+ SERVICE_NAME="axonhub"
17
+ # Resolve non-root user's HOME when running via sudo
18
+ if [[ -n "$SUDO_USER" && "$SUDO_USER" != "root" ]]; then
19
+ USER_HOME="$(eval echo ~${SUDO_USER})"
20
+ else
21
+ USER_HOME="$HOME"
22
+ fi
23
+ BASE_DIR="${USER_HOME}/.config/axonhub"
24
+ PID_FILE="${BASE_DIR}/axonhub.pid"
25
+ PROCESS_NAME="axonhub"
26
+
27
+ print_info() {
28
+ echo -e "${BLUE}[INFO]${NC} $1"
29
+ }
30
+
31
+ print_success() {
32
+ echo -e "${GREEN}[SUCCESS]${NC} $1"
33
+ }
34
+
35
+ print_warning() {
36
+ echo -e "${YELLOW}[WARNING]${NC} $1"
37
+ }
38
+
39
+ print_error() {
40
+ echo -e "${RED}[ERROR]${NC} $1"
41
+ }
42
+
43
+ # Note: systemd-related logic removed for simplicity; this script always stops directly
44
+
45
+ stop_by_pid() {
46
+ print_info "Stopping AxonHub using PID file..."
47
+
48
+ if [[ ! -f "$PID_FILE" ]]; then
49
+ print_warning "PID file not found at $PID_FILE"
50
+ return 1
51
+ fi
52
+
53
+ local pid=$(cat "$PID_FILE")
54
+
55
+ if [[ ! "$pid" =~ ^[0-9]+$ ]]; then
56
+ print_error "Invalid PID in file: $pid"
57
+ rm -f "$PID_FILE"
58
+ return 1
59
+ fi
60
+
61
+ if ! kill -0 "$pid" 2>/dev/null; then
62
+ print_warning "Process with PID $pid is not running"
63
+ rm -f "$PID_FILE"
64
+ return 1
65
+ fi
66
+
67
+ print_info "Sending SIGTERM to process $pid..."
68
+ if kill -TERM "$pid" 2>/dev/null; then
69
+ # Wait for graceful shutdown
70
+ local timeout=10
71
+ local count=0
72
+
73
+ while kill -0 "$pid" 2>/dev/null && [[ $count -lt $timeout ]]; do
74
+ sleep 1
75
+ ((count++))
76
+ done
77
+
78
+ if kill -0 "$pid" 2>/dev/null; then
79
+ print_warning "Process did not stop gracefully, sending SIGKILL..."
80
+ kill -KILL "$pid" 2>/dev/null || true
81
+ sleep 2
82
+ fi
83
+
84
+ if ! kill -0 "$pid" 2>/dev/null; then
85
+ print_success "AxonHub stopped successfully (PID: $pid)"
86
+ rm -f "$PID_FILE"
87
+ else
88
+ print_error "Failed to stop AxonHub process"
89
+ return 1
90
+ fi
91
+ else
92
+ print_error "Failed to send signal to process $pid"
93
+ return 1
94
+ fi
95
+ }
96
+
97
+ stop_by_process_name() {
98
+ print_info "Stopping AxonHub by process name..."
99
+
100
+ local pids
101
+ pids=$(pgrep -f "$PROCESS_NAME" 2>/dev/null || true)
102
+
103
+ if [[ -z "$pids" ]]; then
104
+ print_warning "No AxonHub processes found"
105
+ return 1
106
+ fi
107
+
108
+ print_info "Found AxonHub processes: $pids"
109
+
110
+ for pid in $pids; do
111
+ print_info "Stopping process $pid..."
112
+
113
+ if kill -TERM "$pid" 2>/dev/null; then
114
+ # Wait for graceful shutdown
115
+ local timeout=10
116
+ local count=0
117
+
118
+ while kill -0 "$pid" 2>/dev/null && [[ $count -lt $timeout ]]; do
119
+ sleep 1
120
+ ((count++))
121
+ done
122
+
123
+ if kill -0 "$pid" 2>/dev/null; then
124
+ print_warning "Process $pid did not stop gracefully, sending SIGKILL..."
125
+ kill -KILL "$pid" 2>/dev/null || true
126
+ fi
127
+ fi
128
+ done
129
+
130
+ sleep 2
131
+
132
+ # Check if any processes are still running
133
+ local remaining_pids
134
+ remaining_pids=$(pgrep -f "$PROCESS_NAME" 2>/dev/null || true)
135
+
136
+ if [[ -z "$remaining_pids" ]]; then
137
+ print_success "All AxonHub processes stopped successfully"
138
+ rm -f "$PID_FILE"
139
+ else
140
+ print_error "Some AxonHub processes are still running: $remaining_pids"
141
+ return 1
142
+ fi
143
+ }
144
+
145
+ check_running_processes() {
146
+ local pids
147
+ pids=$(pgrep -f "$PROCESS_NAME" 2>/dev/null || true)
148
+
149
+ if [[ -n "$pids" ]]; then
150
+ print_info "Running AxonHub processes:"
151
+ ps -p $pids -o pid,ppid,cmd --no-headers 2>/dev/null || true
152
+ return 0
153
+ else
154
+ return 1
155
+ fi
156
+ }
157
+
158
+ main() {
159
+ print_info "Stopping AxonHub..."
160
+
161
+ local stopped=false
162
+
163
+ # Try PID file first
164
+ if stop_by_pid; then
165
+ stopped=true
166
+ fi
167
+
168
+ # If PID file method didn't work, try by process name
169
+ if [[ "$stopped" != true ]]; then
170
+ if stop_by_process_name; then
171
+ stopped=true
172
+ fi
173
+ fi
174
+
175
+ # Final check
176
+ if [[ "$stopped" != true ]]; then
177
+ if check_running_processes; then
178
+ print_error "Failed to stop all AxonHub processes"
179
+ return 1
180
+ else
181
+ print_info "No AxonHub processes were running"
182
+ fi
183
+ fi
184
+
185
+ # Clean up PID file
186
+ rm -f "$PID_FILE"
187
+
188
+ print_success "AxonHub has been stopped"
189
+ }
190
+
191
+ # Handle script arguments
192
+ case "${1:-}" in
193
+ --force)
194
+ print_info "Force stopping all AxonHub processes..."
195
+ if check_running_processes; then
196
+ pkill -KILL -f "$PROCESS_NAME" 2>/dev/null || true
197
+ sleep 2
198
+ if ! check_running_processes; then
199
+ print_success "All AxonHub processes force-stopped"
200
+ rm -f "$PID_FILE"
201
+ else
202
+ print_error "Failed to force-stop some processes"
203
+ exit 1
204
+ fi
205
+ else
206
+ print_info "No AxonHub processes found"
207
+ fi
208
+ ;;
209
+ --help|-h)
210
+ echo "Usage: $0 [--force]"
211
+ echo
212
+ echo "This script stops AxonHub directly (no systemd)."
213
+ echo "Options:"
214
+ echo " --force Force kill all AxonHub processes"
215
+ echo " --help, -h Show this help message"
216
+ exit 0
217
+ ;;
218
+ "")
219
+ main
220
+ ;;
221
+ *)
222
+ print_error "Unknown option: $1"
223
+ print_info "Use --help for usage information"
224
+ exit 1
225
+ ;;
226
+ esac
docker-compose.yml ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AxonHub Docker Compose Configuration
2
+ #
3
+ # CONFIGURATION OPTIONS:
4
+ # 1. 环境变量 - 适用于敏感配置 (如数据库DSN、密码等),优先级高于配置文件
5
+ # 参考: https://github.com/looplj/axonhub/blob/main/docs/deployment/configuration.md#configuration-priority
6
+ #
7
+ # 2. 配置文件 - 适用于复杂配置 (如缓存、日志、监控等)
8
+ # 创建 config.yml 文件并取消 volumes 配置的注释
9
+ # 配置参考: https://github.com/looplj/axonhub/blob/main/docs/deployment/configuration.md
10
+ #
11
+ # 3. 简单部署 - 如果不需要复杂配置,可以注释掉 config 的 volumes 配置
12
+ # 仅使用环境变量进行基本配置即可运行
13
+
14
+ services:
15
+ # PostgreSQL 数据库
16
+ postgres:
17
+ image: postgres:16-alpine
18
+ container_name: axonhub-postgres
19
+ environment:
20
+ POSTGRES_DB: axonhub
21
+ POSTGRES_USER: axonhub
22
+ POSTGRES_PASSWORD: ${DB_PASSWORD:-axonhub_password}
23
+ volumes:
24
+ - postgres_data:/var/lib/postgresql/data
25
+ ports:
26
+ - "5432:5432"
27
+ networks:
28
+ - axonhub-network
29
+ restart: unless-stopped
30
+ healthcheck:
31
+ test: ["CMD-SHELL", "pg_isready -U axonhub"]
32
+ interval: 10s
33
+ timeout: 5s
34
+ retries: 5
35
+
36
+ # AxonHub 主服务
37
+ axonhub:
38
+ image: looplj/axonhub:latest
39
+ container_name: axonhub-app
40
+ environment:
41
+ # 数据库配置可以通过环境变量设置,优先级高于配置文件
42
+ AXONHUB_DB_DIALECT: postgres
43
+ AXONHUB_DB_DSN: postgres://axonhub:${DB_PASSWORD:-axonhub_password}@postgres:5432/axonhub?sslmode=disable
44
+ ports:
45
+ - "8090:8090"
46
+ volumes:
47
+ # 配置文件挂载 - 用于复杂配置,如缓存、日志、监控等
48
+ # 参考: https://github.com/looplj/axonhub/blob/main/docs/deployment/configuration.md
49
+ - ./config.yml:/app/config.yml:ro
50
+ # - axonhub_data:/data
51
+ networks:
52
+ - axonhub-network
53
+ depends_on:
54
+ postgres:
55
+ condition: service_healthy
56
+ restart: unless-stopped
57
+ healthcheck:
58
+ test:
59
+ [
60
+ "CMD",
61
+ "wget",
62
+ "--no-verbose",
63
+ "--tries=1",
64
+ "--spider",
65
+ "http://localhost:8090/health",
66
+ ]
67
+ interval: 30s
68
+ timeout: 10s
69
+ retries: 3
70
+ start_period: 40s
71
+
72
+ # SQLite 配置示例 (取消注释以使用 SQLite)
73
+ # 注意: 此服务与上面的 'axonhub' 服务冲突。请只启用其中一个。
74
+ # 先执行
75
+ # mkdir data
76
+ # chmod 777 data
77
+ # axonhub-sqlite:
78
+ # image: looplj/axonhub:latest
79
+ # container_name: axonhub-sqlite-app
80
+ # environment:
81
+ # AXONHUB_DB_DIALECT: sqlite3
82
+ # AXONHUB_DB_DSN: file:/data/axonhub.db?cache=shared&_fk=1
83
+ # ports:
84
+ # - "8090:8090"
85
+ # volumes:
86
+ # # 配置文件挂载 - 用于复杂配置
87
+ # # 参考: https://github.com/looplj/axonhub/blob/main/docs/deployment/configuration.md
88
+ # - ./config.yml:/app/config.yml:ro
89
+ # - ./data:/data
90
+ # restart: unless-stopped
91
+ # healthcheck:
92
+ # test:
93
+ # [
94
+ # "CMD",
95
+ # "wget",
96
+ # "--no-verbose",
97
+ # "--tries=1",
98
+ # "--spider",
99
+ # "http://localhost:8090/health",
100
+ # ]
101
+ # interval: 30s
102
+ # timeout: 10s
103
+ # retries: 3
104
+ # start_period: 40s
105
+
106
+ # MySQL 配置示例 (取消注释以使用 MySQL)
107
+ # 注意: 此服务与上面的 'axonhub' 服务冲突。请只启用其中一个。
108
+ # mysql:
109
+ # image: mysql:8.0
110
+ # container_name: axonhub-mysql
111
+ # environment:
112
+ # MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-axonhub_root_password}
113
+ # MYSQL_DATABASE: axonhub
114
+ # MYSQL_USER: axonhub
115
+ # MYSQL_PASSWORD: ${MYSQL_PASSWORD:-axonhub_password}
116
+ # volumes:
117
+ # - mysql_data:/var/lib/mysql
118
+ # ports:
119
+ # - "3306:3306"
120
+ # networks:
121
+ # - axonhub-network
122
+ # restart: unless-stopped
123
+ # healthcheck:
124
+ # test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD:-axonhub_root_password}"]
125
+ # interval: 10s
126
+ # timeout: 5s
127
+ # retries: 5
128
+ #
129
+ # # AxonHub 主服务 (MySQL 版本)
130
+ # # 注意: 取消注释此服务时,请注释掉上面的 PostgreSQL 版本 axonhub 服务
131
+ # axonhub-mysql:
132
+ # image: looplj/axonhub:latest
133
+ # container_name: axonhub-mysql-app
134
+ # environment:
135
+ # AXONHUB_DB_DIALECT: mysql
136
+ # # DSN 格式参考: ${MYSQL_USER}:${MYSQL_PASSWORD}@tcp(mysql:3306)/${MYSQL_DATABASE}?charset=utf8mb4&parseTime=True&loc=Local
137
+ # AXONHUB_DB_DSN: axonhub:${MYSQL_PASSWORD:-axonhub_password}@tcp(mysql:3306)/axonhub?charset=utf8mb4&parseTime=True&loc=Local
138
+ # ports:
139
+ # - "8090:8090"
140
+ # volumes:
141
+ # # 配置文件挂载 - 用于复杂配置
142
+ # # 参考: https://github.com/looplj/axonhub/blob/main/docs/deployment/configuration.md
143
+ # - ./config.yml:/app/config.yml:ro
144
+ # # - axonhub_data:/data
145
+ # networks:
146
+ # - axonhub-network
147
+ # depends_on:
148
+ # mysql:
149
+ # condition: service_healthy
150
+ # restart: unless-stopped
151
+ # healthcheck:
152
+ # test:
153
+ # [
154
+ # "CMD",
155
+ # "wget",
156
+ # "--no-verbose",
157
+ # "--tries=1",
158
+ # "--spider",
159
+ # "http://localhost:8090/health",
160
+ # ]
161
+ # interval: 30s
162
+ # timeout: 10s
163
+ # retries: 3
164
+ # start_period: 40s
165
+
166
+ volumes:
167
+ postgres_data:
168
+ mysql_data:
169
+
170
+ networks:
171
+ axonhub-network:
172
+ driver: bridge
docs/axonhub-architecture-light.svg ADDED
docs/axonhub-architecture.svg ADDED
docs/en/api-reference/anthropic-api.md ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Anthropic API Reference
2
+
3
+ ## Overview
4
+
5
+ AxonHub supports the native Anthropic Messages API for applications that prefer Anthropic's specific features and response format. You can use the Anthropic SDK to access not only Claude models but also OpenAI, Gemini, and other supported models.
6
+
7
+ ## Key Benefits
8
+
9
+ - **API Interoperability**: Use Anthropic Messages API to call OpenAI, Gemini, and other supported models
10
+ - **Zero Code Changes**: Continue using your existing Anthropic client SDK without modification
11
+ - **Automatic Translation**: AxonHub automatically converts between API formats when needed
12
+ - **Provider Flexibility**: Access any supported AI provider using the Anthropic API format
13
+
14
+ ## Supported Endpoints
15
+
16
+ **Endpoints:**
17
+ - `POST /anthropic/v1/messages` - Text generation
18
+ - `POST /v1/messages` - Text generation (alternative)
19
+ - `GET /anthropic/v1/models` - List available models
20
+
21
+ **Example Request:**
22
+ ```go
23
+ import (
24
+ "github.com/anthropics/anthropic-sdk-go"
25
+ "github.com/anthropics/anthropic-sdk-go/option"
26
+ )
27
+
28
+ // Create Anthropic client with AxonHub configuration
29
+ client := anthropic.NewClient(
30
+ option.WithAPIKey("your-axonhub-api-key"),
31
+ option.WithBaseURL("http://localhost:8090/anthropic"),
32
+
33
+ )
34
+
35
+ // Call OpenAI model using Anthropic API format
36
+ messages := []anthropic.MessageParam{
37
+ anthropic.NewUserMessage(anthropic.NewTextBlock("Hello, GPT!")),
38
+ }
39
+
40
+ response, err := client.Messages.New(ctx, anthropic.MessageNewParams{
41
+ Model: anthropic.Model("gpt-4o"),
42
+ Messages: messages,
43
+ MaxTokens: 1024,
44
+ })
45
+ if err != nil {
46
+ // Handle error appropriately
47
+ panic(err)
48
+ }
49
+
50
+ // Extract text content from response
51
+ responseText := ""
52
+ for _, block := range response.Content {
53
+ if textBlock := block.AsText(); textBlock != nil {
54
+ responseText += textBlock.Text
55
+ }
56
+ }
57
+ fmt.Println(responseText)
58
+ ```
59
+
60
+ ## API Translation Capabilities
61
+
62
+ AxonHub automatically translates between API formats, enabling powerful scenarios:
63
+
64
+ ### Use Anthropic SDK with OpenAI Models
65
+ ```go
66
+ // Anthropic SDK calling OpenAI model
67
+ messages := []anthropic.MessageParam{
68
+ anthropic.NewUserMessage(anthropic.NewTextBlock("What is machine learning?")),
69
+ }
70
+
71
+ response, err := client.Messages.New(ctx, anthropic.MessageNewParams{
72
+ Model: anthropic.Model("gpt-4o"), // OpenAI model
73
+ Messages: messages,
74
+ MaxTokens: 1024,
75
+ })
76
+
77
+ // Access response
78
+ for _, block := range response.Content {
79
+ if textBlock := block.AsText(); textBlock != nil {
80
+ fmt.Println(textBlock.Text)
81
+ }
82
+ }
83
+ // AxonHub automatically translates Anthropic format → OpenAI format
84
+ ```
85
+
86
+ ### Use Anthropic SDK with Gemini Models
87
+ ```go
88
+ // Anthropic SDK calling Gemini model
89
+ messages := []anthropic.MessageParam{
90
+ anthropic.NewUserMessage(anthropic.NewTextBlock("Explain quantum computing")),
91
+ }
92
+
93
+ response, err := client.Messages.New(ctx, anthropic.MessageNewParams{
94
+ Model: anthropic.Model("gemini-2.5"), // Gemini model
95
+ Messages: messages,
96
+ MaxTokens: 1024,
97
+ })
98
+
99
+ // Access response
100
+ for _, block := range response.Content {
101
+ if textBlock := block.AsText(); textBlock != nil {
102
+ fmt.Println(textBlock.Text)
103
+ }
104
+ }
105
+ // AxonHub automatically translates Anthropic format → Gemini format
106
+ ```
107
+
108
+ ## Authentication
109
+
110
+ The Anthropic API format uses the following authentication:
111
+
112
+ - **Header**: `X-API-Key: <your-api-key>`
113
+
114
+ The API keys are managed through AxonHub's API Key management system and provide the same permissions regardless of which API format you use.
115
+
116
+ ## Streaming Support
117
+
118
+ Anthropic API format supports streaming responses:
119
+
120
+ ```go
121
+ // Anthropic SDK streaming
122
+ messages := []anthropic.MessageParam{
123
+ anthropic.NewUserMessage(anthropic.NewTextBlock("Count to five")),
124
+ }
125
+
126
+ stream := client.Messages.NewStreaming(ctx, anthropic.MessageNewParams{
127
+ Model: anthropic.Model("gpt-4o"),
128
+ Messages: messages,
129
+ MaxTokens: 1024,
130
+ })
131
+
132
+ // Collect streamed content
133
+ var content string
134
+ for stream.Next() {
135
+ event := stream.Current()
136
+ switch event := event.(type) {
137
+ case anthropic.ContentBlockDeltaEvent:
138
+ if event.Type == "content_block_delta" {
139
+ content += event.Delta.Text
140
+ fmt.Print(event.Delta.Text) // Print as it streams
141
+ }
142
+ }
143
+ }
144
+
145
+ if err := stream.Err(); err != nil {
146
+ panic(err)
147
+ }
148
+
149
+ fmt.Println("\nComplete response:", content)
150
+ ```
151
+
152
+ ## Error Handling
153
+
154
+ Anthropic format error responses:
155
+
156
+ ```json
157
+ {
158
+ "type": "error",
159
+ "error": {
160
+ "type": "invalid_request_error",
161
+ "message": "Invalid API key"
162
+ }
163
+ }
164
+ ```
165
+
166
+ ## Tool Support
167
+
168
+ AxonHub supports **function tools** (custom function calling) through the Anthropic API format. However, provider-specific tools are **not supported**:
169
+
170
+ | Tool Type | Support Status | Notes |
171
+ | --------- | -------------- | ----- |
172
+ | **Function Tools** | �� Supported | Custom function definitions work across all providers |
173
+ | **Web Search** | ❌ Not Supported | Provider-specific (OpenAI, Anthropic, etc.) |
174
+ | **Code Interpreter** | ❌ Not Supported | Provider-specific (OpenAI, Anthropic, etc.) |
175
+ | **File Search** | ❌ Not Supported | Provider-specific |
176
+ | **Computer Use** | ❌ Not Supported | Anthropic-specific |
177
+
178
+ > **Note**: Only generic function tools that can be translated across providers are supported. Provider-specific tools like web search, code interpreter, and computer use require direct access to the provider's infrastructure and cannot be proxied through AxonHub.
179
+
180
+ ## Best Practices
181
+
182
+ 1. **Use Tracing Headers**: Include `AH-Trace-Id` and `AH-Thread-Id` headers for better observability
183
+ 2. **Model Selection**: Specify the target model explicitly in your requests
184
+ 3. **Error Handling**: Implement proper error handling for API responses
185
+ 4. **Streaming**: Use streaming for better user experience with long responses
186
+ 5. **Use Function Tools**: For tool calling, use generic function tools instead of provider-specific tools
187
+
188
+ ## Migration Guide
189
+
190
+ ### From Anthropic to AxonHub
191
+ ```go
192
+ // Before: Direct Anthropic
193
+ client := anthropic.NewClient(
194
+ option.WithAPIKey("anthropic-key"),
195
+ )
196
+
197
+ // After: AxonHub with Anthropic API
198
+ client := anthropic.NewClient(
199
+ option.WithAPIKey("axonhub-api-key"),
200
+ option.WithBaseURL("http://localhost:8090/anthropic"),
201
+ )
202
+ // Your existing code continues to work!
203
+ ```
docs/en/api-reference/embedding-api.md ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Embedding API Reference
2
+
3
+ ## Overview
4
+
5
+ AxonHub provides comprehensive support for text and multimodal embedding generation through OpenAI-compatible and Jina AI-specific APIs.
6
+
7
+ ## Key Benefits
8
+
9
+ - **OpenAI Compatibility**: Use existing OpenAI SDKs without modification
10
+ - **Jina AI Support**: Native Jina embedding format support for specialized use cases
11
+ - **Multiple Input Types**: Support for single text, text arrays, token arrays, and multiple token arrays
12
+ - **Flexible Output Formats**: Choose between float arrays or base64-encoded embeddings
13
+
14
+ ## Supported Endpoints
15
+
16
+ **Endpoints:**
17
+ - `POST /v1/embeddings` - OpenAI-compatible embedding API
18
+ - `POST /jina/v1/embeddings` - Jina AI-specific embedding API
19
+
20
+ ## Request Format
21
+
22
+ ```json
23
+ {
24
+ "input": "The text to embed",
25
+ "model": "text-embedding-3-small",
26
+ "encoding_format": "float",
27
+ "dimensions": 1536,
28
+ "user": "user-id"
29
+ }
30
+ ```
31
+
32
+ **Parameters:**
33
+
34
+ | Parameter | Type | Required | Description |
35
+ |-----------|------|----------|-------------|
36
+ | `input` | string \| string[] \| number[] \| number[][] | ✅ | The text(s) to embed. Can be a single string, array of strings, token array, or multiple token arrays. |
37
+ | `model` | string | ✅ | The model to use for embedding generation. |
38
+ | `encoding_format` | string | ❌ | Format to return embeddings in. Either `float` or `base64`. Default: `float`. |
39
+ | `dimensions` | integer | ❌ | Number of dimensions for the output embeddings. |
40
+ | `user` | string | ❌ | Unique identifier for the end-user. |
41
+
42
+ **Jina-Specific Parameters:**
43
+
44
+ | Parameter | Type | Required | Description |
45
+ |-----------|------|----------|-------------|
46
+ | `task` | string | ❌ | Task type for Jina embeddings. Options: `text-matching`, `retrieval.query`, `retrieval.passage`, `separation`, `classification`, `none`. |
47
+
48
+ ## Response Format
49
+
50
+ ```json
51
+ {
52
+ "object": "list",
53
+ "data": [
54
+ {
55
+ "object": "embedding",
56
+ "embedding": [0.123, 0.456, ...],
57
+ "index": 0
58
+ }
59
+ ],
60
+ "model": "text-embedding-3-small",
61
+ "usage": {
62
+ "prompt_tokens": 4,
63
+ "total_tokens": 4
64
+ }
65
+ }
66
+ ```
67
+
68
+ ## Examples
69
+
70
+ ### OpenAI SDK (Python)
71
+
72
+ ```python
73
+ import openai
74
+
75
+ client = openai.OpenAI(
76
+ api_key="your-axonhub-api-key",
77
+ base_url="http://localhost:8090/v1"
78
+ )
79
+
80
+ response = client.embeddings.create(
81
+ input="Hello, world!",
82
+ model="text-embedding-3-small"
83
+ )
84
+
85
+ print(response.data[0].embedding[:5]) # First 5 dimensions
86
+ ```
87
+
88
+ ### OpenAI SDK (Go)
89
+
90
+ ```go
91
+ package main
92
+
93
+ import (
94
+ "context"
95
+ "fmt"
96
+ "log"
97
+
98
+ "github.com/openai/openai-go"
99
+ "github.com/openai/openai-go/option"
100
+ )
101
+
102
+ func main() {
103
+ client := openai.NewClient(
104
+ option.WithAPIKey("your-axonhub-api-key"),
105
+ option.WithBaseURL("http://localhost:8090/v1"),
106
+ )
107
+
108
+ embedding, err := client.Embeddings.New(context.TODO(), openai.EmbeddingNewParams{
109
+ Input: openai.Union[string](openai.String("Hello, world!")),
110
+ Model: openai.String("text-embedding-3-small"),
111
+ option.WithHeader("AH-Trace-Id", "trace-example-123"),
112
+ option.WithHeader("AH-Thread-Id", "thread-example-abc"),
113
+ })
114
+ if err != nil {
115
+ log.Fatal(err)
116
+ }
117
+
118
+ fmt.Printf("Embedding dimensions: %d\n", len(embedding.Data[0].Embedding))
119
+ fmt.Printf("First 5 values: %v\n", embedding.Data[0].Embedding[:5])
120
+ }
121
+ ```
122
+
123
+ ### Multiple Texts
124
+
125
+ ```python
126
+ response = client.embeddings.create(
127
+ input=["Hello, world!", "How are you?"],
128
+ model="text-embedding-3-small"
129
+ )
130
+
131
+ for i, data in enumerate(response.data):
132
+ print(f"Text {i}: {data.embedding[:3]}...")
133
+ ```
134
+
135
+ ### Jina-Specific Task
136
+
137
+ ```python
138
+ import requests
139
+
140
+ response = requests.post(
141
+ "http://localhost:8090/jina/v1/embeddings",
142
+ headers={
143
+ "Authorization": "Bearer your-axonhub-api-key",
144
+ "Content-Type": "application/json"
145
+ },
146
+ json={
147
+ "input": "What is machine learning?",
148
+ "model": "jina-embeddings-v2-base-en",
149
+ "task": "retrieval.query"
150
+ }
151
+ )
152
+
153
+ result = response.json()
154
+ print(result["data"][0]["embedding"][:5])
155
+ ```
156
+
157
+ ## Authentication
158
+
159
+ The Embedding API uses Bearer token authentication:
160
+
161
+ - **Header**: `Authorization: Bearer <your-api-key>`
162
+
163
+ The API keys are managed through AxonHub's API Key management system.
164
+
165
+ ## Best Practices
166
+
167
+ 1. **Use Tracing Headers**: Include `AH-Trace-Id` and `AH-Thread-Id` headers for better observability
168
+ 2. **Batch Requests**: When embedding multiple texts, send them in a single request for better performance
169
+ 3. **Choose Appropriate Dimensions**: Use the `dimensions` parameter to reduce embedding size if full dimensionality isn't needed
170
+ 4. **Select Proper Encoding**: Use `base64` encoding if you need to transmit embeddings over the network to reduce payload size
171
+ 5. **Jina Task Types**: When using Jina embeddings, select the appropriate `task` type for your use case to optimize retrieval quality
172
+
173
+ ## Related Resources
174
+
175
+ - [OpenAI API](openai-api.md)
176
+ - [Rerank API](rerank-api.md)
docs/en/api-reference/gemini-api.md ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Gemini API Reference
2
+
3
+ ## Overview
4
+
5
+ AxonHub provides native support for the Gemini API, enabling access to Gemini's powerful multi-modal capabilities. You can use the Gemini SDK to access not only Gemini models but also OpenAI, Anthropic, and other supported models.
6
+
7
+ ## Key Benefits
8
+
9
+ - **API Interoperability**: Use Gemini API to call OpenAI, Anthropic, and other supported models
10
+ - **Zero Code Changes**: Continue using your existing Gemini client SDK without modification
11
+ - **Automatic Translation**: AxonHub automatically converts between API formats when needed
12
+ - **Multi-modal Support**: Access text and image capabilities through the Gemini API format
13
+
14
+ ## Supported Endpoints
15
+
16
+ **Endpoints:**
17
+ - `POST /gemini/v1beta/models/{model}:generateContent` - Text and multi-modal content generation
18
+ - `POST /v1beta/models/{model}:generateContent` - Text and multi-modal content generation (alternative)
19
+ - `GET /gemini/v1beta/models` - List available models
20
+ - `GET /v1beta/models` - List available models (alternative)
21
+
22
+ **Example Request:**
23
+ ```go
24
+ import (
25
+ "context"
26
+ "google.golang.org/genai"
27
+ )
28
+
29
+ // Create Gemini client with AxonHub configuration
30
+ ctx := context.Background()
31
+ client, err := genai.NewClient(ctx, &genai.ClientConfig{
32
+ APIKey: "your-axonhub-api-key",
33
+ Backend: genai.Backend(genai.APIBackendUnspecified), // Use default backend
34
+ HTTPOptions: genai.HTTPOptions{
35
+ BaseURL: "http://localhost:8090/gemini",
36
+ },
37
+ })
38
+ if err != nil {
39
+ // Handle error appropriately
40
+ panic(err)
41
+ }
42
+
43
+ // Call OpenAI model using Gemini API format
44
+ modelName := "gpt-4o" // OpenAI model accessed via Gemini API format
45
+ content := &genai.Content{
46
+ Parts: []*genai.Part{
47
+ {Text: genai.Ptr("Hello, GPT!")},
48
+ },
49
+ }
50
+
51
+ // Optional: Configure generation parameters
52
+ config := &genai.GenerateContentConfig{
53
+ Temperature: genai.Ptr(float32(0.7)),
54
+ MaxOutputTokens: genai.Ptr(int32(1024)),
55
+ }
56
+
57
+ response, err := client.Models.GenerateContent(ctx, modelName, []*genai.Content{content}, config)
58
+ if err != nil {
59
+ // Handle error appropriately
60
+ panic(err)
61
+ }
62
+
63
+ // Extract text from response
64
+ if len(response.Candidates) > 0 &&
65
+ len(response.Candidates[0].Content.Parts) > 0 {
66
+ responseText := response.Candidates[0].Content.Parts[0].Text
67
+ fmt.Println(*responseText)
68
+ }
69
+ ```
70
+
71
+ **Example: Multi-turn Conversation**
72
+ ```go
73
+ // Create a chat session with conversation history
74
+ modelName := "claude-3-5-sonnet"
75
+ config := &genai.GenerateContentConfig{
76
+ Temperature: genai.Ptr(float32(0.5)),
77
+ }
78
+
79
+ chat, err := client.Chats.Create(ctx, modelName, config, nil)
80
+ if err != nil {
81
+ panic(err)
82
+ }
83
+
84
+ // First message
85
+ response1, err := chat.SendMessage(ctx, genai.Part{Text: genai.Ptr("My name is Alice")})
86
+ if err != nil {
87
+ panic(err)
88
+ }
89
+
90
+ // Follow-up message (model remembers context)
91
+ response2, err := chat.SendMessage(ctx, genai.Part{Text: genai.Ptr("What is my name?")})
92
+ if err != nil {
93
+ panic(err)
94
+ }
95
+
96
+ // Extract response
97
+ if len(response2.Candidates) > 0 {
98
+ text := response2.Candidates[0].Content.Parts[0].Text
99
+ fmt.Println(*text) // Should contain "Alice"
100
+ }
101
+ ```
102
+
103
+ ## API Translation Capabilities
104
+
105
+ AxonHub automatically translates between API formats, enabling powerful scenarios:
106
+
107
+ ### Use Gemini SDK with OpenAI Models
108
+ ```go
109
+ // Gemini SDK calling OpenAI model
110
+ content := &genai.Content{
111
+ Parts: []*genai.Part{
112
+ {Text: genai.Ptr("Explain neural networks")},
113
+ },
114
+ }
115
+
116
+ response, err := client.Models.GenerateContent(
117
+ ctx,
118
+ "gpt-4o", // OpenAI model
119
+ []*genai.Content{content},
120
+ nil,
121
+ )
122
+
123
+ // Access response
124
+ if len(response.Candidates) > 0 &&
125
+ len(response.Candidates[0].Content.Parts) > 0 {
126
+ text := response.Candidates[0].Content.Parts[0].Text
127
+ fmt.Println(*text)
128
+ }
129
+ // AxonHub automatically translates Gemini format → OpenAI format
130
+ ```
131
+
132
+ ### Use Gemini SDK with Anthropic Models
133
+ ```go
134
+ // Gemini SDK calling Anthropic model
135
+ content := &genai.Content{
136
+ Parts: []*genai.Part{
137
+ {Text: genai.Ptr("What is artificial intelligence?")},
138
+ },
139
+ }
140
+
141
+ response, err := client.Models.GenerateContent(
142
+ ctx,
143
+ "claude-3-5-sonnet", // Anthropic model
144
+ []*genai.Content{content},
145
+ nil,
146
+ )
147
+
148
+ // Access response
149
+ if len(response.Candidates) > 0 &&
150
+ len(response.Candidates[0].Content.Parts) > 0 {
151
+ text := response.Candidates[0].Content.Parts[0].Text
152
+ fmt.Println(*text)
153
+ }
154
+ // AxonHub automatically translates Gemini format → Anthropic format
155
+ ```
156
+
157
+ ## Authentication
158
+
159
+ The Gemini API format uses the following authentication:
160
+
161
+ - **Header**: `X-Goog-API-Key: <your-api-key>`
162
+
163
+ The API keys are managed through AxonHub's API Key management system and provide the same permissions regardless of which API format you use.
164
+
165
+ ## Streaming Support
166
+
167
+ Gemini API format supports streaming responses for real-time content generation.
168
+
169
+ ## Error Handling
170
+
171
+ Gemini format error responses follow the standard Gemini API error format.
172
+
173
+ ## Tool Support
174
+
175
+ AxonHub supports **function tools** (custom function calling) through the Gemini API format. However, provider-specific tools are **not supported**:
176
+
177
+ | Tool Type | Support Status | Notes |
178
+ | --------- | -------------- | ----- |
179
+ | **Function Tools** | ✅ Supported | Custom function definitions work across all providers |
180
+ | **Web Search** | ❌ Not Supported | Provider-specific |
181
+ | **Code Interpreter** | ❌ Not Supported | Provider-specific |
182
+ | **File Search** | ❌ Not Supported | Provider-specific |
183
+ | **Computer Use** | ❌ Not Supported | Anthropic-specific |
184
+
185
+ > **Note**: Only generic function tools that can be translated across providers are supported. Provider-specific tools require direct access to the provider's infrastructure and cannot be proxied through AxonHub.
186
+
187
+ ## Best Practices
188
+
189
+ 1. **Use Tracing Headers**: Include `AH-Trace-Id` and `AH-Thread-Id` headers for better observability
190
+ 2. **Model Selection**: Specify the target model explicitly in your requests
191
+ 3. **Error Handling**: Implement proper error handling for API responses
192
+ 4. **Streaming**: Use streaming for better user experience with long responses
193
+ 5. **Multi-modal Content**: Leverage Gemini API's multi-modal capabilities when working with images
194
+
195
+ ## Migration Guide
196
+
197
+ ### From Gemini to AxonHub
198
+ ```go
199
+ // Before: Direct Gemini
200
+ ctx := context.Background()
201
+ client, err := genai.NewClient(ctx, &genai.ClientConfig{
202
+ APIKey: "gemini-api-key",
203
+ })
204
+
205
+ // After: AxonHub with Gemini API
206
+ ctx := context.Background()
207
+ client, err := genai.NewClient(ctx, &genai.ClientConfig{
208
+ APIKey: "your-axonhub-api-key",
209
+ HTTPOptions: genai.HTTPOptions{
210
+ BaseURL: "http://localhost:8090/gemini",
211
+ },
212
+ })
213
+ // Your existing code continues to work!
214
+ ```