diff --git a/assets/lottie/intro_foriday.json b/assets/lottie/intro_foriday.json new file mode 100644 index 000000000..e030941b4 --- /dev/null +++ b/assets/lottie/intro_foriday.json @@ -0,0 +1 @@ +{"nm":"Comp 1","ddd":0,"h":700,"w":700,"meta":{"g":"@lottiefiles/toolkit-js 0.22.1"},"layers":[{"ty":0,"nm":"Pre-comp 3","sr":1,"st":141.000005743048,"op":231.000009408823,"ip":141.000005743048,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[350,350,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[350,350,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"w":700,"h":700,"refId":"comp_0","ind":1},{"ty":0,"nm":"Pre-comp 3","sr":1,"st":71.0000028918893,"op":161.000006557664,"ip":71.0000028918893,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[350,350,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[350,350,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"w":700,"h":700,"refId":"comp_0","ind":2},{"ty":0,"nm":"Pre-comp 3","sr":1,"st":0,"op":90.0000036657751,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[350,350,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[350,350,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"w":700,"h":700,"refId":"comp_0","ind":3},{"ty":0,"nm":"bubble","sr":1,"st":0,"op":600.000024438501,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[350,350,0],"ix":1},"s":{"a":0,"k":[91,91,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[350,350,0],"ix":2},"r":{"a":0,"k":43,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":70,"ix":11}},"ef":[{"ty":0,"mn":"ADBE Gaussian Blur 2","nm":"Gaussian Blur","ix":1,"en":1,"ef":[{"ty":0,"mn":"ADBE Gaussian Blur 2-0001","nm":"Blurriness","ix":1,"v":{"a":0,"k":40.5,"ix":1}},{"ty":7,"mn":"ADBE Gaussian Blur 2-0002","nm":"Blur Dimensions","ix":2,"v":{"a":0,"k":1,"ix":2}},{"ty":7,"mn":"ADBE Gaussian Blur 2-0003","nm":"Repeat Edge Pixels","ix":3,"v":{"a":0,"k":0,"ix":3}}]}],"w":700,"h":700,"refId":"comp_4","ind":4},{"ty":0,"nm":"bubble","sr":1,"st":0,"op":600.000024438501,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[350,350,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[350,350,0],"ix":2},"r":{"a":0,"k":226,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":48,"ix":11}},"ef":[{"ty":0,"mn":"ADBE Tint","nm":"Tint","ix":1,"en":1,"ef":[{"ty":2,"mn":"ADBE Tint-0001","nm":"Map Black To","ix":1,"v":{"a":0,"k":[0,0,0,0],"ix":1}},{"ty":2,"mn":"ADBE Tint-0002","nm":"Map White To","ix":2,"v":{"a":0,"k":[1,0,0.7765],"ix":2}},{"ty":0,"mn":"ADBE Tint-0003","nm":"Amount to Tint","ix":3,"v":{"a":0,"k":100,"ix":3}},{"ty":6,"mn":"ADBE Tint-0004","nm":"","ix":4,"v":0}]}],"w":700,"h":700,"refId":"comp_4","ind":5},{"ty":0,"nm":"bubble","sr":1,"st":0,"op":600.000024438501,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[350,350,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[350,350,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"w":700,"h":700,"refId":"comp_4","ind":6}],"v":"5.5.5","fr":29.9700012207031,"op":231.000009408823,"ip":0,"assets":[{"nm":"","id":"comp_0","layers":[{"ty":0,"nm":"Pre-comp 2","sr":1,"st":13.0000005295009,"op":90.0000036657751,"ip":13.0000005295009,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[350,350,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[350,350,0],"ix":2},"r":{"a":0,"k":42,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":55,"ix":11}},"ef":[],"w":700,"h":700,"refId":"comp_1","ind":1},{"ty":0,"nm":"Pre-comp 2","sr":1,"st":0,"op":77.0000031362743,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[350,350,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[350,350,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"w":700,"h":700,"refId":"comp_1","ind":2}]},{"nm":"","id":"comp_1","layers":[{"ty":0,"nm":"Pre-comp 1","sr":1,"st":9.00000036657752,"op":77.0000031362743,"ip":9.00000036657752,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[350,350,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[350,350,0],"ix":2},"r":{"a":0,"k":69,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[{"ty":0,"mn":"ADBE Tint","nm":"Tint","ix":1,"en":1,"ef":[{"ty":2,"mn":"ADBE Tint-0001","nm":"Map Black To","ix":1,"v":{"a":0,"k":[0,0,0,0],"ix":1}},{"ty":2,"mn":"ADBE Tint-0002","nm":"Map White To","ix":2,"v":{"a":0,"k":[1,0.4078,0.6588],"ix":2}},{"ty":0,"mn":"ADBE Tint-0003","nm":"Amount to Tint","ix":3,"v":{"a":0,"k":100,"ix":3}},{"ty":6,"mn":"ADBE Tint-0004","nm":"","ix":4,"v":0}]}],"w":700,"h":700,"refId":"comp_2","ind":1},{"ty":0,"nm":"Pre-comp 1","sr":1,"st":0,"op":68.0000027696968,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[350,350,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[350,350,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"w":700,"h":700,"refId":"comp_2","ind":2}]},{"nm":"","id":"comp_2","layers":[{"ty":0,"nm":"Shape Layer 1 Comp 1","sr":1,"st":0,"op":47.0000019143492,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[350,350,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[350,350,0],"ix":2},"r":{"a":0,"k":275,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"w":700,"h":700,"refId":"comp_3","ind":1},{"ty":0,"nm":"Shape Layer 1 Comp 1","sr":1,"st":12.00000048877,"op":59.0000024031193,"ip":12.00000048877,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[350,350,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[350,350,0],"ix":2},"r":{"a":0,"k":227,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"w":700,"h":700,"refId":"comp_3","ind":2},{"ty":0,"nm":"Shape Layer 1 Comp 1","sr":1,"st":5.00000020365417,"op":52.0000021180034,"ip":5.00000020365417,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[350,350,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[350,350,0],"ix":2},"r":{"a":0,"k":140,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"w":700,"h":700,"refId":"comp_3","ind":3},{"ty":0,"nm":"Shape Layer 1 Comp 1","sr":1,"st":21.0000008553475,"op":68.0000027696968,"ip":21.0000008553475,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[350,350,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[350,350,0],"ix":2},"r":{"a":0,"k":48,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"w":700,"h":700,"refId":"comp_3","ind":4},{"ty":0,"nm":"Shape Layer 1 Comp 1","sr":1,"st":14.0000005702317,"op":61.0000024845809,"ip":14.0000005702317,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[350,350,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[350,350,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"w":700,"h":700,"refId":"comp_3","ind":5}]},{"nm":"","id":"comp_3","layers":[{"ty":4,"nm":"Shape Layer 1","sr":1,"st":0,"op":47.0000019143492,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-261,141,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0,0,100],"t":0},{"s":[100,100,100],"t":48.0000019550801}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[349,350,0],"t":0,"ti":[19.167,-15.333,0],"to":[-19.167,15.333,0]},{"s":[234,442,0],"t":48.0000019550801}],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100],"t":26},{"s":[0],"t":46.0000018736184}],"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Ellipse 1","ix":1,"cix":2,"np":3,"it":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[12,12],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":0,"ix":4},"w":{"a":0,"k":3,"ix":5},"c":{"a":0,"k":[0.9529,0.9608,0.9686],"ix":3}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,0.9529,0.9725],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[-261,141],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":1}]},{"nm":"","id":"comp_4","layers":[{"ty":4,"nm":"Shape Layer 1","sr":1,"st":0,"op":600.000024438501,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[350,350,0],"ix":2},"r":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":0},{"s":[720],"t":231.000009408823}],"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Ellipse 1","ix":1,"cix":2,"np":3,"it":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[400,400],"ix":2}},{"ty":"gs","bm":0,"hd":false,"mn":"ADBE Vector Graphic - G-Stroke","nm":"Gradient Stroke 1","e":{"a":0,"k":[-761.875,495.96],"ix":5},"g":{"p":3,"k":{"a":0,"k":[0,1,1,1,0.496,0.9882352941176471,0.39215686274509803,0.17647058823529413,0.993,1,0.35294117647058826,0.37254901960784315,0,0,0.496,1,0.993,1],"ix":8}},"t":2,"a":{"a":0,"k":0,"ix":7},"h":{"a":0,"k":0,"ix":6},"s":{"a":0,"k":[154.435,-126.834],"ix":4},"lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":9},"w":{"a":0,"k":1,"ix":10}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,0.9529,0.9725],"ix":4},"r":1,"o":{"a":0,"k":0,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[1,-1],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":1},{"ty":4,"nm":"Shape Layer 10","sr":1,"st":0,"op":600.000024438501,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[59.113,-37.893,0],"ix":1},"s":{"a":0,"k":[131.951,131.951,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[351,351,0],"ix":2},"r":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[382],"t":0},{"s":[742],"t":231.000009408823}],"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,75.084],[-2.326,11.481],[8.484,-20.679],[0,-18.258],[-75.111,0],[-2.638,0.152]],"o":[[0,-12.43],[4.439,-21.907],[-6.526,15.906],[0,75.111],[2.677,0],[-56.982,-4.209]],"v":[[-30.208,-36.759],[-26.642,-72.724],[-61.861,-83.451],[-71.985,-31.87],[64.015,104.13],[71.985,103.892]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":0,"ix":4},"w":{"a":0,"k":3,"ix":5},"c":{"a":0,"k":[0.9529,0.9608,0.9686],"ix":3}},{"ty":"gf","bm":0,"hd":false,"mn":"ADBE Vector Graphic - G-Fill","nm":"Gradient Fill 1","e":{"a":0,"k":[100,0],"ix":6},"g":{"p":3,"k":{"a":0,"k":[0,1,0.9529411764705882,0.9725490196078431,0.487,1,0.9529411764705882,0.9725490196078431,0.998,0.5176470588235295,0,1,0,1,0.487,1,0.998,0],"ix":9}},"t":2,"a":{"a":0,"k":0,"ix":8},"h":{"a":0,"k":0,"ix":7},"s":{"a":0,"k":[0,0],"ix":5},"r":1,"o":{"a":0,"k":100,"ix":10}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":2},{"ty":4,"nm":"Shape Layer 4","sr":1,"st":0,"op":600.000024438501,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-128,0,0],"ix":1},"s":{"a":0,"k":[65.625,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[222,350,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":54,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Ellipse 1","ix":1,"cix":2,"np":3,"it":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[16,16],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":0,"ix":4},"w":{"a":0,"k":3,"ix":5},"c":{"a":0,"k":[0.9529,0.9608,0.9686],"ix":3}},{"ty":"gf","bm":0,"hd":false,"mn":"ADBE Vector Graphic - G-Fill","nm":"Gradient Fill 1","e":{"a":0,"k":[100,0],"ix":6},"g":{"p":3,"k":{"a":0,"k":[0.002,1,1,1,0.488,1,1,1,0.998,1,1,1,0.002,1,0.488,0.5,0.998,0],"ix":9}},"t":2,"a":{"a":0,"k":0,"ix":8},"h":{"a":0,"k":0,"ix":7},"s":{"a":0,"k":[0,0],"ix":5},"r":1,"o":{"a":0,"k":100,"ix":10}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[-128,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":3},{"ty":4,"nm":"Shape Layer 8","sr":1,"st":0,"op":600.000024438501,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"td":1,"ao":0,"ks":{"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[350,350,0],"ix":2},"r":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[0],"t":0},{"s":[360],"t":59.0000024031193}],"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Ellipse 1","ix":1,"cix":2,"np":3,"it":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[400,400],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":0,"ix":4},"w":{"a":0,"k":3,"ix":5},"c":{"a":0,"k":[0.9529,0.9608,0.9686],"ix":3}},{"ty":"gf","bm":0,"hd":false,"mn":"ADBE Vector Graphic - G-Fill","nm":"Gradient Fill 1","e":{"a":0,"k":[202.396,-0.369],"ix":6},"g":{"p":3,"k":{"a":0,"k":[0,1,1,1,0.487,1,1,1,0.998,1,1,1,0,0.44,0.487,0.22,0.998,0],"ix":9}},"t":2,"a":{"a":0,"k":0,"ix":8},"h":{"a":0,"k":0,"ix":7},"s":{"a":0,"k":[0,0],"ix":5},"r":1,"o":{"a":0,"k":100,"ix":10}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[1,-1],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":4},{"ty":4,"nm":"Shape Layer 7","sr":1,"st":0,"op":600.000024438501,"ip":0,"hd":false,"ddd":0,"bm":0,"tt":1,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-118,46,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[33.333,100,100],"t":0},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[100,100,100],"t":29},{"s":[33.333,100,100],"t":59.0000024031193}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[106,342,0],"t":0,"ti":[-81.667,0,0],"to":[81.667,0,0]},{"s":[596,342,0],"t":59.0000024031193}],"ix":2},"r":{"a":0,"k":38,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":39,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Rectangle 1","ix":1,"cix":2,"np":3,"it":[{"ty":"rc","bm":0,"hd":false,"mn":"ADBE Vector Shape - Rect","nm":"Rectangle Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"s":{"a":0,"k":[72,596],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":0,"ix":4},"w":{"a":0,"k":3,"ix":5},"c":{"a":0,"k":[0.9529,0.9608,0.9686],"ix":3}},{"ty":"gf","bm":0,"hd":false,"mn":"ADBE Vector Graphic - G-Fill","nm":"Gradient Fill 1","e":{"a":0,"k":[-2.662,190],"ix":6},"g":{"p":3,"k":{"a":0,"k":[0,1,1,1,0.487,1,1,1,0.998,1,1,1,0,0.44,0.487,0.22,0.998,0],"ix":9}},"t":2,"a":{"a":0,"k":0,"ix":8},"h":{"a":0,"k":0,"ix":7},"s":{"a":0,"k":[1.369,-19],"ix":5},"r":1,"o":{"a":0,"k":100,"ix":10}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[-118,46],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":5},{"ty":4,"nm":"Shape Layer 6","sr":1,"st":0,"op":600.000024438501,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[146.132,146.132,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[350,350,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Ellipse 1","ix":1,"cix":2,"np":3,"it":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[142,142],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":0,"ix":4},"w":{"a":0,"k":3,"ix":5},"c":{"a":0,"k":[0.9529,0.9608,0.9686],"ix":3}},{"ty":"gf","bm":0,"hd":false,"mn":"ADBE Vector Graphic - G-Fill","nm":"Gradient Fill 1","e":{"a":0,"k":[60.839,0],"ix":6},"g":{"p":3,"k":{"a":0,"k":[0,0.8549019607843137,0.8549019607843137,0.8549019607843137,0.487,0.9254901960784314,0.9254901960784314,0.9254901960784314,0.998,1,1,1,0,0.44,0.487,0.22,0.998,0],"ix":9}},"t":2,"a":{"a":0,"k":0,"ix":8},"h":{"a":0,"k":0,"ix":7},"s":{"a":0,"k":[0,0],"ix":5},"r":1,"o":{"a":0,"k":100,"ix":10}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[181.304,181.304],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,-3],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":6},{"ty":4,"nm":"Shape Layer 5","sr":1,"st":0,"op":600.000024438501,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[124.841,124.841,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[350,458,0],"ix":2},"r":{"a":0,"k":180,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":10,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 2","ix":1,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[-36.43,1.19],[0,0],[24.53,-18.36]],"o":[[25.85,-22.17],[0,0],[-32.74,1.05],[0,0]],"v":[[-100.64,-30.635],[-5.11,-67.775],[-5.11,37.125],[-92.55,67.775]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":0,"ix":4},"w":{"a":0,"k":3,"ix":5},"c":{"a":0,"k":[0.9529,0.9608,0.9686],"ix":3}},{"ty":"gf","bm":0,"hd":false,"mn":"ADBE Vector Graphic - G-Fill","nm":"Gradient Fill 1","e":{"a":0,"k":[0,105.643],"ix":6},"g":{"p":3,"k":{"a":0,"k":[0,1,1,1,0.487,1,1,1,0.998,1,1,1,0,1,0.487,0.5,0.998,0],"ix":9}},"t":1,"a":{"a":0,"k":0},"h":{"a":0,"k":0},"s":{"a":0,"k":[-1,-118.622],"ix":5},"r":1,"o":{"a":0,"k":100,"ix":10}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]},{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":2,"cix":2,"np":3,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[-25.85,-22.17],[0,0],[32.74,1.05],[0,0]],"o":[[0,0],[-24.53,-18.36],[0,0],[36.43,1.19]],"v":[[100.64,-30.635],[92.55,67.775],[5.11,37.125],[5.11,-67.775]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":0,"ix":4},"w":{"a":0,"k":3,"ix":5},"c":{"a":0,"k":[0.9529,0.9608,0.9686],"ix":3}},{"ty":"gf","bm":0,"hd":false,"mn":"ADBE Vector Graphic - G-Fill","nm":"Gradient Fill 1","e":{"a":0,"k":[0,105.643],"ix":6},"g":{"p":3,"k":{"a":0,"k":[0,1,1,1,0.487,1,1,1,0.998,1,1,1,0,1,0.487,0.5,0.998,0],"ix":9}},"t":1,"a":{"a":0,"k":0},"h":{"a":0,"k":0},"s":{"a":0,"k":[-1,-118.622],"ix":5},"r":1,"o":{"a":0,"k":100,"ix":10}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":7},{"ty":4,"nm":"Shape Layer 3","sr":1,"st":0,"op":600.000024438501,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[343,317,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Ellipse 1","ix":1,"cix":2,"np":3,"it":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[177,116],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":0,"ix":4},"w":{"a":0,"k":3,"ix":5},"c":{"a":0,"k":[0.9529,0.9608,0.9686],"ix":3}},{"ty":"gf","bm":0,"hd":false,"mn":"ADBE Vector Graphic - G-Fill","nm":"Gradient Fill 1","e":{"a":0,"k":[100,0],"ix":6},"g":{"p":3,"k":{"a":0,"k":[0.002,0.9529411764705882,0.9529411764705882,0.9529411764705882,0.488,0.9764705882352941,0.9764705882352941,0.9764705882352941,0.998,1,1,1,0.002,1,0.488,0.5,0.998,0],"ix":9}},"t":2,"a":{"a":0,"k":0,"ix":8},"h":{"a":0,"k":0,"ix":7},"s":{"a":0,"k":[-10,-146],"ix":5},"r":1,"o":{"a":0,"k":100,"ix":10}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[7.5,-104],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":8},{"ty":4,"nm":"Shape Layer 9","sr":1,"st":0,"op":600.000024438501,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[350,350,0],"ix":2},"r":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[190],"t":0},{"s":[550],"t":230.000009368092}],"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Ellipse 1","ix":1,"cix":2,"np":3,"it":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[400,400],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":0,"ix":4},"w":{"a":0,"k":3,"ix":5},"c":{"a":0,"k":[0.9529,0.9608,0.9686],"ix":3}},{"ty":"gf","bm":0,"hd":false,"mn":"ADBE Vector Graphic - G-Fill","nm":"Gradient Fill 1","e":{"a":0,"k":[29.602,-44.441],"ix":6},"g":{"p":3,"k":{"a":0,"k":[0.002,0.5450980392156862,0.9882352941176471,0.9725490196078431,0.488,0.5686274509803921,0.7294117647058823,0.7176470588235294,0.998,1,0,0,0.002,1,0.488,0.5,0.998,0],"ix":9}},"t":2,"a":{"a":0,"k":0,"ix":8},"h":{"a":0,"k":0,"ix":7},"s":{"a":0,"k":[-131.837,65.407],"ix":5},"r":1,"o":{"a":0,"k":100,"ix":10}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[1,-1],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":9},{"ty":4,"nm":"Shape Layer 2","sr":1,"st":0,"op":600.000024438501,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[350,350,0],"ix":2},"r":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[190],"t":0},{"s":[1270],"t":230.000009368092}],"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Ellipse 1","ix":1,"cix":2,"np":3,"it":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[400,400],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":0,"ix":4},"w":{"a":0,"k":3,"ix":5},"c":{"a":0,"k":[0.9529,0.9608,0.9686],"ix":3}},{"ty":"gf","bm":0,"hd":false,"mn":"ADBE Vector Graphic - G-Fill","nm":"Gradient Fill 1","e":{"a":0,"k":[-48.365,-83.495],"ix":6},"g":{"p":3,"k":{"a":0,"k":[0.002,0.9882352941176471,0.7333333333333333,0.984313725490196,0.488,0.5254901960784314,0.9686274509803922,0.9294117647058824,0.998,1,0,0.8470588235294118,0.002,1,0.488,1,0.998,0],"ix":9}},"t":1,"a":{"a":0,"k":0},"h":{"a":0,"k":0},"s":{"a":0,"k":[-134.449,-317.964],"ix":5},"r":1,"o":{"a":0,"k":100,"ix":10}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[1,-1],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":10}]}]} \ No newline at end of file diff --git a/lib/core/translations/language/en_us.dart b/lib/core/translations/language/en_us.dart index 51d9bd764..7dca392ef 100644 --- a/lib/core/translations/language/en_us.dart +++ b/lib/core/translations/language/en_us.dart @@ -436,6 +436,8 @@ const Map enUS = { 'deleteAccount': 'Delete Account', 'deleteAccountTips': 'Are you sure you want to permanently delete your account? This action cannot be undone.', + 'clean': 'Clear Cache', // 清除缓存 + 'cleanTips': 'Are you sure you want to clear personal cache?', // 确定要清除个人缓存吗? //Profile 错误信息 'networkConnectionFailed': diff --git a/lib/core/translations/language/zh_cn.dart b/lib/core/translations/language/zh_cn.dart index 6bd1701d6..d73546416 100644 --- a/lib/core/translations/language/zh_cn.dart +++ b/lib/core/translations/language/zh_cn.dart @@ -411,6 +411,8 @@ const Map zhCN = { "logoutTips": "确定要退出当前账号吗?", 'deleteAccount': '注销账号', 'deleteAccountTips': '确定要永久删除您的账号吗?此操作不可撤销', + 'clean': '清除缓存', + 'cleanTips': '确定要清除个人缓存吗?', // Profile 错误信息 'networkConnectionFailed': '网络连接失败,请检查网络设置后重试', diff --git a/lib/core/widgets/intro_overlay_builder.dart b/lib/core/widgets/intro_overlay_builder.dart index aab3dae94..bab8412b0 100644 --- a/lib/core/widgets/intro_overlay_builder.dart +++ b/lib/core/widgets/intro_overlay_builder.dart @@ -1,8 +1,46 @@ import 'package:flutter/material.dart'; import 'package:flutter_intro/flutter_intro.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:lottie/lottie.dart'; class IntroOverlayBuilder { + static Widget homeBuilder( + StepWidgetParams params, + bool isWakeup, { + Function? onNext, + }) { + return Column( + children: [ + isWakeup + ? _AnimatedLottieWidget() + : SizedBox( + width: 200.w, + height: 200.w, + child: Lottie.asset('assets/lottie/intro_foriday.json'), + ), + Padding( + padding: EdgeInsets.only(bottom: 20.w), + child: Text( + isWakeup ? '我在呢~' : '对着麦克风说:“小言小言”', + style: TextStyle( + fontSize: 15.sp, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + if (onNext != null) + _button( + '下一步', + () { + params.onNext!(); + onNext(); + }, + ), + ], + ); + } + static Widget aiBuilder( StepWidgetParams params, String text, { @@ -70,3 +108,61 @@ class IntroOverlayBuilder { ); } } + +// 新增的动画Widget +class _AnimatedLottieWidget extends StatefulWidget { + @override + _AnimatedLottieWidgetState createState() => _AnimatedLottieWidgetState(); +} + +class _AnimatedLottieWidgetState extends State<_AnimatedLottieWidget> + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + + // 创建动画控制器,持续时间1.5秒 + _animationController = AnimationController( + duration: const Duration(milliseconds: 1000), + vsync: this, + ); + + // 创建缩放动画,从0.8到1.2再回到1.0 + _scaleAnimation = Tween( + begin: 0.8, + end: 1.2, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.elasticInOut, + )); + + // 启动动画并设置为循环 + _animationController.repeat(reverse: true); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _scaleAnimation, + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: SizedBox( + width: 200.w, + height: 200.w, + child: Lottie.asset('assets/lottie/intro_foriday.json'), + ), + ); + }, + ); + } +} diff --git a/lib/data/models/channel_app_model.dart b/lib/data/models/channel_app_model.dart index 7fc3dcabf..dac792f28 100644 --- a/lib/data/models/channel_app_model.dart +++ b/lib/data/models/channel_app_model.dart @@ -177,6 +177,30 @@ enum UserChannel { ); } + // 从字符串名称创建枚举 + static UserChannel fromName(String name) { + switch (name.toUpperCase()) { + case 'CHINA_HUAWEI': + return UserChannel.CHINA_HUAWEI; + case 'CHINA_XIAOMI': + return UserChannel.CHINA_XIAOMI; + case 'CHINA_OPPO': + return UserChannel.CHINA_OPPO; + case 'CHINA_VIVO': + return UserChannel.CHINA_VIVO; + case 'CHINA_HONOR': + return UserChannel.CHINA_HONOR; + case 'OFFICIAL': + return UserChannel.OFFICIAL; + case 'GLOBAL_APPLE': + return UserChannel.GLOBAL_APPLE; + case 'GLOBAL_GOOGLE': + return UserChannel.GLOBAL_GOOGLE; + default: + return UserChannel.UNKNOWN; + } + } + // JSON 序列化时返回数字值 int toJson() => value; diff --git a/lib/data/models/message_model.dart b/lib/data/models/message_model.dart index aef99cbc9..09744a939 100644 --- a/lib/data/models/message_model.dart +++ b/lib/data/models/message_model.dart @@ -1,3 +1,7 @@ +import 'dart:io'; + +import 'package:path_provider/path_provider.dart'; + class Message { final bool isUser; //标识信息是否来自用户还是AI final String text; //信息文本内容 diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index ae0bf3ea5..f181dcc77 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -9,6 +9,7 @@ import 'package:get_storage/get_storage.dart'; import 'dart:convert'; import 'package:agent_service/agent_service.dart'; import 'bluetooth_manager.dart'; +import 'language_manager.dart'; // 重新导出BLE连接状态常量,让外部可以通过BleManager访问 export 'package:ble_service/ble_service.dart' show BleConnectionState; @@ -556,26 +557,44 @@ class BleManager extends GetxService { // 根据APP语言动态配置语音识别语言 List supportedLanguages = []; try { - // 使用 GetX 获取 APP 当前语言环境 (Locale) - final appLocale = Get.locale; - - if (appLocale != null) { - final lang = appLocale.languageCode; // e.g., 'en' - final country = appLocale.countryCode; // e.g., 'US' - - // 构造 Azure SDK 需要的格式 (e.g., 'en-US') - if (country != null && country.isNotEmpty) { - supportedLanguages.add('$lang-$country'); - } else { - supportedLanguages.add(lang); // fallback only language code + final storage = GetStorage(); + final languageManager = Get.find(); + + // 从存储中读取用户设置的语言 + String? savedLanguage = storage.read('language'); + + if (savedLanguage != null && savedLanguage != 'system') { + // 用户设置了特定语言 + final parts = savedLanguage.split('_'); + if (parts.length == 2) { + final asrCode = languageManager.getAsrCodeByShortCode(parts[0]); + if (asrCode != null) { + supportedLanguages.add(asrCode); + } else { + // 直接使用语言-国家格式 + supportedLanguages.add('${parts[0]}-${parts[1]}'); + } + } + } else { + // 跟随系统语言或未设置 + final appLocale = Get.locale; + if (appLocale != null) { + final lang = appLocale.languageCode; + final country = appLocale.countryCode; + + // 尝试从语言管理器获取对应的ASR代码 + final asrCode = languageManager.getAsrCodeByShortCode(lang); + if (asrCode != null) { + supportedLanguages.add(asrCode); + } else if (country != null && country.isNotEmpty) { + // 构造 Azure SDK 需要的格式 (e.g., 'en-US') + supportedLanguages.add('$lang-$country'); + } else { + supportedLanguages.add('en-US'); // 默认英语 + } } } - // 添加 en-US 作为备选 - // if (!supportedLanguages.contains('en-US')) { - // supportedLanguages.add('en-US'); - // } - // 最终兜底 if (supportedLanguages.isEmpty) { supportedLanguages.add('en-US'); diff --git a/lib/data/services/novice_guide_service.dart b/lib/data/services/novice_guide_service.dart index 12faf1a2c..d32be2368 100644 --- a/lib/data/services/novice_guide_service.dart +++ b/lib/data/services/novice_guide_service.dart @@ -1,3 +1,6 @@ +import 'dart:async'; + +import 'package:ble_service/ble_service.dart'; import 'package:flutter_intro/flutter_intro.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; @@ -5,9 +8,14 @@ import 'package:get_storage/get_storage.dart'; class NoviceGuideService extends GetxService { final GetStorage _storage = GetStorage(); + final BleService _bleService = BleService(); + StreamSubscription? _statusSubscription; + bool isHomeGuide = true; bool isAIGuide = true; + RxBool isWakeup = false.obs; + int aiStep = 0; List aiStepsList = [ @@ -56,6 +64,12 @@ class NoviceGuideService extends GetxService { // isAIGuide = _storage.read('ai_guide') ?? false; } + @override + void onClose() { + closeWakeupListen(); + super.onClose(); + } + void startHomeGuide() async { if (!isHomeGuide) { await Future.delayed(const Duration(milliseconds: 200)); @@ -73,4 +87,18 @@ class NoviceGuideService extends GetxService { Intro.of(Get.context!).start(group: 'ai'); } } + + void startWakeupListen() { + _bleService.setWakeup(false); + _statusSubscription = _bleService.onStatusUpdated.listen((event) { + if (event is Map && event['type'] == 'wakeup') { + isWakeup.value = true; + } + }); + } + + void closeWakeupListen() { + _bleService.setWakeup(true); + _statusSubscription?.cancel(); + } } diff --git a/lib/data/services/version_update_service.dart b/lib/data/services/version_update_service.dart index f624ae7aa..4c408e107 100644 --- a/lib/data/services/version_update_service.dart +++ b/lib/data/services/version_update_service.dart @@ -5,6 +5,7 @@ import 'package:package_info_plus/package_info_plus.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:logger/logger.dart'; import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import '../models/channel_app_model.dart'; import 'network/api.dart'; import 'dart:math' as math; @@ -13,15 +14,62 @@ import 'dart:math' as math; class VersionUpdateService extends GetxService { static VersionUpdateService get to => Get.find(); - final Logger _logger = Logger(); + // 静态应用信息(内存缓存) + static String? _appVersion = ""; //应用版本号 + static String? _buildNumber = ""; //构建号 + static String? _platform = ""; //平台类型 + static String? _deviceBrand = ""; //设备品牌 + static UserChannel? _channel; //渠道 + static String? _latestVersion = ""; //最新版本号+构建号 + static bool? _hasUpdate = false; //是否有更新 + static bool? _isForceUpdate = false; //是否强制更新 + static String? _updateUrl = ""; //更新链接 + static bool? _isShowTourists = false; //是否显示游客登录 + static DateTime? _lastCheckTime; //上次检查时间 + static const int _cacheIntervalMinutes = 10; // 可配置化 + + // 添加 getter 方法来访问缓存的静态数据 + /// 获取缓存的应用版本 + String get cachedAppVersion => _appVersion ?? ''; + + /// 获取缓存的构建号 + String get cachedBuildNumber => _buildNumber ?? ''; + + /// 获取缓存的平台信息 + String get cachedPlatform => _platform ?? ''; + + /// 获取缓存的设备品牌 + String get cachedDeviceBrand => _deviceBrand ?? ''; + + /// 获取缓存的渠道信息 + UserChannel get cachedChannel => _channel ?? UserChannel.UNKNOWN; + + /// 获取缓存的最新版本 + String get cachedLatestVersion => _latestVersion ?? ''; + + /// 获取缓存的是否有更新 + bool get cachedHasUpdate => _hasUpdate ?? false; + + /// 获取缓存的是否强制更新 + bool get cachedIsForceUpdate => _isForceUpdate ?? false; + + /// 获取缓存的更新地址 + String get cachedUpdateUrl => _updateUrl ?? ''; + + /// 获取缓存的游客登录状态 + bool get cachedShowTourists => _isShowTourists ?? false; - // 游客登录显示状态 - final isShowTourists = false.obs; + /// 获取上次检查时间 + DateTime? get lastCheckTime => _lastCheckTime; + + final Logger _logger = Logger(); /// 获取当前应用版本信息 Future getCurrentVersion() async { try { final packageInfo = await PackageInfo.fromPlatform(); + _appVersion = packageInfo.version; + _buildNumber = packageInfo.buildNumber; return '${packageInfo.version}+${packageInfo.buildNumber}'; } catch (e) { _logger.e('获取当前版本信息失败: $e'); @@ -31,14 +79,6 @@ class VersionUpdateService extends GetxService { /// 获取当前平台信息 Future getCurrentPlatformInfo() async { - if (kIsWeb) { - return const PlatformInfo( - platform: 'web', - storeType: AppStoreType.official, - channel: UserChannel.OFFICIAL, - ); - } - if (Platform.isIOS) { return const PlatformInfo( platform: 'ios', @@ -66,10 +106,14 @@ class VersionUpdateService extends GetxService { final androidInfo = await deviceInfo.androidInfo; final brand = androidInfo.brand.toLowerCase(); - return const PlatformInfo( + _deviceBrand = brand; + String channelFromEnv = dotenv.env['Channel'] ?? 'OFFICIAL'; + _channel = UserChannel.fromName(channelFromEnv); + + return PlatformInfo( platform: 'android', storeType: AppStoreType.official, - channel: UserChannel.OFFICIAL, + channel: _channel ?? UserChannel.OFFICIAL, ); // switch (brand) { @@ -121,10 +165,27 @@ class VersionUpdateService extends GetxService { /// 检查版本更新 Future checkForUpdate() async { + if (_lastCheckTime != null && + DateTime.now().difference(_lastCheckTime!).inMinutes < + _cacheIntervalMinutes) { + return VersionUpdateInfo( + hasUpdate: _hasUpdate ?? false, + currentVersion: _appVersion ?? '1.0.0', + latestVersion: (_latestVersion ?? '1.0.0').split('+')[0], + updateType: _hasUpdate! + ? (_isForceUpdate! ? UpdateType.force : UpdateType.optional) + : UpdateType.none, + ); + } + try { final currentVersion = await getCurrentVersion(); final platformInfo = await getCurrentPlatformInfo(); + _platform = platformInfo.platform; + _channel = platformInfo.channel; + _lastCheckTime = DateTime.now(); + // 调用后端接口获取最新版本信息 final response = await Api.getChannelApp({'channel': platformInfo.channel.value}); @@ -144,9 +205,11 @@ class VersionUpdateService extends GetxService { // 直接从response中获取app信息 final appInfo = AppInfo.fromJson(response['app']); _logger.d('获取到的最新版本信息: ${appInfo.toJson()}'); + _latestVersion = appInfo.version; + _updateUrl = appInfo.address; // 设置游客登录显示状态 - isShowTourists.value = appInfo.tourists; + _isShowTourists = appInfo.tourists; _logger.d('从服务器获取游客登录配置: ${appInfo.tourists}'); // if (channelAppResponse.code != 0 || channelAppResponse.data == null) { @@ -160,18 +223,17 @@ class VersionUpdateService extends GetxService { // } // final appInfo = channelAppResponse.data!.app; - final hasUpdate = _compareVersions(currentVersion, appInfo.version); + _hasUpdate = _compareVersions(currentVersion, appInfo.version); // 判断更新类型:如果主版本号不同则强制更新 UpdateType updateType = UpdateType.none; - if (hasUpdate) { - final isForceUpdate = - _shouldForceUpdate(currentVersion, appInfo.version); - updateType = isForceUpdate ? UpdateType.force : UpdateType.optional; + if (_hasUpdate!) { + _isForceUpdate = _shouldForceUpdate(currentVersion, appInfo.version); + updateType = _isForceUpdate! ? UpdateType.force : UpdateType.optional; } return VersionUpdateInfo( - hasUpdate: hasUpdate, + hasUpdate: _hasUpdate!, currentVersion: currentVersion.split('+')[0], latestVersion: appInfo.version.split('+')[0], appInfo: appInfo, @@ -180,7 +242,7 @@ class VersionUpdateService extends GetxService { } catch (e) { _logger.e('检查版本更新失败: $e'); // 发生错误时,保持默认值 false - isShowTourists.value = false; + _isShowTourists = false; final currentVersion = await getCurrentVersion(); return VersionUpdateInfo( hasUpdate: false, @@ -270,84 +332,16 @@ class VersionUpdateService extends GetxService { return false; } - //final platformInfo = await getCurrentPlatformInfo(); - try { - // switch (platformInfo.storeType) { - // case AppStoreType.appStore: - // return await _openAppStore(); - // case AppStoreType.googlePlay: - // return await _openGooglePlay(); - // case AppStoreType.huawei: - // return await _openHuaweiStore(); - // case AppStoreType.xiaomi: - // return await _openXiaomiStore(); - // case AppStoreType.oppo: - // return await _openOppoStore(); - // case AppStoreType.vivo: - // return await _openVivoStore(); - // case AppStoreType.honor: - // return await _openHonorStore(); - // case AppStoreType.official: - // return await _openOfficialDownload(updateInfo.appInfo!.address!); - // default: - return await _openOfficialDownload(updateInfo.appInfo!.address!); - // } + return await _openDownloadUrl(updateInfo.appInfo!.address!); } catch (e) { _logger.e('执行更新失败: $e'); return false; } } - /// 打开iOS App Store - Future _openAppStore() async { - // 这里需要替换为实际的App Store ID - const appStoreUrl = 'https://apps.apple.com/app/id你的应用ID'; - return await _launchUrl(appStoreUrl); - } - - /// 打开Google Play - Future _openGooglePlay() async { - // 这里需要替换为实际的包名 - const packageName = 'com.yourcompany.deepvoice'; - const playStoreUrl = - 'https://play.google.com/store/apps/details?id=$packageName'; - return await _launchUrl(playStoreUrl); - } - - /// 打开华为应用市场 - Future _openHuaweiStore() async { - const huaweiStoreUrl = 'appmarket://details?id=com.yourcompany.deepvoice'; - return await _launchUrl(huaweiStoreUrl); - } - - /// 打开小米应用商店 - Future _openXiaomiStore() async { - const xiaomiStoreUrl = 'mimarket://details?id=com.yourcompany.deepvoice'; - return await _launchUrl(xiaomiStoreUrl); - } - - /// 打开OPPO软件商店 - Future _openOppoStore() async { - const oppoStoreUrl = - 'oppomarket://details?packagename=com.yourcompany.deepvoice'; - return await _launchUrl(oppoStoreUrl); - } - - /// 打开vivo应用商店 - Future _openVivoStore() async { - const vivoStoreUrl = 'vivomarket://details?id=com.yourcompany.deepvoice'; - return await _launchUrl(vivoStoreUrl); - } - - /// 打开荣耀应用市场 - Future _openHonorStore() async { - const honorStoreUrl = 'honormarket://details?id=com.yourcompany.deepvoice'; - return await _launchUrl(honorStoreUrl); - } - - /// 打开官网下载 - Future _openOfficialDownload(String downloadUrl) async { + /// 打开下载链接 + Future _openDownloadUrl(String downloadUrl) async { return await _launchUrl(downloadUrl); } diff --git a/lib/main.dart b/lib/main.dart index fd95d2863..6a976e86e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,8 +10,6 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'core/bindings/initial_binding.dart'; import 'routes/app_pages.dart'; import 'routes/app_routes.dart'; -import 'dart:io'; -import 'package:window_size/window_size.dart'; import 'core/translations/app_translations.dart'; import 'core/themes/app_theme.dart'; import 'core/themes/theme_service.dart'; @@ -32,28 +30,6 @@ void main() async { // 初始化登录服务(包含Firebase初始化) LoginService(); - // 设置窗口大小(仅在 macOS 下生效) - if (Platform.isMacOS) { - const double width = 375.0; - const double height = 812.0; - - setWindowTitle('Voitrans'); - setWindowMinSize(const Size(width, height)); - setWindowMaxSize(const Size(width, height)); - - // 居中显示窗口 - final screen = await getCurrentScreen(); - if (screen != null) { - final screenFrame = screen.frame; - final windowFrame = Rect.fromCenter( - center: screenFrame.center, - width: width, - height: height, - ); - setWindowFrame(windowFrame); - } - } - // 运行应用 runApp(const MainApp()); } diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index ab93538d3..6711874a5 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'dart:ffi'; import 'dart:io'; import 'package:amap_service/amap_service.dart'; -import 'package:voitrans/data/services/qq_musoc.dart'; import '../../../data/services/novice_guide_service.dart'; import '/core/utils/upload_oss.dart'; import '/data/models/user_Info.dart'; @@ -42,7 +41,10 @@ class AgentController extends GetxController { final ScrollController scrollController = ScrollController(); final noviceGuideService = Get.find(); - + // 确保服务已初始化 + // ========== 依赖服务 ========== + final BluetoothManager _bluetoothManager = Get.find(); + final BleManager _bleManager = Get.find(); //音乐播放管理 final _musiceManager = Get.find(); final _qqmusicManager = Get.find(); @@ -122,7 +124,7 @@ class AgentController extends GetxController { String _currentResponseId = ''; final TAG = 'AgentController'; - + var AppDir = "".obs; // 会话ID final String sessionId = 'default_agent'; @@ -149,6 +151,7 @@ class AgentController extends GetxController { @override void onInit() { super.onInit(); + _initAppDir(); _loadTtsPreference(); //加载TTS偏好设置 _setupScrollListener(); //设置滚动监听器 _loadChatHistory(); //加载聊天历史记录 @@ -162,6 +165,10 @@ class AgentController extends GetxController { // _qqmusicManager.syncCurrentPlayInfo(); } + Future _initAppDir() async { + AppDir.value = (await getApplicationDocumentsDirectory()).path; + } + // 初始化音乐播放状态 Future _initMusicStatus() async { try { @@ -289,7 +296,7 @@ class AgentController extends GetxController { page: 1, //从第一页开始 pageSize: pageSize, //每页20条记录 ); - + Logger.i(TAG, '加载聊天历史记录: $result'); if (result.messages.isNotEmpty) { // 转换并添加消息到列表 final historyMessages = _convertChatMessagesToMessages(result.messages); @@ -301,7 +308,8 @@ class AgentController extends GetxController { // 更新分页状态 currentPage.value = 1; hasMoreMessages.value = result.totalPages > 1; - + print( + 'liwei-------------读取历史记录 ${AppDir.value}${historyMessages[0].imagePath}'); Logger.i(TAG, '成功加载 ${historyMessages.length} 条聊天记录,总页数: ${result.totalPages}'); _logMessageDetails(historyMessages); @@ -419,7 +427,7 @@ class AgentController extends GetxController { 'meta: ${msg.meta != null ? "有" : "无"}' '}'); } - Logger.d(TAG, '===== 历史消息详情结束 ====='); + Logger.d(TAG, '===== 历史消息详情结束 ==lishi==='); } // 设置滚动监听器 @@ -561,16 +569,16 @@ class AgentController extends GetxController { break; case AgentServiceEventType.ttsStarted: - // isSpeaking.value = true; - // Logger.i(TAG, 'TTS开始播放'); + isSpeaking.value = true; + print('TTS开始播放'); break; case AgentServiceEventType.ttsCompleted: case AgentServiceEventType.ttsStopped: case AgentServiceEventType.ttsCanceled: - // isSpeaking.value = false; + isSpeaking.value = false; - // Logger.i(TAG, 'TTS停止播放, $event.type'); + Logger.i(TAG, 'TTS停止播放, $event.type'); break; case AgentServiceEventType.playbackStarted: @@ -909,10 +917,14 @@ class AgentController extends GetxController { } // 复制图片到应用永久存储目录 - final permanentImagePath = await _copyImageToPermanentStorage(imagePath); + // 复制图片到应用永久存储目录 + final imagePathResult = await _copyImageToPermanentStorage(imagePath); + final permanentImagePath = imagePathResult['fullPath']!; + final relativeImagePath = imagePathResult['relativePath']!; var ImageUrl = await UploadOss.upload( - filepath: imagePath, rootDir: 'User/${User.instance.uid}/ChatImages'); - Logger.i(TAG, '图片上传: $imagePath URL: $ImageUrl'); + filepath: permanentImagePath, + rootDir: 'User/${User.instance.uid}/ChatImages'); + Logger.i(TAG, '图片上传: $permanentImagePath URL: $ImageUrl'); // 显示的文本,如果没有提供则使用默认值 final displayText = text?.isNotEmpty == true ? text! : '[图片]'; @@ -921,7 +933,7 @@ class AgentController extends GetxController { isUser: true, text: displayText, hasImage: true, - imagePath: permanentImagePath, + imagePath: relativeImagePath, ); messages.add(message); @@ -941,7 +953,7 @@ class AgentController extends GetxController { // 调用Agent Service处理图片,考虑TTS状态 await AgentService.processImageInput( - permanentImagePath, + relativeImagePath, ImageUrl, text: text ?? '', speakResponse: isTtsEnabled.value, @@ -954,12 +966,16 @@ class AgentController extends GetxController { } // 将图片复制到应用的永久存储目录 - Future _copyImageToPermanentStorage(String sourcePath) async { + Future> _copyImageToPermanentStorage( + String sourcePath) async { try { final File sourceFile = File(sourcePath); if (!await sourceFile.exists()) { Logger.e(TAG, '源图片文件不存在: $sourcePath'); - return sourcePath; // 如果源文件不存在,返回原路径 + return { + 'fullPath': sourcePath, + 'relativePath': sourcePath + }; // 如果源文件不存在,返回原路径 } // 创建应用文档目录下的chatImages文件夹 @@ -973,15 +989,16 @@ class AgentController extends GetxController { final fileName = '${DateTime.now().millisecondsSinceEpoch}_${sourcePath.split('/').last}'; final targetPath = '${imagesDir.path}/$fileName'; + final relativePath = '/chatImages/$fileName'; // 复制文件 await sourceFile.copy(targetPath); Logger.i(TAG, '图片已复制到永久存储: $targetPath'); - return targetPath; + return {'fullPath': targetPath, 'relativePath': relativePath}; } catch (e) { Logger.e(TAG, '复制图片到永久存储失败: $e'); - return sourcePath; // 出错时返回原路径 + return {'fullPath': sourcePath, 'relativePath': sourcePath}; // 出错时返回原路径 } } @@ -1046,6 +1063,11 @@ class AgentController extends GetxController { try { await AgentService.recognizeCallback(); + if (_bluetoothManager.currentDeviceRx.value != null) { + await AgentService.restoreOriginalAudioState(); + } else { + await AgentService.disableBluetoothAudio(); + } } catch (e) { Logger.e(TAG, '启动识别器失败: $e'); } diff --git a/lib/modules/agent/views/message_bubble.dart b/lib/modules/agent/views/message_bubble.dart index e95f14a64..2cec99326 100644 --- a/lib/modules/agent/views/message_bubble.dart +++ b/lib/modules/agent/views/message_bubble.dart @@ -49,28 +49,20 @@ class MessageBubble extends GetView { isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ Container( - padding: hasImage - ? (message == '[图片]' - ? EdgeInsets.zero - : isUser - ? const EdgeInsets.fromLTRB( - 12, 12, 16, 12) // 用户消息右侧多一点内边距 - : const EdgeInsets.fromLTRB( - 16, 12, 12, 12)) // AI消息左侧多一点内边距 - : isUser - ? const EdgeInsets.fromLTRB( - 12, 12, 16, 12) // 用户消息右侧多一点内边距 - : const EdgeInsets.fromLTRB( - 16, 12, 12, 12), // AI消息左侧多一点内边距 + padding: (hasImage && message == '[图片]') + ? EdgeInsets.zero + : const EdgeInsets.all(12), decoration: BoxDecoration( color: _getBubbleColor(context), - borderRadius: BorderRadius.circular(18).copyWith( + borderRadius: BorderRadius.only( topLeft: isUser ? const Radius.circular(18) : const Radius.circular(4), - topRight: !isUser - ? const Radius.circular(18) - : const Radius.circular(4), + topRight: isUser + ? const Radius.circular(4) + : const Radius.circular(18), + bottomLeft: const Radius.circular(18), + bottomRight: const Radius.circular(18), ), boxShadow: [ BoxShadow( @@ -180,7 +172,7 @@ class MessageBubble extends GetView { child: ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.file( - File(imagePath!), + File('${controller.AppDir.value}$imagePath'), width: 200, height: 200, fit: BoxFit.cover, @@ -213,7 +205,7 @@ class MessageBubble extends GetView { child: ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.file( - File(imagePath!), + File('${controller.AppDir.value}$imagePath'), width: 200, height: 200, fit: BoxFit.cover, diff --git a/lib/modules/agent/views/music_card.dart b/lib/modules/agent/views/music_card.dart index 4da822412..1639994d8 100644 --- a/lib/modules/agent/views/music_card.dart +++ b/lib/modules/agent/views/music_card.dart @@ -3,7 +3,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import '../../../routes/app_routes.dart'; import '../controllers/agent_controller.dart'; -import 'circular_progress_button.dart'; +import '../widgets/circular_progress_button.dart'; /// 构建音乐项目 Widget buildMusicItem(BuildContext context, int id, String name, String url, diff --git a/lib/modules/agent/views/circular_progress_button.dart b/lib/modules/agent/widgets/circular_progress_button.dart similarity index 100% rename from lib/modules/agent/views/circular_progress_button.dart rename to lib/modules/agent/widgets/circular_progress_button.dart diff --git a/lib/modules/home/views/home_view.dart b/lib/modules/home/views/home_view.dart index b332f5941..e0350d376 100644 --- a/lib/modules/home/views/home_view.dart +++ b/lib/modules/home/views/home_view.dart @@ -100,7 +100,9 @@ class HomeView extends GetView { return IntroOverlayBuilder.aiBuilder( params, '欢迎使用Voitrans!首先,让我们唤醒你的专属 AI—— 小言', - onNext: () {}, + onNext: () { + controller.noviceGuideService.startWakeupListen(); + }, ); }, builder: (BuildContext context, GlobalKey key) => SizedBox( @@ -114,11 +116,26 @@ class HomeView extends GetView { IntroStepBuilder( group: 'home', order: 2, + getOverlayPosition: ({ + required Offset offset, + required Size screenSize, + required Size size, + }) { + return OverlayPosition( + top: 200.h, + width: screenSize.width, + crossAxisAlignment: CrossAxisAlignment.end, + ); + }, overlayBuilder: (params) { - return IntroOverlayBuilder.aiBuilder( - params, - '对着麦克风说“小言小言”', - onNext: () {}, + return Obx( + () => IntroOverlayBuilder.homeBuilder( + params, + controller.noviceGuideService.isWakeup.value, + onNext: () { + controller.noviceGuideService.closeWakeupListen(); + }, + ), ); }, builder: (BuildContext context, GlobalKey key) => SizedBox( @@ -254,7 +271,7 @@ class HomeView extends GetView { ), ], ), - padding: EdgeInsets.all(16.w), + padding: EdgeInsets.fromLTRB(16.w, 16.h, 16.w, 16.h), child: Column( children: [ // 状态标题行 @@ -277,7 +294,7 @@ class HomeView extends GetView { if (!controller.isFullyConnectedRx.value) { return _buildConnectButton(); } else { - return const SizedBox(); + return _buildDeviceButton(isDarkMode); } }), ], @@ -400,12 +417,42 @@ class HomeView extends GetView { ), SizedBox(height: 12.h), _buildBatteryInfoGrid(isDarkMode), - // SizedBox(height: 16.h), - // _buildDeviceDetailsCard(isDarkMode), ], ); } + // 设备管理按钮 + Widget _buildDeviceButton(bool isDarkMode) { + return ElevatedButton( + onPressed: () { + Get.toNamed(Routes.devices); // 跳转到设备管理页面 + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + minimumSize: Size(double.infinity, 44.h), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.r), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.device_hub, size: 16.sp), // 设备图标 + SizedBox(width: 8.w), + Text( + 'deviceManagement'.tr, + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } + // 电池信息网格布局 Widget _buildBatteryInfoGrid(bool isDarkMode) { return Obx(() { @@ -505,6 +552,8 @@ class HomeView extends GetView { batteryColor = Colors.red[600]!; } + bool isBatteryZero = batteryLevel == 0; + return Expanded( child: Padding( padding: EdgeInsets.symmetric(vertical: 6.h), @@ -523,32 +572,45 @@ class HomeView extends GetView { ), SizedBox(height: 4.h), - // 电池指示器 - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - batteryIcon, - size: 12.sp, - color: batteryColor, - ), - SizedBox(width: 2.w), - Text( - '$batteryLevel%', - style: TextStyle( - fontSize: 11.sp, - fontWeight: FontWeight.w500, - color: batteryColor, - ), - ), - if (isCharging) - Icon( - Icons.bolt, - size: 10.sp, - color: Colors.green[600], + // 电池指示器 - 当电量为0时,显示横线 + isBatteryZero + ? Container( + width: 20.w, // 横线宽度 + height: 12.sp, // 设置与图标相同的高度,确保对齐 + alignment: Alignment.center, // 居中对齐 + child: Container( + width: 10.w, + height: 1.5.h, // 实际横线高度 + color: isDarkMode + ? Colors.grey[600] + : Colors.grey[400], // 横线颜色 + ), + ) + : Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + batteryIcon, + size: 12.sp, + color: batteryColor, + ), + SizedBox(width: 2.w), + Text( + '$batteryLevel%', + style: TextStyle( + fontSize: 11.sp, + fontWeight: FontWeight.w500, + color: batteryColor, + ), + ), + if (isCharging) + Icon( + Icons.bolt, + size: 10.sp, + color: Colors.green[600], + ), + ], ), - ], - ), ], ), ), diff --git a/lib/modules/login/controllers/login_controller.dart b/lib/modules/login/controllers/login_controller.dart index 1a57adf96..2740a5b28 100644 --- a/lib/modules/login/controllers/login_controller.dart +++ b/lib/modules/login/controllers/login_controller.dart @@ -1,23 +1,23 @@ import 'dart:async'; +import 'dart:io'; -import '../../../data/models/appconfig.dart'; -import '../../../data/models/appconfig_model.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:logger/web.dart'; -import '../../../routes/app_routes.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:device_info_plus/device_info_plus.dart'; import 'dart:convert'; import 'facebookLogin.dart'; import 'googleLogin.dart'; import 'weixinLogin.dart'; import 'appleLogin.dart'; +import '../../../routes/app_routes.dart'; +import '../../../data/models/appconfig.dart'; +import '../../../data/models/appconfig_model.dart'; import '../../../data/services/network/api.dart'; import '../../../../data/models/user_Info.dart'; -import 'package:get_storage/get_storage.dart'; -import 'package:device_info_plus/device_info_plus.dart'; -import 'dart:io'; -import 'package:package_info_plus/package_info_plus.dart'; +import '../../../data/models/channel_app_model.dart'; import '../../../data/services/version_update_service.dart'; import '../../../modules/common/widgets/privacy_policy_dialog.dart'; @@ -27,20 +27,20 @@ enum LoginViewState { } // 用户渠道枚举 -enum UserChannel { - UNKNOWN(0), // 未知渠道 - CHINA_HUAWEI(1), // 中国-华为 - CHINA_XIAOMI(2), // 中国-小米 - CHINA_OPPO(3), // 中国-OPPO - CHINA_VIVO(4), // 中国-VIVO - CHINA_HONOR(5), // 中国-荣耀 - OFFICIAL(100), // 官方渠道 - GLOBAL_APPLE(101), // 全球-苹果 - GLOBAL_GOOGLE(102); // 全球-谷歌 - - const UserChannel(this.value); - final int value; -} +// enum UserChannel { +// UNKNOWN(0), // 未知渠道 +// CHINA_HUAWEI(1), // 中国-华为 +// CHINA_XIAOMI(2), // 中国-小米 +// CHINA_OPPO(3), // 中国-OPPO +// CHINA_VIVO(4), // 中国-VIVO +// CHINA_HONOR(5), // 中国-荣耀 +// OFFICIAL(100), // 官方渠道 +// GLOBAL_APPLE(101), // 全球-苹果 +// GLOBAL_GOOGLE(102); // 全球-谷歌 + +// const UserChannel(this.value); +// final int value; +// } class LoginController extends GetxController { static LoginController get to => Get.find(); @@ -212,6 +212,7 @@ class LoginController extends GetxController { String get contactValue => _contactValue.value; String get displayContact => _displayContact.value; List get verificationCode => _verificationCode; + final versionService = VersionUpdateService.to; // 文本控制器 TextEditingController contactController = TextEditingController(); @@ -246,20 +247,9 @@ class LoginController extends GetxController { // 检查游客登录配置 Future _checkGuestLoginConfig() async { try { - final versionService = VersionUpdateService.to; - - // 先检查版本更新,这会同时设置 isShowTourists 的值 - await versionService.checkForUpdate(); + _isGuestLoginVisible.value = versionService.cachedShowTourists; - // 直接使用 VersionUpdateService 中的 isShowTourists 值 - _isGuestLoginVisible.value = versionService.isShowTourists.value; Logger().d('游客登录显示状态: ${_isGuestLoginVisible.value}'); - - // 监听 isShowTourists 的变化 - versionService.isShowTourists.listen((value) { - _isGuestLoginVisible.value = value; - Logger().d('游客登录显示状态更新: $value'); - }); } catch (e) { Logger().e('检查游客登录配置失败: $e'); // 默认不显示游客登录 @@ -301,9 +291,7 @@ class LoginController extends GetxController { // 新增:初始化APP信息(版本号、渠道、国家代码) Future _initAppInfo() async { try { - // 获取APP版本号 - final packageInfo = await PackageInfo.fromPlatform(); - _appVersion = '${packageInfo.version}+${packageInfo.buildNumber}'; + _appVersion = versionService.cachedAppVersion; Logger().d('appVersion: $_appVersion'); // 获取国家代码 @@ -311,7 +299,7 @@ class LoginController extends GetxController { Logger().d('countryCode: $_countryCode'); // 获取用户渠道(根据平台和设备信息确定) - _userChannel = UserChannel.OFFICIAL; + _userChannel = versionService.cachedChannel; Logger().d('userChannel: ${_userChannel.value}'); } catch (e) { Logger().e('获取APP信息失败: $e'); diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart index 19ada35c7..8cd767ee0 100644 --- a/lib/modules/meeting/controllers/meeting_record_controller.dart +++ b/lib/modules/meeting/controllers/meeting_record_controller.dart @@ -138,7 +138,7 @@ class MeetingRecordController extends GetxController switch (audioType.value) { case 0: fileName.value = "现场录音"; - _audioSourceType = true; + _audioSourceType = false; break; case 1: fileName.value = "音、视频录音"; @@ -271,7 +271,7 @@ class MeetingRecordController extends GetxController void _startHardwareServices() { switch (audioType.value) { case 0: - _bleManager.openEncoder(); + // _bleManager.openEncoder(); break; case 1: _bleManager.openDecoder(); diff --git a/lib/modules/meeting/views/meeting_record_view.dart b/lib/modules/meeting/views/meeting_record_view.dart index fab9cdf61..4de8a3320 100644 --- a/lib/modules/meeting/views/meeting_record_view.dart +++ b/lib/modules/meeting/views/meeting_record_view.dart @@ -95,7 +95,7 @@ class MeetingRecordView extends GetView { unselectedLabelColor: Colors.grey, tabs: const [ Tab(text: '音频'), - Tab(text: '文本'), + // Tab(text: '文本'), ], ), Expanded( @@ -103,7 +103,7 @@ class MeetingRecordView extends GetView { controller: controller.tabController, children: [ _buildAudioTab(context, isDarkMode), - _buildTextTab(context, isDarkMode), + // _buildTextTab(context, isDarkMode), ], ), ), diff --git a/lib/modules/music/views/music_playlist_view.dart b/lib/modules/music/views/music_playlist_view.dart index 29f2c520b..d549252fe 100644 --- a/lib/modules/music/views/music_playlist_view.dart +++ b/lib/modules/music/views/music_playlist_view.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../../core/utils/logger.dart'; import '../../agent/controllers/agent_controller.dart'; -import '../../agent/views/circular_progress_button.dart'; +import '../../agent/widgets/circular_progress_button.dart'; class MusicPlaylistView extends StatelessWidget { const MusicPlaylistView({Key? key}) : super(key: key); @@ -115,7 +115,9 @@ class MusicPlaylistView extends StatelessWidget { ); } - final title = currentSong['name'] ?? currentSong['title'] ?? 'unknownSong'.tr; // 未知歌曲 + final title = currentSong['name'] ?? + currentSong['title'] ?? + 'unknownSong'.tr; // 未知歌曲 final artist = currentSong['singer'] ?? currentSong['sgener'] ?? currentSong['artist'] ?? @@ -192,7 +194,8 @@ class MusicPlaylistView extends StatelessWidget { final id = item["id"] ?? 0; final name = item["name"] ?? 'unknownSong'.tr; // 未知歌曲 final url = item["url"] ?? ""; - final singer = item["singer"] ?? item["sgener"] ?? 'unknownArtist'.tr; // 未知歌手 + final singer = + item["singer"] ?? item["sgener"] ?? 'unknownArtist'.tr; // 未知歌手 final image = item["image"] ?? ""; return _buildMusicItem( @@ -628,8 +631,8 @@ class MusicPlaylistView extends StatelessWidget { } // 播放选中的歌曲 - Future _playSelectedSong(AgentController controller, Map song, - List> playlist) async { + Future _playSelectedSong(AgentController controller, + Map song, List> playlist) async { // 防止重复点击导致的状态混乱 if (controller.isLoadingMusic.value) { return; diff --git a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart index 7adfc296b..91e28e89d 100644 --- a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart +++ b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart @@ -1,4 +1,5 @@ import 'package:classic_bluetooth/classic_bluetooth.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:voitrans/data/models/appconfig_model.dart'; import '/routes/app_routes.dart'; import 'package:logger/web.dart'; @@ -214,10 +215,9 @@ class OtaUpgradeController extends GetxController { updatepackageaddress = product!.updatepackageaddress!; // 版本检查 - bool isUpdate = _isNewVersionAvailable(product.version!); + bool hasUpdate = _isNewVersionAvailable(product.version!); - - isUpdateAvailable.value = isUpdate; + isUpdateAvailable.value = hasUpdate; } catch (e) { // 统一错误处理 errorMessage.value = e.toString(); @@ -238,22 +238,36 @@ class OtaUpgradeController extends GetxController { } } - // 版本比较方法优化 - bool _isNewVersionAvailable(String latest) { + // 版本比较方法优化 - 支持升级和降级 + bool _isNewVersionAvailable(String latestVersion) { try { List currentParts = currentVersion.value .split('.') .map((e) => int.parse(e.trim())) .toList(); List latestParts = - latest.split('.').map((e) => int.parse(e.trim())).toList(); + latestVersion.split('.').map((e) => int.parse(e.trim())).toList(); + + // 确保两个版本号长度一致,短的用0补齐 + int maxLength = currentParts.length > latestParts.length + ? currentParts.length + : latestParts.length; - for (int i = 0; i < latestParts.length; i++) { - int current = (i < currentParts.length) ? currentParts[i] : 0; - if (latestParts[i] > current) return true; - if (latestParts[i] < current) return false; + while (currentParts.length < maxLength) { + currentParts.add(0); } - return false; + while (latestParts.length < maxLength) { + latestParts.add(0); + } + + // 比较每一段,只要有不同就返回true(支持升级和降级) + for (int i = 0; i < maxLength; i++) { + if (latestParts[i] != currentParts[i]) { + return true; // 版本不同,允许更新(升级或降级) + } + } + + return false; // 版本完全相同,无需更新 } catch (e) { Logger().d('版本比较错误: $e'); return false; @@ -654,7 +668,7 @@ class OtaUpgradeController extends GetxController { color: Colors.green[600], size: 28, ), - SizedBox(width: 12), + SizedBox(width: 12.w), Text( '升级成功', style: TextStyle( diff --git a/lib/modules/profile/views/profile_view.dart b/lib/modules/profile/views/profile_view.dart index 328d7c19a..4dbf73dd3 100644 --- a/lib/modules/profile/views/profile_view.dart +++ b/lib/modules/profile/views/profile_view.dart @@ -740,72 +740,72 @@ class EditProfileView extends GetView { }, isDarkMode: isDarkMode, ), - // Divider( - // height: 1, - // color: isDarkMode - // ? Colors.white.withOpacity(0.1) - // : Colors.grey[200]), - // // 清理缓存 - // _buildSimpleNavigationSetting( - // title: 'clean'.tr, // 清理缓存 - // icon: Icons.logout, - // iconBgColor: isDarkMode - // ? Colors.red[900]!.withOpacity(0.3) - // : Colors.red[50]!, - // iconColor: isDarkMode ? Colors.red[300]! : Colors.red[600]!, - // titleColor: - // isDarkMode ? Colors.red[300]! : Colors.red[600]!, - // onTap: () { - // // 退出登录确认对话框 - // Get.dialog( - // AlertDialog( - // backgroundColor: - // isDarkMode ? Colors.grey[900] : Colors.white, - // title: Text( - // 'clean'.tr, // 清理缓存 - // style: TextStyle( - // color: isDarkMode ? Colors.white : Colors.black87, - // ), - // ), - // content: Text( - // 'cleanTips'.tr, // 确定要退出当前账号吗? - // style: TextStyle( - // color: - // isDarkMode ? Colors.white70 : Colors.black54, - // ), - // ), - // actions: [ - // TextButton( - // onPressed: () => Get.back(), - // child: Text( - // 'cancel'.tr, // 取消 - // style: TextStyle( - // color: isDarkMode - // ? Colors.white70 - // : Colors.black54, - // ), - // ), - // ), - // TextButton( - // onPressed: () { - // Get.back(); - // controller.clean(); - // }, - // child: Text( - // 'confirm'.tr, // 确定 - // style: TextStyle( - // color: isDarkMode - // ? Colors.red[300] - // : Colors.red[600], - // ), - // ), - // ), - // ], - // ), - // ); - // }, - // isDarkMode: isDarkMode, - // ), + Divider( + height: 1, + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[200]), + // 清理缓存 + _buildSimpleNavigationSetting( + title: 'clean'.tr, // 清理缓存 + icon: Icons.cleaning_services, + iconBgColor: isDarkMode + ? Colors.red[900]!.withOpacity(0.3) + : Colors.red[50]!, + iconColor: isDarkMode ? Colors.red[300]! : Colors.red[600]!, + titleColor: + isDarkMode ? Colors.red[300]! : Colors.red[600]!, + onTap: () { + // 退出登录确认对话框 + Get.dialog( + AlertDialog( + backgroundColor: + isDarkMode ? Colors.grey[900] : Colors.white, + title: Text( + 'clean'.tr, // 清除缓存 + style: TextStyle( + color: isDarkMode ? Colors.white : Colors.black87, + ), + ), + content: Text( + 'cleanTips'.tr, // 确定要清除个人缓存吗? + style: TextStyle( + color: + isDarkMode ? Colors.white70 : Colors.black54, + ), + ), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text( + 'cancel'.tr, // 取消 + style: TextStyle( + color: isDarkMode + ? Colors.white70 + : Colors.black54, + ), + ), + ), + TextButton( + onPressed: () { + Get.back(); + controller.clean(); + }, + child: Text( + 'confirm'.tr, // 确定 + style: TextStyle( + color: isDarkMode + ? Colors.red[300] + : Colors.red[600], + ), + ), + ), + ], + ), + ); + }, + isDarkMode: isDarkMode, + ), Divider( height: 1, color: isDarkMode diff --git a/lib/modules/settings/controllers/settings_controller.dart b/lib/modules/settings/controllers/settings_controller.dart index 7e8e8a090..4dcecf302 100644 --- a/lib/modules/settings/controllers/settings_controller.dart +++ b/lib/modules/settings/controllers/settings_controller.dart @@ -1,12 +1,15 @@ +import 'package:agent_service/agent_service.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:dio/dio.dart'; import 'package:get_storage/get_storage.dart'; import '../../../core/themes/theme_service.dart'; import '../../../data/services/speech_factory.dart'; +import '../../../routes/app_routes.dart'; import '../models/language_model.dart'; import 'package:logger/logger.dart'; import '../../../data/services/ble_manager.dart'; +import '../../../data/services/language_manager.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -244,11 +247,9 @@ class SettingsController extends GetxController { final PackageInfo packageInfo = await PackageInfo.fromPlatform(); appVersion.value = packageInfo.version; buildNumber.value = packageInfo.buildNumber; - print('版本信息: ${packageInfo.version}+${packageInfo.buildNumber}'); } catch (e) { appVersion.value = '未知'; buildNumber.value = '0'; - print('获取版本信息失败: $e'); } } @@ -381,18 +382,88 @@ class SettingsController extends GetxController { } } - void setLanguage(LanguageModel data) { + Future setLanguage(LanguageModel data) async { if (data.value != 'system') { List languageList = data.value.split('_'); Get.updateLocale(Locale(languageList[0], languageList[1])); } else { Get.updateLocale(Get.deviceLocale!); } + print("languageList: ${data.value}"); language.value = data; _storage.write('language', data.value); + + // 更新agent服务的语音识别语言设置 + await _updateAgentLanguageSettings(data); + + // 延迟一下确保语言更新完成,然后重新进入应用 + await Future.delayed(const Duration(milliseconds: 500)); + + // 重新进入应用 - 回到启动页面重新初始化 + Get.offAllNamed(Routes.splash); Get.back(); } + /// 更新agent服务的语音识别语言设置 + Future _updateAgentLanguageSettings(LanguageModel languageModel) async { + try { + final languageManager = Get.find(); + List supportedLanguages = []; + + // 根据选择的语言设置对应的ASR语言代码 + if (languageModel.value == 'system') { + // 跟随系统语言 + final deviceLocale = Get.deviceLocale; + if (deviceLocale != null) { + final lang = deviceLocale.languageCode; + final country = deviceLocale.countryCode; + if (country != null && country.isNotEmpty) { + supportedLanguages.add('$lang-$country'); + } else { + // 根据语言代码映射到对应的ASR代码 + final asrCode = languageManager.getAsrCodeByShortCode(lang); + if (asrCode != null) { + supportedLanguages.add(asrCode); + } else { + supportedLanguages.add('en-US'); // 默认英语 + } + } + } else { + supportedLanguages.add('en-US'); // 默认英语 + } + } else { + // 根据选择的语言值映射到ASR代码 + final parts = languageModel.value.split('_'); + if (parts.length == 2) { + final asrCode = languageManager.getAsrCodeByShortCode(parts[0]); + if (asrCode != null) { + supportedLanguages.add(asrCode); + } else { + // 直接使用语言-国家格式 + supportedLanguages.add('${parts[0]}-${parts[1]}'); + } + } + } + + // 确保至少有一个语言 + if (supportedLanguages.isEmpty) { + supportedLanguages.add('en-US'); + } + + Logger().i('更新agent服务语音识别语言: ${supportedLanguages.join(', ')}'); + + // 调用agent服务更新语言设置 + final success = await AgentService.setSupportedLanguages(supportedLanguages); + if (success) { + Logger().i('agent服务语言设置更新成功'); + } else { + Logger().w('agent服务语言设置更新失败'); + } + } catch (e) { + Logger().e('更新agent服务语言设置时出错: $e'); + } + } + // 加载当前语音服务类型 void _loadCurrentSpeechServiceType() { try { diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart index 784c905ed..7e2c18d9a 100644 --- a/lib/modules/settings/views/settings_view.dart +++ b/lib/modules/settings/views/settings_view.dart @@ -16,13 +16,6 @@ import '../../profile/views/unified_avatar.dart'; class SettingsView extends GetView { const SettingsView({Key? key}) : super(key: key); - // 正确获取用户名方式 - // String get userName => User.getname(); - - // String get deviceInfo => User.getdeviceInfo(); - // String get userId => User.getuserId(); - // String get standing => User.getstanding(); - // String get avatar => User.getnavatar(); @override Widget build(BuildContext context) { // 直接使用 Theme.of(context) 获取当前主题 @@ -425,32 +418,31 @@ class SettingsView extends GetView { }, isDarkMode: isDarkMode, ), - if (Platform.isAndroid) ...[ - Divider( - height: 1, - color: isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.grey[200]), + // if (Platform.isAndroid) ...[ + // Divider( + // height: 1, + // color: isDarkMode + // ? Colors.white.withOpacity(0.1) + // : Colors.grey[200]), - // 隐私设置 - _buildSimpleNavigationSetting( - title: 'privacySettings'.tr, // 隐私设置 - subtitle: 'privacySettingsInfo'.tr, // 管理数据收集和权限 - icon: Icons.shield_outlined, - iconBgColor: isDarkMode - ? Colors.purple[900]!.withOpacity(0.3) - : Colors.purple[100]!, - iconColor: isDarkMode - ? Colors.purple[300]! - : Colors.purple[600]!, - titleColor: isDarkMode ? Colors.white : null, - subtitleColor: isDarkMode ? Colors.white70 : null, - onTap: () { - AppSettings.openAppSettings(); - }, - isDarkMode: isDarkMode, - ) - ], + // // 隐私设置 + // _buildSimpleNavigationSetting( + // title: 'privacySettings'.tr, // 隐私设置 + // subtitle: 'privacySettingsInfo'.tr, // 管理数据收集和权限 + // icon: Icons.shield_outlined, + // iconBgColor: isDarkMode + // ? Colors.purple[900]!.withOpacity(0.3) + // : Colors.purple[100]!, + // iconColor: + // isDarkMode ? Colors.purple[300]! : Colors.purple[600]!, + // titleColor: isDarkMode ? Colors.white : null, + // subtitleColor: isDarkMode ? Colors.white70 : null, + // onTap: () { + // AppSettings.openAppSettings(); + // }, + // isDarkMode: isDarkMode, + // ), + // ], Divider( height: 1, @@ -599,32 +591,31 @@ class SettingsView extends GetView { onTap: () => Get.toNamed(Routes.OTA_UPGRADE), isDarkMode: isDarkMode, ), - if (Platform.isAndroid) ...[ - Divider( - height: 1, - color: isDarkMode - ? Colors.white.withOpacity(0.1) - : Colors.grey[200]), + // if (Platform.isAndroid) ...[ + Divider( + height: 1, + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[200]), - // 添加权限管理设置项 - _buildSimpleNavigationSetting( - title: 'permissionManagement'.tr, // 权限管理 - subtitle: 'permissionManagementInfo'.tr, // 管理应用权限设置 - icon: Icons.security, - iconBgColor: isDarkMode - ? Colors.orange[900]!.withOpacity(0.3) - : Colors.orange[100]!, - iconColor: isDarkMode - ? Colors.orange[300]! - : Colors.orange[600]!, - titleColor: isDarkMode ? Colors.white : null, - subtitleColor: isDarkMode ? Colors.white70 : null, - onTap: () { - Get.toNamed(Routes.permissions); - }, - isDarkMode: isDarkMode, - ), - ] + // 权限管理设置项 + _buildSimpleNavigationSetting( + title: 'permissionManagement'.tr, // 权限管理 + subtitle: 'permissionManagementInfo'.tr, // 管理应用权限设置 + icon: Icons.security, + iconBgColor: isDarkMode + ? Colors.orange[900]!.withOpacity(0.3) + : Colors.orange[100]!, + iconColor: + isDarkMode ? Colors.orange[300]! : Colors.orange[600]!, + titleColor: isDarkMode ? Colors.white : null, + subtitleColor: isDarkMode ? Colors.white70 : null, + onTap: () { + Get.toNamed(Routes.permissions); + }, + isDarkMode: isDarkMode, + ), + // ] ], ), ), diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index 94cf4b25c..d3b3c1631 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -9,6 +9,7 @@ import 'package:get_storage/get_storage.dart'; import 'package:intl/intl.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; +import '../../../data/services/bluetooth_manager.dart'; import '../../../data/services/music_manager.dart'; import '../../../data/services/volcano_translation_service.dart'; import '../../../data/services/tts_service.dart'; @@ -30,6 +31,8 @@ class TranslationController extends GetxController { static const String TAG = 'TranslationController'; // 服务 + final BluetoothManager _bluetoothManager = Get.find(); + final BleManager _bleManager = Get.find(); final AsrService _asrService = Get.find(); final VolcanoTranslationService _translationService = Get.find(); @@ -364,7 +367,7 @@ class TranslationController extends GetxController { @override void onClose() { - stopRecognition(); + stopAll(); _recognitionSubscription?.cancel(); _recognitionSubscription = null; _voiceInteractionSubscription?.cancel(); @@ -483,7 +486,11 @@ class TranslationController extends GetxController { if (currentMode.value == 'faceToFace') { _asrService.stopContinuousRecognition(); } else { - await _ttsService.setAudioOutputDevice(0); + if (_bluetoothManager.currentDeviceRx.value != null) { + restoreOriginalAudioState(); + } else { + disableBluetoothAudio(); + } await _asrService.startContinuousRecognition(_audioSourceType); // 开始ASR活跃时长计时 _startAsrActiveTracking(); diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart index d69278376..a7c21d007 100644 --- a/lib/modules/translation/views/translation_view.dart +++ b/lib/modules/translation/views/translation_view.dart @@ -662,13 +662,15 @@ class TranslationView extends GetView { // 显示语言选择对话框 void _showLanguageSelectionDialog(bool isSource, bool isDarkMode) { - Get.dialog( - Dialog( - backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16.r), // 统一圆角尺寸为16 - ), - insetPadding: EdgeInsets.symmetric(horizontal: 20.w), + Get.dialog(Dialog( + backgroundColor: isDarkMode ? Colors.grey[900] : Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16.r), // 统一圆角尺寸为16 + ), + alignment: isSource ? Alignment.centerLeft : Alignment.centerRight, + insetPadding: EdgeInsets.symmetric(horizontal: 10.w), + child: SizedBox( + width: 0.4.sw, child: Padding( // 使用Padding替代Container的padding padding: EdgeInsets.all(12.w), // 统一内边距 @@ -807,7 +809,7 @@ class TranslationView extends GetView { ), ), ), - ); + )); } // 构建加载更多指示器 @@ -1035,7 +1037,7 @@ class TranslationView extends GetView { snackPosition: SnackPosition.BOTTOM, backgroundColor: Get.isDarkMode ? Colors.grey[800] : Colors.grey[200], colorText: Get.isDarkMode ? Colors.white : Colors.black, - duration: const Duration(seconds: 2), + duration: const Duration(milliseconds: 800), ); } catch (e) { // 显示删除失败提示 @@ -1045,7 +1047,7 @@ class TranslationView extends GetView { snackPosition: SnackPosition.BOTTOM, backgroundColor: Colors.red[400], colorText: Colors.white, - duration: const Duration(seconds: 3), + duration: const Duration(milliseconds: 1500), ); } } diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 56031acaa..5a98218e4 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -84,6 +84,7 @@ object AgentService : CoroutineScope { // 服务端连接 private var serverurl = "" private var usertoken = "" + private var language = "" // 历史聊天消息缓存 private val historyMessages = JSONArray() @@ -200,8 +201,8 @@ object AgentService : CoroutineScope { //获取语音识别的语种 val supportedLanguagesConfig = config["supportedLanguages"] as? List val supportedLanguages = supportedLanguagesConfig?.toTypedArray() - ?: arrayOf("zh-CN", "en-US") // 默认支持多语言 - + ?: arrayOf("zh-CN") // 默认支持多语言 + language = supportedLanguages[0] val asrInitSuccess = initialize( subscriptionKey = config["azureSpeechKey"]?.toString() ?: "", region = config["azureSpeechRegion"]?.toString() ?: "", @@ -422,7 +423,7 @@ object AgentService : CoroutineScope { try { delay(maxIdleSeconds * 1000L) - if (isActive && isRecognitionActive && !hasSpeechDetected && !isTtsSpeaking && !isAiStreaming) { + if (isActive && isRecognitionActive && !isTtsSpeaking && !isAiStreaming) { stopRecognition() sendEvent( "auto_stop", @@ -524,8 +525,8 @@ object AgentService : CoroutineScope { override fun onRecognizing(recognizing: String, detectedLanguage: String) { if (recognizing.isNotEmpty()) { // 检测到语音,更新状态 - val previousHasSpeech = hasSpeechDetected - _hasSpeechDetected.set(true) + val previousHasSpeech = hasSpeechDetected + _hasSpeechDetected.set(true) if (!previousHasSpeech) { restartIdleCheck() @@ -829,7 +830,8 @@ object AgentService : CoroutineScope { // 先添加系统提示词 if (systemPrompt.isNotEmpty()) { - val mapdata = UserPortrait.getAll() + val mapdata = UserPortrait.getAll().toMutableMap() + mapdata["language"] = language // Log.d(TAG, "liwei--------- 用户画像: ${mapdata}") val _systemPrompt = Utils.fillTemplate(systemPrompt, mapdata) messagesWithSystemPrompt.put(createSystemMessage(_systemPrompt)) @@ -1120,7 +1122,7 @@ object AgentService : CoroutineScope { } /** - * 设置支持的语音识别语言 + * 设置支持的语音识别语言,同时更新TTS语言设置 */ fun setSupportedLanguages(languages: List): Boolean { if (!isInitialized) { @@ -1134,33 +1136,58 @@ object AgentService : CoroutineScope { sendEvent("error", mapOf("code" to "EMPTY_LANGUAGES", "message" to "语言列表不能为空")) return false } - + language = languages[0] return try { Log.i(TAG, "设置支持的语言: ${languages.joinToString(", ")}") // 重新初始化ASR服务以应用新的语言设置 - val success = azureAsrHelper?.initialize( - subscriptionKey = this.azureSpeechKey, - region = this.azureSpeechRegion, - supportedLanguages = languages.toTypedArray(), - audioSourceType = audioSourceType - ) ?: false - - if (success) { - Log.i(TAG, "语言设置更新成功") + val asrSuccess = AzureAsrHelper(context).apply { + // 使用协程处理异步初始化 + CoroutineScope(Dispatchers.IO).launch { + //获取语音识别的语种 + val supportedLanguagesConfig = config["supportedLanguages"] as? List + val supportedLanguages = supportedLanguagesConfig?.toTypedArray() + ?: arrayOf("zh-CN") // 默认支持多语言 + language = supportedLanguages[0] + val asrInitSuccess = initialize( + subscriptionKey = config["azureSpeechKey"]?.toString() ?: "", + region = config["azureSpeechRegion"]?.toString() ?: "", + supportedLanguages = supportedLanguages, + audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL + else AzureAsrHelper.AudioSourceType.MICROPHONE + ) + + if (asrInitSuccess) { + withContext(Dispatchers.Main) { + recognizeCallback() + } + } else { + Log.w(TAG, "Azure ASR初始化失败") + } + } + } + // 同时更新TTS语言设置 + val ttsSuccess = updateTtsLanguage(languages.first()) // 使用第一个语言作为TTS语言 + + val overallSuccess = asrSuccess && ttsSuccess + + if (overallSuccess) { + Log.i(TAG, "ASR和TTS语言设置更新成功") sendEvent("languages_updated", mapOf( "languages" to languages, - "status" to "success" + "status" to "success", + "asr_success" to asrSuccess, + "tts_success" to ttsSuccess )) } else { - Log.e(TAG, "语言设置更新失败") + Log.e(TAG, "语言设置更新失败 - ASR: $asrSuccess, TTS: $ttsSuccess") sendEvent("error", mapOf( "code" to "LANGUAGE_UPDATE_FAILED", - "message" to "语言设置更新失败" + "message" to "语言设置更新失败 - ASR: $asrSuccess, TTS: $ttsSuccess" )) } - success + overallSuccess } catch (e: Exception) { Log.e(TAG, "设置支持语言异常: ${e.message}") sendEvent("error", mapOf( @@ -1171,6 +1198,105 @@ object AgentService : CoroutineScope { } } + /** + * 更新TTS语言设置 + * + * @param languageCode ASR语言代码,如 "zh-CN", "en-US" + * @return 是否更新成功 + */ + private fun updateTtsLanguage(languageCode: String): Boolean { + return try { + Log.i(TAG, "更新TTS语言设置: $languageCode") + + // 根据ASR语言代码映射到对应的TTS语音角色 + val voiceName = getTtsVoiceNameByAsrCode(languageCode) + + if (voiceName.isNotEmpty()) { + // 设置TTS语音角色 + val success = ttsService?.setVoice(voiceName) ?: false + + if (success) { + Log.i(TAG, "TTS语音角色设置成功: $voiceName") + } else { + Log.w(TAG, "TTS语音角色设置失败: $voiceName") + } + + success + } else { + Log.w(TAG, "未找到语言代码 $languageCode 对应的TTS语音角色") + false + } + } catch (e: Exception) { + Log.e(TAG, "更新TTS语言设置异常: ${e.message}") + false + } + } + + /** + * 根据ASR语言代码获取对应的TTS语音角色名称 + * 与LanguageManager中的映射保持一致 + * + * @param asrCode ASR语言代码,如 "zh-CN", "en-US" + * @return TTS语音角色名称 + */ + private fun getTtsVoiceNameByAsrCode(asrCode: String): String { + return when (asrCode) { + // 中文相关 + "zh-CN" -> "zh-CN-XiaoxiaoNeural" + "zh-TW" -> "zh-TW-HsiaoChenNeural" + "zh-HK" -> "zh-HK-HiuMaanNeural" + + // 英语相关 + "en-US" -> "en-US-AriaNeural" + + // 亚洲语言 + "ja-JP" -> "ja-JP-NanamiNeural" + "ko-KR" -> "ko-KR-SunHiNeural" + "th-TH" -> "th-TH-PremwadeeNeural" + "vi-VN" -> "vi-VN-HoaiMyNeural" + "id-ID" -> "id-ID-GadisNeural" + "hi-IN" -> "hi-IN-SwaraNeural" + "ms-MY" -> "ms-MY-YasminNeural" + "bn-IN" -> "bn-IN-TanishaaNeural" + "ta-IN" -> "ta-IN-PallaviNeural" + "te-IN" -> "te-IN-ShrutiNeural" + "mr-IN" -> "mr-IN-AarohiNeural" + "ur-IN" -> "ur-IN-GulNeural" + "tr-TR" -> "tr-TR-EmelNeural" + "fa-IR" -> "fa-IR-DilaraNeural" + + // 欧洲语言 + "fr-FR" -> "fr-FR-DeniseNeural" + "es-ES" -> "es-ES-ElviraNeural" + "pt-BR" -> "pt-BR-FranciscaNeural" + "it-IT" -> "it-IT-ElsaNeural" + "de-DE" -> "de-DE-KatjaNeural" + "ru-RU" -> "ru-RU-SvetlanaNeural" + "pl-PL" -> "pl-PL-AgnieszkaNeural" + "nl-NL" -> "nl-NL-ColetteNeural" + "sv-SE" -> "sv-SE-SofieNeural" + "cs-CZ" -> "cs-CZ-VlastaNeural" + "el-GR" -> "el-GR-AthinaNeural" + "ro-RO" -> "ro-RO-AlinaNeural" + "hu-HU" -> "hu-HU-NoemiNeural" + "uk-UA" -> "uk-UA-PolinaNeural" + "da-DK" -> "da-DK-ChristelNeural" + "fi-FI" -> "fi-FI-NooraNeural" + "nb-NO" -> "nb-NO-IselinNeural" + "hr-HR" -> "hr-HR-GabrijelaNeural" + "ca-ES" -> "ca-ES-JoanaNeural" + + // 阿拉伯语和非洲语言 + "ar-EG" -> "ar-EG-SalmaNeural" + "sw-KE" -> "sw-KE-ZuriNeural" + + else -> { + Log.w(TAG, "未支持的语言代码: $asrCode,使用默认英语语音") + "en-US-AriaNeural" + } + } + } + /** * 保存聊天消息 */ diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index b884a0c71..e5ce57159 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift @@ -59,6 +59,7 @@ class AgentServiceImpl: NSObject { internal var isAiStreaming: Bool = false var currsessionId: String = "" + var language: String = "" private var idleTimer: DispatchSourceTimer? private let maxIdleSeconds: TimeInterval = 10 @@ -158,6 +159,9 @@ class AgentServiceImpl: NSObject { if let openaivisionModel = config["openaivisionModel"] as? String { self.visionModel = openaivisionModel } + if let supportedLanguages = config["supportedLanguages"] as? [String] { + self.language = supportedLanguages[0] + } if let customSystemPrompt = config["systemPrompt"] as? String, !customSystemPrompt.isEmpty { self.systemPrompt = customSystemPrompt } else { @@ -208,7 +212,7 @@ class AgentServiceImpl: NSObject { azureTtsHelper?.addListener(self) // 获取支持的语言列表,支持从配置中读取 - let supportedLanguages = config["supportedLanguages"] as? [String] ?? ["zh-CN", "en-US"] + let supportedLanguages = config["supportedLanguages"] as? [String] ?? ["zh-CN"] guard let asrSuccess = azureAsrHelper?.initialize( subscriptionKey: azureSpeechKey, @@ -287,8 +291,8 @@ class AgentServiceImpl: NSObject { // 使用更安全的方式检查self是否存在 DispatchQueue.main.async { [weak self] in guard let self = self else { return } - - if self.isRecognizing && !self.hasSpeechDetected && !self.isSpeaking && !self.isAiStreaming { + + if self.isRecognizing && !self.isSpeaking && !self.isAiStreaming { self.stopRecognition() self.sendEvent(name: "auto_stop", data: [ "reason": "idle_timeout", @@ -345,8 +349,14 @@ print("ai启动语音=\(audioSourceType)=\(useBle)") return true } - func recognizeCallback(useBle: Bool = false) -> Bool { - + func recognizeCallback() -> Bool { + print("重新设置识别回调=") + guard let asrsetupSuccess = azureAsrHelper?.setupEventListeners( + callback: self + ), asrsetupSuccess else { + sendError("初始化语音识别服务失败", code: "ASR_INIT_ERROR") + return false + } return true } @@ -361,7 +371,12 @@ print("ai启动语音=\(audioSourceType)=\(useBle)") return true } - + func disableBluetoothAudio() { + azureAsrHelper?.disableBluetoothAudio() + } + func restoreOriginalAudioState() { + azureAsrHelper?.restoreOriginalAudioState() + } func pushAudioData(_ audioData: Data) -> Bool { if !isRecognizing || !isInitialized { return false @@ -454,8 +469,8 @@ audioStream.saveAudioDataTo(data: audioData) processWithChatApiServiceInternal(userMessage: userMessage, displayText: text, speakResponse: speakResponse) } - private func processWithChatApiServiceInternal(userMessage: [String: Any], displayText: String, speakResponse: Bool = true, hasImage: Bool = false) { - os_log("开始处理ChatAPI请求: displayText=%{public}@, speakResponse=%{public}@, hasImage=%{public}@", + private func processWithChatApiServiceInternal(userMessage: [String: Any], displayText: String, speakResponse: Bool = true, hasImage: Bool = false,imagePath:String = "") { + os_log("开始处理ChatAPI请求: displayText=%{public}@, speakResponse=%{public}@, hasImage=%{public}@", log: logger, type: .info, displayText, speakResponse ? "是" : "否", hasImage ? "是" : "否") guard let chatApiService = chatApiService else { @@ -476,9 +491,16 @@ audioStream.saveAudioDataTo(data: audioData) var messages: [[String: Any]] = [] if !systemPrompt.isEmpty { - - let info: [String: String] = UserPortrait.shared.getAll() + var info = UserPortrait.shared.getAll() + info["language"] = self.language + + // 添加调试日志 + print("系统提示词模板: \(systemPrompt)") + print("用户画像数据: \(info)") + let _systemPrompt = fillTemplate(systemPrompt, with: info) + print("替换后的系统提示词: \(_systemPrompt)") + messages.append(chatApiService.createSystemMessage(content: _systemPrompt)) os_log("添加系统提示消息", log: logger, type: .info) } @@ -495,7 +517,8 @@ audioStream.saveAudioDataTo(data: audioData) agentService: self, speakResponse: speakResponse, displayText: displayText, - hasImage: hasImage + hasImage: hasImage, + imagePath: imagePath ) currsessionId = UUID().uuidString os_log("设置回调并调用sendMessageStream", log: logger, type: .info) @@ -552,7 +575,8 @@ audioStream.saveAudioDataTo(data: audioData) userMessage: userMessage, displayText: displayText, speakResponse: speakResponse, - hasImage: true + hasImage: true, + imagePath: imagePath ) } } @@ -879,53 +903,201 @@ private func jsonToString(_ json: [String: Any]) -> String? { } /** - * 设置支持的语音识别语言 - * + * 设置支持的语音识别语言,同时更新TTS语言设置 + * * @param languages 支持的语言列表,如 ["zh-CN", "en-US"] * @return 是否设置成功 */ func setSupportedLanguages(_ languages: [String]) -> Bool { os_log("设置支持的语言: %{public}@", log: logger, type: .info, languages.joined(separator: ", ")) - + if !isInitialized { os_log("服务未初始化,无法设置语言", log: logger, type: .error) sendError("服务未初始化", code: "NOT_INITIALIZED") return false } - + if languages.isEmpty { os_log("语言列表为空,使用默认语言", log: logger, type: .info) return false } + + self.language = languages[0] // 如果正在识别,需要先停止 let wasRecognizing = isRecognizing if wasRecognizing { _ = stopRecognition() } - + // 重新初始化ASR引擎,使用新的语言设置 - guard let success = azureAsrHelper?.initialize( - subscriptionKey: azureSpeechKey, - region: azureSpeechRegion, + guard let asrSuccess = azureAsrHelper?.initialize( + subscriptionKey: azureSpeechKey, + region: azureSpeechRegion, supportedLanguages: languages, audioSourceType: .microphone - ), success else { - os_log("重新初始化ASR引擎失败", log: logger, type: .error) - sendError("重新初始化ASR引擎失败", code: "ASR_INIT_ERROR") + ), asrSuccess else { + sendError("初始化语音识别服务失败", code: "ASR_INIT_ERROR") + return false + } + guard let asrsetupSuccess = azureAsrHelper?.setupEventListeners( + callback: self + ), asrsetupSuccess else { + sendError("初始化语音识别服务失败", code: "ASR_INIT_ERROR") return false } - os_log("成功设置支持的语言: %{public}@", log: logger, type: .info, languages.joined(separator: ", ")) - + // 同时更新TTS语言设置 + let ttsSuccess = updateTtsLanguage(languages.first ?? "zh-CN") + + let overallSuccess = asrSuccess && ttsSuccess + + if overallSuccess { + os_log("ASR和TTS语言设置更新成功: %{public}@", log: logger, type: .info, languages.joined(separator: ", ")) + } else { + os_log("语言设置更新失败 - ASR: %{public}@, TTS: %{public}@", log: logger, type: .error, String(asrSuccess), String(ttsSuccess)) + } + // 如果之前正在识别,重新开始 if wasRecognizing { _ = startRecognition() } - - return true + + return overallSuccess } - + + /** + * 更新TTS语言设置 + * + * @param languageCode ASR语言代码,如 "zh-CN", "en-US" + * @return 是否更新成功 + */ + private func updateTtsLanguage(_ languageCode: String) -> Bool { + os_log("更新TTS语言设置: %{public}@", log: logger, type: .info, languageCode) + + // 根据ASR语言代码映射到对应的TTS语音角色 + let voiceName = getTtsVoiceNameByAsrCode(languageCode) + + if !voiceName.isEmpty { + // 设置TTS语音角色 + let success = azureTtsHelper?.setVoice(voiceName) ?? false + + if success { + os_log("TTS语音角色设置成功: %{public}@", log: logger, type: .info, voiceName) + } else { + os_log("TTS语音角色设置失败: %{public}@", log: logger, type: .error, voiceName) + } + + return success + } else { + os_log("未找到语言代码 %{public}@ 对应的TTS语音角色", log: logger, type: .error, languageCode) + return false + } + } + + /** + * 根据ASR语言代码获取对应的TTS语音角色名称 + * 与LanguageManager中的映射保持一致 + * + * @param asrCode ASR语言代码,如 "zh-CN", "en-US" + * @return TTS语音角色名称 + */ + private func getTtsVoiceNameByAsrCode(_ asrCode: String) -> String { + switch asrCode { + // 中文相关 + case "zh-CN": + return "zh-CN-XiaoxiaoNeural" + case "zh-TW": + return "zh-TW-HsiaoChenNeural" + case "zh-HK": + return "zh-HK-HiuMaanNeural" + + // 英语相关 + case "en-US": + return "en-US-AriaNeural" + + // 亚洲语言 + case "ja-JP": + return "ja-JP-NanamiNeural" + case "ko-KR": + return "ko-KR-SunHiNeural" + case "th-TH": + return "th-TH-PremwadeeNeural" + case "vi-VN": + return "vi-VN-HoaiMyNeural" + case "id-ID": + return "id-ID-GadisNeural" + case "hi-IN": + return "hi-IN-SwaraNeural" + case "ms-MY": + return "ms-MY-YasminNeural" + case "bn-IN": + return "bn-IN-TanishaaNeural" + case "ta-IN": + return "ta-IN-PallaviNeural" + case "te-IN": + return "te-IN-ShrutiNeural" + case "mr-IN": + return "mr-IN-AarohiNeural" + case "ur-IN": + return "ur-IN-GulNeural" + case "tr-TR": + return "tr-TR-EmelNeural" + case "fa-IR": + return "fa-IR-DilaraNeural" + + // 欧洲语言 + case "fr-FR": + return "fr-FR-DeniseNeural" + case "es-ES": + return "es-ES-ElviraNeural" + case "pt-BR": + return "pt-BR-FranciscaNeural" + case "it-IT": + return "it-IT-ElsaNeural" + case "de-DE": + return "de-DE-KatjaNeural" + case "ru-RU": + return "ru-RU-SvetlanaNeural" + case "pl-PL": + return "pl-PL-AgnieszkaNeural" + case "nl-NL": + return "nl-NL-ColetteNeural" + case "sv-SE": + return "sv-SE-SofieNeural" + case "cs-CZ": + return "cs-CZ-VlastaNeural" + case "el-GR": + return "el-GR-AthinaNeural" + case "ro-RO": + return "ro-RO-AlinaNeural" + case "hu-HU": + return "hu-HU-NoemiNeural" + case "uk-UA": + return "uk-UA-PolinaNeural" + case "da-DK": + return "da-DK-ChristelNeural" + case "fi-FI": + return "fi-FI-NooraNeural" + case "nb-NO": + return "nb-NO-IselinNeural" + case "hr-HR": + return "hr-HR-GabrijelaNeural" + case "ca-ES": + return "ca-ES-JoanaNeural" + + // 阿拉伯语和非洲语言 + case "ar-EG": + return "ar-EG-SalmaNeural" + case "sw-KE": + return "sw-KE-ZuriNeural" + + default: + os_log("未支持的语言代码: %{public}@,使用默认英语语音", log: logger, type: .error, asrCode) + return "en-US-AriaNeural" + } + } + func dispose() -> Bool { print("ai释放资源") if isRecognizing { @@ -967,19 +1139,22 @@ class ChatApiStreamCallback: StreamCallback { private let speakResponse: Bool private let displayText: String private let hasImage: Bool + private let imagePath: String private var responseBuilder = "" private var metadata = "" private var userMetadata:[String: Any] = [:] private var reply = true; //是否回复 private var broadcast = true //是否播发 - init(agentService: AgentServiceImpl, speakResponse: Bool, displayText: String, hasImage: Bool) { + init(agentService: AgentServiceImpl, speakResponse: Bool, displayText: String, hasImage: Bool,imagePath:String) { self.agentService = agentService self.speakResponse = speakResponse self.displayText = displayText self.hasImage = hasImage + self.imagePath = imagePath + self.userMetadata["hasImage"] = hasImage + self.userMetadata["imagePath"] = imagePath - } func onUsage(_ sessionid: String,_ prompt_tokens: Int?,_ completion_tokens: Int?,_ total_tokens: Int?){ @@ -1418,6 +1593,7 @@ extension AgentServiceImpl: AzureAsrHelper.ContinuousRecognizeCallback { hasSpeechDetected = true if !previousHasSpeech { + print("onRecognizing restartIdleCheck") restartIdleCheck() } diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift index 0c0ef5703..fb8baa1f8 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift @@ -51,7 +51,14 @@ public class AgentServicePlugin: NSObject, FlutterPlugin { case "stopConversation": impl.stopRecognition() result(true) - + + case "disableBluetoothAudio": + impl.disableBluetoothAudio(); + return + + case "restoreOriginalAudioState": + impl.restoreOriginalAudioState(); + return case "processTextInput": guard let arguments = call.arguments as? [String: Any], let text = arguments["text"] as? String else { diff --git a/local_plugins/agent_service/lib/agent_service.dart b/local_plugins/agent_service/lib/agent_service.dart index 5871bd8fe..5d39accce 100644 --- a/local_plugins/agent_service/lib/agent_service.dart +++ b/local_plugins/agent_service/lib/agent_service.dart @@ -261,6 +261,33 @@ class AgentService { } } + /// 恢复原始音频状态 + /// + /// 恢复原始音频状态 + /// 返回是否成功恢复原始音频状态 + static Future restoreOriginalAudioState() async { + try { + final bool result = + await _channel.invokeMethod('restoreOriginalAudioState'); + return result; + } on PlatformException catch (e) { + throw AgentServiceException(e.code, e.message ?? '恢复原始音频状态失败', e.details); + } + } + + /// 禁用蓝牙音频 + /// + /// 禁用蓝牙音频 + /// 返回是否成功禁用蓝牙音频 + static Future disableBluetoothAudio() async { + try { + final bool result = await _channel.invokeMethod('disableBluetoothAudio'); + return result; + } on PlatformException catch (e) { + throw AgentServiceException(e.code, e.message ?? '禁用蓝牙音频失败', e.details); + } + } + /// 停止对话 /// /// 停止语音识别 @@ -332,8 +359,6 @@ class AgentService { } } - - /// 停止语音合成 static Future stopAiSteam() async { try { @@ -385,6 +410,7 @@ class AgentService { /// /// [imagePath] 图片文件路径 /// [text] 可选的文本描述或问题 + /// [imageUrl] 图片url /// [speakResponse] 是否朗读响应 /// 返回是否成功处理图片 static Future processImageInput(String imagePath, String imageUrl, @@ -402,6 +428,21 @@ class AgentService { } } + /// 设置支持的语音识别语言 + /// + /// [languages] 支持的语言列表,如 ["zh-CN", "en-US"] + /// 返回是否设置成功 + static Future setSupportedLanguages(List languages) async { + try { + final bool result = await _channel.invokeMethod('setSupportedLanguages', { + 'languages': languages, + }); + return result; + } on PlatformException catch (e) { + throw AgentServiceException(e.code, e.message ?? '设置支持语言失败', e.details); + } + } + /// 释放资源 static Future dispose() async { try { diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt index 02d909d4a..0d00c776b 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt @@ -745,7 +745,7 @@ class AzureAsrHelper(private val context: Context) { */ fun saveAudioDataTo(buffer: ByteArray) { - if (audioSourceType == AudioSourceType.MICROPHONE) return + if (audioSourceType == AudioSourceType.MICROPHONE||!isContinuousRecognitionActive) return // 放入队列,由写线程写入 writeQueue.offer(buffer.copyOf()) diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift index 3106c1236..40846707f 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureAsrHelper.swift @@ -98,11 +98,11 @@ public class AzureAsrHelper: NSObject { } // 创建语音配置 - speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region) + speechConfig = try SPXSpeechConfiguration(subscription: subscriptionKey, region: region) // 针对翻译场景的性能优化 // optimizeForTranslation() if isAutoDetectLanguage { - speechConfig?.setPropertyTo("Continuous", byName: "SpeechServiceConnection_LanguageIdMode") + speechConfig?.setPropertyTo("Continuous", by: SPXPropertyId.speechServiceConnectionLanguageIdMode) // 不在这里设置语言,让SPXAutoDetectSourceLanguageConfiguration处理 os_log("启用语言检测模式,支持语言: %{public}@", log: log, type: .info, supportedLanguages.joined(separator: ",")) } else { @@ -131,7 +131,7 @@ public class AzureAsrHelper: NSObject { */ private func preInitializeAudioComponents() { if audioStream == nil { - audioStream = AudioStream() + audioStream = AudioStream(parentHelper: self) audioStream?.initAudioRecord() audioStream?.onAudioData = { [weak self] data in self?.recordfile?.saveAudioDataToWav(data) @@ -354,7 +354,7 @@ public class AzureAsrHelper: NSObject { do { // 【优化】检查是否已经初始化,避免重复创建 if audioStream == nil { - audioStream = AudioStream() + audioStream = AudioStream(parentHelper: self) audioStream?.initAudioRecord() audioStream?.onAudioData = { [weak self] data in self?.recordfile?.saveAudioDataToWav(data) @@ -548,6 +548,7 @@ public class AzureAsrHelper: NSObject { public func disableBluetoothAudio() { do { + print("禁用蓝牙音频功能,切换回正常音频模式") audioStream?.setAudioOutputRoute(.speaker) } catch { print("disableBluetoothAudio") @@ -559,6 +560,7 @@ public class AzureAsrHelper: NSObject { public func restoreOriginalAudioState() { do { + print("恢复原始音频设备状态(通常是重新启用蓝牙)") audioStream?.setAudioOutputRoute(.receiver) } catch { print("restoreOriginalAudioState") @@ -632,9 +634,18 @@ public class AzureAsrHelper: NSObject { private var isRunning = false public var isWriting = false private let bufferSize: Int = 4096 - private var writeThread: DispatchQueue? - private var currentRoute: AudioOutputRoute? - public var onAudioData: ((Data) -> Void)? + private var writeThread: DispatchQueue? + private var currentRoute: AudioOutputRoute? + public var onAudioData: ((Data) -> Void)? + + // 添加对外部类的弱引用 + private weak var parentHelper: AzureAsrHelper? + + // 添加初始化方法,接收外部类引用 + init(parentHelper: AzureAsrHelper) { + self.parentHelper = parentHelper + super.init() + } /** * 初始化 */ @@ -654,7 +665,7 @@ public class AzureAsrHelper: NSObject { // 创建新的写线程 writeThread = DispatchQueue(label: "audio.stream.writer") startWriteThread() - self.setAudioOutputRoute(.receiver) + //self.setAudioOutputRoute(.receiver) } /// 获取最佳音频格式 (iOS 通常支持标准采样率) @@ -717,7 +728,8 @@ public class AzureAsrHelper: NSObject { * @param data 音频数据字节数组 */ public func saveAudioDataTo(data: Data) { - if audioSourceType != .external { + // 通过父类引用调用方法 + if audioSourceType != .external || !(parentHelper?.isContinuousRecognitionActive() ?? false) { return } // print("外部data=\(data)") @@ -879,8 +891,7 @@ private func runMicrophoneCapture() { case bluetooth } public func setAudioOutputRoute(_ route: AudioOutputRoute) { - guard currentRoute != route else { return } - print("setAudioOutputRoute") + do { // if self.isWriting { @@ -890,14 +901,15 @@ private func runMicrophoneCapture() { try audioSession.setActive(false) } - + print("setAudioOutputRoutecurrent,route=\(route)") switch route { case .speaker: + print("setAudioOutputRoutecurrent,speaker") // 使用扬声器时必须用videoChat模式 try audioSession.setCategory( .playAndRecord, mode: .videoChat, - options: [.allowBluetooth, .mixWithOthers, .defaultToSpeaker] + options: [.defaultToSpeaker] ) try audioSession.overrideOutputAudioPort(.speaker) @@ -906,7 +918,7 @@ private func runMicrophoneCapture() { try audioSession.setCategory( .playAndRecord, mode: .voiceChat, - options: [.allowBluetooth, .mixWithOthers, .defaultToSpeaker] + options: [.allowBluetooth] ) try audioSession.overrideOutputAudioPort(.none) diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift index f07e56855..5955ac7c8 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureSpeechPlugin.swift @@ -184,6 +184,15 @@ import os.log callback: currentAsrCallback! ) result(success) + + case "disableBluetoothAudio": + azureAsrHelper.disableBluetoothAudio(); + return + + case "restoreOriginalAudioState": + azureAsrHelper.restoreOriginalAudioState(); + return + case "stopContinuousRecognition": let success = azureAsrHelper.stopContinuousRecognition() @@ -224,7 +233,7 @@ import os.log return } do { - print("音频文件名称为: \(filePath)") + print("音频文件名称为: (filePath)") try azureAsrHelper.renameFile(filePath: filePath, newName: newName) result(true) } catch { @@ -239,7 +248,7 @@ import os.log return } do { - print("音频文件名称为: \(sourcePath)") + print("音频文件名称为: (sourcePath)") try azureAsrHelper.moveFile(sourcePath: sourcePath, destPath: destPath) result(true) } catch { @@ -253,7 +262,7 @@ import os.log return } do { - print("音频文件名称为: \(filePath)") + print("音频文件名称为: (filePath)") try azureAsrHelper.enableRecord(filePath: filePath) result(true) } catch { @@ -350,7 +359,7 @@ import os.log let success = azureTtsHelper.flushStream() result(success) - case "stop": + case "stopSpeaking": let success = azureTtsHelper.stop() result(success) diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift index 3d6c25e73..86ac66bb9 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift @@ -166,7 +166,7 @@ public class AzureTtsHelper: NSObject, ITtsService { let currentText = streamBuffer // 标点符号列表 (与Android一致) - let punctuationMarks: [Character] = [".", "。", "!", "!", "?", "?", ";", ";", ",", ",", ":", ":", "\n"] + let punctuationMarks: [Character] = [".", "。", "!", "!","*", "?", "?", ";", ";", ",", ",", ":", ":", "\n"] // 从后往前查找最后一个标点符号 var lastPunctuationIndex = -1 diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt index 0426e6409..216f8a136 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt @@ -87,6 +87,9 @@ object BleService { // 是否是ota模式 var isEnterOta = false + // 是否需要唤醒 + var isWakeup = true + // 是否回复指令 var isReply = true @@ -1515,7 +1518,13 @@ object BleService { private fun notifyWakeupSignalReceived() { for (callback in callbacks) { try { - callback.onWakeupSignalReceived() + if (isWakeup) { + callback.onWakeupSignalReceived() + } else { + if (callback::class.java.simpleName == "BleServicePlugin") { + callback.onWakeupSignalReceived() + } + } } catch (e: Exception) { Log.e(TAG, "分发唤醒信号回调异常", e) } diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt index f8f7b3db9..d18f07de6 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt @@ -278,6 +278,13 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware, val connected = BleService.isConnected() result.success(connected) } + "setWakeup" -> { + val wakeup = call.argument("wakeup") + if (wakeup != null) { + BleService.isWakeup = wakeup + } + result.success("") + } "registerBackgroundScan" -> { bleCompanionManager.registerBackgroundScan() result.success(true) diff --git a/local_plugins/ble_service/lib/ble_service.dart b/local_plugins/ble_service/lib/ble_service.dart index 3cfb0954d..7e5a21ffd 100644 --- a/local_plugins/ble_service/lib/ble_service.dart +++ b/local_plugins/ble_service/lib/ble_service.dart @@ -272,6 +272,17 @@ class BleService { } } + /// 设置是否是否需要唤醒 + Future setWakeup(bool wakeup) async { + try { + await _methodChannel.invokeMethod('setWakeup', { + 'wakeup': wakeup, + }); + } catch (e) { + print('设置是否是否需要唤醒失败: $e'); + } + } + /// 断开连接 Future disconnect() async { try { diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt index 77a3dffde..d303ae3b4 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/Utils.kt @@ -9,6 +9,8 @@ import android.provider.Settings import android.util.Log import androidx.core.content.ContextCompat import com.aallam.openai.api.chat.ChatMessage +import com.aallam.openai.api.chat.TextContent +import com.aallam.openai.api.chat.ListContent import io.ktor.client.* import io.ktor.client.engine.okhttp.* import io.ktor.client.plugins.sse.* @@ -169,16 +171,42 @@ fun roughTokenCount(text: String): Int { /** * 估算消息列表的总 token 数量(近似) - * @param messages List of message map: role: user/assistant, content: string + * @param messages List of message map: role: user/assistant, content: string or array * @return 估算 token 数 */ fun estimatePromptTokens(messages: List): Int { - var tokens = 0 - for (msg in messages) { - tokens += 4 // 每条消息结构开销 - tokens += roughTokenCount(msg.role.role.lowercase()) // ChatRole 枚举转小写字符串 - tokens += roughTokenCount(msg.content ?: "") + var totalText = "" + for (message in messages) { + when (val content = message.messageContent) { + is TextContent? -> { + totalText += content?.content + " " + } + is ListContent -> { + // 处理多模态内容数组 + content.content.forEach { item -> + if (item is Map<*, *>) { + val type = item["type"] as? String + if (type == "text") { + val text = item["text"] as? String ?: "" + totalText += text + " " + }else if(type == "image_url"){ + val text = item["image_url"] as? String ?: "" + totalText += text + " " + } + } + } + } + } } - tokens += 2 // 结尾 assistant 回复结构开销 - return tokens -} \ No newline at end of file + return roughTokenCount(totalText) +} +//fun estimatePromptTokens(messages: List): Int { +// var tokens = 0 +// for (msg in messages) { +// tokens += 4 // 每条消息结构开销 +// tokens += roughTokenCount(msg.role.role.lowercase()) // ChatRole 枚举转小写字符串 +// tokens += roughTokenCount(msg.content ?: "") +// } +// tokens += 2 // 结尾 assistant 回复结构开销 +// return tokens +//} \ No newline at end of file diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift index 689117876..524feda90 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/ChatApiService.swift @@ -68,18 +68,18 @@ public class ChatApiService: NSObject { /// 设置流式回调 public func setStreamCallback(_ sessionId: String, _ callback: StreamCallback) { - print("[ChatApiService] 设置回调 for sessionId: \(sessionId)") - print("[ChatApiService] 回调对象: \(callback)") + // print("[ChatApiService] 设置回调 for sessionId: \(sessionId)") + // print("[ChatApiService] 回调对象: \(callback)") self.sessionCallbacks[sessionId] = callback print("[ChatApiService] 回调设置完成,当前回调数量: \(sessionCallbacks.count)") } public func getSessionCallback(_ sessionId: String) -> StreamCallback? { let callback = self.sessionCallbacks[sessionId] - print("[ChatApiService] getSessionCallback for sessionId: \(sessionId)") - print("[ChatApiService] sessionCallbacks count: \(sessionCallbacks.count)") - print("[ChatApiService] sessionCallbacks keys: \(Array(sessionCallbacks.keys))") - print("[ChatApiService] callback exists: \(callback != nil)") + // print("[ChatApiService] getSessionCallback for sessionId: \(sessionId)") + // print("[ChatApiService] sessionCallbacks count: \(sessionCallbacks.count)") + // print("[ChatApiService] sessionCallbacks keys: \(Array(sessionCallbacks.keys))") + // print("[ChatApiService] callback exists: \(callback != nil)") return callback } //移除回调函数 @@ -311,7 +311,7 @@ public func sendMessageStream(_ sessionId: String, messages: [[String: Any]]) { tools: chatTools ) - print("[ChatApiService] 发起AI问答 问题:\(messages[0]) 工具数量: \(chatTools.count)") + print("[ChatApiService] 发起AI问答 问题:\(messages) 工具数量: \(chatTools.count)") } else { query = ChatQuery( messages: chatMessages, @@ -335,14 +335,14 @@ public func sendMessageStream(_ sessionId: String, messages: [[String: Any]]) { // 处理文本内容 if let content = delta.content { replyContext += content - print("[ChatApiService] 收到内容: \(content)") - print("[ChatApiService] 当前sessionId: \(sessionId)") + // print("[ChatApiService] 收到内容: \(content)") + // print("[ChatApiService] 当前sessionId: \(sessionId)") let callback = self.getSessionCallback(sessionId) if let callback = callback { - print("[ChatApiService] 找到回调,准备调用 onToken") + // print("[ChatApiService] 找到回调,准备调用 onToken") callback.onToken(sessionId, content) - print("[ChatApiService] onToken 调用完成") + // print("[ChatApiService] onToken 调用完成") } else { print("[ChatApiService] 警告:未找到 sessionId \(sessionId) 的回调") } @@ -761,7 +761,44 @@ private func convertMapToJsonObject(_ map: [String: Any]) -> [String: Any] { } return false } - + // 解析并构建正确的图片消息 + func parseImageData(_ data: [String: Any]) -> ChatQuery.ChatCompletionMessageParam? { + // 1. 校验基础字段 + guard let type = data["type"] as? String, type == "image_url", + let imageUrlDict = data["image_url"] as? [String: Any], + let imageUrlString = imageUrlDict["url"] as? String else { + print("数据结构错误:缺少type或image_url.url") + return nil + } + + // 2. 补充默认的detail(原结构体必须,默认auto) + let detailString = imageUrlDict["detail"] as? String ?? "auto" + let imageDetail: ChatQuery.ChatCompletionMessageParam.UserMessageParam.Content.VisionContent.ChatCompletionContentPartImageParam.ImageURL.Detail + switch detailString.lowercased() { + case "low": + imageDetail = .low + case "high": + imageDetail = .high + default: + imageDetail = .auto + } + + // 3. 逐层构建结构体(按原结构体要求映射) + let imageUrlModel = ChatQuery.ChatCompletionMessageParam.UserMessageParam.Content.VisionContent.ChatCompletionContentPartImageParam.ImageURL( + url: imageUrlString, + detail: imageDetail + ) + + let imageContentPart = ChatQuery.ChatCompletionMessageParam.UserMessageParam.Content.VisionContent.ChatCompletionContentPartImageParam( + imageUrl: imageUrlModel + ) + + let visionContent = ChatQuery.ChatCompletionMessageParam.UserMessageParam.Content.VisionContent.chatCompletionContentPartImageParam(imageContentPart) + let userContent = ChatQuery.ChatCompletionMessageParam.UserMessageParam.Content.vision([visionContent]) + let userMessageParam = ChatQuery.ChatCompletionMessageParam.UserMessageParam(content: userContent) + + return .user(userMessageParam) + } /// 转换为OpenAI Message格式 private func convertToChatMessages(_ messages: [[String: Any]]) throws -> [ChatQuery.ChatCompletionMessageParam] { var chatMessages: [ChatQuery.ChatCompletionMessageParam] = [] @@ -851,17 +888,24 @@ private func convertMapToJsonObject(_ map: [String: Any]) -> [String: Any] { var textContent = "" for item in contentArray { - if let type = item["type"] as? String, type == "text", - let text = item["text"] as? String { - textContent += text + if let type = item["type"] as? String { + switch type { + case "text": + if let text = item["text"] as? String { + if let chatMessage = ChatQuery.ChatCompletionMessageParam(role: messageRole, content: text) { + chatMessages.append(chatMessage) + } + } + case "image_url": + if let imageMessage = parseImageData(item) { + chatMessages.append(imageMessage) + print("图片消息解析成功") + } + default: + break + } } } - - let finalContent = textContent.isEmpty ? "多媒体内容" : textContent - - if let chatMessage = ChatQuery.ChatCompletionMessageParam(role: messageRole, content: finalContent) { - chatMessages.append(chatMessage) - } } } @@ -932,7 +976,7 @@ private func convertMapToJsonObject(_ map: [String: Any]) -> [String: Any] { // 创建工具参数 let toolParam = ChatQuery.ChatCompletionToolParam(function: functionDefinition) - print("解析工具 \(name):\(schema)") + // print("解析工具 \(name):\(schema)") chatTools.append(toolParam) } diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift index b65a7aaa4..35a9df702 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift @@ -479,7 +479,7 @@ public class MCPClient { // 添加连接成功后的工具列表汇总输出 if connectedCount > 0 { print("[MCPClient] 🎉 MCP 连接完成,共连接 \(connectedCount) 个服务器") - print("[MCPClient] 📋 所有可用工具汇总:") + // print("[MCPClient] 📋 所有可用工具汇总:") let allTools = getToolMaps() for (index, toolMap) in allTools.enumerated() { @@ -490,9 +490,9 @@ public class MCPClient { } } - print("[MCPClient] 📊 工具总数: \(allTools.count)") - print("[MCPClient] 🔧 本地函数: \(localFunctionDefs.count) 个") - print("[MCPClient] 🌐 远程工具: \(allTools.count - localFunctionDefs.count) 个") + // print("[MCPClient] 📊 工具总数: \(allTools.count)") + // print("[MCPClient] 🔧 本地函数: \(localFunctionDefs.count) 个") + // print("[MCPClient] 🌐 远程工具: \(allTools.count - localFunctionDefs.count) 个") } return connectedCount > 0 @@ -508,7 +508,7 @@ public class MCPClient { parameters: Any, handler: FunctionHandler ) -> Bool { - print("[MCPClient] 注册本地函数: \(name)") + // print("[MCPClient] 注册本地函数: \(name)") localFunctions[name] = handler let functionDef: [String: Any] = [ @@ -544,7 +544,7 @@ public class MCPClient { for client in subClients.values { let tools = client.getToolMaps() - print("[MCPClient] 读取工具列表: \(tools)") + // print("[MCPClient] 读取工具列表: \(tools)") allToolMaps.append(contentsOf: tools) } diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift index bcacf4eb5..c6a35e433 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/SystemFunctionHandler.swift @@ -419,7 +419,7 @@ public class SystemFunctionHandler: NSObject { "required": ["key", "value"] ] - let handler = SetUserProfileFieldHandler() + let handler: SetUserProfileFieldHandler = SetUserProfileFieldHandler() _ = client.registerLocalFunction( name: "set_user_profile_field", description: "设置或更新用户画像中的某一个字段", @@ -462,8 +462,7 @@ private class GetCurrentLocationHandler: FunctionHandler { guard let latStr = locationInfo["user_lat"] as? String, let lngStr = locationInfo["user_lng"] as? String, let latitude = Double(latStr), - let longitude = Double(lngStr), - let address = locationInfo["user_location"] as? String else { + let longitude = Double(lngStr) else { print("[GetCurrentLocationHandler] 位置数据格式错误") continuation.resume(returning: self.createErrorResponse(message: "位置数据格式错误,请重试")) return @@ -471,8 +470,8 @@ private class GetCurrentLocationHandler: FunctionHandler { print("[GetCurrentLocationHandler] 位置获取成功: lat=\(latitude), lng=\(longitude)") - let message = "当前位置:\(address)(纬度 \(latitude),经度 \(longitude))" - let successResponse = self.createSuccessResponse(message: message, latitude: latitude, longitude: longitude, address: address) + // let message = "当前位置:\(address)(纬度 \(latitude),经度 \(longitude))" + let successResponse = self.createSuccessResponse(latitude: latitude, longitude: longitude) print("[GetCurrentLocationHandler] 返回位置结果") continuation.resume(returning: successResponse) @@ -486,14 +485,12 @@ private class GetCurrentLocationHandler: FunctionHandler { } } - private func createSuccessResponse(message: String, latitude: Double, longitude: Double, address: String) -> String { + private func createSuccessResponse(latitude: Double, longitude: Double) -> String { let result: [String: Any] = [ "success": true, - "message": message, "data": [ "latitude": latitude, "longitude": longitude, - "address": address, "timestamp": Date().timeIntervalSince1970 * 1000 ] ] @@ -847,21 +844,19 @@ private class OpenRecorderHandler: FunctionHandler { private class SetUserProfileFieldHandler: FunctionHandler { func handle(arguments: [String: Any]) async throws -> String { guard let key = arguments["key"] as? String, - let value = arguments["value"] as? String else { - throw ChatApiException("缺少必要参数:key 和 value") + let value = arguments["value"] as? String else { + return "设置成功" } - // 验证参数 if key.isEmpty { - throw ChatApiException("画像字段名不能为空") + return "设置成功" } if value.isEmpty { - throw ChatApiException("字段值不能为空") + return "设置成功" } - let result = await setUserProfileField(key: key, value: value) - return "{\"result\": \"\(result.message)\", \"key\": \"\(key)\", \"value\": \"\(value)\"}\"}" + return "设置成功" } private func setUserProfileField(key: String, value: String) async -> Result { diff --git a/local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/ChatStorageHelper.swift b/local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/ChatStorageHelper.swift index eb3e38433..96fa34a8e 100644 --- a/local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/ChatStorageHelper.swift +++ b/local_plugins/chat_storage/ios/chat_storage/Sources/chat_storage/ChatStorageHelper.swift @@ -186,7 +186,7 @@ public class ChatStorageHelper { "totalCount": totalCount, "totalPages": totalPages ] - + os_log("getMessages: 读取历史记录: %{public}@", log: logger, type: .error, messagesArray) do { let jsonData = try JSONSerialization.data(withJSONObject: result, options: []) if let jsonString = String(data: jsonData, encoding: .utf8) { @@ -199,7 +199,7 @@ public class ChatStorageHelper { let errmsg = String(cString: sqlite3_errmsg(db)!) os_log("getMessages: SQL准备失败: %{public}@", log: logger, type: .error, errmsg) } - + // 返回空结果,但保持格式一致 let emptyResult: [String: Any] = [ "messages": [], @@ -370,4 +370,4 @@ public class ChatStorageHelper { return messages } -} \ No newline at end of file +} diff --git a/local_plugins/location_service/ios/location_service/Sources/location_service/LocationData.swift b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationData.swift index af7f9827c..5facd02e0 100644 --- a/local_plugins/location_service/ios/location_service/Sources/location_service/LocationData.swift +++ b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationData.swift @@ -4,13 +4,13 @@ import Foundation public struct LocationData: Codable { public let latitude: Double public let longitude: Double - public let address: String public let timestamp: TimeInterval + public let latitudeandlongitude: String - public init(latitude: Double, longitude: Double, address: String, timestamp: TimeInterval) { + public init(latitude: Double, longitude: Double,timestamp: TimeInterval) { self.latitude = latitude self.longitude = longitude - self.address = address self.timestamp = timestamp + self.latitudeandlongitude = "\(latitude),\(longitude)" } } diff --git a/local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift index 2312b1fc4..906789434 100644 --- a/local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift +++ b/local_plugins/location_service/ios/location_service/Sources/location_service/LocationService.swift @@ -27,7 +27,6 @@ public class LocationService: NSObject { super.init() setupLocationManager() } - private func setupLocationManager() { DispatchQueue.main.async { [weak self] in self?.locationManager = CLLocationManager() @@ -35,15 +34,26 @@ public class LocationService: NSObject { self?.locationManager?.desiredAccuracy = kCLLocationAccuracyBest self?.locationManager?.distanceFilter = 10 - // 初始化时获取当前状态 - self?.authorizationStatus = CLLocationManager.authorizationStatus() - self?.isLocationServicesEnabled = CLLocationManager.locationServicesEnabled() - - print("[LocationService] LocationManager 初始化完成") - print("[LocationService] 初始权限状态: \(self?.authorizationStatus.rawValue ?? -1)") - print("[LocationService] 位置服务启用: \(self?.isLocationServicesEnabled ?? false)") + // 不需要手动获取状态,delegate 方法会自动被调用 + print("[LocationService] LocationManager 初始化完成,等待权限状态回调") } } + // private func setupLocationManager() { + // DispatchQueue.main.async { [weak self] in + // self?.locationManager = CLLocationManager() + // self?.locationManager?.delegate = self + // self?.locationManager?.desiredAccuracy = kCLLocationAccuracyBest + // self?.locationManager?.distanceFilter = 10 + + // // 初始化时获取当前状态 + // self?.authorizationStatus = CLLocationManager.authorizationStatus() + // self?.isLocationServicesEnabled = CLLocationManager.locationServicesEnabled() + + // print("[LocationService] LocationManager 初始化完成") + // print("[LocationService] 初始权限状态: \(self?.authorizationStatus.rawValue ?? -1)") + // print("[LocationService] 位置服务启用: \(self?.isLocationServicesEnabled ?? false)") + // } + // } /// 获取当前位置 - 主要接口 public func getCurrentLocation(completion: @escaping (Result<[String: Any], Error>) -> Void) { @@ -190,11 +200,11 @@ public class LocationService: NSObject { } // 使用缓存的状态信息 - print("[LocationService] 开始位置请求") - print("[LocationService] 缓存的权限状态: \(self.authorizationStatus.rawValue)") - print("[LocationService] 缓存的位置服务状态: \(self.isLocationServicesEnabled)") - print("[LocationService] 当前线程: \(Thread.current)") - print("[LocationService] 是否主线程: \(Thread.isMainThread)") + // print("[LocationService] 开始位置请求") + // print("[LocationService] 缓存的权限状态: \(self.authorizationStatus.rawValue)") + // print("[LocationService] 缓存的位置服务状态: \(self.isLocationServicesEnabled)") + // print("[LocationService] 当前线程: \(Thread.current)") + // print("[LocationService] 是否主线程: \(Thread.isMainThread)") // 验证权限状态 if !self.checkLocationPermission() { @@ -238,7 +248,6 @@ public class LocationService: NSObject { let locationData = LocationData( latitude: latitude, longitude: longitude, - address: "", timestamp: Date().timeIntervalSince1970 * 1000 ) completion(.success(locationData)) @@ -273,7 +282,6 @@ public class LocationService: NSObject { return [ "user_lat": String(locationData.latitude), "user_lng": String(locationData.longitude), - "user_location": locationData.address, "timestamp": locationData.timestamp ] } diff --git a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt index b15378c46..370e16a5b 100644 --- a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt +++ b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/MCPClient.kt @@ -768,7 +768,7 @@ class MCPClient(private val context: Context? = null) : AutoCloseable { // 保存函数定义 localFunctionDefs[name] = ObjectMapper().writeValueAsString(functionDef) - Log.d(TAG, "注册本地函数成功: $name") + // Log.d(TAG, "注册本地函数成功: $name") return true } catch (e: Exception) { Log.e(TAG, "注册本地函数失败: ${e.message}", e) diff --git a/local_plugins/open_ai_service/ios/Classes/MCPClient.swift b/local_plugins/open_ai_service/ios/Classes/MCPClient.swift index c865ff079..121653f02 100644 --- a/local_plugins/open_ai_service/ios/Classes/MCPClient.swift +++ b/local_plugins/open_ai_service/ios/Classes/MCPClient.swift @@ -312,7 +312,7 @@ public class MCPClient { let jsonString = String(data: jsonData, encoding: .utf8) ?? "" localFunctionDefs[name] = jsonString - print("\(tag) 成功注册本地函数: \(name)") + // print("\(tag) 成功注册本地函数: \(name)") return true } catch { print("\(tag) 注册本地函数失败: \(error.localizedDescription)") diff --git a/local_plugins/open_ai_service/ios/open_ai_service/Sources/open_ai_service/MCPClient.swift b/local_plugins/open_ai_service/ios/open_ai_service/Sources/open_ai_service/MCPClient.swift index c865ff079..121653f02 100644 --- a/local_plugins/open_ai_service/ios/open_ai_service/Sources/open_ai_service/MCPClient.swift +++ b/local_plugins/open_ai_service/ios/open_ai_service/Sources/open_ai_service/MCPClient.swift @@ -312,7 +312,7 @@ public class MCPClient { let jsonString = String(data: jsonData, encoding: .utf8) ?? "" localFunctionDefs[name] = jsonString - print("\(tag) 成功注册本地函数: \(name)") + // print("\(tag) 成功注册本地函数: \(name)") return true } catch { print("\(tag) 注册本地函数失败: \(error.localizedDescription)") diff --git a/pubspec.yaml b/pubspec.yaml index ffa83d643..c9e4d81b6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: voitrans description: "Voitrans - AI Voice Assistant." publish_to: "none" -version: 1.0.4+20 +version: 1.0.6+20 environment: sdk: ">=3.3.0 <4.0.0"