Compare commits
868 Commits
release/0.
...
v0.8.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e404fa3ac0 | ||
|
|
7913811549 | ||
|
|
af49d15563 | ||
|
|
6e58240c38 | ||
|
|
3aea725f7b | ||
|
|
fd093b3325 | ||
|
|
0136b87311 | ||
|
|
aeecffb517 | ||
|
|
5f5b28ced3 | ||
|
|
15fba1d89f | ||
|
|
f08bcb5e10 | ||
|
|
7017db418b | ||
|
|
6cd2835066 | ||
|
|
07a4fe1fc9 | ||
|
|
1ee8f7f980 | ||
|
|
d0a6a97f71 | ||
|
|
e24942083a | ||
|
|
e7857bb2e4 | ||
|
|
8f52cbc319 | ||
|
|
ca8be18531 | ||
|
|
54f18cdd2f | ||
|
|
0ca8b1960c | ||
|
|
3f4b247faa | ||
|
|
a86f5c6078 | ||
|
|
38a18b9b8e | ||
|
|
f2d622b53f | ||
|
|
fa87c2ee9f | ||
|
|
43b33ca42a | ||
|
|
fa9d21be8b | ||
|
|
d3dee295d6 | ||
|
|
35427d08a8 | ||
|
|
193d68398b | ||
|
|
d14ac65db9 | ||
|
|
c90dcce859 | ||
|
|
dfa6836580 | ||
|
|
8f4bd9a3a6 | ||
|
|
0080cb33ac | ||
|
|
2400823ca4 | ||
|
|
695fad283c | ||
|
|
6de7807f01 | ||
|
|
3de7e1bb40 | ||
|
|
ac4b520295 | ||
|
|
823fd54ef0 | ||
|
|
5413089db0 | ||
|
|
4df1dac2c2 | ||
|
|
5d91deaade | ||
|
|
b781a1197e | ||
|
|
176b100274 | ||
|
|
d87e314880 | ||
|
|
9a693ec4b1 | ||
|
|
1bf8271562 | ||
|
|
8a5209c084 | ||
|
|
89c25e3019 | ||
|
|
c8e7495f50 | ||
|
|
1b4e47d709 | ||
|
|
6548f939a2 | ||
|
|
1458b57c0d | ||
|
|
1c1adb8331 | ||
|
|
6f65f9e7cc | ||
|
|
5b8e7b538c | ||
|
|
a0bb19b8bb | ||
|
|
3fa4f8e900 | ||
|
|
8a1c1842ee | ||
|
|
7886cdc613 | ||
|
|
454b6e0236 | ||
|
|
c1e07dddaa | ||
|
|
1b3137d947 | ||
|
|
72423e80a8 | ||
|
|
5fd12d5dd8 | ||
|
|
c9ddd8919e | ||
|
|
b22a0894b8 | ||
|
|
e17e21b105 | ||
|
|
57172f1296 | ||
|
|
2d5f246486 | ||
|
|
0077c6dba7 | ||
|
|
1423ac0902 | ||
|
|
6958299ec2 | ||
|
|
6329345e73 | ||
|
|
ef0845a9c5 | ||
|
|
509962a773 | ||
|
|
b9e890379e | ||
|
|
933e601816 | ||
|
|
b23e768973 | ||
|
|
4bee3cefa6 | ||
|
|
1ae2b74279 | ||
|
|
00473e7ac0 | ||
|
|
7bdce19d01 | ||
|
|
431f740476 | ||
|
|
6e2e3afc85 | ||
|
|
4d90ed776f | ||
|
|
7c8e8d8dd3 | ||
|
|
0b33b3a683 | ||
|
|
ef5e344c4f | ||
|
|
034e968888 | ||
|
|
5c80f6e9a2 | ||
|
|
cf979f436c | ||
|
|
328c9dbb28 | ||
|
|
9870959ca6 | ||
|
|
1ac9103735 | ||
|
|
69be54c1cf | ||
|
|
689467ec91 | ||
|
|
be09808d1f | ||
|
|
18f40753dd | ||
|
|
b23c1ba932 | ||
|
|
e1fb5b12d4 | ||
|
|
06bfbc4a84 | ||
|
|
8d186fc066 | ||
|
|
0a9aec5929 | ||
|
|
155ba0f1f7 | ||
|
|
52592ffd6c | ||
|
|
92aedde121 | ||
|
|
b2df94a3ba | ||
|
|
19572a3cc9 | ||
|
|
8d7e7a6ddf | ||
|
|
af9f490172 | ||
|
|
099443fddc | ||
|
|
947277bdec | ||
|
|
b1bd4bb5ec | ||
|
|
d210b3f39c | ||
|
|
901b436107 | ||
|
|
097826508a | ||
|
|
cdfbdd5dd5 | ||
|
|
d31bb978f2 | ||
|
|
d067e2c00a | ||
|
|
3beab030f4 | ||
|
|
566339f09f | ||
|
|
dec9b8317c | ||
|
|
c77d338ab1 | ||
|
|
bc3f76dd73 | ||
|
|
4fcd7ff61f | ||
|
|
f930ffe153 | ||
|
|
26c22379ae | ||
|
|
4c9bff5e8c | ||
|
|
8808a5aa77 | ||
|
|
6fb6674803 | ||
|
|
c1cf19ef67 | ||
|
|
de868a2763 | ||
|
|
5fc33102a9 | ||
|
|
f74ad02b9d | ||
|
|
dca1e8a251 | ||
|
|
22fab86010 | ||
|
|
da895ea502 | ||
|
|
b94168957e | ||
|
|
c792a30932 | ||
|
|
dd01d1c3f9 | ||
|
|
23c895e839 | ||
|
|
7a5960e737 | ||
|
|
5c6110a57e | ||
|
|
17843ffd71 | ||
|
|
7296ab0425 | ||
|
|
47664023ee | ||
|
|
33918c6837 | ||
|
|
66a6052f88 | ||
|
|
31b5b838a2 | ||
|
|
575a3d88b9 | ||
|
|
bef811ce13 | ||
|
|
dee5620fc9 | ||
|
|
b1174861d4 | ||
|
|
a1f296ee08 | ||
|
|
3e34795ef5 | ||
|
|
f73d8dcb97 | ||
|
|
82ad393a05 | ||
|
|
257406120d | ||
|
|
9ba3216fa8 | ||
|
|
fca3f0cffb | ||
|
|
5712b39cf9 | ||
|
|
07dc1436af | ||
|
|
9e52397732 | ||
|
|
20ff4f3687 | ||
|
|
73ce3039ce | ||
|
|
c27a038fc5 | ||
|
|
c3c4efc6ec | ||
|
|
61296bc855 | ||
|
|
78da0076cf | ||
|
|
279b38fec3 | ||
|
|
0e348fcca4 | ||
|
|
7d79fae84d | ||
|
|
f642e0325d | ||
|
|
43a408f1c9 | ||
|
|
b97b5cf6e2 | ||
|
|
6c269a99cf | ||
|
|
24841f2dd9 | ||
|
|
c2e60c1e52 | ||
|
|
7a87d43bdc | ||
|
|
5fa1c90674 | ||
|
|
c79225ef27 | ||
|
|
0bd8fdea1f | ||
|
|
18dbd043ae | ||
|
|
a7535dec2b | ||
|
|
dbd4b7f763 | ||
|
|
68eacbbcf7 | ||
|
|
cecefce84d | ||
|
|
2406c1ff71 | ||
|
|
a4a0ac25cf | ||
|
|
fbb23f0b9a | ||
|
|
016e2bc70e | ||
|
|
070c2328e0 | ||
|
|
c117da42e4 | ||
|
|
950856d86f | ||
|
|
93a3240862 | ||
|
|
1116a3736b | ||
|
|
19af57e46b | ||
|
|
401c33c505 | ||
|
|
8c968c1a87 | ||
|
|
4fee58999d | ||
|
|
bd0752df96 | ||
|
|
47a15a6674 | ||
|
|
2a64bad76e | ||
|
|
7ee8698165 | ||
|
|
9ae35e5fec | ||
|
|
b618927454 | ||
|
|
e8ad34d2f0 | ||
|
|
007fdc9a5d | ||
|
|
61b62fe5af | ||
|
|
d574e6b396 | ||
|
|
f798b183e6 | ||
|
|
e8ef4b824a | ||
|
|
d87f8bf36a | ||
|
|
50b4169148 | ||
|
|
4a63a175e4 | ||
|
|
2508b7935a | ||
|
|
22697baa37 | ||
|
|
2fc587fd31 | ||
|
|
2a3bd5bd97 | ||
|
|
ac68729734 | ||
|
|
19c10f2369 | ||
|
|
8af8dd1ae0 | ||
|
|
258fccd56f | ||
|
|
93dc696e90 | ||
|
|
00d70d2934 | ||
|
|
2f39767211 | ||
|
|
5beb4c3ce4 | ||
|
|
8280f342a8 | ||
|
|
e5baf9ead6 | ||
|
|
06c4222aff | ||
|
|
f57f9913d5 | ||
|
|
579891a53a | ||
|
|
061f78b897 | ||
|
|
6671915cf7 | ||
|
|
0cfcfa77d3 | ||
|
|
9e6a56a77b | ||
|
|
714c6fc553 | ||
|
|
d7e15f5eb0 | ||
|
|
b388b08226 | ||
|
|
3bf42a169c | ||
|
|
29ee21e9d7 | ||
|
|
5eafd2aa3c | ||
|
|
b0f077ffbb | ||
|
|
0fe3060cf1 | ||
|
|
c2eaf27c20 | ||
|
|
275d490ca7 | ||
|
|
0707d60f0b | ||
|
|
9b5e785d67 | ||
|
|
69d9f41085 | ||
|
|
ffb4d16950 | ||
|
|
68d4081fcf | ||
|
|
92eb4266da | ||
|
|
b220dd051f | ||
|
|
e466e87fcc | ||
|
|
0428a30071 | ||
|
|
faaf7169da | ||
|
|
04a61a50dc | ||
|
|
749e543e37 | ||
|
|
d94335dc62 | ||
|
|
e135956eba | ||
|
|
66b14766a3 | ||
|
|
923e789211 | ||
|
|
52eacdd9df | ||
|
|
a9bae6bb3f | ||
|
|
b0d8ccbf23 | ||
|
|
4b18075314 | ||
|
|
784d7d60e2 | ||
|
|
ece1119c31 | ||
|
|
b9ffcd2959 | ||
|
|
d900bda756 | ||
|
|
8ac435dc6d | ||
|
|
99942e37d6 | ||
|
|
d2a535e6ff | ||
|
|
61c2e524b4 | ||
|
|
b434247838 | ||
|
|
b3f69ac401 | ||
|
|
93006d8423 | ||
|
|
a7fdab5bb2 | ||
|
|
bedd1eceb5 | ||
|
|
dac9e1f571 | ||
|
|
aed87665f4 | ||
|
|
04bbab3d7e | ||
|
|
4798d3e8ec | ||
|
|
c940e930bc | ||
|
|
8280306f82 | ||
|
|
59e12b0867 | ||
|
|
ded531a3c2 | ||
|
|
3a471bc1aa | ||
|
|
ea4f88a20d | ||
|
|
8e857b9aee | ||
|
|
f55a332ead | ||
|
|
893a0f70fb | ||
|
|
fdb05fb8d3 | ||
|
|
730d4bb1ab | ||
|
|
fee703e879 | ||
|
|
4f2f7003c8 | ||
|
|
97540206b3 | ||
|
|
793e078676 | ||
|
|
7453600ee5 | ||
|
|
6c53ef4eff | ||
|
|
60eb696859 | ||
|
|
e456925c23 | ||
|
|
038ecc8b70 | ||
|
|
3bfcd15dc0 | ||
|
|
6abdb8684f | ||
|
|
080ae0986a | ||
|
|
bfb61c8449 | ||
|
|
23d7511f55 | ||
|
|
2d73bcc6de | ||
|
|
a24f4a2bc1 | ||
|
|
bf51003c66 | ||
|
|
2df9dce78b | ||
|
|
3ed45fab41 | ||
|
|
7c9cf95698 | ||
|
|
af2c358822 | ||
|
|
1226e544ec | ||
|
|
faa7ed1ae2 | ||
|
|
22cdeb677b | ||
|
|
b210d078d0 | ||
|
|
ce30de20b6 | ||
|
|
8dde4c3c6d | ||
|
|
bdb60a656a | ||
|
|
8a56367d02 | ||
|
|
aebe9bab54 | ||
|
|
9ab31a7614 | ||
|
|
f6556f25d5 | ||
|
|
16a8a763f4 | ||
|
|
4b1cd1b727 | ||
|
|
37ccaf7743 | ||
|
|
06a984f39d | ||
|
|
6d1c034052 | ||
|
|
672d05d124 | ||
|
|
322d0a7cb8 | ||
|
|
5725e78931 | ||
|
|
69b6072e37 | ||
|
|
1a9d417c0a | ||
|
|
8a552c4cb3 | ||
|
|
9da9a36cf3 | ||
|
|
d08ab62f92 | ||
|
|
1cb112bcdf | ||
|
|
2fb4851d1f | ||
|
|
32c3ba017e | ||
|
|
d09b968cc0 | ||
|
|
90d84da849 | ||
|
|
d2c4160c6d | ||
|
|
2b4beae9df | ||
|
|
7dab6f2e33 | ||
|
|
1aab48783d | ||
|
|
5089e6b294 | ||
|
|
3be08cb6cd | ||
|
|
6c4833dcd1 | ||
|
|
ce17629a8d | ||
|
|
5e15c4dd2d | ||
|
|
c04d82ee30 | ||
|
|
7962aea008 | ||
|
|
5493b62bb9 | ||
|
|
86c39ac36f | ||
|
|
6bf05c9ed7 | ||
|
|
a2c1b4a7d8 | ||
|
|
07b3b908f9 | ||
|
|
71989af586 | ||
|
|
adacf0b5c5 | ||
|
|
09ae3d74c4 | ||
|
|
839fdd7d66 | ||
|
|
b0a9a995fb | ||
|
|
3205d131c9 | ||
|
|
52d193b2e8 | ||
|
|
d9e52c734a | ||
|
|
3ce85617da | ||
|
|
0ba984b277 | ||
|
|
8da8cc7f91 | ||
|
|
bc63311003 | ||
|
|
3a00ae1f44 | ||
|
|
e8cad189be | ||
|
|
495a985ae1 | ||
|
|
8f1e6cf379 | ||
|
|
05e8dab710 | ||
|
|
0ba2bfe645 | ||
|
|
f282da3bcb | ||
|
|
eba689754c | ||
|
|
d13c153f5e | ||
|
|
4999fd544d | ||
|
|
36233ba9aa | ||
|
|
4408bce159 | ||
|
|
e7b8e78f9c | ||
|
|
29e7e365f1 | ||
|
|
8ea7ecc477 | ||
|
|
99b75378c3 | ||
|
|
5f56859898 | ||
|
|
7f2445a6f5 | ||
|
|
b3a54b3ff8 | ||
|
|
c8c8497a2f | ||
|
|
04019135a0 | ||
|
|
e7935db84b | ||
|
|
0f67305100 | ||
|
|
6179c3fbd9 | ||
|
|
13705f9098 | ||
|
|
1dea343aa2 | ||
|
|
293a8ff3e9 | ||
|
|
6e422aea33 | ||
|
|
39e52469f2 | ||
|
|
3ff5141184 | ||
|
|
db31513c0b | ||
|
|
5da2c7ff1a | ||
|
|
d109f2891f | ||
|
|
f946951580 | ||
|
|
f6ebfc2e44 | ||
|
|
540dbc2a28 | ||
|
|
2c8128724e | ||
|
|
528f3c51a0 | ||
|
|
a4d94624cd | ||
|
|
87bd16c4ba | ||
|
|
dc54d24d2b | ||
|
|
8457f6c4b7 | ||
|
|
a2d83744b5 | ||
|
|
946450874f | ||
|
|
577a417292 | ||
|
|
a74065bdbb | ||
|
|
0c320234fd | ||
|
|
f5ae2e51f9 | ||
|
|
8e24e40fdd | ||
|
|
85648b1e5a | ||
|
|
b997788437 | ||
|
|
542bafe6c4 | ||
|
|
98965a56e1 | ||
|
|
21c427bc39 | ||
|
|
6b67bb24b4 | ||
|
|
06dd9507ee | ||
|
|
b588235b62 | ||
|
|
f457f6aaca | ||
|
|
c8fe90cbee | ||
|
|
6bd87fa568 | ||
|
|
293fc6e0fe | ||
|
|
2a8ae05363 | ||
|
|
9613f6b624 | ||
|
|
ee78b9b57c | ||
|
|
5ce4dddd7a | ||
|
|
e67285fde1 | ||
|
|
4e31d47936 | ||
|
|
954d126a8f | ||
|
|
5b31ab7435 | ||
|
|
b3c321be67 | ||
|
|
d7ad83f0d5 | ||
|
|
3006429a9a | ||
|
|
9364c48ef0 | ||
|
|
3e140c1bc6 | ||
|
|
7ff3e00759 | ||
|
|
0632c5242c | ||
|
|
76b0163bd3 | ||
|
|
6421662f5d | ||
|
|
54195e0591 | ||
|
|
5fc29a6fd3 | ||
|
|
f41a15c7b8 | ||
|
|
938bc53966 | ||
|
|
093b3cae1f | ||
|
|
0816702084 | ||
|
|
c70eb7157f | ||
|
|
23f95d7dc8 | ||
|
|
682017ba96 | ||
|
|
891c8c1200 | ||
|
|
a611c1c04b | ||
|
|
558966a129 | ||
|
|
2f354d2267 | ||
|
|
eca9601ab0 | ||
|
|
0b9f0448c8 | ||
|
|
675aae16e9 | ||
|
|
9b0e7937f9 | ||
|
|
03eb26d999 | ||
|
|
f2ffeeaf45 | ||
|
|
d7632e29a6 | ||
|
|
a750266e1c | ||
|
|
5f892d29c8 | ||
|
|
f3e11961dc | ||
|
|
9e224d0067 | ||
|
|
3da3a3fb13 | ||
|
|
70b469d349 | ||
|
|
5310ec7c44 | ||
|
|
6cb5998cd6 | ||
|
|
6bbe5ad30d | ||
|
|
cd6d034d6c | ||
|
|
df3a49becf | ||
|
|
ca9eb65fdd | ||
|
|
8d5a24992a | ||
|
|
7a85c30752 | ||
|
|
0fa8afd517 | ||
|
|
d805f288ae | ||
|
|
12fbc7ecf4 | ||
|
|
0ff17dc27c | ||
|
|
d2f68acae8 | ||
|
|
f3dfffb8d1 | ||
|
|
c805b16fcd | ||
|
|
56126e22f2 | ||
|
|
d3836da9cb | ||
|
|
ae2b27c4b4 | ||
|
|
8519748512 | ||
|
|
70134cd77f | ||
|
|
356baa1e38 | ||
|
|
5fcc04a200 | ||
|
|
77a306beb2 | ||
|
|
f5ee61f589 | ||
|
|
c99cdb5d0d | ||
|
|
c3a3387ee3 | ||
|
|
c189125aa4 | ||
|
|
4cc8ab6482 | ||
|
|
d1aa06d537 | ||
|
|
d5688d31f6 | ||
|
|
ff8bf20680 | ||
|
|
5061ec081a | ||
|
|
e83c9f5553 | ||
|
|
8eb4bf3954 | ||
|
|
3d91079020 | ||
|
|
ab74270cd4 | ||
|
|
b815e7b296 | ||
|
|
fce50b513c | ||
|
|
781a80e03f | ||
|
|
1058da653d | ||
|
|
0573155285 | ||
|
|
3427a8844a | ||
|
|
e353fda7a2 | ||
|
|
f956991bda | ||
|
|
0bc6941df6 | ||
|
|
c64b1fbb72 | ||
|
|
72143f6a8f | ||
|
|
d5d4d4fabc | ||
|
|
03e08bec32 | ||
|
|
233894f027 | ||
|
|
8f0bd61c14 | ||
|
|
8a0dc3a7d3 | ||
|
|
453e13c88d | ||
|
|
4daaa22cba | ||
|
|
97f062773b | ||
|
|
fb73769063 | ||
|
|
4cac8ef3c9 | ||
|
|
156fce531c | ||
|
|
480edbe501 | ||
|
|
81fab81d1b | ||
|
|
cba8ff394c | ||
|
|
6f4e80c749 | ||
|
|
a9eed57cf7 | ||
|
|
890d693102 | ||
|
|
9038fe1bdf | ||
|
|
ca1c8559cf | ||
|
|
bd2bd49e6d | ||
|
|
c1d27448bc | ||
|
|
e6d2685521 | ||
|
|
8006844b9f | ||
|
|
5d4989f68f | ||
|
|
b7e50118f0 | ||
|
|
5fc0a6504a | ||
|
|
c9053bccc5 | ||
|
|
e4672062f8 | ||
|
|
06583abad9 | ||
|
|
ce568362c6 | ||
|
|
ed67a72b68 | ||
|
|
7850f49429 | ||
|
|
440172aae8 | ||
|
|
19989e4c26 | ||
|
|
4265d7cfa9 | ||
|
|
2d562ccfd6 | ||
|
|
74a422a5e2 | ||
|
|
450d1d66b4 | ||
|
|
4a944ad23f | ||
|
|
26fb650e04 | ||
|
|
d3278bb4c4 | ||
|
|
b438881a50 | ||
|
|
b11b662071 | ||
|
|
11156c941c | ||
|
|
0394667680 | ||
|
|
856a5158e4 | ||
|
|
fb00f47031 | ||
|
|
55a52bb0f3 | ||
|
|
630044b740 | ||
|
|
69f51f8ec8 | ||
|
|
ab053ef7d1 | ||
|
|
89639e36bc | ||
|
|
cf8f9be8dc | ||
|
|
156631c263 | ||
|
|
d8da8d6abf | ||
|
|
61d71cf1d0 | ||
|
|
7eb086cade | ||
|
|
c9d0bce153 | ||
|
|
1d1d8d21cd | ||
|
|
c4153202ba | ||
|
|
8ddd8a726d | ||
|
|
e16082af9a | ||
|
|
2c7962f5d3 | ||
|
|
d6f552d539 | ||
|
|
8f86c4419b | ||
|
|
b4affbc1d5 | ||
|
|
0834d8cb3d | ||
|
|
d0b9a7f50c | ||
|
|
7df524e9ef | ||
|
|
cb90a4ad01 | ||
|
|
01d8fe44ce | ||
|
|
73e93e955c | ||
|
|
c742b4d61e | ||
|
|
ce06bea744 | ||
|
|
48de0b83c4 | ||
|
|
f0afff68c4 | ||
|
|
67e0cc752b | ||
|
|
d78c4481f0 | ||
|
|
75b60f94d2 | ||
|
|
c17d867aa6 | ||
|
|
327a78f1cb | ||
|
|
af51ead948 | ||
|
|
9fab48e64f | ||
|
|
acfa112415 | ||
|
|
79094d4f3b | ||
|
|
da7559426c | ||
|
|
c8f11d7258 | ||
|
|
c45961f027 | ||
|
|
a6105f4807 | ||
|
|
77b58baff7 | ||
|
|
8543f1dc65 | ||
|
|
17a3d72852 | ||
|
|
b5ba49ff8f | ||
|
|
83972d29b7 | ||
|
|
58a9adad34 | ||
|
|
7d1e066997 | ||
|
|
9be10beadc | ||
|
|
8529fbd9e2 | ||
|
|
86095b5bf1 | ||
|
|
c081d23cc4 | ||
|
|
6841e69008 | ||
|
|
ee5623d290 | ||
|
|
ae3e08d5f6 | ||
|
|
15e0766bbb | ||
|
|
25fb3502e1 | ||
|
|
6e7b8ceb39 | ||
|
|
b4f46aeecd | ||
|
|
4162a6491b | ||
|
|
f7648413ed | ||
|
|
9dde59a6c7 | ||
|
|
f7c20f6d79 | ||
|
|
747cabe447 | ||
|
|
b723b16671 | ||
|
|
bffad0c3a3 | ||
|
|
0a48f70643 | ||
|
|
0a229e8156 | ||
|
|
f7ed6f8e61 | ||
|
|
20e817b203 | ||
|
|
1284c8c8ef | ||
|
|
7ac9b89b7d | ||
|
|
c0e29a08ba | ||
|
|
cc788d1b25 | ||
|
|
7fa23e72c0 | ||
|
|
5ce5d03d69 | ||
|
|
4ac6a9e798 | ||
|
|
472686e8ff | ||
|
|
dc38602d32 | ||
|
|
5c867fd121 | ||
|
|
d7879d9ef0 | ||
|
|
8aa2932878 | ||
|
|
f19ff5fdd5 | ||
|
|
7cce1ce30a | ||
|
|
92e9b0ef75 | ||
|
|
a54a357e4b | ||
|
|
2e5c3473e1 | ||
|
|
5f3d1d9880 | ||
|
|
a575fb49db | ||
|
|
53b4fcb842 | ||
|
|
2c95009d1f | ||
|
|
5b8bbd672e | ||
|
|
54f1f6970c | ||
|
|
8a1e65640e | ||
|
|
0312dfbb16 | ||
|
|
02afeba564 | ||
|
|
1a2462ef17 | ||
|
|
c76b634739 | ||
|
|
fc88e21811 | ||
|
|
e1cebb1c9a | ||
|
|
7d7b775fe0 | ||
|
|
6c53fb14a6 | ||
|
|
aad0f447c0 | ||
|
|
9d3c77755d | ||
|
|
67dd178166 | ||
|
|
802385464d | ||
|
|
eff2f7f63a | ||
|
|
7039eae9c7 | ||
|
|
fbed6580fa | ||
|
|
7cdd2bd6c0 | ||
|
|
651c452fe1 | ||
|
|
5b843ee25b | ||
|
|
f7d71c6c5c | ||
|
|
dda8bbb6e3 | ||
|
|
6932abe674 | ||
|
|
79c5bfb3d4 | ||
|
|
ace6e18da8 | ||
|
|
a5b27820cb | ||
|
|
d2189e1442 | ||
|
|
36a80951a0 | ||
|
|
2ea88c03d3 | ||
|
|
5d9499e4dd | ||
|
|
fe479778d1 | ||
|
|
81eaeb5d5f | ||
|
|
ea53430d70 | ||
|
|
be26970761 | ||
|
|
f7dd90a5d1 | ||
|
|
2254b76232 | ||
|
|
ea1737ab8d | ||
|
|
01bee3d8e4 | ||
|
|
805ab8b3d8 | ||
|
|
6742495c6f | ||
|
|
3964ecbe88 | ||
|
|
ebdf7b5a6c | ||
|
|
26ce75a547 | ||
|
|
53811969c5 | ||
|
|
2438899ff5 | ||
|
|
a718c41d5d | ||
|
|
274c32ebdd | ||
|
|
a664f1a869 | ||
|
|
455813e53c | ||
|
|
30d1c080a0 | ||
|
|
f7217583a3 | ||
|
|
8f7c790700 | ||
|
|
4cef232271 | ||
|
|
5b602bff75 | ||
|
|
4ad1d15781 | ||
|
|
f25a449e20 | ||
|
|
9d39440438 | ||
|
|
02faa4586b | ||
|
|
9acb1c69f7 | ||
|
|
a9d515f160 | ||
|
|
37a094c351 | ||
|
|
f5166ac3fc | ||
|
|
23ac30086f | ||
|
|
c6f6f76489 | ||
|
|
cbe4645bc6 | ||
|
|
82cac0b12e | ||
|
|
1ae44941dd | ||
|
|
4b23c013d9 | ||
|
|
cf9b7d9d10 | ||
|
|
eeaf3c658b | ||
|
|
8fba42adbf | ||
|
|
dd8af73887 | ||
|
|
3eb9fd0acb | ||
|
|
c315ea9c96 | ||
|
|
864ad8a371 | ||
|
|
05d1bc22c6 | ||
|
|
e4a8c53079 | ||
|
|
c72542c92c | ||
|
|
7f83954714 | ||
|
|
8c88017703 | ||
|
|
3a2db112f3 | ||
|
|
f6877ecfef | ||
|
|
17ebc50b83 | ||
|
|
d800f1ce84 | ||
|
|
c000bc3c69 | ||
|
|
c277791ef3 | ||
|
|
7612657ded | ||
|
|
e421662576 | ||
|
|
2afddf497b | ||
|
|
c405eb08b5 | ||
|
|
e6dd986115 | ||
|
|
bf3e21f15c | ||
|
|
09139c2553 | ||
|
|
b85e7491a9 | ||
|
|
2fee3d1389 | ||
|
|
999efa5947 | ||
|
|
35b7fdf96b | ||
|
|
5ffaa4361e | ||
|
|
63db9fecb3 | ||
|
|
e687ae2819 | ||
|
|
4cfa4bc63f | ||
|
|
73f3e2cf73 | ||
|
|
6f132db328 | ||
|
|
b8053ff368 | ||
|
|
9ba457c91f | ||
|
|
255e484dcf | ||
|
|
e5fb03bbcd | ||
|
|
bea16b72df | ||
|
|
bea938bc34 | ||
|
|
b516acb173 | ||
|
|
ee96125385 | ||
|
|
6934285d83 | ||
|
|
5a52b141ed | ||
|
|
fdcbadf918 | ||
|
|
ebda018e13 | ||
|
|
f653a6eb79 | ||
|
|
307bcc95d1 | ||
|
|
a7f8ce36df | ||
|
|
f5f5bbf5eb | ||
|
|
8131ea8fc8 | ||
|
|
fac826b335 | ||
|
|
e069ddf8fa | ||
|
|
ccd12742d3 | ||
|
|
17695c361d | ||
|
|
0c8c9a9f12 | ||
|
|
aa1e8d8a40 | ||
|
|
0d9344ff19 | ||
|
|
98418ec5c3 | ||
|
|
5ab50db51c | ||
|
|
aa2177d35a | ||
|
|
9118406de3 | ||
|
|
ef47b27886 | ||
|
|
654178c8cd | ||
|
|
f73415827c | ||
|
|
d414a38877 | ||
|
|
85a0f9d007 | ||
|
|
358d799af8 | ||
|
|
cf0a216329 | ||
|
|
8615265ee1 | ||
|
|
ec23d72332 | ||
|
|
b3f6c45bc1 | ||
|
|
56b3112a07 | ||
|
|
b9c743d67e | ||
|
|
09af56b1c2 | ||
|
|
872b089b15 | ||
|
|
fd33c31b72 | ||
|
|
8b8a00b666 | ||
|
|
24d9db4c51 | ||
|
|
1d90aed187 | ||
|
|
7fe72c42b2 | ||
|
|
b880b5416f | ||
|
|
7b895474ef | ||
|
|
e3515b9eb2 | ||
|
|
c66e8e7b49 | ||
|
|
992d2dee45 | ||
|
|
0cde96844d | ||
|
|
6c36bd0a08 | ||
|
|
d791303967 | ||
|
|
0ff3f99c18 | ||
|
|
cfbfda4de3 | ||
|
|
a5be4cc3ae | ||
|
|
959f32327d | ||
|
|
1dd1cb9e44 | ||
|
|
16836375c4 | ||
|
|
71fca7fb86 | ||
|
|
b707c74203 | ||
|
|
acb119d80e | ||
|
|
b9f9a8fca2 | ||
|
|
f2c8122c46 | ||
|
|
9b1351db23 | ||
|
|
569edbb11a | ||
|
|
2580e4d6f3 | ||
|
|
32d51f3c25 | ||
|
|
8b90c0b3f0 | ||
|
|
067cbd5ab2 | ||
|
|
c7b8663c06 | ||
|
|
0c1a800f16 | ||
|
|
6c1f56d50e | ||
|
|
235bc99846 | ||
|
|
f94a0429d5 | ||
|
|
17331ddbaa | ||
|
|
527ecd37e1 | ||
|
|
6456658576 | ||
|
|
f8abe60dc2 | ||
|
|
01eb2c25e0 | ||
|
|
2ad2f26b2b | ||
|
|
75185f5e66 | ||
|
|
0bcb8ce6c3 | ||
|
|
b2b1e6b944 | ||
|
|
e6a1333f83 | ||
|
|
bf7b9092df | ||
|
|
1f3cc2c686 | ||
|
|
e09391a286 | ||
|
|
6d5d49ef50 | ||
|
|
9848b8b295 | ||
|
|
0fea730908 | ||
|
|
10a695ba0f | ||
|
|
65567221ac | ||
|
|
0f891be026 | ||
|
|
b22d28b79c |
18
.containerignore
Normal file
@@ -0,0 +1,18 @@
|
||||
.git
|
||||
.github
|
||||
.idea
|
||||
.ace-tool
|
||||
.playwright-mcp
|
||||
.superpowers
|
||||
.worktrees
|
||||
bin
|
||||
build
|
||||
dist
|
||||
frontend/dist
|
||||
node_modules
|
||||
frontend/node_modules
|
||||
tools/__pycache__
|
||||
GoNavi-Wails
|
||||
GoNavi-Wails.exe
|
||||
optional-driver-agent
|
||||
optional-driver-agent.exe
|
||||
18
.dockerignore
Normal file
@@ -0,0 +1,18 @@
|
||||
.git
|
||||
.github
|
||||
.idea
|
||||
.ace-tool
|
||||
.playwright-mcp
|
||||
.superpowers
|
||||
.worktrees
|
||||
bin
|
||||
build
|
||||
dist
|
||||
frontend/dist
|
||||
node_modules
|
||||
frontend/node_modules
|
||||
tools/__pycache__
|
||||
GoNavi-Wails
|
||||
GoNavi-Wails.exe
|
||||
optional-driver-agent
|
||||
optional-driver-agent.exe
|
||||
923
.github/workflows/dev-build.yml
vendored
260
.github/workflows/docker-images.yml
vendored
Normal file
@@ -0,0 +1,260 @@
|
||||
name: Docker Images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
concurrency:
|
||||
group: docker-images-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build ${{ matrix.image_name }} (${{ matrix.platform }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
image_name:
|
||||
- gonavi-mcp-server
|
||||
- gonavi-web-server
|
||||
- gonavi-build-env
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
include:
|
||||
- image_name: gonavi-mcp-server
|
||||
dockerfile: Dockerfile.mcp-server
|
||||
description: GoNavi MCP Server container image
|
||||
- image_name: gonavi-web-server
|
||||
dockerfile: Dockerfile.web-server
|
||||
description: GoNavi Web Server (browser UI) container image
|
||||
- image_name: gonavi-build-env
|
||||
dockerfile: Dockerfile.build-env
|
||||
description: GoNavi Linux build environment image
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Validate runtime Dockerfiles
|
||||
if: matrix.image_name == 'gonavi-web-server'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
bash tools/validate-web-server-dockerfile.test.sh
|
||||
env_file="$(mktemp)"
|
||||
trap 'rm -f "$env_file"' EXIT
|
||||
printf 'GONAVI_HOST_DATA_ROOT=%s\nGONAVI_WEB_PASSWORD=123456\n' "${RUNNER_TEMP}/gonavi-data" >"$env_file"
|
||||
rendered="$(docker compose --env-file "$env_file" -f docker-compose.web-server.yml config)"
|
||||
grep -Eq "GONAVI_WEB_PASSWORD: ['\"]?123456['\"]?$" <<<"$rendered"
|
||||
|
||||
- name: Prepare build variables
|
||||
id: prep
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "owner_lc=$(printf '%s' "${GITHUB_REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"
|
||||
echo "platform_pair=$(printf '%s' "${{ matrix.platform }}" | tr '/' '-')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/${{ steps.prep.outputs.owner_lc }}/${{ matrix.image_name }}
|
||||
labels: |
|
||||
org.opencontainers.image.title=${{ matrix.image_name }}
|
||||
org.opencontainers.image.description=${{ matrix.description }}
|
||||
org.opencontainers.image.licenses=Apache-2.0
|
||||
|
||||
- name: Build smoke image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.dockerfile }}
|
||||
platforms: ${{ matrix.platform }}
|
||||
load: true
|
||||
tags: codex-smoke/${{ matrix.image_name }}:local
|
||||
cache-from: type=gha,scope=${{ matrix.image_name }}-${{ steps.prep.outputs.platform_pair }}
|
||||
|
||||
- name: Smoke test MCP Server image
|
||||
if: matrix.image_name == 'gonavi-mcp-server'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cid="$(docker run -d -p 8765:8765 -e GONAVI_MCP_HTTP_TOKEN=smoke-token codex-smoke/${{ matrix.image_name }}:local http)"
|
||||
trap 'docker rm -f "$cid" >/dev/null 2>&1 || true' EXIT
|
||||
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
||||
if curl --fail --silent http://127.0.0.1:8765/healthz >/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
docker logs "$cid"
|
||||
echo "MCP Server image smoke test failed" >&2
|
||||
exit 1
|
||||
|
||||
- name: Smoke test Web Server image
|
||||
if: matrix.image_name == 'gonavi-web-server'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cookie_jar="$(mktemp)"
|
||||
cid=""
|
||||
trap 'rm -f "$cookie_jar"; if [[ -n "$cid" ]]; then docker rm -f "$cid" >/dev/null 2>&1 || true; fi' EXIT
|
||||
cid="$(docker run -d -p 127.0.0.1::34116 -e GONAVI_WEB_ADDR=0.0.0.0:34116 -e GONAVI_WEB_PASSWORD=123456 codex-smoke/${{ matrix.image_name }}:local web-server)"
|
||||
host_port="$(docker port "$cid" 34116/tcp | awk -F: 'NR == 1 { print $NF }')"
|
||||
if [[ ! "$host_port" =~ ^[0-9]+$ ]]; then
|
||||
docker logs "$cid"
|
||||
echo "Failed to resolve the Web Server smoke-test port" >&2
|
||||
exit 1
|
||||
fi
|
||||
base_url="http://127.0.0.1:${host_port}"
|
||||
ready=false
|
||||
for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do
|
||||
if curl --fail --silent "${base_url}/__gonavi/healthz" >/dev/null; then
|
||||
ready=true
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if [[ "$ready" != "true" ]]; then
|
||||
docker logs "$cid"
|
||||
echo "Web Server image smoke test failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
status="$(curl --fail --silent --show-error "${base_url}/__gonavi/auth/status")"
|
||||
grep -Fq '"configured":true' <<<"$status"
|
||||
login="$(curl --fail --silent --show-error -c "$cookie_jar" -H 'Content-Type: application/json' -d '{"password":"123456","code":""}' "${base_url}/__gonavi/auth/login")"
|
||||
grep -Fq '"success":true' <<<"$login"
|
||||
app_status="$(curl --silent --show-error -o /dev/null -w '%{http_code}' -b "$cookie_jar" "${base_url}/")"
|
||||
[[ "$app_status" == "200" ]]
|
||||
|
||||
- name: Smoke test build environment image
|
||||
if: matrix.image_name == 'gonavi-build-env'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker run --rm codex-smoke/${{ matrix.image_name }}:local \
|
||||
bash -lc 'go version && node -v && npm -v && wails version'
|
||||
|
||||
- name: Push by digest
|
||||
id: push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.dockerfile }}
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
tags: ghcr.io/${{ steps.prep.outputs.owner_lc }}/${{ matrix.image_name }}
|
||||
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=gha,scope=${{ matrix.image_name }}-${{ steps.prep.outputs.platform_pair }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.image_name }}-${{ steps.prep.outputs.platform_pair }}
|
||||
|
||||
- name: Export digest
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p "${{ runner.temp }}/digests"
|
||||
digest="${{ steps.push.outputs.digest }}"
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests-${{ matrix.image_name }}-${{ steps.prep.outputs.platform_pair }}
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge:
|
||||
name: Publish ${{ matrix.image_name }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- image_name: gonavi-mcp-server
|
||||
description: GoNavi MCP Server container image
|
||||
- image_name: gonavi-web-server
|
||||
description: GoNavi Web Server (browser UI) container image
|
||||
- image_name: gonavi-build-env
|
||||
description: GoNavi Linux build environment image
|
||||
steps:
|
||||
- name: Normalize image namespace
|
||||
id: vars
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "owner_lc=$(printf '%s' "${GITHUB_REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digests-${{ matrix.image_name }}-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/${{ steps.vars.outputs.owner_lc }}/${{ matrix.image_name }}
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
type=raw,value=dev-latest,enable=${{ github.ref == 'refs/heads/dev' }}
|
||||
type=sha,format=short,prefix=dev-,enable=${{ github.ref == 'refs/heads/dev' }}
|
||||
type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
type=semver,pattern={{major}}.{{minor}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
labels: |
|
||||
org.opencontainers.image.title=${{ matrix.image_name }}
|
||||
org.opencontainers.image.description=${{ matrix.description }}
|
||||
org.opencontainers.image.licenses=Apache-2.0
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: ${{ runner.temp }}/digests
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker buildx imagetools create \
|
||||
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf 'ghcr.io/${{ steps.vars.outputs.owner_lc }}/${{ matrix.image_name }}@sha256:%s ' *)
|
||||
|
||||
- name: Inspect image
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker buildx imagetools inspect "ghcr.io/${{ steps.vars.outputs.owner_lc }}/${{ matrix.image_name }}:${{ steps.meta.outputs.version }}"
|
||||
25
.github/workflows/release-winget.yml
vendored
@@ -1,25 +0,0 @@
|
||||
name: Publish to WinGet
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_tag:
|
||||
required: true
|
||||
description: 'Tag of release you want to publish'
|
||||
type: string
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: vedantmgoyal9/winget-releaser@v2
|
||||
with:
|
||||
identifier: Syngnat.GoNavi
|
||||
installers-regex: 'GoNavi-windows-(amd64|arm64)\.exe$'
|
||||
release-tag: ${{ inputs.release_tag || github.ref_name }}
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
872
.github/workflows/release.yml
vendored
5
.gitignore
vendored
@@ -10,12 +10,17 @@ build/bin/
|
||||
|
||||
# wails / node artifacts (按需)
|
||||
node_modules/
|
||||
frontend/wailsjs/tsconfig.json
|
||||
|
||||
dist/
|
||||
.DS_Store
|
||||
.gemini-clipboard
|
||||
.env
|
||||
docker.*.env
|
||||
GoNavi-Wails
|
||||
GoNavi-Wails.exe
|
||||
optional-driver-agent
|
||||
optional-driver-agent.exe
|
||||
.ace-tool/
|
||||
.superpowers/
|
||||
.claude/
|
||||
|
||||
106
AI_EXTENSIONS_ROADMAP.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# AI 扩展能力路线
|
||||
|
||||
当前 GoNavi 的 AI 链路是:
|
||||
|
||||
1. 前端 `AIChatPanel` 组装 system messages。
|
||||
2. 前端声明本地固定工具 `LOCAL_TOOLS`。
|
||||
3. 后端 `aiservice.Service` 只负责 Provider 配置、安全级别与模型转发。
|
||||
|
||||
这套结构已经足够承接“用户级提示词”,但要继续承接 MCP 和 Skills,需要先把“提示词 / 工具 / 技能”三层职责拆开。
|
||||
|
||||
## 1. 用户级自定义提示词
|
||||
|
||||
已落地的方向:
|
||||
|
||||
- 配置存储在 `ai_config.json` 的 `userPromptSettings`。
|
||||
- 由 `AISettingsModal` 提供编辑入口。
|
||||
- 由 `AIChatPanel` 在运行时追加为 system message。
|
||||
|
||||
建议长期保持 4 个层级:
|
||||
|
||||
- `global`: 所有 AI 会话统一追加。
|
||||
- `database`: 数据库 / SQL 场景追加。
|
||||
- `jvm`: JVM 资源浏览与分析场景追加。
|
||||
- `jvmDiagnostic`: JVM 诊断命令规划场景追加。
|
||||
|
||||
这样既能满足“个人习惯”定制,也不会把所有场景揉成一条超长总提示词。
|
||||
|
||||
## 2. MCP 能力开放
|
||||
|
||||
目标不是把 MCP 做成新的聊天面板,而是把它变成“外部工具源”。
|
||||
|
||||
建议后续拆成三层:
|
||||
|
||||
1. `tool registry`
|
||||
- 统一收口内置工具、本地扩展工具、MCP 工具。
|
||||
- 对模型只暴露统一的 `tools[]`。
|
||||
2. `mcp server config`
|
||||
- 保存 server 名称、transport、启动命令或 URL、超时、启用状态。
|
||||
- 由后端维护生命周期与连通性。
|
||||
3. `mcp runtime bridge`
|
||||
- 负责 `list tools / call tool / errors / timeout / auth`。
|
||||
|
||||
### MCP 是否需要单独 GitHub 仓库
|
||||
|
||||
不需要把“GoNavi 对 MCP 的支持”单独拆仓库。
|
||||
|
||||
更合理的边界是:
|
||||
|
||||
- `GoNavi 主仓库`
|
||||
- 维护 MCP client、配置、UI、工具注册和运行时桥接。
|
||||
- `单独仓库(可选)`
|
||||
- 只在你要发布一个可复用的 MCP Server 时才有价值。
|
||||
- 例如 `gonavi-mcp-sql-tools`、`gonavi-mcp-jvm-agent` 这类独立 server。
|
||||
|
||||
结论:
|
||||
|
||||
- “客户端支持 MCP” 不需要新仓库。
|
||||
- “某个独立 MCP Server” 是否拆仓库,取决于它要不要单独发布、复用或部署。
|
||||
|
||||
## 3. Skills 设计
|
||||
|
||||
Skills 不建议直接等同于“另一种提示词”。
|
||||
|
||||
更合适的定义是:
|
||||
|
||||
- `skill manifest`
|
||||
- 名称、说明、适用场景、是否默认启用。
|
||||
- `skill prompt`
|
||||
- 该技能追加的 system prompt / few-shot / 输出约束。
|
||||
- `skill tool requirements`
|
||||
- 该技能依赖哪些内置工具或 MCP 工具。
|
||||
- `skill shortcuts`
|
||||
- 可选地给欢迎卡片、斜杠命令或快速动作提供入口。
|
||||
|
||||
一个 Skill 本质上应该是“提示词 + 工具依赖 + 使用入口”的组合,而不是单独一段文案。
|
||||
|
||||
### Skills 是否需要单独 GitHub 仓库
|
||||
|
||||
第一阶段不需要。
|
||||
|
||||
建议顺序:
|
||||
|
||||
1. 先在 GoNavi 主仓库内把 Skills manifest/runtime 跑通。
|
||||
2. 等格式稳定后,再考虑增加“本地目录导入”或“Git 仓库导入”。
|
||||
|
||||
只有当你明确要做下面两件事时,独立仓库才值得:
|
||||
|
||||
- 把 Skills 当作社区共享资产分发。
|
||||
- 让不同团队独立维护自己的 skill pack。
|
||||
|
||||
## 建议的下一步实现顺序
|
||||
|
||||
1. 抽出统一 `ToolRegistry`,让 `LOCAL_TOOLS` 不再硬编码在聊天面板内部。
|
||||
2. 在 AI 设置中新增 `MCP Servers` 配置页。
|
||||
3. 后端先支持最小 transport:
|
||||
- `stdio`
|
||||
- `http/sse`(如果后续确认需要)
|
||||
4. 在 AI 设置中新增 `Skills` 配置页。
|
||||
5. 让 Skill 以 manifest 形式声明:
|
||||
- `id`
|
||||
- `name`
|
||||
- `description`
|
||||
- `systemPrompt`
|
||||
- `requiredTools`
|
||||
- `scopes`
|
||||
6. 再决定是否增加“从 Git 仓库同步 MCP/Skills 包”的分发能力。
|
||||
58
Dockerfile.build-env
Normal file
@@ -0,0 +1,58 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
ARG GO_IMAGE=golang:1.25-bookworm
|
||||
ARG NODE_IMAGE=node:20-bookworm
|
||||
|
||||
FROM ${NODE_IMAGE} AS node
|
||||
|
||||
FROM ${GO_IMAGE}
|
||||
|
||||
ARG WEBKIT_API=4.0
|
||||
ARG WAILS_VERSION=v2.11.0
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV PATH=/go/bin:/usr/local/go/bin:${PATH}
|
||||
|
||||
# Debian's /etc/profile resets PATH in login shells (bash -l), dropping the Go
|
||||
# toolchain; restore it via profile.d which is sourced after that reset.
|
||||
RUN printf 'export PATH=/go/bin:/usr/local/go/bin:$PATH\n' > /etc/profile.d/10-gonavi-build-env.sh
|
||||
|
||||
# Copy only Node.js runtime assets so the Go toolchain from the base image
|
||||
# stays intact for smoke tests and interactive build shells.
|
||||
COPY --from=node /usr/local/bin/node /usr/local/bin/node
|
||||
COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
|
||||
|
||||
# npm/npx/corepack are symlinks in the node image; COPY dereferences them into
|
||||
# standalone scripts whose relative requires break, so recreate the symlinks.
|
||||
RUN ln -sf ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
|
||||
&& ln -sf ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx \
|
||||
&& ln -sf ../lib/node_modules/corepack/dist/corepack.js /usr/local/bin/corepack
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
bash \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
curl \
|
||||
file \
|
||||
git \
|
||||
make \
|
||||
pkg-config \
|
||||
python3 \
|
||||
unzip \
|
||||
zip \
|
||||
libgtk-3-dev \
|
||||
&& if [ "${WEBKIT_API}" = "4.1" ]; then \
|
||||
apt-get install -y --no-install-recommends libwebkit2gtk-4.1-dev libsoup-3.0-dev; \
|
||||
else \
|
||||
apt-get install -y --no-install-recommends libwebkit2gtk-4.0-dev; \
|
||||
fi \
|
||||
&& go install github.com/wailsapp/wails/v2/cmd/wails@${WAILS_VERSION} \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN mkdir -p /usr/share/doc/gonavi
|
||||
COPY LICENSE NOTICE /usr/share/doc/gonavi/
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
CMD ["bash"]
|
||||
49
Dockerfile.mcp-server
Normal file
@@ -0,0 +1,49 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:1.25-bookworm AS builder
|
||||
|
||||
# Declared without defaults so BuildKit injects the real target platform;
|
||||
# a default like TARGETARCH=amd64 would win and ship amd64 binaries to arm64.
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
COPY third_party/highgo-pq/go.mod ./third_party/highgo-pq/go.mod
|
||||
COPY third_party/go-irisnative/go.mod third_party/go-irisnative/go.sum ./third_party/go-irisnative/
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN ./tools/generate-driver-agent-revisions.sh --platform "${TARGETOS}/${TARGETARCH}"
|
||||
|
||||
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
|
||||
go build -trimpath -ldflags="-s -w" -o /out/gonavi-mcp-server ./cmd/gonavi-mcp-server
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates tzdata \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& useradd --system --create-home --home-dir /var/lib/gonavi --uid 10001 gonavi \
|
||||
&& mkdir -p /data /var/lib/gonavi/logs /usr/share/doc/gonavi \
|
||||
&& chown -R gonavi:gonavi /data /var/lib/gonavi
|
||||
|
||||
COPY --from=builder /out/gonavi-mcp-server /usr/local/bin/gonavi-mcp-server
|
||||
COPY --from=builder /src/LICENSE /usr/share/doc/gonavi/LICENSE
|
||||
COPY --from=builder /src/NOTICE /usr/share/doc/gonavi/NOTICE
|
||||
|
||||
ENV HOME=/var/lib/gonavi \
|
||||
GONAVI_DATA_ROOT=/data \
|
||||
GONAVI_LOG_DIR=/var/lib/gonavi/logs \
|
||||
GONAVI_MCP_HTTP_ADDR=0.0.0.0:8765 \
|
||||
GONAVI_MCP_HTTP_PATH=/mcp \
|
||||
GONAVI_MCP_SCHEMA_ONLY=true
|
||||
|
||||
VOLUME ["/data"]
|
||||
EXPOSE 8765
|
||||
|
||||
USER gonavi
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/gonavi-mcp-server"]
|
||||
CMD ["http"]
|
||||
65
Dockerfile.web-server
Normal file
@@ -0,0 +1,65 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
#
|
||||
# GoNavi Web Server(浏览器访问版)镜像。
|
||||
# 复用主程序 web-server 模式 + 嵌入 frontend/dist,不依赖桌面 WebView/CGO。
|
||||
|
||||
FROM --platform=$BUILDPLATFORM node:20-bookworm AS frontend
|
||||
|
||||
# 使用 monorepo 布局:frontend 源码通过相对路径 ../../../shared/i18n 引用仓库 shared/
|
||||
# (见 frontend/src/i18n/catalog.ts、shared/i18n/messages.ts、shared/i18n/translate.ts)。
|
||||
# 仅 COPY frontend/ 会导致 tsc 报 Cannot find module '../../../shared/i18n/*'。
|
||||
WORKDIR /src/frontend
|
||||
COPY frontend/package.json frontend/package-lock.json ./
|
||||
COPY frontend/patches ./patches
|
||||
RUN npm ci --no-audit --no-fund
|
||||
COPY frontend/ ./
|
||||
COPY shared/ /src/shared/
|
||||
# wailsjs 绑定已入库;容器内只构建静态前端
|
||||
RUN npm run build
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:1.25-bookworm AS builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
COPY third_party/highgo-pq/go.mod ./third_party/highgo-pq/go.mod
|
||||
COPY third_party/go-irisnative/go.mod third_party/go-irisnative/go.sum ./third_party/go-irisnative/
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN ./tools/generate-driver-agent-revisions.sh --platform "${TARGETOS}/${TARGETARCH}"
|
||||
COPY --from=frontend /src/frontend/dist ./frontend/dist
|
||||
|
||||
# 纯 Go 构建主程序(web-server 模式不需要 Wails WebView / CGO)
|
||||
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
|
||||
go build -trimpath -ldflags="-s -w" -o /out/gonavi .
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates tzdata curl \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& useradd --system --create-home --home-dir /var/lib/gonavi --uid 10001 gonavi \
|
||||
&& mkdir -p /data /var/lib/gonavi/logs /usr/share/doc/gonavi \
|
||||
&& chown -R gonavi:gonavi /data /var/lib/gonavi
|
||||
|
||||
COPY --from=builder /out/gonavi /usr/local/bin/gonavi
|
||||
COPY --from=builder /src/LICENSE /usr/share/doc/gonavi/LICENSE
|
||||
COPY --from=builder /src/NOTICE /usr/share/doc/gonavi/NOTICE
|
||||
|
||||
ENV HOME=/var/lib/gonavi \
|
||||
GONAVI_DATA_ROOT=/data \
|
||||
GONAVI_LOG_DIR=/var/lib/gonavi/logs \
|
||||
GONAVI_WEB_ADDR=0.0.0.0:34116
|
||||
|
||||
VOLUME ["/data"]
|
||||
EXPOSE 34116
|
||||
|
||||
USER gonavi
|
||||
|
||||
# 未通过环境变量配置密码时,首次访问 /setup 初始化;认证状态落在数据目录 web_auth.json
|
||||
ENTRYPOINT ["/usr/local/bin/gonavi"]
|
||||
CMD ["web-server"]
|
||||
181
NOTICE
Normal file
@@ -0,0 +1,181 @@
|
||||
GoNavi
|
||||
Copyright 2026 Syngnat
|
||||
|
||||
This product includes software developed by the following projects.
|
||||
|
||||
Apache RocketMQ
|
||||
Copyright 2016-2022 The Apache Software Foundation
|
||||
|
||||
This product includes software developed at
|
||||
The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
Apache IoTDB
|
||||
Copyright 2018-2020 The Apache Software Foundation.
|
||||
|
||||
This product includes software developed at
|
||||
The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
============================================================================
|
||||
|
||||
IoTDB project uses 4 Chinese Patents:
|
||||
* 201711384490X
|
||||
* 201810111712.9
|
||||
* 201711322631.5
|
||||
* 201711319331.1
|
||||
|
||||
According to the Apache 2.0 License. The owner of the patents, Tsinghua University,
|
||||
grant the users the right to the use of patent under the requirement of Apache 2.0 License.
|
||||
|
||||
============================================================================
|
||||
|
||||
Apache Thrift
|
||||
Copyright (C) 2006 - 2019, The Apache Software Foundation
|
||||
|
||||
This product includes software developed at
|
||||
The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
go-yaml
|
||||
Copyright 2011-2016 Canonical Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
Eclipse Paho MQTT Go
|
||||
|
||||
This content is produced and maintained by the Eclipse Paho project.
|
||||
|
||||
* Project home: https://www.eclipse.org/paho/
|
||||
|
||||
Note that a separate mqtt v5 client (https://github.com/eclipse/paho.golang) also
|
||||
exists. It is a full rewrite and deliberately incompatible with this library.
|
||||
|
||||
Eclipse Mosquitto trademarks of the Eclipse Foundation. Eclipse, and the
|
||||
Eclipse Logo are registered trademarks of the Eclipse Foundation.
|
||||
|
||||
Paho is a trademark of the Eclipse Foundation. Eclipse, and the Eclipse Logo are
|
||||
registered trademarks of the Eclipse Foundation.
|
||||
|
||||
All content is the property of the respective authors or their employers.
|
||||
For more information regarding authorship of content, please consult the
|
||||
listed source code repository logs.
|
||||
|
||||
This program and the accompanying materials are made available under the terms of the
|
||||
Eclipse Public License v2.0 and Eclipse Distribution License v1.0 which accompany this
|
||||
distribution.
|
||||
|
||||
The Eclipse Public License is available at
|
||||
https://www.eclipse.org/legal/epl-2.0/
|
||||
and the Eclipse Distribution License is available at
|
||||
http://www.eclipse.org/org/documents/edl-v10.php.
|
||||
|
||||
For an explanation of what dual-licensing means to you, see:
|
||||
https://www.eclipse.org/legal/eplfaq.php#DUALLIC
|
||||
|
||||
SPDX-License-Identifier: EPL-2.0 or BSD-3-Clause
|
||||
|
||||
The project maintains the following source code repository:
|
||||
|
||||
* https://github.com/eclipse/paho.mqtt.golang
|
||||
|
||||
This project makes use of the following third-party projects:
|
||||
|
||||
Go Programming Language and Standard Library
|
||||
* License: BSD-style license (https://golang.org/LICENSE)
|
||||
|
||||
Go Networking
|
||||
* License: BSD 3-Clause style license and patent grant.
|
||||
* Project: https://cs.opensource.google/go/x/net
|
||||
|
||||
Go Sync
|
||||
* License: BSD 3-Clause style license and patent grant.
|
||||
* Project: https://cs.opensource.google/go/x/sync/
|
||||
|
||||
Gorilla Websockets v1.4.2
|
||||
* License: BSD 2-Clause "Simplified" License
|
||||
* Project: https://github.com/gorilla/websocket
|
||||
|
||||
Content may contain encryption software. The country in which you are currently
|
||||
may have restrictions on the import, possession, and use, and/or re-export to
|
||||
another country. BEFORE using any encryption software, please check the country's
|
||||
laws, regulations and policies concerning the import, possession, and use, and/or
|
||||
re-export of encryption software, to see if this is permitted.
|
||||
|
||||
Vendored HighGo pq-sm3 (derived from lib/pq)
|
||||
Copyright (c) 2011-2013, 'pq' Contributors
|
||||
Portions Copyright (C) 2011 Blake Mizerany
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
The vendored SCRAM implementation includes the following BSD 3-Clause notice:
|
||||
|
||||
Copyright (c) 2014 - Gustavo Niemeyer <gustavo@niemeyer.net>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Vendored InterSystems IRIS native driver
|
||||
Copyright (c) 2025 Dmitry Maslennikov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
429
README.md
@@ -1,194 +1,343 @@
|
||||
# GoNavi - A Modern Lightweight Database Client
|
||||
<p align="center">
|
||||
<img src="assets/readme-logo.png" width="128" alt="GoNavi Logo" />
|
||||
</p>
|
||||
|
||||
[](https://go.dev/)
|
||||
[](https://wails.io)
|
||||
[](https://reactjs.org/)
|
||||
[](LICENSE)
|
||||
[](https://github.com/Syngnat/GoNavi/actions)
|
||||
[](https://github.com/Syngnat/GoNavi/stargazers)
|
||||
[](https://github.com/Syngnat/GoNavi/releases)
|
||||
<h1 align="center">GoNavi</h1>
|
||||
|
||||
**Language**: English | [简体中文](README.zh-CN.md)
|
||||
<p align="center">
|
||||
<b>Navigate every data source — native speed, agent-ready, zero Electron bloat.</b>
|
||||
</p>
|
||||
|
||||
GoNavi is a modern, high-performance, cross-platform database client built with **Wails (Go)** and **React**.
|
||||
It delivers native-like responsiveness with low resource usage.
|
||||
<p align="center">
|
||||
A high-performance cross-platform database client built with
|
||||
<a href="https://wails.io">Wails</a> (Go) + <a href="https://react.dev">React</a>.
|
||||
Desktop-first. MCP-ready. ~10MB class binaries.
|
||||
</p>
|
||||
|
||||
Compared with many Electron-based clients, GoNavi is typically smaller in binary size (around 10MB class), starts faster, and uses less memory.
|
||||
<p align="center">
|
||||
<a href="https://github.com/Syngnat/GoNavi/releases"><img src="https://img.shields.io/github/v/release/Syngnat/GoNavi?style=for-the-badge&color=8B5CF6" alt="Release" /></a>
|
||||
<a href="https://github.com/Syngnat/GoNavi/releases"><img src="https://img.shields.io/github/downloads/Syngnat/GoNavi/total?style=for-the-badge&color=6366F1&label=downloads" alt="Downloads" /></a>
|
||||
<a href="https://github.com/Syngnat/GoNavi/stargazers"><img src="https://img.shields.io/github/stars/Syngnat/GoNavi?style=for-the-badge&color=F59E0B" alt="Stars" /></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/License-Apache%202.0-22C55E?style=for-the-badge" alt="License" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://go.dev"><img src="https://img.shields.io/github/go-mod/go-version/Syngnat/GoNavi?style=flat-square&logo=go&logoColor=white&label=Go" alt="Go" /></a>
|
||||
<a href="https://wails.io"><img src="https://img.shields.io/badge/Wails-v2-red?style=flat-square" alt="Wails" /></a>
|
||||
<a href="https://reactjs.org"><img src="https://img.shields.io/badge/React-18-61DAFB?style=flat-square&logo=react&logoColor=white" alt="React" /></a>
|
||||
<a href="https://github.com/Syngnat/GoNavi/actions"><img src="https://img.shields.io/github/actions/workflow/status/Syngnat/GoNavi/release.yml?style=flat-square&label=Build" alt="Build" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<b>Language</b>: English · <a href="README.zh-CN.md">简体中文</a>
|
||||
·
|
||||
<a href="https://github.com/Syngnat/GoNavi/releases"><b>⬇ Download</b></a>
|
||||
·
|
||||
<a href="#-quick-start"><b>⚡ Quick Start</b></a>
|
||||
·
|
||||
<a href="#-key-features"><b>✨ Features</b></a>
|
||||
·
|
||||
<a href="#-mcp--agents"><b>🤖 MCP</b></a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## Project Overview
|
||||
## Why GoNavi?
|
||||
|
||||
GoNavi is designed for developers and DBAs who need a unified desktop experience across multiple databases.
|
||||
Most database GUIs are Electron shells with megabytes of tax. GoNavi takes a different path:
|
||||
|
||||
- **Native-performance architecture**: Wails (Go + WebView) with lightweight runtime overhead.
|
||||
- **Large dataset usability**: virtualized rendering and optimized DataGrid workflows for high-volume tables.
|
||||
- **Unified connectivity**: URI build/parse, SSH tunnel, proxy support, and on-demand driver activation.
|
||||
- **Production-oriented workflow**: SQL editor, object management, batch export/backup, sync tools, execution logs, and update checks.
|
||||
| | Typical Electron client | **GoNavi** |
|
||||
|---|---|---|
|
||||
| Runtime | Chromium + Node | **Go + native WebView** |
|
||||
| Binary size | Hundreds of MB | **~10MB class** |
|
||||
| Startup | Heavy | **Fast** |
|
||||
| Memory | High baseline | **Lean** |
|
||||
| AI / Agents | Bolt-on or absent | **First-class MCP + multi-provider AI** |
|
||||
| Data sources | Mostly RDBMS | **SQL · Cache · Vector · MQ · Search · Time-series · Domestic DBs** |
|
||||
|
||||
## Supported Data Sources
|
||||
> **One cockpit for MySQL, Postgres, Redis, Kafka, Milvus, OceanBase, ClickHouse…**
|
||||
> Query, edit, audit, sync — and hand structured context to coding agents without leaking passwords off-host.
|
||||
|
||||
> `Built-in`: available out of the box.
|
||||
> `Optional driver agent`: install/enable via Driver Manager first.
|
||||
---
|
||||
|
||||
## At a Glance
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ GoNavi Workbench │
|
||||
│ ┌─────────────┐ ┌──────────────────┐ ┌─────────────────────────┐ │
|
||||
│ │ Connections │ │ Monaco SQL + AI │ │ Virtualized DataGrid │ │
|
||||
│ │ SSH / Proxy │ │ Schema context │ │ Batch edit · Export │ │
|
||||
│ │ Drivers │ │ Slash commands │ │ Txn submit / rollback │ │
|
||||
│ └─────────────┘ └────────┬─────────┘ └─────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────▼─────────────┐ │
|
||||
│ │ Go core · Audit · Sync │ │
|
||||
│ │ MCP HTTP · Web Server │ │
|
||||
│ └───────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Product screenshots
|
||||
|
||||
Each image is a **full GoNavi application window**, scaled proportionally for README display.
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/Syngnat/GoNavi/dev/assets/screenshots/01-home-workbench.png" alt="GoNavi full window — connections, queries, and workbench" width="560" />
|
||||
|
||||
<img src="https://raw.githubusercontent.com/Syngnat/GoNavi/dev/assets/screenshots/04-ai-assistant.png" alt="GoNavi full window — AI assistant with schema context" width="560" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/Syngnat/GoNavi/dev/assets/screenshots/06-new-connection.png" alt="GoNavi full window — new connection data-source selector" width="560" />
|
||||
</p>
|
||||
|
||||
<p align="center"><sub>Real desktop captures · full window</sub></p>
|
||||
|
||||
---
|
||||
|
||||
## ✨ Key Features
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%" valign="top">
|
||||
|
||||
### 🤖 AI that knows your schema
|
||||
- OpenAI · Gemini · Claude · custom OpenAI-compatible APIs
|
||||
- Attach live table schemas as context
|
||||
- Slash commands: generate SQL, explain, optimize, review
|
||||
- **MCP**: install into Claude Code / Codex, or Streamable HTTP for remote agents
|
||||
- Secrets stay on the GoNavi host — agents get tools, not raw passwords
|
||||
|
||||
</td>
|
||||
<td width="50%" valign="top">
|
||||
|
||||
### ⚡ Built for large data
|
||||
- Virtualized DataGrid for heavy result sets
|
||||
- In-place cell edit · batch CRUD · transaction submit/rollback
|
||||
- Large-field popup editor · smart read/write modes
|
||||
- Export: CSV · XLSX · JSON · Markdown
|
||||
- Monaco editor with context-aware completion
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%" valign="top">
|
||||
|
||||
### 🔌 Connectivity without drama
|
||||
- URI generate / parse
|
||||
- SSH tunnel · proxy
|
||||
- Connection JSON import / export
|
||||
- On-demand optional driver agents
|
||||
- Custom Driver + DSN extensibility
|
||||
|
||||
</td>
|
||||
<td width="50%" valign="top">
|
||||
|
||||
### 🛡️ Observable & shippable
|
||||
- SQL execution logs with timing
|
||||
- Audit center (redacted by default, retention, export)
|
||||
- Desktop + experimental **Web Server** mode
|
||||
- Docker / K8s / Helm / Podman packaging
|
||||
- Auto update checks · multi-arch releases
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### 🧩 Stack
|
||||
|
||||
`Go 1.24` · `Wails v2` · `React 18` · `TypeScript` · `Vite` · `Ant Design 5` · `Zustand` · `Monaco`
|
||||
|
||||
---
|
||||
|
||||
## 🗄 Supported Data Sources
|
||||
|
||||
> **Built-in** — ready out of the box · **Optional agent** — install via Driver Manager
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Built-in** | MySQL · GoldenDB · PostgreSQL · Oracle · Redis · Chroma · Qdrant · Milvus · RocketMQ · MQTT · Kafka · RabbitMQ |
|
||||
| **Optional** | MariaDB · Doris · StarRocks · Sphinx · SQL Server · SQLite · DuckDB · OceanBase · Dameng · Kingbase · HighGo · Vastbase · OpenGauss · GaussDB · IRIS · MongoDB · TDengine · IoTDB · ClickHouse · Trino · Elasticsearch · Custom Driver/DSN |
|
||||
|
||||
<details>
|
||||
<summary><b>Full capability matrix</b></summary>
|
||||
|
||||
| Category | Data Source | Driver Mode | Typical Capabilities |
|
||||
|---|---|---|---|
|
||||
| Relational | MySQL | Built-in | Schema browsing, SQL query, data editing, export/backup |
|
||||
| Domestic DB | GoldenDB | Built-in | MySQL-compatible query workflow and distributed transaction scenarios |
|
||||
| Relational | PostgreSQL | Built-in | Schema browsing, SQL query, data editing, object management |
|
||||
| Relational | Oracle | Built-in | Query execution, object browsing, data editing |
|
||||
| Cache | Redis | Built-in | Key browsing, command execution, encoding/view switch |
|
||||
| Vector Database | Chroma | Built-in | Collection browsing, vector retrieval, metadata filtering |
|
||||
| Vector Database | Qdrant | Built-in | Collection browsing, vector search, payload filtering |
|
||||
| Vector Database | Milvus | Built-in | Collection browsing, vector search, scalar filtering |
|
||||
| Message Queue | RocketMQ | Built-in | Topic browsing, consumer-group inspection, message-oriented workflow |
|
||||
| Message Queue | MQTT | Built-in | Broker and topic-filter workflow with QoS-aware connection settings |
|
||||
| Message Queue | Kafka | Built-in | Topic browsing, broker metadata, consumer-group workflow |
|
||||
| Message Queue | RabbitMQ | Built-in | Queue/exchange browsing, virtual host inspection, management API workflow |
|
||||
| Relational | MariaDB | Optional driver agent | Querying, object management, data editing |
|
||||
| Relational | Doris | Optional driver agent | Querying, object browsing, SQL execution |
|
||||
| Columnar Analytics | StarRocks | Optional driver agent | Querying, object browsing, SQL execution |
|
||||
| Search | Sphinx | Optional driver agent | SphinxQL querying and object browsing |
|
||||
| Relational | SQL Server | Optional driver agent | Schema browsing, SQL query, object management |
|
||||
| File-based | SQLite | Optional driver agent | Local DB browsing, editing, export |
|
||||
| File-based | DuckDB | Optional driver agent | Large-table query, pagination, file-DB workflow |
|
||||
| Domestic DB | OceanBase | Optional driver agent | MySQL / Oracle tenant access, object browsing, query workflow |
|
||||
| Domestic DB | Dameng | Optional driver agent | Querying, object browsing, data editing |
|
||||
| Domestic DB | Kingbase | Optional driver agent | Querying, object browsing, data editing |
|
||||
| Domestic DB | HighGo | Optional driver agent | Querying, object browsing, data editing |
|
||||
| Domestic DB | Vastbase | Optional driver agent | Querying, object browsing, data editing |
|
||||
| Domestic DB | OpenGauss | Optional driver agent | PostgreSQL-like schema browsing, SQL query, object management |
|
||||
| Domestic DB | GaussDB | Optional driver agent | PostgreSQL-like schema browsing, SQL query, object management |
|
||||
| Multi-model | InterSystems IRIS | Optional driver agent | Namespace browsing, SQL query, object management |
|
||||
| Document | MongoDB | Optional driver agent | Document query, collection browsing, connection management |
|
||||
| Time-series | TDengine | Optional driver agent | Time-series schema browsing and querying |
|
||||
| Time-series | Apache IoTDB | Optional driver agent | Storage group / device / timeseries browsing and querying |
|
||||
| Columnar Analytics | ClickHouse | Optional driver agent | Analytical query, object browsing, SQL execution |
|
||||
| Federated Query | Trino | Optional driver agent | Cross-source SQL via multiple catalogs, `catalog.schema` browsing, SQL execution |
|
||||
| Search | Elasticsearch | Optional driver agent | Index browsing, mapping inspection, JSON DSL / query_string search |
|
||||
| Extensibility | Custom Driver/DSN | Custom | Extend to more data sources via Driver + DSN |
|
||||
|
||||
<h2 align="center">📸 Screenshots</h2>
|
||||
|
||||
<div align="center">
|
||||
<img width="25%" alt="image" src="https://github.com/user-attachments/assets/0eefe07f-2836-44fa-9ddf-a0d2124b90e2" />
|
||||
<img width="25%" alt="image" src="https://github.com/user-attachments/assets/6765e539-83ea-4cd6-9c9e-f42790fa05b5" />
|
||||
<img width="25%" alt="image" src="https://github.com/user-attachments/assets/60e3d187-171a-4248-94e0-c6b08736e235" />
|
||||
<br />
|
||||
<img width="25%" alt="image" src="https://github.com/user-attachments/assets/7a478602-0f08-4b30-8f6a-879f4a60ae32" />
|
||||
<img width="14%" alt="image" src="https://github.com/user-attachments/assets/6442ca7d-ce9e-46d9-aecd-405ba88f5a5e" />
|
||||
<img width="25%" alt="image" src="https://github.com/user-attachments/assets/bc17895e-02a4-4cc5-b471-c3803cf25a2b" />
|
||||
</div>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## Key Features
|
||||
|
||||
### AI Assistant (New)
|
||||
- **Multi-provider Support**: OpenAI, Google Gemini, Anthropic Claude, and custom API support.
|
||||
- **Context-Aware Chat**: Attach table schemas to the AI context for accurate SQL generation and assistance.
|
||||
- **Slash Commands**: Quick commands for generating SQL, explaining queries, optimizing performance, and reviewing schema designs.
|
||||
|
||||
### Performance
|
||||
- **Smooth interaction under load**: optimized table interaction (including column resize workflow on large datasets).
|
||||
- **Virtualized rendering**: keeps large result sets responsive.
|
||||
|
||||
### Data Management (DataGrid)
|
||||
- In-place cell editing.
|
||||
- Batch insert/update/delete with transaction-oriented submit/rollback.
|
||||
- Large-field popup editor.
|
||||
- Context actions (set NULL, copy/export, etc.).
|
||||
- Smart read/write mode switching based on query context.
|
||||
- Export formats: CSV, Excel (XLSX), JSON, Markdown.
|
||||
|
||||
### SQL Editor
|
||||
- Monaco Editor core.
|
||||
- Context-aware completion for databases/tables/columns.
|
||||
- Multi-tab query workflow.
|
||||
|
||||
### Batch Export / Backup
|
||||
- Database-level and table-level batch export/backup.
|
||||
- Scope-aware operation flow to reduce mistakes.
|
||||
|
||||
### Connectivity
|
||||
- URI generation/parsing.
|
||||
- SSH tunnel support.
|
||||
- Proxy support.
|
||||
- Config import/export (JSON).
|
||||
- Optional driver management and activation.
|
||||
|
||||
### Redis Tools
|
||||
- Multi-view value rendering (auto/raw text/UTF-8/hex).
|
||||
- Built-in command execution panel.
|
||||
|
||||
### Observability and Update
|
||||
- SQL execution logs with timing information.
|
||||
- Startup/scheduled/manual update checks.
|
||||
|
||||
### UI/UX
|
||||
- Ant Design 5 based interface.
|
||||
- Light/Dark themes.
|
||||
- Flexible sidebar and layout behavior.
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Backend**: Go 1.24 + Wails v2
|
||||
- **Frontend**: React 18 + TypeScript + Vite
|
||||
- **UI**: Ant Design 5
|
||||
- **State Management**: Zustand
|
||||
- **Editor**: Monaco Editor
|
||||
|
||||
---
|
||||
|
||||
## Installation and Run
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Go](https://go.dev/dl/) 1.21+
|
||||
- [Node.js](https://nodejs.org/) 18+
|
||||
- [Wails CLI](https://wails.io/docs/gettingstarted/installation):
|
||||
`go install github.com/wailsapp/wails/v2/cmd/wails@latest`
|
||||
|
||||
### Development Mode
|
||||
- [Wails CLI](https://wails.io/docs/gettingstarted/installation)
|
||||
|
||||
```bash
|
||||
go install github.com/wailsapp/wails/v2/cmd/wails@v2.11.0
|
||||
```
|
||||
|
||||
### Develop
|
||||
|
||||
```bash
|
||||
# Clone
|
||||
git clone https://github.com/Syngnat/GoNavi.git
|
||||
cd GoNavi
|
||||
|
||||
# Start development with hot reload
|
||||
wails dev
|
||||
wails dev # full hot reload
|
||||
node tools/wails-fast-dev.mjs # faster when Go exports unchanged
|
||||
node tools/wails-fast-dev.mjs --refresh-bindings # after Go export signature changes
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
# Build for current platform
|
||||
wails build
|
||||
|
||||
# Clean build (recommended before release)
|
||||
wails build -clean
|
||||
wails build -clean # clean build before release
|
||||
```
|
||||
|
||||
Artifacts are generated in `build/bin`.
|
||||
Artifacts → `build/bin`.
|
||||
|
||||
### Cross-Platform Release (GitHub Actions)
|
||||
### Prefer a binary?
|
||||
|
||||
The repository includes a release workflow.
|
||||
Push a `v*` tag to trigger automated build and release.
|
||||
Release notes are generated automatically from merged pull requests and categorized by `.github/release.yaml`.
|
||||
|
||||
Target artifacts include:
|
||||
- macOS (AMD64 / ARM64)
|
||||
- Windows (AMD64)
|
||||
- Linux (AMD64, WebKitGTK 4.0 and 4.1 variants)
|
||||
Grab the latest build from **[Releases](https://github.com/Syngnat/GoNavi/releases)**
|
||||
(macOS AMD64/ARM64 · Windows AMD64 · Linux WebKitGTK 4.0/4.1).
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
## 🌐 Web Server (Experimental)
|
||||
|
||||
### macOS: "App is damaged and can’t be opened"
|
||||
Same Go backend + React UI over HTTP — **not** a containerized Wails window.
|
||||
|
||||
Without Apple notarization, Gatekeeper may block startup.
|
||||
```powershell
|
||||
go build .
|
||||
.\GoNavi-Wails.exe web-server --addr 127.0.0.1:34116
|
||||
```
|
||||
|
||||
1. Move `GoNavi.app` to **Applications**.
|
||||
2. Open **Terminal**.
|
||||
3. Run:
|
||||
- First visit → `/setup` (admin password; optional Google Authenticator)
|
||||
- Bridge: `window.go.*` / `window.runtime.*` → HTTP / SSE
|
||||
- Sessions, recovery codes, login rate limits
|
||||
|
||||
#### Docker / Podman
|
||||
|
||||
```bash
|
||||
cp docker.web-server.env.example docker.web-server.env
|
||||
# set GONAVI_HOST_DATA_ROOT (absolute path)
|
||||
# optional: GONAVI_WEB_PASSWORD (min 6 chars)
|
||||
docker compose --env-file docker.web-server.env -f docker-compose.web-server.yml up -d
|
||||
```
|
||||
|
||||
Open `http://127.0.0.1:34116`. Mount the active data root at `/data`
|
||||
(`connections.json`, `daily_secrets.json`, optional `drivers/`).
|
||||
Auth state → `web_auth.json`.
|
||||
|
||||
> **Do not expose an unhardened web entry to the public internet.** Use reverse proxy + HTTPS in production.
|
||||
|
||||
Health: `GET /__gonavi/healthz`
|
||||
Local source build: add `-f docker-compose.web-server.local.yml --build`.
|
||||
|
||||
After changing env passwords:
|
||||
|
||||
```bash
|
||||
docker compose --env-file docker.web-server.env -f docker-compose.web-server.yml up -d --force-recreate
|
||||
```
|
||||
|
||||
(`docker restart` does **not** reload env files.)
|
||||
|
||||
---
|
||||
|
||||
## 🤖 MCP & Agents
|
||||
|
||||
Ship schema tools to agents without shipping your vault:
|
||||
|
||||
```bash
|
||||
cp docker.mcp-server.env.example docker.mcp-server.env
|
||||
docker compose --env-file docker.mcp-server.env -f docker-compose.mcp-server.yml up -d
|
||||
```
|
||||
|
||||
| Surface | Entry |
|
||||
|---|---|
|
||||
| MCP container | `docker-compose.mcp-server.yml` → `ghcr.io/syngnat/gonavi-mcp-server` |
|
||||
| Web UI container | `docker-compose.web-server.yml` → `ghcr.io/syngnat/gonavi-web-server` |
|
||||
| Podman / Quadlet | [deploy/podman/gonavi-mcp-server](deploy/podman/gonavi-mcp-server) |
|
||||
| Kubernetes | [deploy/k8s/gonavi-mcp-server](deploy/k8s/gonavi-mcp-server) |
|
||||
| Helm | [deploy/helm/gonavi-mcp-server](deploy/helm/gonavi-mcp-server) |
|
||||
| Build-only image | `Dockerfile.build-env` → `ghcr.io/syngnat/gonavi-build-env` |
|
||||
|
||||
Safety defaults: remote `schema-only` omits `execute_sql`; mutating SQL requires explicit `allowMutating=true`.
|
||||
Details: [cmd/gonavi-mcp-server/README.md](cmd/gonavi-mcp-server/README.md).
|
||||
|
||||
### Linux build environment only
|
||||
|
||||
```bash
|
||||
docker build -f Dockerfile.build-env -t gonavi-build-env:local .
|
||||
docker run --rm -it -v "$PWD:/workspace" -w /workspace gonavi-build-env:local bash
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Release Pipeline
|
||||
|
||||
Push a `v*` tag → GitHub Actions builds multi-arch releases.
|
||||
Notes auto-generated from merged PRs via `.github/release.yaml`.
|
||||
|
||||
---
|
||||
|
||||
## 🛠 Troubleshooting
|
||||
|
||||
<details>
|
||||
<summary><b>macOS: “App is damaged and can’t be opened”</b></summary>
|
||||
|
||||
Without Apple notarization, Gatekeeper may block the app:
|
||||
|
||||
```bash
|
||||
sudo xattr -rd com.apple.quarantine /Applications/GoNavi.app
|
||||
```
|
||||
|
||||
Or right-click the app in Finder and choose **Open** with Control key flow.
|
||||
Or right-click → Open (Control-click flow). Move the app to **Applications** first.
|
||||
|
||||
### Linux: missing `libwebkit2gtk` / `libjavascriptcoregtk`
|
||||
</details>
|
||||
|
||||
GoNavi depends on WebKitGTK runtime libraries.
|
||||
<details>
|
||||
<summary><b>Linux: missing WebKitGTK</b></summary>
|
||||
|
||||
```bash
|
||||
# Debian 13 / Ubuntu 24.04+
|
||||
@@ -200,21 +349,33 @@ sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-0 libwebkit2gtk-4.0-37 libjavascriptcoregtk-4.0-18
|
||||
```
|
||||
|
||||
If you use Linux artifacts with the `-WebKit41` suffix, prefer Debian 13 / Ubuntu 24.04+.
|
||||
Artifacts with `-WebKit41` prefer Debian 13 / Ubuntu 24.04+.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Linux: Chinese glyphs as tofu boxes</b></summary>
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y fonts-noto-cjk fonts-wqy-microhei
|
||||
fc-cache -fv
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
## 🤝 Contributing
|
||||
|
||||
Issues and pull requests are welcome.
|
||||
|
||||
For the full workflow, branch model, and maintainer sync rules, see:
|
||||
Issues and PRs welcome. Branch from **`dev`**, PR against **`dev`**.
|
||||
|
||||
- [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
|
||||
External contributors should branch from `dev` and open pull requests against `dev`.
|
||||
---
|
||||
|
||||
## ⭐ Star History
|
||||
|
||||
## Star History
|
||||
<a href="https://www.star-history.com/?repos=Syngnat%2FGoNavi&type=date&legend=top-left">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=Syngnat/GoNavi&type=date&theme=dark&legend=top-left" />
|
||||
@@ -223,6 +384,8 @@ External contributors should branch from `dev` and open pull requests against `d
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
---
|
||||
|
||||
## Links
|
||||
|
||||
- [linux.do](https://linux.do/)
|
||||
@@ -230,4 +393,8 @@ External contributors should branch from `dev` and open pull requests against `d
|
||||
|
||||
## License
|
||||
|
||||
Licensed under [Apache-2.0](LICENSE).
|
||||
[Apache-2.0](LICENSE)
|
||||
|
||||
<p align="center">
|
||||
<sub>Built for people who live in SQL, schemas, and agent loops.</sub>
|
||||
</p>
|
||||
|
||||
420
README.zh-CN.md
@@ -1,179 +1,342 @@
|
||||
# GoNavi - 现代化轻量级数据库客户端
|
||||
<p align="center">
|
||||
<img src="assets/readme-logo.png" width="128" alt="GoNavi Logo" />
|
||||
</p>
|
||||
|
||||
[](https://go.dev/)
|
||||
[](https://wails.io)
|
||||
[](https://reactjs.org/)
|
||||
[](LICENSE)
|
||||
[](https://github.com/Syngnat/GoNavi/actions)
|
||||
[](https://github.com/Syngnat/GoNavi/stargazers)
|
||||
[](https://github.com/Syngnat/GoNavi/releases)
|
||||
<h1 align="center">GoNavi</h1>
|
||||
|
||||
**语言**: [English](README.md) | 简体中文
|
||||
<p align="center">
|
||||
<b>一套客户端,航行全部数据源 — 原生性能 · Agent 就绪 · 告别 Electron 膨胀。</b>
|
||||
</p>
|
||||
|
||||
GoNavi 是基于 **Wails (Go)** 与 **React** 构建的跨平台数据库管理工具,强调原生性能、低资源占用与多数据源统一工作流。
|
||||
<p align="center">
|
||||
基于 <a href="https://wails.io">Wails</a>(Go)+ <a href="https://react.dev">React</a> 的跨平台数据库工作台。
|
||||
桌面优先,MCP 原生,二进制约 <b>10MB</b> 量级。
|
||||
</p>
|
||||
|
||||
相比常见 Electron 客户端,GoNavi 在体积、启动速度和内存占用上更轻量。
|
||||
<p align="center">
|
||||
<a href="https://github.com/Syngnat/GoNavi/releases"><img src="https://img.shields.io/github/v/release/Syngnat/GoNavi?style=for-the-badge&color=8B5CF6" alt="Release" /></a>
|
||||
<a href="https://github.com/Syngnat/GoNavi/releases"><img src="https://img.shields.io/github/downloads/Syngnat/GoNavi/total?style=for-the-badge&color=6366F1&label=downloads" alt="Downloads" /></a>
|
||||
<a href="https://github.com/Syngnat/GoNavi/stargazers"><img src="https://img.shields.io/github/stars/Syngnat/GoNavi?style=for-the-badge&color=F59E0B" alt="Stars" /></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/License-Apache%202.0-22C55E?style=for-the-badge" alt="License" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://go.dev"><img src="https://img.shields.io/github/go-mod/go-version/Syngnat/GoNavi?style=flat-square&logo=go&logoColor=white&label=Go" alt="Go" /></a>
|
||||
<a href="https://wails.io"><img src="https://img.shields.io/badge/Wails-v2-red?style=flat-square" alt="Wails" /></a>
|
||||
<a href="https://reactjs.org"><img src="https://img.shields.io/badge/React-18-61DAFB?style=flat-square&logo=react&logoColor=white" alt="React" /></a>
|
||||
<a href="https://github.com/Syngnat/GoNavi/actions"><img src="https://img.shields.io/github/actions/workflow/status/Syngnat/GoNavi/release.yml?style=flat-square&label=Build" alt="Build" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<b>语言</b>:<a href="README.md">English</a> · 简体中文
|
||||
·
|
||||
<a href="https://github.com/Syngnat/GoNavi/releases"><b>⬇ 下载</b></a>
|
||||
·
|
||||
<a href="#-快速开始"><b>⚡ 快速开始</b></a>
|
||||
·
|
||||
<a href="#-核心能力"><b>✨ 特性</b></a>
|
||||
·
|
||||
<a href="#-mcp--agents"><b>🤖 MCP</b></a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 项目简介
|
||||
## 为什么是 GoNavi?
|
||||
|
||||
GoNavi 面向开发者与 DBA,核心目标是让数据库操作在桌面端做到“快、稳、统一”。
|
||||
市面上大量数据库 GUI 仍是 Electron 壳:体积大、启动慢、内存高。GoNavi 换了一条路:
|
||||
|
||||
- **原生性能架构**:Wails(Go + WebView),降低运行时开销。
|
||||
- **大数据可用性**:虚拟滚动 + DataGrid 交互优化,提升大结果集可操作性。
|
||||
- **统一连接能力**:支持 URI 生成/解析、SSH 隧道、代理、驱动按需安装。
|
||||
- **工程化能力完整**:覆盖 SQL 编辑、对象管理、批量导出/备份、数据同步、执行日志、在线更新。
|
||||
| | 常见 Electron 客户端 | **GoNavi** |
|
||||
|---|---|---|
|
||||
| 运行时 | Chromium + Node | **Go + 系统 WebView** |
|
||||
| 体积 | 动辄数百 MB | **约 10MB 量级** |
|
||||
| 启动 | 偏重 | **更快** |
|
||||
| 内存 | 基线高 | **更轻** |
|
||||
| AI / Agent | 外挂或缺失 | **MCP + 多模型一等公民** |
|
||||
| 数据源 | 以 RDBMS 为主 | **SQL · 缓存 · 向量 · 消息 · 搜索 · 时序 · 国产库** |
|
||||
|
||||
## 支持的数据源
|
||||
> **MySQL、Postgres、Redis、Kafka、Milvus、OceanBase、ClickHouse… 一个工作台打通。**
|
||||
> 查询、编辑、审计、同步;把结构化上下文交给编码 Agent,密码仍留在本机。
|
||||
|
||||
> `内置`:主程序开箱即用。
|
||||
> `可选驱动代理`:需在驱动管理中安装启用后可用。
|
||||
---
|
||||
|
||||
## 一眼看懂
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ GoNavi 工作台 │
|
||||
│ ┌─────────────┐ ┌──────────────────┐ ┌─────────────────────────┐ │
|
||||
│ │ 连接管理 │ │ Monaco SQL + AI │ │ 虚拟滚动 DataGrid │ │
|
||||
│ │ SSH / 代理 │ │ 表结构上下文 │ │ 批量编辑 · 导出 │ │
|
||||
│ │ 驱动代理 │ │ 快捷指令 │ │ 事务提交 / 回滚 │ │
|
||||
│ └─────────────┘ └────────┬─────────┘ └─────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────▼─────────────┐ │
|
||||
│ │ Go 核心 · 审计 · 同步 │ │
|
||||
│ │ MCP HTTP · Web Server │ │
|
||||
│ └───────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 产品截图
|
||||
|
||||
每张均为 **完整 GoNavi 应用窗口**,再通过 README 宽度控件等比例缩小展示。
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/Syngnat/GoNavi/dev/assets/screenshots/01-home-workbench.png" alt="GoNavi 全窗口 — 连接、查询与工作台" width="560" />
|
||||
|
||||
<img src="https://raw.githubusercontent.com/Syngnat/GoNavi/dev/assets/screenshots/04-ai-assistant.png" alt="GoNavi 全窗口 — 带表结构上下文的 AI 助手" width="560" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/Syngnat/GoNavi/dev/assets/screenshots/06-new-connection.png" alt="GoNavi 全窗口 — 新建连接的数据源选择器" width="560" />
|
||||
</p>
|
||||
|
||||
<p align="center"><sub>真实桌面全窗口截图</sub></p>
|
||||
|
||||
---
|
||||
|
||||
## ✨ 核心能力
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%" valign="top">
|
||||
|
||||
### 🤖 懂表结构的 AI
|
||||
- OpenAI · Gemini · Claude · 自定义 OpenAI 兼容 API
|
||||
- 将当前库表结构注入对话上下文
|
||||
- 快捷指令:生成 SQL、解释、优化、表设计评审
|
||||
- **MCP**:一键装到 Claude Code / Codex,或 Streamable HTTP 给远端 Agent
|
||||
- 连接与密码留在运行 GoNavi 的主机上
|
||||
|
||||
</td>
|
||||
<td width="50%" valign="top">
|
||||
|
||||
### ⚡ 大数据仍流畅
|
||||
- 虚拟滚动 DataGrid,扛住大结果集
|
||||
- 单元格编辑 · 批量增删改 · 事务提交/回滚
|
||||
- 大字段弹窗 · 按查询智能切换读写
|
||||
- 导出:CSV · XLSX · JSON · Markdown
|
||||
- Monaco + 库/表/字段上下文补全
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%" valign="top">
|
||||
|
||||
### 🔌 连接与驱动
|
||||
- URI 生成 / 解析
|
||||
- SSH 隧道 · 代理
|
||||
- 连接配置 JSON 导入导出
|
||||
- 可选驱动代理按需安装
|
||||
- Custom Driver + DSN 扩展
|
||||
|
||||
</td>
|
||||
<td width="50%" valign="top">
|
||||
|
||||
### 🛡️ 可观测 & 可交付
|
||||
- SQL 执行日志(含耗时)
|
||||
- 审计中心(默认脱敏、保留策略、导出)
|
||||
- 桌面端 + 实验性 **Web Server**
|
||||
- Docker / K8s / Helm / Podman
|
||||
- 更新检查 · 多架构发布
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### 🧩 技术栈
|
||||
|
||||
`Go 1.24` · `Wails v2` · `React 18` · `TypeScript` · `Vite` · `Ant Design 5` · `Zustand` · `Monaco`
|
||||
|
||||
---
|
||||
|
||||
## 🗄 支持的数据源
|
||||
|
||||
> **内置**:开箱即用 · **可选驱动代理**:在驱动管理中安装启用
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **内置** | MySQL · GoldenDB · PostgreSQL · Oracle · Redis · Chroma · Qdrant · Milvus · RocketMQ · MQTT · Kafka · RabbitMQ |
|
||||
| **可选** | MariaDB · Doris · StarRocks · Sphinx · SQL Server · SQLite · DuckDB · OceanBase · 达梦 · 人大金仓 · 瀚高 · 海量 · openGauss · GaussDB · IRIS · MongoDB · TDengine · IoTDB · ClickHouse · Trino · Elasticsearch · Custom Driver/DSN |
|
||||
|
||||
<details>
|
||||
<summary><b>完整能力矩阵</b></summary>
|
||||
|
||||
| 类别 | 数据源 | 驱动模式 | 典型能力 |
|
||||
|---|---|---|---|
|
||||
| 关系型 | MySQL | 内置 | 库表浏览、SQL 查询、数据编辑、导出/备份 |
|
||||
| 国产数据库 | GoldenDB | 内置 | MySQL 兼容查询工作流、分布式事务场景 |
|
||||
| 关系型 | PostgreSQL | 内置 | 库表浏览、SQL 查询、数据编辑、对象管理 |
|
||||
| 关系型 | Oracle | 内置 | 连接查询、对象浏览、数据编辑 |
|
||||
| 缓存 | Redis | 内置 | Key 浏览、命令执行、编码/视图切换 |
|
||||
| 向量数据库 | Chroma | 内置 | Collection 浏览、向量检索、元数据过滤 |
|
||||
| 向量数据库 | Qdrant | 内置 | Collection 浏览、向量搜索、Payload 过滤 |
|
||||
| 向量数据库 | Milvus | 内置 | Collection 浏览、向量搜索、标量过滤 |
|
||||
| 消息队列 | RocketMQ | 内置 | Topic 浏览、消费组检查、消息型工作流 |
|
||||
| 消息队列 | MQTT | 内置 | Broker / Topic Filter 工作流与 QoS 连接配置 |
|
||||
| 消息队列 | Kafka | 内置 | Topic 浏览、Broker 元数据、消费组工作流 |
|
||||
| 消息队列 | RabbitMQ | 内置 | Queue / Exchange 浏览、Virtual Host 检查、Management API 工作流 |
|
||||
| 关系型 | MariaDB | 可选驱动代理 | 连接查询、对象管理、数据编辑 |
|
||||
| 关系型 | Doris | 可选驱动代理 | 连接查询、对象浏览、SQL 执行 |
|
||||
| 列式分析 | StarRocks | 可选驱动代理 | 连接查询、对象浏览、SQL 执行 |
|
||||
| 搜索 | Sphinx | 可选驱动代理 | SphinxQL 查询与对象浏览 |
|
||||
| 关系型 | SQL Server | 可选驱动代理 | 库表浏览、SQL 查询、对象管理 |
|
||||
| 文件型 | SQLite | 可选驱动代理 | 本地文件库浏览、编辑、导出 |
|
||||
| 文件型 | DuckDB | 可选驱动代理 | 大表查询、分页浏览、文件库管理 |
|
||||
| 国产数据库 | OceanBase | 可选驱动代理 | MySQL / Oracle 租户接入、对象浏览、查询工作流 |
|
||||
| 国产数据库 | Dameng | 可选驱动代理 | 连接查询、对象浏览、数据编辑 |
|
||||
| 国产数据库 | Kingbase | 可选驱动代理 | 连接查询、对象浏览、数据编辑 |
|
||||
| 国产数据库 | HighGo | 可选驱动代理 | 连接查询、对象浏览、数据编辑 |
|
||||
| 国产数据库 | Vastbase | 可选驱动代理 | 连接查询、对象浏览、数据编辑 |
|
||||
| 国产数据库 | OpenGauss | 可选驱动代理 | 类 PostgreSQL 的库表浏览、SQL 查询、对象管理 |
|
||||
| 国产数据库 | GaussDB | 可选驱动代理 | 类 PostgreSQL 的库表浏览、SQL 查询、对象管理 |
|
||||
| 多模型数据库 | InterSystems IRIS | 可选驱动代理 | Namespace 浏览、SQL 查询、对象管理 |
|
||||
| 文档型 | MongoDB | 可选驱动代理 | 文档查询、集合浏览、连接管理 |
|
||||
| 时序 | TDengine | 可选驱动代理 | 时序库表浏览、查询分析 |
|
||||
| 时序 | Apache IoTDB | 可选驱动代理 | Storage Group / Device / Timeseries 浏览与查询 |
|
||||
| 列式分析 | ClickHouse | 可选驱动代理 | 分析查询、对象浏览、SQL 执行 |
|
||||
| 联邦查询 | Trino | 可选驱动代理 | 跨多数据源联邦 SQL、`catalog.schema` 浏览、SQL 执行 |
|
||||
| 搜索 | Elasticsearch | 可选驱动代理 | 索引浏览、Mapping 检查、JSON DSL / query_string 查询 |
|
||||
| 扩展接入 | Custom Driver/DSN | 自定义 | 通过 Driver + DSN 接入更多数据源 |
|
||||
|
||||
<h2 align="center">📸 项目截图</h2>
|
||||
|
||||
<div align="center">
|
||||
<img width="25%" alt="image" src="https://github.com/user-attachments/assets/0eefe07f-2836-44fa-9ddf-a0d2124b90e2" />
|
||||
<img width="25%" alt="image" src="https://github.com/user-attachments/assets/6765e539-83ea-4cd6-9c9e-f42790fa05b5" />
|
||||
<img width="25%" alt="image" src="https://github.com/user-attachments/assets/60e3d187-171a-4248-94e0-c6b08736e235" />
|
||||
<br />
|
||||
<img width="25%" alt="image" src="https://github.com/user-attachments/assets/7a478602-0f08-4b30-8f6a-879f4a60ae32" />
|
||||
<img width="14%" alt="image" src="https://github.com/user-attachments/assets/6442ca7d-ce9e-46d9-aecd-405ba88f5a5e" />
|
||||
<img width="25%" alt="image" src="https://github.com/user-attachments/assets/bc17895e-02a4-4cc5-b471-c3803cf25a2b" />
|
||||
</div>
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 核心特性
|
||||
|
||||
### AI 智能助手 (New)
|
||||
- **多模型服务商支持**:内置跨平台接入 OpenAI, Google Gemini, Anthropic Claude,同时支持任意自定义兼容 OpenAI 格式的 API。
|
||||
- **关联表结构上下文**:原生支持将当前数据库表结构直接提取作为上下文发送给 AI,让 SQL 生成、分析变得更精准。
|
||||
- **快捷指令**:内置多种快捷对话指(如一键生成 SQL、解释执行逻辑、分析性能优化、表字段代码评审等)。
|
||||
|
||||
### 性能与交互
|
||||
- 大数据场景下保持流畅交互(含 DataGrid 列宽拖拽、批量编辑流程优化)。
|
||||
- 虚拟滚动渲染,降低大结果集卡顿风险。
|
||||
|
||||
### 数据管理(DataGrid)
|
||||
- 单元格所见即所得编辑。
|
||||
- 批量新增/修改/删除,支持事务提交与回滚。
|
||||
- 大字段弹窗编辑。
|
||||
- 右键上下文操作(NULL、复制、导出等)。
|
||||
- 根据查询上下文智能切换读写模式。
|
||||
- 支持 CSV / XLSX / JSON / Markdown 导出。
|
||||
|
||||
### SQL 编辑器
|
||||
- 基于 Monaco Editor。
|
||||
- 上下文补全(数据库/表/字段)。
|
||||
- 多标签查询工作流。
|
||||
|
||||
### 连接与驱动
|
||||
- URI 生成与解析。
|
||||
- SSH 隧道、代理支持。
|
||||
- 连接配置 JSON 导入/导出。
|
||||
- 可选驱动安装与启用管理。
|
||||
|
||||
### Redis 工具
|
||||
- 自动/原始文本/UTF-8/十六进制等视图模式。
|
||||
- 内置命令执行面板。
|
||||
|
||||
### 可观测性与更新
|
||||
- SQL 执行日志(含耗时)。
|
||||
- 启动/定时/手动更新检查。
|
||||
|
||||
### UI 体验
|
||||
- Ant Design 5 体系。
|
||||
- 深色/浅色主题切换。
|
||||
- 灵活布局与侧边栏行为。
|
||||
|
||||
---
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **后端**: Go 1.24 + Wails v2
|
||||
- **前端**: React 18 + TypeScript + Vite
|
||||
- **UI 框架**: Ant Design 5
|
||||
- **状态管理**: Zustand
|
||||
- **编辑器**: Monaco Editor
|
||||
|
||||
---
|
||||
|
||||
## 安装与运行
|
||||
## 🚀 快速开始
|
||||
|
||||
### 前置要求
|
||||
|
||||
- [Go](https://go.dev/dl/) 1.21+
|
||||
- [Node.js](https://nodejs.org/) 18+
|
||||
- [Wails CLI](https://wails.io/docs/gettingstarted/installation):
|
||||
`go install github.com/wailsapp/wails/v2/cmd/wails@latest`
|
||||
|
||||
### 开发模式
|
||||
- [Wails CLI](https://wails.io/docs/gettingstarted/installation)
|
||||
|
||||
```bash
|
||||
go install github.com/wailsapp/wails/v2/cmd/wails@v2.11.0
|
||||
```
|
||||
|
||||
### 开发
|
||||
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone https://github.com/Syngnat/GoNavi.git
|
||||
cd GoNavi
|
||||
|
||||
# 启动开发(热重载)
|
||||
wails dev
|
||||
wails dev # 完整热重载
|
||||
node tools/wails-fast-dev.mjs # Go 导出签名未变时更快
|
||||
node tools/wails-fast-dev.mjs --refresh-bindings # 修改导出方法签名后刷新绑定
|
||||
```
|
||||
|
||||
### 编译构建
|
||||
### 构建
|
||||
|
||||
```bash
|
||||
# 构建当前平台
|
||||
wails build
|
||||
|
||||
# 清理后构建(发布前推荐)
|
||||
wails build -clean
|
||||
wails build -clean # 发布前推荐
|
||||
```
|
||||
|
||||
构建产物位于 `build/bin`。
|
||||
产物位于 `build/bin`。
|
||||
|
||||
### 跨平台发布(GitHub Actions)
|
||||
### 只想直接用?
|
||||
|
||||
仓库内置发布流水线,推送 `v*` Tag 可自动构建并发布 Release。
|
||||
Release 更新说明会基于已合并 Pull Request 自动生成,并按 `.github/release.yaml` 分类。
|
||||
|
||||
支持目标:
|
||||
- macOS (AMD64 / ARM64)
|
||||
- Windows (AMD64)
|
||||
- Linux (AMD64,含 WebKitGTK 4.0 / 4.1 变体)
|
||||
前往 **[Releases](https://github.com/Syngnat/GoNavi/releases)** 下载
|
||||
(macOS AMD64/ARM64 · Windows AMD64 · Linux WebKitGTK 4.0/4.1)。
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
## 🌐 Web Server(实验中)
|
||||
|
||||
### macOS 提示“应用已损坏,无法打开”
|
||||
同一套 Go 后端 + React 前端提供浏览器访问(**不是**把 Wails 桌面窗口容器化)。
|
||||
|
||||
在未进行 Apple Notarization 时,Gatekeeper 可能拦截应用。
|
||||
```powershell
|
||||
go build .
|
||||
.\GoNavi-Wails.exe web-server --addr 127.0.0.1:34116
|
||||
```
|
||||
|
||||
- 首次访问进入 `/setup`(管理员密码;可选 Google Authenticator)
|
||||
- 桥接:`window.go.*` / `window.runtime.*` → HTTP / SSE
|
||||
- Session、恢复码、登录限流
|
||||
|
||||
#### Docker / Podman
|
||||
|
||||
```bash
|
||||
cp docker.web-server.env.example docker.web-server.env
|
||||
# 设置 GONAVI_HOST_DATA_ROOT 为绝对路径
|
||||
# 可选:GONAVI_WEB_PASSWORD(至少 6 位)
|
||||
docker compose --env-file docker.web-server.env -f docker-compose.web-server.yml up -d
|
||||
```
|
||||
|
||||
打开 `http://127.0.0.1:34116`。将活动数据目录挂载到 `/data`
|
||||
(`connections.json`、`daily_secrets.json`,可选 `drivers/`)。
|
||||
认证状态写入 `web_auth.json`。
|
||||
|
||||
> **不要把未加固的 Web 入口直接暴露到公网。** 生产环境请配合反向代理与 HTTPS。
|
||||
|
||||
健康检查:`GET /__gonavi/healthz`
|
||||
本地源码构建:叠加 `-f docker-compose.web-server.local.yml --build`。
|
||||
|
||||
修改 env 密码后需重建容器:
|
||||
|
||||
```bash
|
||||
docker compose --env-file docker.web-server.env -f docker-compose.web-server.yml up -d --force-recreate
|
||||
```
|
||||
|
||||
(`docker restart` **不会**重新加载 env 文件。)
|
||||
|
||||
---
|
||||
|
||||
## 🤖 MCP & Agents
|
||||
|
||||
把结构工具交给 Agent,而不是把密钥交出去:
|
||||
|
||||
```bash
|
||||
cp docker.mcp-server.env.example docker.mcp-server.env
|
||||
docker compose --env-file docker.mcp-server.env -f docker-compose.mcp-server.yml up -d
|
||||
```
|
||||
|
||||
| 形态 | 入口 |
|
||||
|---|---|
|
||||
| MCP 容器 | `docker-compose.mcp-server.yml` → `ghcr.io/syngnat/gonavi-mcp-server` |
|
||||
| Web UI 容器 | `docker-compose.web-server.yml` → `ghcr.io/syngnat/gonavi-web-server` |
|
||||
| Podman / Quadlet | [deploy/podman/gonavi-mcp-server](deploy/podman/gonavi-mcp-server) |
|
||||
| Kubernetes | [deploy/k8s/gonavi-mcp-server](deploy/k8s/gonavi-mcp-server) |
|
||||
| Helm | [deploy/helm/gonavi-mcp-server](deploy/helm/gonavi-mcp-server) |
|
||||
| 仅构建镜像 | `Dockerfile.build-env` → `ghcr.io/syngnat/gonavi-build-env` |
|
||||
|
||||
安全默认:远端 `schema-only` 不暴露 `execute_sql`;变更类 SQL 需显式 `allowMutating=true`。
|
||||
详见:[cmd/gonavi-mcp-server/README.md](cmd/gonavi-mcp-server/README.md)。
|
||||
|
||||
### 仅 Linux 构建环境
|
||||
|
||||
```bash
|
||||
docker build -f Dockerfile.build-env -t gonavi-build-env:local .
|
||||
docker run --rm -it -v "$PWD:/workspace" -w /workspace gonavi-build-env:local bash
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 发布流水线
|
||||
|
||||
推送 `v*` Tag → GitHub Actions 自动多架构构建。
|
||||
Release 说明按 `.github/release.yaml` 从已合并 PR 生成。
|
||||
|
||||
---
|
||||
|
||||
## 🛠 常见问题
|
||||
|
||||
<details>
|
||||
<summary><b>macOS:提示「应用已损坏,无法打开」</b></summary>
|
||||
|
||||
未做 Apple Notarization 时,Gatekeeper 可能拦截:
|
||||
|
||||
```bash
|
||||
sudo xattr -rd com.apple.quarantine /Applications/GoNavi.app
|
||||
```
|
||||
|
||||
### Linux 缺少 `libwebkit2gtk` / `libjavascriptcoregtk`
|
||||
也可在 Finder 中右键打开。建议先移到「应用程序」。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Linux:缺少 WebKitGTK</b></summary>
|
||||
|
||||
```bash
|
||||
# Debian 13 / Ubuntu 24.04+
|
||||
@@ -185,19 +348,32 @@ sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-0 libwebkit2gtk-4.0-37 libjavascriptcoregtk-4.0-18
|
||||
```
|
||||
|
||||
带 `-WebKit41` 后缀的产物更适配 Debian 13 / Ubuntu 24.04+。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Linux:中文显示为方框</b></summary>
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y fonts-noto-cjk fonts-wqy-microhei
|
||||
fc-cache -fv
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 贡献指南
|
||||
## 🤝 贡献
|
||||
|
||||
欢迎提交 Issue 与 Pull Request。
|
||||
|
||||
完整流程、分支模型与维护者同步规则请查看:
|
||||
欢迎 Issue 与 PR。请从 **`dev`** 拉分支,并向 **`dev`** 提 PR。
|
||||
|
||||
- [CONTRIBUTING.zh-CN.md](CONTRIBUTING.zh-CN.md)
|
||||
|
||||
外部贡献者应从 `dev` 拉出分支,并统一向 `dev` 发起 Pull Request。
|
||||
---
|
||||
|
||||
## Star History (Star 增长趋势)
|
||||
## ⭐ Star History
|
||||
|
||||
<a href="https://www.star-history.com/?repos=Syngnat%2FGoNavi&type=date&legend=top-left">
|
||||
<picture>
|
||||
@@ -207,6 +383,8 @@ sudo apt-get install -y libgtk-3-0 libwebkit2gtk-4.0-37 libjavascriptcoregtk-4.0
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
---
|
||||
|
||||
## 友情链接
|
||||
|
||||
- [linux.do](https://linux.do/)
|
||||
@@ -214,4 +392,8 @@ sudo apt-get install -y libgtk-3-0 libwebkit2gtk-4.0-37 libjavascriptcoregtk-4.0
|
||||
|
||||
## 开源协议
|
||||
|
||||
本项目采用 [Apache-2.0 协议](LICENSE)。
|
||||
[Apache-2.0](LICENSE)
|
||||
|
||||
<p align="center">
|
||||
<sub>为泡在 SQL、表结构与 Agent 循环里的人而造。</sub>
|
||||
</p>
|
||||
|
||||
BIN
assets/readme-logo.png
Normal file
|
After Width: | Height: | Size: 202 KiB |
BIN
assets/screenshots/01-home-workbench.png
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
assets/screenshots/04-ai-assistant.png
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
assets/screenshots/06-new-connection.png
Normal file
|
After Width: | Height: | Size: 180 KiB |
@@ -5,7 +5,7 @@ set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
DEFAULT_DRIVERS=(mariadb oceanbase doris sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss mongodb tdengine clickhouse)
|
||||
DEFAULT_DRIVERS=(mariadb oceanbase doris starrocks sphinx sqlserver sqlite duckdb dameng kingbase highgo vastbase opengauss gaussdb iris mongodb tdengine iotdb clickhouse elasticsearch trino)
|
||||
DEFAULT_PLATFORMS=(darwin/amd64 darwin/arm64 windows/amd64 windows/arm64 linux/amd64 linux/arm64)
|
||||
DUCKDB_WINDOWS_LIBRARY_VERSION="v1.4.4"
|
||||
DUCKDB_WINDOWS_LIBRARY_URL="https://github.com/duckdb/duckdb/releases/download/${DUCKDB_WINDOWS_LIBRARY_VERSION}/libduckdb-windows-amd64.zip"
|
||||
@@ -23,6 +23,8 @@ usage() {
|
||||
--out-dir <目录> 输出目录根路径,默认:dist/driver-agents
|
||||
--bundle-name <文件名> 驱动总包 zip 名称,默认:GoNavi-DriverAgents.zip
|
||||
--strict 任一驱动构建失败即中断(默认失败后继续,最后汇总)
|
||||
--upx 要求使用 UPX 压缩支持的平台产物(默认 auto:有 upx 则压缩)
|
||||
--no-upx 禁用 UPX 压缩
|
||||
-h, --help 显示帮助
|
||||
|
||||
示例:
|
||||
@@ -40,7 +42,9 @@ normalize_driver() {
|
||||
case "$name" in
|
||||
doris|diros) echo "doris" ;;
|
||||
open_gauss|open-gauss) echo "opengauss" ;;
|
||||
mariadb|oceanbase|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|opengauss|mongodb|tdengine|clickhouse)
|
||||
gaussdb|gauss_db|gauss-db) echo "gaussdb" ;;
|
||||
elasticsearch|elastic) echo "elasticsearch" ;;
|
||||
mariadb|oceanbase|starrocks|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|opengauss|gaussdb|iris|mongodb|tdengine|iotdb|clickhouse|trino)
|
||||
echo "$name"
|
||||
;;
|
||||
*)
|
||||
@@ -142,6 +146,40 @@ PY
|
||||
fi
|
||||
}
|
||||
|
||||
zip_duckdb_windows_package() {
|
||||
local bundle_stage_dir="$1"
|
||||
local zip_path="$2"
|
||||
|
||||
rm -f "$zip_path"
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
BUNDLE_STAGE_DIR="$bundle_stage_dir" BUNDLE_ZIP_PATH="$zip_path" python3 - <<'PY'
|
||||
import os
|
||||
import zipfile
|
||||
|
||||
stage_dir = os.environ["BUNDLE_STAGE_DIR"]
|
||||
zip_path = os.environ["BUNDLE_ZIP_PATH"]
|
||||
entries = [
|
||||
("Windows/duckdb-driver-agent-windows-amd64.exe", os.path.join(stage_dir, "Windows", "duckdb-driver-agent-windows-amd64.exe")),
|
||||
("Windows/duckdb.dll", os.path.join(stage_dir, "Windows", "duckdb.dll")),
|
||||
]
|
||||
|
||||
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
||||
for arcname, src in entries:
|
||||
if not os.path.isfile(src):
|
||||
raise FileNotFoundError(src)
|
||||
zf.write(src, arcname)
|
||||
PY
|
||||
elif command -v zip >/dev/null 2>&1; then
|
||||
(
|
||||
cd "$bundle_stage_dir"
|
||||
zip -qry "$zip_path" "Windows/duckdb-driver-agent-windows-amd64.exe" "Windows/duckdb.dll"
|
||||
)
|
||||
else
|
||||
echo "❌ 未找到 python3 或 zip,无法生成 DuckDB Windows 专属驱动包。"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
prepare_duckdb_windows_library() {
|
||||
local cache_root="$1"
|
||||
local lib_dir="$cache_root/duckdb-windows-${DUCKDB_WINDOWS_LIBRARY_VERSION}"
|
||||
@@ -199,7 +237,9 @@ driver_csv=""
|
||||
target_platform=""
|
||||
out_root="dist/driver-agents"
|
||||
bundle_name="GoNavi-DriverAgents.zip"
|
||||
duckdb_windows_zip_name="duckdb-driver.zip"
|
||||
strict_mode="false"
|
||||
upx_mode="${GONAVI_DRIVER_AGENT_UPX:-auto}"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
@@ -223,6 +263,14 @@ while [[ $# -gt 0 ]]; do
|
||||
strict_mode="true"
|
||||
shift
|
||||
;;
|
||||
--upx)
|
||||
upx_mode="required"
|
||||
shift
|
||||
;;
|
||||
--no-upx)
|
||||
upx_mode="off"
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
@@ -372,10 +420,12 @@ for platform in "${platforms[@]}"; do
|
||||
continue
|
||||
fi
|
||||
|
||||
GONAVI_DRIVER_AGENT_UPX="$upx_mode" "$SCRIPT_DIR/tools/compress-driver-artifact.sh" "$output_path" "$platform" "$platform_dir/$asset_name"
|
||||
cp "$output_path" "$bundle_platform_dir/$asset_name"
|
||||
if [[ -n "$duckdb_lib_dir" ]]; then
|
||||
cp "$duckdb_lib_dir/$DUCKDB_WINDOWS_SUPPORT_DLL" "$output_dir_abs/$DUCKDB_WINDOWS_SUPPORT_DLL"
|
||||
cp "$duckdb_lib_dir/$DUCKDB_WINDOWS_SUPPORT_DLL" "$bundle_platform_dir/$DUCKDB_WINDOWS_SUPPORT_DLL"
|
||||
GONAVI_DRIVER_AGENT_UPX="$upx_mode" "$SCRIPT_DIR/tools/compress-driver-artifact.sh" "$output_dir_abs/$DUCKDB_WINDOWS_SUPPORT_DLL" "$platform" "$platform_dir/$DUCKDB_WINDOWS_SUPPORT_DLL"
|
||||
cp "$output_dir_abs/$DUCKDB_WINDOWS_SUPPORT_DLL" "$bundle_platform_dir/$DUCKDB_WINDOWS_SUPPORT_DLL"
|
||||
built_assets+=("$platform_dir/$DUCKDB_WINDOWS_SUPPORT_DLL")
|
||||
fi
|
||||
built_assets+=("$platform_dir/$asset_name")
|
||||
@@ -389,6 +439,14 @@ fi
|
||||
|
||||
zip_bundle "$bundle_zip_path" "$bundle_stage_dir"
|
||||
|
||||
duckdb_asset_path="$out_root_abs/windows-amd64/duckdb-driver-agent-windows-amd64.exe"
|
||||
duckdb_dll_path="$out_root_abs/windows-amd64/$DUCKDB_WINDOWS_SUPPORT_DLL"
|
||||
if [[ -f "$duckdb_asset_path" && -f "$duckdb_dll_path" ]]; then
|
||||
duckdb_zip_path="$out_root_abs/windows-amd64/$duckdb_windows_zip_name"
|
||||
zip_duckdb_windows_package "$bundle_stage_dir" "$duckdb_zip_path"
|
||||
built_assets+=("Windows/$duckdb_windows_zip_name")
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ 构建完成"
|
||||
echo " 单文件输出根目录:$out_root_abs"
|
||||
|
||||
@@ -355,6 +355,9 @@ if command -v sha256sum &> /dev/null; then
|
||||
: > SHA256SUMS
|
||||
for f in *; do
|
||||
[ -f "$f" ] || continue
|
||||
case "$f" in
|
||||
SHA256SUMS|latest.json|latest-dev.json) continue ;;
|
||||
esac
|
||||
sha256sum "$f" >> SHA256SUMS
|
||||
done
|
||||
cd ..
|
||||
@@ -363,6 +366,9 @@ elif command -v shasum &> /dev/null; then
|
||||
: > SHA256SUMS
|
||||
for f in *; do
|
||||
[ -f "$f" ] || continue
|
||||
case "$f" in
|
||||
SHA256SUMS|latest.json|latest-dev.json) continue ;;
|
||||
esac
|
||||
shasum -a 256 "$f" >> SHA256SUMS
|
||||
done
|
||||
cd ..
|
||||
@@ -370,6 +376,35 @@ else
|
||||
echo -e "${YELLOW} ⚠️ 未找到 sha256sum/shasum,跳过校验文件生成。${NC}"
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}📄 生成静态更新清单...${NC}"
|
||||
if command -v python3 &> /dev/null; then
|
||||
# 正式版:latest.json;dev/test 版本:latest-dev.json(对应客户端 dev 通道)
|
||||
case "$VERSION" in
|
||||
dev-*|*-dev*|*dev*|*test*)
|
||||
TAG_FOR_MANIFEST="dev-latest"
|
||||
CHANNEL_FOR_MANIFEST="dev"
|
||||
OUT_MANIFEST="$DIST_DIR/latest-dev.json"
|
||||
;;
|
||||
*)
|
||||
TAG_FOR_MANIFEST="v${VERSION#v}"
|
||||
CHANNEL_FOR_MANIFEST="latest"
|
||||
OUT_MANIFEST="$DIST_DIR/latest.json"
|
||||
;;
|
||||
esac
|
||||
if python3 tools/generate-update-latest-manifest.py \
|
||||
--assets-dir "$DIST_DIR" \
|
||||
--version "$VERSION" \
|
||||
--tag "$TAG_FOR_MANIFEST" \
|
||||
--channel "$CHANNEL_FOR_MANIFEST" \
|
||||
--output "$OUT_MANIFEST"; then
|
||||
echo -e "${GREEN} ✅ 已生成 $OUT_MANIFEST (channel=${CHANNEL_FOR_MANIFEST})${NC}"
|
||||
else
|
||||
echo -e "${YELLOW} ⚠️ 生成更新清单失败(不影响本地产物,正式/dev 发版由 CI 生成)${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW} ⚠️ 未找到 python3,跳过更新清单${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [ "${#BUILD_FAILURES[@]}" -gt 0 ]; then
|
||||
echo -e "${RED}❌ 构建未完全成功,失败平台:${BUILD_FAILURES[*]}${NC}"
|
||||
|
||||
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 406 KiB |
BIN
build/windows/appicon-transparent.png
Normal file
|
After Width: | Height: | Size: 524 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 85 KiB |
100
build/windows/installer.wxs
Normal file
@@ -0,0 +1,100 @@
|
||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
|
||||
xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
|
||||
<Package Name="$(var.ProductName)"
|
||||
Manufacturer="Syngnat"
|
||||
Language="1033"
|
||||
Version="$(var.ProductVersion)"
|
||||
ProductCode="*"
|
||||
UpgradeCode="$(var.UpgradeCode)"
|
||||
Scope="perMachine"
|
||||
InstallerVersion="500"
|
||||
Compressed="yes">
|
||||
<MajorUpgrade AllowDowngrades="yes"
|
||||
Schedule="afterInstallInitialize" />
|
||||
<MediaTemplate EmbedCab="yes" CompressionLevel="high" />
|
||||
|
||||
<Icon Id="GoNaviIcon" SourceFile="$(var.IconPath)" />
|
||||
<Property Id="ARPPRODUCTICON" Value="GoNaviIcon" />
|
||||
<Property Id="ARPURLINFOABOUT" Value="https://github.com/Syngnat/GoNavi" />
|
||||
<Property Id="INSTALLDESKTOPSHORTCUT" Value="1" Secure="yes" />
|
||||
<SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLFOLDER]" After="CostFinalize" />
|
||||
<WixVariable Id="WixUILicenseRtf" Value="$(var.LicenseRtf)" />
|
||||
|
||||
<Feature Id="MainFeature" Title="$(var.ProductName)" Level="1">
|
||||
<ComponentRef Id="MainExecutable" />
|
||||
<ComponentRef Id="DesktopShortcutComponent" />
|
||||
</Feature>
|
||||
|
||||
<Component Id="MainExecutable"
|
||||
Directory="INSTALLFOLDER"
|
||||
Guid="$(var.ComponentGuid)"
|
||||
Bitness="always64">
|
||||
<File Id="GoNaviExe"
|
||||
Source="$(var.SourceExe)"
|
||||
Name="GoNavi.exe"
|
||||
KeyPath="yes">
|
||||
<!-- Keep shortcut icons bound to the stable executable path. Referencing the MSI Icon table
|
||||
can leave Windows shell caches pointing at the removed product cache after a major upgrade. -->
|
||||
<Shortcut Id="StartMenuShortcut"
|
||||
Directory="ProgramMenuFolder"
|
||||
Name="$(var.ProductName)"
|
||||
WorkingDirectory="INSTALLFOLDER"
|
||||
Advertise="no" />
|
||||
</File>
|
||||
<File Id="GoNaviMsiInstallMarker"
|
||||
Source="$(var.InstallMarker)"
|
||||
Name=".gonavi-msi-install" />
|
||||
<File Id="GoNaviLicense"
|
||||
Source="$(var.LicenseFile)"
|
||||
Name="LICENSE.txt" />
|
||||
<File Id="GoNaviNotice"
|
||||
Source="$(var.NoticeFile)"
|
||||
Name="NOTICE.txt" />
|
||||
<RegistryValue Root="HKLM"
|
||||
Key="Software\Syngnat\$(var.RegistryKeyName)"
|
||||
Name="InstallType"
|
||||
Value="MSI"
|
||||
Type="string"
|
||||
KeyPath="no" />
|
||||
<RegistryValue Root="HKLM"
|
||||
Key="Software\Syngnat\$(var.RegistryKeyName)"
|
||||
Name="InstallPath"
|
||||
Value="[INSTALLFOLDER]GoNavi.exe"
|
||||
Type="string"
|
||||
KeyPath="no" />
|
||||
</Component>
|
||||
|
||||
<!-- Online updates pass the user's pre-update desktop shortcut state. Keeping this resource
|
||||
in its own component lets a major upgrade remove an unwanted old shortcut without
|
||||
recreating it, while first-time and manual installs retain the existing default. -->
|
||||
<Component Id="DesktopShortcutComponent"
|
||||
Directory="DesktopFolder"
|
||||
Guid="$(var.DesktopShortcutComponentGuid)"
|
||||
Bitness="always64"
|
||||
Condition="INSTALLDESKTOPSHORTCUT = 1">
|
||||
<Shortcut Id="DesktopShortcut"
|
||||
Name="$(var.ProductName)"
|
||||
Target="[#GoNaviExe]"
|
||||
WorkingDirectory="INSTALLFOLDER"
|
||||
Advertise="no" />
|
||||
<RegistryValue Root="HKLM"
|
||||
Key="Software\Syngnat\$(var.RegistryKeyName)"
|
||||
Name="DesktopShortcutInstalled"
|
||||
Value="1"
|
||||
Type="integer"
|
||||
KeyPath="yes" />
|
||||
</Component>
|
||||
|
||||
<ui:WixUI Id="WixUI_InstallDir"
|
||||
InstallDirectory="INSTALLFOLDER"
|
||||
ExtendedPathValidation="yes" />
|
||||
</Package>
|
||||
|
||||
<Fragment>
|
||||
<StandardDirectory Id="ProgramFiles6432Folder">
|
||||
<Directory Id="INSTALLFOLDER" Name="$(var.InstallFolderName)" />
|
||||
</StandardDirectory>
|
||||
<StandardDirectory Id="ProgramMenuFolder" />
|
||||
<StandardDirectory Id="DesktopFolder" />
|
||||
</Fragment>
|
||||
</Wix>
|
||||
381
cmd/gonavi-mcp-server/README.md
Normal file
@@ -0,0 +1,381 @@
|
||||
# GoNavi MCP Server
|
||||
|
||||
`gonavi-mcp-server` 会把 GoNavi 已保存连接背后的数据库能力通过 MCP 暴露给外部客户端。本机客户端默认使用 `stdio`;云端 Agent 可使用显式开启的 Streamable HTTP 模式。
|
||||
|
||||
## 当前提供的 tools
|
||||
|
||||
- `get_connections`
|
||||
- 返回 GoNavi 已保存连接的 `id/name/type/target/defaultDatabase` 等摘要信息
|
||||
- `get_databases`
|
||||
- 入参:`connectionId`
|
||||
- `get_tables`
|
||||
- 入参:`connectionId`、可选 `dbName`
|
||||
- 返回表列表,并在 `views` 字段附带视图列表,兼容旧客户端只调用 `get_tables` 的场景
|
||||
- `get_views`
|
||||
- 入参:`connectionId`、可选 `dbName`
|
||||
- 返回视图列表
|
||||
- `get_objects`
|
||||
- 入参:`connectionId`、可选 `dbName`、可选 `objectTypes`
|
||||
- 返回表、视图、触发器、函数、过程、序列、包、事件,以及消息队列类 `topic/queue/exchange` 等对象清单
|
||||
- `get_columns`
|
||||
- 入参:`connectionId`、可选 `dbName`、`tableName`
|
||||
- `get_table_ddl`
|
||||
- 入参:`connectionId`、可选 `dbName`、`tableName`
|
||||
- `execute_sql`
|
||||
- 入参:`connectionId`、可选 `dbName`、`sql`
|
||||
- 默认只允许只读 SQL
|
||||
- 如果 SQL 包含 DDL/DML,必须显式传 `allowMutating=true`
|
||||
- `maxRowsPerResult` 用来限制单个结果集返回的行数,默认 `200`
|
||||
|
||||
远程 Agent 只需要结构元数据时,启动 HTTP 模式请加 `--schema-only`。该模式不注册 `execute_sql`,只保留连接摘要、对象清单、表/视图、字段、索引、外键、触发器和 DDL 工具。
|
||||
|
||||
## 运行方式
|
||||
|
||||
开发态直接运行:
|
||||
|
||||
```powershell
|
||||
go run ./cmd/gonavi-mcp-server
|
||||
```
|
||||
|
||||
显式运行本机 `stdio`:
|
||||
|
||||
```powershell
|
||||
go run ./cmd/gonavi-mcp-server stdio
|
||||
```
|
||||
|
||||
也可以先编译:
|
||||
|
||||
```powershell
|
||||
go build -o .\bin\gonavi-mcp-server.exe .\cmd\gonavi-mcp-server
|
||||
```
|
||||
|
||||
## Docker / Podman / Compose
|
||||
|
||||
当前容器化支持仅覆盖 `gonavi-mcp-server`,不包含 Wails 桌面 GUI。
|
||||
|
||||
当前支持矩阵:
|
||||
|
||||
- Docker Desktop / Linux 服务器 / NAS:直接使用 Compose 或 `docker run`
|
||||
- Podman / Quadlet:使用 `deploy/podman/gonavi-mcp-server`
|
||||
- Kubernetes:使用 `deploy/k8s/gonavi-mcp-server`
|
||||
- Helm:使用 `deploy/helm/gonavi-mcp-server`
|
||||
- 仅构建环境:使用仓库根目录 `Dockerfile.build-env`
|
||||
- 桌面 GUI 浏览器访问版:当前不提供,此仓库主应用仍是 Wails 桌面程序,不是现成的 Web 服务
|
||||
|
||||
仓库根目录已提供以下文件:
|
||||
|
||||
- `Dockerfile.mcp-server`
|
||||
- `docker-compose.mcp-server.yml`
|
||||
- `docker.mcp-server.env.example`
|
||||
- `deploy/podman/gonavi-mcp-server/*`
|
||||
|
||||
推荐流程:
|
||||
|
||||
```bash
|
||||
cp docker.mcp-server.env.example docker.mcp-server.env
|
||||
docker compose --env-file docker.mcp-server.env -f docker-compose.mcp-server.yml up -d
|
||||
```
|
||||
|
||||
默认 Compose 会拉取 GHCR 预构建镜像。如果你要基于当前工作区源码本地构建,再叠加:
|
||||
|
||||
```bash
|
||||
docker compose --env-file docker.mcp-server.env \
|
||||
-f docker-compose.mcp-server.yml \
|
||||
-f docker-compose.mcp-server.local.yml \
|
||||
up -d --build
|
||||
```
|
||||
|
||||
其中 `GONAVI_HOST_DATA_ROOT` 必须指向 GoNavi 当前活动数据目录。该目录内至少应包含:
|
||||
|
||||
- `connections.json`
|
||||
- `daily_secrets.json`
|
||||
- `drivers/`(如果目标连接依赖可选 driver agent)
|
||||
|
||||
容器内默认会设置:
|
||||
|
||||
- `GONAVI_DATA_ROOT=/data`
|
||||
- `GONAVI_MCP_HTTP_ADDR=0.0.0.0:8765`
|
||||
- `GONAVI_MCP_HTTP_PATH=/mcp`
|
||||
|
||||
`GONAVI_DATA_ROOT` 会覆盖默认活动数据目录解析逻辑,避免宿主机路径与容器内路径不一致时依赖 `storage_root.json` 的绝对路径。
|
||||
|
||||
如果你只想手动构建镜像:
|
||||
|
||||
```bash
|
||||
docker build -f Dockerfile.mcp-server -t gonavi-mcp-server:local .
|
||||
docker run --rm -p 8765:8765 \
|
||||
-e GONAVI_MCP_HTTP_TOKEN=replace-with-a-random-token \
|
||||
-e GONAVI_MCP_SCHEMA_ONLY=true \
|
||||
-e GONAVI_DATA_ROOT=/data \
|
||||
-v /absolute/path/to/gonavi-data:/data \
|
||||
gonavi-mcp-server:local http
|
||||
```
|
||||
|
||||
如果你直接使用已发布镜像:
|
||||
|
||||
```bash
|
||||
docker run --rm -p 8765:8765 \
|
||||
-e GONAVI_MCP_HTTP_TOKEN=replace-with-a-random-token \
|
||||
-e GONAVI_MCP_SCHEMA_ONLY=true \
|
||||
-e GONAVI_DATA_ROOT=/data \
|
||||
-v /absolute/path/to/gonavi-data:/data \
|
||||
ghcr.io/syngnat/gonavi-mcp-server:latest http
|
||||
```
|
||||
|
||||
### Podman
|
||||
|
||||
仓库内还提供了 Podman 原生部署样例:
|
||||
|
||||
- `deploy/podman/gonavi-mcp-server/gonavi-mcp-server.env.example`
|
||||
- `deploy/podman/gonavi-mcp-server/gonavi-mcp-server.container`
|
||||
- `deploy/podman/gonavi-mcp-server/README.md`
|
||||
|
||||
直接运行已发布镜像:
|
||||
|
||||
```bash
|
||||
cp deploy/podman/gonavi-mcp-server/gonavi-mcp-server.env.example ./gonavi-mcp-server.env
|
||||
podman run -d --name gonavi-mcp-server --replace \
|
||||
-p 8765:8765 \
|
||||
--env-file ./gonavi-mcp-server.env \
|
||||
-v /absolute/path/to/gonavi-data:/data:Z \
|
||||
ghcr.io/syngnat/gonavi-mcp-server:latest http
|
||||
```
|
||||
|
||||
如果你要基于当前源码本地构建:
|
||||
|
||||
```bash
|
||||
podman build -f Dockerfile.mcp-server -t localhost/gonavi-mcp-server:local .
|
||||
podman run -d --name gonavi-mcp-server --replace \
|
||||
-p 8765:8765 \
|
||||
--env-file ./gonavi-mcp-server.env \
|
||||
-v /absolute/path/to/gonavi-data:/data:Z \
|
||||
localhost/gonavi-mcp-server:local http
|
||||
```
|
||||
|
||||
其中:
|
||||
|
||||
- `gonavi-mcp-server.env` 用 `deploy/podman/gonavi-mcp-server/gonavi-mcp-server.env.example` 初始化
|
||||
- `:Z` 适用于开启 SELinux 的宿主机;未启用 SELinux 可去掉
|
||||
- 更适合长期运行的方式见 [deploy/podman/gonavi-mcp-server/README.md](../../deploy/podman/gonavi-mcp-server/README.md) 中的 Quadlet 示例
|
||||
|
||||
`podman compose` 本身依赖外部 compose provider,所以仓库对 Podman 的主支持路径是 `podman run` 与 Quadlet,而不是假设所有环境都能直接复用 Compose。
|
||||
|
||||
## Kubernetes
|
||||
|
||||
仓库内置了最小 K8s 示例:
|
||||
|
||||
- `deploy/k8s/gonavi-mcp-server/kustomization.yaml`
|
||||
- `deploy/k8s/gonavi-mcp-server/base/deployment.yaml`
|
||||
- `deploy/k8s/gonavi-mcp-server/base/service.yaml`
|
||||
- `deploy/k8s/gonavi-mcp-server/README.md`
|
||||
- `deploy/k8s/gonavi-mcp-server/overlays/*`
|
||||
|
||||
推荐先从现有 GoNavi 数据目录生成 Secret:
|
||||
|
||||
```bash
|
||||
kubectl create namespace gonavi
|
||||
kubectl -n gonavi create secret generic gonavi-mcp-server-data \
|
||||
--from-file=connections.json=/absolute/path/to/gonavi-data/connections.json \
|
||||
--from-file=daily_secrets.json=/absolute/path/to/gonavi-data/daily_secrets.json \
|
||||
--from-literal=GONAVI_MCP_HTTP_TOKEN=replace-with-a-random-token
|
||||
kubectl apply -k deploy/k8s/gonavi-mcp-server
|
||||
```
|
||||
|
||||
如果需要 NAS hostPath、可选 driver agent PVC、Ingress,或两者组合,可直接使用 `overlays/nas-hostpath`、`overlays/drivers-pvc`、`overlays/ingress`、`overlays/ingress-with-drivers-pvc`。
|
||||
|
||||
更完整的说明见 [deploy/k8s/gonavi-mcp-server/README.md](../../deploy/k8s/gonavi-mcp-server/README.md)。
|
||||
|
||||
## Helm
|
||||
|
||||
如果你希望把镜像、Secret、Ingress、hostPath / PVC 挂载做成参数化部署,而不是维护多份 Kustomize overlay,可直接使用:
|
||||
|
||||
- `deploy/helm/gonavi-mcp-server`
|
||||
|
||||
快速安装:
|
||||
|
||||
```bash
|
||||
helm upgrade --install gonavi-mcp-server deploy/helm/gonavi-mcp-server -n gonavi --create-namespace
|
||||
```
|
||||
|
||||
Chart 详细说明见 [deploy/helm/gonavi-mcp-server/README.md](../../deploy/helm/gonavi-mcp-server/README.md)。
|
||||
|
||||
## Docker / Podman Build Environment
|
||||
|
||||
如果你的目标不是运行 MCP,而是给 Linux 服务器 / NAS / CI 准备一套可重复的 Wails 构建环境,可直接使用仓库根目录的 `Dockerfile.build-env`:
|
||||
|
||||
```bash
|
||||
docker build -f Dockerfile.build-env -t gonavi-build-env:local .
|
||||
docker run --rm -it -v "$PWD:/workspace" -w /workspace gonavi-build-env:local bash
|
||||
```
|
||||
|
||||
如果你使用 Podman,也可以直接执行:
|
||||
|
||||
```bash
|
||||
podman build -f Dockerfile.build-env -t localhost/gonavi-build-env:local .
|
||||
podman run --rm -it -v "$PWD:/workspace" -w /workspace localhost/gonavi-build-env:local bash
|
||||
```
|
||||
|
||||
镜像内已预装 Go、Node、Wails CLI、GTK3 与 WebKitGTK 开发依赖,适合执行:
|
||||
|
||||
```bash
|
||||
wails build
|
||||
```
|
||||
|
||||
这个镜像默认安装 WebKitGTK 4.0 构建依赖,适合作为通用 Linux / NAS 构建环境。镜像基座支持多架构,`amd64` / `arm64` 会跟随容器平台。
|
||||
|
||||
预构建镜像会发布到 GHCR:
|
||||
|
||||
- `ghcr.io/syngnat/gonavi-mcp-server:latest`
|
||||
- `ghcr.io/syngnat/gonavi-build-env:latest`
|
||||
|
||||
它只负责构建 Linux 产物,不会把 Wails 主程序变成浏览器版服务。
|
||||
|
||||
远程 Agent 使用 Streamable HTTP 时必须设置 bearer token:
|
||||
|
||||
```powershell
|
||||
$env:GONAVI_MCP_HTTP_TOKEN = "<随机token>"
|
||||
go run ./cmd/gonavi-mcp-server http --addr 127.0.0.1:8765 --path /mcp --schema-only
|
||||
```
|
||||
|
||||
安装包主程序也支持同样模式:
|
||||
|
||||
```powershell
|
||||
& "C:\Program Files\GoNavi\GoNavi.exe" mcp-server http --addr 127.0.0.1:8765 --path /mcp --token "<随机token>" --schema-only
|
||||
```
|
||||
|
||||
默认建议只监听 `127.0.0.1`,再通过 SSH 隧道、反向代理或内网网关暴露给云端 Agent。不要在没有 TLS、防火墙和鉴权的情况下直接监听公网地址。
|
||||
|
||||
无图形界面或需要把配置交给云端 Agent 时,可直接生成 OpenClaw / Hermans 等远程 MCP 配置:
|
||||
|
||||
```powershell
|
||||
& "C:\Program Files\GoNavi\GoNavi.exe" mcp-server remote-config --client openclaw --url "https://<你的域名或隧道地址>/mcp" --token "<随机token>" --schema-only
|
||||
```
|
||||
|
||||
独立 server 开发态也支持同样能力:
|
||||
|
||||
```powershell
|
||||
go run ./cmd/gonavi-mcp-server remote-config --client hermans --url "https://<你的域名或隧道地址>/mcp" --token "<随机token>" --schema-only
|
||||
```
|
||||
|
||||
## Claude Code / Codex / OpenClaw / Hermans
|
||||
|
||||
正式安装包场景,推荐直接在 GoNavi 里使用“AI 设置 -> MCP 服务 -> 安装到 Claude Code / 安装到 Codex”。
|
||||
|
||||
它会自动把当前安装的 `GoNavi.exe` 写入 Claude Code 的用户级 `~/.claude.json`,命令形态类似:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"gonavi": {
|
||||
"type": "stdio",
|
||||
"command": "C:\\Program Files\\GoNavi\\GoNavi.exe",
|
||||
"args": ["mcp-server"],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这样用户不需要自己找本机 `gonavi-mcp-server.exe` 路径,安装包本体就能直接作为 MCP 入口。
|
||||
|
||||
Codex 当前使用 `~/.codex/config.toml`,GoNavi 会写入类似下面这段:
|
||||
|
||||
```toml
|
||||
[mcp_servers.gonavi]
|
||||
command = 'C:\Program Files\GoNavi\GoNavi.exe'
|
||||
args = ['mcp-server']
|
||||
startup_timeout_sec = 60
|
||||
```
|
||||
|
||||
仓库开发态如果要在本机 `Claude Code CLI` 里稳定使用这个 MCP,仍然推荐走仓库内包装脚本:
|
||||
|
||||
```powershell
|
||||
.\tools\claude-gonavi-mcp.ps1 -p "必须调用 gonavi MCP 的 get_connections 工具"
|
||||
```
|
||||
|
||||
或者:
|
||||
|
||||
```cmd
|
||||
tools\claude-gonavi-mcp.cmd -p "必须调用 gonavi MCP 的 get_connections 工具"
|
||||
```
|
||||
|
||||
这个脚本会先构建 `bin\gonavi-mcp-server.exe`,再通过 `--mcp-config` 和 `--strict-mcp-config` 把 GoNavi MCP 单独注入当前 Claude 会话,避免默认混合 MCP 加载时序导致的首轮工具未挂载问题。
|
||||
|
||||
OpenClaw、Hermans 这类部署在云端或远端 Linux 的 Agent,不能直接使用 Windows 本机的 `stdio` 命令。GoNavi 的连接信息和数据库密码仍应留在 Windows 本机,由 GoNavi MCP 读取保存连接和系统凭据;远端 Agent 只拿到 MCP tools 和 `connectionId`。
|
||||
|
||||
推荐接入形态:
|
||||
|
||||
1. Windows 本机运行 GoNavi,并保持能访问已保存的数据库连接。
|
||||
2. 在 Windows 本机启动 `GoNavi.exe mcp-server http --addr 127.0.0.1:8765 --path /mcp --token <随机token> --schema-only`。
|
||||
3. 通过 SSH 隧道、反向代理或内网网关把 `http://127.0.0.1:8765/mcp` 暴露为云端 Agent 可访问的 HTTPS 地址。
|
||||
4. 在 OpenClaw / Hermans 中添加远程 MCP Server,transport 选择 Streamable HTTP,URL 指向 `/mcp` 地址,并设置请求头 `Authorization: Bearer <随机token>`。
|
||||
5. 先调用 `get_connections` 获取 `connectionId`,再调用 `get_databases`、`get_objects`、`get_tables`、`get_views`、`get_columns`、`get_table_ddl` 等工具读取结构。
|
||||
|
||||
如果目标 Agent 支持 `mcpServers` JSON,可按下面的通用片段配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"gonavi": {
|
||||
"type": "streamable-http",
|
||||
"url": "https://<你的域名或隧道地址>/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer <随机token>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
不要把数据库 `host/user/password` 写入云端 Agent 的配置文件。默认 `--schema-only` 不暴露 `execute_sql`;如果你明确需要远程执行 SQL,可以去掉该参数,此时 `execute_sql` 仍受 GoNavi AI 安全设置控制,写操作必须显式传 `allowMutating=true`。
|
||||
|
||||
## MCP 客户端配置示例
|
||||
|
||||
开发态:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"gonavi": {
|
||||
"command": "go",
|
||||
"args": ["run", "./cmd/gonavi-mcp-server"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Windows 独立 server 编译产物(开发态):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"gonavi": {
|
||||
"command": "D:\\Work\\CodeRepos\\GoNavi\\bin\\gonavi-mcp-server.exe",
|
||||
"args": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Windows 已安装 GoNavi(推荐给最终用户):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"gonavi": {
|
||||
"type": "stdio",
|
||||
"command": "C:\\Program Files\\GoNavi\\GoNavi.exe",
|
||||
"args": ["mcp-server"],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 使用说明
|
||||
|
||||
- 先调用 `get_connections`,拿到 `connectionId`
|
||||
- 之后所有数据库工具都只传 `connectionId`,由 GoNavi 服务端内部解析保存连接和密钥
|
||||
- 如果 `dbName` 为空,会优先使用该保存连接里的默认数据库
|
||||
- Server 会读取 GoNavi 当前活动数据目录里的连接配置,并通过系统 keyring/凭据管理器解析密文
|
||||
- 如果本机凭据存储不可用,依赖密钥的连接会返回对应错误
|
||||
42
cmd/gonavi-mcp-server/main.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"GoNavi-Wails/internal/mcpserver"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
err := run(ctx, os.Args[1:])
|
||||
if err != nil {
|
||||
log.Printf("GoNavi MCP Server 退出: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func run(ctx context.Context, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return mcpserver.RunAppStdioServer(ctx)
|
||||
}
|
||||
|
||||
mode := strings.ToLower(strings.TrimSpace(args[0]))
|
||||
switch mode {
|
||||
case "stdio", "--stdio":
|
||||
return mcpserver.RunAppStdioServer(ctx)
|
||||
case "http", "--http", "streamable-http", "--streamable-http":
|
||||
options, err := mcpserver.ParseHTTPServerOptions(args[1:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("GoNavi MCP Streamable HTTP Server 启动:addr=%s path=%s schemaOnly=%v", options.Addr, options.Path, options.SchemaOnly)
|
||||
return mcpserver.RunAppStreamableHTTPServer(ctx, options)
|
||||
case "remote-config", "--remote-config":
|
||||
return mcpserver.WriteRemoteMCPClientConfig(os.Stdout, args[1:])
|
||||
default:
|
||||
return fmt.Errorf("未知 MCP server 模式: %s(支持 stdio/http/remote-config)", args[0])
|
||||
}
|
||||
}
|
||||
27
cmd/mingw-import-lib/main.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"GoNavi-Wails/internal/buildutil"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
dllPath string
|
||||
dlltoolPath string
|
||||
outputLib string
|
||||
)
|
||||
|
||||
flag.StringVar(&dllPath, "dll", "", "Path to the source DLL")
|
||||
flag.StringVar(&dlltoolPath, "dlltool", "", "Optional path to dlltool executable")
|
||||
flag.StringVar(&outputLib, "output-lib", "", "Output import library path")
|
||||
flag.Parse()
|
||||
|
||||
if err := buildutil.GenerateWindowsImportLibraryFromDLL(dllPath, dlltoolPath, outputLib); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "generate mingw import library failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,11 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
@@ -17,6 +21,7 @@ import (
|
||||
type agentRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
Method string `json:"method"`
|
||||
SessionID string `json:"sessionId,omitempty"`
|
||||
Config *connection.ConnectionConfig `json:"config,omitempty"`
|
||||
Query string `json:"query,omitempty"`
|
||||
TimeoutMs int64 `json:"timeoutMs,omitempty"`
|
||||
@@ -31,6 +36,8 @@ type agentResponse struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Fields []string `json:"fields,omitempty"`
|
||||
Messages []string `json:"messages,omitempty"`
|
||||
ChunkType string `json:"chunkType,omitempty"`
|
||||
RowsAffected int64 `json:"rowsAffected,omitempty"`
|
||||
}
|
||||
|
||||
@@ -39,7 +46,11 @@ const (
|
||||
agentMethodClose = "close"
|
||||
agentMethodMetadata = "metadata"
|
||||
agentMethodPing = "ping"
|
||||
agentMethodOpenSession = "openSession"
|
||||
agentMethodCloseSession = "closeSession"
|
||||
agentMethodQuery = "query"
|
||||
agentMethodQueryMulti = "queryMulti"
|
||||
agentMethodStreamQuery = "streamQuery"
|
||||
agentMethodExec = "exec"
|
||||
agentMethodGetDatabases = "getDatabases"
|
||||
agentMethodGetTables = "getTables"
|
||||
@@ -54,23 +65,67 @@ const (
|
||||
|
||||
const legacyClickHouseDefaultTimeout = 2 * time.Hour
|
||||
|
||||
var (
|
||||
agentDriverType string
|
||||
agentDatabaseFactory func() db.Database
|
||||
const (
|
||||
agentChunkColumns = "columns"
|
||||
agentChunkRows = "rows"
|
||||
agentChunkDone = "done"
|
||||
// agentStreamBatchSize 控制 driver-agent 向主进程发送 row chunk 的批次大小。
|
||||
// 调小到 64:单批 JSON 编码 + 主进程解码的瞬时内存峰值降为原来的 1/4,
|
||||
// 代价是 IPC 次数变为 4 倍,但每批仅一次 stdin/stdout 行读写,整体影响可忽略。
|
||||
// 重要:减小批次不能根除内存峰值,仍需配合 SetGCPercent + 周期 GC(见 main)。
|
||||
agentStreamBatchSize = 64
|
||||
agentMemoryTrimRowsThreshold = 100000
|
||||
agentMemoryTrimMinInterval = 3 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
agentDriverType string
|
||||
agentDatabaseFactory func() db.Database
|
||||
agentMemoryTrimRunning atomic.Bool
|
||||
agentMemoryTrimLastAt atomic.Int64
|
||||
runAgentMemoryTrimAsync = func(fn func()) {
|
||||
go fn()
|
||||
}
|
||||
agentMemoryTrimFn = func() {
|
||||
runtime.GC()
|
||||
debug.FreeOSMemory()
|
||||
}
|
||||
)
|
||||
|
||||
type agentRuntime struct {
|
||||
inst db.Database
|
||||
sessions map[string]db.StatementExecer
|
||||
nextSessionID int64
|
||||
}
|
||||
|
||||
func main() {
|
||||
if agentDatabaseFactory == nil || strings.TrimSpace(agentDriverType) == "" {
|
||||
fmt.Fprintf(os.Stderr, "未配置驱动代理 provider,请使用 gonavi_<driver>_driver 标签构建\n")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
// driver-agent 是独立进程,主进程无法控制其 GC 行为。
|
||||
// 大结果集(88W+ 行)通过 JSON-lines 跨进程传输时,每行有 5-8 倍内存副本;
|
||||
// Go 默认 GOGC=100 + Windows MADV_FREE 不归还 RSS,会导致 driver-agent 进程
|
||||
// 内存峰值达到数据总量的 10+ 倍(用户实测 88W 普通业务表撑到 8G+)。
|
||||
//
|
||||
// GC 策略组合:
|
||||
// - SetGCPercent(50):堆增长 50% 即触发 GC,比默认 100 更早收敛
|
||||
// - InitMemorySoftLimit:起始 2GB,运行时由 MaybeGrowMemoryLimit 自适应抬升到最多 8GB
|
||||
// (起步保守 + 按需扩张,避免静态 2GB 限制在大表场景触发 GC 硬模式降速 15-25%)
|
||||
//
|
||||
// 代价:CPU 开销增加约 5-10%。导出场景是 I/O 密集型,可忽略。
|
||||
debug.SetGCPercent(50)
|
||||
db.InitMemorySoftLimit(db.MemorySoftLimitInitialBytes)
|
||||
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
scanner.Buffer(make([]byte, 0, 16<<10), 8<<20)
|
||||
writer := bufio.NewWriter(os.Stdout)
|
||||
defer writer.Flush()
|
||||
|
||||
var inst db.Database
|
||||
runtimeState := &agentRuntime{
|
||||
sessions: make(map[string]db.StatementExecer),
|
||||
}
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" {
|
||||
@@ -87,23 +142,32 @@ func main() {
|
||||
continue
|
||||
}
|
||||
|
||||
resp := handleRequest(&inst, req)
|
||||
if strings.TrimSpace(req.Method) == agentMethodStreamQuery {
|
||||
if err := handleStreamRequest(runtimeState, req, writer); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "写入流式响应失败:%v\n", err)
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
resp := handleRequest(runtimeState, req)
|
||||
if err := writeResponse(writer, resp); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "写入响应失败:%v\n", err)
|
||||
break
|
||||
}
|
||||
if strings.TrimSpace(req.Method) == agentMethodQuery {
|
||||
maybeReleaseAgentMemory("query-response", countAgentResponseRows(resp.Data))
|
||||
}
|
||||
}
|
||||
|
||||
if inst != nil {
|
||||
_ = inst.Close()
|
||||
}
|
||||
runtimeState.close()
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "读取请求失败:%v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func handleRequest(inst *db.Database, req agentRequest) agentResponse {
|
||||
func handleRequest(runtimeState *agentRuntime, req agentRequest) agentResponse {
|
||||
resp := agentResponse{ID: req.ID, Success: true}
|
||||
method := strings.TrimSpace(req.Method)
|
||||
|
||||
@@ -112,9 +176,7 @@ func handleRequest(inst *db.Database, req agentRequest) agentResponse {
|
||||
if req.Config == nil {
|
||||
return fail(resp, "连接配置为空")
|
||||
}
|
||||
if *inst != nil {
|
||||
_ = (*inst).Close()
|
||||
}
|
||||
runtimeState.close()
|
||||
next := agentDatabaseFactory()
|
||||
if next == nil {
|
||||
return fail(resp, "驱动代理初始化失败")
|
||||
@@ -122,14 +184,13 @@ func handleRequest(inst *db.Database, req agentRequest) agentResponse {
|
||||
if err := next.Connect(*req.Config); err != nil {
|
||||
return fail(resp, err.Error())
|
||||
}
|
||||
*inst = next
|
||||
runtimeState.inst = next
|
||||
return resp
|
||||
case agentMethodClose:
|
||||
if *inst != nil {
|
||||
if err := (*inst).Close(); err != nil {
|
||||
if runtimeState.inst != nil {
|
||||
if err := runtimeState.close(); err != nil {
|
||||
return fail(resp, err.Error())
|
||||
}
|
||||
*inst = nil
|
||||
}
|
||||
return resp
|
||||
case agentMethodMetadata:
|
||||
@@ -139,74 +200,146 @@ func handleRequest(inst *db.Database, req agentRequest) agentResponse {
|
||||
"protocolSchema": "json-lines-v1",
|
||||
}
|
||||
return resp
|
||||
case agentMethodOpenSession:
|
||||
if runtimeState.inst == nil {
|
||||
return fail(resp, "connection not open")
|
||||
}
|
||||
provider, ok := runtimeState.inst.(db.SessionExecerProvider)
|
||||
if !ok {
|
||||
return fail(resp, fmt.Sprintf("当前数据源(%s)不支持 SQL 编辑器托管事务", strings.TrimSpace(agentDriverType)))
|
||||
}
|
||||
openCtx := context.Background()
|
||||
var cancel context.CancelFunc
|
||||
if req.TimeoutMs > 0 {
|
||||
openCtx, cancel = context.WithTimeout(context.Background(), time.Duration(req.TimeoutMs)*time.Millisecond)
|
||||
defer cancel()
|
||||
}
|
||||
session, err := provider.OpenSessionExecer(openCtx)
|
||||
if err != nil {
|
||||
return fail(resp, err.Error())
|
||||
}
|
||||
sessionID := runtimeState.nextID()
|
||||
runtimeState.sessions[sessionID] = session
|
||||
resp.Data = sessionID
|
||||
return resp
|
||||
case agentMethodCloseSession:
|
||||
if err := runtimeState.closeSession(req.SessionID); err != nil {
|
||||
return fail(resp, err.Error())
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
if *inst == nil {
|
||||
if runtimeState.inst == nil {
|
||||
return fail(resp, "connection not open")
|
||||
}
|
||||
|
||||
if session, ok, err := runtimeState.session(req.SessionID); err != nil {
|
||||
return fail(resp, err.Error())
|
||||
} else if ok {
|
||||
switch method {
|
||||
case agentMethodQuery:
|
||||
data, fields, messages, err := queryStatementWithMessagesOptionalTimeout(session, req.Query, req.TimeoutMs)
|
||||
if err != nil {
|
||||
return fail(resp, err.Error())
|
||||
}
|
||||
resp.Data = data
|
||||
resp.Fields = fields
|
||||
resp.Messages = messages
|
||||
case agentMethodQueryMulti:
|
||||
data, messages, supported, err := queryMultiStatementWithMessagesOptionalTimeout(session, req.Query, req.TimeoutMs)
|
||||
if err != nil {
|
||||
return fail(resp, err.Error())
|
||||
}
|
||||
if !supported {
|
||||
return fail(resp, "当前事务会话不支持多结果集查询")
|
||||
}
|
||||
resp.Data = data
|
||||
resp.Messages = messages
|
||||
case agentMethodExec:
|
||||
affected, err := execStatementWithOptionalTimeout(session, req.Query, req.TimeoutMs)
|
||||
if err != nil {
|
||||
return fail(resp, err.Error())
|
||||
}
|
||||
resp.RowsAffected = affected
|
||||
default:
|
||||
return fail(resp, "当前事务会话不支持该方法")
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
switch method {
|
||||
case agentMethodPing:
|
||||
if err := (*inst).Ping(); err != nil {
|
||||
if err := runtimeState.inst.Ping(); err != nil {
|
||||
return fail(resp, err.Error())
|
||||
}
|
||||
case agentMethodQuery:
|
||||
data, fields, err := queryWithOptionalTimeout(*inst, req.Query, req.TimeoutMs)
|
||||
data, fields, messages, err := queryWithMessagesOptionalTimeout(runtimeState.inst, req.Query, req.TimeoutMs)
|
||||
if err != nil {
|
||||
return fail(resp, err.Error())
|
||||
}
|
||||
resp.Data = data
|
||||
resp.Fields = fields
|
||||
resp.Messages = messages
|
||||
case agentMethodQueryMulti:
|
||||
data, messages, supported, err := queryMultiWithMessagesOptionalTimeout(runtimeState.inst, req.Query, req.TimeoutMs)
|
||||
if err != nil {
|
||||
return fail(resp, err.Error())
|
||||
}
|
||||
if !supported {
|
||||
return fail(resp, "当前驱动不支持原生多结果集查询")
|
||||
}
|
||||
resp.Data = data
|
||||
resp.Messages = messages
|
||||
case agentMethodExec:
|
||||
affected, err := execWithOptionalTimeout(*inst, req.Query, req.TimeoutMs)
|
||||
affected, err := execWithOptionalTimeout(runtimeState.inst, req.Query, req.TimeoutMs)
|
||||
if err != nil {
|
||||
return fail(resp, err.Error())
|
||||
}
|
||||
resp.RowsAffected = affected
|
||||
case agentMethodGetDatabases:
|
||||
data, err := (*inst).GetDatabases()
|
||||
data, err := runtimeState.inst.GetDatabases()
|
||||
if err != nil {
|
||||
return fail(resp, err.Error())
|
||||
}
|
||||
resp.Data = data
|
||||
case agentMethodGetTables:
|
||||
data, err := (*inst).GetTables(req.DBName)
|
||||
data, err := runtimeState.inst.GetTables(req.DBName)
|
||||
if err != nil {
|
||||
return fail(resp, err.Error())
|
||||
}
|
||||
resp.Data = data
|
||||
case agentMethodGetCreateStmt:
|
||||
data, err := (*inst).GetCreateStatement(req.DBName, req.TableName)
|
||||
data, err := runtimeState.inst.GetCreateStatement(req.DBName, req.TableName)
|
||||
if err != nil {
|
||||
return fail(resp, err.Error())
|
||||
}
|
||||
resp.Data = data
|
||||
case agentMethodGetColumns:
|
||||
data, err := (*inst).GetColumns(req.DBName, req.TableName)
|
||||
data, err := runtimeState.inst.GetColumns(req.DBName, req.TableName)
|
||||
if err != nil {
|
||||
return fail(resp, err.Error())
|
||||
}
|
||||
resp.Data = data
|
||||
case agentMethodGetAllColumns:
|
||||
data, err := (*inst).GetAllColumns(req.DBName)
|
||||
data, err := runtimeState.inst.GetAllColumns(req.DBName)
|
||||
if err != nil {
|
||||
return fail(resp, err.Error())
|
||||
}
|
||||
resp.Data = data
|
||||
case agentMethodGetIndexes:
|
||||
data, err := (*inst).GetIndexes(req.DBName, req.TableName)
|
||||
data, err := runtimeState.inst.GetIndexes(req.DBName, req.TableName)
|
||||
if err != nil {
|
||||
return fail(resp, err.Error())
|
||||
}
|
||||
resp.Data = data
|
||||
case agentMethodGetForeignKey:
|
||||
data, err := (*inst).GetForeignKeys(req.DBName, req.TableName)
|
||||
data, err := runtimeState.inst.GetForeignKeys(req.DBName, req.TableName)
|
||||
if err != nil {
|
||||
return fail(resp, err.Error())
|
||||
}
|
||||
resp.Data = data
|
||||
case agentMethodGetTriggers:
|
||||
data, err := (*inst).GetTriggers(req.DBName, req.TableName)
|
||||
data, err := runtimeState.inst.GetTriggers(req.DBName, req.TableName)
|
||||
if err != nil {
|
||||
return fail(resp, err.Error())
|
||||
}
|
||||
@@ -215,7 +348,7 @@ func handleRequest(inst *db.Database, req agentRequest) agentResponse {
|
||||
if req.Changes == nil {
|
||||
return fail(resp, "变更集为空")
|
||||
}
|
||||
applier, ok := (*inst).(interface {
|
||||
applier, ok := runtimeState.inst.(interface {
|
||||
ApplyChanges(tableName string, changes connection.ChangeSet) error
|
||||
})
|
||||
if !ok {
|
||||
@@ -231,6 +364,169 @@ func handleRequest(inst *db.Database, req agentRequest) agentResponse {
|
||||
return resp
|
||||
}
|
||||
|
||||
type agentStreamResponseWriter struct {
|
||||
writer *bufio.Writer
|
||||
requestID int64
|
||||
columns []string
|
||||
rows [][]interface{}
|
||||
rowCount int64
|
||||
}
|
||||
|
||||
func newAgentStreamResponseWriter(writer *bufio.Writer, requestID int64) *agentStreamResponseWriter {
|
||||
return &agentStreamResponseWriter{
|
||||
writer: writer,
|
||||
requestID: requestID,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *agentStreamResponseWriter) SetColumns(columns []string) error {
|
||||
w.columns = append([]string(nil), columns...)
|
||||
return writeResponse(w.writer, agentResponse{
|
||||
ID: w.requestID,
|
||||
Success: true,
|
||||
ChunkType: agentChunkColumns,
|
||||
Fields: w.columns,
|
||||
})
|
||||
}
|
||||
|
||||
func (w *agentStreamResponseWriter) ConsumeRow(row map[string]interface{}) error {
|
||||
if len(w.columns) == 0 {
|
||||
return fmt.Errorf("流式查询缺少列定义")
|
||||
}
|
||||
values := make([]interface{}, len(w.columns))
|
||||
for idx, column := range w.columns {
|
||||
values[idx] = row[column]
|
||||
}
|
||||
return w.ConsumeRowValues(values)
|
||||
}
|
||||
|
||||
func (w *agentStreamResponseWriter) ConsumeRowValues(values []interface{}) error {
|
||||
row := append([]interface{}(nil), values...)
|
||||
w.rows = append(w.rows, row)
|
||||
w.rowCount++
|
||||
if len(w.rows) < agentStreamBatchSize {
|
||||
return nil
|
||||
}
|
||||
return w.flushRows()
|
||||
}
|
||||
|
||||
func (w *agentStreamResponseWriter) flushRows() error {
|
||||
if len(w.rows) == 0 {
|
||||
return nil
|
||||
}
|
||||
rows := w.rows
|
||||
w.rows = nil
|
||||
return writeResponse(w.writer, agentResponse{
|
||||
ID: w.requestID,
|
||||
Success: true,
|
||||
ChunkType: agentChunkRows,
|
||||
Data: rows,
|
||||
})
|
||||
}
|
||||
|
||||
func (w *agentStreamResponseWriter) finish() error {
|
||||
return w.flushRows()
|
||||
}
|
||||
|
||||
func handleStreamRequest(runtimeState *agentRuntime, req agentRequest, writer *bufio.Writer) error {
|
||||
resp := agentResponse{ID: req.ID, Success: true}
|
||||
if runtimeState.inst == nil {
|
||||
return writeResponse(writer, fail(resp, "connection not open"))
|
||||
}
|
||||
|
||||
streamWriter := newAgentStreamResponseWriter(writer, req.ID)
|
||||
if session, ok, err := runtimeState.session(req.SessionID); err != nil {
|
||||
return writeResponse(writer, fail(resp, err.Error()))
|
||||
} else if ok {
|
||||
if err := streamStatementWithOptionalTimeout(session, req.Query, req.TimeoutMs, streamWriter); err != nil {
|
||||
_ = streamWriter.finish()
|
||||
return writeResponse(writer, fail(resp, err.Error()))
|
||||
}
|
||||
if err := streamWriter.finish(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeResponse(writer, agentResponse{ID: req.ID, Success: true, ChunkType: agentChunkDone}); err != nil {
|
||||
return err
|
||||
}
|
||||
maybeReleaseAgentMemory("stream-query-session", streamWriter.rowCount)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := streamDatabaseWithOptionalTimeout(runtimeState.inst, req.Query, req.TimeoutMs, streamWriter); err != nil {
|
||||
_ = streamWriter.finish()
|
||||
return writeResponse(writer, fail(resp, err.Error()))
|
||||
}
|
||||
if err := streamWriter.finish(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeResponse(writer, agentResponse{ID: req.ID, Success: true, ChunkType: agentChunkDone}); err != nil {
|
||||
return err
|
||||
}
|
||||
maybeReleaseAgentMemory("stream-query-db", streamWriter.rowCount)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *agentRuntime) nextID() string {
|
||||
r.ensureSessionMap()
|
||||
r.nextSessionID++
|
||||
return "session-" + strconv.FormatInt(r.nextSessionID, 10)
|
||||
}
|
||||
|
||||
func (r *agentRuntime) session(sessionID string) (db.StatementExecer, bool, error) {
|
||||
r.ensureSessionMap()
|
||||
sessionID = strings.TrimSpace(sessionID)
|
||||
if sessionID == "" {
|
||||
return nil, false, nil
|
||||
}
|
||||
session, ok := r.sessions[sessionID]
|
||||
if !ok || session == nil {
|
||||
return nil, false, fmt.Errorf("事务会话不存在或已结束")
|
||||
}
|
||||
return session, true, nil
|
||||
}
|
||||
|
||||
func (r *agentRuntime) closeSession(sessionID string) error {
|
||||
r.ensureSessionMap()
|
||||
sessionID = strings.TrimSpace(sessionID)
|
||||
if sessionID == "" {
|
||||
return fmt.Errorf("事务会话 ID 不能为空")
|
||||
}
|
||||
session, ok := r.sessions[sessionID]
|
||||
if ok {
|
||||
delete(r.sessions, sessionID)
|
||||
}
|
||||
if !ok || session == nil {
|
||||
return fmt.Errorf("事务会话不存在或已结束")
|
||||
}
|
||||
return session.Close()
|
||||
}
|
||||
|
||||
func (r *agentRuntime) close() error {
|
||||
var closeErr error
|
||||
r.ensureSessionMap()
|
||||
for sessionID, session := range r.sessions {
|
||||
delete(r.sessions, sessionID)
|
||||
if session != nil {
|
||||
if err := session.Close(); err != nil && closeErr == nil {
|
||||
closeErr = err
|
||||
}
|
||||
}
|
||||
}
|
||||
if r.inst != nil {
|
||||
if err := r.inst.Close(); err != nil && closeErr == nil {
|
||||
closeErr = err
|
||||
}
|
||||
r.inst = nil
|
||||
}
|
||||
return closeErr
|
||||
}
|
||||
|
||||
func (r *agentRuntime) ensureSessionMap() {
|
||||
if r.sessions == nil {
|
||||
r.sessions = make(map[string]db.StatementExecer)
|
||||
}
|
||||
}
|
||||
|
||||
func writeResponse(writer *bufio.Writer, resp agentResponse) error {
|
||||
// 对响应数据做统一 JSON 安全归一化:
|
||||
// 将 map[any]any(如 duckdb.Map)递归转换为 map[string]any,避免序列化失败导致代理进程退出。
|
||||
@@ -301,25 +597,234 @@ func normalizeAgentResponseData(v interface{}) interface{} {
|
||||
}
|
||||
}
|
||||
|
||||
func queryWithOptionalTimeout(inst db.Database, query string, timeoutMs int64) ([]map[string]interface{}, []string, error) {
|
||||
type agentQueryRunner interface {
|
||||
Query(string) ([]map[string]interface{}, []string, error)
|
||||
}
|
||||
|
||||
type agentQueryContextRunner interface {
|
||||
QueryContext(context.Context, string) ([]map[string]interface{}, []string, error)
|
||||
}
|
||||
|
||||
type agentQueryMessageRunner interface {
|
||||
QueryWithMessages(query string) ([]map[string]interface{}, []string, []string, error)
|
||||
}
|
||||
|
||||
type agentQueryMessageContextRunner interface {
|
||||
QueryContextWithMessages(context.Context, string) ([]map[string]interface{}, []string, []string, error)
|
||||
}
|
||||
|
||||
type agentMultiResultMessageRunner interface {
|
||||
QueryMultiWithMessages(query string) ([]connection.ResultSetData, []string, error)
|
||||
}
|
||||
|
||||
type agentMultiResultMessageContextRunner interface {
|
||||
QueryMultiContextWithMessages(context.Context, string) ([]connection.ResultSetData, []string, error)
|
||||
}
|
||||
|
||||
type agentMultiResultRunner interface {
|
||||
QueryMulti(query string) ([]connection.ResultSetData, error)
|
||||
}
|
||||
|
||||
type agentMultiResultContextRunner interface {
|
||||
QueryMultiContext(context.Context, string) ([]connection.ResultSetData, error)
|
||||
}
|
||||
|
||||
type agentExecRunner interface {
|
||||
Exec(string) (int64, error)
|
||||
}
|
||||
|
||||
type agentExecContextRunner interface {
|
||||
ExecContext(context.Context, string) (int64, error)
|
||||
}
|
||||
|
||||
func queryWithMessagesOptionalTimeout(inst agentQueryRunner, query string, timeoutMs int64) ([]map[string]interface{}, []string, []string, error) {
|
||||
effectiveTimeoutMs := timeoutMs
|
||||
if effectiveTimeoutMs <= 0 && strings.EqualFold(strings.TrimSpace(agentDriverType), "clickhouse") {
|
||||
effectiveTimeoutMs = int64(legacyClickHouseDefaultTimeout / time.Millisecond)
|
||||
}
|
||||
if effectiveTimeoutMs <= 0 {
|
||||
return inst.Query(query)
|
||||
if q, ok := inst.(agentQueryMessageRunner); ok {
|
||||
return q.QueryWithMessages(query)
|
||||
}
|
||||
data, fields, err := inst.Query(query)
|
||||
return data, fields, nil, err
|
||||
}
|
||||
if q, ok := inst.(interface {
|
||||
QueryContext(context.Context, string) ([]map[string]interface{}, []string, error)
|
||||
}); ok {
|
||||
if q, ok := inst.(agentQueryMessageContextRunner); ok {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(effectiveTimeoutMs)*time.Millisecond)
|
||||
defer cancel()
|
||||
return q.QueryContext(ctx, query)
|
||||
return q.QueryContextWithMessages(ctx, query)
|
||||
}
|
||||
return inst.Query(query)
|
||||
if q, ok := inst.(agentQueryContextRunner); ok {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(effectiveTimeoutMs)*time.Millisecond)
|
||||
defer cancel()
|
||||
data, fields, err := q.QueryContext(ctx, query)
|
||||
return data, fields, nil, err
|
||||
}
|
||||
if q, ok := inst.(agentQueryMessageRunner); ok {
|
||||
return q.QueryWithMessages(query)
|
||||
}
|
||||
data, fields, err := inst.Query(query)
|
||||
return data, fields, nil, err
|
||||
}
|
||||
|
||||
func execWithOptionalTimeout(inst db.Database, query string, timeoutMs int64) (int64, error) {
|
||||
func queryWithOptionalTimeout(inst agentQueryRunner, query string, timeoutMs int64) ([]map[string]interface{}, []string, error) {
|
||||
data, fields, _, err := queryWithMessagesOptionalTimeout(inst, query, timeoutMs)
|
||||
return data, fields, err
|
||||
}
|
||||
|
||||
func queryStatementWithOptionalTimeout(inst db.StatementExecer, query string, timeoutMs int64) ([]map[string]interface{}, []string, error) {
|
||||
queryRunner, ok := inst.(agentQueryRunner)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("当前事务会话不支持查询语句")
|
||||
}
|
||||
return queryWithOptionalTimeout(queryRunner, query, timeoutMs)
|
||||
}
|
||||
|
||||
func queryStatementWithMessagesOptionalTimeout(inst db.StatementExecer, query string, timeoutMs int64) ([]map[string]interface{}, []string, []string, error) {
|
||||
queryRunner, ok := inst.(agentQueryRunner)
|
||||
if !ok {
|
||||
return nil, nil, nil, fmt.Errorf("当前事务会话不支持查询语句")
|
||||
}
|
||||
return queryWithMessagesOptionalTimeout(queryRunner, query, timeoutMs)
|
||||
}
|
||||
|
||||
func queryMultiWithMessagesOptionalTimeout(inst db.Database, query string, timeoutMs int64) ([]connection.ResultSetData, []string, bool, error) {
|
||||
effectiveTimeoutMs := timeoutMs
|
||||
if effectiveTimeoutMs <= 0 && strings.EqualFold(strings.TrimSpace(agentDriverType), "clickhouse") {
|
||||
effectiveTimeoutMs = int64(legacyClickHouseDefaultTimeout / time.Millisecond)
|
||||
}
|
||||
if effectiveTimeoutMs > 0 {
|
||||
if q, ok := inst.(agentMultiResultMessageContextRunner); ok {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(effectiveTimeoutMs)*time.Millisecond)
|
||||
defer cancel()
|
||||
data, messages, err := q.QueryMultiContextWithMessages(ctx, query)
|
||||
return data, messages, true, err
|
||||
}
|
||||
if q, ok := inst.(agentMultiResultContextRunner); ok {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(effectiveTimeoutMs)*time.Millisecond)
|
||||
defer cancel()
|
||||
data, err := q.QueryMultiContext(ctx, query)
|
||||
return data, nil, true, err
|
||||
}
|
||||
}
|
||||
if q, ok := inst.(agentMultiResultMessageRunner); ok {
|
||||
data, messages, err := q.QueryMultiWithMessages(query)
|
||||
return data, messages, true, err
|
||||
}
|
||||
if q, ok := inst.(agentMultiResultRunner); ok {
|
||||
data, err := q.QueryMulti(query)
|
||||
return data, nil, true, err
|
||||
}
|
||||
return nil, nil, false, nil
|
||||
}
|
||||
|
||||
func queryMultiStatementWithMessagesOptionalTimeout(inst db.StatementExecer, query string, timeoutMs int64) ([]connection.ResultSetData, []string, bool, error) {
|
||||
effectiveTimeoutMs := timeoutMs
|
||||
if effectiveTimeoutMs <= 0 && strings.EqualFold(strings.TrimSpace(agentDriverType), "clickhouse") {
|
||||
effectiveTimeoutMs = int64(legacyClickHouseDefaultTimeout / time.Millisecond)
|
||||
}
|
||||
if effectiveTimeoutMs > 0 {
|
||||
if q, ok := inst.(agentMultiResultMessageContextRunner); ok {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(effectiveTimeoutMs)*time.Millisecond)
|
||||
defer cancel()
|
||||
data, messages, err := q.QueryMultiContextWithMessages(ctx, query)
|
||||
return data, messages, true, err
|
||||
}
|
||||
if q, ok := inst.(agentMultiResultContextRunner); ok {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(effectiveTimeoutMs)*time.Millisecond)
|
||||
defer cancel()
|
||||
data, err := q.QueryMultiContext(ctx, query)
|
||||
return data, nil, true, err
|
||||
}
|
||||
}
|
||||
if q, ok := inst.(agentMultiResultMessageRunner); ok {
|
||||
data, messages, err := q.QueryMultiWithMessages(query)
|
||||
return data, messages, true, err
|
||||
}
|
||||
if q, ok := inst.(agentMultiResultRunner); ok {
|
||||
data, err := q.QueryMulti(query)
|
||||
return data, nil, true, err
|
||||
}
|
||||
return nil, nil, false, nil
|
||||
}
|
||||
|
||||
func streamWithOptionalTimeout(inst db.StreamQueryExecer, query string, timeoutMs int64, consumer db.QueryStreamConsumer) error {
|
||||
effectiveTimeoutMs := timeoutMs
|
||||
if effectiveTimeoutMs <= 0 && strings.EqualFold(strings.TrimSpace(agentDriverType), "clickhouse") {
|
||||
effectiveTimeoutMs = int64(legacyClickHouseDefaultTimeout / time.Millisecond)
|
||||
}
|
||||
if effectiveTimeoutMs <= 0 {
|
||||
return inst.StreamQuery(query, consumer)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(effectiveTimeoutMs)*time.Millisecond)
|
||||
defer cancel()
|
||||
return inst.StreamQueryContext(ctx, query, consumer)
|
||||
}
|
||||
|
||||
func streamBufferedQueryResult(fields []string, data []map[string]interface{}, consumer db.QueryStreamConsumer) error {
|
||||
if err := consumer.SetColumns(fields); err != nil {
|
||||
return err
|
||||
}
|
||||
if valueConsumer, ok := consumer.(db.QueryStreamValueConsumer); ok {
|
||||
for _, row := range data {
|
||||
values := make([]interface{}, len(fields))
|
||||
for idx, field := range fields {
|
||||
values[idx] = row[field]
|
||||
}
|
||||
if err := valueConsumer.ConsumeRowValues(values); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
for _, row := range data {
|
||||
if err := consumer.ConsumeRow(row); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func streamStatementWithOptionalTimeout(inst db.StatementExecer, query string, timeoutMs int64, consumer db.QueryStreamConsumer) error {
|
||||
if streamer, ok := inst.(db.StreamQueryExecer); ok {
|
||||
return streamWithOptionalTimeout(streamer, query, timeoutMs, consumer)
|
||||
}
|
||||
data, fields, err := queryStatementWithOptionalTimeout(inst, query, timeoutMs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return streamBufferedQueryResult(fields, data, consumer)
|
||||
}
|
||||
|
||||
func streamDatabaseWithOptionalTimeout(inst db.Database, query string, timeoutMs int64, consumer db.QueryStreamConsumer) error {
|
||||
if streamer, ok := inst.(db.StreamQueryExecer); ok {
|
||||
return streamWithOptionalTimeout(streamer, query, timeoutMs, consumer)
|
||||
}
|
||||
if provider, ok := inst.(db.SessionExecerProvider); ok {
|
||||
openCtx := context.Background()
|
||||
var cancel context.CancelFunc
|
||||
effectiveTimeoutMs := timeoutMs
|
||||
if effectiveTimeoutMs <= 0 && strings.EqualFold(strings.TrimSpace(agentDriverType), "clickhouse") {
|
||||
effectiveTimeoutMs = int64(legacyClickHouseDefaultTimeout / time.Millisecond)
|
||||
}
|
||||
if effectiveTimeoutMs > 0 {
|
||||
openCtx, cancel = context.WithTimeout(context.Background(), time.Duration(effectiveTimeoutMs)*time.Millisecond)
|
||||
defer cancel()
|
||||
}
|
||||
session, err := provider.OpenSessionExecer(openCtx)
|
||||
if err == nil {
|
||||
defer session.Close()
|
||||
return streamStatementWithOptionalTimeout(session, query, timeoutMs, consumer)
|
||||
}
|
||||
}
|
||||
data, fields, err := queryWithOptionalTimeout(inst, query, timeoutMs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return streamBufferedQueryResult(fields, data, consumer)
|
||||
}
|
||||
|
||||
func execWithOptionalTimeout(inst agentExecRunner, query string, timeoutMs int64) (int64, error) {
|
||||
effectiveTimeoutMs := timeoutMs
|
||||
if effectiveTimeoutMs <= 0 && strings.EqualFold(strings.TrimSpace(agentDriverType), "clickhouse") {
|
||||
effectiveTimeoutMs = int64(legacyClickHouseDefaultTimeout / time.Millisecond)
|
||||
@@ -327,12 +832,52 @@ func execWithOptionalTimeout(inst db.Database, query string, timeoutMs int64) (i
|
||||
if effectiveTimeoutMs <= 0 {
|
||||
return inst.Exec(query)
|
||||
}
|
||||
if e, ok := inst.(interface {
|
||||
ExecContext(context.Context, string) (int64, error)
|
||||
}); ok {
|
||||
if e, ok := inst.(agentExecContextRunner); ok {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(effectiveTimeoutMs)*time.Millisecond)
|
||||
defer cancel()
|
||||
return e.ExecContext(ctx, query)
|
||||
}
|
||||
return inst.Exec(query)
|
||||
}
|
||||
|
||||
func execStatementWithOptionalTimeout(inst db.StatementExecer, query string, timeoutMs int64) (int64, error) {
|
||||
return execWithOptionalTimeout(inst, query, timeoutMs)
|
||||
}
|
||||
|
||||
func countAgentResponseRows(data interface{}) int64 {
|
||||
rows, ok := data.([]map[string]interface{})
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return int64(len(rows))
|
||||
}
|
||||
|
||||
func maybeReleaseAgentMemory(reason string, rows int64) {
|
||||
if rows < agentMemoryTrimRowsThreshold {
|
||||
return
|
||||
}
|
||||
if !agentMemoryTrimRunning.CompareAndSwap(false, true) {
|
||||
return
|
||||
}
|
||||
|
||||
runAgentMemoryTrimAsync(func() {
|
||||
defer agentMemoryTrimRunning.Store(false)
|
||||
if delay := nextAgentMemoryTrimDelay(); delay > 0 {
|
||||
time.Sleep(delay)
|
||||
}
|
||||
agentMemoryTrimFn()
|
||||
agentMemoryTrimLastAt.Store(time.Now().UnixNano())
|
||||
})
|
||||
}
|
||||
|
||||
func nextAgentMemoryTrimDelay() time.Duration {
|
||||
lastUnixNano := agentMemoryTrimLastAt.Load()
|
||||
if lastUnixNano <= 0 {
|
||||
return 0
|
||||
}
|
||||
elapsed := time.Since(time.Unix(0, lastUnixNano))
|
||||
if elapsed >= agentMemoryTrimMinInterval {
|
||||
return 0
|
||||
}
|
||||
return agentMemoryTrimMinInterval - elapsed
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -77,8 +78,8 @@ func TestHandleRequestMetadataReportsAgentRevision(t *testing.T) {
|
||||
agentDriverType = "clickhouse"
|
||||
agentDatabaseFactory = func() db.Database { return nil }
|
||||
|
||||
var inst db.Database
|
||||
resp := handleRequest(&inst, agentRequest{ID: 7, Method: agentMethodMetadata})
|
||||
runtimeState := &agentRuntime{sessions: make(map[string]db.StatementExecer)}
|
||||
resp := handleRequest(runtimeState, agentRequest{ID: 7, Method: agentMethodMetadata})
|
||||
if !resp.Success {
|
||||
t.Fatalf("metadata request failed: %s", resp.Error)
|
||||
}
|
||||
@@ -100,6 +101,9 @@ type fakeAgentTimeoutDB struct {
|
||||
execCalled bool
|
||||
execContextCalled bool
|
||||
deadlineSet bool
|
||||
queryMessages []string
|
||||
multiResults []connection.ResultSetData
|
||||
multiMessages []string
|
||||
}
|
||||
|
||||
func (f *fakeAgentTimeoutDB) Connect(config connection.ConnectionConfig) error { return nil }
|
||||
@@ -116,6 +120,14 @@ func (f *fakeAgentTimeoutDB) QueryContext(ctx context.Context, query string) ([]
|
||||
}
|
||||
return []map[string]interface{}{{"ok": 1}}, []string{"ok"}, nil
|
||||
}
|
||||
func (f *fakeAgentTimeoutDB) QueryWithMessages(query string) ([]map[string]interface{}, []string, []string, error) {
|
||||
data, fields, err := f.QueryContext(context.Background(), query)
|
||||
return data, fields, append([]string(nil), f.queryMessages...), err
|
||||
}
|
||||
func (f *fakeAgentTimeoutDB) QueryContextWithMessages(ctx context.Context, query string) ([]map[string]interface{}, []string, []string, error) {
|
||||
data, fields, err := f.QueryContext(ctx, query)
|
||||
return data, fields, append([]string(nil), f.queryMessages...), err
|
||||
}
|
||||
func (f *fakeAgentTimeoutDB) Exec(query string) (int64, error) {
|
||||
f.execCalled = true
|
||||
return 0, errors.New("exec should not be called")
|
||||
@@ -149,6 +161,121 @@ func (f *fakeAgentTimeoutDB) GetForeignKeys(dbName, tableName string) ([]connect
|
||||
func (f *fakeAgentTimeoutDB) GetTriggers(dbName, tableName string) ([]connection.TriggerDefinition, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f *fakeAgentTimeoutDB) QueryMultiWithMessages(query string) ([]connection.ResultSetData, []string, error) {
|
||||
return append([]connection.ResultSetData(nil), f.multiResults...), append([]string(nil), f.multiMessages...), nil
|
||||
}
|
||||
func (f *fakeAgentTimeoutDB) QueryMultiContextWithMessages(ctx context.Context, query string) ([]connection.ResultSetData, []string, error) {
|
||||
if _, ok := ctx.Deadline(); ok {
|
||||
f.deadlineSet = true
|
||||
}
|
||||
return f.QueryMultiWithMessages(query)
|
||||
}
|
||||
|
||||
type fakeAgentSessionDB struct {
|
||||
fakeAgentTimeoutDB
|
||||
session *fakeAgentStatementSession
|
||||
}
|
||||
|
||||
func (f *fakeAgentSessionDB) OpenSessionExecer(ctx context.Context) (db.StatementExecer, error) {
|
||||
f.session = &fakeAgentStatementSession{}
|
||||
return f.session, nil
|
||||
}
|
||||
|
||||
type fakeAgentStatementSession struct {
|
||||
queryCalls int
|
||||
execCalls int
|
||||
closed bool
|
||||
messages []string
|
||||
}
|
||||
|
||||
func (f *fakeAgentStatementSession) Query(query string) ([]map[string]interface{}, []string, error) {
|
||||
return f.QueryContext(context.Background(), query)
|
||||
}
|
||||
|
||||
func (f *fakeAgentStatementSession) QueryContext(ctx context.Context, query string) ([]map[string]interface{}, []string, error) {
|
||||
f.queryCalls++
|
||||
return []map[string]interface{}{{"session_ok": 1}}, []string{"session_ok"}, nil
|
||||
}
|
||||
func (f *fakeAgentStatementSession) QueryWithMessages(query string) ([]map[string]interface{}, []string, []string, error) {
|
||||
data, fields, err := f.QueryContext(context.Background(), query)
|
||||
return data, fields, append([]string(nil), f.messages...), err
|
||||
}
|
||||
func (f *fakeAgentStatementSession) QueryContextWithMessages(ctx context.Context, query string) ([]map[string]interface{}, []string, []string, error) {
|
||||
data, fields, err := f.QueryContext(ctx, query)
|
||||
return data, fields, append([]string(nil), f.messages...), err
|
||||
}
|
||||
|
||||
func (f *fakeAgentStatementSession) Exec(query string) (int64, error) {
|
||||
return f.ExecContext(context.Background(), query)
|
||||
}
|
||||
|
||||
func (f *fakeAgentStatementSession) ExecContext(ctx context.Context, query string) (int64, error) {
|
||||
f.execCalls++
|
||||
return 9, nil
|
||||
}
|
||||
|
||||
func (f *fakeAgentStatementSession) Close() error {
|
||||
f.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeAgentStreamSession struct {
|
||||
closed bool
|
||||
streamCalls int
|
||||
deadlineSet bool
|
||||
}
|
||||
|
||||
func (f *fakeAgentStreamSession) Exec(query string) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (f *fakeAgentStreamSession) ExecContext(ctx context.Context, query string) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (f *fakeAgentStreamSession) Close() error {
|
||||
f.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeAgentStreamSession) StreamQuery(query string, consumer db.QueryStreamConsumer) error {
|
||||
return f.StreamQueryContext(context.Background(), query, consumer)
|
||||
}
|
||||
|
||||
func (f *fakeAgentStreamSession) StreamQueryContext(ctx context.Context, query string, consumer db.QueryStreamConsumer) error {
|
||||
f.streamCalls++
|
||||
if _, ok := ctx.Deadline(); ok {
|
||||
f.deadlineSet = true
|
||||
}
|
||||
if err := consumer.SetColumns([]string{"id", "name"}); err != nil {
|
||||
return err
|
||||
}
|
||||
if valueConsumer, ok := consumer.(db.QueryStreamValueConsumer); ok {
|
||||
if err := valueConsumer.ConsumeRowValues([]interface{}{1, "alice"}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := valueConsumer.ConsumeRowValues([]interface{}{2, "bob"}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := consumer.ConsumeRow(map[string]interface{}{"id": 1, "name": "alice"}); err != nil {
|
||||
return err
|
||||
}
|
||||
return consumer.ConsumeRow(map[string]interface{}{"id": 2, "name": "bob"})
|
||||
}
|
||||
|
||||
type fakeAgentSessionStreamDB struct {
|
||||
fakeAgentTimeoutDB
|
||||
session *fakeAgentStreamSession
|
||||
openCalls int
|
||||
}
|
||||
|
||||
func (f *fakeAgentSessionStreamDB) OpenSessionExecer(ctx context.Context) (db.StatementExecer, error) {
|
||||
f.openCalls++
|
||||
f.session = &fakeAgentStreamSession{}
|
||||
return f.session, nil
|
||||
}
|
||||
|
||||
func TestQueryWithOptionalTimeout_UsesQueryContext(t *testing.T) {
|
||||
fake := &fakeAgentTimeoutDB{}
|
||||
@@ -198,3 +325,277 @@ func TestQueryWithOptionalTimeout_ClickHouseLegacyModeUsesQueryContext(t *testin
|
||||
t.Fatalf("clickhouse legacy query 调用路径异常,QueryContext=%v Query=%v", fake.queryContextCalled, fake.queryCalled)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleRequest_QueryIncludesServerMessages(t *testing.T) {
|
||||
old := agentDriverType
|
||||
defer func() { agentDriverType = old }()
|
||||
agentDriverType = "sqlserver"
|
||||
|
||||
fake := &fakeAgentTimeoutDB{
|
||||
queryMessages: []string{"PRINT sql line 1", "PRINT sql line 2"},
|
||||
}
|
||||
runtimeState := &agentRuntime{inst: fake, sessions: make(map[string]db.StatementExecer)}
|
||||
|
||||
resp := handleRequest(runtimeState, agentRequest{
|
||||
ID: 11,
|
||||
Method: agentMethodQuery,
|
||||
Query: "exec dbo.p_get_select",
|
||||
TimeoutMs: int64((2 * time.Second).Milliseconds()),
|
||||
})
|
||||
if !resp.Success {
|
||||
t.Fatalf("query request failed: %s", resp.Error)
|
||||
}
|
||||
if len(resp.Messages) != 2 || resp.Messages[0] != "PRINT sql line 1" {
|
||||
t.Fatalf("expected query messages to be preserved, got %#v", resp.Messages)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleRequest_QueryMultiIncludesResultSetsAndMessages(t *testing.T) {
|
||||
old := agentDriverType
|
||||
defer func() { agentDriverType = old }()
|
||||
agentDriverType = "sqlserver"
|
||||
|
||||
fake := &fakeAgentTimeoutDB{
|
||||
multiResults: []connection.ResultSetData{
|
||||
{
|
||||
StatementIndex: 1,
|
||||
Rows: []map[string]interface{}{{"name": "master"}},
|
||||
Columns: []string{"name"},
|
||||
},
|
||||
{
|
||||
StatementIndex: 1,
|
||||
Rows: []map[string]interface{}{},
|
||||
Columns: []string{},
|
||||
Messages: []string{"PRINT generated sql"},
|
||||
},
|
||||
},
|
||||
multiMessages: []string{"batch top-level message"},
|
||||
}
|
||||
runtimeState := &agentRuntime{inst: fake, sessions: make(map[string]db.StatementExecer)}
|
||||
|
||||
resp := handleRequest(runtimeState, agentRequest{
|
||||
ID: 12,
|
||||
Method: agentMethodQueryMulti,
|
||||
Query: "exec dbo.p_get_select",
|
||||
TimeoutMs: int64((2 * time.Second).Milliseconds()),
|
||||
})
|
||||
if !resp.Success {
|
||||
t.Fatalf("queryMulti request failed: %s", resp.Error)
|
||||
}
|
||||
if len(resp.Messages) != 1 || resp.Messages[0] != "batch top-level message" {
|
||||
t.Fatalf("expected top-level messages to be preserved, got %#v", resp.Messages)
|
||||
}
|
||||
resultSets, ok := resp.Data.([]connection.ResultSetData)
|
||||
if !ok {
|
||||
t.Fatalf("expected []connection.ResultSetData, got %T", resp.Data)
|
||||
}
|
||||
if len(resultSets) != 2 {
|
||||
t.Fatalf("expected 2 result sets, got %#v", resultSets)
|
||||
}
|
||||
if len(resultSets[1].Messages) != 1 || resultSets[1].Messages[0] != "PRINT generated sql" {
|
||||
t.Fatalf("expected message-only result set to be preserved, got %#v", resultSets[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleRequest_UsesPinnedSessionForSessionScopedQueryAndExec(t *testing.T) {
|
||||
old := agentDriverType
|
||||
defer func() { agentDriverType = old }()
|
||||
agentDriverType = "sqlserver"
|
||||
|
||||
fake := &fakeAgentSessionDB{}
|
||||
runtimeState := &agentRuntime{
|
||||
inst: fake,
|
||||
sessions: make(map[string]db.StatementExecer),
|
||||
}
|
||||
|
||||
openResp := handleRequest(runtimeState, agentRequest{ID: 1, Method: agentMethodOpenSession})
|
||||
if !openResp.Success {
|
||||
t.Fatalf("openSession failed: %s", openResp.Error)
|
||||
}
|
||||
sessionID, ok := openResp.Data.(string)
|
||||
if !ok || strings.TrimSpace(sessionID) == "" {
|
||||
t.Fatalf("unexpected session id payload: %#v", openResp.Data)
|
||||
}
|
||||
if fake.session == nil {
|
||||
t.Fatal("expected OpenSessionExecer to create a pinned session")
|
||||
}
|
||||
|
||||
queryResp := handleRequest(runtimeState, agentRequest{
|
||||
ID: 2,
|
||||
Method: agentMethodQuery,
|
||||
SessionID: sessionID,
|
||||
Query: "SELECT 1",
|
||||
})
|
||||
if !queryResp.Success {
|
||||
t.Fatalf("session query failed: %s", queryResp.Error)
|
||||
}
|
||||
if len(queryResp.Messages) != 0 {
|
||||
t.Fatalf("expected empty default session messages, got %#v", queryResp.Messages)
|
||||
}
|
||||
if fake.queryCalled || fake.queryContextCalled {
|
||||
t.Fatalf("expected session query to bypass database-level query path, got Query=%v QueryContext=%v", fake.queryCalled, fake.queryContextCalled)
|
||||
}
|
||||
if fake.session.queryCalls != 1 {
|
||||
t.Fatalf("expected pinned session queryCalls=1, got %d", fake.session.queryCalls)
|
||||
}
|
||||
|
||||
execResp := handleRequest(runtimeState, agentRequest{
|
||||
ID: 3,
|
||||
Method: agentMethodExec,
|
||||
SessionID: sessionID,
|
||||
Query: "UPDATE t SET v = 1",
|
||||
})
|
||||
if !execResp.Success {
|
||||
t.Fatalf("session exec failed: %s", execResp.Error)
|
||||
}
|
||||
if fake.execCalled || fake.execContextCalled {
|
||||
t.Fatalf("expected session exec to bypass database-level exec path, got Exec=%v ExecContext=%v", fake.execCalled, fake.execContextCalled)
|
||||
}
|
||||
if fake.session.execCalls != 1 {
|
||||
t.Fatalf("expected pinned session execCalls=1, got %d", fake.session.execCalls)
|
||||
}
|
||||
|
||||
closeResp := handleRequest(runtimeState, agentRequest{
|
||||
ID: 4,
|
||||
Method: agentMethodCloseSession,
|
||||
SessionID: sessionID,
|
||||
})
|
||||
if !closeResp.Success {
|
||||
t.Fatalf("closeSession failed: %s", closeResp.Error)
|
||||
}
|
||||
if !fake.session.closed {
|
||||
t.Fatal("expected pinned session to close")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleStreamRequest_UsesSessionStreamerAndWritesChunks(t *testing.T) {
|
||||
old := agentDriverType
|
||||
originalAsync := runAgentMemoryTrimAsync
|
||||
originalTrim := agentMemoryTrimFn
|
||||
originalLastAt := agentMemoryTrimLastAt.Load()
|
||||
defer func() { agentDriverType = old }()
|
||||
defer func() {
|
||||
runAgentMemoryTrimAsync = originalAsync
|
||||
agentMemoryTrimFn = originalTrim
|
||||
agentMemoryTrimRunning.Store(false)
|
||||
agentMemoryTrimLastAt.Store(originalLastAt)
|
||||
}()
|
||||
agentDriverType = "oceanbase"
|
||||
agentMemoryTrimRunning.Store(false)
|
||||
agentMemoryTrimLastAt.Store(0)
|
||||
|
||||
fake := &fakeAgentSessionStreamDB{}
|
||||
runtimeState := &agentRuntime{
|
||||
inst: fake,
|
||||
sessions: make(map[string]db.StatementExecer),
|
||||
}
|
||||
|
||||
trimmed := 0
|
||||
runAgentMemoryTrimAsync = func(fn func()) {
|
||||
fn()
|
||||
}
|
||||
agentMemoryTrimFn = func() {
|
||||
trimmed++
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
writer := bufio.NewWriter(&out)
|
||||
if err := handleStreamRequest(runtimeState, agentRequest{
|
||||
ID: 9,
|
||||
Method: agentMethodStreamQuery,
|
||||
Query: "SELECT * FROM person_info",
|
||||
TimeoutMs: int64((2 * time.Second).Milliseconds()),
|
||||
}, writer); err != nil {
|
||||
t.Fatalf("handleStreamRequest 返回错误: %v", err)
|
||||
}
|
||||
|
||||
if fake.openCalls != 1 {
|
||||
t.Fatalf("expected OpenSessionExecer called once, got %d", fake.openCalls)
|
||||
}
|
||||
if fake.session == nil || fake.session.streamCalls != 1 {
|
||||
t.Fatalf("expected session streamer used once, session=%#v", fake.session)
|
||||
}
|
||||
if !fake.session.deadlineSet {
|
||||
t.Fatal("expected stream query context deadline to be set")
|
||||
}
|
||||
if !fake.session.closed {
|
||||
t.Fatal("expected session to close after streaming")
|
||||
}
|
||||
if fake.queryCalled || fake.queryContextCalled {
|
||||
t.Fatalf("unexpected fallback query path, Query=%v QueryContext=%v", fake.queryCalled, fake.queryContextCalled)
|
||||
}
|
||||
|
||||
lines := strings.Split(strings.TrimSpace(out.String()), "\n")
|
||||
if len(lines) != 3 {
|
||||
t.Fatalf("expected 3 stream responses, got %d: %q", len(lines), out.String())
|
||||
}
|
||||
|
||||
var columnsResp struct {
|
||||
Success bool `json:"success"`
|
||||
ChunkType string `json:"chunkType"`
|
||||
Fields []string `json:"fields"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(lines[0]), &columnsResp); err != nil {
|
||||
t.Fatalf("decode columns response failed: %v", err)
|
||||
}
|
||||
if !columnsResp.Success || columnsResp.ChunkType != agentChunkColumns || len(columnsResp.Fields) != 2 {
|
||||
t.Fatalf("unexpected columns response: %#v", columnsResp)
|
||||
}
|
||||
|
||||
var rowsResp struct {
|
||||
Success bool `json:"success"`
|
||||
ChunkType string `json:"chunkType"`
|
||||
Data [][]interface{} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(lines[1]), &rowsResp); err != nil {
|
||||
t.Fatalf("decode rows response failed: %v", err)
|
||||
}
|
||||
if !rowsResp.Success || rowsResp.ChunkType != agentChunkRows || len(rowsResp.Data) != 2 {
|
||||
t.Fatalf("unexpected rows response: %#v", rowsResp)
|
||||
}
|
||||
if got := rowsResp.Data[1][1]; got != "bob" {
|
||||
t.Fatalf("unexpected streamed row payload: %v", rowsResp.Data)
|
||||
}
|
||||
|
||||
var doneResp struct {
|
||||
Success bool `json:"success"`
|
||||
ChunkType string `json:"chunkType"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(lines[2]), &doneResp); err != nil {
|
||||
t.Fatalf("decode done response failed: %v", err)
|
||||
}
|
||||
if !doneResp.Success || doneResp.ChunkType != agentChunkDone {
|
||||
t.Fatalf("unexpected done response: %#v", doneResp)
|
||||
}
|
||||
if trimmed != 0 {
|
||||
t.Fatalf("小流式任务不应触发内存回收,got=%d", trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaybeReleaseAgentMemory_TriggersTrimForLargeJobs(t *testing.T) {
|
||||
originalAsync := runAgentMemoryTrimAsync
|
||||
originalTrim := agentMemoryTrimFn
|
||||
originalLastAt := agentMemoryTrimLastAt.Load()
|
||||
t.Cleanup(func() {
|
||||
runAgentMemoryTrimAsync = originalAsync
|
||||
agentMemoryTrimFn = originalTrim
|
||||
agentMemoryTrimRunning.Store(false)
|
||||
agentMemoryTrimLastAt.Store(originalLastAt)
|
||||
})
|
||||
|
||||
agentMemoryTrimRunning.Store(false)
|
||||
agentMemoryTrimLastAt.Store(0)
|
||||
triggered := 0
|
||||
runAgentMemoryTrimAsync = func(fn func()) {
|
||||
fn()
|
||||
}
|
||||
agentMemoryTrimFn = func() {
|
||||
triggered++
|
||||
}
|
||||
|
||||
maybeReleaseAgentMemory("test-large-query", agentMemoryTrimRowsThreshold)
|
||||
|
||||
if triggered != 1 {
|
||||
t.Fatalf("大查询完成后应触发一次内存回收,got=%d", triggered)
|
||||
}
|
||||
}
|
||||
|
||||
12
cmd/optional-driver-agent/provider_elasticsearch.go
Normal file
@@ -0,0 +1,12 @@
|
||||
//go:build gonavi_elasticsearch_driver
|
||||
|
||||
package main
|
||||
|
||||
import "GoNavi-Wails/internal/db"
|
||||
|
||||
func init() {
|
||||
agentDriverType = "elasticsearch"
|
||||
agentDatabaseFactory = func() db.Database {
|
||||
return &db.ElasticsearchDB{}
|
||||
}
|
||||
}
|
||||
12
cmd/optional-driver-agent/provider_gaussdb.go
Normal file
@@ -0,0 +1,12 @@
|
||||
//go:build gonavi_gaussdb_driver
|
||||
|
||||
package main
|
||||
|
||||
import "GoNavi-Wails/internal/db"
|
||||
|
||||
func init() {
|
||||
agentDriverType = "gaussdb"
|
||||
agentDatabaseFactory = func() db.Database {
|
||||
return &db.GaussDB{}
|
||||
}
|
||||
}
|
||||
12
cmd/optional-driver-agent/provider_iotdb.go
Normal file
@@ -0,0 +1,12 @@
|
||||
//go:build gonavi_iotdb_driver
|
||||
|
||||
package main
|
||||
|
||||
import "GoNavi-Wails/internal/db"
|
||||
|
||||
func init() {
|
||||
agentDriverType = "iotdb"
|
||||
agentDatabaseFactory = func() db.Database {
|
||||
return &db.IoTDBDB{}
|
||||
}
|
||||
}
|
||||
12
cmd/optional-driver-agent/provider_iris.go
Normal file
@@ -0,0 +1,12 @@
|
||||
//go:build gonavi_iris_driver
|
||||
|
||||
package main
|
||||
|
||||
import "GoNavi-Wails/internal/db"
|
||||
|
||||
func init() {
|
||||
agentDriverType = "iris"
|
||||
agentDatabaseFactory = func() db.Database {
|
||||
return &db.IrisDB{}
|
||||
}
|
||||
}
|
||||
12
cmd/optional-driver-agent/provider_starrocks.go
Normal file
@@ -0,0 +1,12 @@
|
||||
//go:build gonavi_starrocks_driver
|
||||
|
||||
package main
|
||||
|
||||
import "GoNavi-Wails/internal/db"
|
||||
|
||||
func init() {
|
||||
agentDriverType = "starrocks"
|
||||
agentDatabaseFactory = func() db.Database {
|
||||
return &db.StarRocksDB{}
|
||||
}
|
||||
}
|
||||
12
cmd/optional-driver-agent/provider_trino.go
Normal file
@@ -0,0 +1,12 @@
|
||||
//go:build gonavi_trino_driver
|
||||
|
||||
package main
|
||||
|
||||
import "GoNavi-Wails/internal/db"
|
||||
|
||||
func init() {
|
||||
agentDriverType = "trino"
|
||||
agentDatabaseFactory = func() db.Database {
|
||||
return &db.TrinoDB{}
|
||||
}
|
||||
}
|
||||
7
deploy/helm/gonavi-mcp-server/Chart.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
apiVersion: v2
|
||||
name: gonavi-mcp-server
|
||||
description: Helm chart for deploying GoNavi MCP Server
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "latest"
|
||||
kubeVersion: ">=1.24.0-0"
|
||||
94
deploy/helm/gonavi-mcp-server/README.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# GoNavi MCP Server Helm Chart
|
||||
|
||||
这个 Chart 用来部署 `gonavi-mcp-server`,覆盖当前仓库 K8s 示例里的几类常见场景:
|
||||
|
||||
- 基于现有 Secret 直接部署
|
||||
- 通过 Helm values 内联创建测试用 Secret
|
||||
- 启用 Ingress
|
||||
- 追加 `drivers/` PVC
|
||||
- NAS / k3s 单机节点直接挂宿主机数据目录
|
||||
|
||||
Chart 目录:
|
||||
|
||||
- `Chart.yaml`
|
||||
- `values.yaml`
|
||||
- `values-examples/*.yaml`
|
||||
- `templates/*.yaml`
|
||||
|
||||
## 1. 默认安装
|
||||
|
||||
默认值假设你已经有一个现成 Secret:
|
||||
|
||||
- 名称:`gonavi-mcp-server-data`
|
||||
- 至少包含:
|
||||
- `connections.json`
|
||||
- `daily_secrets.json`
|
||||
- `GONAVI_MCP_HTTP_TOKEN`
|
||||
|
||||
安装命令:
|
||||
|
||||
```bash
|
||||
helm upgrade --install gonavi-mcp-server deploy/helm/gonavi-mcp-server -n gonavi --create-namespace
|
||||
```
|
||||
|
||||
## 2. 直接创建测试用 Secret
|
||||
|
||||
如果只是本地验证模板或快速试跑,可以使用示例 values:
|
||||
|
||||
```bash
|
||||
helm upgrade --install gonavi-mcp-server deploy/helm/gonavi-mcp-server \
|
||||
-n gonavi --create-namespace \
|
||||
-f deploy/helm/gonavi-mcp-server/values-examples/inline-secret.yaml
|
||||
```
|
||||
|
||||
这个示例只适合最小联调,不适合真实生产连接数据。
|
||||
|
||||
## 3. 常用示例
|
||||
|
||||
启用 Ingress:
|
||||
|
||||
```bash
|
||||
helm upgrade --install gonavi-mcp-server deploy/helm/gonavi-mcp-server \
|
||||
-n gonavi --create-namespace \
|
||||
-f deploy/helm/gonavi-mcp-server/values-examples/ingress.yaml
|
||||
```
|
||||
|
||||
挂载 drivers PVC:
|
||||
|
||||
```bash
|
||||
helm upgrade --install gonavi-mcp-server deploy/helm/gonavi-mcp-server \
|
||||
-n gonavi --create-namespace \
|
||||
-f deploy/helm/gonavi-mcp-server/values-examples/drivers-pvc.yaml
|
||||
```
|
||||
|
||||
同时启用 Ingress 与 drivers PVC:
|
||||
|
||||
```bash
|
||||
helm upgrade --install gonavi-mcp-server deploy/helm/gonavi-mcp-server \
|
||||
-n gonavi --create-namespace \
|
||||
-f deploy/helm/gonavi-mcp-server/values-examples/ingress-with-drivers-pvc.yaml
|
||||
```
|
||||
|
||||
NAS / k3s hostPath:
|
||||
|
||||
```bash
|
||||
helm upgrade --install gonavi-mcp-server deploy/helm/gonavi-mcp-server \
|
||||
-n gonavi --create-namespace \
|
||||
-f deploy/helm/gonavi-mcp-server/values-examples/nas-hostpath.yaml
|
||||
```
|
||||
|
||||
## 4. 关键参数
|
||||
|
||||
- `image.repository` / `image.tag`
|
||||
- `secret.create`
|
||||
- `secret.name`
|
||||
- `data.mode=secret|hostPath|pvc`
|
||||
- `drivers.mode=none|pvc|hostPath`
|
||||
- `ingress.enabled`
|
||||
|
||||
## 5. 约束
|
||||
|
||||
- `data.mode=secret` 时,Secret 必须能提供 `connections.json` 和 `daily_secrets.json`
|
||||
- `data.mode=hostPath` 时,宿主机目录需要包含 GoNavi 活动数据目录内容
|
||||
- `drivers.mode!=none` 时,会在 `/data/drivers` 追加独立挂载
|
||||
- Chart 默认只部署 MCP Server,不部署桌面 GUI
|
||||
49
deploy/helm/gonavi-mcp-server/templates/_helpers.tpl
Normal file
@@ -0,0 +1,49 @@
|
||||
{{- define "gonavi-mcp-server.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "gonavi-mcp-server.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := include "gonavi-mcp-server.name" . -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "gonavi-mcp-server.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "gonavi-mcp-server.labels" -}}
|
||||
helm.sh/chart: {{ include "gonavi-mcp-server.chart" . }}
|
||||
app.kubernetes.io/name: {{ include "gonavi-mcp-server.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "gonavi-mcp-server.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "gonavi-mcp-server.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "gonavi-mcp-server.namespace" -}}
|
||||
{{- default .Release.Namespace .Values.namespaceOverride -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "gonavi-mcp-server.secretName" -}}
|
||||
{{- if .Values.secret.name -}}
|
||||
{{- .Values.secret.name -}}
|
||||
{{- else -}}
|
||||
{{- include "gonavi-mcp-server.fullname" . -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "gonavi-mcp-server.serviceName" -}}
|
||||
{{- include "gonavi-mcp-server.fullname" . -}}
|
||||
{{- end -}}
|
||||
130
deploy/helm/gonavi-mcp-server/templates/deployment.yaml
Normal file
@@ -0,0 +1,130 @@
|
||||
{{- $dataMode := .Values.data.mode -}}
|
||||
{{- $driversMode := .Values.drivers.mode -}}
|
||||
{{- if not (or (eq $dataMode "secret") (eq $dataMode "hostPath") (eq $dataMode "pvc")) -}}
|
||||
{{- fail "data.mode must be one of: secret, hostPath, pvc" -}}
|
||||
{{- end -}}
|
||||
{{- if not (or (eq $driversMode "none") (eq $driversMode "pvc") (eq $driversMode "hostPath")) -}}
|
||||
{{- fail "drivers.mode must be one of: none, pvc, hostPath" -}}
|
||||
{{- end -}}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "gonavi-mcp-server.fullname" . }}
|
||||
namespace: {{ include "gonavi-mcp-server.namespace" . }}
|
||||
labels:
|
||||
{{- include "gonavi-mcp-server.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "gonavi-mcp-server.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "gonavi-mcp-server.selectorLabels" . | nindent 8 }}
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: gonavi-mcp-server
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
args:
|
||||
- http
|
||||
{{- range .Values.mcp.extraArgs }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.port }}
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: GONAVI_DATA_ROOT
|
||||
value: {{ .Values.mcp.dataRoot | quote }}
|
||||
- name: GONAVI_LOG_DIR
|
||||
value: {{ .Values.mcp.logDir | quote }}
|
||||
- name: GONAVI_MCP_HTTP_ADDR
|
||||
value: {{ .Values.mcp.httpAddr | quote }}
|
||||
- name: GONAVI_MCP_HTTP_PATH
|
||||
value: {{ .Values.mcp.httpPath | quote }}
|
||||
- name: GONAVI_MCP_SCHEMA_ONLY
|
||||
value: {{ ternary "true" "false" .Values.mcp.schemaOnly | quote }}
|
||||
- name: GONAVI_MCP_HTTP_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "gonavi-mcp-server.secretName" . }}
|
||||
key: {{ .Values.secret.tokenKey }}
|
||||
{{- with .Values.mcp.extraEnv }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: gonavi-data
|
||||
mountPath: {{ .Values.data.mountPath | quote }}
|
||||
readOnly: true
|
||||
{{- if ne $driversMode "none" }}
|
||||
- name: gonavi-drivers
|
||||
mountPath: {{ .Values.drivers.mountPath | quote }}
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
initialDelaySeconds: 3
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 20
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.containerSecurityContext | nindent 12 }}
|
||||
volumes:
|
||||
- name: gonavi-data
|
||||
{{- if eq $dataMode "secret" }}
|
||||
projected:
|
||||
sources:
|
||||
- secret:
|
||||
name: {{ include "gonavi-mcp-server.secretName" . }}
|
||||
items:
|
||||
- key: {{ .Values.secret.connectionsJsonKey }}
|
||||
path: connections.json
|
||||
- key: {{ .Values.secret.dailySecretsJsonKey }}
|
||||
path: daily_secrets.json
|
||||
{{- else if eq $dataMode "hostPath" }}
|
||||
hostPath:
|
||||
path: {{ required "data.hostPath.path is required when data.mode=hostPath" .Values.data.hostPath.path | quote }}
|
||||
type: {{ default "Directory" .Values.data.hostPath.type | quote }}
|
||||
{{- else if eq $dataMode "pvc" }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ required "data.pvc.claimName is required when data.mode=pvc" .Values.data.pvc.claimName | quote }}
|
||||
{{- end }}
|
||||
{{- if eq $driversMode "pvc" }}
|
||||
- name: gonavi-drivers
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ required "drivers.pvc.claimName is required when drivers.mode=pvc" .Values.drivers.pvc.claimName | quote }}
|
||||
{{- else if eq $driversMode "hostPath" }}
|
||||
- name: gonavi-drivers
|
||||
hostPath:
|
||||
path: {{ required "drivers.hostPath.path is required when drivers.mode=hostPath" .Values.drivers.hostPath.path | quote }}
|
||||
type: {{ default "Directory" .Values.drivers.hostPath.type | quote }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
34
deploy/helm/gonavi-mcp-server/templates/ingress.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
{{- if .Values.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "gonavi-mcp-server.fullname" . }}
|
||||
namespace: {{ include "gonavi-mcp-server.namespace" . }}
|
||||
labels:
|
||||
{{- include "gonavi-mcp-server.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
ingressClassName: {{ .Values.ingress.className | quote }}
|
||||
{{- with .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path | quote }}
|
||||
pathType: {{ .pathType | quote }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "gonavi-mcp-server.serviceName" $ }}
|
||||
port:
|
||||
number: {{ $.Values.service.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
27
deploy/helm/gonavi-mcp-server/templates/secret.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
{{- if .Values.secret.create }}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "gonavi-mcp-server.secretName" . }}
|
||||
namespace: {{ include "gonavi-mcp-server.namespace" . }}
|
||||
labels:
|
||||
{{- include "gonavi-mcp-server.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
stringData:
|
||||
{{ .Values.secret.tokenKey }}: {{ required "secret.stringData.token is required when secret.create=true" .Values.secret.stringData.token | quote }}
|
||||
{{- if eq .Values.data.mode "secret" }}
|
||||
{{ .Values.secret.connectionsJsonKey }}: |
|
||||
{{ required "secret.stringData.connectionsJson is required when data.mode=secret and secret.create=true" .Values.secret.stringData.connectionsJson | nindent 4 }}
|
||||
{{ .Values.secret.dailySecretsJsonKey }}: |
|
||||
{{ required "secret.stringData.dailySecretsJson is required when data.mode=secret and secret.create=true" .Values.secret.stringData.dailySecretsJson | nindent 4 }}
|
||||
{{- else }}
|
||||
{{- if .Values.secret.stringData.connectionsJson }}
|
||||
{{ .Values.secret.connectionsJsonKey }}: |
|
||||
{{ .Values.secret.stringData.connectionsJson | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- if .Values.secret.stringData.dailySecretsJson }}
|
||||
{{ .Values.secret.dailySecretsJsonKey }}: |
|
||||
{{ .Values.secret.stringData.dailySecretsJson | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
20
deploy/helm/gonavi-mcp-server/templates/service.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "gonavi-mcp-server.serviceName" . }}
|
||||
namespace: {{ include "gonavi-mcp-server.namespace" . }}
|
||||
labels:
|
||||
{{- include "gonavi-mcp-server.labels" . | nindent 4 }}
|
||||
{{- with .Values.service.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
selector:
|
||||
{{- include "gonavi-mcp-server.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
@@ -0,0 +1,4 @@
|
||||
drivers:
|
||||
mode: pvc
|
||||
pvc:
|
||||
claimName: gonavi-drivers
|
||||
@@ -0,0 +1,19 @@
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
hosts:
|
||||
- host: gonavi-mcp.example.com
|
||||
paths:
|
||||
- path: /mcp
|
||||
pathType: Prefix
|
||||
- path: /healthz
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: gonavi-mcp-server-tls
|
||||
hosts:
|
||||
- gonavi-mcp.example.com
|
||||
|
||||
drivers:
|
||||
mode: pvc
|
||||
pvc:
|
||||
claimName: gonavi-drivers
|
||||
14
deploy/helm/gonavi-mcp-server/values-examples/ingress.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
hosts:
|
||||
- host: gonavi-mcp.example.com
|
||||
paths:
|
||||
- path: /mcp
|
||||
pathType: Prefix
|
||||
- path: /healthz
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: gonavi-mcp-server-tls
|
||||
hosts:
|
||||
- gonavi-mcp.example.com
|
||||
@@ -0,0 +1,13 @@
|
||||
secret:
|
||||
create: true
|
||||
stringData:
|
||||
token: replace-with-a-random-token
|
||||
connectionsJson: |
|
||||
{
|
||||
"connections": []
|
||||
}
|
||||
dailySecretsJson: |
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"connections": {}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
data:
|
||||
mode: hostPath
|
||||
hostPath:
|
||||
path: /volume1/docker/gonavi/data
|
||||
type: Directory
|
||||
|
||||
secret:
|
||||
create: true
|
||||
stringData:
|
||||
token: replace-with-a-random-token
|
||||
99
deploy/helm/gonavi-mcp-server/values.yaml
Normal file
@@ -0,0 +1,99 @@
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
namespaceOverride: ""
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: ghcr.io/syngnat/gonavi-mcp-server
|
||||
tag: latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 8765
|
||||
annotations: {}
|
||||
|
||||
mcp:
|
||||
dataRoot: /data
|
||||
logDir: /var/lib/gonavi/logs
|
||||
httpAddr: 0.0.0.0:8765
|
||||
httpPath: /mcp
|
||||
schemaOnly: true
|
||||
extraArgs: []
|
||||
extraEnv: []
|
||||
|
||||
secret:
|
||||
create: false
|
||||
name: gonavi-mcp-server-data
|
||||
tokenKey: GONAVI_MCP_HTTP_TOKEN
|
||||
connectionsJsonKey: connections.json
|
||||
dailySecretsJsonKey: daily_secrets.json
|
||||
stringData:
|
||||
token: ""
|
||||
connectionsJson: ""
|
||||
dailySecretsJson: ""
|
||||
|
||||
data:
|
||||
mode: secret
|
||||
mountPath: /data
|
||||
hostPath:
|
||||
path: /volume1/docker/gonavi/data
|
||||
type: Directory
|
||||
pvc:
|
||||
claimName: ""
|
||||
|
||||
drivers:
|
||||
mode: none
|
||||
mountPath: /data/drivers
|
||||
pvc:
|
||||
claimName: gonavi-drivers
|
||||
hostPath:
|
||||
path: /volume1/docker/gonavi/drivers
|
||||
type: Directory
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
className: nginx
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
||||
hosts:
|
||||
- host: gonavi-mcp.example.com
|
||||
paths:
|
||||
- path: /mcp
|
||||
pathType: Prefix
|
||||
- path: /healthz
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: gonavi-mcp-server-tls
|
||||
hosts:
|
||||
- gonavi-mcp.example.com
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
|
||||
podAnnotations: {}
|
||||
|
||||
podSecurityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 10001
|
||||
runAsGroup: 10001
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
|
||||
containerSecurityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
||||
151
deploy/k8s/gonavi-mcp-server/README.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# GoNavi MCP Server Kubernetes 示例
|
||||
|
||||
这个目录提供 `gonavi-mcp-server` 的最小 K8s 部署清单,适合:
|
||||
|
||||
- Linux 服务器上的单集群部署
|
||||
- NAS 自带 K8s / k3s / k8s-lite 环境
|
||||
- 需要把 GoNavi MCP 通过 Ingress / Gateway 暴露给远端 Agent 的场景
|
||||
|
||||
如果你希望通过 values 参数统一控制镜像、Secret、Ingress、PVC/hostPath,而不是维护多份 overlay,请直接使用 [deploy/helm/gonavi-mcp-server](../../helm/gonavi-mcp-server)。
|
||||
|
||||
目录结构:
|
||||
|
||||
- `kustomization.yaml`:基础部署入口
|
||||
- `base/kustomization.yaml`:基础资源集合
|
||||
- `overlays/nas-hostpath`:直接挂 NAS / 单机节点上的 GoNavi 数据目录
|
||||
- `base/deployment.yaml` / `base/service.yaml`:基础资源
|
||||
- `overlays/drivers-pvc`:为 `/data/drivers` 增加 PVC 挂载
|
||||
- `overlays/ingress`:增加 Ingress 暴露 `/mcp`
|
||||
- `overlays/ingress-with-drivers-pvc`:同时启用 Ingress 和 drivers PVC
|
||||
|
||||
## 前提
|
||||
|
||||
容器仍然依赖 GoNavi 的活动数据目录。至少要准备:
|
||||
|
||||
- `connections.json`
|
||||
- `daily_secrets.json`
|
||||
|
||||
如果目标连接依赖可选 driver agent,还需要额外挂载 `/data/drivers`。
|
||||
|
||||
`base/deployment.yaml` 默认镜像指向:
|
||||
|
||||
```text
|
||||
ghcr.io/syngnat/gonavi-mcp-server:latest
|
||||
```
|
||||
|
||||
如果你要跟随 `dev` 分支,可以改成:
|
||||
|
||||
```text
|
||||
ghcr.io/syngnat/gonavi-mcp-server:dev-latest
|
||||
```
|
||||
|
||||
如果你要使用自建镜像,再按下面方式覆盖 `image`:
|
||||
|
||||
- 在当前节点可访问的 Docker / Podman / containerd 环境中构建并导入这个镜像
|
||||
- 把镜像推送到你的私有仓库,并同步修改 `base/deployment.yaml` 里的 `image`
|
||||
|
||||
例如:
|
||||
|
||||
```bash
|
||||
docker build -f Dockerfile.mcp-server -t gonavi-mcp-server:local .
|
||||
# or
|
||||
podman build -f Dockerfile.mcp-server -t localhost/gonavi-mcp-server:local .
|
||||
```
|
||||
|
||||
## 1. 创建 Secret
|
||||
|
||||
推荐直接从现有 GoNavi 数据文件生成:
|
||||
|
||||
```bash
|
||||
kubectl create secret generic gonavi-mcp-server-data \
|
||||
--from-file=connections.json=/absolute/path/to/gonavi-data/connections.json \
|
||||
--from-file=daily_secrets.json=/absolute/path/to/gonavi-data/daily_secrets.json \
|
||||
--from-literal=GONAVI_MCP_HTTP_TOKEN=replace-with-a-random-token
|
||||
```
|
||||
|
||||
如需单独 namespace,请先执行:
|
||||
|
||||
```bash
|
||||
kubectl create namespace gonavi
|
||||
kubectl -n gonavi create secret generic gonavi-mcp-server-data \
|
||||
--from-file=connections.json=/absolute/path/to/gonavi-data/connections.json \
|
||||
--from-file=daily_secrets.json=/absolute/path/to/gonavi-data/daily_secrets.json \
|
||||
--from-literal=GONAVI_MCP_HTTP_TOKEN=replace-with-a-random-token
|
||||
```
|
||||
|
||||
## 2. 应用清单
|
||||
|
||||
```bash
|
||||
kubectl apply -k deploy/k8s/gonavi-mcp-server
|
||||
```
|
||||
|
||||
如果使用独立 namespace,请先把 YAML 里的 `namespace` 改成你的目标值,或自行用 `kustomize` / Helm 做二次封装。
|
||||
|
||||
清单内已经带了基础安全上下文和默认资源配额;如果你的连接数量、对象规模或并发访问更高,建议按实际负载调整 `resources`。
|
||||
|
||||
### 常用 Overlay
|
||||
|
||||
仅基础部署:
|
||||
|
||||
```bash
|
||||
kubectl apply -k deploy/k8s/gonavi-mcp-server
|
||||
```
|
||||
|
||||
需要可选 driver agent:
|
||||
|
||||
```bash
|
||||
kubectl apply -k deploy/k8s/gonavi-mcp-server/overlays/drivers-pvc
|
||||
```
|
||||
|
||||
NAS / k3s 单机节点直接挂宿主机目录:
|
||||
|
||||
```bash
|
||||
kubectl apply -k deploy/k8s/gonavi-mcp-server/overlays/nas-hostpath
|
||||
```
|
||||
|
||||
需要对外暴露 `/mcp`:
|
||||
|
||||
```bash
|
||||
kubectl apply -k deploy/k8s/gonavi-mcp-server/overlays/ingress
|
||||
```
|
||||
|
||||
同时需要 Ingress 与 drivers PVC:
|
||||
|
||||
```bash
|
||||
kubectl apply -k deploy/k8s/gonavi-mcp-server/overlays/ingress-with-drivers-pvc
|
||||
```
|
||||
|
||||
应用 overlay 前,记得先改里面的占位值,例如:
|
||||
|
||||
- `overlays/ingress/ingress.yaml` 里的域名、TLS Secret、`ingressClassName`
|
||||
- `overlays/drivers-pvc/patch-deployment.yaml` 里的 PVC 名称 `gonavi-drivers`
|
||||
- `overlays/nas-hostpath/patch-deployment.yaml` 里的宿主机目录 `/volume1/docker/gonavi/data`
|
||||
|
||||
`nas-hostpath` overlay 会直接把整个 GoNavi 数据目录挂到 `/data`,因此 `connections.json`、`daily_secrets.json`、`drivers/` 都从宿主机目录读取;此时仍建议保留 `GONAVI_MCP_HTTP_TOKEN` 的 Secret 注入,不要把 token 直接硬编码进 Deployment。
|
||||
|
||||
## 3. 校验
|
||||
|
||||
```bash
|
||||
kubectl get pods -n gonavi
|
||||
kubectl get svc -n gonavi
|
||||
kubectl port-forward -n gonavi svc/gonavi-mcp-server 8765:8765
|
||||
curl -H "Authorization: Bearer replace-with-a-random-token" http://127.0.0.1:8765/healthz
|
||||
```
|
||||
|
||||
`/healthz` 返回 `ok` 说明 Pod 已对外提供 HTTP 服务。
|
||||
|
||||
## 4. 暴露给远端 Agent
|
||||
|
||||
- 集群内使用:直接访问 `http://gonavi-mcp-server.gonavi.svc.cluster.local:8765/mcp`
|
||||
- 集群外使用:通过 Ingress / Gateway / 反向代理暴露 `/mcp`
|
||||
- 远端 Agent 请求头:`Authorization: Bearer <你的 token>`
|
||||
|
||||
## 5. drivers 挂载
|
||||
|
||||
当前示例默认只挂载 `connections.json` 与 `daily_secrets.json`。如果保存连接里包含 Dameng、ClickHouse、DuckDB 等依赖 driver agent 的数据源,请在 Deployment 中额外补一个卷,把宿主机或 PVC 中的 `drivers/` 目录挂载到:
|
||||
|
||||
```text
|
||||
/data/drivers
|
||||
```
|
||||
|
||||
否则对应连接会因为缺少 driver agent 无法使用。
|
||||
89
deploy/k8s/gonavi-mcp-server/base/deployment.yaml
Normal file
@@ -0,0 +1,89 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: gonavi-mcp-server
|
||||
namespace: gonavi
|
||||
labels:
|
||||
app.kubernetes.io/name: gonavi-mcp-server
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: gonavi-mcp-server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: gonavi-mcp-server
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 10001
|
||||
runAsGroup: 10001
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: gonavi-mcp-server
|
||||
# 如需跟随 dev 分支,可改成 ghcr.io/syngnat/gonavi-mcp-server:dev-latest
|
||||
image: ghcr.io/syngnat/gonavi-mcp-server:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- http
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8765
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: GONAVI_DATA_ROOT
|
||||
value: /data
|
||||
- name: GONAVI_LOG_DIR
|
||||
value: /var/lib/gonavi/logs
|
||||
- name: GONAVI_MCP_HTTP_ADDR
|
||||
value: 0.0.0.0:8765
|
||||
- name: GONAVI_MCP_HTTP_PATH
|
||||
value: /mcp
|
||||
- name: GONAVI_MCP_SCHEMA_ONLY
|
||||
value: "true"
|
||||
- name: GONAVI_MCP_HTTP_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: gonavi-mcp-server-data
|
||||
key: GONAVI_MCP_HTTP_TOKEN
|
||||
volumeMounts:
|
||||
- name: gonavi-data
|
||||
mountPath: /data
|
||||
readOnly: true
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
initialDelaySeconds: 3
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: http
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 20
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
volumes:
|
||||
- name: gonavi-data
|
||||
projected:
|
||||
sources:
|
||||
- secret:
|
||||
name: gonavi-mcp-server-data
|
||||
items:
|
||||
- key: connections.json
|
||||
path: connections.json
|
||||
- key: daily_secrets.json
|
||||
path: daily_secrets.json
|
||||
6
deploy/k8s/gonavi-mcp-server/base/kustomization.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
15
deploy/k8s/gonavi-mcp-server/base/service.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: gonavi-mcp-server
|
||||
namespace: gonavi
|
||||
labels:
|
||||
app.kubernetes.io/name: gonavi-mcp-server
|
||||
spec:
|
||||
selector:
|
||||
app.kubernetes.io/name: gonavi-mcp-server
|
||||
ports:
|
||||
- name: http
|
||||
port: 8765
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
5
deploy/k8s/gonavi-mcp-server/kustomization.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- base
|
||||
@@ -0,0 +1,8 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- ../../base
|
||||
|
||||
patches:
|
||||
- path: patch-deployment.yaml
|
||||
@@ -0,0 +1,18 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: gonavi-mcp-server
|
||||
namespace: gonavi
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: gonavi-mcp-server
|
||||
volumeMounts:
|
||||
- name: gonavi-drivers
|
||||
mountPath: /data/drivers
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: gonavi-drivers
|
||||
persistentVolumeClaim:
|
||||
claimName: gonavi-drivers
|
||||
@@ -0,0 +1,33 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: gonavi-mcp-server
|
||||
namespace: gonavi
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- gonavi-mcp.example.com
|
||||
secretName: gonavi-mcp-server-tls
|
||||
rules:
|
||||
- host: gonavi-mcp.example.com
|
||||
http:
|
||||
paths:
|
||||
- path: /mcp
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: gonavi-mcp-server
|
||||
port:
|
||||
number: 8765
|
||||
- path: /healthz
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: gonavi-mcp-server
|
||||
port:
|
||||
number: 8765
|
||||
@@ -0,0 +1,9 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- ../../base
|
||||
- ingress.yaml
|
||||
|
||||
patches:
|
||||
- path: patch-deployment.yaml
|
||||
@@ -0,0 +1,18 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: gonavi-mcp-server
|
||||
namespace: gonavi
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: gonavi-mcp-server
|
||||
volumeMounts:
|
||||
- name: gonavi-drivers
|
||||
mountPath: /data/drivers
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: gonavi-drivers
|
||||
persistentVolumeClaim:
|
||||
claimName: gonavi-drivers
|
||||
33
deploy/k8s/gonavi-mcp-server/overlays/ingress/ingress.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: gonavi-mcp-server
|
||||
namespace: gonavi
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- gonavi-mcp.example.com
|
||||
secretName: gonavi-mcp-server-tls
|
||||
rules:
|
||||
- host: gonavi-mcp.example.com
|
||||
http:
|
||||
paths:
|
||||
- path: /mcp
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: gonavi-mcp-server
|
||||
port:
|
||||
number: 8765
|
||||
- path: /healthz
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: gonavi-mcp-server
|
||||
port:
|
||||
number: 8765
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- ../../base
|
||||
- ingress.yaml
|
||||
@@ -0,0 +1,8 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- ../../base
|
||||
|
||||
patches:
|
||||
- path: patch-deployment.yaml
|
||||
@@ -0,0 +1,14 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: gonavi-mcp-server
|
||||
namespace: gonavi
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
volumes:
|
||||
- $patch: replace
|
||||
name: gonavi-data
|
||||
hostPath:
|
||||
path: /volume1/docker/gonavi/data
|
||||
type: Directory
|
||||
101
deploy/podman/gonavi-mcp-server/README.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# GoNavi MCP Server Podman 示例
|
||||
|
||||
这个目录提供 `gonavi-mcp-server` 的 Podman 原生部署入口,覆盖两类常见场景:
|
||||
|
||||
- 直接用 `podman run` 在 Linux 服务器 / NAS 上启动
|
||||
- 用 rootless Quadlet + systemd 做常驻服务
|
||||
|
||||
目录内容:
|
||||
|
||||
- `gonavi-mcp-server.env.example`:给 `podman run` / Quadlet 共用的容器环境变量示例
|
||||
- `gonavi-mcp-server.container`:Quadlet 示例
|
||||
|
||||
前提仍然一样:宿主机 GoNavi 活动数据目录内至少应包含:
|
||||
|
||||
- `connections.json`
|
||||
- `daily_secrets.json`
|
||||
|
||||
如果目标连接依赖可选 driver agent,还要保证同一数据目录下已有 `drivers/`。
|
||||
|
||||
## 1. 直接运行已发布镜像
|
||||
|
||||
```bash
|
||||
cp deploy/podman/gonavi-mcp-server/gonavi-mcp-server.env.example ./gonavi-mcp-server.env
|
||||
```
|
||||
|
||||
把 `GONAVI_MCP_HTTP_TOKEN` 改成随机值后运行:
|
||||
|
||||
```bash
|
||||
podman run -d --name gonavi-mcp-server --replace \
|
||||
-p 8765:8765 \
|
||||
--env-file ./gonavi-mcp-server.env \
|
||||
-v /absolute/path/to/gonavi-data:/data:Z \
|
||||
ghcr.io/syngnat/gonavi-mcp-server:latest http
|
||||
```
|
||||
|
||||
查看状态:
|
||||
|
||||
```bash
|
||||
podman ps
|
||||
podman logs -f gonavi-mcp-server
|
||||
```
|
||||
|
||||
如果你的宿主机没有启用 SELinux,可把挂载参数末尾的 `:Z` 去掉;如果启用了 SELinux,建议保留它。
|
||||
|
||||
## 2. 本地源码构建镜像
|
||||
|
||||
仓库根目录已经提供 `.containerignore`,Podman 会优先读取它来裁剪构建上下文。
|
||||
|
||||
```bash
|
||||
podman build -f Dockerfile.mcp-server -t localhost/gonavi-mcp-server:local .
|
||||
podman run -d --name gonavi-mcp-server --replace \
|
||||
-p 8765:8765 \
|
||||
--env-file ./gonavi-mcp-server.env \
|
||||
-v /absolute/path/to/gonavi-data:/data:Z \
|
||||
localhost/gonavi-mcp-server:local http
|
||||
```
|
||||
|
||||
## 3. Rootless Quadlet 常驻服务
|
||||
|
||||
Podman Quadlet 适合 Linux 服务器 / NAS 上做 rootless 常驻服务。
|
||||
|
||||
把文件放到用户级目录:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.config/containers/systemd
|
||||
cp deploy/podman/gonavi-mcp-server/gonavi-mcp-server.container ~/.config/containers/systemd/
|
||||
cp deploy/podman/gonavi-mcp-server/gonavi-mcp-server.env.example ~/.config/containers/systemd/gonavi-mcp-server.env
|
||||
```
|
||||
|
||||
然后修改两处:
|
||||
|
||||
- `~/.config/containers/systemd/gonavi-mcp-server.container` 里的 `Volume=/absolute/path/to/gonavi-data:/data:Z`
|
||||
- `~/.config/containers/systemd/gonavi-mcp-server.env` 里的 `GONAVI_MCP_HTTP_TOKEN`
|
||||
|
||||
启动服务:
|
||||
|
||||
```bash
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now gonavi-mcp-server.service
|
||||
systemctl --user status gonavi-mcp-server.service
|
||||
journalctl --user -u gonavi-mcp-server.service -f
|
||||
```
|
||||
|
||||
如果你希望用户退出登录后服务仍然保持运行,再执行:
|
||||
|
||||
```bash
|
||||
loginctl enable-linger "$USER"
|
||||
```
|
||||
|
||||
Rootless Quadlet 默认搜索路径是 `~/.config/containers/systemd/`;如果你要做系统级 rootful 部署,可改放到 `/etc/containers/systemd/`。
|
||||
|
||||
## 4. 关于 Compose
|
||||
|
||||
Podman 的 `podman compose` 依赖外部 compose provider。也就是说,是否能直接复用仓库根目录的 `docker-compose.mcp-server.yml`,取决于你的 Podman 环境是否已经安装并配置了对应 provider。
|
||||
|
||||
因此,这个仓库对 Podman 的主支持路径是:
|
||||
|
||||
- `podman run`
|
||||
- Quadlet(推荐长期运行)
|
||||
|
||||
如果你的环境已经有可用的 compose provider,再去复用根目录 Compose 文件即可。
|
||||
22
deploy/podman/gonavi-mcp-server/gonavi-mcp-server.container
Normal file
@@ -0,0 +1,22 @@
|
||||
[Unit]
|
||||
Description=GoNavi MCP Server (rootless Podman)
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Container]
|
||||
ContainerName=gonavi-mcp-server
|
||||
Image=ghcr.io/syngnat/gonavi-mcp-server:latest
|
||||
Pull=newer
|
||||
EnvironmentFile=./gonavi-mcp-server.env
|
||||
# 修改成你的 GoNavi 活动数据目录;SELinux 主机建议保留 :Z
|
||||
Volume=/absolute/path/to/gonavi-data:/data:Z
|
||||
PublishPort=8765:8765
|
||||
Exec=http
|
||||
LogDriver=journald
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
TimeoutStartSec=900
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,15 @@
|
||||
# 容器内 GoNavi 活动数据根目录。通常不需要修改;Volume= 已把宿主机目录挂到 /data。
|
||||
GONAVI_DATA_ROOT=/data
|
||||
|
||||
# 日志目录。默认与镜像内目录保持一致。
|
||||
GONAVI_LOG_DIR=/var/lib/gonavi/logs
|
||||
|
||||
# MCP HTTP 监听地址与路径。
|
||||
GONAVI_MCP_HTTP_ADDR=0.0.0.0:8765
|
||||
GONAVI_MCP_HTTP_PATH=/mcp
|
||||
|
||||
# 远程 MCP 客户端访问时必须携带的 Bearer Token
|
||||
GONAVI_MCP_HTTP_TOKEN=replace-with-a-random-token
|
||||
|
||||
# true = 只暴露结构查询工具,不注册 execute_sql
|
||||
GONAVI_MCP_SCHEMA_ONLY=true
|
||||
7
docker-compose.mcp-server.local.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
services:
|
||||
gonavi-mcp-server:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.mcp-server
|
||||
image: ${GONAVI_MCP_IMAGE_LOCAL:-gonavi-mcp-server:local}
|
||||
pull_policy: never
|
||||
20
docker-compose.mcp-server.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
services:
|
||||
gonavi-mcp-server:
|
||||
image: ${GONAVI_MCP_IMAGE:-ghcr.io/syngnat/gonavi-mcp-server:latest}
|
||||
pull_policy: ${GONAVI_MCP_PULL_POLICY:-missing}
|
||||
container_name: gonavi-mcp-server
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${GONAVI_MCP_HTTP_PORT:-8765}:8765"
|
||||
environment:
|
||||
GONAVI_DATA_ROOT: /data
|
||||
GONAVI_LOG_DIR: /var/lib/gonavi/logs
|
||||
GONAVI_MCP_HTTP_ADDR: 0.0.0.0:8765
|
||||
GONAVI_MCP_HTTP_PATH: ${GONAVI_MCP_HTTP_PATH:-/mcp}
|
||||
GONAVI_MCP_HTTP_TOKEN: ${GONAVI_MCP_HTTP_TOKEN}
|
||||
GONAVI_MCP_SCHEMA_ONLY: ${GONAVI_MCP_SCHEMA_ONLY:-true}
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ${GONAVI_HOST_DATA_ROOT}
|
||||
target: /data
|
||||
command: ["http"]
|
||||
7
docker-compose.web-server.local.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
services:
|
||||
gonavi-web-server:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.web-server
|
||||
image: ${GONAVI_WEB_IMAGE_LOCAL:-gonavi-web-server:local}
|
||||
pull_policy: never
|
||||
24
docker-compose.web-server.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
services:
|
||||
gonavi-web-server:
|
||||
image: ${GONAVI_WEB_IMAGE:-ghcr.io/syngnat/gonavi-web-server:latest}
|
||||
pull_policy: ${GONAVI_WEB_PULL_POLICY:-missing}
|
||||
container_name: gonavi-web-server
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${GONAVI_WEB_HTTP_PORT:-34116}:34116"
|
||||
environment:
|
||||
GONAVI_DATA_ROOT: /data
|
||||
GONAVI_LOG_DIR: /var/lib/gonavi/logs
|
||||
GONAVI_WEB_ADDR: 0.0.0.0:34116
|
||||
GONAVI_WEB_PASSWORD: ${GONAVI_WEB_PASSWORD:-}
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ${GONAVI_HOST_DATA_ROOT}
|
||||
target: /data
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:34116/__gonavi/healthz"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
command: ["web-server"]
|
||||
20
docker.mcp-server.env.example
Normal file
@@ -0,0 +1,20 @@
|
||||
# 宿主机 GoNavi 活动数据目录,目录内至少应包含 connections.json 与 daily_secrets.json
|
||||
GONAVI_HOST_DATA_ROOT=/absolute/path/to/gonavi-data
|
||||
|
||||
# 预构建镜像地址。稳定版通常使用 latest,跟随 dev 分支可改成 ghcr.io/syngnat/gonavi-mcp-server:dev-latest
|
||||
GONAVI_MCP_IMAGE=ghcr.io/syngnat/gonavi-mcp-server:latest
|
||||
|
||||
# 预构建镜像拉取策略:always / missing / never
|
||||
GONAVI_MCP_PULL_POLICY=missing
|
||||
|
||||
# 远程 MCP 客户端访问时必须携带的 Bearer Token
|
||||
GONAVI_MCP_HTTP_TOKEN=replace-with-a-random-token
|
||||
|
||||
# 宿主机暴露端口
|
||||
GONAVI_MCP_HTTP_PORT=8765
|
||||
|
||||
# MCP HTTP 路径
|
||||
GONAVI_MCP_HTTP_PATH=/mcp
|
||||
|
||||
# true = 只暴露结构查询工具,不注册 execute_sql
|
||||
GONAVI_MCP_SCHEMA_ONLY=true
|
||||
19
docker.web-server.env.example
Normal file
@@ -0,0 +1,19 @@
|
||||
# 宿主机 GoNavi 活动数据目录。
|
||||
# 建议至少包含:connections.json、daily_secrets.json;
|
||||
# 如需可选驱动代理,一并挂载 drivers/。
|
||||
# Web 认证状态会写入 web_auth.json;未设置环境密码时首次访问 /setup 初始化。
|
||||
GONAVI_HOST_DATA_ROOT=/absolute/path/to/gonavi-data
|
||||
|
||||
# 预构建镜像地址。稳定版通常使用 latest,跟随 dev 分支可改成 ghcr.io/syngnat/gonavi-web-server:dev-latest
|
||||
GONAVI_WEB_IMAGE=ghcr.io/syngnat/gonavi-web-server:latest
|
||||
|
||||
# 预构建镜像拉取策略:always / missing / never
|
||||
GONAVI_WEB_PULL_POLICY=missing
|
||||
|
||||
# 宿主机暴露端口(映射到容器 34116)
|
||||
GONAVI_WEB_HTTP_PORT=34116
|
||||
|
||||
# Web 管理员密码(至少 6 位)。留空时首次访问 /setup 初始化;公网部署请使用更强密码。
|
||||
# 设置或修改后请执行 docker compose up -d --force-recreate;请限制本文件的读取权限。
|
||||
# 新部署如需启用 2FA,请先留空并通过 /setup 初始化,再设置为同一密码并重新创建容器。
|
||||
GONAVI_WEB_PASSWORD=
|
||||
@@ -96,6 +96,12 @@
|
||||
"version": "1.11.1",
|
||||
"checksumPolicy": "off",
|
||||
"downloadUrl": "builtin://activate/postgres"
|
||||
},
|
||||
"elasticsearch": {
|
||||
"engine": "go",
|
||||
"version": "8.19.0",
|
||||
"checksumPolicy": "off",
|
||||
"downloadUrl": "builtin://activate/elasticsearch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
45
docs/需求追踪/需求进度追踪-Oracle对象跳转与存储过程修改执行-20260625.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# 需求进度追踪 - Oracle对象跳转与存储过程修改执行
|
||||
|
||||
## 1. 需求摘要
|
||||
- 需求名称:Oracle对象跳转与存储过程修改执行
|
||||
- 提出日期:2026-06-25
|
||||
- 负责人:Codex
|
||||
- 目标:SQL编辑器支持 Oracle 序列、存储包 Ctrl/Cmd 点击跳转;对象编辑执行 Oracle 存储过程时不再生成非法第二条语句。
|
||||
- 非目标:不调整 Oracle 连接驱动、不改数据库 schema、不重构侧栏对象树。
|
||||
|
||||
## 2. 范围与验收
|
||||
- 范围:QueryEditor 对象元数据、hover/跳转逻辑、DefinitionViewer 对象编辑 SQL 生成、相关 i18n 和回归测试。
|
||||
- 验收标准:序列/存储包在 SQL 编辑器可出现链接提示并打开定义页;带 SQLPlus `/` 的 Oracle PL/SQL 编辑 SQL 不生成 `/;`,执行时保留完整定义交给后端拆分。
|
||||
- 依赖与约束:复用现有侧栏 sequence/package tab 类型;不混入 `frontend/wailsjs/go/models.ts` 既有未提交改动。
|
||||
|
||||
## 3. 里程碑与进度
|
||||
- [x] 阶段 1(需求澄清):确认序列/存储包侧栏已可打开,但 SQL 编辑器跳转缺失;存储过程修改仍报 ORA-00900。
|
||||
- [x] 阶段 2(影响分析):影响前端 QueryEditor、DefinitionViewer、i18n、测试;后端拆分已有基础保护。
|
||||
- [x] 阶段 3(方案设计):补齐元数据与跳转类型;对象编辑 SQL 保留 SQLPlus `/` 语义。
|
||||
- [x] 阶段 4(实施计划):先补导航闭环,再修 PL/SQL 生成/执行,最后跑定向测试。
|
||||
- [x] 阶段 5(实现与自检):已完成定向测试、QueryEditor 全文件测试、前端 build、后端 Oracle 拆分测试。
|
||||
- [ ] 阶段 6(评审与交付):待提交推送。
|
||||
- [ ] 阶段 7(发布与观察):发布后验证 Oracle 对象编辑与 SQL 编辑器跳转。
|
||||
|
||||
## 4. 变更清单
|
||||
- 已完成:补齐 QueryEditor sequence/package 元数据、hover、点击打开定义页;修复对象编辑 SQLPlus `/;`;执行路径兼容旧编辑页 `/;`。
|
||||
- 进行中:提交并推送。
|
||||
- 待处理:发布后验证 Oracle 实库。
|
||||
|
||||
## 5. 风险与阻塞
|
||||
- 风险:三段名称 `schema.package.proc` 与 `db.schema.table` 存在歧义。
|
||||
- 阻塞:无。
|
||||
- 缓解措施:仅当第一段不是可见数据库时,将三段名称按 Oracle schema-qualified sequence/package 解析。
|
||||
|
||||
## 6. 决策记录
|
||||
- 决策 1:序列和存储包复用侧栏现有 `sequence-def` / `package-def` tab 结构。
|
||||
- 决策 2:Oracle PL/SQL 定义执行时保留 SQLPlus `/` 分隔符,由后端拆分器去掉 delimiter,避免前端用分号重组造成 ORA-00900。
|
||||
|
||||
## 7. 验证记录
|
||||
- 验证项:QueryEditor 导航回归测试、DefinitionViewer 对象编辑测试、SQL 语句拆分测试、i18n catalog 测试、前端 build、后端 Oracle 拆分测试。
|
||||
- 结果:通过。
|
||||
- 证据(日志/截图/链接):`npm test -- src/components/QueryEditor.external-sql-save.test.tsx`;`npm test -- src/components/DefinitionViewer.object-edit.test.tsx src/i18n/catalog.test.ts src/utils/sqlStatementSelection.test.ts`;`go test ./internal/app -run 'TestDBQueryMultiKeepsOracle|TestDBQueryMultiSkipsOracle'`;`npm run build`。
|
||||
|
||||
## 8. 下一步
|
||||
- 下一步行动:提交并推送到 dev。
|
||||
- 负责人:Codex
|
||||
74
docs/需求追踪/需求进度追踪-生产连接只读保护-20260623.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# 需求进度追踪 - 生产连接只读保护
|
||||
|
||||
## 1. 需求摘要
|
||||
- 需求名称:生产连接只读保护
|
||||
- 提出日期:2026-06-23
|
||||
- 负责人:Codex
|
||||
- 目标:为 SQL 类数据库与 MongoDB 连接增加连接级只读保护,启用后仅允许查询,阻止写入、DDL、导入和同步目标操作
|
||||
- 非目标:不为所有侧栏写操作都新增前端隐藏逻辑;不引入新的环境分级体系;不改造 JVM 只读能力
|
||||
|
||||
## 2. 范围与验收
|
||||
- 范围:
|
||||
- 连接配置模型、保存/回填与 RPC 序列化链路
|
||||
- 连接弹窗只读开关、查询编辑器本地拦截、DataGrid 导入入口收口
|
||||
- 后端 SQL/Mongo 查询判定与写操作统一守卫
|
||||
- 验收标准:
|
||||
- 支持的数据源出现“生产连接/只允许查询”开关
|
||||
- 启用后普通查询仍可执行,非查询 SQL / Mongo 写命令被前后端阻止
|
||||
- 导入、结构变更、清表、同步目标等关键写入口被后端拒绝
|
||||
- 依赖与约束:
|
||||
- 保持现有数据源能力判定与 QueryEditor 执行链路
|
||||
- MongoDB 前端判定以保守拦截为主,最终正确性由后端守卫兜底
|
||||
|
||||
## 3. 里程碑与进度
|
||||
- [x] 阶段 1(需求澄清):确认采用连接级 `readOnly` 布尔字段,不新建环境系统
|
||||
- [x] 阶段 2(影响分析):梳理前端能力面板、查询执行、导入与后端写入口
|
||||
- [x] 阶段 3(方案设计):确定“前端预拦截 + 后端最终守卫”双层保护
|
||||
- [x] 阶段 4(实施计划):接入配置链路、能力判定、查询判定与写入口守卫
|
||||
- [x] 阶段 5(实现与自检):补文案、测试与定向验证
|
||||
- [x] 阶段 6(评审与交付):确认范围、风险、回滚点和验证命令
|
||||
- [ ] 阶段 7(发布与观察):待体验验证
|
||||
|
||||
## 4. 变更清单
|
||||
- 已完成:
|
||||
- 新增连接级 `readOnly` 配置字段及前后端序列化支持
|
||||
- 连接弹窗为 SQL 类数据库与 MongoDB 增加生产连接保护开关
|
||||
- QueryEditor 增加本地非查询拦截,DataGrid 导入入口在只读连接下禁用
|
||||
- 后端为查询、DDL、导入、清表、同步等写入口增加统一只读守卫
|
||||
- MongoDB 查询判定改为命令级白名单,不再把所有 JSON 命令都视为只读
|
||||
- 补充前端/后端定向测试与需求追踪文档
|
||||
- 进行中:
|
||||
- 等待体验包验证连接弹窗、查询拦截和写操作拒绝文案
|
||||
- 待处理:
|
||||
- 如需进一步优化体验,再补侧栏对象级写菜单的前端隐藏/禁用
|
||||
|
||||
## 5. 风险与阻塞
|
||||
- 风险:
|
||||
- 前端 SQL/Mongo 只读判定是保守策略,边界命令可能仍需后端兜底
|
||||
- 现有部分侧栏写菜单仍可能显示,但执行时会被后端拒绝
|
||||
- 阻塞:
|
||||
- 暂无
|
||||
- 缓解措施:
|
||||
- 关键写入口统一走后端守卫;前端只负责提前反馈与减少误操作
|
||||
|
||||
## 6. 决策记录
|
||||
- 决策 1:只对 SQL 类数据库和 MongoDB 支持连接级生产保护,其他数据源忽略 `readOnly`
|
||||
- 决策 2:采用顶层 `readOnly` 布尔字段,避免新增环境枚举和迁移成本
|
||||
- 决策 3:MongoDB 只读判定按命令白名单处理,防止把写命令误放行
|
||||
|
||||
## 7. 验证记录
|
||||
- 验证项:
|
||||
- 前端数据源能力判定、RPC 配置、连接配置测试
|
||||
- 后端只读连接守卫与 SQL/Mongo 查询判定测试
|
||||
- 结果:
|
||||
- 通过
|
||||
- 证据(日志/截图/链接):
|
||||
- `go test ./internal/app -run 'Test(EnsureReadOnlyConnectionAllows|SupportsConnectionReadOnlyMode|IsReadOnlySQLQuery)'`
|
||||
- `npm --prefix frontend test -- src/utils/dataSourceCapabilities.test.ts src/utils/connectionRpcConfig.test.ts src/components/connectionModal/connectionModalConfig.keepalive.test.ts`
|
||||
|
||||
## 8. 下一步
|
||||
- 下一步行动:
|
||||
- 用体验包回归验证生产连接下的查询、导入、建库删库、同步目标和 Mongo 命令拦截
|
||||
- 如需更完整 UX,再补侧栏写菜单的只读态收口
|
||||
- 负责人:
|
||||
- Codex
|
||||
@@ -25,6 +25,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
<script type="module" src="/src/bootstrap.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
907
frontend/package-lock.json
generated
@@ -2,12 +2,15 @@
|
||||
"name": "gonavi-client",
|
||||
"private": true,
|
||||
"version": "0.6.5",
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run"
|
||||
"test": "vitest run",
|
||||
"postinstall": "patch-package --error-on-fail",
|
||||
"i18n:scan": "go run ../tools/i18n-scan --root .."
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
@@ -16,15 +19,19 @@
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@types/dagre": "^0.7.54",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"antd": "^5.12.0",
|
||||
"clsx": "^2.1.0",
|
||||
"dagre": "^0.8.5",
|
||||
"fflate": "^0.8.3",
|
||||
"mermaid": "^11.13.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-resizable": "^3.1.3",
|
||||
"react-syntax-highlighter": "^16.1.1",
|
||||
"reactflow": "^11.11.4",
|
||||
"recharts": "^3.8.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"sql-formatter": "^15.7.0",
|
||||
@@ -38,6 +45,7 @@
|
||||
"@types/react-test-renderer": "^18.0.7",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"patch-package": "^8.0.1",
|
||||
"react-test-renderer": "^18.2.0",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.8",
|
||||
|
||||
@@ -1 +1 @@
|
||||
d0464f9da25e9356e61652e638c99ffe
|
||||
339d5c974e6ab5cba2d5def171f5ce0a
|
||||
|
||||
461
frontend/patches/rc-table+7.54.0.patch
Normal file
@@ -0,0 +1,461 @@
|
||||
diff --git a/node_modules/rc-table/es/VirtualTable/BodyGrid.js b/node_modules/rc-table/es/VirtualTable/BodyGrid.js
|
||||
index 42dcde8..2cca743 100644
|
||||
--- a/node_modules/rc-table/es/VirtualTable/BodyGrid.js
|
||||
+++ b/node_modules/rc-table/es/VirtualTable/BodyGrid.js
|
||||
@@ -23,6 +23,8 @@ var Grid = /*#__PURE__*/React.forwardRef(function (props, ref) {
|
||||
sticky = _useContext2.sticky,
|
||||
scrollY = _useContext2.scrollY,
|
||||
listItemHeight = _useContext2.listItemHeight,
|
||||
+ listItemHeightFixed = _useContext2.listItemHeightFixed,
|
||||
+ listItemColumnVirtual = _useContext2.listItemColumnVirtual,
|
||||
getComponent = _useContext2.getComponent,
|
||||
onTablePropScroll = _useContext2.onScroll;
|
||||
|
||||
@@ -113,8 +115,14 @@ var Grid = /*#__PURE__*/React.forwardRef(function (props, ref) {
|
||||
var extraRender = function extraRender(info) {
|
||||
+ // The column-window fast path explicitly requires rows without spans.
|
||||
+ // Avoid scanning every column on each vertical window update.
|
||||
+ if (listItemColumnVirtual) {
|
||||
+ return null;
|
||||
+ }
|
||||
var start = info.start,
|
||||
end = info.end,
|
||||
getSize = info.getSize,
|
||||
- offsetY = info.offsetY;
|
||||
+ offsetY = info.offsetY,
|
||||
+ offsetX = info.offsetX;
|
||||
|
||||
// Do nothing if no data
|
||||
if (end < 0) {
|
||||
@@ -198,6 +200,7 @@ var Grid = /*#__PURE__*/React.forwardRef(function (props, ref) {
|
||||
style: {
|
||||
top: -offsetY + sizeInfo.top
|
||||
},
|
||||
+ offsetX: offsetX,
|
||||
extra: true,
|
||||
getHeight: getHeight
|
||||
});
|
||||
@@ -239,6 +242,7 @@ var Grid = /*#__PURE__*/React.forwardRef(function (props, ref) {
|
||||
className: tblPrefixCls,
|
||||
height: scrollY,
|
||||
itemHeight: listItemHeight || 24,
|
||||
+ itemHeightFixed: listItemHeightFixed,
|
||||
data: flattenData,
|
||||
itemKey: function itemKey(item) {
|
||||
return getRowKey(item.record);
|
||||
@@ -262,6 +266,7 @@ var Grid = /*#__PURE__*/React.forwardRef(function (props, ref) {
|
||||
data: item,
|
||||
rowKey: rowKey,
|
||||
index: index,
|
||||
+ offsetX: itemProps.offsetX,
|
||||
style: itemProps.style
|
||||
});
|
||||
}));
|
||||
diff --git a/node_modules/rc-table/es/VirtualTable/BodyLine.js b/node_modules/rc-table/es/VirtualTable/BodyLine.js
|
||||
index c760819..1a6c396 100644
|
||||
--- a/node_modules/rc-table/es/VirtualTable/BodyLine.js
|
||||
+++ b/node_modules/rc-table/es/VirtualTable/BodyLine.js
|
||||
@@ -2,7 +2,7 @@ import _extends from "@babel/runtime/helpers/esm/extends";
|
||||
import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
|
||||
import _defineProperty from "@babel/runtime/helpers/esm/defineProperty";
|
||||
import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties";
|
||||
-var _excluded = ["data", "index", "className", "rowKey", "style", "extra", "getHeight"];
|
||||
+var _excluded = ["data", "index", "className", "rowKey", "style", "offsetX", "extra", "getHeight"];
|
||||
import { useContext } from '@rc-component/context';
|
||||
import classNames from 'classnames';
|
||||
import * as React from 'react';
|
||||
@@ -10,7 +10,7 @@ import Cell from "../Cell";
|
||||
import TableContext, { responseImmutable } from "../context/TableContext";
|
||||
import useRowInfo from "../hooks/useRowInfo";
|
||||
import VirtualCell from "./VirtualCell";
|
||||
-import { StaticContext } from "./context";
|
||||
+import { GridContext, StaticContext } from "./context";
|
||||
import { computedExpandedClassName } from "../utils/expandUtil";
|
||||
var BodyLine = /*#__PURE__*/React.forwardRef(function (props, ref) {
|
||||
var data = props.data,
|
||||
@@ -18,20 +18,25 @@ var BodyLine = /*#__PURE__*/React.forwardRef(function (props, ref) {
|
||||
className = props.className,
|
||||
rowKey = props.rowKey,
|
||||
style = props.style,
|
||||
+ offsetX = props.offsetX,
|
||||
extra = props.extra,
|
||||
getHeight = props.getHeight,
|
||||
restProps = _objectWithoutProperties(props, _excluded);
|
||||
var record = data.record,
|
||||
indent = data.indent,
|
||||
renderIndex = data.index;
|
||||
- var _useContext = useContext(TableContext, ['prefixCls', 'flattenColumns', 'fixColumn', 'componentWidth', 'scrollX']),
|
||||
+ var _useContext = useContext(TableContext, ['prefixCls', 'flattenColumns', 'fixColumn', 'componentWidth', 'scrollX', 'direction']),
|
||||
scrollX = _useContext.scrollX,
|
||||
flattenColumns = _useContext.flattenColumns,
|
||||
prefixCls = _useContext.prefixCls,
|
||||
fixColumn = _useContext.fixColumn,
|
||||
- componentWidth = _useContext.componentWidth;
|
||||
- var _useContext2 = useContext(StaticContext, ['getComponent']),
|
||||
- getComponent = _useContext2.getComponent;
|
||||
+ componentWidth = _useContext.componentWidth,
|
||||
+ direction = _useContext.direction;
|
||||
+ var _useContext2 = useContext(StaticContext, ['getComponent', 'listItemColumnVirtual']),
|
||||
+ getComponent = _useContext2.getComponent,
|
||||
+ listItemColumnVirtual = _useContext2.listItemColumnVirtual;
|
||||
+ var _useContext3 = useContext(GridContext, ['columnsOffset']),
|
||||
+ columnsOffset = _useContext3.columnsOffset;
|
||||
var rowInfo = useRowInfo(record, rowKey, index, indent);
|
||||
var RowComponent = getComponent(['body', 'row'], 'div');
|
||||
var cellComponent = getComponent(['body', 'cell'], 'div');
|
||||
@@ -71,12 +76,7 @@ var BodyLine = /*#__PURE__*/React.forwardRef(function (props, ref) {
|
||||
rowStyle.position = 'absolute';
|
||||
rowStyle.pointerEvents = 'none';
|
||||
}
|
||||
- var rowNode = /*#__PURE__*/React.createElement(RowComponent, _extends({}, rowProps, restProps, {
|
||||
- "data-row-key": rowKey,
|
||||
- ref: rowSupportExpand ? null : ref,
|
||||
- className: classNames(className, "".concat(prefixCls, "-row"), rowProps === null || rowProps === void 0 ? void 0 : rowProps.className, _defineProperty({}, "".concat(prefixCls, "-row-extra"), extra)),
|
||||
- style: _objectSpread(_objectSpread({}, rowStyle), rowProps === null || rowProps === void 0 ? void 0 : rowProps.style)
|
||||
- }), flattenColumns.map(function (column, colIndex) {
|
||||
+ var renderVirtualCell = function renderVirtualCell(column, colIndex) {
|
||||
return /*#__PURE__*/React.createElement(VirtualCell, {
|
||||
key: colIndex,
|
||||
component: cellComponent,
|
||||
@@ -90,7 +90,60 @@ var BodyLine = /*#__PURE__*/React.forwardRef(function (props, ref) {
|
||||
inverse: extra,
|
||||
getHeight: getHeight
|
||||
});
|
||||
- }));
|
||||
+ };
|
||||
+ var columnNodes;
|
||||
+ if (listItemColumnVirtual && direction !== 'rtl' && !extra && columnsOffset && columnsOffset.length === flattenColumns.length) {
|
||||
+ var mergedOffsetX = Math.max(0, offsetX || 0);
|
||||
+ var viewportWidth = Math.max(0, componentWidth || 0);
|
||||
+ var overscanWidth = Math.max(320, viewportWidth);
|
||||
+ var visibleStart = Math.max(0, mergedOffsetX - overscanWidth);
|
||||
+ var visibleEnd = mergedOffsetX + viewportWidth + overscanWidth;
|
||||
+ var nodes = [];
|
||||
+ var spacerStart = -1;
|
||||
+ var spacerWidth = 0;
|
||||
+ var flushSpacer = function flushSpacer() {
|
||||
+ if (spacerWidth <= 0) return;
|
||||
+ nodes.push(/*#__PURE__*/React.createElement(cellComponent, {
|
||||
+ "aria-hidden": true,
|
||||
+ className: "".concat(prefixCls, "-cell-virtual-spacer"),
|
||||
+ key: "virtual-spacer-".concat(spacerStart),
|
||||
+ style: {
|
||||
+ border: 0,
|
||||
+ flex: "0 0 ".concat(spacerWidth, "px"),
|
||||
+ minWidth: 0,
|
||||
+ padding: 0,
|
||||
+ pointerEvents: 'none',
|
||||
+ visibility: 'hidden',
|
||||
+ width: "".concat(spacerWidth, "px")
|
||||
+ }
|
||||
+ }));
|
||||
+ spacerStart = -1;
|
||||
+ spacerWidth = 0;
|
||||
+ };
|
||||
+ flattenColumns.forEach(function (column, colIndex) {
|
||||
+ var columnStart = colIndex > 0 ? columnsOffset[colIndex - 1] : 0;
|
||||
+ var columnEnd = columnsOffset[colIndex] || columnStart;
|
||||
+ var fixedColumn = column.fixed === true || column.fixed === 'left' || column.fixed === 'right';
|
||||
+ var visibleColumn = fixedColumn || columnEnd >= visibleStart && columnStart <= visibleEnd;
|
||||
+ if (visibleColumn) {
|
||||
+ flushSpacer();
|
||||
+ nodes.push(renderVirtualCell(column, colIndex));
|
||||
+ } else {
|
||||
+ if (spacerStart < 0) spacerStart = colIndex;
|
||||
+ spacerWidth += Math.max(0, columnEnd - columnStart);
|
||||
+ }
|
||||
+ });
|
||||
+ flushSpacer();
|
||||
+ columnNodes = nodes;
|
||||
+ } else {
|
||||
+ columnNodes = flattenColumns.map(renderVirtualCell);
|
||||
+ }
|
||||
+ var rowNode = /*#__PURE__*/React.createElement(RowComponent, _extends({}, rowProps, restProps, {
|
||||
+ "data-row-key": rowKey,
|
||||
+ ref: rowSupportExpand ? null : ref,
|
||||
+ className: classNames(className, "".concat(prefixCls, "-row"), rowProps === null || rowProps === void 0 ? void 0 : rowProps.className, _defineProperty({}, "".concat(prefixCls, "-row-extra"), extra)),
|
||||
+ style: _objectSpread(_objectSpread({}, rowStyle), rowProps === null || rowProps === void 0 ? void 0 : rowProps.style)
|
||||
+ }), columnNodes);
|
||||
if (rowSupportExpand) {
|
||||
return /*#__PURE__*/React.createElement("div", {
|
||||
ref: ref
|
||||
diff --git a/node_modules/rc-table/es/VirtualTable/context.d.ts b/node_modules/rc-table/es/VirtualTable/context.d.ts
|
||||
index e558165..ccd74fc 100644
|
||||
--- a/node_modules/rc-table/es/VirtualTable/context.d.ts
|
||||
+++ b/node_modules/rc-table/es/VirtualTable/context.d.ts
|
||||
@@ -3,6 +3,8 @@ import type { GetComponent, TableSticky } from '../interface';
|
||||
export interface StaticContextProps {
|
||||
scrollY: number;
|
||||
listItemHeight: number;
|
||||
+ listItemHeightFixed?: boolean;
|
||||
+ listItemColumnVirtual?: boolean;
|
||||
sticky: boolean | TableSticky;
|
||||
getComponent: GetComponent;
|
||||
onScroll?: React.UIEventHandler<HTMLDivElement>;
|
||||
diff --git a/node_modules/rc-table/es/VirtualTable/index.d.ts b/node_modules/rc-table/es/VirtualTable/index.d.ts
|
||||
index 31fe8e0..c5097b2 100644
|
||||
--- a/node_modules/rc-table/es/VirtualTable/index.d.ts
|
||||
+++ b/node_modules/rc-table/es/VirtualTable/index.d.ts
|
||||
@@ -8,6 +8,9 @@ export interface VirtualTableProps<RecordType> extends Omit<TableProps<RecordTyp
|
||||
y: number;
|
||||
};
|
||||
listItemHeight?: number;
|
||||
+ listItemHeightFixed?: boolean;
|
||||
+ /** LTR-only fast path; requires body cells without colSpan/rowSpan. */
|
||||
+ listItemColumnVirtual?: boolean;
|
||||
}
|
||||
export type ForwardGenericVirtualTable = (<RecordType>(props: TableProps<RecordType> & React.RefAttributes<Reference>) => React.ReactElement) & {
|
||||
displayName?: string;
|
||||
diff --git a/node_modules/rc-table/es/VirtualTable/index.js b/node_modules/rc-table/es/VirtualTable/index.js
|
||||
index fb00e9b..a778aa6 100644
|
||||
--- a/node_modules/rc-table/es/VirtualTable/index.js
|
||||
+++ b/node_modules/rc-table/es/VirtualTable/index.js
|
||||
@@ -27,6 +27,8 @@ function VirtualTable(props, ref) {
|
||||
prefixCls = _props$prefixCls === void 0 ? DEFAULT_PREFIX : _props$prefixCls,
|
||||
className = props.className,
|
||||
listItemHeight = props.listItemHeight,
|
||||
+ listItemHeightFixed = props.listItemHeightFixed,
|
||||
+ listItemColumnVirtual = props.listItemColumnVirtual,
|
||||
components = props.components,
|
||||
onScroll = props.onScroll;
|
||||
var _ref = scroll || {},
|
||||
@@ -61,10 +63,12 @@ function VirtualTable(props, ref) {
|
||||
sticky: sticky,
|
||||
scrollY: scrollY,
|
||||
listItemHeight: listItemHeight,
|
||||
+ listItemHeightFixed: listItemHeightFixed,
|
||||
+ listItemColumnVirtual: listItemColumnVirtual,
|
||||
getComponent: getComponent,
|
||||
onScroll: onInternalScroll
|
||||
};
|
||||
- }, [sticky, scrollY, listItemHeight, getComponent, onInternalScroll]);
|
||||
+ }, [sticky, scrollY, listItemHeight, listItemHeightFixed, listItemColumnVirtual, getComponent, onInternalScroll]);
|
||||
|
||||
// ========================== Render ==========================
|
||||
return /*#__PURE__*/React.createElement(StaticContext.Provider, {
|
||||
diff --git a/node_modules/rc-table/lib/VirtualTable/BodyGrid.js b/node_modules/rc-table/lib/VirtualTable/BodyGrid.js
|
||||
index dee2539..b077789 100644
|
||||
--- a/node_modules/rc-table/lib/VirtualTable/BodyGrid.js
|
||||
+++ b/node_modules/rc-table/lib/VirtualTable/BodyGrid.js
|
||||
@@ -33,6 +33,8 @@ var Grid = /*#__PURE__*/React.forwardRef(function (props, ref) {
|
||||
sticky = _useContext2.sticky,
|
||||
scrollY = _useContext2.scrollY,
|
||||
listItemHeight = _useContext2.listItemHeight,
|
||||
+ listItemHeightFixed = _useContext2.listItemHeightFixed,
|
||||
+ listItemColumnVirtual = _useContext2.listItemColumnVirtual,
|
||||
getComponent = _useContext2.getComponent,
|
||||
onTablePropScroll = _useContext2.onScroll;
|
||||
|
||||
@@ -123,8 +125,14 @@ var Grid = /*#__PURE__*/React.forwardRef(function (props, ref) {
|
||||
var extraRender = function extraRender(info) {
|
||||
+ // The column-window fast path explicitly requires rows without spans.
|
||||
+ // Avoid scanning every column on each vertical window update.
|
||||
+ if (listItemColumnVirtual) {
|
||||
+ return null;
|
||||
+ }
|
||||
var start = info.start,
|
||||
end = info.end,
|
||||
getSize = info.getSize,
|
||||
- offsetY = info.offsetY;
|
||||
+ offsetY = info.offsetY,
|
||||
+ offsetX = info.offsetX;
|
||||
|
||||
// Do nothing if no data
|
||||
if (end < 0) {
|
||||
@@ -208,6 +210,7 @@ var Grid = /*#__PURE__*/React.forwardRef(function (props, ref) {
|
||||
style: {
|
||||
top: -offsetY + sizeInfo.top
|
||||
},
|
||||
+ offsetX: offsetX,
|
||||
extra: true,
|
||||
getHeight: getHeight
|
||||
});
|
||||
@@ -249,6 +252,7 @@ var Grid = /*#__PURE__*/React.forwardRef(function (props, ref) {
|
||||
className: tblPrefixCls,
|
||||
height: scrollY,
|
||||
itemHeight: listItemHeight || 24,
|
||||
+ itemHeightFixed: listItemHeightFixed,
|
||||
data: flattenData,
|
||||
itemKey: function itemKey(item) {
|
||||
return getRowKey(item.record);
|
||||
@@ -272,6 +276,7 @@ var Grid = /*#__PURE__*/React.forwardRef(function (props, ref) {
|
||||
data: item,
|
||||
rowKey: rowKey,
|
||||
index: index,
|
||||
+ offsetX: itemProps.offsetX,
|
||||
style: itemProps.style
|
||||
});
|
||||
}));
|
||||
diff --git a/node_modules/rc-table/lib/VirtualTable/BodyLine.js b/node_modules/rc-table/lib/VirtualTable/BodyLine.js
|
||||
index e1275fb..d962380 100644
|
||||
--- a/node_modules/rc-table/lib/VirtualTable/BodyLine.js
|
||||
+++ b/node_modules/rc-table/lib/VirtualTable/BodyLine.js
|
||||
@@ -19,7 +19,7 @@ var _useRowInfo = _interopRequireDefault(require("../hooks/useRowInfo"));
|
||||
var _VirtualCell = _interopRequireDefault(require("./VirtualCell"));
|
||||
var _context2 = require("./context");
|
||||
var _expandUtil = require("../utils/expandUtil");
|
||||
-var _excluded = ["data", "index", "className", "rowKey", "style", "extra", "getHeight"];
|
||||
+var _excluded = ["data", "index", "className", "rowKey", "style", "offsetX", "extra", "getHeight"];
|
||||
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); }
|
||||
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != _typeof(e) && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
||||
var BodyLine = /*#__PURE__*/React.forwardRef(function (props, ref) {
|
||||
@@ -28,20 +28,25 @@ var BodyLine = /*#__PURE__*/React.forwardRef(function (props, ref) {
|
||||
className = props.className,
|
||||
rowKey = props.rowKey,
|
||||
style = props.style,
|
||||
+ offsetX = props.offsetX,
|
||||
extra = props.extra,
|
||||
getHeight = props.getHeight,
|
||||
restProps = (0, _objectWithoutProperties2.default)(props, _excluded);
|
||||
var record = data.record,
|
||||
indent = data.indent,
|
||||
renderIndex = data.index;
|
||||
- var _useContext = (0, _context.useContext)(_TableContext.default, ['prefixCls', 'flattenColumns', 'fixColumn', 'componentWidth', 'scrollX']),
|
||||
+ var _useContext = (0, _context.useContext)(_TableContext.default, ['prefixCls', 'flattenColumns', 'fixColumn', 'componentWidth', 'scrollX', 'direction']),
|
||||
scrollX = _useContext.scrollX,
|
||||
flattenColumns = _useContext.flattenColumns,
|
||||
prefixCls = _useContext.prefixCls,
|
||||
fixColumn = _useContext.fixColumn,
|
||||
- componentWidth = _useContext.componentWidth;
|
||||
- var _useContext2 = (0, _context.useContext)(_context2.StaticContext, ['getComponent']),
|
||||
- getComponent = _useContext2.getComponent;
|
||||
+ componentWidth = _useContext.componentWidth,
|
||||
+ direction = _useContext.direction;
|
||||
+ var _useContext2 = (0, _context.useContext)(_context2.StaticContext, ['getComponent', 'listItemColumnVirtual']),
|
||||
+ getComponent = _useContext2.getComponent,
|
||||
+ listItemColumnVirtual = _useContext2.listItemColumnVirtual;
|
||||
+ var _useContext3 = (0, _context.useContext)(_context2.GridContext, ['columnsOffset']),
|
||||
+ columnsOffset = _useContext3.columnsOffset;
|
||||
var rowInfo = (0, _useRowInfo.default)(record, rowKey, index, indent);
|
||||
var RowComponent = getComponent(['body', 'row'], 'div');
|
||||
var cellComponent = getComponent(['body', 'cell'], 'div');
|
||||
@@ -81,12 +86,7 @@ var BodyLine = /*#__PURE__*/React.forwardRef(function (props, ref) {
|
||||
rowStyle.position = 'absolute';
|
||||
rowStyle.pointerEvents = 'none';
|
||||
}
|
||||
- var rowNode = /*#__PURE__*/React.createElement(RowComponent, (0, _extends2.default)({}, rowProps, restProps, {
|
||||
- "data-row-key": rowKey,
|
||||
- ref: rowSupportExpand ? null : ref,
|
||||
- className: (0, _classnames.default)(className, "".concat(prefixCls, "-row"), rowProps === null || rowProps === void 0 ? void 0 : rowProps.className, (0, _defineProperty2.default)({}, "".concat(prefixCls, "-row-extra"), extra)),
|
||||
- style: (0, _objectSpread2.default)((0, _objectSpread2.default)({}, rowStyle), rowProps === null || rowProps === void 0 ? void 0 : rowProps.style)
|
||||
- }), flattenColumns.map(function (column, colIndex) {
|
||||
+ var renderVirtualCell = function renderVirtualCell(column, colIndex) {
|
||||
return /*#__PURE__*/React.createElement(_VirtualCell.default, {
|
||||
key: colIndex,
|
||||
component: cellComponent,
|
||||
@@ -100,7 +100,60 @@ var BodyLine = /*#__PURE__*/React.forwardRef(function (props, ref) {
|
||||
inverse: extra,
|
||||
getHeight: getHeight
|
||||
});
|
||||
- }));
|
||||
+ };
|
||||
+ var columnNodes;
|
||||
+ if (listItemColumnVirtual && direction !== 'rtl' && !extra && columnsOffset && columnsOffset.length === flattenColumns.length) {
|
||||
+ var mergedOffsetX = Math.max(0, offsetX || 0);
|
||||
+ var viewportWidth = Math.max(0, componentWidth || 0);
|
||||
+ var overscanWidth = Math.max(320, viewportWidth);
|
||||
+ var visibleStart = Math.max(0, mergedOffsetX - overscanWidth);
|
||||
+ var visibleEnd = mergedOffsetX + viewportWidth + overscanWidth;
|
||||
+ var nodes = [];
|
||||
+ var spacerStart = -1;
|
||||
+ var spacerWidth = 0;
|
||||
+ var flushSpacer = function flushSpacer() {
|
||||
+ if (spacerWidth <= 0) return;
|
||||
+ nodes.push(/*#__PURE__*/React.createElement(cellComponent, {
|
||||
+ "aria-hidden": true,
|
||||
+ className: "".concat(prefixCls, "-cell-virtual-spacer"),
|
||||
+ key: "virtual-spacer-".concat(spacerStart),
|
||||
+ style: {
|
||||
+ border: 0,
|
||||
+ flex: "0 0 ".concat(spacerWidth, "px"),
|
||||
+ minWidth: 0,
|
||||
+ padding: 0,
|
||||
+ pointerEvents: 'none',
|
||||
+ visibility: 'hidden',
|
||||
+ width: "".concat(spacerWidth, "px")
|
||||
+ }
|
||||
+ }));
|
||||
+ spacerStart = -1;
|
||||
+ spacerWidth = 0;
|
||||
+ };
|
||||
+ flattenColumns.forEach(function (column, colIndex) {
|
||||
+ var columnStart = colIndex > 0 ? columnsOffset[colIndex - 1] : 0;
|
||||
+ var columnEnd = columnsOffset[colIndex] || columnStart;
|
||||
+ var fixedColumn = column.fixed === true || column.fixed === 'left' || column.fixed === 'right';
|
||||
+ var visibleColumn = fixedColumn || columnEnd >= visibleStart && columnStart <= visibleEnd;
|
||||
+ if (visibleColumn) {
|
||||
+ flushSpacer();
|
||||
+ nodes.push(renderVirtualCell(column, colIndex));
|
||||
+ } else {
|
||||
+ if (spacerStart < 0) spacerStart = colIndex;
|
||||
+ spacerWidth += Math.max(0, columnEnd - columnStart);
|
||||
+ }
|
||||
+ });
|
||||
+ flushSpacer();
|
||||
+ columnNodes = nodes;
|
||||
+ } else {
|
||||
+ columnNodes = flattenColumns.map(renderVirtualCell);
|
||||
+ }
|
||||
+ var rowNode = /*#__PURE__*/React.createElement(RowComponent, (0, _extends2.default)({}, rowProps, restProps, {
|
||||
+ "data-row-key": rowKey,
|
||||
+ ref: rowSupportExpand ? null : ref,
|
||||
+ className: (0, _classnames.default)(className, "".concat(prefixCls, "-row"), rowProps === null || rowProps === void 0 ? void 0 : rowProps.className, (0, _defineProperty2.default)({}, "".concat(prefixCls, "-row-extra"), extra)),
|
||||
+ style: (0, _objectSpread2.default)((0, _objectSpread2.default)({}, rowStyle), rowProps === null || rowProps === void 0 ? void 0 : rowProps.style)
|
||||
+ }), columnNodes);
|
||||
if (rowSupportExpand) {
|
||||
return /*#__PURE__*/React.createElement("div", {
|
||||
ref: ref
|
||||
diff --git a/node_modules/rc-table/lib/VirtualTable/context.d.ts b/node_modules/rc-table/lib/VirtualTable/context.d.ts
|
||||
index e558165..ccd74fc 100644
|
||||
--- a/node_modules/rc-table/lib/VirtualTable/context.d.ts
|
||||
+++ b/node_modules/rc-table/lib/VirtualTable/context.d.ts
|
||||
@@ -3,6 +3,8 @@ import type { GetComponent, TableSticky } from '../interface';
|
||||
export interface StaticContextProps {
|
||||
scrollY: number;
|
||||
listItemHeight: number;
|
||||
+ listItemHeightFixed?: boolean;
|
||||
+ listItemColumnVirtual?: boolean;
|
||||
sticky: boolean | TableSticky;
|
||||
getComponent: GetComponent;
|
||||
onScroll?: React.UIEventHandler<HTMLDivElement>;
|
||||
diff --git a/node_modules/rc-table/lib/VirtualTable/index.d.ts b/node_modules/rc-table/lib/VirtualTable/index.d.ts
|
||||
index 31fe8e0..c5097b2 100644
|
||||
--- a/node_modules/rc-table/lib/VirtualTable/index.d.ts
|
||||
+++ b/node_modules/rc-table/lib/VirtualTable/index.d.ts
|
||||
@@ -8,6 +8,9 @@ export interface VirtualTableProps<RecordType> extends Omit<TableProps<RecordTyp
|
||||
y: number;
|
||||
};
|
||||
listItemHeight?: number;
|
||||
+ listItemHeightFixed?: boolean;
|
||||
+ /** LTR-only fast path; requires body cells without colSpan/rowSpan. */
|
||||
+ listItemColumnVirtual?: boolean;
|
||||
}
|
||||
export type ForwardGenericVirtualTable = (<RecordType>(props: TableProps<RecordType> & React.RefAttributes<Reference>) => React.ReactElement) & {
|
||||
displayName?: string;
|
||||
diff --git a/node_modules/rc-table/lib/VirtualTable/index.js b/node_modules/rc-table/lib/VirtualTable/index.js
|
||||
index 8e31a9b..24ab161 100644
|
||||
--- a/node_modules/rc-table/lib/VirtualTable/index.js
|
||||
+++ b/node_modules/rc-table/lib/VirtualTable/index.js
|
||||
@@ -38,6 +38,8 @@ function VirtualTable(props, ref) {
|
||||
prefixCls = _props$prefixCls === void 0 ? _Table.DEFAULT_PREFIX : _props$prefixCls,
|
||||
className = props.className,
|
||||
listItemHeight = props.listItemHeight,
|
||||
+ listItemHeightFixed = props.listItemHeightFixed,
|
||||
+ listItemColumnVirtual = props.listItemColumnVirtual,
|
||||
components = props.components,
|
||||
onScroll = props.onScroll;
|
||||
var _ref = scroll || {},
|
||||
@@ -72,10 +74,12 @@ function VirtualTable(props, ref) {
|
||||
sticky: sticky,
|
||||
scrollY: scrollY,
|
||||
listItemHeight: listItemHeight,
|
||||
+ listItemHeightFixed: listItemHeightFixed,
|
||||
+ listItemColumnVirtual: listItemColumnVirtual,
|
||||
getComponent: getComponent,
|
||||
onScroll: onInternalScroll
|
||||
};
|
||||
- }, [sticky, scrollY, listItemHeight, getComponent, onInternalScroll]);
|
||||
+ }, [sticky, scrollY, listItemHeight, listItemHeightFixed, listItemColumnVirtual, getComponent, onInternalScroll]);
|
||||
|
||||
// ========================== Render ==========================
|
||||
return /*#__PURE__*/React.createElement(_context.StaticContext.Provider, {
|
||||
336
frontend/patches/rc-virtual-list+3.19.2.patch
Normal file
@@ -0,0 +1,336 @@
|
||||
diff --git a/node_modules/rc-virtual-list/es/List.d.ts b/node_modules/rc-virtual-list/es/List.d.ts
|
||||
index c517366..e414513 100644
|
||||
--- a/node_modules/rc-virtual-list/es/List.d.ts
|
||||
+++ b/node_modules/rc-virtual-list/es/List.d.ts
|
||||
@@ -20,6 +20,8 @@ export interface ListProps<T> extends Omit<React.HTMLAttributes<any>, 'children'
|
||||
data: T[];
|
||||
height?: number;
|
||||
itemHeight?: number;
|
||||
+ /** Treat every item as exactly itemHeight and calculate the visible range in O(1). */
|
||||
+ itemHeightFixed?: boolean;
|
||||
/** If not match virtual scroll condition, Set List still use height of container. */
|
||||
fullHeight?: boolean;
|
||||
itemKey: React.Key | ((item: T) => React.Key);
|
||||
diff --git a/node_modules/rc-virtual-list/es/List.js b/node_modules/rc-virtual-list/es/List.js
|
||||
index 2926056..e917ba1 100644
|
||||
--- a/node_modules/rc-virtual-list/es/List.js
|
||||
+++ b/node_modules/rc-virtual-list/es/List.js
|
||||
@@ -4,7 +4,7 @@ import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
|
||||
import _defineProperty from "@babel/runtime/helpers/esm/defineProperty";
|
||||
import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray";
|
||||
import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties";
|
||||
-var _excluded = ["prefixCls", "className", "height", "itemHeight", "fullHeight", "style", "data", "children", "itemKey", "virtual", "direction", "scrollWidth", "component", "onScroll", "onVirtualScroll", "onVisibleChange", "innerProps", "extraRender", "styles", "showScrollBar"];
|
||||
+var _excluded = ["prefixCls", "className", "height", "itemHeight", "itemHeightFixed", "fullHeight", "style", "data", "children", "itemKey", "virtual", "direction", "scrollWidth", "component", "onScroll", "onVirtualScroll", "onVisibleChange", "innerProps", "extraRender", "styles", "showScrollBar"];
|
||||
import classNames from 'classnames';
|
||||
import ResizeObserver from 'rc-resize-observer';
|
||||
import { useEvent } from 'rc-util';
|
||||
@@ -35,6 +35,7 @@ export function RawList(props, ref) {
|
||||
className = props.className,
|
||||
height = props.height,
|
||||
itemHeight = props.itemHeight,
|
||||
+ itemHeightFixed = props.itemHeightFixed,
|
||||
_props$fullHeight = props.fullHeight,
|
||||
fullHeight = _props$fullHeight === void 0 ? true : _props$fullHeight,
|
||||
style = props.style,
|
||||
@@ -65,7 +66,7 @@ export function RawList(props, ref) {
|
||||
}, [itemKey]);
|
||||
|
||||
// ================================ Height ================================
|
||||
- var _useHeights = useHeights(getKey, null, null),
|
||||
+ var _useHeights = useHeights(getKey, null, null, itemHeightFixed),
|
||||
_useHeights2 = _slicedToArray(_useHeights, 4),
|
||||
setInstanceRef = _useHeights2[0],
|
||||
collectHeight = _useHeights2[1],
|
||||
@@ -74,11 +75,12 @@ export function RawList(props, ref) {
|
||||
|
||||
// ================================= MISC =================================
|
||||
var useVirtual = !!(virtual !== false && height && itemHeight);
|
||||
- var containerHeight = React.useMemo(function () {
|
||||
+ var measuredContainerHeight = React.useMemo(function () {
|
||||
return Object.values(heights.maps).reduce(function (total, curr) {
|
||||
return total + curr;
|
||||
}, 0);
|
||||
}, [heights.id, heights.maps]);
|
||||
+ var containerHeight = itemHeightFixed ? itemHeight * (data || EMPTY_DATA).length : measuredContainerHeight;
|
||||
var inVirtual = useVirtual && data && (Math.max(itemHeight * data.length, containerHeight) > height || !!scrollWidth);
|
||||
var isRTL = direction === 'rtl';
|
||||
var mergedClassName = classNames(prefixCls, _defineProperty({}, "".concat(prefixCls, "-rtl"), isRTL), className);
|
||||
@@ -159,6 +161,28 @@ export function RawList(props, ref) {
|
||||
offset: undefined
|
||||
};
|
||||
}
|
||||
+ if (itemHeightFixed && itemHeight > 0) {
|
||||
+ var fixedDataLen = mergedData.length;
|
||||
+ var fixedScrollHeight = fixedDataLen * itemHeight;
|
||||
+ if (fixedDataLen === 0) {
|
||||
+ return {
|
||||
+ scrollHeight: 0,
|
||||
+ start: 0,
|
||||
+ end: -1,
|
||||
+ offset: 0
|
||||
+ };
|
||||
+ }
|
||||
+ var fixedMaxScrollTop = Math.max(0, fixedScrollHeight - height);
|
||||
+ var fixedOffsetTop = Math.max(0, Math.min(fixedMaxScrollTop, offsetTop));
|
||||
+ var fixedStartIndex = Math.min(fixedDataLen - 1, Math.max(0, Math.ceil(fixedOffsetTop / itemHeight) - 1));
|
||||
+ var fixedEndIndex = Math.min(fixedDataLen - 1, Math.floor((fixedOffsetTop + height) / itemHeight) + 1);
|
||||
+ return {
|
||||
+ scrollHeight: fixedScrollHeight,
|
||||
+ start: fixedStartIndex,
|
||||
+ end: fixedEndIndex,
|
||||
+ offset: fixedStartIndex * itemHeight
|
||||
+ };
|
||||
+ }
|
||||
var itemTop = 0;
|
||||
var startIndex;
|
||||
var startOffset;
|
||||
@@ -201,7 +225,7 @@ export function RawList(props, ref) {
|
||||
end: endIndex,
|
||||
offset: startOffset
|
||||
};
|
||||
- }, [inVirtual, useVirtual, offsetTop, mergedData, heightUpdatedMark, height]),
|
||||
+ }, [inVirtual, useVirtual, offsetTop, mergedData, heightUpdatedMark, height, itemHeightFixed, itemHeight]),
|
||||
scrollHeight = _React$useMemo.scrollHeight,
|
||||
start = _React$useMemo.start,
|
||||
end = _React$useMemo.end,
|
||||
@@ -213,6 +237,10 @@ export function RawList(props, ref) {
|
||||
// Which will make scroll jump.
|
||||
// Let's sync scroll top to avoid jump
|
||||
React.useLayoutEffect(function () {
|
||||
+ if (itemHeightFixed) {
|
||||
+ heights.resetRecord();
|
||||
+ return;
|
||||
+ }
|
||||
var changedRecord = heights.getRecord();
|
||||
if (changedRecord.size === 1) {
|
||||
var recordKey = Array.from(changedRecord.keys())[0];
|
||||
@@ -232,7 +260,7 @@ export function RawList(props, ref) {
|
||||
}
|
||||
}
|
||||
heights.resetRecord();
|
||||
- }, [scrollHeight]);
|
||||
+ }, [scrollHeight, itemHeightFixed, heightUpdatedMark]);
|
||||
|
||||
// ================================= Size =================================
|
||||
var _React$useState = React.useState({
|
||||
diff --git a/node_modules/rc-virtual-list/es/hooks/useHeights.d.ts b/node_modules/rc-virtual-list/es/hooks/useHeights.d.ts
|
||||
--- a/node_modules/rc-virtual-list/es/hooks/useHeights.d.ts
|
||||
+++ b/node_modules/rc-virtual-list/es/hooks/useHeights.d.ts
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { GetKey } from '../interface';
|
||||
import CacheMap from '../utils/CacheMap';
|
||||
-export default function useHeights<T>(getKey: GetKey<T>, onItemAdd?: (item: T) => void, onItemRemove?: (item: T) => void): [
|
||||
+export default function useHeights<T>(getKey: GetKey<T>, onItemAdd?: (item: T) => void, onItemRemove?: (item: T) => void, disabled?: boolean): [
|
||||
setInstanceRef: (item: T, instance: HTMLElement) => void,
|
||||
collectHeight: (sync?: boolean) => void,
|
||||
cacheMap: CacheMap,
|
||||
diff --git a/node_modules/rc-virtual-list/es/hooks/useHeights.js b/node_modules/rc-virtual-list/es/hooks/useHeights.js
|
||||
index 0323aae..38e6e75 100644
|
||||
--- a/node_modules/rc-virtual-list/es/hooks/useHeights.js
|
||||
+++ b/node_modules/rc-virtual-list/es/hooks/useHeights.js
|
||||
@@ -6,7 +6,7 @@ function parseNumber(value) {
|
||||
var num = parseFloat(value);
|
||||
return isNaN(num) ? 0 : num;
|
||||
}
|
||||
-export default function useHeights(getKey, onItemAdd, onItemRemove) {
|
||||
+export default function useHeights(getKey, onItemAdd, onItemRemove, disabled) {
|
||||
var _React$useState = React.useState(0),
|
||||
_React$useState2 = _slicedToArray(_React$useState, 2),
|
||||
updatedMark = _React$useState2[0],
|
||||
@@ -14,12 +14,17 @@ export default function useHeights(getKey, onItemAdd, onItemRemove) {
|
||||
var instanceRef = useRef(new Map());
|
||||
var heightsRef = useRef(new CacheMap());
|
||||
var promiseIdRef = useRef(0);
|
||||
+ var disabledRef = useRef(disabled);
|
||||
+ disabledRef.current = disabled;
|
||||
function cancelRaf() {
|
||||
promiseIdRef.current += 1;
|
||||
}
|
||||
function collectHeight() {
|
||||
var sync = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
|
||||
cancelRaf();
|
||||
+ if (disabledRef.current) {
|
||||
+ return;
|
||||
+ }
|
||||
var doCollect = function doCollect() {
|
||||
var changed = false;
|
||||
instanceRef.current.forEach(function (element, key) {
|
||||
@@ -79,5 +84,10 @@ export default function useHeights(getKey, onItemAdd, onItemRemove) {
|
||||
useEffect(function () {
|
||||
return cancelRaf;
|
||||
}, []);
|
||||
+ useEffect(function () {
|
||||
+ if (!disabled) {
|
||||
+ collectHeight(true);
|
||||
+ }
|
||||
+ }, [disabled]);
|
||||
return [setInstanceRef, collectHeight, heightsRef.current, updatedMark];
|
||||
}
|
||||
diff --git a/node_modules/rc-virtual-list/lib/List.d.ts b/node_modules/rc-virtual-list/lib/List.d.ts
|
||||
index c517366..e414513 100644
|
||||
--- a/node_modules/rc-virtual-list/lib/List.d.ts
|
||||
+++ b/node_modules/rc-virtual-list/lib/List.d.ts
|
||||
@@ -20,6 +20,8 @@ export interface ListProps<T> extends Omit<React.HTMLAttributes<any>, 'children'
|
||||
data: T[];
|
||||
height?: number;
|
||||
itemHeight?: number;
|
||||
+ /** Treat every item as exactly itemHeight and calculate the visible range in O(1). */
|
||||
+ itemHeightFixed?: boolean;
|
||||
/** If not match virtual scroll condition, Set List still use height of container. */
|
||||
fullHeight?: boolean;
|
||||
itemKey: React.Key | ((item: T) => React.Key);
|
||||
diff --git a/node_modules/rc-virtual-list/lib/List.js b/node_modules/rc-virtual-list/lib/List.js
|
||||
index f0cb64d..d24a9ad 100644
|
||||
--- a/node_modules/rc-virtual-list/lib/List.js
|
||||
+++ b/node_modules/rc-virtual-list/lib/List.js
|
||||
@@ -32,7 +32,7 @@ var _useScrollDrag = _interopRequireDefault(require("./hooks/useScrollDrag"));
|
||||
var _useScrollTo = _interopRequireDefault(require("./hooks/useScrollTo"));
|
||||
var _ScrollBar = _interopRequireDefault(require("./ScrollBar"));
|
||||
var _scrollbarUtil = require("./utils/scrollbarUtil");
|
||||
-var _excluded = ["prefixCls", "className", "height", "itemHeight", "fullHeight", "style", "data", "children", "itemKey", "virtual", "direction", "scrollWidth", "component", "onScroll", "onVirtualScroll", "onVisibleChange", "innerProps", "extraRender", "styles", "showScrollBar"];
|
||||
+var _excluded = ["prefixCls", "className", "height", "itemHeight", "itemHeightFixed", "fullHeight", "style", "data", "children", "itemKey", "virtual", "direction", "scrollWidth", "component", "onScroll", "onVirtualScroll", "onVisibleChange", "innerProps", "extraRender", "styles", "showScrollBar"];
|
||||
var EMPTY_DATA = [];
|
||||
var ScrollStyle = {
|
||||
overflowY: 'auto',
|
||||
@@ -44,6 +44,7 @@ function RawList(props, ref) {
|
||||
className = props.className,
|
||||
height = props.height,
|
||||
itemHeight = props.itemHeight,
|
||||
+ itemHeightFixed = props.itemHeightFixed,
|
||||
_props$fullHeight = props.fullHeight,
|
||||
fullHeight = _props$fullHeight === void 0 ? true : _props$fullHeight,
|
||||
style = props.style,
|
||||
@@ -74,7 +75,7 @@ function RawList(props, ref) {
|
||||
}, [itemKey]);
|
||||
|
||||
// ================================ Height ================================
|
||||
- var _useHeights = (0, _useHeights3.default)(getKey, null, null),
|
||||
+ var _useHeights = (0, _useHeights3.default)(getKey, null, null, itemHeightFixed),
|
||||
_useHeights2 = (0, _slicedToArray2.default)(_useHeights, 4),
|
||||
setInstanceRef = _useHeights2[0],
|
||||
collectHeight = _useHeights2[1],
|
||||
@@ -83,11 +84,12 @@ function RawList(props, ref) {
|
||||
|
||||
// ================================= MISC =================================
|
||||
var useVirtual = !!(virtual !== false && height && itemHeight);
|
||||
- var containerHeight = React.useMemo(function () {
|
||||
+ var measuredContainerHeight = React.useMemo(function () {
|
||||
return Object.values(heights.maps).reduce(function (total, curr) {
|
||||
return total + curr;
|
||||
}, 0);
|
||||
}, [heights.id, heights.maps]);
|
||||
+ var containerHeight = itemHeightFixed ? itemHeight * (data || EMPTY_DATA).length : measuredContainerHeight;
|
||||
var inVirtual = useVirtual && data && (Math.max(itemHeight * data.length, containerHeight) > height || !!scrollWidth);
|
||||
var isRTL = direction === 'rtl';
|
||||
var mergedClassName = (0, _classnames.default)(prefixCls, (0, _defineProperty2.default)({}, "".concat(prefixCls, "-rtl"), isRTL), className);
|
||||
@@ -168,6 +170,28 @@ function RawList(props, ref) {
|
||||
offset: undefined
|
||||
};
|
||||
}
|
||||
+ if (itemHeightFixed && itemHeight > 0) {
|
||||
+ var fixedDataLen = mergedData.length;
|
||||
+ var fixedScrollHeight = fixedDataLen * itemHeight;
|
||||
+ if (fixedDataLen === 0) {
|
||||
+ return {
|
||||
+ scrollHeight: 0,
|
||||
+ start: 0,
|
||||
+ end: -1,
|
||||
+ offset: 0
|
||||
+ };
|
||||
+ }
|
||||
+ var fixedMaxScrollTop = Math.max(0, fixedScrollHeight - height);
|
||||
+ var fixedOffsetTop = Math.max(0, Math.min(fixedMaxScrollTop, offsetTop));
|
||||
+ var fixedStartIndex = Math.min(fixedDataLen - 1, Math.max(0, Math.ceil(fixedOffsetTop / itemHeight) - 1));
|
||||
+ var fixedEndIndex = Math.min(fixedDataLen - 1, Math.floor((fixedOffsetTop + height) / itemHeight) + 1);
|
||||
+ return {
|
||||
+ scrollHeight: fixedScrollHeight,
|
||||
+ start: fixedStartIndex,
|
||||
+ end: fixedEndIndex,
|
||||
+ offset: fixedStartIndex * itemHeight
|
||||
+ };
|
||||
+ }
|
||||
var itemTop = 0;
|
||||
var startIndex;
|
||||
var startOffset;
|
||||
@@ -210,7 +234,7 @@ function RawList(props, ref) {
|
||||
end: endIndex,
|
||||
offset: startOffset
|
||||
};
|
||||
- }, [inVirtual, useVirtual, offsetTop, mergedData, heightUpdatedMark, height]),
|
||||
+ }, [inVirtual, useVirtual, offsetTop, mergedData, heightUpdatedMark, height, itemHeightFixed, itemHeight]),
|
||||
scrollHeight = _React$useMemo.scrollHeight,
|
||||
start = _React$useMemo.start,
|
||||
end = _React$useMemo.end,
|
||||
@@ -222,6 +246,10 @@ function RawList(props, ref) {
|
||||
// Which will make scroll jump.
|
||||
// Let's sync scroll top to avoid jump
|
||||
React.useLayoutEffect(function () {
|
||||
+ if (itemHeightFixed) {
|
||||
+ heights.resetRecord();
|
||||
+ return;
|
||||
+ }
|
||||
var changedRecord = heights.getRecord();
|
||||
if (changedRecord.size === 1) {
|
||||
var recordKey = Array.from(changedRecord.keys())[0];
|
||||
@@ -241,7 +269,7 @@ function RawList(props, ref) {
|
||||
}
|
||||
}
|
||||
heights.resetRecord();
|
||||
- }, [scrollHeight]);
|
||||
+ }, [scrollHeight, itemHeightFixed, heightUpdatedMark]);
|
||||
|
||||
// ================================= Size =================================
|
||||
var _React$useState = React.useState({
|
||||
diff --git a/node_modules/rc-virtual-list/lib/hooks/useHeights.d.ts b/node_modules/rc-virtual-list/lib/hooks/useHeights.d.ts
|
||||
--- a/node_modules/rc-virtual-list/lib/hooks/useHeights.d.ts
|
||||
+++ b/node_modules/rc-virtual-list/lib/hooks/useHeights.d.ts
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { GetKey } from '../interface';
|
||||
import CacheMap from '../utils/CacheMap';
|
||||
-export default function useHeights<T>(getKey: GetKey<T>, onItemAdd?: (item: T) => void, onItemRemove?: (item: T) => void): [
|
||||
+export default function useHeights<T>(getKey: GetKey<T>, onItemAdd?: (item: T) => void, onItemRemove?: (item: T) => void, disabled?: boolean): [
|
||||
setInstanceRef: (item: T, instance: HTMLElement) => void,
|
||||
collectHeight: (sync?: boolean) => void,
|
||||
cacheMap: CacheMap,
|
||||
diff --git a/node_modules/rc-virtual-list/lib/hooks/useHeights.js b/node_modules/rc-virtual-list/lib/hooks/useHeights.js
|
||||
index 471ffc6..a6d2ce5 100644
|
||||
--- a/node_modules/rc-virtual-list/lib/hooks/useHeights.js
|
||||
+++ b/node_modules/rc-virtual-list/lib/hooks/useHeights.js
|
||||
@@ -14,7 +14,7 @@ function parseNumber(value) {
|
||||
var num = parseFloat(value);
|
||||
return isNaN(num) ? 0 : num;
|
||||
}
|
||||
-function useHeights(getKey, onItemAdd, onItemRemove) {
|
||||
+function useHeights(getKey, onItemAdd, onItemRemove, disabled) {
|
||||
var _React$useState = React.useState(0),
|
||||
_React$useState2 = (0, _slicedToArray2.default)(_React$useState, 2),
|
||||
updatedMark = _React$useState2[0],
|
||||
@@ -22,12 +22,17 @@ function useHeights(getKey, onItemAdd, onItemRemove) {
|
||||
var instanceRef = (0, _react.useRef)(new Map());
|
||||
var heightsRef = (0, _react.useRef)(new _CacheMap.default());
|
||||
var promiseIdRef = (0, _react.useRef)(0);
|
||||
+ var disabledRef = (0, _react.useRef)(disabled);
|
||||
+ disabledRef.current = disabled;
|
||||
function cancelRaf() {
|
||||
promiseIdRef.current += 1;
|
||||
}
|
||||
function collectHeight() {
|
||||
var sync = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
|
||||
cancelRaf();
|
||||
+ if (disabledRef.current) {
|
||||
+ return;
|
||||
+ }
|
||||
var doCollect = function doCollect() {
|
||||
var changed = false;
|
||||
instanceRef.current.forEach(function (element, key) {
|
||||
@@ -87,5 +92,10 @@ function useHeights(getKey, onItemAdd, onItemRemove) {
|
||||
(0, _react.useEffect)(function () {
|
||||
return cancelRaf;
|
||||
}, []);
|
||||
+ (0, _react.useEffect)(function () {
|
||||
+ if (!disabled) {
|
||||
+ collectHeight(true);
|
||||
+ }
|
||||
+ }, [disabled]);
|
||||
return [setInstanceRef, collectHeight, heightsRef.current, updatedMark];
|
||||
}
|
||||
BIN
frontend/public/brand-icons/01-database-hug.webp
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
frontend/public/brand-icons/02-database-search.webp
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
frontend/public/brand-icons/03-bandana-badge.webp
Normal file
|
After Width: | Height: | Size: 137 KiB |
BIN
frontend/public/brand-icons/04-magnifier-wink.webp
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
frontend/public/brand-icons/05-window-peek.webp
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
frontend/public/brand-icons/06-hex-collar.webp
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
frontend/public/brand-icons/07-graph-sit.webp
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
frontend/public/brand-icons/08-cloud-banner.webp
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
frontend/public/brand-icons/09-terminal-sit.webp
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
frontend/public/brand-icons/10-compass-bandana.webp
Normal file
|
After Width: | Height: | Size: 140 KiB |
86
frontend/public/brand-icons/manifest.json
Normal file
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"version": 9,
|
||||
"defaultId": "02",
|
||||
"icons": [
|
||||
{
|
||||
"id": "01",
|
||||
"slug": "database-hug",
|
||||
"titleZh": "抱库小狗",
|
||||
"titleEn": "Database hug",
|
||||
"iconPath": "/brand-icons/01-database-hug.webp",
|
||||
"fullPath": "/brand-icons/01-database-hug.webp"
|
||||
},
|
||||
{
|
||||
"id": "02",
|
||||
"slug": "database-search",
|
||||
"titleZh": "搜库小狗",
|
||||
"titleEn": "Database search",
|
||||
"iconPath": "/brand-icons/02-database-search.webp",
|
||||
"fullPath": "/brand-icons/02-database-search.webp"
|
||||
},
|
||||
{
|
||||
"id": "03",
|
||||
"slug": "bandana-badge",
|
||||
"titleZh": "头巾徽章",
|
||||
"titleEn": "Bandana badge",
|
||||
"iconPath": "/brand-icons/03-bandana-badge.webp",
|
||||
"fullPath": "/brand-icons/03-bandana-badge.webp"
|
||||
},
|
||||
{
|
||||
"id": "04",
|
||||
"slug": "magnifier-wink",
|
||||
"titleZh": "放大镜眨眼",
|
||||
"titleEn": "Magnifier wink",
|
||||
"iconPath": "/brand-icons/04-magnifier-wink.webp",
|
||||
"fullPath": "/brand-icons/04-magnifier-wink.webp"
|
||||
},
|
||||
{
|
||||
"id": "05",
|
||||
"slug": "window-peek",
|
||||
"titleZh": "窗口探头",
|
||||
"titleEn": "Window peek",
|
||||
"iconPath": "/brand-icons/05-window-peek.webp",
|
||||
"fullPath": "/brand-icons/05-window-peek.webp"
|
||||
},
|
||||
{
|
||||
"id": "06",
|
||||
"slug": "hex-collar",
|
||||
"titleZh": "六边项圈",
|
||||
"titleEn": "Hex collar",
|
||||
"iconPath": "/brand-icons/06-hex-collar.webp",
|
||||
"fullPath": "/brand-icons/06-hex-collar.webp"
|
||||
},
|
||||
{
|
||||
"id": "07",
|
||||
"slug": "graph-sit",
|
||||
"titleZh": "关系图",
|
||||
"titleEn": "Graph sit",
|
||||
"iconPath": "/brand-icons/07-graph-sit.webp",
|
||||
"fullPath": "/brand-icons/07-graph-sit.webp"
|
||||
},
|
||||
{
|
||||
"id": "08",
|
||||
"slug": "cloud-banner",
|
||||
"titleZh": "云朵横幅",
|
||||
"titleEn": "Cloud banner",
|
||||
"iconPath": "/brand-icons/08-cloud-banner.webp",
|
||||
"fullPath": "/brand-icons/08-cloud-banner.webp"
|
||||
},
|
||||
{
|
||||
"id": "09",
|
||||
"slug": "terminal-sit",
|
||||
"titleZh": "终端旁坐",
|
||||
"titleEn": "Terminal sit",
|
||||
"iconPath": "/brand-icons/09-terminal-sit.webp",
|
||||
"fullPath": "/brand-icons/09-terminal-sit.webp"
|
||||
},
|
||||
{
|
||||
"id": "10",
|
||||
"slug": "compass-bandana",
|
||||
"titleZh": "罗盘头巾",
|
||||
"titleEn": "Compass bandana",
|
||||
"iconPath": "/brand-icons/10-compass-bandana.webp",
|
||||
"fullPath": "/brand-icons/10-compass-bandana.webp"
|
||||
}
|
||||
]
|
||||
}
|
||||
1
frontend/public/db-icons/chroma.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="163" height="40" fill="none" xmlns="http://www.w3.org/2000/svg"><mask id="prefix__a" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="2" width="53" height="36"><path d="M52.503 2.501H.003v35h52.5v-35z" fill="#fff"/></mask><g mask="url(#prefix__a)"><path d="M17.503 2.501c-9.665 0-17.5 7.835-17.5 17.5s7.835 17.5 17.5 17.5 17.5-7.835 17.5-17.5-7.835-17.5-17.5-17.5z" fill="#327EFF"/><path d="M35.003 2.501c-9.665 0-17.5 7.835-17.5 17.5s7.835 17.5 17.5 17.5 17.5-7.834 17.5-17.5c0-9.665-7.835-17.5-17.5-17.5z" fill="#FFDE2D"/><path d="M17.503 20.002c0-9.665 7.835-17.5 17.5-17.5v17.5h-17.5z" fill="#FF6446"/><path d="M35.003 20.001c0 9.665-7.835 17.5-17.5 17.5v-17.5h17.5z" fill="#FF6446"/></g><path d="M68.543 30.45c-6.03 0-10.32-4.65-10.32-11.1 0-6.36 3.96-11.22 10.29-11.22 5.28 0 8.4 3.15 8.85 7.23h-4.32c-.39-2.16-2.01-3.57-4.53-3.57-3.96 0-5.85 3.3-5.85 7.56 0 4.38 2.28 7.53 5.88 7.53 2.55 0 4.35-1.53 4.62-3.78h4.26a7.596 7.596 0 01-2.37 5.07c-1.47 1.38-3.54 2.28-6.51 2.28zM83.41 8.55v8.07h.09c1.11-1.62 2.37-2.43 4.47-2.43 3.18 0 5.31 2.4 5.31 5.76V30H89.2v-9.45c0-1.65-.96-2.82-2.67-2.82-1.8 0-3.12 1.44-3.12 3.54V30h-4.08V8.55h4.08zm16.315 6.06v2.46h.09c.93-1.74 1.98-2.64 3.78-2.64.45 0 .72.03.96.12v3.57h-.09c-2.67-.27-4.59 1.14-4.59 4.38V30h-4.08V14.61h3.93zm13.438 15.84c-4.77 0-8.04-3.54-8.04-8.13 0-4.59 3.27-8.13 8.04-8.13s8.04 3.54 8.04 8.13c0 4.59-3.27 8.13-8.04 8.13zm0-3.12c2.49 0 3.9-2.01 3.9-5.01s-1.41-5.04-3.9-5.04c-2.52 0-3.9 2.04-3.9 5.04s1.38 5.01 3.9 5.01zM123.1 30V14.61h3.93v2.07h.09c.84-1.41 2.34-2.49 4.47-2.49 1.95 0 3.51 1.08 4.26 2.7h.06c1.05-1.68 2.67-2.7 4.62-2.7 3.24 0 5.07 2.1 5.07 5.46V30h-4.08v-9.66c0-1.74-.87-2.64-2.37-2.64-1.71 0-2.76 1.32-2.76 3.36V30h-4.08v-9.66c0-1.74-.87-2.64-2.37-2.64-1.65 0-2.76 1.32-2.76 3.36V30h-4.08zm34.71 0c-.24-.3-.39-1.02-.48-1.71h-.06c-.78 1.17-1.89 2.07-4.53 2.07-3.15 0-5.37-1.65-5.37-4.71 0-3.39 2.76-4.47 6.18-4.95 2.55-.36 3.72-.57 3.72-1.74 0-1.11-.87-1.83-2.58-1.83-1.92 0-2.85.69-2.97 2.16h-3.63c.12-2.7 2.13-5.07 6.629-5.07 4.621 0 6.481 2.07 6.481 5.67v7.83c0 1.17.18 1.86.54 2.13V30h-3.93zm-4.08-2.49c2.34 0 3.63-1.44 3.63-2.94v-2.31c-.72.42-1.83.66-2.851.9-2.129.48-3.179.96-3.179 2.4s.96 1.95 2.4 1.95z" fill="#000"/></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
BIN
frontend/public/db-icons/dameng.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
10
frontend/public/db-icons/elasticsearch.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-15 0 286 286" width="800px" height="800px" preserveAspectRatio="xMidYMid">
|
||||
<g>
|
||||
<path d="M14.3443,80.1733 L203.5503,80.1733 C224.4013,80.1733 243.0203,70.6123 255.5133,55.6863 C229.4533,21.8353 188.5523,0.0003 142.5303,0.0003 C86.1783,0.0003 37.4763,32.7113 14.3443,80.1733" fill="#F0BF1A"/>
|
||||
<path d="M187.5152,102.4438 L5.7552,102.4438 C2.0332,115.1648 0.0002,128.6068 0.0002,142.5298 C0.0002,156.4538 2.0332,169.8968 5.7552,182.6168 L187.5152,182.6168 C209.3402,182.6168 227.6022,164.8008 227.6022,142.5298 C227.6022,120.2598 209.7862,102.4438 187.5152,102.4438" fill="#07A5DE"/>
|
||||
<path d="M255.9996,228.7548 C243.5856,214.1638 225.1166,204.8868 204.4406,204.8868 L14.3446,204.8868 C37.4766,252.3498 86.1786,285.0598 142.5296,285.0598 C188.8356,285.0598 229.9656,262.9628 255.9996,228.7548" fill="#3EBEB0"/>
|
||||
<path d="M5.7555,102.4438 C2.0325,115.1648 0.0005,128.6068 0.0005,142.5298 C0.0005,156.4538 2.0325,169.8968 5.7555,182.6168 L124.7135,182.6168 C127.8315,170.5908 129.6125,157.2288 129.6125,142.5298 C129.6125,127.8318 127.8315,114.4698 124.7135,102.4438 L5.7555,102.4438 Z" fill="#231F20"/>
|
||||
<path d="M70.8199,19.1528 C46.7669,33.4058 26.7239,54.7848 14.2529,80.1738 L119.3689,80.1738 C108.6789,55.6758 91.7539,35.1878 70.8199,19.1528" fill="#D7A229"/>
|
||||
<path d="M75.274,268.1347 C95.762,251.6547 112.242,229.8297 122.487,204.8867 L14.253,204.8867 C27.615,231.6117 48.995,253.8817 75.274,268.1347" fill="#019B8F"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
frontend/public/db-icons/gaussdb.ico
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
frontend/public/db-icons/goldendb.ico
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
frontend/public/db-icons/highgo.ico
Normal file
|
After Width: | Height: | Size: 17 KiB |
57
frontend/public/db-icons/iotdb.svg
Normal file
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1"
|
||||
id="Õ_xBA__x2264__x201E__1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 107.7 107.7"
|
||||
style="enable-background:new 0 0 107.7 107.7;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#9E2878;}
|
||||
</style>
|
||||
<g>
|
||||
<g id="g1133" transform="translate(-244.51235,-228.78793)">
|
||||
<path id="path1119" class="st0" d="M340.8,253.8c2.6-1,5.5,0.4,6.2,3c0.7,2.6-1.1,5.7-3.7,6.4c-3.2,0.6-6.2-1.4-9.3,0.2
|
||||
c-3.1,1.7-4,5.2-4.7,8.3c-1.4,5-8.5,7.3-12.1,4.2c-3.3-2.4-3.4-7.8-0.2-11c2.2-2.5,5.9-3.3,8.8-2c2.7,1.2,6,1.7,8.6-0.4
|
||||
C337.5,260.1,336.7,255.1,340.8,253.8L340.8,253.8z"/>
|
||||
<path id="path1121" class="st0" d="M280.5,244.7c4.2-2.2,9.5,1.5,8.2,6.1c-1.4,5.4-0.7,11.5,2.9,15.5c3.4,4,9.8,4.8,14.6,1.9
|
||||
c3.7-2.1,6-5.8,7.4-9.6c1-3.1,0.6-6.2,1.1-9.3c1-3.8,5.8-6,9.1-4.2c3.2,1.4,4,5.9,1.7,8.8c-1.4,2.2-4.2,2.7-6.3,3.8
|
||||
c-3.6,1.9-6.5,4.9-8.4,8.6c-1.2,2.1-1.1,4.5-1.7,6.7c-0.9,1.9-3,2.7-4.8,2.9c-4.8,0.6-9.5,3-13,6.7c-1.7,1.7-3.1,4.2-5.6,4.2
|
||||
c-2.7-0.2-4.7-2.6-7.4-2.7c-4.9-0.7-10.2,0.7-14.4,4c-3.7,3.1-9.4,0.8-9.6-3.7c-0.9-5.1,6.2-9.5,10.1-6.2c4.3,4,10.8,5.6,16.9,3.7
|
||||
c5.6-1.9,9.7-8.3,8.6-14c-0.9-4.9-4.2-9.3-8.6-11.4c-1.7-0.8-3.6-1.6-4.2-3.5C275.6,250.2,277.3,246.2,280.5,244.7L280.5,244.7z"
|
||||
/>
|
||||
<path id="path1123" class="st0" d="M277.8,235.9c2.2-0.8,4.2,1.3,3.3,3.4c-0.6,2.1-3.7,2.7-4.8,1.1
|
||||
C275,238.9,276,236.4,277.8,235.9z"/>
|
||||
<path id="path1125" class="st0" d="M246.9,278.8c2.2-0.8,4.2,1.2,3.3,3.4c-0.6,2.1-3.7,2.7-4.8,1C244,281.9,245,279.3,246.9,278.8
|
||||
L246.9,278.8z"/>
|
||||
<path id="path1127" class="st0" d="M328.2,236.2c2.2-0.7,4.2,1.3,3.3,3.5c-0.6,2-3.7,2.7-4.8,1
|
||||
C325.4,239.2,326.3,236.8,328.2,236.2z"/>
|
||||
<path id="path1129" class="st0" d="M253.6,257.7c0.4-3.7,5.5-5.9,8.1-3.6c1.9,1.1,1.6,3.6,2.2,5.4c0.4,2.4,2.7,4.3,5.2,4.3
|
||||
c3.2,0.3,6.4-2.3,9.5-1.2c4.8,1.2,6.5,7.5,3.2,11.5c-2.9,4.1-9.3,4.5-12,0.7c-2.4-2.8-0.5-7.1-2.7-10.1c-1.7-2.9-5.4-2.7-8.3-1.9
|
||||
C255.8,263.8,253,260.7,253.6,257.7L253.6,257.7z"/>
|
||||
<path id="path1131" class="st0" d="M300.8,230c3.3-1.9,7.5,0.9,6.7,4.6c0,2.3-2.2,3.6-3.5,5.2c-1.9,1.9-2.3,4.9-1.2,7
|
||||
c1.3,2.9,4.9,4,5.5,7.2c1.2,4.8-3.3,10.1-8.2,9.8c-4.8,0.1-8.3-5-6.3-9.5c1.2-3.8,5.8-4.9,7.2-8.5c1.7-3.2-0.4-6.1-2.4-8.1
|
||||
C296.7,235.6,297.9,231.4,300.8,230z"/>
|
||||
</g>
|
||||
<g id="g1149" transform="translate(-244.51235,-228.78793)">
|
||||
<path id="path1135" class="st0" d="M256,311.5c-2.6,1-5.5-0.4-6.2-3c-0.7-2.6,1.1-5.7,3.7-6.4c3.2-0.6,6.2,1.4,9.3-0.2
|
||||
c3.1-1.6,4-5.1,4.7-8.2c1.4-5,8.5-7.3,12.1-4.2c3.3,2.4,3.4,7.8,0.2,10.9c-2.2,2.5-5.9,3.3-8.8,2c-2.7-1.2-6-1.7-8.6,0.4
|
||||
C259.1,305,259.9,310.2,256,311.5L256,311.5z"/>
|
||||
<path id="path1137" class="st0" d="M316.1,320.5c-4.2,2.2-9.5-1.5-8.2-6c1.4-5.4,0.7-11.6-2.9-15.6c-3.4-4-9.8-4.7-14.6-1.9
|
||||
c-3.7,2-6,5.7-7.4,9.5c-1,3.1-0.6,6.2-1.1,9.3c-1,3.8-5.8,6-9.1,4.3c-3.2-1.4-4-5.9-1.7-8.9c1.4-2.2,4.2-2.7,6.3-3.8
|
||||
c3.6-1.9,6.5-4.9,8.4-8.6c1.2-2,1.1-4.5,1.7-6.6c0.9-1.9,3-2.7,4.8-2.9c4.8-0.6,9.5-3,13-6.7c1.7-1.6,3.1-4.2,5.6-4.1
|
||||
c2.7,0.1,4.7,2.5,7.4,2.7c4.9,0.6,10.2-0.8,14.4-4c3.7-3.2,9.4-0.9,9.6,3.6c0.9,5.1-6.2,9.5-10.1,6.2c-4.3-4-10.8-5.6-16.9-3.7
|
||||
c-5.6,1.9-9.7,8.3-8.6,14c0.9,4.8,4.2,9.3,8.6,11.3c1.7,0.8,3.6,1.6,4.2,3.5C321.2,314.9,319.4,319.1,316.1,320.5L316.1,320.5z"/>
|
||||
<path id="path1139" class="st0" d="M318.9,329.4c-2.2,0.8-4.2-1.3-3.3-3.4c0.6-2.1,3.7-2.7,4.8-1.1
|
||||
C321.7,326.3,320.7,328.8,318.9,329.4z"/>
|
||||
<path id="path1141" class="st0" d="M349.9,286.5c-2.2,0.7-4.2-1.3-3.3-3.5c0.6-2.1,3.7-2.7,4.8-1
|
||||
C352.8,283.5,351.8,285.9,349.9,286.5z"/>
|
||||
<path id="path1143" class="st0" d="M268.5,329c-2.2,0.7-4.2-1.3-3.3-3.5c0.6-2,3.7-2.7,4.8-1C271.3,325.9,270.3,328.5,268.5,329z"
|
||||
/>
|
||||
<path id="path1145" class="st0" d="M343.1,307.4c-0.4,3.7-5.5,5.9-8.1,3.6c-1.9-1.1-1.6-3.6-2.2-5.4c-0.4-2.4-2.7-4.3-5.2-4.3
|
||||
c-3.2-0.3-6.4,2.3-9.5,1.2c-4.8-1.2-6.5-7.5-3.2-11.5c2.9-4.1,9.3-4.5,12-0.7c2.4,2.8,0.5,7,2.7,10c1.7,2.9,5.4,2.7,8.3,1.9
|
||||
C341,301.4,343.8,304.4,343.1,307.4L343.1,307.4z"/>
|
||||
<path id="path1147" class="st0" d="M295.8,335.2c-3.3,2-7.5-0.9-6.7-4.6c0-2.3,2.2-3.6,3.5-5.2c1.9-1.9,2.3-4.9,1.2-7
|
||||
c-1.3-2.9-4.9-4-5.5-7.2c-1.2-4.8,3.3-10.1,8.2-9.8c4.8-0.1,8.3,5,6.3,9.6c-1.2,3.7-5.8,4.8-7.2,8.4c-1.7,3.2,0.4,6.1,2.4,8.2
|
||||
C300,329.6,298.8,333.8,295.8,335.2L295.8,335.2z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
BIN
frontend/public/db-icons/iris.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |