Compare commits
786 Commits
v0.7.5
...
feature/20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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
|
||||
818
.github/workflows/dev-build.yml
vendored
251
.github/workflows/docker-images.yml
vendored
Normal file
@@ -0,0 +1,251 @@
|
||||
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 }}
|
||||
|
||||
- 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 34116:34116 -e GONAVI_WEB_ADDR=0.0.0.0:34116 -e GONAVI_WEB_PASSWORD=123456 codex-smoke/${{ matrix.image_name }}:local web-server)"
|
||||
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 http://127.0.0.1:34116/__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 http://127.0.0.1:34116/__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":""}' http://127.0.0.1:34116/__gonavi/auth/login)"
|
||||
grep -Fq '"success":true' <<<"$login"
|
||||
app_status="$(curl --silent --show-error -o /dev/null -w '%{http_code}' -b "$cookie_jar" http://127.0.0.1:34116/)"
|
||||
[[ "$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 }}
|
||||
|
||||
- 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 }}
|
||||
815
.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 包”的分发能力。
|
||||
55
Dockerfile.build-env
Normal file
@@ -0,0 +1,55 @@
|
||||
# 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/*
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
CMD ["bash"]
|
||||
47
Dockerfile.mcp-server
Normal file
@@ -0,0 +1,47 @@
|
||||
# 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 \
|
||||
&& chown -R gonavi:gonavi /data /var/lib/gonavi
|
||||
|
||||
COPY --from=builder /out/gonavi-mcp-server /usr/local/bin/gonavi-mcp-server
|
||||
|
||||
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"]
|
||||
62
Dockerfile.web-server
Normal file
@@ -0,0 +1,62 @@
|
||||
# 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 ./
|
||||
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 \
|
||||
&& chown -R gonavi:gonavi /data /var/lib/gonavi
|
||||
|
||||
COPY --from=builder /out/gonavi /usr/local/bin/gonavi
|
||||
|
||||
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"]
|
||||
165
README.md
@@ -34,9 +34,17 @@ GoNavi is designed for developers and DBAs who need a unified desktop experience
|
||||
| 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 |
|
||||
@@ -44,13 +52,20 @@ GoNavi is designed for developers and DBAs who need a unified desktop experience
|
||||
| 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>
|
||||
@@ -73,6 +88,9 @@ GoNavi is designed for developers and DBAs who need a unified desktop experience
|
||||
- **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.
|
||||
- **Built-in MCP Workflow**: Manage MCP servers in AI Settings, install GoNavi MCP to Claude Code / Codex, or expose Streamable HTTP for remote Agents.
|
||||
- **Remote-Agent Boundary**: Keep saved connections and database passwords on the host running GoNavi while cloud Agents consume schema tools over MCP.
|
||||
- **Safety Guardrails**: Remote `schema-only` mode omits `execute_sql`; when SQL execution is enabled, non-read-only statements still require explicit `allowMutating=true`.
|
||||
|
||||
### Performance
|
||||
- **Smooth interaction under load**: optimized table interaction (including column resize workflow on large datasets).
|
||||
@@ -108,6 +126,10 @@ GoNavi is designed for developers and DBAs who need a unified desktop experience
|
||||
|
||||
### Observability and Update
|
||||
- SQL execution logs with timing information.
|
||||
- SQL audit center for persisted SQL-editor operations, transaction boundaries, data edits/imports, SQL-file jobs, data sync, object DDL, table design, message publishing, and built-in AI/MCP database actions, with filters, transaction timelines, JSON/CSV export, retention controls, and writer health.
|
||||
- Audit content is redacted by default: Redis values and message payloads are hidden, while non-SQL operations that cannot be parsed safely retain metadata only. The audit database lives at `audit/sql_audit.db` under the active data root.
|
||||
- SQL-file, import, and sync entries are privacy-safe task summaries (content hash, target/count metadata), not raw per-row evidence. Source values identify the called GoNavi entry point rather than an unforgeable user identity.
|
||||
- The local SHA-256 hash chain is a consistency check, not a keyed signature or tamper-proof guarantee. Known persistence gaps are marked with `audit_gap` after recovery.
|
||||
- Startup/scheduled/manual update checks.
|
||||
|
||||
### UI/UX
|
||||
@@ -133,17 +155,25 @@ GoNavi is designed for developers and DBAs who need a unified desktop experience
|
||||
- [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`
|
||||
`go install github.com/wailsapp/wails/v2/cmd/wails@v2.11.0`
|
||||
|
||||
### Development Mode
|
||||
|
||||
```bash
|
||||
```shell
|
||||
# Clone
|
||||
git clone https://github.com/Syngnat/GoNavi.git
|
||||
cd GoNavi
|
||||
|
||||
# Start development with hot reload
|
||||
wails dev
|
||||
|
||||
# Faster local startup when exported Go method signatures are unchanged
|
||||
node tools/wails-fast-dev.mjs
|
||||
|
||||
# Refresh Wails JS bindings after changing exported Go method signatures
|
||||
node tools/wails-fast-dev.mjs --refresh-bindings
|
||||
|
||||
# Windows desktop builds use an opaque WebView without an Acrylic backdrop by default.
|
||||
```
|
||||
|
||||
### Build
|
||||
@@ -158,6 +188,127 @@ wails build -clean
|
||||
|
||||
Artifacts are generated in `build/bin`.
|
||||
|
||||
### Browser Access Mode (Web Server, Experimental)
|
||||
|
||||
GoNavi provides a `web-server` mode that reuses the same Go backend and React frontend for browser access (**not** a containerized desktop Wails window).
|
||||
|
||||
#### Run locally
|
||||
|
||||
```powershell
|
||||
go build .
|
||||
.\GoNavi-Wails.exe web-server --addr 127.0.0.1:34116
|
||||
```
|
||||
|
||||
Without a configured password, the first browser visit is redirected to `/setup` to create the web admin password. Google Authenticator is optional but supported out of the box, together with `web_auth.json`, session cookies, recovery codes, and login rate limiting.
|
||||
|
||||
Current scope already includes:
|
||||
|
||||
- Browser-side Wails bridge (`window.go.*` / `window.runtime.*` -> HTTP / SSE)
|
||||
- First-run setup page, login page, and logout endpoint
|
||||
- Session idle timeout / absolute timeout / remember-login window
|
||||
- Google Authenticator TOTP plus recovery codes
|
||||
- Docker / Compose packaging (below)
|
||||
|
||||
Still in progress:
|
||||
|
||||
- Browser upload/download workbenches for external SQL and connection-package flows
|
||||
- More web capability gating for desktop-only features
|
||||
- Reverse-proxy / HTTPS / zero-trust deployment guidance
|
||||
|
||||
#### Docker / Podman (Web Server)
|
||||
|
||||
```bash
|
||||
cp docker.web-server.env.example docker.web-server.env
|
||||
# set GONAVI_HOST_DATA_ROOT to an absolute path
|
||||
# optionally set GONAVI_WEB_PASSWORD to at least 6 characters
|
||||
docker compose --env-file docker.web-server.env -f docker-compose.web-server.yml up -d
|
||||
```
|
||||
|
||||
You can alternatively put the same variables in the project-level `.env` file and omit `--env-file docker.web-server.env`.
|
||||
|
||||
Open `http://127.0.0.1:34116` (or the mapped host port). When `GONAVI_WEB_PASSWORD` is set, a newly created container synchronizes it and opens the login page directly. Otherwise, complete `/setup` on first visit.
|
||||
|
||||
Mount the GoNavi active data root at `/data`. Prefer including `connections.json`, `daily_secrets.json`, and optional `drivers/`. Web auth state is stored as `web_auth.json` in that directory.
|
||||
|
||||
`GONAVI_WEB_PASSWORD` takes precedence over the password hash already stored in `web_auth.json`, while preserving 2FA and session settings. Removing the variable leaves the last synchronized password active. Treat the env file as a secret because the plaintext value is visible to users who can inspect the container; restrict its file permissions.
|
||||
|
||||
After changing the env password, recreate the container so Compose reloads the env file:
|
||||
|
||||
```bash
|
||||
docker compose --env-file docker.web-server.env -f docker-compose.web-server.yml up -d --force-recreate
|
||||
```
|
||||
|
||||
Docker Desktop Restart and `docker restart` keep the old container environment and do not reload the env file.
|
||||
|
||||
On a new deployment, an environment password starts password-only authentication. To enable 2FA, leave `GONAVI_WEB_PASSWORD` empty for the initial `/setup`, then set it to the same password before recreating the container.
|
||||
|
||||
The container listens on `0.0.0.0:34116` by default (`GONAVI_WEB_ADDR`). **Do not expose an unhardened Web entrypoint to the public internet**; use a reverse proxy and HTTPS for production.
|
||||
|
||||
Default Compose pulls the GHCR image. For a local source build, add the override:
|
||||
|
||||
```bash
|
||||
docker compose --env-file docker.web-server.env \
|
||||
-f docker-compose.web-server.yml \
|
||||
-f docker-compose.web-server.local.yml \
|
||||
up -d --build
|
||||
```
|
||||
|
||||
Health check: `GET /__gonavi/healthz`.
|
||||
|
||||
### Docker / Podman (MCP Server)
|
||||
|
||||
Container packaging also provides `gonavi-mcp-server` (MCP HTTP only; no desktop GUI / Web UI).
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
Mount the GoNavi active data root into the container. The mounted directory should contain `connections.json`, `daily_secrets.json`, and optional `drivers/` assets when optional driver agents are required.
|
||||
|
||||
The default Compose file pulls the published GHCR image. For local source builds, add the override file:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
For Podman, use the same published OCI image with `podman run`, or the native Quadlet example under [deploy/podman/gonavi-mcp-server](deploy/podman/gonavi-mcp-server). That path is intended for Linux servers / NAS hosts where rootless systemd services are preferred.
|
||||
|
||||
Deployment matrix:
|
||||
|
||||
- Web UI: `docker-compose.web-server.yml` (`ghcr.io/syngnat/gonavi-web-server`)
|
||||
- MCP: `docker-compose.mcp-server.yml` (`ghcr.io/syngnat/gonavi-mcp-server`)
|
||||
- Podman / Quadlet: [deploy/podman/gonavi-mcp-server](deploy/podman/gonavi-mcp-server)
|
||||
- Kubernetes: [deploy/k8s/gonavi-mcp-server](deploy/k8s/gonavi-mcp-server) (`kustomization.yaml` + overlays)
|
||||
- Helm chart: [deploy/helm/gonavi-mcp-server](deploy/helm/gonavi-mcp-server)
|
||||
- Build-only Linux environment: `Dockerfile.build-env`
|
||||
|
||||
See [cmd/gonavi-mcp-server/README.md](cmd/gonavi-mcp-server/README.md) for deployment details and security boundaries.
|
||||
|
||||
### Docker / Podman (Build Environment Only)
|
||||
|
||||
If you only need a reproducible Linux build environment for Wails, use `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
|
||||
```
|
||||
|
||||
The same Dockerfile also works with Podman, for example `podman build -f Dockerfile.build-env -t localhost/gonavi-build-env:local .` and `podman run --rm -it -v "$PWD:/workspace" -w /workspace localhost/gonavi-build-env:local bash`.
|
||||
|
||||
The default image installs the WebKitGTK 4.0 build toolchain for broader Linux/NAS compatibility. The image bases are multi-arch, so `amd64` and `arm64` follow the target container platform.
|
||||
|
||||
Published images are pushed to GHCR:
|
||||
|
||||
- `ghcr.io/syngnat/gonavi-web-server:latest`
|
||||
- `ghcr.io/syngnat/gonavi-mcp-server:latest`
|
||||
- `ghcr.io/syngnat/gonavi-build-env:latest`
|
||||
|
||||
This image is for building Linux artifacts only. It does not turn the Wails desktop GUI into a browser-accessible web application.
|
||||
|
||||
### Cross-Platform Release (GitHub Actions)
|
||||
|
||||
The repository includes a release workflow.
|
||||
@@ -203,6 +354,16 @@ sudo apt-get install -y libgtk-3-0 libwebkit2gtk-4.0-37 libjavascriptcoregtk-4.0
|
||||
|
||||
If you use Linux artifacts with the `-WebKit41` suffix, prefer Debian 13 / Ubuntu 24.04+.
|
||||
|
||||
### Linux: Chinese text appears as square boxes
|
||||
|
||||
Minimal Ubuntu 24.04 LTS desktop/server environments may not include Chinese CJK fonts. Install Noto / WenQuanYi fonts and restart GoNavi:
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y fonts-noto-cjk fonts-wqy-microhei
|
||||
fc-cache -fv
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
167
README.zh-CN.md
@@ -33,9 +33,17 @@ GoNavi 面向开发者与 DBA,核心目标是让数据库操作在桌面端做
|
||||
| 类别 | 数据源 | 驱动模式 | 典型能力 |
|
||||
|---|---|---|---|
|
||||
| 关系型 | 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 执行 |
|
||||
@@ -43,13 +51,20 @@ GoNavi 面向开发者与 DBA,核心目标是让数据库操作在桌面端做
|
||||
| 关系型 | 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>
|
||||
@@ -72,6 +87,9 @@ GoNavi 面向开发者与 DBA,核心目标是让数据库操作在桌面端做
|
||||
- **多模型服务商支持**:内置跨平台接入 OpenAI, Google Gemini, Anthropic Claude,同时支持任意自定义兼容 OpenAI 格式的 API。
|
||||
- **关联表结构上下文**:原生支持将当前数据库表结构直接提取作为上下文发送给 AI,让 SQL 生成、分析变得更精准。
|
||||
- **快捷指令**:内置多种快捷对话指(如一键生成 SQL、解释执行逻辑、分析性能优化、表字段代码评审等)。
|
||||
- **内置 MCP 工作流**:可在 AI 设置里管理 MCP 服务,一键安装 GoNavi MCP 到 Claude Code / Codex,或开启 Streamable HTTP 供远端 Agent 使用。
|
||||
- **远端 Agent 边界清晰**:数据库连接与密码继续保留在运行 GoNavi 的主机上,云端 Agent 通过 MCP 读取结构与上下文。
|
||||
- **安全控制可追溯**:远端 `schema-only` 模式默认不暴露 `execute_sql`;如开启 SQL 执行,非只读语句仍需显式传入 `allowMutating=true`。
|
||||
|
||||
### 性能与交互
|
||||
- 大数据场景下保持流畅交互(含 DataGrid 列宽拖拽、批量编辑流程优化)。
|
||||
@@ -102,6 +120,10 @@ GoNavi 面向开发者与 DBA,核心目标是让数据库操作在桌面端做
|
||||
|
||||
### 可观测性与更新
|
||||
- SQL 执行日志(含耗时)。
|
||||
- SQL 审计中心:持久化记录 SQL 编辑器、事务边界、数据编辑/导入、SQL 文件任务、数据同步、对象 DDL、表设计、消息发布以及内置 AI/MCP 数据库操作,支持筛选、事务时间线、JSON/CSV 导出、保留策略与写入健康状态。
|
||||
- 审计内容默认脱敏;Redis 隐藏值与消息载荷,无法安全解析的非 SQL 操作仅保留元数据。审计库位于当前数据目录的 `audit/sql_audit.db`。
|
||||
- SQL 文件、导入和同步记录为隐私安全的任务摘要(内容哈希、目标与计数元数据),不是逐行原始证据;来源字段表示调用的 GoNavi 功能入口,不代表不可伪造的用户身份。
|
||||
- 本地 SHA-256 哈希链用于一致性检查,并非密钥签名或不可篡改证明;写入故障恢复后会用 `audit_gap` 标记已知空洞。
|
||||
- 启动/定时/手动更新检查。
|
||||
|
||||
### UI 体验
|
||||
@@ -127,17 +149,25 @@ GoNavi 面向开发者与 DBA,核心目标是让数据库操作在桌面端做
|
||||
- [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`
|
||||
`go install github.com/wailsapp/wails/v2/cmd/wails@v2.11.0`
|
||||
|
||||
### 开发模式
|
||||
|
||||
```bash
|
||||
```shell
|
||||
# 克隆项目
|
||||
git clone https://github.com/Syngnat/GoNavi.git
|
||||
cd GoNavi
|
||||
|
||||
# 启动开发(热重载)
|
||||
wails dev
|
||||
|
||||
# 本地快速启动:未修改 Go 导出方法签名时使用
|
||||
node tools/wails-fast-dev.mjs
|
||||
|
||||
# 修改 Go 导出方法签名后刷新 Wails JS 绑定
|
||||
node tools/wails-fast-dev.mjs --refresh-bindings
|
||||
|
||||
# Windows 桌面端默认使用不透明 WebView,并关闭 Acrylic 背景
|
||||
```
|
||||
|
||||
### 编译构建
|
||||
@@ -152,6 +182,129 @@ wails build -clean
|
||||
|
||||
构建产物位于 `build/bin`。
|
||||
|
||||
### 浏览器访问版(Web Server,实验中)
|
||||
|
||||
仓库主程序提供 `web-server` 运行模式,用同一套 Go 后端 + React 前端提供浏览器访问入口(**不是**桌面 Wails 窗口容器化)。
|
||||
|
||||
#### 本机直接启动
|
||||
|
||||
```powershell
|
||||
go build .
|
||||
.\GoNavi-Wails.exe web-server --addr 127.0.0.1:34116
|
||||
```
|
||||
|
||||
未配置密码时,首次访问会进入 `/setup` 完成 Web 管理员密码初始化;可选启用 Google Authenticator,服务端会落地 `web_auth.json`、Session Cookie、恢复码与登录失败限流。
|
||||
|
||||
当前阶段已具备:
|
||||
|
||||
- 浏览器端 Wails bridge(`window.go.*` / `window.runtime.*` -> HTTP / SSE)
|
||||
- 首次初始化页、登录页、登出接口
|
||||
- Session 空闲超时 / 绝对超时 / 记住登录时长
|
||||
- Google Authenticator TOTP 与恢复码
|
||||
- Docker / Compose 部署入口(见下方)
|
||||
|
||||
当前仍在继续收口:
|
||||
|
||||
- 外部 SQL / 连接包导入导出等文件型能力的浏览器上传下载工作台
|
||||
- 更多桌面专属能力的 Web 门禁与替代交互
|
||||
- 反向代理 / HTTPS / 零信任部署说明
|
||||
|
||||
#### Docker / Podman(Web Server)
|
||||
|
||||
```bash
|
||||
cp docker.web-server.env.example docker.web-server.env
|
||||
# 编辑 GONAVI_HOST_DATA_ROOT 为绝对路径
|
||||
# 可选设置至少 6 位的 GONAVI_WEB_PASSWORD
|
||||
docker compose --env-file docker.web-server.env -f docker-compose.web-server.yml up -d
|
||||
```
|
||||
|
||||
也可以把相同变量写入项目目录下的标准 `.env` 文件,此时可省略 `--env-file docker.web-server.env`。
|
||||
|
||||
浏览器打开 `http://127.0.0.1:34116`(或宿主机映射端口)。设置 `GONAVI_WEB_PASSWORD` 后,新创建的容器会同步该密码并直接进入登录页;未设置时首次访问完成 `/setup`。
|
||||
|
||||
Web 工具中心支持通过浏览器上传/下载连接恢复包;数据目录页显示当前容器挂载目录。Docker 的数据根目录由 `GONAVI_HOST_DATA_ROOT` 挂载控制,如需切换请修改 env 后重新创建容器,避免在容器内迁移到未持久化路径。
|
||||
|
||||
请把 GoNavi 活动数据目录挂载为 `/data`。建议至少包含 `connections.json`、`daily_secrets.json`;如需可选驱动代理应包含 `drivers/`。Web 认证状态写入同目录下的 `web_auth.json`。
|
||||
|
||||
`GONAVI_WEB_PASSWORD` 会覆盖 `web_auth.json` 中已有的密码哈希,但保留 2FA 和会话策略;移除环境变量后继续使用最后一次同步的密码。env 文件包含明文密码,可被有容器检查权限的用户看到,请限制文件读取权限。
|
||||
|
||||
修改 env 密码后,需要重新创建容器,让 Compose 重新读取 env 文件:
|
||||
|
||||
```bash
|
||||
docker compose --env-file docker.web-server.env -f docker-compose.web-server.yml up -d --force-recreate
|
||||
```
|
||||
|
||||
Docker Desktop 的 Restart 和 `docker restart` 会沿用旧容器环境,不会重新读取 env 文件。
|
||||
|
||||
新部署直接设置环境密码时会启用纯密码认证。如需启用 2FA,请先留空 `GONAVI_WEB_PASSWORD` 并通过 `/setup` 初始化,再将它设置为同一密码后重新创建容器。
|
||||
|
||||
容器内默认监听 `0.0.0.0:34116`(`GONAVI_WEB_ADDR`)。**不要把未加固的 Web 入口直接暴露到公网**;生产环境请配合反向代理与 HTTPS。
|
||||
|
||||
默认 Compose 拉取 GHCR 预构建镜像。本地源码构建叠加 override:
|
||||
|
||||
```bash
|
||||
docker compose --env-file docker.web-server.env \
|
||||
-f docker-compose.web-server.yml \
|
||||
-f docker-compose.web-server.local.yml \
|
||||
up -d --build
|
||||
```
|
||||
|
||||
健康检查:`GET /__gonavi/healthz`。
|
||||
|
||||
### Docker / Podman(MCP Server)
|
||||
|
||||
容器化另提供 `gonavi-mcp-server`(仅 MCP HTTP,不含桌面 GUI / Web UI)。
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
请把 GoNavi 的活动数据目录挂载进容器。该目录内至少应包含 `connections.json`、`daily_secrets.json`,如需可选驱动代理还应包含 `drivers/`。
|
||||
|
||||
默认 Compose 会直接拉取 GHCR 预构建镜像。如果你要用当前仓库源码本地构建,再叠加一个 override:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
如果你使用 Podman,可直接复用同一套 OCI 镜像,走 `podman run` 或 Quadlet。仓库内已经提供 [deploy/podman/gonavi-mcp-server](deploy/podman/gonavi-mcp-server) 作为 Podman 原生部署入口,适合 Linux 服务器 / NAS 的 rootless systemd 常驻服务。
|
||||
|
||||
部署矩阵:
|
||||
|
||||
- Web UI:`docker-compose.web-server.yml`(镜像 `ghcr.io/syngnat/gonavi-web-server`)
|
||||
- MCP:`docker-compose.mcp-server.yml`(镜像 `ghcr.io/syngnat/gonavi-mcp-server`)
|
||||
- Podman / Quadlet:[deploy/podman/gonavi-mcp-server](deploy/podman/gonavi-mcp-server)
|
||||
- Kubernetes:[deploy/k8s/gonavi-mcp-server](deploy/k8s/gonavi-mcp-server)(`kustomization.yaml` + overlays)
|
||||
- Helm Chart:[deploy/helm/gonavi-mcp-server](deploy/helm/gonavi-mcp-server)
|
||||
- 仅构建环境:`Dockerfile.build-env`
|
||||
|
||||
完整部署方式与安全边界见 [cmd/gonavi-mcp-server/README.md](cmd/gonavi-mcp-server/README.md)。
|
||||
|
||||
### Docker / Podman(仅构建环境)
|
||||
|
||||
如果只是想要一套稳定的 Linux 构建环境来编译 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
|
||||
```
|
||||
|
||||
同一个 Dockerfile 也可以直接给 Podman 使用,例如 `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`。
|
||||
|
||||
这个镜像默认安装 WebKitGTK 4.0 构建依赖,兼容面更适合常见 Linux / NAS 场景。镜像底座本身支持多架构,`amd64` / `arm64` 会跟随容器平台。
|
||||
|
||||
仓库会把预构建镜像推送到 GHCR:
|
||||
|
||||
- `ghcr.io/syngnat/gonavi-web-server:latest`
|
||||
- `ghcr.io/syngnat/gonavi-mcp-server:latest`
|
||||
- `ghcr.io/syngnat/gonavi-build-env:latest`
|
||||
|
||||
这个镜像只负责构建 Linux 产物,不会把 Wails 桌面 GUI 变成可浏览器访问的 Web 服务。
|
||||
|
||||
### 跨平台发布(GitHub Actions)
|
||||
|
||||
仓库内置发布流水线,推送 `v*` Tag 可自动构建并发布 Release。
|
||||
@@ -186,6 +339,16 @@ sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-0 libwebkit2gtk-4.0-37 libjavascriptcoregtk-4.0-18
|
||||
```
|
||||
|
||||
### Linux 中文显示为方框
|
||||
|
||||
Ubuntu 24.04 LTS 的最小化桌面或服务器环境可能没有安装中文 CJK 字体,GoNavi 打开后中文会显示为方框。安装 Noto / 文泉驿字体后重启 GoNavi:
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y fonts-noto-cjk fonts-wqy-microhei
|
||||
fc-cache -fv
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 贡献指南
|
||||
|
||||
@@ -5,7 +5,7 @@ set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
DEFAULT_DRIVERS=(mariadb oceanbase doris starrocks 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)
|
||||
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"
|
||||
@@ -42,7 +42,9 @@ normalize_driver() {
|
||||
case "$name" in
|
||||
doris|diros) echo "doris" ;;
|
||||
open_gauss|open-gauss) echo "opengauss" ;;
|
||||
mariadb|oceanbase|starrocks|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)
|
||||
echo "$name"
|
||||
;;
|
||||
*)
|
||||
@@ -144,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}"
|
||||
@@ -201,6 +237,7 @@ 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}"
|
||||
|
||||
@@ -402,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: 266 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 40 KiB |
76
build/windows/installer.wxs
Normal file
@@ -0,0 +1,76 @@
|
||||
<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" />
|
||||
<SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLFOLDER]" After="CostFinalize" />
|
||||
<WixVariable Id="WixUILicenseRtf" Value="$(var.LicenseRtf)" />
|
||||
|
||||
<Feature Id="MainFeature" Title="$(var.ProductName)" Level="1">
|
||||
<ComponentRef Id="MainExecutable" />
|
||||
</Feature>
|
||||
|
||||
<Component Id="MainExecutable"
|
||||
Directory="INSTALLFOLDER"
|
||||
Guid="$(var.ComponentGuid)"
|
||||
Bitness="always64">
|
||||
<File Id="GoNaviExe"
|
||||
Source="$(var.SourceExe)"
|
||||
Name="GoNavi.exe"
|
||||
KeyPath="yes">
|
||||
<Shortcut Id="StartMenuShortcut"
|
||||
Directory="ProgramMenuFolder"
|
||||
Name="$(var.ProductName)"
|
||||
WorkingDirectory="INSTALLFOLDER"
|
||||
Icon="GoNaviIcon"
|
||||
Advertise="no" />
|
||||
<Shortcut Id="DesktopShortcut"
|
||||
Directory="DesktopFolder"
|
||||
Name="$(var.ProductName)"
|
||||
WorkingDirectory="INSTALLFOLDER"
|
||||
Icon="GoNaviIcon"
|
||||
Advertise="no" />
|
||||
</File>
|
||||
<File Id="GoNaviMsiInstallMarker"
|
||||
Source="$(var.InstallMarker)"
|
||||
Name=".gonavi-msi-install" />
|
||||
<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>
|
||||
|
||||
<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_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
|
||||
167
frontend/package-lock.json
generated
@@ -14,15 +14,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",
|
||||
@@ -1213,6 +1217,108 @@
|
||||
"react-dom": ">=16.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@reactflow/background": {
|
||||
"version": "11.3.14",
|
||||
"resolved": "https://registry.npmmirror.com/@reactflow/background/-/background-11.3.14.tgz",
|
||||
"integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@reactflow/core": "11.11.4",
|
||||
"classcat": "^5.0.3",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@reactflow/controls": {
|
||||
"version": "11.2.14",
|
||||
"resolved": "https://registry.npmmirror.com/@reactflow/controls/-/controls-11.2.14.tgz",
|
||||
"integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@reactflow/core": "11.11.4",
|
||||
"classcat": "^5.0.3",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@reactflow/core": {
|
||||
"version": "11.11.4",
|
||||
"resolved": "https://registry.npmmirror.com/@reactflow/core/-/core-11.11.4.tgz",
|
||||
"integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3": "^7.4.0",
|
||||
"@types/d3-drag": "^3.0.1",
|
||||
"@types/d3-selection": "^3.0.3",
|
||||
"@types/d3-zoom": "^3.0.1",
|
||||
"classcat": "^5.0.3",
|
||||
"d3-drag": "^3.0.0",
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-zoom": "^3.0.0",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@reactflow/minimap": {
|
||||
"version": "11.7.14",
|
||||
"resolved": "https://registry.npmmirror.com/@reactflow/minimap/-/minimap-11.7.14.tgz",
|
||||
"integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@reactflow/core": "11.11.4",
|
||||
"@types/d3-selection": "^3.0.3",
|
||||
"@types/d3-zoom": "^3.0.1",
|
||||
"classcat": "^5.0.3",
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-zoom": "^3.0.0",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@reactflow/node-resizer": {
|
||||
"version": "2.2.14",
|
||||
"resolved": "https://registry.npmmirror.com/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz",
|
||||
"integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@reactflow/core": "11.11.4",
|
||||
"classcat": "^5.0.4",
|
||||
"d3-drag": "^3.0.0",
|
||||
"d3-selection": "^3.0.0",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@reactflow/node-toolbar": {
|
||||
"version": "1.3.14",
|
||||
"resolved": "https://registry.npmmirror.com/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz",
|
||||
"integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@reactflow/core": "11.11.4",
|
||||
"classcat": "^5.0.3",
|
||||
"zustand": "^4.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
||||
@@ -1927,6 +2033,12 @@
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/dagre": {
|
||||
"version": "0.7.54",
|
||||
"resolved": "https://registry.npmmirror.com/@types/dagre/-/dagre-0.7.54.tgz",
|
||||
"integrity": "sha512-QjcRY+adGbYvBFS7cwv5txhVIwX1XXIUswWl+kSQTbI6NjgZydrZkEKX/etzVd7i+bCsCb40Z/xlBY5eoFuvWQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/debug": {
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
|
||||
@@ -2508,6 +2620,12 @@
|
||||
"chevrotain": "^11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/classcat": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/classcat/-/classcat-5.0.5.tgz",
|
||||
"integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/classnames": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||
@@ -3080,6 +3198,16 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/dagre": {
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmmirror.com/dagre/-/dagre-0.8.5.tgz",
|
||||
"integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graphlib": "^2.1.8",
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
},
|
||||
"node_modules/dagre-d3-es": {
|
||||
"version": "7.0.14",
|
||||
"resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz",
|
||||
@@ -3347,6 +3475,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
|
||||
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/format": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
|
||||
@@ -3380,6 +3514,15 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/graphlib": {
|
||||
"version": "2.1.8",
|
||||
"resolved": "https://registry.npmmirror.com/graphlib/-/graphlib-2.1.8.tgz",
|
||||
"integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
},
|
||||
"node_modules/hachure-fill": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz",
|
||||
@@ -3668,6 +3811,12 @@
|
||||
"integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz",
|
||||
@@ -5706,6 +5855,24 @@
|
||||
"react": "^18.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/reactflow": {
|
||||
"version": "11.11.4",
|
||||
"resolved": "https://registry.npmmirror.com/reactflow/-/reactflow-11.11.4.tgz",
|
||||
"integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@reactflow/background": "11.3.14",
|
||||
"@reactflow/controls": "11.2.14",
|
||||
"@reactflow/core": "11.11.4",
|
||||
"@reactflow/minimap": "11.7.14",
|
||||
"@reactflow/node-resizer": "2.2.14",
|
||||
"@reactflow/node-toolbar": "1.3.14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17",
|
||||
"react-dom": ">=17"
|
||||
}
|
||||
},
|
||||
"node_modules/recharts": {
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz",
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run"
|
||||
"test": "vitest run",
|
||||
"i18n:scan": "go run ../tools/i18n-scan --root .."
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
@@ -16,15 +17,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",
|
||||
|
||||
@@ -1 +1 @@
|
||||
0295a42fd931778d85157816d79d29e5
|
||||
1d8f9adbde8018f90d013cc740e0405b
|
||||
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 |
BIN
frontend/public/db-icons/jvm.ico
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
frontend/public/db-icons/kafka.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
frontend/public/db-icons/kingbase.ico
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
1
frontend/public/db-icons/milvus.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="#00A1EA"><title>Milvus</title><path d="M15.536 11.997a4.005 4.005 0 0 1-4.015 4.018c-2.36-.005-4.05-1.954-4.01-4.08a4.013 4.013 0 0 1 8.025.061zm8.463-.04a.823.823 0 0 1-.25.616q-1.122 1.128-2.248 2.25c-.082.083-.165.1-.25.054-.091-.048-.124-.121-.098-.242a12.764 12.764 0 0 0 .25-1.626 12.153 12.153 0 0 0 .028-1.49 11.56 11.56 0 0 0-.27-2.114.255.255 0 0 1 .017-.213.194.194 0 0 1 .273-.052.628.628 0 0 1 .086.075l2.174 2.174c.171.17.29.366.289.567zM11.226 20c-1.89-.002-3.488-.565-4.926-1.611a8.317 8.317 0 0 1-.952-.816q-1.125-1.112-2.253-2.221l-1.931-1.899c-.291-.285-.583-.57-.873-.857-.393-.39-.387-.83.012-1.217.296-.288.59-.579.885-.868q1.433-1.41 2.867-2.818c.532-.522 1.057-1.051 1.604-1.555a7.996 7.996 0 0 1 3.786-1.95A7.668 7.668 0 0 1 11.135 4c1.89.015 3.612.546 5.113 1.723a7.987 7.987 0 0 1 2.282 2.77 7.567 7.567 0 0 1 .76 2.556c.255 2.404-.414 4.518-2.046 6.312a7.772 7.772 0 0 1-3.927 2.366 8.995 8.995 0 0 1-2.091.274zm.364-2.137a5.819 5.819 0 0 0 .605-.036 5.35 5.35 0 0 0 .38-.05c1.525-.28 2.76-1.051 3.704-2.263.92-1.178 1.28-2.542 1.168-4.028a5.626 5.626 0 0 0-.631-2.168c-.627-1.213-1.596-2.067-2.817-2.646-1.096-.519-2.248-.649-3.443-.464A5.814 5.814 0 0 0 7.33 7.8c-.858.83-1.707 1.669-2.56 2.504-.426.419-.85.841-1.28 1.257-.274.266-.286.588-.013.857q.804.793 1.61 1.583c.722.709 1.44 1.42 2.165 2.126 1.184 1.153 2.604 1.737 4.339 1.738z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
35
frontend/public/db-icons/mqtt.svg
Normal file
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.2" baseProfile="tiny" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
|
||||
y="0px" viewBox="0 0 1100 300" overflow="visible" xml:space="preserve">
|
||||
<g id="black_bg" display="none">
|
||||
<rect x="-2384.9" y="-306.8" display="inline" width="4531.8" height="1152"/>
|
||||
</g>
|
||||
<g id="logos">
|
||||
<g>
|
||||
<path fill="#FFFFFF" d="M388,150.1c1.7,3.7,3.3,7.5,4.8,11.4c1.5-4,3.1-7.8,4.9-11.6c1.7-3.7,3.5-7.4,5.3-10.9l50.5-99.5
|
||||
c0.9-1.7,1.8-3,2.8-4c0.9-0.9,2-1.6,3.2-2.1c1.2-0.5,2.5-0.7,4-0.7c1.5,0,3.2,0,5.2,0h34.8v196.6h-40.2V116.3
|
||||
c0-5.5,0.3-11.4,0.8-17.8l-52.1,101.1c-1.6,3.2-3.8,5.5-6.5,7.2c-2.7,1.6-5.9,2.4-9.4,2.4h-6.2c-3.5,0-6.6-0.8-9.4-2.4
|
||||
c-2.7-1.6-4.9-4-6.5-7.2L321.4,98.3c0.4,3.2,0.6,6.3,0.8,9.4c0.2,3.1,0.3,6,0.3,8.6v113.1h-40.2V32.8h34.8c2,0,3.7,0,5.2,0
|
||||
c1.5,0,2.8,0.2,4,0.7c1.2,0.5,2.2,1.1,3.2,2.1c0.9,0.9,1.9,2.3,2.8,4l50.6,99.9C384.6,142.9,386.3,146.4,388,150.1z"/>
|
||||
<path fill="#FFFFFF" d="M719.1,131c0,8.5-0.9,16.6-2.6,24.4c-1.8,7.8-4.3,15.1-7.6,21.9c-3.3,6.8-7.3,13.2-12.2,19
|
||||
c-4.8,5.9-10.3,11-16.4,15.5l49.8,54.4h-37.5c-5.4,0-10.3-0.7-14.7-2c-4.4-1.3-8.3-3.9-11.7-7.9L641.7,229c-4,0.8-8,1.4-12.1,1.9
|
||||
c-4.1,0.4-8.3,0.7-12.6,0.7c-15.3,0-29.2-2.6-41.8-7.7c-12.6-5.1-23.4-12.2-32.3-21.3c-9-9-15.9-19.7-20.8-31.9
|
||||
c-4.9-12.2-7.4-25.5-7.4-39.7c0-14.2,2.5-27.4,7.4-39.7c4.9-12.2,11.8-22.9,20.8-31.9c9-9,19.7-16.1,32.3-21.2
|
||||
c12.6-5.1,26.5-7.7,41.8-7.7c15.3,0,29.2,2.6,41.8,7.8c12.6,5.2,23.3,12.3,32.2,21.3c8.9,9,15.8,19.6,20.7,31.9
|
||||
C716.6,103.7,719.1,116.8,719.1,131z M672.4,131c0-9.7-1.3-18.5-3.8-26.3c-2.5-7.8-6.1-14.4-10.9-19.8
|
||||
c-4.7-5.4-10.5-9.5-17.3-12.4c-6.8-2.9-14.6-4.3-23.4-4.3c-8.8,0-16.7,1.4-23.6,4.3c-6.9,2.9-12.7,7-17.4,12.4
|
||||
c-4.7,5.4-8.3,12-10.9,19.8c-2.5,7.8-3.8,16.5-3.8,26.3c0,9.8,1.3,18.6,3.8,26.4c2.5,7.8,6.1,14.4,10.9,19.8
|
||||
c4.7,5.4,10.5,9.5,17.4,12.4c6.9,2.8,14.7,4.3,23.6,4.3c8.7,0,16.5-1.4,23.4-4.3c6.8-2.8,12.6-7,17.3-12.4
|
||||
c4.7-5.4,8.3-12,10.9-19.8C671.1,149.6,672.4,140.8,672.4,131z"/>
|
||||
<path fill="#FFFFFF" d="M865.6,32.7v36.2h-53.3v160.5h-45.6V68.9h-53.3V32.7H865.6z"/>
|
||||
<path fill="#FFFFFF" d="M1040.1,32.7v36.2h-55.3v160.5h-45.6V68.9h-53.3V32.7H1040.1z"/>
|
||||
</g>
|
||||
<path fill="#FFFFFF" d="M34.9,144c-0.2,0-0.4,0-0.6,0v77.6c0,5.6,4.6,10.2,10.2,10.2h79.9C123.7,183.3,83.8,144,34.9,144z"/>
|
||||
<path fill="#FFFFFF" d="M34.9,80c-0.2,0-0.4,0-0.6,0v33c65.9,0.3,119.5,53.3,120.2,118.8h34.2C188.1,148,119.3,80,34.9,80z"/>
|
||||
<path fill="#FFFFFF" d="M237.2,221.7v-70.1C214,94.8,167.3,50,109.1,29H44.5c-5.6,0-10.2,4.6-10.2,10.2V49
|
||||
c101.4,0.3,183.9,82,184.5,182.8h8.2C232.6,231.8,237.2,227.3,237.2,221.7z"/>
|
||||
<path fill="#FFFFFF" d="M210.5,57.3c9.4,9.4,19,21.3,26.7,31.8v-50c0-5.6-4.5-10.1-10.1-10.1h-51.5
|
||||
C187.5,37.3,199.9,46.8,210.5,57.3z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
BIN
frontend/public/db-icons/oceanbase.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
frontend/public/db-icons/opengauss.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
frontend/public/db-icons/oracle.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
35
frontend/public/db-icons/qdrant.svg
Normal file
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Capa_2" data-name="Capa 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 346.42 400">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #9e0d38;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #dc244c;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #ff516b;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Vectors">
|
||||
<g>
|
||||
<g>
|
||||
<polygon class="cls-2" points="173.21 0 0 100 0 300 173.21 400 238.16 362.5 238.16 287.5 173.21 325 64.96 262.5 64.96 137.5 173.21 75 281.46 137.5 281.46 387.5 346.42 350 346.42 100 173.21 0"/>
|
||||
<polygon class="cls-2" points="108.26 162.5 108.26 237.5 173.21 275 238.16 237.5 238.16 162.5 173.21 125 108.26 162.5"/>
|
||||
</g>
|
||||
<g>
|
||||
<polygon class="cls-1" points="238.16 287.5 238.16 362.5 173.21 400 173.21 325 238.16 287.5"/>
|
||||
<polygon class="cls-1" points="346.42 100 346.42 350 281.46 387.5 281.46 137.5 346.42 100"/>
|
||||
<polygon class="cls-3" points="346.42 100 281.46 137.5 173.21 75 64.96 137.5 0 100 173.21 0 346.42 100"/>
|
||||
<polygon class="cls-2" points="173.21 325 173.21 400 0 300 0 100 64.96 137.5 64.96 262.5 173.21 325"/>
|
||||
<polygon class="cls-3" points="238.16 162.5 173.21 200 108.26 162.5 173.21 125 238.16 162.5"/>
|
||||
<polygon class="cls-2" points="173.21 200 173.21 275 108.26 237.5 108.26 162.5 173.21 200"/>
|
||||
<polygon class="cls-1" points="238.16 162.5 238.16 237.5 173.21 275 173.21 200 238.16 162.5"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
7
frontend/public/db-icons/rabbitmq.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg role="img" viewBox="0 0 132.29167 132.29166" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>RabbitMQ</title>
|
||||
<path
|
||||
d="M 127.32189 54.270409 H 84.999977 a 5.2690424 5.2690424 0 0 1 -5.302308 -5.302309 V 6.6461888 A 5.2690424 5.2690424 0 0 0 74.39536 1.3438804 H 58.555783 A 5.2690424 5.2690424 0 0 0 53.253474 6.6461888 V 48.9681 a 5.2690424 5.2690424 0 0 1 -5.302309 5.302309 H 32.111589 A 5.2690424 5.2690424 0 0 1 26.80928 48.9681 V 6.6461888 A 5.2690424 5.2690424 0 0 0 21.506972 1.3106149 H 5.6341299 A 5.2690424 5.2690424 0 0 0 0.3650864 6.6461888 V 128.31903 a 5.2690424 5.2690424 0 0 0 5.3023089 5.30231 H 127.32189 a 5.2690424 5.2690424 0 0 0 5.30231 -5.30231 V 59.572717 a 5.2690424 5.2690424 0 0 0 -5.30231 -5.302308 z m -21.17517 44.908545 a 7.9542581 7.9542581 0 0 1 -7.95425 7.987516 H 87.573659 a 7.9542581 7.9542581 0 0 1 -7.954257 -7.987516 V 88.593818 a 7.9542581 7.9542581 0 0 1 7.954257 -7.987517 h 10.618811 a 7.9542581 7.9542581 0 0 1 7.95425 7.987517 z"
|
||||
fill="#FF6600"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1016 B |
BIN
frontend/public/db-icons/rocketmq.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
frontend/public/db-icons/starrocks.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
frontend/public/db-icons/tdengine.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
72
frontend/public/db-icons/vastbase.svg
Normal file
@@ -0,0 +1,72 @@
|
||||
<svg width="159" height="48" viewBox="0 0 159 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1_798)">
|
||||
<path d="M93.932 39.4721C93.6905 39.3017 93.4059 39.1653 93.0955 39.0461C92.7764 38.9268 92.4573 38.8246 92.1382 38.7223C91.8277 38.6371 90.0598 38.2623 89.7062 38.1004C88.7404 37.6403 89.1888 37.1717 89.3527 37.061C89.4993 36.9673 89.6545 36.9332 89.8615 36.8906C90.0598 36.848 90.422 36.8054 91.0602 36.8054C91.4052 36.8054 91.8019 36.848 92.0865 36.8991C92.371 36.9502 92.6298 37.0184 92.8454 37.078C93.0092 37.1206 93.0868 37.1547 93.3456 37.1717C93.587 37.1973 93.7509 37.078 93.8716 36.9843C93.9751 36.9076 94.0441 36.7372 94.0441 36.6605C94.0441 36.5583 94.0441 36.2601 94.0441 36.2175C94.0441 35.996 93.8199 35.8171 93.5353 35.7319C93.5094 35.7234 93.4749 35.7149 93.4404 35.6978C93.2421 35.6382 93.0265 35.5871 92.7764 35.5445C92.5263 35.5019 92.2503 35.4593 91.9657 35.4337C91.6725 35.4082 91.3879 35.3911 91.0947 35.3911C90.5428 35.3911 90.0081 35.4337 89.5079 35.5274C89.0077 35.6126 88.5765 35.7489 88.2143 35.9364C87.8435 36.1153 87.5502 36.3453 87.3346 36.6265C87.1191 36.8991 87.0156 37.2228 87.0156 37.5807C87.0156 37.8959 87.0932 38.1685 87.2398 38.3986C87.3864 38.6286 87.5934 38.8246 87.8262 38.9864C88.0677 39.1568 88.3437 39.2931 88.6455 39.4124C89.3354 39.685 91.2758 40.0173 92.052 40.2729C92.8281 40.5285 92.7591 41.3634 92.2589 41.5935C91.7501 41.8235 91.1033 41.832 90.3703 41.832C89.9132 41.832 89.3958 41.7809 89.0508 41.7127C88.7059 41.6446 88.4989 41.5594 88.2574 41.4827C88.0073 41.406 87.7227 41.2186 87.4468 41.2186C87.1104 41.2186 86.8086 41.3634 86.8086 41.6616C86.8086 41.7894 86.8086 41.7979 86.8086 41.832C86.8086 41.8661 86.8086 41.9172 86.8086 41.9683V42.275C86.8086 42.488 86.9379 42.6073 87.0587 42.6755C87.1708 42.7436 87.2484 42.7777 87.4295 42.8459C87.6106 42.9055 87.8435 42.9736 88.1194 43.0333C88.3954 43.0929 88.7145 43.1526 89.0767 43.1952C89.4389 43.2378 89.7149 43.2633 90.1288 43.2633C90.7497 43.2633 91.4569 43.2122 92.0002 43.11C92.5435 43.0077 93.0178 42.8629 93.4318 42.6499C93.8371 42.4539 94.1648 42.1984 94.3977 41.9002C94.6391 41.602 94.7599 41.2527 94.7599 40.8522C94.7599 40.5455 94.6823 40.2814 94.527 40.0599C94.3718 39.8384 94.1735 39.6424 93.932 39.4721Z" fill="#A0A0A0"/>
|
||||
<path d="M119.071 36.0554C118.235 35.3994 117.528 35.3057 115.777 35.3057H113.078C113.078 35.3057 113.034 35.3057 113 35.3227H112.965C112.534 35.3227 112.181 35.672 112.181 36.098V42.4707C112.181 42.8967 112.534 43.246 112.965 43.246H113.103C113.103 43.246 113.147 43.246 113.164 43.246H115.587C117.321 43.246 118.088 43.1779 118.968 42.5304C120.003 41.7636 120.218 41.0479 120.218 39.1651C120.218 37.4697 120.054 36.8392 119.063 36.0639L119.071 36.0554ZM118.028 41.2524L117.881 41.3717C117.579 41.6699 116.475 41.7977 115.579 41.7977H114.294C114.104 41.7892 113.905 41.6784 113.905 41.4824V37.2056C113.905 37.0096 114.121 36.7285 114.311 36.7285L115.544 36.7114C116.57 36.7114 117.433 36.737 118.054 37.3419C118.416 37.6997 118.485 38.2024 118.485 39.2247C118.485 40.3664 118.459 40.8264 118.019 41.2609L118.028 41.2524Z" fill="#A0A0A0"/>
|
||||
<path d="M107.101 35.3911H99.9258C99.7016 35.3911 99.5205 35.57 99.5205 35.7915V36.6605C99.5205 36.8906 99.7102 37.0695 99.9345 37.0695H102.582V42.4284C102.582 42.8544 102.642 43.2037 103.074 43.2037H103.832C104.264 43.2037 104.281 42.8544 104.281 42.4284V37.0695H107.101C107.299 37.0695 107.455 36.9076 107.455 36.7117V35.7489C107.455 35.553 107.291 35.3911 107.092 35.3911H107.101Z" fill="#A0A0A0"/>
|
||||
<path d="M145.236 35.3911H138.061C137.836 35.3911 137.655 35.57 137.655 35.7915V36.6605C137.655 36.8906 137.845 37.0695 138.069 37.0695H140.717V42.4284C140.717 42.8544 140.777 43.2037 141.208 43.2037H141.967C142.398 43.2037 142.416 42.8544 142.416 42.4284V37.0695H145.236C145.434 37.0695 145.598 36.9076 145.598 36.7117V35.7489C145.598 35.553 145.434 35.3911 145.236 35.3911Z" fill="#A0A0A0"/>
|
||||
<path d="M69.535 35.3994H68.9572C68.5174 35.3738 68.4139 35.4164 68.2846 35.655L68.1811 35.8424L68.1121 35.9702L67.9482 36.2684C67.9482 36.2684 67.9482 36.2684 67.9482 36.2769L65.3697 40.9542L62.5755 35.9276C62.5755 35.9276 62.5755 35.9191 62.5669 35.9105L62.4203 35.6464C62.2909 35.4164 62.1012 35.3908 61.7821 35.3823H61.1439C60.661 35.3908 60.6179 35.7742 60.7472 36.0128L61.256 36.9499C61.256 36.9499 61.2733 36.984 61.2819 37.0011L64.4555 42.6922C64.6366 42.9989 64.9643 43.2119 65.3352 43.2119C65.706 43.2119 66.0337 42.9989 66.2148 42.6922L69.4143 37.0181L69.949 35.9872C70.0783 35.7572 70.0611 35.3823 69.535 35.3994Z" fill="#A0A0A0"/>
|
||||
<path d="M82.0395 41.585L78.8831 35.9279C78.702 35.6127 78.3829 35.4082 78.0121 35.4082C77.6413 35.4082 77.3222 35.6127 77.1411 35.9279L73.9933 41.6191C73.9933 41.6191 73.9761 41.6531 73.9675 41.6702L73.4587 42.6073C73.3293 42.8374 73.4587 43.1015 73.6829 43.2378H74.4677C74.873 43.2378 74.9851 43.2037 75.1145 42.9737L75.2611 42.7096C75.2611 42.7096 75.2611 42.7011 75.2697 42.6925L75.8389 41.6531H78.6934C78.8486 41.6531 79.116 41.4316 78.9349 41.0738L78.5295 40.3496C78.4951 40.2815 78.4261 40.2389 78.3484 40.2389H76.6064L78.0207 37.6659L80.5734 42.3432C80.5734 42.3432 80.5734 42.3432 80.5734 42.3518L80.7373 42.65L80.8063 42.7777L80.9098 42.9652C81.0391 43.1952 81.272 43.2293 81.6169 43.2293L82.3758 43.2463C82.5483 43.2378 82.807 42.9141 82.6259 42.6159L82.0136 41.585H82.0395Z" fill="#A0A0A0"/>
|
||||
<path d="M132.895 41.585L129.739 35.9279C129.558 35.6127 129.238 35.4082 128.868 35.4082C128.497 35.4082 128.178 35.6127 127.997 35.9279L124.849 41.6191C124.849 41.6191 124.832 41.6531 124.823 41.6702L124.314 42.6073C124.185 42.8374 124.314 43.1015 124.538 43.2378H125.323C125.728 43.2378 125.841 43.2037 125.97 42.9737L126.117 42.7096C126.117 42.7096 126.117 42.7011 126.125 42.6925L126.694 41.6531H129.549C129.704 41.6531 129.971 41.4316 129.79 41.0738L129.385 40.3496C129.351 40.2815 129.282 40.2389 129.204 40.2389H127.462L128.876 37.6659L131.429 42.3432C131.429 42.3432 131.429 42.3432 131.429 42.3518L131.593 42.6585L131.662 42.7863L131.765 42.9737C131.895 43.2037 132.127 43.2378 132.481 43.2378L133.24 43.2548C133.412 43.2463 133.671 42.9226 133.49 42.6244L132.878 41.585H132.895Z" fill="#A0A0A0"/>
|
||||
<path d="M158.94 42.6156L158.328 41.5762L155.171 35.9191C154.99 35.6039 154.671 35.3994 154.3 35.3994C153.929 35.3994 153.61 35.6039 153.429 35.9191L150.281 41.6103C150.281 41.6103 150.264 41.6444 150.256 41.6614L149.747 42.5986C149.617 42.8286 149.747 43.0927 149.971 43.229H150.756C151.161 43.229 151.273 43.1949 151.403 42.9649L151.549 42.7008C151.549 42.7008 151.549 42.6923 151.558 42.6838L152.127 41.6444H154.981C155.137 41.6444 155.404 41.4228 155.223 41.065L154.818 40.3408C154.783 40.2727 154.714 40.2301 154.637 40.2301H152.894L154.309 37.6571L156.862 42.3345C156.862 42.3345 156.862 42.3345 156.862 42.343L157.025 42.6497L157.094 42.7775L157.198 42.9649C157.327 43.1949 157.56 43.229 157.905 43.229L158.664 43.2461C158.836 43.2375 159.095 42.9138 158.914 42.6156H158.94Z" fill="#A0A0A0"/>
|
||||
<path d="M141.476 29.751V22.2366C141.89 22.0833 142.304 21.9214 142.718 21.751C143.192 21.5551 143.244 21.4869 143.235 21.112C143.235 20.8053 143.244 20.0556 143.244 19.5615C143.244 19.1951 143.02 19.0758 142.692 19.161C142.295 19.2633 141.881 19.3655 141.467 19.4677V14.9523H142.977C143.14 14.9523 143.27 14.8245 143.27 14.6626V12.7372C143.27 12.5753 143.14 12.4475 142.977 12.4475H141.467V10.1813C141.467 10.0194 141.338 9.8916 141.174 9.8916H138.828C138.665 9.8916 138.535 10.0194 138.535 9.90864V12.4475H137.095C136.931 12.4475 136.802 12.5753 136.802 12.7372V14.6626C136.802 14.8245 136.931 14.9523 137.095 14.9523H138.535V20.0812C138.061 20.1578 137.595 20.2175 137.129 20.2516C137.026 20.2601 136.922 20.2686 136.871 20.3538C136.793 20.4816 136.81 20.6861 136.81 20.6861V22.6712C136.81 22.6712 136.767 23.1653 136.897 23.2931C136.966 23.3612 137.164 23.4038 137.336 23.3698C137.742 23.2931 138.138 23.1994 138.535 23.1057V28.4901C138.509 28.6009 138.458 28.6861 138.328 28.7968C138.251 28.8565 138.138 28.8991 137.992 28.9161L137.224 28.9417C137.06 28.9417 136.931 29.0695 136.931 29.2313V31.1568C136.931 31.3186 137.06 31.4464 137.224 31.4464H139.863L139.898 31.4294C140.769 31.3357 141.441 30.4837 141.467 29.8107V29.7936C141.467 29.7936 141.467 29.7766 141.467 29.7681C141.467 29.7595 141.467 29.7425 141.467 29.734L141.476 29.751Z" fill="#19140F"/>
|
||||
<path d="M158.361 19.1268H153.98V17.3121H157.792C158.033 17.3121 158.223 17.1162 158.223 16.8861V10.2663C158.223 10.0278 158.025 9.84033 157.792 9.84033L146.426 9.92553C146.426 9.92553 146.365 9.90849 146.331 9.90849H143.985C143.821 9.90849 143.692 10.0363 143.692 10.1982V23.0714C143.562 25.3632 143.131 26.8797 142.234 28.8904V31.4548H142.303C142.898 31.4037 146.279 27.6209 146.615 22.0576L146.633 21.6486H151.057V23.4719H147.141C146.969 23.4719 146.822 23.6167 146.822 23.7871V31.2077C146.822 31.3781 146.969 31.523 147.141 31.523H157.913C158.085 31.523 158.232 31.3781 158.232 31.2077V23.7871C158.232 23.6167 158.085 23.4719 157.913 23.4719H153.997V21.6486H158.378C158.542 21.6486 158.672 21.5208 158.672 21.359V19.4335C158.672 19.2716 158.542 19.1438 158.378 19.1438L158.361 19.1268ZM155.222 26.2408V28.7626C155.222 28.8819 155.127 28.9756 155.006 28.9756H149.979C149.858 28.9756 149.763 28.8819 149.763 28.7626V26.2408C149.763 26.1215 149.858 26.0278 149.979 26.0278H155.006C155.127 26.0278 155.222 26.1215 155.222 26.2408ZM147.124 12.4303H154.696C154.972 12.4303 155.205 12.6604 155.205 12.933V14.4154C155.205 14.688 154.972 14.9181 154.696 14.9181H147.124C146.883 14.9181 146.684 14.7477 146.633 14.5262V12.8307C146.676 12.6007 146.883 12.4303 147.124 12.4303ZM146.641 19.1268V17.3121H151.048V19.1268H146.641Z" fill="#19140F"/>
|
||||
<path d="M114.397 10.4023H112.051C111.889 10.4023 111.758 10.532 111.758 10.692V13.0094C111.758 13.1694 111.889 13.299 112.051 13.299H114.397C114.559 13.299 114.69 13.1694 114.69 13.0094V10.692C114.69 10.532 114.559 10.4023 114.397 10.4023Z" fill="#19140F"/>
|
||||
<path d="M122.58 10.4023H120.235C120.073 10.4023 119.941 10.532 119.941 10.692V13.0094C119.941 13.1694 120.073 13.299 120.235 13.299H122.58C122.742 13.299 122.874 13.1694 122.874 13.0094V10.692C122.874 10.532 122.742 10.4023 122.58 10.4023Z" fill="#19140F"/>
|
||||
<path d="M122.132 29.1031C122.046 29.1031 121.951 29.1031 121.865 29.1031C121.123 29.1031 120.382 28.9839 119.692 28.7794C121.003 27.6633 121.908 26.232 122.072 24.5536H123.176C123.34 24.5536 123.469 24.4258 123.469 24.264V22.3385C123.469 22.1766 123.34 22.0488 123.176 22.0488H115.182V21.3502C115.182 21.1883 115.052 21.0605 114.888 21.0605H112.543C112.379 21.0605 112.249 21.1883 112.249 21.3502V22.0488H111.646C111.482 22.0488 111.353 22.1766 111.353 22.3385V24.264C111.353 24.4258 111.482 24.5536 111.646 24.5536H112.482C112.698 26.249 113.431 27.6378 114.509 28.7112C113.707 28.9583 112.87 29.1031 112.068 29.1031C111.991 29.1031 111.913 29.1031 111.835 29.1031C111.585 29.1031 111.378 29.2991 111.378 29.5462V30.969C111.378 31.2075 111.577 31.412 111.818 31.412C111.896 31.412 111.982 31.412 112.06 31.412C113.716 31.412 115.44 31.0542 116.984 30.4067C118.441 31.0712 120.132 31.412 121.857 31.412C121.934 31.412 122.02 31.412 122.098 31.412C122.348 31.412 122.546 31.2075 122.546 30.9604V29.4865C122.546 29.265 122.357 29.0861 122.132 29.0946V29.1031ZM115.354 24.5621H119.2C119.14 25.5845 118.321 26.6324 117.122 27.4588C116.113 26.6495 115.44 25.6186 115.354 24.5621Z" fill="#19140F"/>
|
||||
<path d="M132.714 29.0521C131.636 28.9755 130.644 28.5239 129.782 27.8338C131.187 26.164 132.041 24.017 132.093 21.6144C132.11 21.4526 132.11 21.3333 132.11 21.2907V14.9435H132.86C133.024 14.9435 133.154 14.8072 133.154 14.6538V12.7284C133.154 12.5665 133.024 12.4387 132.86 12.4387H128.1C128.16 12.2257 128.221 11.9957 128.281 11.7657C128.402 11.263 128.462 10.8455 128.454 10.2918C128.454 10.0617 128.264 9.88281 128.031 9.88281H125.332C124.987 9.88281 124.719 10.1725 124.745 10.5048C124.754 10.6496 124.762 10.8029 124.762 10.9563C124.762 12.4302 124.193 13.7763 123.262 14.7902V14.1597C123.262 13.9978 123.133 13.87 122.969 13.87H118.777V10.198C118.777 10.0362 118.648 9.90837 118.484 9.90837H116.139C115.975 9.90837 115.845 10.0362 115.845 10.198V13.87H111.628C111.464 13.87 111.335 13.9978 111.335 14.1597V16.4771C111.335 16.6389 111.464 16.7667 111.628 16.7667H113.293C113.293 16.7667 113.293 16.7923 113.293 16.8093C113.293 17.6187 112.378 18.1725 111.723 18.5559C111.585 18.6325 111.464 18.6922 111.378 18.8029C111.361 18.8285 111.344 18.8881 111.335 18.9648V20.3791C111.344 20.4472 111.369 20.5069 111.395 20.541C111.464 20.6262 111.585 20.6602 111.697 20.6688C111.818 20.6858 111.87 20.6858 111.99 20.6858C113.715 20.6858 115.595 19.3312 115.845 17.6613V20.4728C115.845 20.6347 115.975 20.7625 116.139 20.7625H118.484C118.648 20.7625 118.777 20.6347 118.777 20.4728V17.6443C119.019 19.3141 120.908 20.6858 122.632 20.6858C122.753 20.6858 122.813 20.6858 122.926 20.6688C123.046 20.6517 123.158 20.6262 123.227 20.541C123.253 20.5069 123.279 20.4558 123.288 20.3961V18.9478C123.279 18.8796 123.271 18.8285 123.245 18.8029C123.158 18.6922 123.029 18.6325 122.9 18.5559C122.244 18.1725 121.33 17.6187 121.33 16.8093C121.33 16.7923 121.33 16.7838 121.33 16.7667H122.831V16.903C122.831 16.903 122.788 17.1331 122.969 17.3205C123.15 17.5079 123.374 17.4227 123.633 17.3546C123.745 17.3205 123.857 17.2864 123.96 17.2524V21.7678C123.96 21.8189 123.96 21.8615 123.96 21.8956C123.96 21.9808 123.96 22.066 123.96 22.1512V22.2449C123.96 22.2449 123.96 22.2449 123.96 22.2534C124.038 24.3407 124.788 26.2321 126.004 27.7401C125.185 28.3535 124.236 28.7965 123.227 28.984C123.003 29.0266 122.839 29.2311 122.839 29.4611V30.9265C122.839 31.1821 123.064 31.3695 123.314 31.3354C125.03 31.0969 126.599 30.4749 127.91 29.5463C129.281 30.526 130.92 31.1735 132.705 31.378C132.947 31.4036 133.154 31.2247 133.154 30.9861C133.154 30.6624 133.154 29.8786 133.154 29.5889C133.154 29.0351 132.757 29.0777 132.679 29.0777L132.714 29.0521ZM127.039 14.9435H128.824C128.824 14.9435 129.178 14.9776 129.178 15.3695V22.066C129.074 23.3525 128.626 24.5878 127.936 25.6613C127.289 24.5282 126.918 23.2417 126.918 21.9978C126.918 21.9297 126.918 21.87 126.901 21.8274V17.0308C126.901 16.869 126.772 16.7412 126.608 16.7412H125.142C125.944 16.2896 126.556 15.6847 127.031 14.952L127.039 14.9435Z" fill="#19140F"/>
|
||||
<path d="M107.308 29.8531H98.5372V29.0097H105.937C106.161 29.0097 106.351 28.8223 106.351 28.6007V27.7828C106.351 27.5613 106.161 27.3739 105.937 27.3739H98.5372V26.5134H105.117C105.264 26.5134 105.376 26.3941 105.376 26.2493V20.1748C105.376 20.0299 105.255 19.9106 105.117 19.9106H88.4472C88.3006 19.9106 88.1885 20.0299 88.1885 20.1748V26.2493C88.1885 26.3941 88.3092 26.5134 88.4472 26.5134H95.0273V27.3739H87.6279C87.4037 27.3739 87.214 27.5613 87.214 27.7828V28.6007C87.214 28.8223 87.4037 29.0097 87.6279 29.0097H95.0273V29.8531H86.2567C86.0325 29.8531 85.8428 30.0406 85.8428 30.2621V31.08C85.8428 31.3015 86.0325 31.4889 86.2567 31.4889H107.299C107.523 31.4889 107.713 31.3015 107.713 31.08V30.2621C107.713 30.0406 107.523 29.8531 107.299 29.8531H107.308ZM102.125 24.5368C102.125 24.7158 101.978 24.8606 101.797 24.8606H98.5372V24.0001H101.797C101.978 24.0001 102.125 24.1449 102.125 24.3238V24.5368ZM101.797 21.5464C101.978 21.5464 102.125 21.6913 102.125 21.8702V22.0832C102.125 22.2621 101.978 22.4069 101.797 22.4069H98.5372V21.5464H101.797ZM91.5001 21.8702C91.5001 21.6913 91.6467 21.5464 91.8278 21.5464H95.0445V22.4069H91.8278C91.6467 22.4069 91.5001 22.2621 91.5001 22.0832V21.8702ZM91.8278 24.8606C91.6467 24.8606 91.5001 24.7158 91.5001 24.5368V24.3238C91.5001 24.1449 91.6467 24.0001 91.8278 24.0001H95.0445V24.8606H91.8278Z" fill="#19140F"/>
|
||||
<path d="M107.317 17.3975H86.2655C86.0369 17.3975 85.8516 17.5806 85.8516 17.8064V18.6158C85.8516 18.8416 86.0369 19.0247 86.2655 19.0247H107.317C107.545 19.0247 107.731 18.8416 107.731 18.6158V17.8064C107.731 17.5806 107.545 17.3975 107.317 17.3975Z" fill="#19140F"/>
|
||||
<path d="M88.2919 16.4944H105.298C105.445 16.4944 105.557 16.3751 105.557 16.2303V10.1557C105.557 10.0109 105.436 9.8916 105.298 9.8916H88.2919C88.1453 9.8916 88.0332 10.0109 88.0332 10.1557V16.2303C88.0332 16.3751 88.1539 16.4944 88.2919 16.4944ZM91.3189 11.8341C91.3189 11.6552 91.4655 11.5103 91.6466 11.5103H101.944C102.125 11.5103 102.271 11.6552 102.271 11.8341V12.0471C102.271 12.226 102.125 12.3708 101.944 12.3708H91.6466C91.4655 12.3708 91.3189 12.226 91.3189 12.0471V11.8341ZM91.3189 14.3304C91.3189 14.1515 91.4655 14.0066 91.6466 14.0066H101.944C102.125 14.0066 102.271 14.1515 102.271 14.3304V14.5434C102.271 14.7223 102.125 14.8671 101.944 14.8671H91.6466C91.4655 14.8671 91.3189 14.7223 91.3189 14.5434V14.3304Z" fill="#19140F"/>
|
||||
<path d="M63.7749 16.043H61.1274C60.9721 16.043 60.8428 16.1708 60.8428 16.3241V20.2176C60.8428 20.371 60.9721 20.4988 61.1274 20.4988H63.7059C63.8612 20.4988 64.0509 20.3284 64.0509 20.175V16.3241C64.0509 16.1708 63.9388 16.043 63.7835 16.043H63.7749Z" fill="#19140F"/>
|
||||
<path d="M63.749 21.606H61.1274C60.9721 21.606 60.8428 21.7338 60.8428 21.8871V31.1651C60.8428 31.3184 60.9721 31.4462 61.1274 31.4462H63.7404C63.8957 31.4462 64.0164 31.3184 64.0164 31.1651V21.8871C64.0164 21.7338 63.9043 21.606 63.749 21.606Z" fill="#19140F"/>
|
||||
<path d="M81.5318 20.4731H80.0916V15.4635C80.0916 15.1568 79.8415 14.9097 79.5311 14.9097H68.3975C68.7425 14.6797 69.2082 14.2963 69.6911 13.7936H81.0489C81.2041 13.7936 81.3248 13.6743 81.3248 13.5125V11.5615C81.3248 11.4081 81.1955 11.2888 81.0489 11.2888H70.8726C70.8898 11.1696 70.933 10.1728 70.933 10.1728C70.933 10.0194 70.8036 9.8916 70.6484 9.8916H68.1474C67.9922 9.8916 67.8628 10.0194 67.8628 10.1728V10.8714C67.8628 12.03 66.5692 13.1546 65.4568 13.3591C65.4568 13.3591 65.0342 13.3762 65.0342 13.7169V15.1057C65.0342 15.259 65.1635 15.3868 65.3101 15.3868L66.5951 15.1823V20.4646H65.7413C65.5861 20.4646 65.4568 20.5838 65.4568 20.7457V22.6882C65.4568 22.8415 65.5861 22.9693 65.7413 22.9693H66.5951V27.9448C66.5951 28.2856 66.828 28.3879 67.1384 28.3879H77.1163V28.7627C77.1163 28.9757 77.1163 29.2228 76.78 29.2228H74.2101C74.0548 29.2228 73.822 29.325 73.8737 29.5636C73.9255 29.8021 74.6758 31.0119 74.6758 31.0119C74.762 31.1653 74.8482 31.4209 75.1328 31.4209H79.6087C80.0399 31.4209 80.0485 31.2079 80.0485 30.9779L80.0744 28.3879L80.954 28.4049C81.1092 28.4049 81.2903 28.2686 81.2903 28.1152V26.1983C81.2817 26.045 81.1179 25.8831 80.9626 25.8831H80.0744V22.9608H81.5059C81.6612 22.9608 81.7905 22.833 81.7905 22.6797V20.7372C81.7905 20.5838 81.6784 20.456 81.5232 20.456L81.5318 20.4731ZM77.1508 25.619C77.1508 25.7723 77.0215 25.9001 76.8663 25.9001H75.0294V24.1365C75.0294 23.8554 74.9172 23.7446 74.6326 23.7446H72.4163C72.2611 23.7446 72.08 23.8639 72.08 24.0684V25.9001H69.9067C69.8205 25.9001 69.5531 25.8234 69.5531 25.6275V23.3101C69.5531 23.1568 69.6739 23.0205 69.8377 23.0205H76.8663C77.0215 23.0205 77.1508 23.2249 77.1508 23.3783V25.619ZM77.1336 20.4731H75.038V18.6925C75.038 18.4965 74.9172 18.3261 74.762 18.3261H72.3645C72.2093 18.3261 72.08 18.4965 72.08 18.6499V20.4731H69.5359V17.7979C69.5359 17.5082 69.7774 17.3719 69.9326 17.3719L76.8318 17.3975C76.987 17.3975 77.1508 17.5338 77.1508 17.7382L77.1336 20.4731Z" fill="#19140F"/>
|
||||
<path d="M61.1184 14.9353H63.7401C63.8953 14.9353 64.0161 14.816 64.0161 14.6541V10.7606C64.0161 10.6073 63.8695 10.4795 63.7142 10.4795H61.1098C60.9546 10.4795 60.8252 10.6073 60.8252 10.7606V14.6541C60.8252 14.8075 60.9546 14.9353 61.1098 14.9353H61.1184Z" fill="#19140F"/>
|
||||
<path d="M34.323 44.7455L34.2626 44.6518C34.2109 44.5411 34.1591 44.4388 34.1074 44.3451L26.915 32.1364C26.5097 31.4718 25.768 31.0288 24.9315 31.0288C24.095 31.0288 23.3447 31.4804 22.9394 32.1534L15.7211 44.2599C15.7211 44.2599 15.7039 44.2855 15.6952 44.3025L15.6435 44.3877C15.6004 44.4644 15.5573 44.5496 15.5141 44.6263L15.4365 44.7455C14.9105 45.606 15.1864 46.7391 16.0574 47.2588L16.8508 47.7359C17.7219 48.2556 18.8689 47.983 19.3949 47.1225L19.8779 46.3387C19.9124 46.2876 19.9382 46.2365 19.9641 46.1768L24.897 37.9383L29.7782 46.1002C29.8127 46.1768 29.8472 46.262 29.8989 46.3387L30.2007 46.8329C30.2439 46.9095 30.2956 46.9862 30.3387 47.0544L30.3818 47.1225C30.9079 47.983 32.0549 48.2556 32.9259 47.7359L33.7193 47.2588C34.5903 46.7391 34.8663 45.606 34.3402 44.7455H34.323Z" fill="url(#paint0_radial_1_798)"/>
|
||||
<path d="M47.7674 26.2578H47.6553C47.5346 26.2492 47.4138 26.2407 47.3017 26.2407L33.0032 26.2918C32.2184 26.3089 31.4595 26.7178 31.0369 27.442C30.6144 28.1662 30.6402 29.0267 31.0197 29.7082L38.031 41.9425C38.031 41.9596 38.0482 41.9681 38.0568 41.9851L38.1086 42.0788C38.1517 42.1555 38.2034 42.2322 38.2552 42.3089L38.3242 42.4367C38.8157 43.3227 39.9455 43.6465 40.8424 43.1609L41.653 42.7178C42.5499 42.2322 42.8776 41.1161 42.3861 40.2301L41.9376 39.4292C41.9117 39.3781 41.8773 39.327 41.8428 39.2759L37.091 30.9436L46.6808 30.8499C46.767 30.8584 46.8533 30.8669 46.9481 30.8669L47.526 30.8499C47.6122 30.8499 47.7071 30.8499 47.7933 30.8499H47.8709C48.8885 30.8243 49.7078 29.9809 49.6819 28.9755L49.6647 28.0639C49.6388 27.0586 48.785 26.2492 47.7674 26.2748V26.2578Z" fill="url(#paint1_radial_1_798)"/>
|
||||
<path d="M38.2819 5.51233L38.2302 5.61457C38.1612 5.70829 38.0922 5.81052 38.0405 5.90424L30.9343 18.1641C30.5549 18.8457 30.5376 19.6976 30.9602 20.4218C31.3828 21.146 32.1503 21.5549 32.9437 21.572L47.1732 21.6912C47.1905 21.6912 47.2077 21.6912 47.2164 21.6912H47.3198C47.4061 21.6912 47.5009 21.6912 47.5958 21.6827H47.7424C48.76 21.7083 49.6138 20.9074 49.6397 19.8936L49.6569 18.982C49.6828 17.9767 48.8635 17.1332 47.8459 17.1076L46.9231 17.0906C46.8628 17.0906 46.8024 17.0906 46.742 17.0906L37.0573 16.9884L41.766 8.73278C41.8178 8.66462 41.8695 8.58795 41.9126 8.51127L42.1886 8.00861C42.2317 7.93193 42.2748 7.85525 42.3179 7.77858L42.3524 7.71042C42.844 6.82437 42.5077 5.70829 41.6194 5.22266L40.8088 4.77964C39.9119 4.29402 38.7821 4.62629 38.2906 5.50381L38.2819 5.51233Z" fill="url(#paint2_radial_1_798)"/>
|
||||
<path d="M15.359 3.25477L15.4194 3.34848C15.4711 3.45924 15.5229 3.56147 15.5746 3.65519L22.767 15.8639C23.1723 16.5285 23.914 16.9715 24.7505 16.9715C25.5871 16.9715 26.3373 16.5199 26.7427 15.8469L33.9609 3.74039C33.9609 3.74039 33.9782 3.71483 33.9868 3.69779L34.0385 3.61259C34.0817 3.53591 34.1248 3.45072 34.1679 3.37404L34.2455 3.25477C34.7716 2.39428 34.4956 1.26115 33.6246 0.741453L32.8312 0.26435C31.9602 -0.255352 30.8132 0.0172783 30.2871 0.877768L29.8042 1.66158C29.7697 1.7127 29.7438 1.76382 29.7179 1.82346L24.785 10.062L19.9039 1.90013C19.8694 1.82346 19.8349 1.73826 19.7831 1.66158L19.4813 1.16744C19.4382 1.09076 19.3864 1.01408 19.3433 0.945926L19.3002 0.877768C18.7741 0.0172783 17.6271 -0.255352 16.7561 0.26435L15.9627 0.741453C15.0917 1.26115 14.8157 2.39428 15.3418 3.25477H15.359Z" fill="url(#paint3_radial_1_798)"/>
|
||||
<path d="M1.91414 21.7426H2.02625C2.14699 21.7511 2.26772 21.7596 2.37983 21.7596L16.6784 21.7085C17.4631 21.6914 18.2221 21.2825 18.6446 20.5583C19.0672 19.8342 19.0413 18.9737 18.6619 18.2921L11.6506 6.05779C11.6506 6.04075 11.6333 6.03223 11.6247 6.0152L11.573 5.92148C11.5299 5.8448 11.4781 5.76812 11.4264 5.69145L11.3574 5.56365C10.8658 4.6776 9.73607 4.35385 8.83918 4.83948L8.02853 5.2825C7.13164 5.76812 6.80393 6.8842 7.29549 7.77025L7.74394 8.57111C7.76981 8.62222 7.80431 8.67334 7.8388 8.72446L12.5906 17.0567L3.00076 17.1504C2.91452 17.1419 2.82828 17.1334 2.73342 17.1334L2.15561 17.1504C2.06937 17.1504 1.97451 17.1504 1.88827 17.1504H1.81065C0.793024 17.1845 -0.0262521 18.028 -0.000380225 19.0418L0.0168677 19.9534C0.0427396 20.9588 0.896512 21.7681 1.91414 21.7426Z" fill="url(#paint4_radial_1_798)"/>
|
||||
<path d="M11.4002 42.488L11.4519 42.3858C11.5209 42.292 11.5899 42.1898 11.6417 42.0961L18.7478 29.8362C19.1273 29.1547 19.1445 28.3027 18.7219 27.5785C18.2994 26.8543 17.5318 26.4454 16.7384 26.4284L2.5175 26.3091C2.50025 26.3091 2.483 26.3091 2.47438 26.3091H2.37089C2.28465 26.3091 2.18979 26.3091 2.09493 26.3176H1.94832C0.930692 26.292 0.0769193 27.0929 0.0510474 28.1067L0.0337995 29.0183C0.00792757 30.0237 0.827204 30.8671 1.84483 30.8927L2.7676 30.9097C2.82796 30.9097 2.88833 30.9097 2.9487 30.9097L12.6334 31.012L7.92473 39.2675C7.87298 39.3357 7.82124 39.4124 7.77812 39.4891L7.50215 39.9917C7.45903 40.0684 7.41591 40.1451 7.37279 40.2218L7.3383 40.2899C6.84673 41.176 7.18306 42.292 8.07133 42.7777L8.88199 43.2207C9.77888 43.7063 10.9086 43.374 11.4002 42.4965V42.488Z" fill="url(#paint5_radial_1_798)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial_1_798" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(24.727 23.6565) scale(24.7299 24.4309)">
|
||||
<stop stop-color="#FFC80F"/>
|
||||
<stop offset="0.33" stop-color="#FFA512"/>
|
||||
<stop offset="0.77" stop-color="#FF7D17"/>
|
||||
<stop offset="1" stop-color="#FF6E19"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint1_radial_1_798" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(24.7267 23.6564) scale(24.7299 24.4309)">
|
||||
<stop stop-color="#FFC80F"/>
|
||||
<stop offset="0.33" stop-color="#FFA512"/>
|
||||
<stop offset="0.77" stop-color="#FF7D17"/>
|
||||
<stop offset="1" stop-color="#FF6E19"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint2_radial_1_798" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(24.7276 23.6565) scale(24.7299 24.4309)">
|
||||
<stop stop-color="#FFC80F"/>
|
||||
<stop offset="0.33" stop-color="#FFA512"/>
|
||||
<stop offset="0.77" stop-color="#FF7D17"/>
|
||||
<stop offset="1" stop-color="#FF6E19"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint3_radial_1_798" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(24.7272 23.6566) scale(24.7299 24.4309)">
|
||||
<stop stop-color="#FFC80F"/>
|
||||
<stop offset="0.33" stop-color="#FFA512"/>
|
||||
<stop offset="0.77" stop-color="#FF7D17"/>
|
||||
<stop offset="1" stop-color="#FF6E19"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint4_radial_1_798" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(24.727 23.6567) scale(24.7299 24.4309)">
|
||||
<stop stop-color="#FFC80F"/>
|
||||
<stop offset="0.33" stop-color="#FFA512"/>
|
||||
<stop offset="0.77" stop-color="#FF7D17"/>
|
||||
<stop offset="1" stop-color="#FF6E19"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint5_radial_1_798" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(24.7267 23.6566) scale(24.7299 24.4309)">
|
||||
<stop stop-color="#FFC80F"/>
|
||||
<stop offset="0.33" stop-color="#FFA512"/>
|
||||
<stop offset="0.77" stop-color="#FF7D17"/>
|
||||
<stop offset="1" stop-color="#FF6E19"/>
|
||||
</radialGradient>
|
||||
<clipPath id="clip0_1_798">
|
||||
<rect width="159" height="48" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 26 KiB |
845
frontend/public/gonavi-theme-settings.extract.css
Normal file
@@ -0,0 +1,845 @@
|
||||
/* ── Theme settings (v2 only): clean desktop settings ─── */
|
||||
body .gonavi-theme-settings {
|
||||
--gn-settings-fg: #0f172a;
|
||||
--gn-settings-muted: #64748b;
|
||||
--gn-settings-line: #e2e8f0;
|
||||
--gn-settings-track: #e2e8f0;
|
||||
--gn-settings-chip-bg: #f8fafc;
|
||||
--gn-settings-chip-fg: #0f172a;
|
||||
--gn-settings-seg-bg: #f8fafc;
|
||||
--gn-settings-seg-border: #e2e8f0;
|
||||
--gn-settings-seg-item: #64748b;
|
||||
--gn-settings-seg-selected-bg: #fff;
|
||||
--gn-settings-seg-selected-fg: #0f172a;
|
||||
--gn-settings-seg-shadow: none;
|
||||
--gn-settings-accent: #16a34a;
|
||||
--gn-settings-accent-soft: #dcfce7;
|
||||
--gn-settings-accent-border: #86efac;
|
||||
--gn-settings-card-bg: #ffffff;
|
||||
}
|
||||
|
||||
body[data-theme='dark'] .gonavi-theme-settings {
|
||||
--gn-settings-fg: rgba(248, 250, 252, 0.94);
|
||||
--gn-settings-muted: rgba(226, 232, 240, 0.58);
|
||||
--gn-settings-line: rgba(255, 255, 255, 0.10);
|
||||
--gn-settings-track: rgba(255, 255, 255, 0.12);
|
||||
--gn-settings-chip-bg: rgba(255, 255, 255, 0.06);
|
||||
--gn-settings-chip-fg: rgba(248, 250, 252, 0.88);
|
||||
--gn-settings-seg-bg: rgba(255, 255, 255, 0.04);
|
||||
--gn-settings-seg-border: rgba(148, 163, 184, 0.18);
|
||||
--gn-settings-seg-item: rgba(226, 232, 240, 0.58);
|
||||
--gn-settings-seg-selected-bg: rgba(30, 41, 59, 0.96);
|
||||
--gn-settings-seg-selected-fg: #f8fafc;
|
||||
--gn-settings-accent: #22c55e;
|
||||
--gn-settings-accent-soft: rgba(34, 197, 94, 0.18);
|
||||
--gn-settings-accent-border: rgba(74, 222, 128, 0.55);
|
||||
--gn-settings-card-bg: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-section {
|
||||
padding-left: 0;
|
||||
border-left: none;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-section + .gonavi-settings-section {
|
||||
margin-top: 20px;
|
||||
padding-top: 0;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0 0 10px;
|
||||
color: var(--gn-settings-fg);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-section-hint {
|
||||
margin: -2px 0 10px;
|
||||
color: var(--gn-settings-muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(120px, 168px) minmax(0, 1fr);
|
||||
gap: 12px 16px;
|
||||
align-items: center;
|
||||
min-height: 40px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-row.is-stacked {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-row + .gonavi-settings-row {
|
||||
border-top: 1px solid var(--gn-settings-line);
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-label {
|
||||
color: var(--gn-settings-fg);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-label-hint {
|
||||
margin-top: 3px;
|
||||
color: var(--gn-settings-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-control {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-slider-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
padding: 2px 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-slider-row.has-marks {
|
||||
align-items: flex-start;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-slider-row.has-marks .gonavi-settings-slider-value {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-slider-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
/*
|
||||
* antd Slider 手柄用 translateX(-50%),在 0%/100% 会各伸出半个圆 + 描边阴影。
|
||||
* 外层 padding 需 ≥ 半宽手柄(9) + 描边(2~3),否则仍会缺一截。
|
||||
*/
|
||||
body .gonavi-theme-settings .gonavi-settings-slider-track-wrap {
|
||||
flex: 0 0 auto;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding-inline: 14px;
|
||||
overflow: visible;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-slider-track-wrap .ant-slider {
|
||||
width: 100%;
|
||||
margin: 0 !important;
|
||||
padding-block: 14px !important;
|
||||
padding-inline: 0 !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
/* 预设刻度:独立一行,严格在进度条下方,不叠在轨道上 */
|
||||
body .gonavi-theme-settings .gonavi-settings-slider-presets {
|
||||
position: relative;
|
||||
height: 18px;
|
||||
margin: 0 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-slider-preset {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transform: translateX(-50%);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--gn-settings-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 18px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-slider-preset:hover:not(:disabled) {
|
||||
color: var(--gn-settings-fg);
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-slider-preset.is-active {
|
||||
color: var(--gn-settings-accent);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-slider-preset:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-slider-value-input.ant-input-number,
|
||||
body .gonavi-theme-settings .ant-input-number-group.ant-input-number-group-wrapper.gonavi-settings-slider-value-input,
|
||||
body .gonavi-theme-settings .gonavi-settings-slider-value-input.ant-input-number-group-wrapper {
|
||||
flex: 0 0 auto !important;
|
||||
width: 64px !important;
|
||||
min-width: 56px !important;
|
||||
max-width: 72px !important;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-slider-value-input .ant-input-number,
|
||||
body .gonavi-theme-settings .gonavi-settings-slider-value-input.ant-input-number {
|
||||
width: 100% !important;
|
||||
border-radius: 999px 0 0 999px !important;
|
||||
border-color: color-mix(in srgb, var(--gn-settings-accent) 28%, var(--gn-settings-line)) !important;
|
||||
background: color-mix(in srgb, var(--gn-settings-accent) 8%, transparent) !important;
|
||||
}
|
||||
|
||||
/* 无 addon 时整颗胶囊 */
|
||||
body .gonavi-theme-settings .gonavi-settings-slider-value-input.ant-input-number:not(.ant-input-number-group-wrapper) {
|
||||
width: 52px !important;
|
||||
min-width: 48px !important;
|
||||
max-width: 56px !important;
|
||||
border-radius: 999px !important;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-slider-value-input .ant-input-number-input {
|
||||
height: 28px !important;
|
||||
padding: 0 2px 0 6px !important;
|
||||
color: var(--gn-settings-fg) !important;
|
||||
font-size: 12px !important;
|
||||
font-weight: 700 !important;
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-slider-value-input .ant-input-number-group-addon {
|
||||
min-width: 18px !important;
|
||||
width: 18px !important;
|
||||
padding: 0 6px 0 0 !important;
|
||||
border: 1px solid color-mix(in srgb, var(--gn-settings-accent) 28%, var(--gn-settings-line)) !important;
|
||||
border-left: none !important;
|
||||
border-radius: 0 999px 999px 0 !important;
|
||||
background: color-mix(in srgb, var(--gn-settings-accent) 8%, transparent) !important;
|
||||
color: var(--gn-settings-muted) !important;
|
||||
font-size: 11px !important;
|
||||
font-weight: 700 !important;
|
||||
line-height: 26px !important;
|
||||
}
|
||||
|
||||
/* 带 % / px 的整体圆角胶囊外观 */
|
||||
body .gonavi-theme-settings .gonavi-settings-slider-value-input.ant-input-number-group-wrapper .ant-input-number {
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-slider-value-input.ant-input-number-disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .ant-slider-rail {
|
||||
height: 8px !important;
|
||||
border-radius: 999px !important;
|
||||
background: var(--gn-settings-track) !important;
|
||||
box-shadow: inset 0 1px 2px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .ant-slider-track {
|
||||
height: 8px !important;
|
||||
border-radius: 999px !important;
|
||||
background: linear-gradient(90deg, color-mix(in srgb, var(--gn-settings-accent) 78%, #fff) 0%, var(--gn-settings-accent) 100%) !important;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .ant-slider-handle {
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
inset-block-start: 10px !important;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* 关掉 antd 默认 before 光晕,避免叠成空心环 */
|
||||
body .gonavi-theme-settings .ant-slider-handle::before {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .ant-slider-handle::after {
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
inset-inline-start: 0 !important;
|
||||
inset-block-start: 0 !important;
|
||||
border-radius: 50% !important;
|
||||
/* 实心白底 + 绿描边,禁用时也不靠整控件 opacity 变空心 */
|
||||
background: #fff !important;
|
||||
box-shadow:
|
||||
0 0 0 2px var(--gn-settings-accent),
|
||||
0 1px 4px rgba(15, 23, 42, 0.14) !important;
|
||||
opacity: 1 !important;
|
||||
transition: box-shadow 0.12s ease !important;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .ant-slider-handle:hover::after,
|
||||
body .gonavi-theme-settings .ant-slider-handle:focus::after {
|
||||
box-shadow:
|
||||
0 0 0 2px var(--gn-settings-accent),
|
||||
0 0 0 4px color-mix(in srgb, var(--gn-settings-accent) 22%, transparent),
|
||||
0 1px 4px rgba(15, 23, 42, 0.14) !important;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .ant-slider:hover .ant-slider-track,
|
||||
body .gonavi-theme-settings .ant-slider:focus-within .ant-slider-track {
|
||||
background: linear-gradient(90deg, color-mix(in srgb, var(--gn-settings-accent) 78%, #fff) 0%, var(--gn-settings-accent) 100%) !important;
|
||||
}
|
||||
|
||||
/* 禁用:不要整条 opacity,否则白心被透掉只剩绿环 */
|
||||
body .gonavi-theme-settings .ant-slider-disabled {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .ant-slider-disabled .ant-slider-rail {
|
||||
background: color-mix(in srgb, var(--gn-settings-track) 70%, transparent) !important;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .ant-slider-disabled .ant-slider-track {
|
||||
background: color-mix(in srgb, var(--gn-settings-accent) 35%, var(--gn-settings-track)) !important;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .ant-slider-disabled .ant-slider-handle {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .ant-slider-disabled .ant-slider-handle::after {
|
||||
background: #fff !important;
|
||||
box-shadow:
|
||||
0 0 0 2px color-mix(in srgb, var(--gn-settings-accent) 40%, #94a3b8),
|
||||
0 1px 3px rgba(15, 23, 42, 0.10) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* 选项 Segmented:小胶囊;is-compact 不拉满整行 */
|
||||
body .gonavi-theme-settings .gonavi-settings-segmented-choice.ant-segmented {
|
||||
width: 100%;
|
||||
height: auto !important;
|
||||
min-height: 0 !important;
|
||||
padding: 0 !important;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-segmented-choice.is-compact.ant-segmented {
|
||||
width: auto !important;
|
||||
max-width: 100%;
|
||||
display: inline-flex !important;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-group {
|
||||
display: flex !important;
|
||||
width: 100%;
|
||||
gap: 6px !important;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-segmented-choice.is-compact .ant-segmented-group {
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-item {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
border-radius: 8px !important;
|
||||
border: 1px solid var(--gn-settings-line) !important;
|
||||
transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease !important;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-segmented-choice.is-compact .ant-segmented-item {
|
||||
flex: 0 0 auto !important;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-item:not(.ant-segmented-item-selected) {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-item:hover:not(.ant-segmented-item-selected) {
|
||||
background: var(--gn-settings-seg-bg) !important;
|
||||
border-color: color-mix(in srgb, var(--gn-settings-accent) 28%, var(--gn-settings-line)) !important;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-item-label {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
min-height: 30px !important;
|
||||
padding: 0 12px !important;
|
||||
font-size: 12.5px !important;
|
||||
font-weight: 600 !important;
|
||||
line-height: 30px !important;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-item:not(.ant-segmented-item-selected) .ant-segmented-item-label {
|
||||
color: var(--gn-settings-muted) !important;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-thumb {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-item.ant-segmented-item-selected,
|
||||
body .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-item[aria-selected='true'] {
|
||||
border-color: var(--gn-settings-accent) !important;
|
||||
background: var(--gn-settings-accent-soft) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-item.ant-segmented-item-selected .ant-segmented-item-label,
|
||||
body .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-item[aria-selected='true'] .ant-segmented-item-label {
|
||||
color: var(--gn-settings-accent) !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
body[data-theme='dark'] .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-item.ant-segmented-item-selected,
|
||||
body[data-theme='dark'] .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-item[aria-selected='true'] {
|
||||
border-color: color-mix(in srgb, var(--gn-settings-accent) 70%, rgba(255, 255, 255, 0.2)) !important;
|
||||
background: color-mix(in srgb, var(--gn-settings-accent) 22%, transparent) !important;
|
||||
}
|
||||
|
||||
body[data-theme='dark'] .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-item.ant-segmented-item-selected .ant-segmented-item-label,
|
||||
body[data-theme='dark'] .gonavi-theme-settings .gonavi-settings-segmented-choice .ant-segmented-item[aria-selected='true'] .ant-segmented-item-label {
|
||||
color: #86efac !important;
|
||||
}
|
||||
|
||||
/* 界面版本预览卡(紧凑双列) */
|
||||
body .gonavi-theme-settings .gonavi-settings-ui-version-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-ui-version-tile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--gn-settings-line);
|
||||
border-radius: 12px;
|
||||
background: var(--gn-settings-card-bg);
|
||||
color: var(--gn-settings-fg);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.12s ease, box-shadow 0.12s ease;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-ui-version-tile:hover {
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-ui-version-tile.is-active {
|
||||
border-color: var(--gn-settings-accent);
|
||||
box-shadow: 0 0 0 1px var(--gn-settings-accent-border);
|
||||
background: var(--gn-settings-card-bg);
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-ui-version-preview {
|
||||
position: relative;
|
||||
height: 58px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--gn-settings-line);
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
body[data-theme='dark'] .gonavi-theme-settings .gonavi-settings-ui-version-preview {
|
||||
background: #0f172a;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-ui-version-preview.is-legacy {
|
||||
display: grid;
|
||||
grid-template-columns: 34% 1fr;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-ui-version-preview.is-v2 {
|
||||
display: grid;
|
||||
grid-template-columns: 18px 1fr;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-ui-version-preview .uv-side {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
padding: 8px 6px;
|
||||
border-right: 1px solid rgba(15, 23, 42, 0.08);
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
body[data-theme='dark'] .gonavi-theme-settings .gonavi-settings-ui-version-preview .uv-side {
|
||||
border-right-color: rgba(255, 255, 255, 0.08);
|
||||
background: rgba(30, 41, 59, 0.85);
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-ui-version-preview.is-v2 .uv-side {
|
||||
align-items: center;
|
||||
padding: 8px 3px;
|
||||
background: linear-gradient(180deg, #16a34a 0%, #15803d 100%);
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-ui-version-preview .uv-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(15, 23, 42, 0.18);
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-ui-version-preview.is-v2 .uv-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-ui-version-preview .uv-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-ui-version-preview .uv-bar {
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background: rgba(15, 23, 42, 0.10);
|
||||
}
|
||||
|
||||
body[data-theme='dark'] .gonavi-theme-settings .gonavi-settings-ui-version-preview .uv-bar {
|
||||
background: rgba(248, 250, 252, 0.12);
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-ui-version-preview .uv-bar.is-accent {
|
||||
width: 42%;
|
||||
background: color-mix(in srgb, var(--gn-settings-accent) 55%, rgba(15, 23, 42, 0.08));
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-ui-version-preview.is-v2 .uv-bar.is-accent {
|
||||
width: 48%;
|
||||
background: color-mix(in srgb, var(--gn-settings-accent) 70%, #fff);
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-ui-version-preview .uv-cards {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 5px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-ui-version-preview .uv-card {
|
||||
border-radius: 5px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
body[data-theme='dark'] .gonavi-theme-settings .gonavi-settings-ui-version-preview .uv-card {
|
||||
background: rgba(30, 41, 59, 0.9);
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-ui-version-preview.is-legacy .uv-card {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-ui-version-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-ui-version-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-ui-version-badge {
|
||||
flex-shrink: 0;
|
||||
padding: 1px 6px;
|
||||
border-radius: 999px;
|
||||
background: rgba(15, 23, 42, 0.06);
|
||||
color: var(--gn-settings-muted);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-ui-version-badge.is-new {
|
||||
background: rgba(2, 132, 199, 0.12);
|
||||
color: #0284c7;
|
||||
}
|
||||
|
||||
body[data-theme='dark'] .gonavi-theme-settings .gonavi-settings-ui-version-badge {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
body[data-theme='dark'] .gonavi-theme-settings .gonavi-settings-ui-version-badge.is-new {
|
||||
background: rgba(56, 189, 248, 0.18);
|
||||
color: #7dd3fc;
|
||||
}
|
||||
|
||||
/* 顶部分段:原生 button Tab(不依赖 antd Segmented) */
|
||||
body .gonavi-theme-settings .gonavi-settings-tabs {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--gn-settings-line);
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-tab {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
margin: 0 0 -1px;
|
||||
padding: 10px 8px 12px;
|
||||
border: 0;
|
||||
border-bottom: 2px solid transparent;
|
||||
background: transparent;
|
||||
color: var(--gn-settings-muted);
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-tab:hover {
|
||||
color: var(--gn-settings-fg);
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-tab.is-active {
|
||||
color: var(--gn-settings-accent);
|
||||
border-bottom-color: var(--gn-settings-accent);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-tab-icon {
|
||||
display: inline-flex;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-inline-choice-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-top: 14px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--gn-settings-line);
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-pills {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-pill {
|
||||
margin: 0;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid var(--gn-settings-line);
|
||||
border-radius: 8px;
|
||||
background: var(--gn-settings-card-bg);
|
||||
color: var(--gn-settings-muted);
|
||||
font: inherit;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-pill:hover {
|
||||
border-color: #cbd5e1;
|
||||
color: var(--gn-settings-fg);
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-pill.is-active {
|
||||
border-color: var(--gn-settings-accent);
|
||||
background: var(--gn-settings-accent-soft);
|
||||
color: var(--gn-settings-accent);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
body[data-theme='dark'] .gonavi-theme-settings .gonavi-settings-pill.is-active {
|
||||
color: #86efac;
|
||||
border-color: color-mix(in srgb, var(--gn-settings-accent) 70%, rgba(255, 255, 255, 0.2));
|
||||
background: color-mix(in srgb, var(--gn-settings-accent) 22%, transparent);
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-inline-meta {
|
||||
margin-top: 8px;
|
||||
color: var(--gn-settings-muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Beta:弱提示,不要大黄条 */
|
||||
body .gonavi-theme-settings .gonavi-settings-beta-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 6px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--gn-settings-accent) 12%, transparent);
|
||||
color: var(--gn-settings-accent);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-alert {
|
||||
margin-top: 6px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--gn-settings-muted);
|
||||
font-size: 11.5px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
body[data-theme='dark'] .gonavi-theme-settings .gonavi-settings-alert {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--gn-settings-muted);
|
||||
}
|
||||
|
||||
/* Compact theme-mode previews */
|
||||
body .gonavi-theme-settings .gonavi-settings-mode-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-mode-tile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--gn-settings-line);
|
||||
border-radius: 12px;
|
||||
background: var(--gn-settings-card-bg);
|
||||
color: var(--gn-settings-fg);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.12s ease, box-shadow 0.12s ease;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-mode-tile:hover {
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-mode-tile.is-active {
|
||||
border-color: var(--gn-settings-accent);
|
||||
box-shadow: 0 0 0 1px var(--gn-settings-accent-border);
|
||||
background: var(--gn-settings-card-bg);
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-mode-preview {
|
||||
height: 44px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--gn-settings-line);
|
||||
display: grid;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-mode-preview.is-system {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-mode-preview-pane {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
align-content: center;
|
||||
padding: 7px 8px;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-mode-preview-pane.is-light {
|
||||
background: linear-gradient(180deg, #f8fafc 0%, #eef2f7 100%);
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-mode-preview-pane.is-dark {
|
||||
background: linear-gradient(180deg, #0f172a 0%, #111827 100%);
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-mode-preview-line {
|
||||
height: 5px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-mode-preview-pane.is-light .gonavi-settings-mode-preview-line:nth-child(1) {
|
||||
width: 58%;
|
||||
background: rgba(15, 23, 42, 0.14);
|
||||
}
|
||||
body .gonavi-theme-settings .gonavi-settings-mode-preview-pane.is-light .gonavi-settings-mode-preview-line:nth-child(2) {
|
||||
width: 78%;
|
||||
background: rgba(22, 163, 74, 0.38);
|
||||
}
|
||||
body .gonavi-theme-settings .gonavi-settings-mode-preview-pane.is-light .gonavi-settings-mode-preview-line:nth-child(3) {
|
||||
width: 42%;
|
||||
background: rgba(15, 23, 42, 0.10);
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-mode-preview-pane.is-dark .gonavi-settings-mode-preview-line:nth-child(1) {
|
||||
width: 58%;
|
||||
background: rgba(248, 250, 252, 0.22);
|
||||
}
|
||||
body .gonavi-theme-settings .gonavi-settings-mode-preview-pane.is-dark .gonavi-settings-mode-preview-line:nth-child(2) {
|
||||
width: 78%;
|
||||
background: rgba(74, 222, 128, 0.45);
|
||||
}
|
||||
body .gonavi-theme-settings .gonavi-settings-mode-preview-pane.is-dark .gonavi-settings-mode-preview-line:nth-child(3) {
|
||||
width: 42%;
|
||||
background: rgba(248, 250, 252, 0.12);
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-mode-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-mode-label {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-mode-check {
|
||||
color: var(--gn-settings-accent);
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Control-only stacked row: no duplicate label column */
|
||||
body .gonavi-theme-settings .gonavi-settings-row.is-control-only {
|
||||
grid-template-columns: 1fr;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
body .gonavi-theme-settings .gonavi-settings-row.is-control-only .gonavi-settings-label {
|
||||
display: none;
|
||||
}
|
||||
@@ -1,52 +1,40 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none">
|
||||
<defs>
|
||||
<!-- Background: Soft Light Grey -->
|
||||
<linearGradient id="bgSoft" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#f5f7fa;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#c3cfe2;stop-opacity:1" />
|
||||
<linearGradient id="orbitA" x1="90" y1="130" x2="422" y2="360" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#E879F9"/>
|
||||
<stop offset="55%" stop-color="#8B5CF6"/>
|
||||
<stop offset="100%" stop-color="#4F46E5"/>
|
||||
</linearGradient>
|
||||
|
||||
<!-- Hexagon: Solid Tech Pink -->
|
||||
<linearGradient id="solidPink" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#FF5F6D;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#FFC371;stop-opacity:1" />
|
||||
<linearGradient id="orbitB" x1="110" y1="380" x2="390" y2="110" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#A5B4FC"/>
|
||||
<stop offset="100%" stop-color="#C084FC"/>
|
||||
</linearGradient>
|
||||
|
||||
<!-- N: Solid Tech Blue/Cyan -->
|
||||
<linearGradient id="solidCyan" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#00c6ff;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#0072ff;stop-opacity:1" />
|
||||
<radialGradient id="core" cx="42%" cy="38%" r="62%" gradientUnits="objectBoundingBox">
|
||||
<stop offset="0%" stop-color="#F5D0FE"/>
|
||||
<stop offset="35%" stop-color="#C084FC"/>
|
||||
<stop offset="100%" stop-color="#4F46E5"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="bgSoft" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#FFFFFF"/>
|
||||
<stop offset="100%" stop-color="#F4F4F5"/>
|
||||
</linearGradient>
|
||||
|
||||
<filter id="hardShadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="4"/>
|
||||
<feOffset dx="4" dy="4" result="offsetblur"/>
|
||||
<feComponentTransfer>
|
||||
<feFuncA type="linear" slope="0.2"/>
|
||||
</feComponentTransfer>
|
||||
<feMerge>
|
||||
<feMergeNode/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
<filter id="soft" x="-30%" y="-30%" width="160%" height="160%">
|
||||
<feDropShadow dx="0" dy="4" stdDeviation="8" flood-color="#4F46E5" flood-opacity="0.18"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect x="32" y="32" width="448" height="448" rx="100" fill="url(#bgSoft)" />
|
||||
|
||||
<!-- Main Content Centered -->
|
||||
<g transform="translate(106, 106) scale(0.6)" filter="url(#hardShadow)">
|
||||
|
||||
<!-- Hex G -->
|
||||
<path d="M 250 0 L 466 125 L 466 375 L 250 500 L 34 375 L 34 125 Z"
|
||||
fill="none" stroke="url(#solidPink)" stroke-width="45" stroke-linejoin="round"/>
|
||||
|
||||
<!-- G Crossbar -->
|
||||
<path d="M 466 300 L 330 300" stroke="url(#solidPink)" stroke-width="45" stroke-linecap="round"/>
|
||||
|
||||
<!-- Inner N -->
|
||||
<path d="M 160 350 L 160 150 L 340 350 L 340 150"
|
||||
fill="none" stroke="url(#solidCyan)" stroke-width="50" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
|
||||
<!-- App tile -->
|
||||
<rect x="32" y="32" width="448" height="448" rx="100" fill="url(#bgSoft)"/>
|
||||
<g transform="translate(256,256) scale(0.92)">
|
||||
<g transform="rotate(35)">
|
||||
<ellipse cx="0" cy="0" rx="168" ry="62" stroke="url(#orbitB)" stroke-width="14" opacity="0.72"/>
|
||||
</g>
|
||||
<circle cx="0" cy="0" r="58" fill="url(#core)" filter="url(#soft)"/>
|
||||
<circle cx="-18" cy="-18" r="16" fill="#FFFFFF" opacity="0.35"/>
|
||||
<g transform="rotate(-25)">
|
||||
<ellipse cx="0" cy="0" rx="198" ry="74" stroke="url(#orbitA)" stroke-width="18"/>
|
||||
<circle cx="178" cy="-24" r="16" fill="#F0ABFC"/>
|
||||
<circle cx="-168" cy="30" r="12" fill="#A5B4FC"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.9 KiB |
58
frontend/public/theme-settings-real-dom.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN" data-theme="light">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Real DOM Theme Settings Check</title>
|
||||
<link rel="stylesheet" href="./gonavi-theme-settings.extract.css" />
|
||||
<style>
|
||||
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif; background: #eef2f6; padding: 24px; }
|
||||
.frame { width: 720px; margin: 0 auto; background: #fff; border-radius: 12px; padding: 16px 20px 24px; border: 1px solid #e2e8f0; }
|
||||
h1 { margin: 0 0 4px; font-size: 16px; }
|
||||
.sub { margin: 0 0 12px; color: #64748b; font-size: 12px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<h1>主题与外观</h1>
|
||||
<p class="sub">切换亮暗主题并调整界面观感。</p>
|
||||
<div class="gonavi-theme-settings">
|
||||
<div class="gonavi-settings-tabs" role="tablist">
|
||||
<button type="button" class="gonavi-settings-tab is-active" role="tab" aria-selected="true">
|
||||
<span class="gonavi-settings-tab-icon">▣</span><span>主题与界面</span>
|
||||
</button>
|
||||
<button type="button" class="gonavi-settings-tab" role="tab" aria-selected="false">
|
||||
<span class="gonavi-settings-tab-icon">◐</span><span>显示与字体</span>
|
||||
</button>
|
||||
<button type="button" class="gonavi-settings-tab" role="tab" aria-selected="false">
|
||||
<span class="gonavi-settings-tab-icon">▦</span><span>工作区</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="gonavi-settings-inline-meta">更改将立即生效,无需保存。</div>
|
||||
|
||||
<section class="gonavi-settings-section" style="margin-top:16px">
|
||||
<div class="gonavi-settings-section-title">亮暗模式</div>
|
||||
<div class="gonavi-settings-mode-grid">
|
||||
<button type="button" class="gonavi-settings-mode-tile"><div class="gonavi-settings-mode-preview"><div class="gonavi-settings-mode-preview-pane is-light"><span class="gonavi-settings-mode-preview-line"></span><span class="gonavi-settings-mode-preview-line"></span><span class="gonavi-settings-mode-preview-line"></span></div></div><div class="gonavi-settings-mode-meta"><span class="gonavi-settings-mode-label">亮色主题</span></div></button>
|
||||
<button type="button" class="gonavi-settings-mode-tile"><div class="gonavi-settings-mode-preview"><div class="gonavi-settings-mode-preview-pane is-dark"><span class="gonavi-settings-mode-preview-line"></span><span class="gonavi-settings-mode-preview-line"></span><span class="gonavi-settings-mode-preview-line"></span></div></div><div class="gonavi-settings-mode-meta"><span class="gonavi-settings-mode-label">暗色主题</span></div></button>
|
||||
<button type="button" class="gonavi-settings-mode-tile is-active"><div class="gonavi-settings-mode-preview is-system"><div class="gonavi-settings-mode-preview-pane is-light"><span class="gonavi-settings-mode-preview-line"></span><span class="gonavi-settings-mode-preview-line"></span><span class="gonavi-settings-mode-preview-line"></span></div><div class="gonavi-settings-mode-preview-pane is-dark"><span class="gonavi-settings-mode-preview-line"></span><span class="gonavi-settings-mode-preview-line"></span><span class="gonavi-settings-mode-preview-line"></span></div></div><div class="gonavi-settings-mode-meta"><span class="gonavi-settings-mode-label">跟随系统</span><span class="gonavi-settings-mode-check">✓</span></div></button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="gonavi-settings-section" style="margin-top:20px">
|
||||
<div class="gonavi-settings-section-title">界面版本 <span class="gonavi-settings-beta-chip">Beta</span></div>
|
||||
<div class="gonavi-settings-ui-version-grid">
|
||||
<button type="button" class="gonavi-settings-ui-version-tile"><div class="gonavi-settings-ui-version-preview is-legacy"><div class="uv-side"><span class="uv-dot"></span><span class="uv-dot"></span><span class="uv-dot"></span></div><div class="uv-main"><div class="uv-bar is-accent"></div><div class="uv-cards"><div class="uv-card"></div><div class="uv-card"></div></div></div></div><div class="gonavi-settings-ui-version-meta"><span class="gonavi-settings-ui-version-title"><span>旧版 UI</span><span class="gonavi-settings-ui-version-badge">默认</span></span></div></button>
|
||||
<button type="button" class="gonavi-settings-ui-version-tile is-active"><div class="gonavi-settings-ui-version-preview is-v2"><div class="uv-side"><span class="uv-dot"></span><span class="uv-dot"></span><span class="uv-dot"></span></div><div class="uv-main"><div class="uv-bar is-accent"></div><div class="uv-cards"><div class="uv-card"></div><div class="uv-card"></div></div></div></div><div class="gonavi-settings-ui-version-meta"><span class="gonavi-settings-ui-version-title"><span>新版 UI</span><span class="gonavi-settings-ui-version-badge is-new">Beta</span></span><span class="gonavi-settings-mode-check">✓</span></div></button>
|
||||
</div>
|
||||
<div class="gonavi-settings-inline-choice-row">
|
||||
<span class="gonavi-settings-label">新版左侧搜索模式</span>
|
||||
<div class="gonavi-settings-pills">
|
||||
<button type="button" class="gonavi-settings-pill is-active">新版命令搜索</button>
|
||||
<button type="button" class="gonavi-settings-pill">旧版侧栏筛选</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
386
frontend/public/theme-settings-visual.html
Normal file
@@ -0,0 +1,386 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>GoNavi Theme Settings Visual</title>
|
||||
<style>
|
||||
:root {
|
||||
--fg: #0f172a;
|
||||
--muted: #64748b;
|
||||
--line: #e2e8f0;
|
||||
--bg: #f8fafc;
|
||||
--panel: #ffffff;
|
||||
--accent: #16a34a;
|
||||
--accent-soft: #dcfce7;
|
||||
--accent-border: #86efac;
|
||||
--radius: 10px;
|
||||
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Noto Sans SC", sans-serif;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
font-family: var(--font);
|
||||
background: #e8edf3;
|
||||
color: var(--fg);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
}
|
||||
.shell {
|
||||
width: min(920px, 100%);
|
||||
height: 640px;
|
||||
background: var(--panel);
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(15,23,42,.08);
|
||||
box-shadow: 0 20px 50px rgba(15,23,42,.10);
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
overflow: hidden;
|
||||
}
|
||||
.side {
|
||||
background: #fbfcfd;
|
||||
border-right: 1px solid var(--line);
|
||||
padding: 18px 12px;
|
||||
}
|
||||
.side .item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.side .item.active {
|
||||
background: #ecfdf5;
|
||||
color: #166534;
|
||||
}
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
background: #fff;
|
||||
}
|
||||
.header {
|
||||
padding: 18px 22px 10px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.header p {
|
||||
margin: 4px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.body {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 16px 22px 24px;
|
||||
}
|
||||
|
||||
/* underline tabs */
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.tab {
|
||||
flex: 1;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 10px 8px 12px;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
.tab.active {
|
||||
color: var(--accent);
|
||||
border-bottom-color: var(--accent);
|
||||
font-weight: 700;
|
||||
}
|
||||
.hint {
|
||||
margin: -8px 0 16px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0 0 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.chip {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 1px 7px;
|
||||
border-radius: 999px;
|
||||
background: #e0f2fe;
|
||||
color: #0369a1;
|
||||
}
|
||||
.chip.beta {
|
||||
background: #ecfdf5;
|
||||
color: #15803d;
|
||||
}
|
||||
.meta {
|
||||
margin: 8px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.grid-3 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
.grid-2 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
.card {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
font: inherit;
|
||||
}
|
||||
.card:hover { border-color: #cbd5e1; }
|
||||
.card.active {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 1px var(--accent-border);
|
||||
}
|
||||
.card .preview {
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--line);
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.preview.light {
|
||||
background: linear-gradient(180deg, #f8fafc, #eef2f7);
|
||||
padding: 8px;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
.preview.dark {
|
||||
background: linear-gradient(180deg, #0f172a, #111827);
|
||||
padding: 8px;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
.preview.system {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.bar {
|
||||
height: 5px;
|
||||
border-radius: 99px;
|
||||
background: rgba(15,23,42,.12);
|
||||
}
|
||||
.preview.dark .bar { background: rgba(248,250,252,.18); }
|
||||
.bar.accent { background: rgba(22,163,74,.4); width: 72%; }
|
||||
.preview.dark .bar.accent { background: rgba(74,222,128,.5); width: 72%; }
|
||||
.card .label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.check { color: var(--accent); font-size: 12px; }
|
||||
|
||||
.ui-preview {
|
||||
height: 58px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--line);
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
margin-bottom: 8px;
|
||||
background: #f1f5f9;
|
||||
}
|
||||
.ui-preview.legacy { grid-template-columns: 34% 1fr; }
|
||||
.ui-preview.v2 { grid-template-columns: 16px 1fr; }
|
||||
.ui-side {
|
||||
background: #fff;
|
||||
border-right: 1px solid var(--line);
|
||||
padding: 8px 6px;
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
align-content: start;
|
||||
}
|
||||
.ui-preview.v2 .ui-side {
|
||||
background: linear-gradient(180deg, #16a34a, #15803d);
|
||||
border: 0;
|
||||
place-items: center;
|
||||
padding: 8px 2px;
|
||||
}
|
||||
.dot {
|
||||
width: 7px; height: 7px; border-radius: 99px;
|
||||
background: rgba(15,23,42,.16);
|
||||
}
|
||||
.ui-preview.v2 .dot { background: rgba(255,255,255,.9); }
|
||||
.ui-main { padding: 8px; display: grid; gap: 6px; }
|
||||
.ui-main .row { height: 8px; border-radius: 4px; background: rgba(15,23,42,.08); }
|
||||
.ui-main .row.a { width: 46%; background: rgba(22,163,74,.35); }
|
||||
.ui-cards { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; height: 24px; }
|
||||
.ui-cards span {
|
||||
border-radius: 5px;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(15,23,42,.06);
|
||||
}
|
||||
|
||||
.row-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-top: 14px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.row-inline .name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.pills {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
}
|
||||
.pill {
|
||||
border: 1px solid var(--line);
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 6px 12px;
|
||||
font: inherit;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
.pill.active {
|
||||
border-color: var(--accent);
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell" id="fixture">
|
||||
<aside class="side">
|
||||
<div class="item active">偏好设置</div>
|
||||
<div class="item">服务配置</div>
|
||||
<div class="item">关于</div>
|
||||
</aside>
|
||||
<section class="main">
|
||||
<div class="header">
|
||||
<h1>主题与外观</h1>
|
||||
<p>切换亮暗主题并调整界面观感。</p>
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="tabs">
|
||||
<button class="tab active" type="button">主题与界面</button>
|
||||
<button class="tab" type="button">显示与字体</button>
|
||||
<button class="tab" type="button">工作区</button>
|
||||
</div>
|
||||
<p class="hint">更改将立即生效,无需保存。</p>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">亮暗模式</div>
|
||||
<div class="grid-3">
|
||||
<button class="card" type="button">
|
||||
<div class="preview light">
|
||||
<div class="bar" style="width:55%"></div>
|
||||
<div class="bar accent"></div>
|
||||
<div class="bar" style="width:40%"></div>
|
||||
</div>
|
||||
<div class="label"><span>亮色主题</span></div>
|
||||
</button>
|
||||
<button class="card" type="button">
|
||||
<div class="preview dark">
|
||||
<div class="bar" style="width:55%"></div>
|
||||
<div class="bar accent"></div>
|
||||
<div class="bar" style="width:40%"></div>
|
||||
</div>
|
||||
<div class="label"><span>暗色主题</span></div>
|
||||
</button>
|
||||
<button class="card active" type="button">
|
||||
<div class="preview system">
|
||||
<div class="preview light" style="border:0;height:100%;border-radius:0;margin:0">
|
||||
<div class="bar" style="width:55%"></div>
|
||||
<div class="bar accent"></div>
|
||||
<div class="bar" style="width:40%"></div>
|
||||
</div>
|
||||
<div class="preview dark" style="border:0;height:100%;border-radius:0;margin:0">
|
||||
<div class="bar" style="width:55%"></div>
|
||||
<div class="bar accent"></div>
|
||||
<div class="bar" style="width:40%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="label"><span>跟随系统</span><span class="check">✓</span></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">
|
||||
界面版本
|
||||
<span class="chip">NEW</span>
|
||||
<span class="chip beta">Beta</span>
|
||||
</div>
|
||||
<div class="grid-2">
|
||||
<button class="card" type="button">
|
||||
<div class="ui-preview legacy">
|
||||
<div class="ui-side"><div class="dot"></div><div class="dot"></div><div class="dot"></div></div>
|
||||
<div class="ui-main">
|
||||
<div class="row a"></div>
|
||||
<div class="ui-cards"><span></span><span></span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="label"><span>旧版 UI · 默认</span></div>
|
||||
</button>
|
||||
<button class="card active" type="button">
|
||||
<div class="ui-preview v2">
|
||||
<div class="ui-side"><div class="dot"></div><div class="dot"></div><div class="dot"></div></div>
|
||||
<div class="ui-main">
|
||||
<div class="row a"></div>
|
||||
<div class="ui-cards"><span></span><span></span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="label"><span>新版 UI · Beta</span><span class="check">✓</span></div>
|
||||
</button>
|
||||
</div>
|
||||
<p class="meta">全平台可切换,立即生效;部分弹窗下次打开时应用新样式。</p>
|
||||
<div class="row-inline">
|
||||
<div class="name">左侧搜索</div>
|
||||
<div class="pills">
|
||||
<button class="pill active" type="button">命令搜索</button>
|
||||
<button class="pill" type="button">侧栏筛选</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
125
frontend/scripts/wails-frontend-install.mjs
Normal file
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const frontendDir = path.resolve(scriptDir, '..');
|
||||
const packageJsonPath = path.join(frontendDir, 'package.json');
|
||||
const packageLockPath = path.join(frontendDir, 'package-lock.json');
|
||||
const nodeModulesPath = path.join(frontendDir, 'node_modules');
|
||||
const npmHiddenLockPath = path.join(nodeModulesPath, '.package-lock.json');
|
||||
const installStatePath = path.join(nodeModulesPath, '.gonavi-install-state.json');
|
||||
const npmCommand = 'npm';
|
||||
const commonArgs = [
|
||||
'--prefer-offline',
|
||||
'--no-audit',
|
||||
'--fund=false',
|
||||
'--fetch-retries=5',
|
||||
'--fetch-retry-mintimeout=20000',
|
||||
'--fetch-retry-maxtimeout=120000',
|
||||
];
|
||||
const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';
|
||||
|
||||
const fail = (message) => {
|
||||
console.error(`[gonavi-frontend-install] ${message}`);
|
||||
process.exit(1);
|
||||
};
|
||||
|
||||
const exitWithStatus = (status) => {
|
||||
process.exit(typeof status === 'number' && status > 0 && status <= 255 ? status : 1);
|
||||
};
|
||||
|
||||
const hashFile = (filePath) => {
|
||||
const hash = createHash('sha256');
|
||||
hash.update(readFileSync(filePath));
|
||||
return hash.digest('hex');
|
||||
};
|
||||
|
||||
const currentState = () => ({
|
||||
packageJson: hashFile(packageJsonPath),
|
||||
packageLock: existsSync(packageLockPath) ? hashFile(packageLockPath) : '',
|
||||
});
|
||||
|
||||
const readInstalledState = () => {
|
||||
if (!existsSync(installStatePath)) return null;
|
||||
try {
|
||||
return JSON.parse(readFileSync(installStatePath, 'utf8'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const writeInstalledState = (state) => {
|
||||
writeFileSync(installStatePath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
||||
};
|
||||
|
||||
const packageInputsAreOlderThanNpmLock = () => {
|
||||
if (!existsSync(npmHiddenLockPath)) return false;
|
||||
const markerTime = statSync(npmHiddenLockPath).mtimeMs;
|
||||
return [packageJsonPath, packageLockPath]
|
||||
.filter(existsSync)
|
||||
.every((filePath) => statSync(filePath).mtimeMs <= markerTime);
|
||||
};
|
||||
|
||||
const runNpm = (subcommand) => {
|
||||
const args = [subcommand, ...commonArgs];
|
||||
if (isCI) {
|
||||
console.log(
|
||||
`[gonavi-frontend-install] cwd=${process.cwd()} frontend=${frontendDir} node=${process.version} platform=${process.platform}/${process.arch} command=${npmCommand} ${args.join(' ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
const result = spawnSync(npmCommand, args, {
|
||||
cwd: frontendDir,
|
||||
env: process.env,
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32',
|
||||
});
|
||||
if (result.error) {
|
||||
fail(`failed to start npm: ${result.error.message}`);
|
||||
}
|
||||
if (result.signal) {
|
||||
fail(`npm was terminated by signal ${result.signal}`);
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
console.error(`[gonavi-frontend-install] npm exited with status ${result.status ?? 'unknown'}`);
|
||||
exitWithStatus(result.status);
|
||||
}
|
||||
};
|
||||
|
||||
if (!existsSync(packageJsonPath)) {
|
||||
fail(`package.json not found at ${packageJsonPath}; cwd=${process.cwd()}`);
|
||||
}
|
||||
|
||||
const state = currentState();
|
||||
const installedState = readInstalledState();
|
||||
const forceInstall = process.env.GONAVI_FORCE_FRONTEND_INSTALL === '1';
|
||||
|
||||
if (!forceInstall && existsSync(nodeModulesPath)) {
|
||||
if (
|
||||
installedState?.packageJson === state.packageJson &&
|
||||
installedState?.packageLock === state.packageLock
|
||||
) {
|
||||
console.log('Frontend dependencies are up to date; skipping npm install.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!installedState && isCI && existsSync(npmHiddenLockPath)) {
|
||||
writeInstalledState(state);
|
||||
console.log('Frontend dependencies are up to date from CI cache; recorded install state.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!installedState && packageInputsAreOlderThanNpmLock()) {
|
||||
writeInstalledState(state);
|
||||
console.log('Frontend dependencies are up to date; recorded install state.');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
runNpm(isCI ? 'ci' : 'install');
|
||||
writeInstalledState(state);
|
||||
27
frontend/src/App.ai-panel-error-boundary.test.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const appSource = readFileSync(
|
||||
fileURLToPath(new globalThis.URL('./App.tsx', import.meta.url)),
|
||||
'utf8',
|
||||
);
|
||||
const aiPanelBoundarySource = readFileSync(
|
||||
fileURLToPath(new globalThis.URL('./components/ai/AIPanelErrorBoundary.tsx', import.meta.url)),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
describe('AI panel lazy-load guard', () => {
|
||||
it('keeps AI panel failures scoped to the panel area with retry support', () => {
|
||||
expect(appSource).toContain("import AIChatPanel from './components/AIChatPanel';");
|
||||
expect(appSource).toContain("import AIPanelErrorBoundary from './components/ai/AIPanelErrorBoundary';");
|
||||
expect(aiPanelBoundarySource).toContain('class AIPanelErrorBoundary extends React.Component');
|
||||
expect(appSource).toContain('<AIPanelErrorBoundary');
|
||||
expect(appSource).toContain('key={aiPanelRenderNonce}');
|
||||
expect(appSource).toContain('AI 面板加载失败');
|
||||
expect(appSource).toContain('重新加载');
|
||||
expect(appSource).toContain('setAiPanelRenderNonce((current) => current + 1)');
|
||||
expect(appSource).toContain('<AIChatPanel width={aiPanelRenderWidth}');
|
||||
expect(appSource).not.toContain('const loadAIChatPanelModule = async (retryNonce: number) => {');
|
||||
});
|
||||
});
|
||||