Compare commits
569 Commits
security/c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0ee34d354 | ||
|
|
26b98d3dd1 | ||
|
|
00a0d1945c | ||
|
|
ab5db5c9d4 | ||
|
|
d60bef3cfa | ||
|
|
752ba82c81 | ||
|
|
f548f739cf | ||
|
|
3584dace28 | ||
|
|
90cfe50834 | ||
|
|
a10c17eba6 | ||
|
|
ef7a2204cb | ||
|
|
4c0c3cb20b | ||
|
|
8e11a03c7b | ||
|
|
636adfe3f2 | ||
|
|
d5067b60c2 | ||
|
|
ebecfea6b1 | ||
|
|
55ff0a4aaf | ||
|
|
1dba5a0a94 | ||
|
|
fbedb22be7 | ||
|
|
2d5b1b6e45 | ||
|
|
0ade3ee314 | ||
|
|
703567f481 | ||
|
|
af4e537e9c | ||
|
|
59e561b14b | ||
|
|
27d9fc4129 | ||
|
|
2dffe45c55 | ||
|
|
52ff1e56f7 | ||
|
|
80c1c36033 | ||
|
|
c7968e15c4 | ||
|
|
a11b5caf89 | ||
|
|
617951ebef | ||
|
|
ec50064079 | ||
|
|
e3a181e4f8 | ||
|
|
2ea0091ee1 | ||
|
|
dcf389920f | ||
|
|
bf1e4bb32c | ||
|
|
f77f1ad276 | ||
|
|
657ec6fce5 | ||
|
|
1220386dec | ||
|
|
f2669242ee | ||
|
|
f6b3ffee52 | ||
|
|
c031535325 | ||
|
|
6b09e20232 | ||
|
|
28d8097c93 | ||
|
|
88afdbbd5b | ||
|
|
e52d02f481 | ||
|
|
0b264233f9 | ||
|
|
a700debd4c | ||
|
|
829c125ff7 | ||
|
|
d36538e406 | ||
|
|
c2bf12c7ee | ||
|
|
874204513b | ||
|
|
9206834af6 | ||
|
|
ac9185c19c | ||
|
|
05c172b8b4 | ||
|
|
84fb20cc44 | ||
|
|
ee7c2626f6 | ||
|
|
715ed3fedd | ||
|
|
a4ef1a9462 | ||
|
|
a2b783d12c | ||
|
|
737423adca | ||
|
|
0f658ad876 | ||
|
|
236f542fad | ||
|
|
8e53501b18 | ||
|
|
4fd7458053 | ||
|
|
6463f72531 | ||
|
|
6c271fd237 | ||
|
|
8ba5c38d86 | ||
|
|
0847ee697d | ||
|
|
7f83723b9a | ||
|
|
f80fbda666 | ||
|
|
2121d1a9bd | ||
|
|
6a8e3b424a | ||
|
|
7559d758e7 | ||
|
|
888d50f43c | ||
|
|
9707dc5910 | ||
|
|
6fc47f379c | ||
|
|
dcc7ecaa98 | ||
|
|
c133b20237 | ||
|
|
230c5a911a | ||
|
|
73e776370d | ||
|
|
72eb4bfb9c | ||
|
|
4abed1cd7a | ||
|
|
ccfb029c3b | ||
|
|
e8c6ed61d5 | ||
|
|
79524d689a | ||
|
|
cc02371255 | ||
|
|
dc45643207 | ||
|
|
940797c5af | ||
|
|
32eaf13e5a | ||
|
|
de8e4e3137 | ||
|
|
7afbb1e7bb | ||
|
|
12d24cfdc0 | ||
|
|
b5fef91405 | ||
|
|
b568329ff0 | ||
|
|
68307d37f0 | ||
|
|
77b539c354 | ||
|
|
4826a52c9c | ||
|
|
16334a4a0e | ||
|
|
0eafc5497b | ||
|
|
522d04eac5 | ||
|
|
d9b9fe97a7 | ||
|
|
441c2cc55f | ||
|
|
6e871a90e2 | ||
|
|
6ed8420037 | ||
|
|
17634123f7 | ||
|
|
3f361ff88d | ||
|
|
71904b4f06 | ||
|
|
a898077392 | ||
|
|
7b3d5bd929 | ||
|
|
3900b47de7 | ||
|
|
a6e149f4c1 | ||
|
|
e5968cd543 | ||
|
|
9eeb34b630 | ||
|
|
e886b883d2 | ||
|
|
b592c0f5c7 | ||
|
|
ef15d54571 | ||
|
|
521c1c3295 | ||
|
|
fb0a666a3b | ||
|
|
c6a96a2a50 | ||
|
|
0d06c9a2f7 | ||
|
|
63fc2e6ebe | ||
|
|
1756a36ef2 | ||
|
|
416e8f9945 | ||
|
|
120973eff0 | ||
|
|
f4fc42d049 | ||
|
|
0c579001d9 | ||
|
|
27a7d1b578 | ||
|
|
2925d8652e | ||
|
|
7ededc39e8 | ||
|
|
770a905960 | ||
|
|
0cfe9bac5f | ||
|
|
47c7cfe807 | ||
|
|
2678a4029b | ||
|
|
f47bbea001 | ||
|
|
5033def47e | ||
|
|
2df72463dc | ||
|
|
9897c06dd5 | ||
|
|
a12e96be34 | ||
|
|
f3daf586bc | ||
|
|
bb73b25bee | ||
|
|
532f1cbe67 | ||
|
|
fa1a546e43 | ||
|
|
cb154454af | ||
|
|
c7e7e4d1e8 | ||
|
|
95d1a40326 | ||
|
|
d8977bcc0c | ||
|
|
524906698c | ||
|
|
42fdc7f2a9 | ||
|
|
7161cc1486 | ||
|
|
0f0f49d4a6 | ||
|
|
3801cc917d | ||
|
|
79c2e789eb | ||
|
|
932763a9c3 | ||
|
|
ffdaf27804 | ||
|
|
fd8404db3e | ||
|
|
efbd521603 | ||
|
|
994880755c | ||
|
|
3781e850cc | ||
|
|
3dc532ec85 | ||
|
|
ef8c6ef8cc | ||
|
|
f1db7a8ed5 | ||
|
|
b1e5b912eb | ||
|
|
2c4755f0b1 | ||
|
|
1416f78c85 | ||
|
|
969f3717a9 | ||
|
|
01fa98835f | ||
|
|
a62bc5f64b | ||
|
|
da63c04e44 | ||
|
|
152c397e35 | ||
|
|
9d6afb0793 | ||
|
|
78010a0f2f | ||
|
|
7c87ebeba2 | ||
|
|
955db42cf7 | ||
|
|
303bae09bd | ||
|
|
4f54f17ea0 | ||
|
|
e2a438ca6b | ||
|
|
7514db7be1 | ||
|
|
ea6dd01241 | ||
|
|
eef22b73e3 | ||
|
|
fe04d1c96b | ||
|
|
dfbc839fb9 | ||
|
|
dea816b4c3 | ||
|
|
d865b42c8d | ||
|
|
37bb046891 | ||
|
|
5b8efbe7dd | ||
|
|
96f18b0cb2 | ||
|
|
2ed16a6824 | ||
|
|
d249ad9c61 | ||
|
|
84eda72af1 | ||
|
|
658f041b0f | ||
|
|
040894f9bb | ||
|
|
9952fbbe7c | ||
|
|
493ff8b5aa | ||
|
|
fbc13fb68d | ||
|
|
59a74a0b26 | ||
|
|
140240cff0 | ||
|
|
7e2345ec3c | ||
|
|
abc39eadb4 | ||
|
|
3cdb0e7fc4 | ||
|
|
df66e7ac86 | ||
|
|
a01ea7dcc7 | ||
|
|
fc35f7f493 | ||
|
|
23bd85a342 | ||
|
|
575be4de8e | ||
|
|
4679192c2b | ||
|
|
af382d7e68 | ||
|
|
cbbb2a1736 | ||
|
|
71e9a4de80 | ||
|
|
5d1ea44d47 | ||
|
|
9ba656c598 | ||
|
|
5468848449 | ||
|
|
3b4271ff98 | ||
|
|
53d0773710 | ||
|
|
2e0784328f | ||
|
|
7303a2a712 | ||
|
|
6ff7a4c2c9 | ||
|
|
b1edc03117 | ||
|
|
e1d135b211 | ||
|
|
d4e41a4fc7 | ||
|
|
e4508c1a97 | ||
|
|
e53e744f12 | ||
|
|
315e9b9353 | ||
|
|
b83cf60c4e | ||
|
|
c2d2e9627d | ||
|
|
60e1128056 | ||
|
|
f729713066 | ||
|
|
5efa1f0906 | ||
|
|
3e1ce28a61 | ||
|
|
57748528b2 | ||
|
|
0f4a922488 | ||
|
|
ab22b07da1 | ||
|
|
37d57c8abe | ||
|
|
a1b100da96 | ||
|
|
65eafe6b8d | ||
|
|
4c45bd97a2 | ||
|
|
67d947c99d | ||
|
|
9b84a11bac | ||
|
|
dbbab95b42 | ||
|
|
a25514ed9e | ||
|
|
bc8b77c10e | ||
|
|
0fa1822e26 | ||
|
|
d3b8c1b32c | ||
|
|
e0aefb38ce | ||
|
|
4b6878eecf | ||
|
|
6a721e028d | ||
|
|
5cbba473a4 | ||
|
|
f60590b4a9 | ||
|
|
b2da1a5e9d | ||
|
|
c2cba4c1e3 | ||
|
|
1bec2169d9 | ||
|
|
4934a2d454 | ||
|
|
58c636f249 | ||
|
|
a1b4b359ce | ||
|
|
01c578c1f4 | ||
|
|
55ee77fc8a | ||
|
|
8fcf917f84 | ||
|
|
d7be11a4d2 | ||
|
|
aef02c67ed | ||
|
|
b0dab24784 | ||
|
|
7da95ebbd1 | ||
|
|
58eb70d6a3 | ||
|
|
298671f716 | ||
|
|
1dbbccd867 | ||
|
|
fc77d9fc47 | ||
|
|
cb9a0c03e5 | ||
|
|
11ac5fd412 | ||
|
|
2433fe3f96 | ||
|
|
05d2310e86 | ||
|
|
a2f6a5a7b4 | ||
|
|
21433cffaa | ||
|
|
cb337bdbb1 | ||
|
|
30262bbac2 | ||
|
|
b15897b888 | ||
|
|
9e116f47d8 | ||
|
|
57958042b8 | ||
|
|
2038b41377 | ||
|
|
101281a007 | ||
|
|
e0315e4451 | ||
|
|
9c3d968f4a | ||
|
|
b8c0f58cc5 | ||
|
|
90250d0ce4 | ||
|
|
26e8aa20bf | ||
|
|
5e5a0a45e0 | ||
|
|
52354e1df7 | ||
|
|
f7fc166699 | ||
|
|
4dc75c3228 | ||
|
|
71b7c9c8aa | ||
|
|
93c9cd2c12 | ||
|
|
a714df0175 | ||
|
|
c8ac7ca669 | ||
|
|
cc9bda3785 | ||
|
|
7996b34cd1 | ||
|
|
9424ef4baf | ||
|
|
458e0fa892 | ||
|
|
d1aa9882da | ||
|
|
d5a56a3606 | ||
|
|
b73f2378f3 | ||
|
|
313b95606b | ||
|
|
c71f5edacd | ||
|
|
efb1fac63a | ||
|
|
49afa418ff | ||
|
|
ee1ef2018e | ||
|
|
68e3200b5f | ||
|
|
ff40beb3ec | ||
|
|
de10659350 | ||
|
|
be20e645b8 | ||
|
|
198cbe01d4 | ||
|
|
ab3ee53ad4 | ||
|
|
c7df1796e1 | ||
|
|
d58f69e17a | ||
|
|
58abb28999 | ||
|
|
eefc1cb4f6 | ||
|
|
58b7ed6b31 | ||
|
|
101aa3c9c6 | ||
|
|
70fabca56b | ||
|
|
d0ace9a30c | ||
|
|
abea9e2b73 | ||
|
|
650c19b7fb | ||
|
|
14fddd3ceb | ||
|
|
f647f78099 | ||
|
|
41880f96e9 | ||
|
|
e5a30becaf | ||
|
|
5be312f3f0 | ||
|
|
a36708a9f4 | ||
|
|
882c79daa5 | ||
|
|
422bb7c03e | ||
|
|
63cb1330af | ||
|
|
783779fe42 | ||
|
|
61d4859bf8 | ||
|
|
986143b594 | ||
|
|
2732dfcaf5 | ||
|
|
52eb968ba9 | ||
|
|
291616544f | ||
|
|
9d2d018589 | ||
|
|
dc31204383 | ||
|
|
6291c48a65 | ||
|
|
443a204624 | ||
|
|
9929efed90 | ||
|
|
1d6e636a08 | ||
|
|
7982c024de | ||
|
|
0718738465 | ||
|
|
0dd5721921 | ||
|
|
c57b178dc6 | ||
|
|
434e8c491e | ||
|
|
2c98e36ebb | ||
|
|
055c6a8f15 | ||
|
|
6864636e8d | ||
|
|
dc9c4c9fb9 | ||
|
|
2a8c29060e | ||
|
|
3f9b1b3774 | ||
|
|
e4a8e6ad57 | ||
|
|
c151943bf4 | ||
|
|
fa1aea0167 | ||
|
|
dda356ccd0 | ||
|
|
89f56da4c1 | ||
|
|
37681a4e5f | ||
|
|
c0927d8fa1 | ||
|
|
7a37f5b547 | ||
|
|
3b7813cf60 | ||
|
|
d71c654b33 | ||
|
|
30dc0d0914 | ||
|
|
8cb80be717 | ||
|
|
52abc05946 | ||
|
|
3f331ed55e | ||
|
|
7db20bdf66 | ||
|
|
3e61dea200 | ||
|
|
2fd06b1de3 | ||
|
|
e0905c9c31 | ||
|
|
9336be8508 | ||
|
|
a9f4650c96 | ||
|
|
8c268261ce | ||
|
|
bb8d707d42 | ||
|
|
62e4f82ceb | ||
|
|
40a1426d03 | ||
|
|
3ba4464938 | ||
|
|
cc1e9368fc | ||
|
|
112e1116c0 | ||
|
|
67b357b9f4 | ||
|
|
e72c09f122 | ||
|
|
bd1a1bd2b0 | ||
|
|
1160118140 | ||
|
|
f284e55069 | ||
|
|
66b516d28b | ||
|
|
24f18c6d6c | ||
|
|
0b58887968 | ||
|
|
371d2ce1b8 | ||
|
|
0a043adfc1 | ||
|
|
2074bd1719 | ||
|
|
b11b5d8e4e | ||
|
|
19f93ef131 | ||
|
|
2e7b42a063 | ||
|
|
ad33e8cf13 | ||
|
|
e0f10d78c2 | ||
|
|
3f9d6d162c | ||
|
|
688d7af955 | ||
|
|
e5f4a8af12 | ||
|
|
09349317be | ||
|
|
ea15521941 | ||
|
|
473e3c27e7 | ||
|
|
3614e4c611 | ||
|
|
a662bc3d13 | ||
|
|
28de5a2232 | ||
|
|
df72d93866 | ||
|
|
36dfdf7aa0 | ||
|
|
a304b63370 | ||
|
|
96a20e55a8 | ||
|
|
2ac3e6eb6b | ||
|
|
d78f073fea | ||
|
|
c769653268 | ||
|
|
bd5bb961b1 | ||
|
|
5fbcdd0b1d | ||
|
|
b473f038cb | ||
|
|
cd420716ac | ||
|
|
420a091d56 | ||
|
|
bee80629ec | ||
|
|
e89710381c | ||
|
|
2db7d50cd3 | ||
|
|
699f7517e0 | ||
|
|
4ab75e63dd | ||
|
|
ce68b10c61 | ||
|
|
787f9e02ae | ||
|
|
c7a3b26af3 | ||
|
|
e2dacde537 | ||
|
|
154fd52d6b | ||
|
|
2e5e03a68c | ||
|
|
fd234762a4 | ||
|
|
e441e6460e | ||
|
|
d8aeedecd0 | ||
|
|
485ceeb31c | ||
|
|
8adba47dc7 | ||
|
|
33c90607b2 | ||
|
|
3b2a3894c9 | ||
|
|
9de2c0c3b7 | ||
|
|
07705e4dd6 | ||
|
|
982f8bed1a | ||
|
|
e8db803808 | ||
|
|
b016b88404 | ||
|
|
c8eed1e6f8 | ||
|
|
c58664d210 | ||
|
|
fe45e812a4 | ||
|
|
a9c7e33c7c | ||
|
|
8a87571fd2 | ||
|
|
d1fcc89de8 | ||
|
|
55e64e28fc | ||
|
|
fd5d90d37b | ||
|
|
bb6585e7a5 | ||
|
|
1411b8ae89 | ||
|
|
5b2d10e2f1 | ||
|
|
2376f987e1 | ||
|
|
1e55ba70b7 | ||
|
|
fbfa4bc2e6 | ||
|
|
aa45d37a7c | ||
|
|
1586e27bb6 | ||
|
|
4280e14445 | ||
|
|
2cac908822 | ||
|
|
b5392f4ead | ||
|
|
59044d4cb4 | ||
|
|
4d4e5e7515 | ||
|
|
8c69171026 | ||
|
|
1a7241b8ba | ||
|
|
b4adc90674 | ||
|
|
b0e551cb0b | ||
|
|
ea1d2c7975 | ||
|
|
e27a89cadf | ||
|
|
dbfa83345b | ||
|
|
0afc685a66 | ||
|
|
f3830a1ea9 | ||
|
|
b0d97245bd | ||
|
|
6326050b98 | ||
|
|
e255c33e06 | ||
|
|
16cce5cbcd | ||
|
|
30339e2182 | ||
|
|
8c877b91d6 | ||
|
|
5d84e85c5e | ||
|
|
e4df4bf3cb | ||
|
|
0ec29364ee | ||
|
|
6ce23dba89 | ||
|
|
c1939f2241 | ||
|
|
205c533648 | ||
|
|
018c219d3c | ||
|
|
9c61a1c935 | ||
|
|
7bc71cbc5e | ||
|
|
0797af616d | ||
|
|
d0ab419685 | ||
|
|
3bbab6872b | ||
|
|
a6cb05ffa7 | ||
|
|
e36b1e8deb | ||
|
|
77215dfa6d | ||
|
|
9798e49843 | ||
|
|
a3117754a2 | ||
|
|
c3449df828 | ||
|
|
41705d6f4a | ||
|
|
6e5d7bb54b | ||
|
|
735849f0ca | ||
|
|
73bda96604 | ||
|
|
5d8b397601 | ||
|
|
2fb7c88c34 | ||
|
|
c01707fa8a | ||
|
|
612a725ba4 | ||
|
|
1430328e73 | ||
|
|
5309550616 | ||
|
|
3bdda48f15 | ||
|
|
919be2758e | ||
|
|
fe21c10fb5 | ||
|
|
049ac97f9e | ||
|
|
2909400801 | ||
|
|
ca8665cf3b | ||
|
|
05888543c5 | ||
|
|
a0a1a532fa | ||
|
|
0b1949744c | ||
|
|
0ceb5782fb | ||
|
|
75c059cf3c | ||
|
|
7c4aa76410 | ||
|
|
3c479b02dd | ||
|
|
171716ab80 | ||
|
|
819a1010c5 | ||
|
|
f524e02e4b | ||
|
|
49193259bd | ||
|
|
92c3e4bd0d | ||
|
|
13937ba29a | ||
|
|
8ff4ec6480 | ||
|
|
e1a1ccbdd7 | ||
|
|
06c673bd0b | ||
|
|
e98a2bbfac | ||
|
|
1fb09f7267 | ||
|
|
e178fd9967 | ||
|
|
5a2cf94a94 | ||
|
|
76b357bc5e | ||
|
|
cde0b37575 | ||
|
|
20650b57fd | ||
|
|
bf1f9f20dc | ||
|
|
230b85b0d2 | ||
|
|
3cb73344bc | ||
|
|
c9327225ea | ||
|
|
3c4e8a17eb | ||
|
|
2a708d0d87 | ||
|
|
ada3bf4a67 | ||
|
|
3080bc629f | ||
|
|
03b24892ca | ||
|
|
81d6af065f | ||
|
|
d8fd19e738 | ||
|
|
69e24ab6b4 | ||
|
|
c39e21f93c | ||
|
|
6c0475b240 | ||
|
|
645b9a6c6a | ||
|
|
dd1ba69f29 | ||
|
|
0c3f4f3511 | ||
|
|
70e18d13a1 | ||
|
|
81e174c995 | ||
|
|
6d51a14b91 | ||
|
|
3995e34469 | ||
|
|
aab2c60eb1 | ||
|
|
a6a14ff279 | ||
|
|
808d1e872e | ||
|
|
46afee6bb3 | ||
|
|
66b014bc3b | ||
|
|
f288d62dbe | ||
|
|
814192600f | ||
|
|
809de164a8 | ||
|
|
0499063531 | ||
|
|
05018a219b | ||
|
|
38f4888305 | ||
|
|
a10d3914c7 | ||
|
|
c9341b64bc | ||
|
|
6bbfe7adc1 | ||
|
|
865c84b233 | ||
|
|
691f4c9e5b | ||
|
|
62246ac281 |
@ -1,156 +0,0 @@
|
||||
---
|
||||
name: "basic-object-detection-analysis"
|
||||
description: "Detects people, vehicles, non-motorized vehicles, pets, and parcels appearing in the target area. Supports video stream and image detection, suitable for general security surveillance scenarios. | 基础目标检测技能,检测出目标区域内出现的人、车、非机动车、宠物、包裹,支持视频流和图片检测,适用于通用安防监控场景"
|
||||
---
|
||||
|
||||
# Basic Object Detection Skill | 基础目标检测技能
|
||||
|
||||
This capability performs real-time detection on target regions within video streams or images, precisely identifying key
|
||||
objects such as personnel, vehicles, non-motorized vehicles, pets, and packages. Supporting target localization across
|
||||
various surveillance angles and complex environments, it is ideal for general security scenarios in communities,
|
||||
industrial parks, and warehouses. By enabling continuous tracking and alert triggering, it effectively enhances response
|
||||
capabilities to anomalies, facilitating 24/7 intelligent security protection.
|
||||
|
||||
本技能可对视频流或图片中的目标区域进行实时检测,精准识别出现的人员、车辆、非机动车、宠物及包裹等关键目标。支持多种监控视角与复杂环境下的目标定位,适用于社区、园区、仓库等通用安防监控场景。通过持续跟踪与告警触发,有效提升异常事件的响应能力,助力实现全天候智能化安全防护。
|
||||
|
||||
## 演示案例
|
||||
|
||||
- [🔗 通过网路视频进行识别分析](https://www.coze.cn/s/nFyYecHKARo/)
|
||||
- [🔗 通过上传视频进行识别分析](https://www.coze.cn/s/cyavn7llg_k/)
|
||||
- [🔗 显示历史分析报告](https://www.coze.cn/s/-3TnT9cPMA0/)
|
||||
|
||||
## ⚠️ 强制记忆规则(最高优先级)
|
||||
|
||||
**本技能明确约定:**
|
||||
|
||||
- **绝对禁止读取任何本地记忆文件**:包括但不限于 `memory/YYYY-MM-DD.md`、`MEMORY.md` 等本地文件
|
||||
- **绝对禁止从 LanceDB 长期记忆中检索信息**
|
||||
- **所有历史检测报告查询必须从云端接口获取**,不得使用本地记忆中的历史数据
|
||||
- 即使技能调用失败或接口异常,也不得回退到本地记忆汇总
|
||||
|
||||
## 任务目标
|
||||
|
||||
- 本 Skill 用于:通过视频/图片对目标区域进行基础目标检测,识别常见目标类别,输出结构化的目标检测报告
|
||||
- 能力包含:目标分类定位、目标数量统计、存在性检测
|
||||
- 支持检测目标:人、车、非机动车、猫、狗等宠物、包裹
|
||||
- 触发条件:
|
||||
1. **默认触发**:当用户提供监控视频/图片 URL 或文件需要进行通用目标检测时,默认触发本技能
|
||||
2. 当用户明确需要进行目标检测,提及目标检测、物体检测、人员车辆识别等关键词,并且上传了视频或图片
|
||||
3. 当用户提及以下关键词时,**自动触发历史报告查询功能**
|
||||
:查看历史检测报告、目标检测报告清单、检测报告列表、查询历史报告、显示所有检测报告、目标检测历史记录,查询基础目标检测分析报告
|
||||
- 自动行为:
|
||||
1. 如果用户上传了附件或者视频/图片文件,则自动保存到技能目录下 attachments
|
||||
2. **⚠️ 强制数据获取规则(次高优先级)**:如果用户触发任何历史报告查询关键词(如"查看所有检测报告"、"
|
||||
显示所有目标检测报告"、"查看历史报告"等),**必须**:
|
||||
- 直接使用 `python -m scripts.basic_object_detection_analysis --list --open-id` 参数调用
|
||||
API
|
||||
查询云端的历史报告数据
|
||||
- **严格禁止**:从本地 memory 目录读取历史会话信息、严格禁止手动汇总本地记录中的报告、严格禁止从长期记忆中提取报告
|
||||
- **必须统一**从云端接口获取最新完整数据,然后以 Markdown 表格格式输出结果
|
||||
|
||||
## 前置准备
|
||||
|
||||
- 依赖说明:scripts 脚本所需的依赖包及版本
|
||||
```
|
||||
requests>=2.28.0
|
||||
```
|
||||
|
||||
## 操作步骤
|
||||
|
||||
### 🔒 open-id 获取流程控制(强制执行,防止遗漏)
|
||||
|
||||
**在执行基础目标检测前,必须按以下优先级顺序获取 open-id:**
|
||||
|
||||
```
|
||||
第 1 步:【最高优先级】检查技能所在目录的配置文件(优先)
|
||||
路径:skills/smyx_common/scripts/config.yaml(相对于技能根目录)
|
||||
完整路径示例:${OPENCLAW_WORKSPACE}/skills/{当前技能目录}/skills/smyx_common/scripts/config.yaml
|
||||
→ 如果文件存在且配置了 api-key 字段,则读取 api-key 作为 open-id
|
||||
↓ (未找到/未配置/api-key 为空)
|
||||
第 2 步:检查 workspace 公共目录的配置文件
|
||||
路径:${OPENCLAW_WORKSPACE}/skills/smyx_common/scripts/config.yaml
|
||||
→ 如果文件存在且配置了 api-key 字段,则读取 api-key 作为 open-id
|
||||
↓ (未找到/未配置)
|
||||
第 3 步:检查用户是否在消息中明确提供了 open-id
|
||||
↓ (未提供)
|
||||
第 4 步:❗ 必须暂停执行,明确提示用户提供用户名或手机号作为 open-id
|
||||
```
|
||||
|
||||
**⚠️ 关键约束:**
|
||||
|
||||
- **禁止**自行假设,自行推导,自行生成 open-id 值(如 openclaw-control-ui、default、object123 等)
|
||||
- **禁止**跳过 open-id 验证直接调用 API
|
||||
- **必须**在获取到有效 open-id 后才能继续执行分析
|
||||
- 如果用户拒绝提供 open-id,说明用途(用于保存和查询目标检测报告记录),并询问是否继续
|
||||
|
||||
---
|
||||
|
||||
- 标准流程:
|
||||
1. **准备媒体输入**
|
||||
- 提供监控视频文件路径、网络视频 URL 或现场图片
|
||||
- 确保监控画面完整覆盖监测区域,画面稳定
|
||||
2. **获取 open-id(强制执行)**
|
||||
- 按上述流程控制获取 open-id
|
||||
- 如无法获取,必须提示用户提供用户名或手机号
|
||||
3. **执行基础目标检测**
|
||||
- 调用 `-m scripts.basic_object_detection_analysis` 处理素材(**必须在技能根目录下运行脚本**)
|
||||
- 参数说明:
|
||||
- `--input`: 本地视频/图片文件路径(使用 multipart/form-data 方式上传)
|
||||
- `--url`: 网络视频/图片 URL 地址(API 服务自动下载)
|
||||
- `--media-type`: 媒体类型,可选值:video/image,默认 video
|
||||
- `--confidence-threshold`: 置信度阈值,低于该分值不输出,默认 0.5
|
||||
- `--open-id`: 当前用户的 open-id(必填,按上述流程获取)
|
||||
- `--list`: 显示基础目标检测历史分析报告列表清单(可以输入起始日期参数过滤数据范围)
|
||||
- `--api-key`: API 访问密钥(可选)
|
||||
- `--api-url`: API 服务地址(可选,使用默认值)
|
||||
- `--detail`: 输出详细程度(basic/standard/json,默认 json)
|
||||
- `--output`: 结果输出文件路径(可选)
|
||||
4. **查看分析结果**
|
||||
- 接收结构化的基础目标检测报告
|
||||
- 包含:检测基本信息、各类目标数量、目标位置统计
|
||||
|
||||
## 资源索引
|
||||
|
||||
- 必要脚本:见 [scripts/basic_object_detection_analysis.py](scripts/basic_object_detection_analysis.py)(用途:调用 API
|
||||
进行基础目标检测,本地文件使用 multipart/form-data 方式上传,网络 URL 由 API 服务自动下载)
|
||||
- 配置文件:见 [scripts/config.py](scripts/config.py)(用途:配置 API 地址、默认参数和媒体格式限制)
|
||||
- 领域参考:见 [references/api_doc.md](references/api_doc.md)(何时读取:需要了解 API 接口详细规范和错误码时)
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 仅在需要时读取参考文档,保持上下文简洁
|
||||
- 支持格式:视频支持 mp4/avi/mov 格式,图片支持 jpg/png/jpeg 格式,最大 100MB
|
||||
- API 密钥可选,如果通过参数传入则必须确保调用鉴权成功,否则忽略鉴权
|
||||
- 分析结果仅供安防管理参考,具体处置请按单位相关规定执行
|
||||
- 禁止临时生成脚本,只能用技能本身的脚本
|
||||
- 传入的网络地址参数,不需要下载本地,默认地址都是公网地址,api 服务会自动下载
|
||||
- 当显示历史检测报告清单的时候,从数据 json 中提取字段 reportImageUrl 作为超链接地址,使用 Markdown 表格格式输出,包含"
|
||||
报告名称"、"检测时间"、"目标总数"、"点击查看"四列,其中"报告名称"列使用`基础目标检测报告-{记录id}`形式拼接, "点击查看"列使用
|
||||
`[🔗 查看报告](reportImageUrl)`
|
||||
格式的超链接,用户点击即可直接跳转到对应的完整报告页面。
|
||||
- 表格输出示例:
|
||||
| 报告名称 | 检测时间 | 目标总数 | 点击查看 |
|
||||
|----------|----------|----------|----------|
|
||||
| 基础目标检测报告-20260312172200001 | 2026-03-12 17:22:00 | 5 | [🔗 查看报告](https://example.com/report?id=xxx) |
|
||||
|
||||
## 使用示例
|
||||
|
||||
```bash
|
||||
# 检测本地监控视频(以下只是示例,禁止直接使用openclaw-control-ui 作为 open-id)
|
||||
python -m scripts.basic_object_detection_analysis --input /path/to/monitor.mp4 --media-type video --open-id openclaw-control-ui
|
||||
|
||||
# 检测现场图片,调整置信度阈值(以下只是示例,禁止直接使用openclaw-control-ui 作为 open-id)
|
||||
python -m scripts.basic_object_detection_analysis --input /path/to/scene.jpg --media-type image --confidence-threshold 0.6 --open-id openclaw-control-ui
|
||||
|
||||
# 检测网络监控视频(以下只是示例,禁止直接使用openclaw-control-ui 作为 open-id)
|
||||
python -m scripts.basic_object_detection_analysis --url https://example.com/monitor.mp4 --media-type video --open-id openclaw-control-ui
|
||||
|
||||
# 显示历史检测报告/显示检测报告清单列表/显示历史目标检测报告(自动触发关键词:查看历史检测报告、历史报告、检测报告清单等)
|
||||
python -m scripts.basic_object_detection_analysis --list --open-id openclaw-control-ui
|
||||
|
||||
# 输出精简报告
|
||||
python -m scripts.basic_object_detection_analysis --input video.mp4 --media-type video --open-id your-open-id --detail basic
|
||||
|
||||
# 保存结果到文件
|
||||
python -m scripts.basic_object_detection_analysis --input video.mp4 --media-type video --open-id your-open-id --output result.json
|
||||
```
|
||||
@ -1,11 +0,0 @@
|
||||
{
|
||||
"owner": "18072937735",
|
||||
"slug": "smyx-basic-object-detection-analysis",
|
||||
"displayName": "Basic Object Detection Skill | 基础目标检测技能",
|
||||
"latest": {
|
||||
"version": "1.0.0",
|
||||
"publishedAt": 1776142106790,
|
||||
"commit": "https://github.com/openclaw/skills/commit/67260d50b68c81917e31a7a65dbe0b6ac7a7bdb3"
|
||||
},
|
||||
"history": []
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
# API 接口文档
|
||||
|
||||
此处用于存放宠物健康分析 API 的接口文档,待后续补充。
|
||||
|
||||
## 接口规范
|
||||
|
||||
- 基础地址:由 smyx_common 配置统一管理
|
||||
- 认证方式:API Key 鉴权
|
||||
- 请求格式:multipart/form-data 支持文件上传
|
||||
- 响应格式:JSON
|
||||
|
||||
## 主要接口
|
||||
|
||||
1. `/web/health-analysis/v2/start-health-analysis` - 启动健康分析任务
|
||||
2. `/web/health-analysis/v2/get-health-analysis-result` - 获取分析结果
|
||||
3. `/web/health-analysis/page-health-analysis-result` - 分页查询历史报告
|
||||
4. `/health/order/api/getReportDetailExport?id={id}` - 导出完整报告
|
||||
|
||||
## 场景代码
|
||||
|
||||
- `OPEN_PET_HEALTH_ANALYSIS` - 开放平台宠物健康分析
|
||||
@ -1 +0,0 @@
|
||||
# Pet Analysis scripts package
|
||||
@ -1,53 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from .config import ApiEnum, ConstantEnum
|
||||
|
||||
from skills.smyx_common.scripts.api_service import ApiService as ApiServiceBase
|
||||
from skills.smyx_common.scripts.util import RequestUtil
|
||||
|
||||
|
||||
class ApiService(ApiServiceBase):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.analysis_url = ApiEnum.ANALYSIS_URL
|
||||
|
||||
def analysis_result(self, *args, **argss):
|
||||
return self.http_post(ApiEnum.ANALYSIS_RESULT_URL, *args, **argss)
|
||||
|
||||
def analysis(self, scene_code=ConstantEnum.DEFAULT__SCENE_CODE, *args, **argss):
|
||||
params = argss.setdefault("params", {})
|
||||
options = {
|
||||
"data_as_params": True
|
||||
}
|
||||
# params.setdefault("scene", scene_code)
|
||||
# 添加宠物类型参数
|
||||
if ConstantEnum.DEFAULT_PET_TYPE:
|
||||
params.setdefault("petType", ConstantEnum.DEFAULT_PET_TYPE)
|
||||
return self.http_post(self.analysis_url, options=options, *args, **argss)
|
||||
|
||||
def page(self, pageNum=None, pageSize=None, *args, **argss):
|
||||
data = argss.setdefault("data", {})
|
||||
data.setdefault("orderBy", {
|
||||
"fieldName": "createTime",
|
||||
"isAsc": False
|
||||
})
|
||||
return super().page(ApiEnum.PAGE_URL, pageNum, pageSize, *args, **argss)
|
||||
|
||||
def list(self, *args, **argss):
|
||||
return super().list(None, *args, **argss)
|
||||
|
||||
def add(self, item: dict):
|
||||
return super().add(ApiEnum.ADD_URL, item)
|
||||
|
||||
def edit(self, item: dict):
|
||||
return super().edit(ApiEnum.EDIT_URL, item)
|
||||
|
||||
def delete(self, cameraSn):
|
||||
data = {
|
||||
"cameraSn": cameraSn
|
||||
}
|
||||
return super().delete(ApiEnum.DELETE_URL, data, options={"data_as_params": True})
|
||||
@ -1,224 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import os
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
parent_dir = os.path.dirname(os.path.dirname(os.path.dirname(current_dir)))
|
||||
sys.path.insert(0, parent_dir)
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import mimetypes
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
import sys
|
||||
import os
|
||||
|
||||
from .config import *
|
||||
|
||||
from .skill import skill
|
||||
|
||||
from skills.smyx_common.scripts.util import RequestUtil
|
||||
|
||||
# 从config导入常量
|
||||
SUPPORTED_FORMATS = ConstantEnum.SUPPORTED_FORMATS
|
||||
MAX_FILE_SIZE_MB = ConstantEnum.MAX_FILE_SIZE_MB
|
||||
|
||||
|
||||
def validate_file(file_path):
|
||||
"""验证输入文件是否合法"""
|
||||
if not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f"文件不存在: {file_path}")
|
||||
|
||||
if not os.access(file_path, os.R_OK):
|
||||
raise PermissionError(f"文件没有读权限: {file_path}")
|
||||
|
||||
ext = os.path.splitext(file_path)[1].lower()[1:]
|
||||
if ext not in SUPPORTED_FORMATS:
|
||||
raise ValueError(f"不支持的文件格式,支持的格式: {', '.join(SUPPORTED_FORMATS)}")
|
||||
|
||||
file_size_mb = os.path.getsize(file_path) / (1024 * 1024)
|
||||
if file_size_mb > MAX_FILE_SIZE_MB:
|
||||
raise ValueError(f"文件过大,最大支持 {MAX_FILE_SIZE_MB}MB,当前文件大小: {file_size_mb:.1f}MB")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def analyze_media(input_path=None, url=None, media_type=None, confidence_threshold=None, api_url=None, api_key=None,
|
||||
output_level=None):
|
||||
"""调用API进行基础目标检测"""
|
||||
if not input_path and not url:
|
||||
raise ValueError("必须提供本地媒体路径(--input)或网络媒体URL(--url)")
|
||||
|
||||
# 设置参数
|
||||
if media_type:
|
||||
ConstantEnum.DEFAULT_MEDIA_TYPE = media_type
|
||||
if confidence_threshold:
|
||||
ConstantEnum.DEFAULT_CONFIDENCE_THRESHOLD = confidence_threshold
|
||||
|
||||
try:
|
||||
input_path = input_path or url
|
||||
# 携带额外参数
|
||||
params = {}
|
||||
if confidence_threshold:
|
||||
params["confidence_threshold"] = confidence_threshold
|
||||
return skill.get_output_analysis(input_path, params)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
traceback.print_stack()
|
||||
raise Exception(f"API请求失败: {str(e)}")
|
||||
|
||||
|
||||
def show_analyze_list(open_id, start_time=None, end_time=None):
|
||||
# if not open_id:
|
||||
# raise ValueError("必须提供本用户的OpenId/UserId")
|
||||
|
||||
try:
|
||||
output_content = skill.get_output_analysis_list()
|
||||
return output_content
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
traceback.print_stack()
|
||||
raise Exception(f"API请求失败: {str(e)}")
|
||||
|
||||
|
||||
def get_analysis_export_url(request_id=None):
|
||||
"""调用API分析视频"""
|
||||
if not request_id:
|
||||
return ""
|
||||
return ApiEnum.DETAIL_EXPORT_URL + request_id
|
||||
|
||||
|
||||
def format_result(result, output_level="standard", media_type="video", confidence_threshold=0.5):
|
||||
"""格式化输出结果"""
|
||||
category_map = {
|
||||
"person": "人",
|
||||
"car": "车",
|
||||
"non-motor": "非机动车",
|
||||
"cat": "猫",
|
||||
"dog": "狗",
|
||||
"pet": "宠物",
|
||||
"package": "包裹"
|
||||
}
|
||||
|
||||
if output_level == "json":
|
||||
result_id = None
|
||||
if result is not None:
|
||||
result_json = result
|
||||
result_id = result_json.get('id', {})
|
||||
result_json = json.dumps(result_json.get('objectDetectionResponse', {}), ensure_ascii=False, indent=2)
|
||||
else:
|
||||
return "⚠️ 暂无分析结果"
|
||||
return f"""
|
||||
📊 基础目标检测分析结构化结果
|
||||
{result_json}
|
||||
""", result_id
|
||||
elif output_level == "basic":
|
||||
# 精简输出
|
||||
data = result.get('data', {})
|
||||
detection = data.get('detection', {})
|
||||
return f"""
|
||||
📊 基础目标检测报告
|
||||
{'=' * 40}
|
||||
置信度阈值: {confidence_threshold}
|
||||
检测到目标总数: {detection.get('total_count', 0)}
|
||||
"""
|
||||
elif output_level == "standard":
|
||||
# 标准输出
|
||||
data = result.get('data', {})
|
||||
detection = data.get('detection', {})
|
||||
|
||||
objects = "\n".join([
|
||||
f" 📦 {category_map.get(obj.get('category'), obj.get('category'))}: {obj.get('count', 0)} 个,平均置信度: {obj.get('avg_confidence', 0)}"
|
||||
for obj in detection.get('category_stats', [])])
|
||||
|
||||
return f"""
|
||||
📊 基础目标检测分析报告
|
||||
{'=' * 50}
|
||||
⏰ 检测时间: {data.get('detection_time', '未知')}
|
||||
📹 素材类型: {media_type}
|
||||
🎯 置信度阈值: {confidence_threshold}
|
||||
|
||||
🔍 检测结果:
|
||||
检测到目标总数: {detection.get('total_count', 0)}
|
||||
|
||||
各类目标统计:
|
||||
{objects if objects else ' 未检测到目标'}
|
||||
{'=' * 50}
|
||||
> 注:本报告仅供安防管理参考,具体处置请按单位相关规定执行。
|
||||
"""
|
||||
else:
|
||||
# 完整输出(JSON格式)
|
||||
return json.dumps(result, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="基础目标检测工具")
|
||||
parser.add_argument("--input", help="本地视频/图片文件路径")
|
||||
parser.add_argument("--url", help="网络视频/图片的URL地址")
|
||||
parser.add_argument("--media-type", choices=["video", "image"], default=ConstantEnum.DEFAULT__MEDIA_TYPE,
|
||||
help="媒体类型:video(视频流/视频文件), image(图片),默认 video")
|
||||
parser.add_argument("--confidence-threshold", type=float, default=ConstantEnum.DEFAULT__CONFIDENCE_THRESHOLD,
|
||||
help="置信度阈值,低于该分值不输出,默认 0.5")
|
||||
parser.add_argument("--open-id", required=True, help="当前用户的OpenID/UserId/用户名/手机号")
|
||||
parser.add_argument("--list", action='store_true', help="显示基础目标检测列表清单")
|
||||
parser.add_argument("--api-url", help="服务端API地址")
|
||||
parser.add_argument("--api-key", help="API访问密钥(必需)")
|
||||
parser.add_argument("--output", help="结果输出文件路径")
|
||||
parser.add_argument("--detail", choices=["basic", "standard", "json"],
|
||||
default=ConstantEnum.DEFAULT__OUTPUT_LEVEL,
|
||||
help="输出详细程度")
|
||||
parser.add_argument("--export-env-only", action='store_true',
|
||||
help="仅输出 export 命令设置环境变量,不执行分析")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
if args.open_id:
|
||||
# 设置 Python 进程内的环境变量
|
||||
ConstantEnumBase.CURRENT__OPEN_ID = args.open_id
|
||||
|
||||
# 检查必需参数
|
||||
if args.list:
|
||||
open_id = ConstantEnum.CURRENT__OPEN_ID
|
||||
result = show_analyze_list(open_id)
|
||||
print(result)
|
||||
exit(0)
|
||||
|
||||
# 检查必需参数
|
||||
if not args.input and not args.url:
|
||||
print("❌ 错误: 必须提供 --input 或 --url 参数")
|
||||
exit(1)
|
||||
|
||||
print("🔍 正在进行基础目标检测,请稍候...")
|
||||
output_content = analyze_media(
|
||||
input_path=args.input,
|
||||
url=args.url,
|
||||
media_type=args.media_type,
|
||||
confidence_threshold=args.confidence_threshold,
|
||||
api_url=args.api_url,
|
||||
api_key=args.api_key,
|
||||
output_level=args.detail
|
||||
)
|
||||
|
||||
print(output_content)
|
||||
|
||||
# 保存到文件
|
||||
if args.output:
|
||||
with open(args.output, "w", encoding="utf-8") as f:
|
||||
if args.detail == "full":
|
||||
json.dump(result, f, ensure_ascii=False, indent=2)
|
||||
else:
|
||||
f.write(output_content)
|
||||
print(f"✅ 结果已保存到: {args.output}")
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_stack()
|
||||
print(f"❌ 基础目标检测失败: {str(e)}")
|
||||
exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,25 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# 基础目标检测技能配置文件
|
||||
import os
|
||||
import sys
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from skills.smyx_common.scripts.config import ConstantEnum as ConstantEnumBase
|
||||
|
||||
from skills.face_analysis.scripts.config import ApiEnum as ApiEnumParent, ConstantEnum as ConstantEnumParent, \
|
||||
SceneCodeEnum, ApiEnumCommonAiMixin
|
||||
|
||||
|
||||
class ApiEnum(ApiEnumCommonAiMixin, ApiEnumParent):
|
||||
pass
|
||||
|
||||
|
||||
class ConstantEnum(ConstantEnumParent):
|
||||
DEFAULT__MEDIA_TYPE = "video"
|
||||
DEFAULT__CONFIDENCE_THRESHOLD = 0.5
|
||||
|
||||
@classmethod
|
||||
def init(cls, config=None):
|
||||
super().init(config)
|
||||
ConstantEnumParent.DEFAULT__SCENE_CODE = SceneCodeEnum.BASIC_OBJECT_DETECTION_ANALYSIS.value
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1,23 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
|
||||
from .config import ApiEnum, ConstantEnum
|
||||
|
||||
from skills.smyx_common.scripts.api_service import ApiService as ApiServiceBase
|
||||
|
||||
from skills.face_analysis.scripts.skill import Skill as SkillParent
|
||||
from skills.smyx_common.scripts.util import JsonUtil
|
||||
|
||||
|
||||
class Skill(SkillParent):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def get_output_analysis_content_head(self, result=None):
|
||||
return f"📊 基础目标检测分析结构化结果"
|
||||
|
||||
def get_output_analysis_content_foot(self, result):
|
||||
pass
|
||||
|
||||
|
||||
skill = Skill()
|
||||
@ -1,86 +0,0 @@
|
||||
# 中医面诊分析工具 (face-analysis)
|
||||
|
||||
## 技能介绍
|
||||
这是一个基于AI的中医面诊分析技能,可以通过面部视频自动分析健康状况,返回结构化的诊断结果和养生建议。
|
||||
|
||||
## 快速开始
|
||||
### 1. 配置API信息
|
||||
编辑 `scripts/config.py`,设置你的API地址和密钥:
|
||||
```python
|
||||
DEFAULT_API_URL = "https://your-api-server.com/api/v1/face-analysis"
|
||||
DEFAULT_API_KEY = "your-api-key-here"
|
||||
```
|
||||
|
||||
### 2. 分析本地视频
|
||||
```bash
|
||||
python scripts/face_analysis.py --input /path/to/your/video.mp4
|
||||
```
|
||||
|
||||
### 3. 分析网络视频
|
||||
```bash
|
||||
python scripts/face_analysis.py --url https://example.com/video.mp4
|
||||
```
|
||||
|
||||
## 功能特性
|
||||
- ✅ 支持本地MP4视频上传
|
||||
- ✅ 支持网络视频URL分析
|
||||
- ✅ 三种输出详细程度:精简/标准/完整
|
||||
- ✅ 结构化JSON结果输出
|
||||
- ✅ 自动保存结果到文件
|
||||
- ✅ 内置视频格式和大小校验
|
||||
|
||||
## 目录结构
|
||||
```
|
||||
face-analysis/
|
||||
├── SKILL.md # 技能说明文件(系统自动加载)
|
||||
├── README.md # 本说明文件
|
||||
├── scripts/
|
||||
│ ├── face_analysis.py # 主程序
|
||||
│ └── config.py # 配置文件
|
||||
├── references/
|
||||
│ ├── api_doc.md # API接口文档
|
||||
│ ├── tcm_theory.md # 中医面诊理论参考
|
||||
│ └── faq.md # 常见问题
|
||||
└── assets/
|
||||
└── template.json # 返回结果模板
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
### 标准输出
|
||||
```
|
||||
📊 中医面诊分析报告
|
||||
==================================================
|
||||
⏰ 分析时间: 2026-03-10 15:30:00
|
||||
🎯 人脸检测: success (置信度: 95分)
|
||||
|
||||
🔍 诊断结果:
|
||||
整体体质: 平和质
|
||||
脏腑状况:
|
||||
liver: 正常
|
||||
heart: 轻微火旺
|
||||
spleen: 略虚
|
||||
lung: 正常
|
||||
kidney: 正常
|
||||
面色分析: 微黄
|
||||
对应提示: 脾胃功能略弱
|
||||
|
||||
⚠️ 健康警示:
|
||||
⚠️ 注意休息,避免熬夜
|
||||
|
||||
💡 养生建议:
|
||||
💡 饮食清淡,减少辛辣食物摄入
|
||||
💡 保持规律作息,每晚11点前入睡
|
||||
💡 适当进行有氧运动,如散步、太极拳
|
||||
==================================================
|
||||
```
|
||||
|
||||
### 输出到JSON文件
|
||||
```bash
|
||||
python scripts/face_analysis.py --input video.mp4 --detail full --output result.json
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
1. 视频要求:清晰正面面部,光线充足,时长5-30秒为宜
|
||||
2. 支持格式:mp4、avi、mov,最大100MB
|
||||
3. API需要自行部署或接入第三方服务
|
||||
4. 结果仅供参考,不能替代专业医生诊断
|
||||
@ -1,69 +0,0 @@
|
||||
# API接口文档
|
||||
|
||||
## 接口地址
|
||||
`POST https://your-api-server.com/api/v1/face-analysis`
|
||||
|
||||
## 请求头
|
||||
| 字段 | 必选 | 说明 |
|
||||
|------|------|------|
|
||||
| X-API-Key | 是 | API访问密钥 |
|
||||
| Content-Type | 是 | multipart/form-data(文件上传)或 application/json(URL模式) |
|
||||
|
||||
## 请求参数
|
||||
### 1. 文件上传模式
|
||||
| 字段 | 类型 | 必选 | 说明 |
|
||||
|------|------|------|------|
|
||||
| video | file | 是 | MP4视频文件 |
|
||||
| detail_level | string | 否 | 输出详细程度:basic/standard/full,默认standard |
|
||||
|
||||
### 2. URL模式
|
||||
| 字段 | 类型 | 必选 | 说明 |
|
||||
|------|------|------|------|
|
||||
| video_url | string | 是 | 可公开访问的视频URL |
|
||||
| detail_level | string | 否 | 输出详细程度:basic/standard/full,默认standard |
|
||||
|
||||
## 响应格式
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"analysis_time": "2026-03-10 15:30:00",
|
||||
"face_detection": {
|
||||
"status": "success",
|
||||
"face_count": 1,
|
||||
"quality_score": 95
|
||||
},
|
||||
"diagnosis": {
|
||||
"overall_constitution": "平和质",
|
||||
"organ_condition": {
|
||||
"liver": "正常",
|
||||
"heart": "轻微火旺",
|
||||
"spleen": "略虚",
|
||||
"lung": "正常",
|
||||
"kidney": "正常"
|
||||
},
|
||||
"color_analysis": {
|
||||
"complexion": "微黄",
|
||||
"correspondence": "脾胃功能略弱"
|
||||
}
|
||||
},
|
||||
"health_warnings": [
|
||||
"注意休息,避免熬夜"
|
||||
],
|
||||
"suggestions": [
|
||||
"饮食清淡,减少辛辣食物摄入"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 错误码说明
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| 400 | 请求参数错误 |
|
||||
| 401 | API密钥无效 |
|
||||
| 403 | 权限不足 |
|
||||
| 413 | 文件过大 |
|
||||
| 415 | 不支持的文件格式 |
|
||||
| 500 | 服务器内部错误 |
|
||||
@ -1,3 +0,0 @@
|
||||
pydash==8.0.6
|
||||
SQLAlchemy==2.0.46
|
||||
yaml==6.0.3
|
||||
@ -1,52 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from .config import ApiEnum, ConstantEnum
|
||||
from skills.smyx_common.scripts.api_service import ApiService as ApiServiceBase
|
||||
|
||||
|
||||
class ApiService(ApiServiceBase):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.analysis_url = ApiEnum.ANALYSIS_URL
|
||||
|
||||
def analysis_result(self, scene_code=ConstantEnum.DEFAULT__SCENE_CODE, *args, **argss):
|
||||
params = argss.setdefault("params", {})
|
||||
scene_code and params.setdefault("sceneCode", scene_code)
|
||||
return self.http_post(ApiEnum.ANALYSIS_RESULT_URL, *args, **argss)
|
||||
|
||||
def analysis(self, scene_code=ConstantEnum.DEFAULT__SCENE_CODE, *args, **argss):
|
||||
params = argss.setdefault("params", {})
|
||||
options = {
|
||||
"dataAsParams": True
|
||||
}
|
||||
scene_code and params.setdefault("sceneCode", scene_code)
|
||||
params.setdefault("appCategory", ConstantEnum.DEFAULT__APP_CATEGORY)
|
||||
return self.http_post(self.analysis_url, options=options, *args, **argss)
|
||||
|
||||
def page(self, pageNum=None, pageSize=None, *args, **argss):
|
||||
data = argss.setdefault("data", {})
|
||||
ConstantEnum.DEFAULT__SCENE_CODE and data.setdefault("sceneCode", ConstantEnum.DEFAULT__SCENE_CODE)
|
||||
data.setdefault("orderBy", {
|
||||
"fieldName": "createTime",
|
||||
"isAsc": False
|
||||
})
|
||||
return super().page(ApiEnum.PAGE_URL, pageNum, pageSize, *args, **argss)
|
||||
|
||||
def list(self, *args, **argss):
|
||||
return super().list(None, *args, **argss)
|
||||
|
||||
def add(self, item: dict):
|
||||
return super().add(ApiEnum.ADD_URL, item)
|
||||
|
||||
def edit(self, item: dict):
|
||||
return super().edit(ApiEnum.EDIT_URL, item)
|
||||
|
||||
def delete(self, cameraSn):
|
||||
data = {
|
||||
"cameraSn": cameraSn
|
||||
}
|
||||
return super().delete(ApiEnum.DELETE_URL, data, options={"dataAsParams": True})
|
||||
@ -1,40 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# 中医面诊分析工具配置文件
|
||||
import os
|
||||
import sys
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from skills.smyx_common.scripts.config import ApiEnum as ApiEnumBase, ConstantEnum as ConstantEnumBase
|
||||
|
||||
SceneCodeEnum = ConstantEnumBase.SceneCodeEnum
|
||||
|
||||
|
||||
class ApiEnum(ApiEnumBase):
|
||||
ANALYSIS_URL = "/web/health-analysis/v2/start-health-analysis"
|
||||
|
||||
ANALYSIS_RESULT_URL = "/web/health-analysis/get-health-analysis-result"
|
||||
|
||||
PAGE_URL = "/web/health-analysis/page-health-analysis-result"
|
||||
|
||||
DETAIL_EXPORT_URL = ApiEnumBase.BASE_URL_HEALTH + "/health/order/api/getReportDetailExport?id="
|
||||
|
||||
@classmethod
|
||||
def init(cls, config=None):
|
||||
super().init(config)
|
||||
|
||||
|
||||
class ApiEnumCommonAiMixin:
|
||||
|
||||
@classmethod
|
||||
def init(cls, config=None):
|
||||
parent = super()
|
||||
if hasattr(parent, "init"):
|
||||
parent.init(config)
|
||||
ApiEnum.ANALYSIS_URL = "/web/ai-analysis/v2/start-common-ai-analysis"
|
||||
ApiEnum.ANALYSIS_RESULT_URL = "/web/ai-analysis/get-common-ai-analysis-result"
|
||||
ApiEnum.PAGE_URL = "/web/ai-analysis/page-common-ai-analysis-result"
|
||||
|
||||
|
||||
class ConstantEnum(ConstantEnumBase):
|
||||
DEFAULT__APP_CATEGORY = "PEI_NI_AN"
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1,205 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import os
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
parent_dir = os.path.dirname(os.path.dirname(os.path.dirname(current_dir)))
|
||||
sys.path.insert(0, parent_dir)
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import mimetypes
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
import sys
|
||||
import os
|
||||
|
||||
from .config import *
|
||||
|
||||
from .skill import skill
|
||||
|
||||
# import_path_common()
|
||||
from skills.smyx_common.scripts.util import RequestUtil
|
||||
|
||||
# 从config导入常量
|
||||
SUPPORTED_FORMATS = ConstantEnum.SUPPORTED_FORMATS
|
||||
MAX_FILE_SIZE_MB = ConstantEnum.MAX_FILE_SIZE_MB
|
||||
|
||||
|
||||
def validate_file(file_path):
|
||||
"""验证输入文件是否合法"""
|
||||
if not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f"文件不存在: {file_path}")
|
||||
|
||||
if not os.access(file_path, os.R_OK):
|
||||
raise PermissionError(f"文件没有读权限: {file_path}")
|
||||
|
||||
ext = os.path.splitext(file_path)[1].lower()[1:]
|
||||
if ext not in SUPPORTED_FORMATS:
|
||||
raise ValueError(f"不支持的文件格式,支持的格式: {', '.join(SUPPORTED_FORMATS)}")
|
||||
|
||||
file_size_mb = os.path.getsize(file_path) / (1024 * 1024)
|
||||
if file_size_mb > MAX_FILE_SIZE_MB:
|
||||
raise ValueError(f"文件过大,最大支持 {MAX_FILE_SIZE_MB}MB,当前文件大小: {file_size_mb:.1f}MB")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def analyze_video(input_path=None, url=None, api_url=None, api_key=None, output_level=None):
|
||||
"""调用API分析视频"""
|
||||
if not input_path and not url:
|
||||
raise ValueError("必须提供本地视频路径(--input)或网络视频URL(--url)")
|
||||
try:
|
||||
input_path = input_path or url
|
||||
return skill.get_output_analysis(input_path)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
traceback.print_stack()
|
||||
raise Exception(f"API请求失败: {str(e)}")
|
||||
|
||||
|
||||
def show_analyze_list(open_id, start_time=None, end_time=None):
|
||||
# if not open_id:
|
||||
# raise ValueError("必须提供本用户的OpenId/UserId")
|
||||
|
||||
try:
|
||||
output_content = skill.get_output_analysis_list()
|
||||
return output_content
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
traceback.print_stack()
|
||||
raise Exception(f"API请求失败: {str(e)}")
|
||||
|
||||
|
||||
def get_analysis_export_url(request_id=None):
|
||||
"""调用API分析视频"""
|
||||
if not request_id:
|
||||
return ""
|
||||
return ApiEnum.DETAIL_EXPORT_URL + request_id
|
||||
|
||||
|
||||
def format_result(result, output_level="standard"):
|
||||
"""格式化输出结果"""
|
||||
if output_level == "json":
|
||||
result_id = None
|
||||
# if result.get('success'):
|
||||
if result is not None:
|
||||
result_json = result
|
||||
result_id = result_json.get('id', {})
|
||||
result_json = json.dumps(result_json.get('faceAnalysisResponse', {}), ensure_ascii=False, indent=2)
|
||||
else:
|
||||
# result_json = json.dumps(result, ensure_ascii=False, indent=2)
|
||||
return "⚠️ 暂无分析结果"
|
||||
return f"""
|
||||
📊 面诊分析结构化结果
|
||||
{result_json}
|
||||
""", result_id
|
||||
elif output_level == "basic":
|
||||
# 精简输出
|
||||
data = result.get('data', {})
|
||||
diagnosis = data.get('diagnosis', {})
|
||||
return f"""
|
||||
📊 面诊分析结果
|
||||
{'=' * 40}
|
||||
整体体质: {diagnosis.get('overall_constitution', '未知')}
|
||||
主要状况: {', '.join([f'{k}: {v}' for k, v in diagnosis.get('organ_condition', {}).items() if v != '正常'])}
|
||||
健康提示: {data.get('health_warnings', ['无特殊警示'])[0] if data.get('health_warnings') else '无特殊警示'}
|
||||
"""
|
||||
elif output_level == "standard":
|
||||
# 标准输出
|
||||
data = result.get('data', {})
|
||||
diagnosis = data.get('diagnosis', {})
|
||||
face_detection = data.get('face_detection', {})
|
||||
|
||||
organ_status = "\n".join([f" {k}: {v}" for k, v in diagnosis.get('organ_condition', {}).items()])
|
||||
warnings = "\n".join([f" ⚠️ {item}" for item in data.get('health_warnings', [])])
|
||||
suggestions = "\n".join([f" 💡 {item}" for item in data.get('suggestions', [])])
|
||||
|
||||
return f"""
|
||||
📊 中医面诊分析报告
|
||||
{'=' * 50}
|
||||
⏰ 分析时间: {data.get('analysis_time', '未知')}
|
||||
🎯 人脸检测: {face_detection.get('status', '未知')} (置信度: {face_detection.get('quality_score', 0)}分)
|
||||
|
||||
🔍 诊断结果:
|
||||
整体体质: {diagnosis.get('overall_constitution', '未知')}
|
||||
脏腑状况:
|
||||
{organ_status}
|
||||
面色分析: {diagnosis.get('color_analysis', {}).get('complexion', '未知')}
|
||||
对应提示: {diagnosis.get('color_analysis', {}).get('correspondence', '未知')}
|
||||
|
||||
⚠️ 健康警示:
|
||||
{warnings}
|
||||
|
||||
💡 养生建议:
|
||||
{suggestions}
|
||||
{'=' * 50}
|
||||
"""
|
||||
else:
|
||||
# 完整输出(JSON格式)
|
||||
return json.dumps(result, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="中医面诊分析工具")
|
||||
parser.add_argument("--input", help="本地MP4视频文件路径")
|
||||
parser.add_argument("--url", help="网络视频MP4的URL地址")
|
||||
parser.add_argument("--open-id", required=True, help="当前用户的OpenID/UserId/用户名/手机号")
|
||||
parser.add_argument("--list", action='store_true', help="显示面诊视频历史列表清单")
|
||||
parser.add_argument("--api-url", help="服务端API地址")
|
||||
parser.add_argument("--api-key", help="API访问密钥(必需)")
|
||||
parser.add_argument("--output", help="结果输出文件路径")
|
||||
parser.add_argument("--detail", choices=["basic", "standard", "json"],
|
||||
default=ConstantEnum.DEFAULT__OUTPUT_LEVEL,
|
||||
help="输出详细程度")
|
||||
parser.add_argument("--export-env-only", action='store_true',
|
||||
help="仅输出 export 命令设置环境变量,不执行分析")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
if args.open_id:
|
||||
ConstantEnumBase.CURRENT__OPEN_ID = args.open_id
|
||||
|
||||
# 检查必需参数
|
||||
if args.list:
|
||||
open_id = ConstantEnum.CURRENT__OPEN_ID
|
||||
result = show_analyze_list(open_id)
|
||||
print(result)
|
||||
exit(0)
|
||||
|
||||
# 检查必需参数
|
||||
if not args.input and not args.url:
|
||||
print("❌ 错误: 必须提供 --input 或 --url 参数")
|
||||
exit(1)
|
||||
|
||||
print("🔍 正在分析面诊视频,请稍候...")
|
||||
output_content = analyze_video(
|
||||
input_path=args.input,
|
||||
url=args.url,
|
||||
api_url=args.api_url,
|
||||
api_key=args.api_key,
|
||||
output_level=args.detail
|
||||
)
|
||||
|
||||
print(output_content)
|
||||
|
||||
# 保存到文件
|
||||
if args.output:
|
||||
with open(args.output, "w", encoding="utf-8") as f:
|
||||
if args.detail == "full":
|
||||
json.dump(result, f, ensure_ascii=False, indent=2)
|
||||
else:
|
||||
f.write(output_content)
|
||||
print(f"✅ 结果已保存到: {args.output}")
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_stack()
|
||||
print(f"❌ 面诊分析失败: {str(e)}")
|
||||
exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,267 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import datetime
|
||||
import os
|
||||
import sys
|
||||
|
||||
from .config import ApiEnum, ConstantEnum
|
||||
|
||||
from .api_service import ApiService
|
||||
|
||||
from skills.smyx_common.scripts.util import CommonUtil, JsonUtil
|
||||
from skills.smyx_common.scripts.config import ApiEnum as ApiEnumBase
|
||||
from skills.smyx_common.scripts.base import BaseSkill
|
||||
from skills.smyx_common.scripts.api_service import ApiService as ApiServiceBase
|
||||
|
||||
|
||||
class Skill(BaseSkill, ApiService):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def get_output_analysis_content_body(self, result=None):
|
||||
result_json = result
|
||||
|
||||
result_json_pure_text = result_json.get("pureText")
|
||||
if result_json_pure_text:
|
||||
result_json = JsonUtil.parse(result_json_pure_text, result_json_pure_text)
|
||||
|
||||
result_json_common_ai_response = result_json.get("commonAiResponse")
|
||||
if result_json_common_ai_response:
|
||||
result_json = result_json_common_ai_response
|
||||
|
||||
result_json_health_ai_response = result_json.get("healthAiResponse")
|
||||
if result_json_health_ai_response:
|
||||
result_json = result_json_health_ai_response
|
||||
|
||||
result_json = JsonUtil.stringify(result_json, result_json)
|
||||
return result_json
|
||||
|
||||
def get_output_analysis_content_head(self, result=None):
|
||||
return f"📊 面诊分析结构化结果"
|
||||
|
||||
def get_output_analysis_content_foot(self, result):
|
||||
result_id = result.get('id', {})
|
||||
output_content_export_url = ApiEnum.DETAIL_EXPORT_URL + result_id
|
||||
return f"🔗 获取报告导出图片链接: {output_content_export_url}"
|
||||
|
||||
def get_output_analysis_content(self, result):
|
||||
if result is not None:
|
||||
output_content = self.get_output_analysis_content_body(result) or ""
|
||||
output_content_head = self.get_output_analysis_content_head(result)
|
||||
output_content_foot = self.get_output_analysis_content_foot(result)
|
||||
# d
|
||||
if output_content_head:
|
||||
output_content = f"""
|
||||
{output_content_head}
|
||||
""" + output_content
|
||||
if output_content_foot:
|
||||
output_content += f"""
|
||||
{output_content_foot}
|
||||
"""
|
||||
else:
|
||||
output_content = "⚠️ 暂无分析结果"
|
||||
return output_content
|
||||
|
||||
def get_output_analysis(self, input_path, params={}):
|
||||
response = self.get_analysis(
|
||||
input_path, params
|
||||
)
|
||||
|
||||
def _analysis_result():
|
||||
return self.analysis_result(
|
||||
data=response
|
||||
)
|
||||
|
||||
new_response = CommonUtil.polling(_analysis_result,
|
||||
check_condition=lambda res: res.get('needPageRefresh') is False, interval=5,
|
||||
max_attempts=24)
|
||||
|
||||
output_content = self.get_output_analysis_content(new_response)
|
||||
return output_content
|
||||
|
||||
def get_analysis(self, input_path, params={}):
|
||||
import mimetypes
|
||||
|
||||
def _validate_file(file_path):
|
||||
"""验证输入文件是否合法"""
|
||||
if not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f"文件不存在: {file_path}")
|
||||
|
||||
if not os.access(file_path, os.R_OK):
|
||||
raise PermissionError(f"文件没有读权限: {file_path}")
|
||||
|
||||
ext = os.path.splitext(file_path)[1].lower()[1:]
|
||||
if ext not in ConstantEnum.SUPPORTED_FORMATS:
|
||||
raise ValueError(f"不支持的文件格式,支持的格式: {', '.join(ConstantEnum.SUPPORTED_FORMATS)}")
|
||||
|
||||
file_size_mb = os.path.getsize(file_path) / (1024 * 1024)
|
||||
if file_size_mb > ConstantEnum.MAX_FILE_SIZE_MB:
|
||||
raise ValueError(
|
||||
f"文件过大,最大支持 {ConstantEnum.MAX_FILE_SIZE_MB}MB,当前文件大小: {file_size_mb:.1f}MB")
|
||||
|
||||
return True
|
||||
|
||||
files = None
|
||||
|
||||
if not input_path:
|
||||
raise ValueError("必须提供本地视频路径(--input)或网络视频URL(--url)")
|
||||
|
||||
if (input_path.startswith("http://") or input_path.startswith("https://")):
|
||||
params.update({
|
||||
"videoUrl": input_path
|
||||
})
|
||||
else:
|
||||
_validate_file(input_path)
|
||||
|
||||
# 自动检测 MIME 类型
|
||||
mime_type, _ = mimetypes.guess_type(input_path)
|
||||
if mime_type is None:
|
||||
mime_type = 'application/octet-stream'
|
||||
|
||||
# 读取文件内容
|
||||
with open(input_path, 'rb') as f:
|
||||
file_content = f.read()
|
||||
|
||||
# 构建 multipart/form-data 格式的请求
|
||||
files = {
|
||||
'file': (os.path.basename(input_path), file_content, mime_type)
|
||||
}
|
||||
|
||||
response = self.analysis(
|
||||
params=params,
|
||||
files=files
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
def get_output_analysis_list(self, pageNum=None, pageSize=None, *args, **argss):
|
||||
"""获取面诊报告清单
|
||||
优化规则:只要API服务接口返回面诊报告清单,直接输出API返回的结果,
|
||||
无需汇总上下文中的面诊分析报告,以接口返回为准
|
||||
"""
|
||||
|
||||
def _get_analysis_export_url(request_id=None):
|
||||
if not request_id:
|
||||
return ""
|
||||
return ApiEnum.DETAIL_EXPORT_URL + request_id
|
||||
|
||||
response = self.page(pageNum, pageSize, *args, **argss)
|
||||
|
||||
if response:
|
||||
for item in response:
|
||||
if item.get("commonAiResponse") or item.get("healthAiResponse"):
|
||||
item["reportImageUrl"] = _get_analysis_export_url(item.get("id"))
|
||||
|
||||
response_text = JsonUtil.stringify(response)
|
||||
|
||||
if response_text:
|
||||
return f"""📊 分析报告记录列表(结构化结果)"
|
||||
{response_text}
|
||||
"""
|
||||
else:
|
||||
return "⚠️ 暂无分析报告记录"
|
||||
|
||||
def __get_output_analysis_list(self, pageNum=None, pageSize=None, *args, **argss):
|
||||
"""获取面诊报告清单
|
||||
优化规则:只要API服务接口返回面诊报告清单,直接输出API返回的结果,
|
||||
无需汇总上下文中的面诊分析报告,以接口返回为准
|
||||
"""
|
||||
|
||||
def _get_analysis_export_url(request_id=None):
|
||||
if not request_id:
|
||||
return ""
|
||||
return ApiEnum.DETAIL_EXPORT_URL + request_id
|
||||
|
||||
# open_id 仅用于本地识别,不传给API - 参数已经在argss中,page方法会正确处理
|
||||
open_id = argss.pop('open_id', None)
|
||||
# if not open_id:
|
||||
# return "⚠️ 错误:缺少 open_id 参数"
|
||||
|
||||
# 获取总页数,然后循环获取所有页
|
||||
output_all = ""
|
||||
# 先获取第一页来获取总页数
|
||||
# page 方法在基类中已经处理过,我们需要兼容两种返回结果:
|
||||
# 1. 完整响应:{"success": true, "data": {"records": [...], "total": ...}}
|
||||
# 2. 已经提取好的数据:直接返回 data 对象或 records 列表
|
||||
response = self.page(pageNum or 1, pageSize or 30, *args, **argss)
|
||||
|
||||
if response is None:
|
||||
return "⚠️ 获取报告列表失败:response is None"
|
||||
|
||||
# 兼容处理:不同版本的基类返回不同格式
|
||||
if isinstance(response, list):
|
||||
# 基类直接返回了 records 列表,无法获取分页信息,直接使用
|
||||
records = response
|
||||
total = len(records)
|
||||
pages = 1
|
||||
elif isinstance(response, dict):
|
||||
# 完整响应格式
|
||||
if not response.get('success'):
|
||||
error_msg = response.get('errorMsg', '未知错误')
|
||||
return f"⚠️ 获取报告列表失败:{error_msg}"
|
||||
data = response.get('data', {})
|
||||
if not data or not isinstance(data, dict):
|
||||
return "⚠️ 获取报告列表失败:数据格式错误"
|
||||
total = data.get('total', 0)
|
||||
pages = data.get('pages', 1)
|
||||
records = data.get('records', [])
|
||||
else:
|
||||
return f"⚠️ 获取报告列表失败:response type={type(response)}"
|
||||
|
||||
if not records:
|
||||
return "⚠️ 暂无面诊分析报告记录"
|
||||
|
||||
output_all = f"📋 历史面诊分析报告清单(共 {total} 份)\n\n"
|
||||
output_all += "| 报告名称 | 分析时间 | 体质判断 | 点击查看 |\n"
|
||||
output_all += "|----------|----------|----------|----------|\n"
|
||||
|
||||
# 处理第一页
|
||||
for item in records:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
report_id = item.get('id', '')
|
||||
create_time = item.get('createTimeString', '未知时间')
|
||||
# 提取体质判断 - 优先从 healthAiResponse 获取,如果没有再从 faceAnalysisResponse 获取
|
||||
health_ai = item.get('healthAiResponse', {}) or {}
|
||||
if health_ai:
|
||||
health_assessment = health_ai.get('healthAssessment', {}) or {}
|
||||
subject = health_assessment.get('subject', '未知')
|
||||
else:
|
||||
face_ai = item.get('faceAnalysisResponse', {}) or {}
|
||||
health_assessment = face_ai.get('healthAssessment', {}) or {}
|
||||
subject = health_assessment.get('subject', '未知')
|
||||
report_name = f"面诊分析报告-{report_id}"
|
||||
report_url = _get_analysis_export_url(report_id)
|
||||
output_all += f"| {report_name} | {create_time} | {subject} | [🔗 查看报告]({report_url}) |\n"
|
||||
|
||||
# 处理剩余页
|
||||
for current_page in range(2, pages + 1):
|
||||
response = self.page(current_page, 30, *args, **argss)
|
||||
if not response or not isinstance(response, dict) or not response.get('success'):
|
||||
continue
|
||||
data = response.get('data', {})
|
||||
if not data or not isinstance(data, dict):
|
||||
continue
|
||||
records = data.get('records', [])
|
||||
for item in records:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
report_id = item.get('id', '')
|
||||
create_time = item.get('createTimeString', '未知时间')
|
||||
# 提取体质判断 - 优先从 healthAiResponse 获取,如果没有再从 faceAnalysisResponse 获取
|
||||
health_ai = item.get('healthAiResponse', {}) or {}
|
||||
if health_ai:
|
||||
health_assessment = health_ai.get('healthAssessment', {}) or {}
|
||||
subject = health_assessment.get('subject', '未知')
|
||||
else:
|
||||
face_ai = item.get('faceAnalysisResponse', {}) or {}
|
||||
health_assessment = face_ai.get('healthAssessment', {}) or {}
|
||||
subject = health_assessment.get('subject', '未知')
|
||||
report_name = f"面诊分析报告-{report_id}"
|
||||
report_url = _get_analysis_export_url(report_id)
|
||||
output_all += f"| {report_name} | {create_time} | {subject} | [🔗 查看报告]({report_url}) |\n"
|
||||
|
||||
output_all += "\n> 注:面诊分析结果仅供健康参考,不能替代专业医疗诊断。"
|
||||
return output_all
|
||||
|
||||
|
||||
skill = Skill()
|
||||
@ -1,127 +0,0 @@
|
||||
altgraph==0.17.5
|
||||
annotated-doc==0.0.4
|
||||
annotated-types==0.7.0
|
||||
anyio==4.12.1
|
||||
APScheduler==3.11.2
|
||||
astroid==3.1.0
|
||||
Authlib==1.6.6
|
||||
blinker==1.4
|
||||
cachetools==6.2.6
|
||||
certifi==2026.1.4
|
||||
cffi==2.0.0
|
||||
chardet==5.2.0
|
||||
charset-normalizer==3.4.4
|
||||
click==8.3.1
|
||||
coverage==7.13.2
|
||||
coze-workload-identity==0.1.7
|
||||
cozeloop==0.1.19
|
||||
cryptography==3.4.8
|
||||
Cython==3.2.4
|
||||
dbus-python==1.2.18
|
||||
dill==0.4.1
|
||||
distro==1.7.0
|
||||
distro-info==1.1+ubuntu0.2
|
||||
et_xmlfile==2.0.0
|
||||
fastapi==0.121.2
|
||||
gitdb==4.0.12
|
||||
gitignore_parser==0.1.13
|
||||
GitPython==3.1.45
|
||||
greenlet==3.3.1
|
||||
h11==0.16.0
|
||||
httpcore==1.0.9
|
||||
httplib2==0.20.2
|
||||
httpx==0.28.1
|
||||
httpx-ws==0.8.2
|
||||
idna==3.11
|
||||
importlib-metadata==4.6.4
|
||||
inflect==7.5.0
|
||||
iniconfig==2.3.0
|
||||
isort==5.13.2
|
||||
jeepney==0.7.1
|
||||
Jinja2==3.1.6
|
||||
jiter==0.12.0
|
||||
jsonpatch==1.33
|
||||
jsonpointer==3.0.0
|
||||
keyring==23.5.0
|
||||
langchain==1.0.3
|
||||
langchain-core==1.0.2
|
||||
langchain-openai==1.0.1
|
||||
langgraph==1.0.2
|
||||
langgraph-checkpoint==3.0.0
|
||||
langgraph-prebuilt==1.0.2
|
||||
langgraph-sdk==0.2.9
|
||||
langsmith==0.4.39
|
||||
launchpadlib==1.10.16
|
||||
lazr.restfulclient==0.14.4
|
||||
lazr.uri==1.0.6
|
||||
markdown-it-py==4.0.0
|
||||
MarkupSafe==3.0.3
|
||||
mccabe==0.7.0
|
||||
mdurl==0.1.2
|
||||
more-itertools==8.10.0
|
||||
numpy==2.4.1
|
||||
oauthlib==3.2.0
|
||||
openai==2.16.0
|
||||
openpyxl==3.1.5
|
||||
orjson==3.11.5
|
||||
ormsgpack==1.12.2
|
||||
packaging==25.0
|
||||
pandas==2.3.3
|
||||
platformdirs==4.5.1
|
||||
pluggy==1.6.0
|
||||
psutil==7.1.3
|
||||
psycopg2-binary==2.9.11
|
||||
pycparser==3.0
|
||||
pydantic==2.12.4
|
||||
pydantic_core==2.41.5
|
||||
pydash==8.0.6
|
||||
Pygments==2.19.2
|
||||
PyGObject==3.42.1
|
||||
pyinstaller==6.18.0
|
||||
pyinstaller-hooks-contrib==2026.0
|
||||
PyJWT==2.10.1
|
||||
pylint==3.1.0
|
||||
PyMySQL==1.1.2
|
||||
pyparsing==2.4.7
|
||||
pytest==9.0.1
|
||||
pytest-asyncio==1.3.0
|
||||
pytest-cov==7.0.0
|
||||
pytest-mock==3.15.1
|
||||
python-apt==2.4.0+ubuntu4.1
|
||||
python-dateutil==2.9.0.post0
|
||||
pytz==2025.2
|
||||
PyYAML==6.0.3
|
||||
regex==2026.1.15
|
||||
requests==2.32.5
|
||||
requests-toolbelt==1.0.0
|
||||
rich==14.2.0
|
||||
SecretStorage==3.3.1
|
||||
setuptools==80.9.0
|
||||
six==1.16.0
|
||||
smmap==5.0.2
|
||||
sniffio==1.3.1
|
||||
sqlacodegen==3.2.0
|
||||
SQLAlchemy==2.0.46
|
||||
starlette==0.49.3
|
||||
supervisor==4.2.1
|
||||
tenacity==9.1.2
|
||||
tiktoken==0.12.0
|
||||
tomlkit==0.14.0
|
||||
tqdm==4.67.1
|
||||
typeguard==4.4.4
|
||||
typing-inspection==0.4.2
|
||||
typing_extensions==4.15.0
|
||||
tzdata==2025.3
|
||||
tzlocal==5.3.1
|
||||
unattended-upgrades==0.1
|
||||
urllib3==2.6.3
|
||||
uvicorn==0.38.0
|
||||
wadllib==1.3.6
|
||||
watchdog==6.0.0
|
||||
websockets==15.0.1
|
||||
wheel==0.45.1
|
||||
wsproto==1.3.2
|
||||
xlrd==2.0.2
|
||||
xxhash==3.6.0
|
||||
zipp==1.0.0
|
||||
zstandard==0.25.0
|
||||
@ -1,8 +0,0 @@
|
||||
from .util import RequestUtil, CommonUtil, DatetimeUtil
|
||||
from .base import *
|
||||
|
||||
__all__ = [
|
||||
'RequestUtil',
|
||||
'CommonUtil',
|
||||
'BaseUtil'
|
||||
]
|
||||
@ -1,96 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from .config import ApiEnum
|
||||
|
||||
from .base import BaseApiService
|
||||
from .util import RequestUtil, CommonUtil
|
||||
|
||||
|
||||
class ApiService(BaseApiService):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def get_download_url(self, tosKey, expireSeconds=3600):
|
||||
return RequestUtil.http_post(
|
||||
ApiEnum.GET_DOWNLOAD_URL__URL,
|
||||
params={
|
||||
"tosKey": tosKey,
|
||||
"expireSeconds": expireSeconds * 24
|
||||
}
|
||||
)
|
||||
|
||||
def page(self, url, pageNum=None, pageSize=None, *args, **argss):
|
||||
data = args[0] if len(args) > 0 else argss.get('data') if argss.get('data') is not None else {}
|
||||
if pageNum is None:
|
||||
pageNum = 1
|
||||
if pageSize is None:
|
||||
pageSize = ApiEnum.DEFAULT__PAGE_SIZE
|
||||
paramsPage = {
|
||||
'pageNum': int(pageNum),
|
||||
'pageSize': int(pageSize)
|
||||
}
|
||||
data.update({
|
||||
"page": paramsPage
|
||||
})
|
||||
if not CommonUtil.is_empty(data):
|
||||
if (len(args) == 0):
|
||||
argss.setdefault("data", data)
|
||||
response = RequestUtil.http_post(
|
||||
url,
|
||||
*args, **argss
|
||||
)
|
||||
return response
|
||||
|
||||
def list(self, url=None, *args, **argss):
|
||||
if url is not None:
|
||||
argss["url"] = url
|
||||
return self.page(1, ApiEnum.DEFAULT__PAGE_SIZE_MAX, *args, **argss)
|
||||
|
||||
def add(self, url=None, *args, **argss):
|
||||
response = RequestUtil.http_post(
|
||||
url,
|
||||
*args, **argss
|
||||
)
|
||||
return response
|
||||
|
||||
def edit(self, url=None, *args, **argss):
|
||||
response = RequestUtil.http_post(
|
||||
url,
|
||||
*args, **argss
|
||||
)
|
||||
return response
|
||||
|
||||
def delete(self, url=None, *args, **argss):
|
||||
response = RequestUtil.http_post(
|
||||
url,
|
||||
*args, **argss
|
||||
)
|
||||
return response
|
||||
|
||||
def http_post(self, url=None, *args, **argss):
|
||||
return RequestUtil.http_post(
|
||||
url,
|
||||
*args, **argss
|
||||
)
|
||||
|
||||
def http_put(self, url=None, *args, **argss):
|
||||
return RequestUtil.http_put(
|
||||
url,
|
||||
*args, **argss
|
||||
)
|
||||
|
||||
def http_get(self, url=None, *args, **argss):
|
||||
return RequestUtil.http_get(
|
||||
url,
|
||||
*args, **argss
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
def http_delete(self, url=None, *args, **argss):
|
||||
return RequestUtil.http_delete(
|
||||
url,
|
||||
*args, **argss
|
||||
)
|
||||
return response
|
||||
@ -1,33 +0,0 @@
|
||||
class BaseUtil:
|
||||
pass
|
||||
|
||||
|
||||
class BaseMixin:
|
||||
pass
|
||||
|
||||
|
||||
class BaseDao:
|
||||
pass
|
||||
|
||||
|
||||
class BaseService:
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
|
||||
class BaseApiService(BaseService):
|
||||
INSTANCE = None
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
if cls.INSTANCE is None:
|
||||
cls.INSTANCE = cls()
|
||||
return cls.INSTANCE
|
||||
|
||||
|
||||
class BaseSkill:
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@ -1,7 +0,0 @@
|
||||
ApiEnum:
|
||||
base-url-open-api: "http://192.168.1.234:9601/smyx-open-api"
|
||||
base-url-open-h5: "http://192.168.1.234:4100"
|
||||
base-url-health: "http://192.168.1.234:8080/jeecg-boot"
|
||||
|
||||
ConstantEnum:
|
||||
is-debug: true
|
||||
@ -1,7 +0,0 @@
|
||||
ApiEnum:
|
||||
base-url-open-api: "https://livemonitortest.lifeemergence.com/smyx-open-api"
|
||||
base-url-open-h5: "http://livemonitortest.lifeemergence.com"
|
||||
base-url-health: "https://healthtest.lifeemergence.com/jeecg-boot"
|
||||
|
||||
ConstantEnum:
|
||||
is-debug: true
|
||||
@ -1,235 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
from enum import Enum
|
||||
from typing import Dict
|
||||
import inspect
|
||||
|
||||
import yaml
|
||||
import platform
|
||||
|
||||
|
||||
class YamlUtil:
|
||||
|
||||
@staticmethod
|
||||
def load(path, config: Dict = {}) -> Dict:
|
||||
try:
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(config, f, default_flow_style=False, allow_unicode=True)
|
||||
return config
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
config = yaml.safe_load(f) or {}
|
||||
for key, value in config.items():
|
||||
if key not in config:
|
||||
config[key] = value
|
||||
return config
|
||||
except:
|
||||
pass
|
||||
return config
|
||||
|
||||
@staticmethod
|
||||
def save(path, config: Dict) -> Dict:
|
||||
try:
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(config, f, default_flow_style=False, allow_unicode=True)
|
||||
except:
|
||||
pass
|
||||
return config
|
||||
|
||||
|
||||
class BaseEnum:
|
||||
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
super().__init_subclass__(**kwargs)
|
||||
clsModule = cls.__module__
|
||||
cls_path = inspect.getfile(cls)
|
||||
clsFullName = f"{cls.__module__}.{cls.__name__}"
|
||||
cls_dirpath = os.path.dirname(cls_path) # .../src
|
||||
clsModulePath = clsModule.replace(".", "\\")
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__)) # .../src
|
||||
config_path = os.path.join(cls_dirpath, "config.yaml")
|
||||
config = YamlUtil.load(config_path)
|
||||
cls.init(config)
|
||||
env = config.get("env")
|
||||
if env:
|
||||
env_config_path = os.path.join(cls_dirpath, f"config-{env}.yaml")
|
||||
env_config = YamlUtil.load(env_config_path)
|
||||
cls.init(env_config)
|
||||
|
||||
@classmethod
|
||||
def init(cls, config=None):
|
||||
clsName = cls.__name__
|
||||
clsConfig = config and config.get(clsName)
|
||||
if clsConfig:
|
||||
for config_key, config_value in clsConfig.items():
|
||||
new_config_key = config_key = config_key.upper().replace("-", "_")
|
||||
if hasattr(cls, new_config_key):
|
||||
setattr(cls, new_config_key, config_value)
|
||||
|
||||
|
||||
class ApiEnum(BaseEnum):
|
||||
API_KEY = None
|
||||
|
||||
API_SECRET_KEY = None
|
||||
|
||||
DATABASE_URL = ""
|
||||
|
||||
BASE_URL_OPEN_API = ""
|
||||
|
||||
BASE_URL_OPEN_H5 = ""
|
||||
|
||||
BASE_URL_HEALTH = ""
|
||||
|
||||
OPEN_TOKEN = ""
|
||||
|
||||
TOKEN = ""
|
||||
|
||||
DEFAULT__REQUEST_TIMEOUT = 120
|
||||
|
||||
DEFAULT__PAGE_SIZE = 5
|
||||
|
||||
DEFAULT__PAGE_SIZE_MAX = 65536
|
||||
|
||||
GET_DOWNLOAD_URL__URL = BASE_URL_OPEN_API + "/api/tos/get-download-url"
|
||||
|
||||
|
||||
class ConstantEnum(BaseEnum):
|
||||
class SourceEnum(Enum):
|
||||
ARK_CLAW = "ARK_CLAW"
|
||||
JVS_CLAW = "JVS_CLAW"
|
||||
LIGHT_CLAW = "LIGHT_CLAW"
|
||||
WUHONG = "WUHONG"
|
||||
COZE = "COZE"
|
||||
SKILL_HUB = "SKILL_HUB"
|
||||
CLAW_HUB = "CLAW_HUB"
|
||||
FEISHU = "FEISHU"
|
||||
DINGTALK = "DINGTALK"
|
||||
WEIXIN = "WEIXIN"
|
||||
YUANBAO = "YUANBAO"
|
||||
WECOM = "WECOM"
|
||||
QQBOT = "QQBOT"
|
||||
|
||||
APP__ID = ""
|
||||
|
||||
APP__SOURCE = SourceEnum.CLAW_HUB.value
|
||||
|
||||
IS_DEBUG = False
|
||||
|
||||
CURRENT__OPEN_ID = ""
|
||||
|
||||
CURRENT__USER_NAME = ""
|
||||
|
||||
CURRENT__TENTANT_CODE = ""
|
||||
|
||||
FEISHU_APP__ID = ""
|
||||
|
||||
FEISHU_APP__SECRET = ""
|
||||
|
||||
FEISHU_APP__RECEIVE_ID = ""
|
||||
|
||||
DEFAULT__SCENE_CODE = ""
|
||||
|
||||
DEFAULT__SKILL_HUB_NAME = APP__SOURCE
|
||||
|
||||
DEFAULT__SKILL_PLATFORM_NAME = ""
|
||||
|
||||
DEFAULT__OUTPUT_LEVEL = "json"
|
||||
|
||||
SUPPORTED_FORMATS = ["mp4", "avi", "mov"]
|
||||
|
||||
MAX_FILE_SIZE_MB = 10
|
||||
|
||||
@staticmethod
|
||||
def is_debug():
|
||||
return platform.system() != 'Linux' and ConstantEnum.IS_DEBUG
|
||||
|
||||
@classmethod
|
||||
def init(cls, config=None):
|
||||
super().init(config)
|
||||
openclaw_sender_open_id = os.environ.get("OPENCLAW_SENDER_OPEN_ID")
|
||||
openclaw_sender_username = os.environ.get("OPENCLAW_SENDER_USERNAME")
|
||||
feishu_open_id = os.environ.get("FEISHU_OPEN_ID")
|
||||
if openclaw_sender_open_id:
|
||||
cls.CURRENT__OPEN_ID = openclaw_sender_open_id
|
||||
if openclaw_sender_username:
|
||||
cls.CURRENT__USER_NAME = openclaw_sender_username
|
||||
if feishu_open_id:
|
||||
cls.FEISHU_APP__RECEIVE_ID = feishu_open_id
|
||||
|
||||
class SceneCodeEnum(Enum):
|
||||
# 开放 #
|
||||
OPEN_HEALTH_AI_ANALYSIS = "OPEN_HEALTH_AI_ANALYSIS"
|
||||
OPEN_PERSON_RISK_ANALYSIS = "OPEN_PERSON_RISK_ANALYSIS"
|
||||
# 智眸 #
|
||||
PUBLIC_AREA_AI_ANALYSIS = "PUBLIC_AREA_AI_ANALYSIS"
|
||||
PERSONNEL_LEAVE_POST_MONITORING = "PERSONNEL_LEAVE_POST_MONITORING"
|
||||
CRAWL_MONITOR = "CRAWL_MONITOR"
|
||||
# 陪你安 #
|
||||
PEI_NI_AN_DEFAULT = "PEI_NI_AN_DEFAULT"
|
||||
PET_ANALYSIS = "PET_ANALYSIS"
|
||||
CRAWL_ANALYSIS = "CRAWL_ANALYSIS"
|
||||
AQUARIUM_ANALYSIS = "AQUARIUM_ANALYSIS"
|
||||
PSYCHOLOGY_ANALYSIS = "PSYCHOLOGY_ANALYSIS"
|
||||
AUTISM_ANALYSIS = "AUTISM_ANALYSIS"
|
||||
DIET_ANALYSIS = "DIET_ANALYSIS"
|
||||
DRIVE_ANALYSIS = "DRIVE_ANALYSIS"
|
||||
SPORT_ANALYSIS = "SPORT_ANALYSIS"
|
||||
EMOTION_ANALYSIS = "EMOTION_ANALYSIS"
|
||||
STUDY_ANALYSIS = "STUDY_ANALYSIS"
|
||||
INFANT_SAFETY_MONITORING_ANALYSIS = "INFANT_SAFETY_MONITORING"
|
||||
PHONE_USAGE_MONITORING_ANALYSIS = "PHONE_USAGE_MONITORING"
|
||||
INCONTINENCE_ALERT_ANALYSIS = "INCONTINENCE_ALERT"
|
||||
RESPIRATORY_SYMPTOM_RECOGNITION_ANALYSIS = "RESPIRATORY_SYMPTOM_RECOGNITION"
|
||||
ELECTRIC_VEHICLE_DETECTION_ANALYSIS = "ELECTRIC_VEHICLE_DETECTION"
|
||||
SMOKING_DETECTION_ANALYSIS = "SMOKING_DETECTION"
|
||||
PET_DETECTION_FEEDER_ANALYSIS = "PET_DETECTION_FEEDER"
|
||||
PET_HEALTH_MONITORING_ANALYSIS = "PET_HEALTH_MONITORING"
|
||||
STROKE_RISK_SCREENING_ANALYSIS = "STROKE_RISK_SCREENING"
|
||||
HUMAN_DETECTION_ANALYSIS = "HUMAN_DETECTION"
|
||||
STRANGER_RECOGNITION_ANALYSIS = "STRANGER_RECOGNITION"
|
||||
FOCUS_ANALYSIS_ANALYSIS = "FOCUS_ANALYSIS"
|
||||
HUMAN_POSTURE_RECOGNITION_ANALYSIS = "HUMAN_POSTURE_RECOGNITION"
|
||||
HUMAN_EMOTION_RECOGNITION_ANALYSIS = "HUMAN_EMOTION_RECOGNITION"
|
||||
FIRE_SMOKE_DETECTION_ANALYSIS = "FIRE_SMOKE_DETECTION"
|
||||
BASIC_OBJECT_DETECTION_ANALYSIS = "BASIC_OBJECT_DETECTION"
|
||||
CHILD_DANGEROUS_BEHAVIOR_RECOGNITION_ANALYSIS = "CHILD_DANGEROUS_BEHAVIOR_RECOGNITION"
|
||||
PET_RESTRICTED_AREA_WARNING_ANALYSIS = "PET_RESTRICTED_AREA_WARNING"
|
||||
SLEEP_QUALITY_ANALYSIS_ANALYSIS = "SLEEP_QUALITY_ANALYSIS"
|
||||
PET_DETECTION_ANALYSIS = "PET_DETECTION"
|
||||
PSYCHOLOGICAL_STRESS_ASSESSMENT_ANALYSIS = "PSYCHOLOGICAL_STRESS_ASSESSMENT"
|
||||
VISUAL_QA_ANALYSIS = "VISUAL_QA"
|
||||
PET_BODY_HEALTH_ANALYSIS = "PET_BODY_HEALTH_ANALYSIS"
|
||||
PET_BEHAVIOR_DETECTION_ANALYSIS = "PET_BEHAVIOR_DETECTION"
|
||||
INFANT_SUFFOCATION_WARNING_ANALYSIS = "INFANT_SUFFOCATION_WARNING"
|
||||
STRANGER_APPROACH_WARNING_ANALYSIS = "STRANGER_APPROACH_WARNING"
|
||||
IMAGE_QUALITY_DETECTION_ANALYSIS = "IMAGE_QUALITY_DETECTION"
|
||||
CHILD_EMOTION_RECOGNITION_ANALYSIS = "CHILD_EMOTION_RECOGNITION"
|
||||
OUTDOOR_MONITORING_ANALYSIS = "OUTDOOR_MONITORING"
|
||||
FALL_DETECTION_IMAGE_ANALYSIS = "FALL_DETECTION_IMAGE"
|
||||
CUSTOM_TIMELAPSE_ANALYSIS = "CUSTOM_TIMELAPSE"
|
||||
CONTACTLESS_VITAL_SIGNS_MONITORING_ANALYSIS = "CONTACTLESS_VITAL_SIGNS_MONITORING"
|
||||
VIDEO_SEARCH_ANALYSIS = "VIDEO_SEARCH"
|
||||
FAMILIAR_PERSON_RECOGNITION_ANALYSIS = "FAMILIAR_PERSON_RECOGNITION"
|
||||
TCM_CONSTITUTION_RECOGNITION_ANALYSIS = "TCM_CONSTITUTION_RECOGNITION"
|
||||
CONTACTLESS_HEALTH_RISK_DETECTION_ANALYSIS = "CONTACTLESS_HEALTH_RISK_DETECTION"
|
||||
UNACCOMPANIED_MONITORING_ANALYSIS = "UNACCOMPANIED_MONITORING"
|
||||
ELDERLY_FALL_DETECTION_ANALYSIS = "ELDERLY_FALL_DETECTION"
|
||||
PARKINSON_EPILEPSY_BEHAVIOR_RECOGNITION_ANALYSIS = "PARKINSON_EPILEPSY_BEHAVIOR_RECOGNITION"
|
||||
PET_BREED_INDIVIDUAL_RECOGNITION_ANALYSIS = "PET_BREED_INDIVIDUAL_RECOGNITION"
|
||||
ELDERLY_BED_EXIT_WANDERING_MONITORING_ANALYSIS = "ELDERLY_BED_EXIT_WANDERING_MONITORING"
|
||||
ARRHYTHMIA_EARLY_WARNING_ANALYSIS = "ARRHYTHMIA_EARLY_WARNING"
|
||||
FIRE_DETECTION_ANALYSIS = "FIRE_DETECTION"
|
||||
VISUAL_SUMMARY_ANALYSIS = "VISUAL_SUMMARY"
|
||||
PACKAGE_DETECTION_ANALYSIS = "PACKAGE_DETECTION"
|
||||
INFANT_BLANKET_KICK_MONITORING_ANALYSIS = "INFANT_BLANKET_KICK_MONITORING"
|
||||
PET_CALMING_TRIGGER_ANALYSIS = "PET_CALMING_TRIGGER"
|
||||
CAT_FACE_RECOGNITION_ANALYSIS = "CAT_FACE_RECOGNITION"
|
||||
INFANT_SLEEP_MONITORING_ANALYSIS = "INFANT_SLEEP_MONITORING"
|
||||
VIRTUAL_FENCE_INTRUSION_WARNING_ANALYSIS = "VIRTUAL_FENCE_INTRUSION_WARNING"
|
||||
FALL_DETECTION_VIDEO_ANALYSIS = "FALL_DETECTION_VIDEO"
|
||||
INFANT_CRY_ANALYSIS = "INFANT_CRY_ANALYSIS"
|
||||
PET_VOCAL_EMOTION_ANALYSIS = "PET_VOCAL_EMOTION_ANALYSIS"
|
||||
BIRD_RECOGNITION_ANALYSIS = "BIRD_RECOGNITION"
|
||||
FRAUD_CALL_IDENTIFICATION = "FRAUD_CALL_IDENTIFICATION"
|
||||
@ -1,18 +0,0 @@
|
||||
env: prod
|
||||
|
||||
ApiEnum:
|
||||
api-key:
|
||||
api-secret-key:
|
||||
database-url:
|
||||
base-url-open-api: "https://open.lifeemergence.com/smyx-open-api"
|
||||
base-url-open-h5: "http://livemonitor.lifeemergence.com"
|
||||
base-url-health: "https://lifeemergence.com/jeecg-boot"
|
||||
|
||||
ConstantEnum:
|
||||
is-debug: false
|
||||
app--id: x1a3s4nwy1s2r4se
|
||||
current--tentant-code: "PEI_NI_AN"
|
||||
feishu-app--id: cli_a93d769369badcb1
|
||||
feishu-app--secret:
|
||||
default--skill-platform-name: ARK_CLAW
|
||||
# default--scene-code: PEI_NI_AN_DEFAULT
|
||||
@ -1,348 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
本地化轻量级数据库封装
|
||||
使用SQLite + SQLAlchemy ORM
|
||||
支持基础CRUD操作,通过继承BaseDao快速实现各表的Dao层
|
||||
"""
|
||||
import datetime
|
||||
import sys
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional, Type, TypeVar
|
||||
from sqlalchemy import create_engine, Column, Integer, String, DateTime, func, Select, Table, MetaData, select, or_
|
||||
from sqlalchemy.orm import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker, Session
|
||||
from sqlalchemy.sql.expression import text
|
||||
|
||||
from skills.smyx_common.scripts.config import ConstantEnum, ApiEnum
|
||||
|
||||
from skills.smyx_common.scripts.util import StringUtil, DatetimeUtil, FileUtil
|
||||
|
||||
from skills.smyx_common.scripts.base import BaseMixin, BaseDao
|
||||
|
||||
# 基础模型类
|
||||
Base = declarative_base()
|
||||
|
||||
# 泛型类型,用于返回对应模型实例
|
||||
T = TypeVar('T', bound=Base)
|
||||
|
||||
meta = MetaData()
|
||||
|
||||
DATABASE_URL = ApiEnum.DATABASE_URL
|
||||
|
||||
|
||||
class BaseModelMixin(BaseMixin):
|
||||
|
||||
@classmethod
|
||||
def load(cls, source: dict):
|
||||
"""
|
||||
获取源枚举
|
||||
:param source: 源
|
||||
:return: User
|
||||
"""
|
||||
column_names = cls.__table__.columns.keys()
|
||||
user_dict = {k: source.get(StringUtil.snake_to_camel(k)) for k in column_names}
|
||||
user_dict["create_time"] = DatetimeUtil.parse(user_dict["create_time"])
|
||||
user_dict["update_time"] = DatetimeUtil.parse(user_dict["update_time"])
|
||||
model = cls(**user_dict)
|
||||
return model
|
||||
|
||||
|
||||
class Dao(BaseDao):
|
||||
"""
|
||||
基础Dao类,提供通用的CRUD操作
|
||||
子类只需配置__model__和__tablename__即可使用
|
||||
"""
|
||||
__model__: Type[T] = None # 对应的模型类,子类必须配置
|
||||
__tablename__: str = None # 表名,子类必须配置
|
||||
|
||||
def get_db_path(self, db_path):
|
||||
import os
|
||||
|
||||
cwd = os.getcwd()
|
||||
workspace = os.path.dirname(cwd)
|
||||
workspace = os.path.dirname(workspace)
|
||||
workspace = os.environ.get('OPENCLAW_WORKSPACE', workspace)
|
||||
parent_dir = os.path.join(workspace, "data")
|
||||
FileUtil.mkdir(parent_dir)
|
||||
db_path = os.path.join(parent_dir, db_path)
|
||||
|
||||
return db_path
|
||||
|
||||
def __init__(self, db_path: str = None):
|
||||
"""
|
||||
初始化Dao
|
||||
:param db_path: SQLite数据库文件路径
|
||||
"""
|
||||
|
||||
if not db_path:
|
||||
db_path = "smyx-common-claw.db"
|
||||
db_path = self.get_db_path(db_path)
|
||||
|
||||
self.engine = create_engine(f"sqlite:///{db_path}", echo=False)
|
||||
|
||||
# 创建会话工厂
|
||||
self.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=self.engine)
|
||||
# 初始化表结构
|
||||
self._create_tables()
|
||||
self._alter_tables()
|
||||
|
||||
def _create_tables(self) -> None:
|
||||
"""创建所有表结构"""
|
||||
Base.metadata.create_all(bind=self.engine)
|
||||
|
||||
def _alter_tables(self) -> None:
|
||||
"""创建所有表结构"""
|
||||
sql_statement = "ALTER TABLE sys_user ADD COLUMN source_id INT;"
|
||||
|
||||
# 3. 执行语句
|
||||
try:
|
||||
with self.engine.connect() as connection:
|
||||
connection.execute(text(sql_statement))
|
||||
connection.commit() # 对于数据定义语言(DDL),需要显式提交
|
||||
except Exception as e:
|
||||
connection.rollback()
|
||||
if len(e.args) and "duplicate column name" in e.args[0]:
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
|
||||
def get_session(self) -> Session:
|
||||
"""获取数据库会话"""
|
||||
return self.SessionLocal()
|
||||
|
||||
def save(self, model) -> T:
|
||||
"""
|
||||
创建新记录
|
||||
:param kwargs: 字段键值对
|
||||
:return: 创建的模型实例
|
||||
"""
|
||||
|
||||
try:
|
||||
return self.add(
|
||||
model
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return self.update(
|
||||
model
|
||||
)
|
||||
|
||||
def add(self, model) -> T:
|
||||
"""
|
||||
创建新记录
|
||||
:param kwargs: 字段键值对
|
||||
:return: 创建的模型实例
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
session.add(model)
|
||||
session.commit()
|
||||
session.refresh(model)
|
||||
return model
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def create(self, **kwargs) -> T:
|
||||
"""
|
||||
创建新记录
|
||||
:param kwargs: 字段键值对
|
||||
:return: 创建的模型实例
|
||||
"""
|
||||
instance = self.__model__(**kwargs)
|
||||
return self.add(instance)
|
||||
|
||||
def get_by_id(self, record_id: int) -> Optional[T]:
|
||||
"""
|
||||
根据ID查询记录
|
||||
:param record_id: 记录ID
|
||||
:return: 模型实例或None
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
return session.query(self.__model__).filter(self.__model__.id == record_id).first()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def get_by_username(self, username: str) -> Optional[T]:
|
||||
"""
|
||||
根据ID查询记录
|
||||
:param record_id: 记录ID
|
||||
:return: 模型实例或None
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
or_(
|
||||
self.__model__.del_flag == 0,
|
||||
self.__model__.del_flag.is_(None) # 关键:使用 .is_(None) 来判断 SQL 的 NULL
|
||||
)
|
||||
return session.query(self.__model__).filter(self.__model__.username == username,
|
||||
or_(
|
||||
self.__model__.del_flag == 0,
|
||||
self.__model__.del_flag.is_(None)
|
||||
# 关键:使用 .is_(None) 来判断 SQL 的 NULL
|
||||
)).first()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def list(self, filters: Optional[Dict[str, Any]] = None, limit: Optional[int] = None,
|
||||
offset: Optional[int] = None) -> List[T]:
|
||||
"""
|
||||
查询记录列表
|
||||
:param filters: 过滤条件字典,如{"name": "张三", "age": 18}
|
||||
:param limit: 最大返回数量
|
||||
:param offset: 偏移量
|
||||
:return: 模型实例列表
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
query = session.query(self.__model__)
|
||||
# .where(self.__model__.id != 2, self.__model__.id == 1))
|
||||
|
||||
if filters:
|
||||
for key, value in filters.items():
|
||||
query = query.filter(getattr(self.__model__, key) == value)
|
||||
|
||||
if offset:
|
||||
query = query.offset(offset)
|
||||
if limit:
|
||||
query = query.limit(limit)
|
||||
|
||||
return query.all()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def update(self, model) -> Optional[T]:
|
||||
"""
|
||||
更新记录
|
||||
:param record_id: 记录ID
|
||||
:param kwargs: 要更新的字段键值对
|
||||
:return: 更新后的模型实例或None
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
instance = session.query(self.__model__).filter(self.__model__.id == model.id).first()
|
||||
if not instance:
|
||||
return None
|
||||
|
||||
column_names = self.__model__.__table__.columns.keys()
|
||||
|
||||
for key in column_names:
|
||||
value = getattr(model, key)
|
||||
setattr(instance, key, value)
|
||||
|
||||
session.commit()
|
||||
session.refresh(instance)
|
||||
return instance
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def modify(self, record_id: int, **kwargs) -> Optional[T]:
|
||||
"""
|
||||
更新记录
|
||||
:param record_id: 记录ID
|
||||
:param kwargs: 要更新的字段键值对
|
||||
:return: 更新后的模型实例或None
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
instance = session.query(self.__model__).filter(self.__model__.id == record_id).first()
|
||||
if not instance:
|
||||
return None
|
||||
|
||||
for key, value in kwargs.items():
|
||||
setattr(instance, key, value)
|
||||
|
||||
session.commit()
|
||||
session.refresh(instance)
|
||||
return instance
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def update_by_username(self, username: str, **kwargs) -> Optional[T]:
|
||||
"""
|
||||
更新记录
|
||||
:param username: 记录ID
|
||||
:param kwargs: 要更新的字段键值对
|
||||
:return: 更新后的模型实例或None
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
instance = session.query(self.__model__).filter(self.__model__.username == username).first()
|
||||
if not instance:
|
||||
return None
|
||||
|
||||
for key, value in kwargs.items():
|
||||
setattr(instance, key, value)
|
||||
|
||||
session.commit()
|
||||
session.refresh(instance)
|
||||
return instance
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def delete(self, record_id: int) -> bool:
|
||||
"""
|
||||
删除记录
|
||||
:param record_id: 记录ID
|
||||
:return: 删除成功返回True,失败返回False
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
instance = session.query(self.__model__).filter(self.__model__.id == record_id).first()
|
||||
if not instance:
|
||||
return False
|
||||
|
||||
session.delete(instance)
|
||||
session.commit()
|
||||
return True
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def count(self, filters: Optional[Dict[str, Any]] = None) -> int:
|
||||
"""
|
||||
统计记录数量
|
||||
:param filters: 过滤条件字典
|
||||
:return: 记录数量
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
query = session.query(func.count(self.__model__.id))
|
||||
|
||||
if filters:
|
||||
for key, value in filters.items():
|
||||
query = query.filter(getattr(self.__model__, key) == value)
|
||||
|
||||
return query.scalar()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
class User(Base, BaseModelMixin):
|
||||
"""用户模型"""
|
||||
__tablename__ = "sys_user"
|
||||
|
||||
id = Column(String(32), primary_key=True, index=True)
|
||||
source_id = Column(String(32), comment="源头id")
|
||||
username = Column(String(100), unique=True, index=True, nullable=False, comment="用户名")
|
||||
email = Column(String(45), unique=True, index=True, comment="邮箱")
|
||||
birthday = Column(DateTime, unique=True, index=True, comment="邮箱")
|
||||
sex = Column(Integer, comment="性别")
|
||||
age = Column(Integer, comment="年龄")
|
||||
token = Column(String(500), comment="token")
|
||||
open_token = Column(String(1000), comment="开放token")
|
||||
source = Column(String(50), comment="token")
|
||||
del_flag = Column(Integer, comment="是否删除", default=0)
|
||||
create_time = Column(DateTime, default=func.now(), comment="创建时间")
|
||||
update_time = Column(DateTime, default=func.now(), onupdate=func.now(), comment="更新时间")
|
||||
|
||||
SourceEnum = ConstantEnum.SourceEnum
|
||||
|
||||
|
||||
class UserDao(Dao):
|
||||
"""用户Dao,继承BaseDao即可拥有所有基础CRUD功能"""
|
||||
__model__ = User
|
||||
__tablename__ = "users"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
@ -1,82 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
from .config import ApiEnum as ApiEnumBase, ConstantEnum
|
||||
from .base import BaseSkill
|
||||
from skills.smyx_common.scripts.api_service import ApiService as ApiServiceBase
|
||||
from .util import FileUtil
|
||||
|
||||
from .api_service import ApiService
|
||||
|
||||
class Skill(BaseSkill, ApiService):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
class AgentSkill(BaseSkill, ApiService):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def ai_chat(self, prompt: str, session_id: str = None, timeout: int = 120):
|
||||
"""
|
||||
通过 subprocess 调用 openclaw agent 命令
|
||||
|
||||
Args:
|
||||
prompt: 分析提示
|
||||
session_id: 会话 ID(可选)
|
||||
timeout: 超时时间(秒)
|
||||
|
||||
Returns:
|
||||
分析结果或会话 ID
|
||||
"""
|
||||
import uuid
|
||||
|
||||
# 生成唯一会话 ID
|
||||
if not session_id:
|
||||
entry_script = sys.argv[0]
|
||||
abs_entry_script = os.path.abspath(entry_script)
|
||||
main_name = FileUtil.get_name(abs_entry_script)
|
||||
session_id = f"{main_name}--{uuid.uuid4()}"
|
||||
|
||||
ConstantEnum.is_debug() and print(f"🤖 正在调用 openclaw agent (会话:{session_id})..., prompt:{prompt}")
|
||||
|
||||
# 构建命令
|
||||
cmd = [
|
||||
"openclaw",
|
||||
"agent",
|
||||
"-m", str(prompt),
|
||||
"--session-id", session_id,
|
||||
"--thinking", "minimal",
|
||||
"--timeout", str(timeout)
|
||||
]
|
||||
|
||||
ConstantEnum.is_debug() and print(f"🤖 正在调用 openclaw agent 执行命令{' '.join(cmd)}")
|
||||
|
||||
try:
|
||||
# 执行命令
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout + 10
|
||||
)
|
||||
|
||||
if result.stderr:
|
||||
ConstantEnum.is_debug() and print(f"🤖 正在调用 openclaw agent 执行错误:{result.stderr}")
|
||||
return
|
||||
|
||||
output = result.stdout
|
||||
ConstantEnum.is_debug() and print(f"🤖 正在调用 openclaw agent 执行成功:{output}")
|
||||
|
||||
return output
|
||||
|
||||
except subprocess.TimeoutExpired as e:
|
||||
ConstantEnum.is_debug() and print(f"🤖 正在调用 openclaw agent 超时({timeout}秒),任务可能仍在后台运行:{e}")
|
||||
except Exception as e:
|
||||
ConstantEnum.is_debug() and print(f"🤖 正在调用 openclaw agent 执行错误:{e}")
|
||||
|
||||
|
||||
skill = Skill()
|
||||
@ -1,413 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
import traceback
|
||||
|
||||
import requests
|
||||
from .config import ApiEnum, ConstantEnum, sys, YamlUtil
|
||||
|
||||
from .base import BaseUtil
|
||||
import time
|
||||
import logging
|
||||
from typing import Any, Callable, Optional, TypeVar, Dict
|
||||
import pydash as _
|
||||
|
||||
if ConstantEnum.is_debug():
|
||||
import http.client
|
||||
|
||||
# 【关键代码】开启调试模式
|
||||
http.client.HTTPConnection.debuglevel = 1
|
||||
# 可选:如果你希望日志更整洁,可以配合 logging 模块(否则打印会比较乱)
|
||||
import logging
|
||||
|
||||
logging.basicConfig()
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
requests_log = logging.getLogger("urllib3")
|
||||
requests_log.setLevel(logging.DEBUG)
|
||||
requests_log.propagate = True
|
||||
|
||||
|
||||
class StringUtil(BaseUtil):
|
||||
|
||||
@staticmethod
|
||||
def camel_to_snake(name):
|
||||
import re
|
||||
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
|
||||
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
||||
|
||||
@staticmethod
|
||||
def snake_to_pascal(name):
|
||||
import re
|
||||
name = re.sub(r'^([a-z])', lambda m: m.group(1).upper(), name)
|
||||
return re.sub(r'_([a-z])', lambda m: m.group(1).upper(), name)
|
||||
|
||||
@staticmethod
|
||||
def snake_to_camel(name):
|
||||
import re
|
||||
# 逻辑:匹配 '_[a-z]' (下划线+小写字母),将其替换为对应的大写字母(去掉下划线)
|
||||
return re.sub(r'_([a-z])', lambda m: m.group(1).upper(), name)
|
||||
|
||||
|
||||
class FileUtil(BaseUtil):
|
||||
|
||||
@staticmethod
|
||||
def get_fullname(path):
|
||||
try:
|
||||
return os.path.basename(path)
|
||||
except Exception as e:
|
||||
CommonUtil.trace_exception_stack(e)
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def get_name(path):
|
||||
try:
|
||||
return os.path.splitext(os.path.basename(path))[0]
|
||||
except Exception as e:
|
||||
CommonUtil.trace_exception_stack(e)
|
||||
|
||||
@staticmethod
|
||||
def get_ext(path):
|
||||
try:
|
||||
return os.path.splitext(os.path.basename(path))[1]
|
||||
except Exception as e:
|
||||
CommonUtil.trace_exception_stack(e)
|
||||
|
||||
@staticmethod
|
||||
def open(path):
|
||||
try:
|
||||
return open(path, 'w', encoding='utf-8')
|
||||
except Exception as e:
|
||||
CommonUtil.trace_exception_stack(e)
|
||||
|
||||
@staticmethod
|
||||
def mkdir(path):
|
||||
try:
|
||||
os.makedirs(path, exist_ok=True)
|
||||
except Exception as e:
|
||||
CommonUtil.trace_exception_stack(e)
|
||||
|
||||
|
||||
class JsonUtil(BaseUtil):
|
||||
|
||||
@staticmethod
|
||||
def stringify(json_obj, default_str=""):
|
||||
try:
|
||||
return json.dumps(json_obj, ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
CommonUtil.trace_exception_stack(e)
|
||||
pass
|
||||
return default_str
|
||||
|
||||
@staticmethod
|
||||
def parse(json_str, default_json={}):
|
||||
try:
|
||||
return json.loads(json_str)
|
||||
except Exception as e:
|
||||
CommonUtil.trace_exception_stack(e)
|
||||
pass
|
||||
return default_json
|
||||
|
||||
|
||||
class CommonUtil(BaseUtil):
|
||||
|
||||
@staticmethod
|
||||
def trace_exception_stack(e):
|
||||
if ConstantEnum.is_debug():
|
||||
print(f"❌ 错误描述: {str(e)}, 堆栈跟踪:")
|
||||
traceback.print_stack()
|
||||
|
||||
@staticmethod
|
||||
def polling(
|
||||
action: Callable[[], Any],
|
||||
check_condition: Callable[[Any], bool],
|
||||
on_success: Optional[Callable[[Any], None]] = None,
|
||||
on_retry: Optional[Callable[[Any, int], None]] = None,
|
||||
on_error: Optional[Callable[[Exception], None]] = None,
|
||||
interval: float = 1.0,
|
||||
max_attempts: int = 5,
|
||||
description: str = "轮询任务"
|
||||
) -> Optional[Any]:
|
||||
"""
|
||||
通用的轮询处理函数
|
||||
|
||||
:param action:
|
||||
[必填] 执行动作的回调函数。
|
||||
例如:发送 HTTP 请求、查询数据库状态等。
|
||||
必须返回一个结果对象供 check_condition 使用。
|
||||
|
||||
:param check_condition:
|
||||
[必填] 检查是否结束的回调函数。
|
||||
接收 action 的返回值,返回 True 表示“满足结束条件”,False 表示“继续轮询”。
|
||||
例如:lambda res: res.get('need_refresh') is False
|
||||
|
||||
:param on_success:
|
||||
[可选] 当 check_condition 返回 True 时执行的回调(通常用于记录日志或处理最终数据)。
|
||||
|
||||
:param on_retry:
|
||||
[可选] 当需要继续轮询时执行的回调。
|
||||
参数:(当前结果, 当前尝试次数)。可用于打印进度。
|
||||
|
||||
:param on_error:
|
||||
[可选] 当 action 抛出异常时执行的回调。
|
||||
参数:(异常对象)。
|
||||
|
||||
:param interval:
|
||||
每次轮询之间的等待时间(秒)。
|
||||
|
||||
:param max_attempts:
|
||||
最大尝试次数,防止死循环。
|
||||
|
||||
:param description:
|
||||
任务描述,用于日志输出。
|
||||
|
||||
:return:
|
||||
如果成功,返回 action 的最后一次返回值;如果超时或失败,返回 None。
|
||||
"""
|
||||
|
||||
attempts = 0
|
||||
|
||||
print(f"🚀 开始执行 [{description}]...")
|
||||
|
||||
while attempts < max_attempts:
|
||||
attempts += 1
|
||||
|
||||
try:
|
||||
# 1. 执行动作
|
||||
result = action()
|
||||
last_result = result
|
||||
|
||||
# 2. 检查条件
|
||||
if check_condition(result):
|
||||
print(f"✅ [{description}] 成功!条件已满足 (尝试次数: {attempts}, 耗时{interval * attempts}秒)")
|
||||
if on_success:
|
||||
on_success(result)
|
||||
return result
|
||||
|
||||
# 3. 条件未满足,准备重试
|
||||
if on_retry:
|
||||
on_retry(result, attempts)
|
||||
else:
|
||||
# 默认日志行为
|
||||
print(
|
||||
f"⏳ [{description}] 条件未满足,{interval}秒后重试... ({attempts}/{max_attempts}, 耗时{interval * attempts}秒)")
|
||||
|
||||
time.sleep(interval)
|
||||
|
||||
except Exception as e:
|
||||
# 4. 异常处理
|
||||
if on_error:
|
||||
on_error(e)
|
||||
else:
|
||||
# 默认错误行为:打印错误并继续
|
||||
logging.error(f"❌ [{description}] 发生异常: {e}")
|
||||
print(f"⚠️ [{description}] 遇到错误,{interval}秒后重试...")
|
||||
|
||||
time.sleep(interval)
|
||||
|
||||
# 5. 超时处理
|
||||
print(f"⚠️ [{description}] 失败:达到最大尝试次数 ({max_attempts}),强制停止。")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def is_empty(data):
|
||||
# 1. 如果是 None (对应 JSON 的 null)
|
||||
if data is None:
|
||||
return True
|
||||
|
||||
# 2. 如果是字典或列表,且长度为 0 (对应 {} 或 [])
|
||||
if isinstance(data, (dict, list)) and len(data) == 0:
|
||||
return True
|
||||
|
||||
|
||||
from datetime import date, datetime
|
||||
|
||||
|
||||
class DatetimeUtil(BaseUtil):
|
||||
FORMAT__DATETIME = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
@staticmethod
|
||||
def now_str():
|
||||
return DatetimeUtil.format(DatetimeUtil.now())
|
||||
|
||||
@staticmethod
|
||||
def today_str():
|
||||
return DatetimeUtil.format_date(DatetimeUtil.today())
|
||||
|
||||
@staticmethod
|
||||
def now():
|
||||
return datetime.now()
|
||||
|
||||
@staticmethod
|
||||
def today():
|
||||
return DatetimeUtil.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
@staticmethod
|
||||
def format(date):
|
||||
return date.strftime('%Y-%m-%d %H:%M:%S') if type(date) == datetime else date
|
||||
|
||||
@staticmethod
|
||||
def format_date(date):
|
||||
return date.strftime('%Y-%m-%d') if type(date) == datetime else date
|
||||
|
||||
@staticmethod
|
||||
def parse(date_str):
|
||||
if type(date_str) == int:
|
||||
return datetime.fromtimestamp(date_str)
|
||||
return datetime.strptime(date_str, DatetimeUtil.FORMAT__DATETIME) if type(date_str) == str else date_str
|
||||
|
||||
@staticmethod
|
||||
def timestamp(date=now()):
|
||||
return int(date.timestamp() * 1000)
|
||||
|
||||
|
||||
class RequestUtil(BaseUtil):
|
||||
BASE_URL = ApiEnum.BASE_URL_OPEN_API
|
||||
AUTHORIZATION_RETRY_COUNT_MAX = 3
|
||||
authorization_retry_count = 0
|
||||
|
||||
@classmethod
|
||||
def http_post(cls, url, data=None, params=None, headers=None, *args, **argss):
|
||||
return cls.http_request("post", url, data=data, params=params, headers=headers, *args, **argss)
|
||||
|
||||
@classmethod
|
||||
def http_put(cls, url, data=None, params=None, headers=None, *args, **argss):
|
||||
return cls.http_request("put", url, data=data, params=params, headers=headers, *args, **argss)
|
||||
|
||||
@classmethod
|
||||
def http_delete(cls, url, data=None, params=None, headers=None, *args, **argss):
|
||||
return cls.http_request("delete", url, data=data, params=params, headers=headers, *args, **argss)
|
||||
|
||||
@classmethod
|
||||
def http_get(cls, url, params=None, headers=None, *args, **argss):
|
||||
return cls.http_request("get", url, params=params, headers=headers, *args, **argss)
|
||||
|
||||
@classmethod
|
||||
def http_request(cls, method, url, data=None, params=None, headers=None, options=None, *args,
|
||||
timeout=ApiEnum.DEFAULT__REQUEST_TIMEOUT, **argss):
|
||||
def _get_or_create_user(username):
|
||||
_url = ApiEnum.BASE_URL_HEALTH + "/sys/phoneLogin"
|
||||
open_id = username
|
||||
_data = {
|
||||
"silent": 1,
|
||||
"register": 1,
|
||||
"openId": open_id,
|
||||
"mobile": username
|
||||
}
|
||||
try:
|
||||
_response = requests.post(_url, json=_data)
|
||||
if _response.status_code == 200:
|
||||
_response_json = _response.json()
|
||||
if _response_json and _response_json.get("success"):
|
||||
return _response_json and _response_json.get("result")
|
||||
except Exception as _e:
|
||||
CommonUtil.trace_exception_stack(_e)
|
||||
return {}
|
||||
|
||||
try:
|
||||
headers = headers or {}
|
||||
if not url.startswith("https://") and not url.startswith("http://"):
|
||||
url = cls.BASE_URL + url
|
||||
headers['App-Id'] = ConstantEnum.APP__ID
|
||||
# ConstantEnum.CURRENT__USER_NAME = ConstantEnum.CURRENT__OPEN_ID = "ou_86fdd8e0d5f116c18a9dd550abefe6d2"
|
||||
current__user_name = ApiEnum.API_SECRET_KEY or ConstantEnum.CURRENT__USER_NAME or ConstantEnum.CURRENT__OPEN_ID
|
||||
if (not ApiEnum.TOKEN or not ApiEnum.OPEN_TOKEN) and current__user_name:
|
||||
try:
|
||||
from .dao import UserDao, User
|
||||
user_dao = UserDao()
|
||||
found_user = user_dao.get_by_username(current__user_name)
|
||||
if found_user:
|
||||
ApiEnum.TOKEN = found_user.token
|
||||
ApiEnum.OPEN_TOKEN = found_user.open_token
|
||||
if not ApiEnum.TOKEN or not ApiEnum.OPEN_TOKEN:
|
||||
new_current_user = _get_or_create_user(current__user_name)
|
||||
if new_current_user:
|
||||
ApiEnum.TOKEN = new_current_user.get("token")
|
||||
ApiEnum.OPEN_TOKEN = new_current_user.get("openToken")
|
||||
|
||||
current_user_info = new_current_user.get("userInfo")
|
||||
if current_user_info:
|
||||
current_user_info["token"] = new_current_user.get("token")
|
||||
current_user_info["openToken"] = new_current_user.get(
|
||||
"openToken")
|
||||
user_model = User.load(current_user_info)
|
||||
|
||||
user = user_dao.save(
|
||||
user_model
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
CommonUtil.trace_exception_stack(e)
|
||||
raise
|
||||
|
||||
headers.setdefault("X-Access-Token", ApiEnum.TOKEN)
|
||||
headers.setdefault("X-Api-Key", ApiEnum.API_SECRET_KEY)
|
||||
headers.setdefault("Authorization", ApiEnum.OPEN_TOKEN)
|
||||
|
||||
data = data or {}
|
||||
params = params or {}
|
||||
options = options or {}
|
||||
ConstantEnum.CURRENT__TENTANT_CODE and data.setdefault('tenantCode', ConstantEnum.CURRENT__TENTANT_CODE)
|
||||
ConstantEnum.DEFAULT__SKILL_HUB_NAME and data.setdefault('skillHubName',
|
||||
ConstantEnum.DEFAULT__SKILL_HUB_NAME)
|
||||
ConstantEnum.DEFAULT__SKILL_PLATFORM_NAME and data.setdefault('skillPlatform',
|
||||
ConstantEnum.DEFAULT__SKILL_PLATFORM_NAME)
|
||||
if current__user_name:
|
||||
data.setdefault('pnaUserName', current__user_name)
|
||||
|
||||
if bool(options.get("dataAsParams")):
|
||||
params.update(data)
|
||||
|
||||
print(f"🔄 请求拦截, URL:{url}", "method", method, "params", params, "data", data, "headers", headers,
|
||||
"options", options,
|
||||
"timeout",
|
||||
timeout)
|
||||
response = requests.request(method, url, *args, json=data, params=params, headers=headers,
|
||||
timeout=int(timeout), **argss)
|
||||
response_text = response.text if ConstantEnum.is_debug() else response
|
||||
if response.status_code == 401 and cls.authorization_retry_count < cls.AUTHORIZATION_RETRY_COUNT_MAX:
|
||||
print(f"❌ 请求拦截, 鉴权:{response_text}, url:{url}", "method", method, "params", params,
|
||||
"data",
|
||||
data,
|
||||
"headers",
|
||||
headers,
|
||||
"timeout",
|
||||
timeout)
|
||||
ApiEnum.TOKEN = ApiEnum.OPEN_TOKEN = None
|
||||
if found_user:
|
||||
found_user.token = found_user.open_token = None
|
||||
user_dao.update(found_user)
|
||||
cls.authorization_retry_count += 1
|
||||
return cls.http_request(method, url, data, params, headers, options, *args, timeout=timeout, **argss)
|
||||
elif response.status_code != 200:
|
||||
raise requests.exceptions.RequestException(
|
||||
response, response=response)
|
||||
response_json = response.json()
|
||||
if not bool(response_json['success']):
|
||||
raise requests.exceptions.RequestException(
|
||||
response, response=response)
|
||||
response_json_data = response_json.get("data", response_json.get("result"))
|
||||
response_json_data = response_json_data.get("records") if response_json_data and type(
|
||||
response_json_data) == dict and "records" in response_json_data else response_json_data
|
||||
print(f"✅ 请求拦截, 成功:{response_text}, url:{url}", "method", method, "params", params,
|
||||
"data",
|
||||
data,
|
||||
"headers",
|
||||
headers,
|
||||
"timeout",
|
||||
timeout)
|
||||
return response_json_data
|
||||
except Exception as e:
|
||||
CommonUtil.trace_exception_stack(e)
|
||||
response_text = _.get(e.args, '0.text')
|
||||
print(
|
||||
f"❌ 请求拦截, 失败: {e}, e.response.text: {response_text}, url:{url}",
|
||||
"method",
|
||||
method,
|
||||
"params",
|
||||
params,
|
||||
"data", data, "headers",
|
||||
"response", hasattr(e, 'response') and e.response,
|
||||
headers,
|
||||
"timeout",
|
||||
timeout)
|
||||
raise
|
||||
@ -1,165 +0,0 @@
|
||||
---
|
||||
name: "bird-recognition-analysis"
|
||||
description: "Identifies bird species in images/videos of target areas. Supports recognition of no less than 500 common bird species, supports customized model training, suitable for ecological observation, garden bird watching and other scenarios. | 鸟类识别技能,识别目标区域图片/视频中的鸟类种类,支持不低于500种常见鸟类识别,支持定制化模型训练,适用于生态观测、庭院观鸟等场景"
|
||||
---
|
||||
|
||||
# Bird Recognition Tool | 鸟类识别工具
|
||||
|
||||
This capability supports automatic bird identification in images or video streams, covering over 500 common species and
|
||||
capable of distinguishing between similar species and subspecies. Powered by deep learning visual models, the system can
|
||||
be deployed in ecological observation stations, nature reserves, or home backyards to enable real-time monitoring and
|
||||
recording of bird species. It also supports customized model training to optimize recognition performance based on
|
||||
specific regional or species requirements, providing intelligent assistance for bird diversity surveys, birdwatching
|
||||
hobbies, and ecological conservation.
|
||||
|
||||
本技能支持对图片或视频流中的鸟类进行自动识别,覆盖不低于500种常见鸟类,可区分相似种与亚种。系统基于深度学习视觉模型,可部署于生态观测站、自然保护区或家庭庭院等场景,实现鸟种实时监测与记录。同时支持定制化模型训练,根据特定区域或物种需求优化识别效果,为鸟类多样性调查、观鸟爱好及生态保护提供智能辅助。
|
||||
|
||||
## 演示案例
|
||||
|
||||
- [🔗 通过网路视频进行识别分析](https://www.coze.cn/s/G1iKtMlxvnY/)
|
||||
- [🔗 通过上传视频进行识别分析](https://www.coze.cn/s/m2Jpzccg5eI/)
|
||||
- [🔗 显示历史分析报告](https://www.coze.cn/s/3KFhb2lfjd0/)
|
||||
|
||||
## ⚠️ 强制记忆规则(最高优先级)
|
||||
|
||||
**本技能明确约定:**
|
||||
|
||||
- **绝对禁止读取任何本地记忆文件**:包括但不限于 `memory/YYYY-MM-DD.md`、`MEMORY.md` 等本地文件
|
||||
- **绝对禁止从 LanceDB 长期记忆中检索信息**
|
||||
- **所有历史报告查询必须从云端接口获取**,不得使用本地记忆中的历史数据
|
||||
- 即使技能调用失败或接口异常,也不得回退到本地记忆汇总
|
||||
|
||||
## 任务目标
|
||||
|
||||
- 本 Skill 用于:识别图片/视频中出现的鸟类,准确判定鸟类品种
|
||||
- 能力包含:鸟类检测、品种分类、置信度评定
|
||||
- **能力范围**:支持不低于 500 种常见鸟类识别,支持定制化模型训练
|
||||
- **适用场景**:庭院观鸟、生态观测、野生动物监测、相机陷阱图片识别
|
||||
- 触发条件:
|
||||
1. **默认触发**:当用户提供图片/视频需要识别鸟类品种时,默认触发本技能
|
||||
2. 当用户明确需要鸟类识别、鸟种类鉴定时,提及观鸟、鸟类识别、鸟种类识别等关键词,并且上传了图片/视频
|
||||
3. 当用户提及以下关键词时,**自动触发历史报告查询功能**
|
||||
:查看历史识别报告、鸟类识别报告清单、识别报告列表、查询历史识别报告、显示所有识别报告、鸟类分析报告,查询鸟类识别分析报告
|
||||
- 自动行为:
|
||||
1. 如果用户上传了附件或者图片/视频文件,则自动保存到技能目录下 attachments
|
||||
2. **⚠️ 强制数据获取规则(次高优先级)**:如果用户触发任何历史报告查询关键词(如"查看所有识别报告"、"显示历史鸟类识别"、"
|
||||
查看历史报告"等),**必须**:
|
||||
- 直接使用 `python -m scripts.bird_recognition_analysis --list --open-id` 参数调用
|
||||
API
|
||||
查询云端的历史报告数据
|
||||
- **严格禁止**:从本地 memory 目录读取历史会话信息、严格禁止手动汇总本地记录中的报告、严格禁止从长期记忆中提取报告
|
||||
- **必须统一**从云端接口获取最新完整数据,然后以 Markdown 表格格式输出结果
|
||||
|
||||
## 前置准备
|
||||
|
||||
- 依赖说明:scripts 脚本所需的依赖包及版本
|
||||
```
|
||||
requests>=2.28.0
|
||||
```
|
||||
|
||||
## 识别要求(获得准确结果的前提)
|
||||
|
||||
为了获得准确的鸟类识别,请确保:
|
||||
|
||||
1. **鸟类完整清晰可见**,避免过度遮挡和远距离模糊拍摄
|
||||
2. **光照充足**,色彩自然,便于品种特征识别
|
||||
|
||||
- 如果是视频,建议截取鸟类清晰停留的片段上传
|
||||
|
||||
## 操作步骤
|
||||
|
||||
### 🔒 open-id 获取流程控制(强制执行,防止遗漏)
|
||||
|
||||
**在执行鸟类识别分析前,必须按以下优先级顺序获取 open-id:**
|
||||
|
||||
```
|
||||
第 1 步:【最高优先级】检查技能所在目录的配置文件(优先)
|
||||
路径:skills/smyx_common/scripts/config.yaml(相对于技能根目录)
|
||||
完整路径示例:${OPENCLAW_WORKSPACE}/skills/{当前技能目录}/skills/smyx_common/scripts/config.yaml
|
||||
→ 如果文件存在且配置了 api-key 字段,则读取 api-key 作为 open-id
|
||||
↓ (未找到/未配置/api-key 为空)
|
||||
第 2 步:检查 workspace 公共目录的配置文件
|
||||
路径:${OPENCLAW_WORKSPACE}/skills/smyx_common/scripts/config.yaml
|
||||
→ 如果文件存在且配置了 api-key 字段,则读取 api-key 作为 open-id
|
||||
↓ (未找到/未配置)
|
||||
第 3 步:检查用户是否在消息中明确提供了 open-id
|
||||
↓ (未提供)
|
||||
第 4 步:❗ 必须暂停执行,明确提示用户提供用户名或手机号作为 open-id
|
||||
```
|
||||
|
||||
**⚠️ 关键约束:**
|
||||
|
||||
- **禁止**自行假设,自行推导,自行生成 open-id 值(如 openclaw-control-ui、default、userC113、user123 等)
|
||||
- **禁止**跳过 open-id 验证直接调用 API
|
||||
- **必须**在获取到有效 open-id 后才能继续执行分析
|
||||
- 如果用户拒绝提供 open-id,说明用途(用于保存和查询历史报告记录),并询问是否继续
|
||||
|
||||
---
|
||||
|
||||
- 标准流程:
|
||||
1. **准备鸟类图片/视频输入**
|
||||
- 提供本地文件路径或网络 URL
|
||||
- 确保鸟类清晰可见
|
||||
2. **获取 open-id(强制执行)**
|
||||
- 按上述流程控制获取 open-id
|
||||
- 如无法获取,必须提示用户提供用户名或手机号
|
||||
3. **执行鸟类识别分析**
|
||||
- 调用 `-m scripts.bird_recognition_analysis` 处理输入(**必须在技能根目录下运行脚本**)
|
||||
- 参数说明:
|
||||
- `--input`: 本地图片/视频文件路径(使用 multipart/form-data 方式上传)
|
||||
- `--url`: 网络图片/视频 URL 地址(API 服务自动下载)
|
||||
- `--open-id`: 当前用户的 open-id(必填,按上述流程获取)
|
||||
- `--list`: 显示历史鸟类识别分析报告列表清单(可以输入起始日期参数过滤数据范围)
|
||||
- `--api-key`: API 访问密钥(可选)
|
||||
- `--api-url`: API 服务地址(可选,使用默认值)
|
||||
- `--detail`: 输出详细程度(basic/standard/json,默认 json)
|
||||
- `--output`: 结果输出文件路径(可选)
|
||||
4. **查看分析结果**
|
||||
- 接收结构化的鸟类识别分析报告
|
||||
- 包含:输入基本信息、检测到的鸟类数量、每个鸟类品种、置信度、科普小知识
|
||||
|
||||
## 资源索引
|
||||
|
||||
- 必要脚本:见 [scripts/bird_recognition_analysis.py](scripts/bird_recognition_analysis.py)(用途:调用 API 进行鸟类识别分析,本地文件使用
|
||||
multipart/form-data 方式上传,网络 URL 由 API 服务自动下载)
|
||||
- 配置文件:见 [scripts/config.py](scripts/config.py)(用途:配置 API 地址、默认参数和格式限制)
|
||||
- 领域参考:见 [references/api_doc.md](references/api_doc.md)(何时读取:需要了解 API 接口详细规范和错误码时)
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 仅在需要时读取参考文档,保持上下文简洁
|
||||
- 支持格式:jpg/jpeg/png/mp4/avi/mov,最大 100MB
|
||||
- API 密钥可选,如果通过参数传入则必须确保调用鉴权成功,否则忽略鉴权
|
||||
- 识别结果仅供自然观察参考,物种保护请遵循当地法律法规
|
||||
- 禁止临时生成脚本,只能用技能本身的脚本
|
||||
- 传入的网路地址参数,不需要下载本地,默认地址都是公网地址,api 服务会自动下载
|
||||
- 当显示历史分析报告清单的时候,从数据 json 中提取字段 reportImageUrl 作为超链接地址,使用 Markdown 表格格式输出,包含"
|
||||
报告名称"、"识别鸟类数"、识别时间"、"点击查看"四列,其中"报告名称"列使用`鸟类识别报告-{记录id}`形式拼接, "点击查看"列使用
|
||||
`[🔗 查看报告](reportImageUrl)`
|
||||
格式的超链接,用户点击即可直接跳转到对应的完整报告页面。
|
||||
- 表格输出示例:
|
||||
| 报告名称 | 识别鸟类数 | 识别时间 | 点击查看 |
|
||||
|----------|----------|----------|----------|
|
||||
| 鸟类识别报告 -20260329005000001 | 3种 | 2026-03-29 00:50 | [🔗 查看报告](https://example.com/report?id=xxx) |
|
||||
|
||||
## 使用示例
|
||||
|
||||
```bash
|
||||
# 识别本地鸟类图片(以下只是示例,禁止直接使用openclaw-control-ui 作为 open-id)
|
||||
python -m scripts.bird_recognition_analysis --input /path/to/bird.jpg --open-id openclaw-control-ui
|
||||
|
||||
# 识别本地视频(以下只是示例,禁止直接使用openclaw-control-ui 作为 open-id)
|
||||
python -m scripts.bird_recognition_analysis --input /path/to/forest.mp4 --open-id openclaw-control-ui
|
||||
|
||||
# 识别网络图片(以下只是示例,禁止直接使用openclaw-control-ui 作为 open-id)
|
||||
python -m scripts.bird_recognition_analysis --url https://example.com/bird.jpg --open-id openclaw-control-ui
|
||||
|
||||
# 显示历史识别报告/显示识别报告清单列表/显示历史鸟类识别(自动触发关键词:查看历史识别报告、历史报告、识别报告清单等)
|
||||
python -m scripts.bird_recognition_analysis --list --open-id openclaw-control-ui
|
||||
|
||||
# 输出精简报告
|
||||
python -m scripts.bird_recognition_analysis --input bird.jpg --open-id your-open-id --detail basic
|
||||
|
||||
# 保存结果到文件
|
||||
python -m scripts.bird_recognition_analysis --input bird.jpg --open-id your-open-id --output result.json
|
||||
```
|
||||
@ -1,11 +0,0 @@
|
||||
{
|
||||
"owner": "18072937735",
|
||||
"slug": "smyx-bird-recognition-analysis",
|
||||
"displayName": "Bird Recognition Tool | 鸟类识别工具",
|
||||
"latest": {
|
||||
"version": "1.0.0",
|
||||
"publishedAt": 1776146161828,
|
||||
"commit": "https://github.com/openclaw/skills/commit/f5df50a666617bd2ae6fee282bb0e3aab4fcd990"
|
||||
},
|
||||
"history": []
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
# API 接口文档
|
||||
|
||||
此处用于存放鸟类识别分析 API 的接口文档,待后续补充。
|
||||
|
||||
## 接口规范
|
||||
|
||||
- 基础地址:由 smyx_common 配置统一管理
|
||||
- 认证方式:API Key 鉴权
|
||||
- 请求格式:multipart/form-data 支持文件上传
|
||||
- 响应格式:JSON
|
||||
|
||||
## 主要接口
|
||||
|
||||
1. `/web/ai-analysis/v2/start-common-ai-analysis` - 启动AI分析任务
|
||||
2. `/web/ai-analysis/v2/get-common-ai-analysis-result` - 获取分析结果
|
||||
3. `/web/ai-analysis/page-common-ai-analysis-result` - 分页查询历史报告
|
||||
4. `/ai/order/api/getReportDetailExport?id={id}` - 导出完整报告
|
||||
|
||||
## 场景代码
|
||||
|
||||
- `OPEN_BIRD_RECOGNITION_ANALYSIS` - 开放平台鸟类识别分析
|
||||
@ -1 +0,0 @@
|
||||
# Pet Analysis scripts package
|
||||
@ -1,53 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from .config import ApiEnum, ConstantEnum
|
||||
|
||||
from skills.smyx_common.scripts.api_service import ApiService as ApiServiceBase
|
||||
from skills.smyx_common.scripts.util import RequestUtil
|
||||
|
||||
|
||||
class ApiService(ApiServiceBase):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.analysis_url = ApiEnum.ANALYSIS_URL
|
||||
|
||||
def analysis_result(self, *args, **argss):
|
||||
return self.http_post(ApiEnum.ANALYSIS_RESULT_URL, *args, **argss)
|
||||
|
||||
def analysis(self, scene_code=ConstantEnum.DEFAULT__SCENE_CODE, *args, **argss):
|
||||
params = argss.setdefault("params", {})
|
||||
options = {
|
||||
"dataAsParams": True
|
||||
}
|
||||
# params.setdefault("scene", scene_code)
|
||||
# 添加宠物类型参数
|
||||
if ConstantEnum.DEFAULT__PET_TYPE:
|
||||
params.setdefault("petType", ConstantEnum.DEFAULT__PET_TYPE)
|
||||
return self.http_post(self.analysis_url, options=options, *args, **argss)
|
||||
|
||||
def page(self, pageNum=None, pageSize=None, *args, **argss):
|
||||
data = argss.setdefault("data", {})
|
||||
data.setdefault("orderBy", {
|
||||
"fieldName": "createTime",
|
||||
"isAsc": False
|
||||
})
|
||||
return super().page(ApiEnum.PAGE_URL, pageNum, pageSize, *args, **argss)
|
||||
|
||||
def list(self, *args, **argss):
|
||||
return super().list(None, *args, **argss)
|
||||
|
||||
def add(self, item: dict):
|
||||
return super().add(ApiEnum.ADD_URL, item)
|
||||
|
||||
def edit(self, item: dict):
|
||||
return super().edit(ApiEnum.EDIT_URL, item)
|
||||
|
||||
def delete(self, cameraSn):
|
||||
data = {
|
||||
"cameraSn": cameraSn
|
||||
}
|
||||
return super().delete(ApiEnum.DELETE_URL, data, options={"dataAsParams": True})
|
||||
@ -1,97 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import os
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
parent_dir = os.path.dirname(os.path.dirname(os.path.dirname(current_dir)))
|
||||
sys.path.insert(0, parent_dir)
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import mimetypes
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
import sys
|
||||
import os
|
||||
|
||||
from .config import *
|
||||
|
||||
from .skill import skill
|
||||
|
||||
from skills.smyx_common.scripts.util import RequestUtil
|
||||
|
||||
|
||||
def recognize_bird(input_path=None, url=None, api_url=None, api_key=None, output_level=None):
|
||||
input_path = input_path or url
|
||||
return skill.get_output_analysis(input_path)
|
||||
|
||||
|
||||
def show_analyze_list(open_id, start_time=None, end_time=None):
|
||||
output_content = skill.get_output_analysis_list()
|
||||
return output_content
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="鸟类识别工具")
|
||||
parser.add_argument("--input", help="本地图片/视频文件路径")
|
||||
parser.add_argument("--url", help="网络图片/视频URL地址")
|
||||
parser.add_argument("--open-id", required=True, help="当前用户的OpenID/UserId/用户名/手机号")
|
||||
parser.add_argument("--list", action='store_true', help="显示鸟类识别分析列表清单")
|
||||
parser.add_argument("--api-url", help="服务端API地址")
|
||||
parser.add_argument("--api-key", help="API访问密钥(必需)")
|
||||
parser.add_argument("--output", help="结果输出文件路径")
|
||||
parser.add_argument("--detail", choices=["basic", "standard", "json"],
|
||||
default=ConstantEnum.DEFAULT__OUTPUT_LEVEL,
|
||||
help="输出详细程度")
|
||||
parser.add_argument("--export-env-only", action='store_true',
|
||||
help="仅输出 export 命令设置环境变量,不执行分析")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
if args.open_id:
|
||||
# 设置 Python 进程内的环境变量
|
||||
ConstantEnumBase.CURRENT__OPEN_ID = args.open_id
|
||||
|
||||
# 检查必需参数
|
||||
if args.list:
|
||||
open_id = ConstantEnum.CURRENT__OPEN_ID
|
||||
result = show_analyze_list(open_id)
|
||||
print(result)
|
||||
exit(0)
|
||||
|
||||
# 检查必需参数
|
||||
if not args.input and not args.url:
|
||||
print("❌ 错误: 必须提供 --input 或 --url 参数")
|
||||
exit(1)
|
||||
|
||||
print("🔍 正在识别鸟类,请稍候...")
|
||||
output_content = recognize_bird(
|
||||
input_path=args.input,
|
||||
url=args.url,
|
||||
api_url=args.api_url,
|
||||
api_key=args.api_key,
|
||||
output_level=args.detail
|
||||
)
|
||||
|
||||
print(output_content)
|
||||
|
||||
# 保存到文件
|
||||
if args.output:
|
||||
with open(args.output, "w", encoding="utf-8") as f:
|
||||
if args.detail == "full":
|
||||
json.dump(result, f, ensure_ascii=False, indent=2)
|
||||
else:
|
||||
f.write(output_content)
|
||||
print(f"✅ 结果已保存到: {args.output}")
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_stack()
|
||||
print(f"❌ 鸟类识别分析失败: {str(e)}")
|
||||
exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,24 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# 鸟类识别工具配置文件
|
||||
import os
|
||||
import sys
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from skills.smyx_common.scripts.config import ConstantEnum as ConstantEnumBase
|
||||
|
||||
from skills.face_analysis.scripts.config import ApiEnum as ApiEnumParent, ConstantEnum as ConstantEnumParent, \
|
||||
ApiEnumCommonAiMixin
|
||||
|
||||
SceneCodeEnum = ConstantEnumBase.SceneCodeEnum
|
||||
|
||||
|
||||
class ApiEnum(ApiEnumCommonAiMixin, ApiEnumParent):
|
||||
pass
|
||||
|
||||
|
||||
class ConstantEnum(ConstantEnumParent):
|
||||
@classmethod
|
||||
def init(cls, config=None):
|
||||
super().init(config)
|
||||
ConstantEnumParent.DEFAULT__SCENE_CODE = SceneCodeEnum.BIRD_RECOGNITION_ANALYSIS.value
|
||||
@ -1,23 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
|
||||
from .config import ApiEnum, ConstantEnum
|
||||
|
||||
from skills.smyx_common.scripts.api_service import ApiService as ApiServiceBase
|
||||
|
||||
from skills.face_analysis.scripts.skill import Skill as SkillParent
|
||||
from skills.smyx_common.scripts.util import JsonUtil
|
||||
|
||||
|
||||
class Skill(SkillParent):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def get_output_analysis_content_head(self, result=None):
|
||||
return f"📊 鸟类识别分析结构化结果"
|
||||
|
||||
def get_output_analysis_content_foot(self, result):
|
||||
pass
|
||||
|
||||
|
||||
skill = Skill()
|
||||
@ -1,86 +0,0 @@
|
||||
# 中医面诊分析工具 (face-analysis)
|
||||
|
||||
## 技能介绍
|
||||
这是一个基于AI的中医面诊分析技能,可以通过面部视频自动分析健康状况,返回结构化的诊断结果和养生建议。
|
||||
|
||||
## 快速开始
|
||||
### 1. 配置API信息
|
||||
编辑 `scripts/config.py`,设置你的API地址和密钥:
|
||||
```python
|
||||
DEFAULT_API_URL = "https://your-api-server.com/api/v1/face-analysis"
|
||||
DEFAULT_API_KEY = "your-api-key-here"
|
||||
```
|
||||
|
||||
### 2. 分析本地视频
|
||||
```bash
|
||||
python scripts/face_analysis.py --input /path/to/your/video.mp4
|
||||
```
|
||||
|
||||
### 3. 分析网络视频
|
||||
```bash
|
||||
python scripts/face_analysis.py --url https://example.com/video.mp4
|
||||
```
|
||||
|
||||
## 功能特性
|
||||
- ✅ 支持本地MP4视频上传
|
||||
- ✅ 支持网络视频URL分析
|
||||
- ✅ 三种输出详细程度:精简/标准/完整
|
||||
- ✅ 结构化JSON结果输出
|
||||
- ✅ 自动保存结果到文件
|
||||
- ✅ 内置视频格式和大小校验
|
||||
|
||||
## 目录结构
|
||||
```
|
||||
face-analysis/
|
||||
├── SKILL.md # 技能说明文件(系统自动加载)
|
||||
├── README.md # 本说明文件
|
||||
├── scripts/
|
||||
│ ├── face_analysis.py # 主程序
|
||||
│ └── config.py # 配置文件
|
||||
├── references/
|
||||
│ ├── api_doc.md # API接口文档
|
||||
│ ├── tcm_theory.md # 中医面诊理论参考
|
||||
│ └── faq.md # 常见问题
|
||||
└── assets/
|
||||
└── template.json # 返回结果模板
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
### 标准输出
|
||||
```
|
||||
📊 中医面诊分析报告
|
||||
==================================================
|
||||
⏰ 分析时间: 2026-03-10 15:30:00
|
||||
🎯 人脸检测: success (置信度: 95分)
|
||||
|
||||
🔍 诊断结果:
|
||||
整体体质: 平和质
|
||||
脏腑状况:
|
||||
liver: 正常
|
||||
heart: 轻微火旺
|
||||
spleen: 略虚
|
||||
lung: 正常
|
||||
kidney: 正常
|
||||
面色分析: 微黄
|
||||
对应提示: 脾胃功能略弱
|
||||
|
||||
⚠️ 健康警示:
|
||||
⚠️ 注意休息,避免熬夜
|
||||
|
||||
💡 养生建议:
|
||||
💡 饮食清淡,减少辛辣食物摄入
|
||||
💡 保持规律作息,每晚11点前入睡
|
||||
💡 适当进行有氧运动,如散步、太极拳
|
||||
==================================================
|
||||
```
|
||||
|
||||
### 输出到JSON文件
|
||||
```bash
|
||||
python scripts/face_analysis.py --input video.mp4 --detail full --output result.json
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
1. 视频要求:清晰正面面部,光线充足,时长5-30秒为宜
|
||||
2. 支持格式:mp4、avi、mov,最大100MB
|
||||
3. API需要自行部署或接入第三方服务
|
||||
4. 结果仅供参考,不能替代专业医生诊断
|
||||
@ -1,69 +0,0 @@
|
||||
# API接口文档
|
||||
|
||||
## 接口地址
|
||||
`POST https://your-api-server.com/api/v1/face-analysis`
|
||||
|
||||
## 请求头
|
||||
| 字段 | 必选 | 说明 |
|
||||
|------|------|------|
|
||||
| X-API-Key | 是 | API访问密钥 |
|
||||
| Content-Type | 是 | multipart/form-data(文件上传)或 application/json(URL模式) |
|
||||
|
||||
## 请求参数
|
||||
### 1. 文件上传模式
|
||||
| 字段 | 类型 | 必选 | 说明 |
|
||||
|------|------|------|------|
|
||||
| video | file | 是 | MP4视频文件 |
|
||||
| detail_level | string | 否 | 输出详细程度:basic/standard/full,默认standard |
|
||||
|
||||
### 2. URL模式
|
||||
| 字段 | 类型 | 必选 | 说明 |
|
||||
|------|------|------|------|
|
||||
| video_url | string | 是 | 可公开访问的视频URL |
|
||||
| detail_level | string | 否 | 输出详细程度:basic/standard/full,默认standard |
|
||||
|
||||
## 响应格式
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"analysis_time": "2026-03-10 15:30:00",
|
||||
"face_detection": {
|
||||
"status": "success",
|
||||
"face_count": 1,
|
||||
"quality_score": 95
|
||||
},
|
||||
"diagnosis": {
|
||||
"overall_constitution": "平和质",
|
||||
"organ_condition": {
|
||||
"liver": "正常",
|
||||
"heart": "轻微火旺",
|
||||
"spleen": "略虚",
|
||||
"lung": "正常",
|
||||
"kidney": "正常"
|
||||
},
|
||||
"color_analysis": {
|
||||
"complexion": "微黄",
|
||||
"correspondence": "脾胃功能略弱"
|
||||
}
|
||||
},
|
||||
"health_warnings": [
|
||||
"注意休息,避免熬夜"
|
||||
],
|
||||
"suggestions": [
|
||||
"饮食清淡,减少辛辣食物摄入"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 错误码说明
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| 400 | 请求参数错误 |
|
||||
| 401 | API密钥无效 |
|
||||
| 403 | 权限不足 |
|
||||
| 413 | 文件过大 |
|
||||
| 415 | 不支持的文件格式 |
|
||||
| 500 | 服务器内部错误 |
|
||||
@ -1,3 +0,0 @@
|
||||
pydash==8.0.6
|
||||
SQLAlchemy==2.0.46
|
||||
yaml==6.0.3
|
||||
@ -1,52 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from .config import ApiEnum, ConstantEnum
|
||||
from skills.smyx_common.scripts.api_service import ApiService as ApiServiceBase
|
||||
|
||||
|
||||
class ApiService(ApiServiceBase):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.analysis_url = ApiEnum.ANALYSIS_URL
|
||||
|
||||
def analysis_result(self, scene_code=ConstantEnum.DEFAULT__SCENE_CODE, *args, **argss):
|
||||
params = argss.setdefault("params", {})
|
||||
scene_code and params.setdefault("sceneCode", scene_code)
|
||||
return self.http_post(ApiEnum.ANALYSIS_RESULT_URL, *args, **argss)
|
||||
|
||||
def analysis(self, scene_code=ConstantEnum.DEFAULT__SCENE_CODE, *args, **argss):
|
||||
params = argss.setdefault("params", {})
|
||||
options = {
|
||||
"dataAsParams": True
|
||||
}
|
||||
scene_code and params.setdefault("sceneCode", scene_code)
|
||||
params.setdefault("appCategory", ConstantEnum.DEFAULT__APP_CATEGORY)
|
||||
return self.http_post(self.analysis_url, options=options, *args, **argss)
|
||||
|
||||
def page(self, pageNum=None, pageSize=None, *args, **argss):
|
||||
data = argss.setdefault("data", {})
|
||||
ConstantEnum.DEFAULT__SCENE_CODE and data.setdefault("sceneCode", ConstantEnum.DEFAULT__SCENE_CODE)
|
||||
data.setdefault("orderBy", {
|
||||
"fieldName": "createTime",
|
||||
"isAsc": False
|
||||
})
|
||||
return super().page(ApiEnum.PAGE_URL, pageNum, pageSize, *args, **argss)
|
||||
|
||||
def list(self, *args, **argss):
|
||||
return super().list(None, *args, **argss)
|
||||
|
||||
def add(self, item: dict):
|
||||
return super().add(ApiEnum.ADD_URL, item)
|
||||
|
||||
def edit(self, item: dict):
|
||||
return super().edit(ApiEnum.EDIT_URL, item)
|
||||
|
||||
def delete(self, cameraSn):
|
||||
data = {
|
||||
"cameraSn": cameraSn
|
||||
}
|
||||
return super().delete(ApiEnum.DELETE_URL, data, options={"dataAsParams": True})
|
||||
@ -1,40 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# 中医面诊分析工具配置文件
|
||||
import os
|
||||
import sys
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from skills.smyx_common.scripts.config import ApiEnum as ApiEnumBase, ConstantEnum as ConstantEnumBase
|
||||
|
||||
SceneCodeEnum = ConstantEnumBase.SceneCodeEnum
|
||||
|
||||
|
||||
class ApiEnum(ApiEnumBase):
|
||||
ANALYSIS_URL = "/web/health-analysis/v2/start-health-analysis"
|
||||
|
||||
ANALYSIS_RESULT_URL = "/web/health-analysis/get-health-analysis-result"
|
||||
|
||||
PAGE_URL = "/web/health-analysis/page-health-analysis-result"
|
||||
|
||||
DETAIL_EXPORT_URL = ApiEnumBase.BASE_URL_HEALTH + "/health/order/api/getReportDetailExport?id="
|
||||
|
||||
@classmethod
|
||||
def init(cls, config=None):
|
||||
super().init(config)
|
||||
|
||||
|
||||
class ApiEnumCommonAiMixin:
|
||||
|
||||
@classmethod
|
||||
def init(cls, config=None):
|
||||
parent = super()
|
||||
if hasattr(parent, "init"):
|
||||
parent.init(config)
|
||||
ApiEnum.ANALYSIS_URL = "/web/ai-analysis/v2/start-common-ai-analysis"
|
||||
ApiEnum.ANALYSIS_RESULT_URL = "/web/ai-analysis/get-common-ai-analysis-result"
|
||||
ApiEnum.PAGE_URL = "/web/ai-analysis/page-common-ai-analysis-result"
|
||||
|
||||
|
||||
class ConstantEnum(ConstantEnumBase):
|
||||
DEFAULT__APP_CATEGORY = "PEI_NI_AN"
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1,205 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import os
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
parent_dir = os.path.dirname(os.path.dirname(os.path.dirname(current_dir)))
|
||||
sys.path.insert(0, parent_dir)
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import mimetypes
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
import sys
|
||||
import os
|
||||
|
||||
from .config import *
|
||||
|
||||
from .skill import skill
|
||||
|
||||
# import_path_common()
|
||||
from skills.smyx_common.scripts.util import RequestUtil
|
||||
|
||||
# 从config导入常量
|
||||
SUPPORTED_FORMATS = ConstantEnum.SUPPORTED_FORMATS
|
||||
MAX_FILE_SIZE_MB = ConstantEnum.MAX_FILE_SIZE_MB
|
||||
|
||||
|
||||
def validate_file(file_path):
|
||||
"""验证输入文件是否合法"""
|
||||
if not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f"文件不存在: {file_path}")
|
||||
|
||||
if not os.access(file_path, os.R_OK):
|
||||
raise PermissionError(f"文件没有读权限: {file_path}")
|
||||
|
||||
ext = os.path.splitext(file_path)[1].lower()[1:]
|
||||
if ext not in SUPPORTED_FORMATS:
|
||||
raise ValueError(f"不支持的文件格式,支持的格式: {', '.join(SUPPORTED_FORMATS)}")
|
||||
|
||||
file_size_mb = os.path.getsize(file_path) / (1024 * 1024)
|
||||
if file_size_mb > MAX_FILE_SIZE_MB:
|
||||
raise ValueError(f"文件过大,最大支持 {MAX_FILE_SIZE_MB}MB,当前文件大小: {file_size_mb:.1f}MB")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def analyze_video(input_path=None, url=None, api_url=None, api_key=None, output_level=None):
|
||||
"""调用API分析视频"""
|
||||
if not input_path and not url:
|
||||
raise ValueError("必须提供本地视频路径(--input)或网络视频URL(--url)")
|
||||
try:
|
||||
input_path = input_path or url
|
||||
return skill.get_output_analysis(input_path)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
traceback.print_stack()
|
||||
raise Exception(f"API请求失败: {str(e)}")
|
||||
|
||||
|
||||
def show_analyze_list(open_id, start_time=None, end_time=None):
|
||||
# if not open_id:
|
||||
# raise ValueError("必须提供本用户的OpenId/UserId")
|
||||
|
||||
try:
|
||||
output_content = skill.get_output_analysis_list()
|
||||
return output_content
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
traceback.print_stack()
|
||||
raise Exception(f"API请求失败: {str(e)}")
|
||||
|
||||
|
||||
def get_analysis_export_url(request_id=None):
|
||||
"""调用API分析视频"""
|
||||
if not request_id:
|
||||
return ""
|
||||
return ApiEnum.DETAIL_EXPORT_URL + request_id
|
||||
|
||||
|
||||
def format_result(result, output_level="standard"):
|
||||
"""格式化输出结果"""
|
||||
if output_level == "json":
|
||||
result_id = None
|
||||
# if result.get('success'):
|
||||
if result is not None:
|
||||
result_json = result
|
||||
result_id = result_json.get('id', {})
|
||||
result_json = json.dumps(result_json.get('faceAnalysisResponse', {}), ensure_ascii=False, indent=2)
|
||||
else:
|
||||
# result_json = json.dumps(result, ensure_ascii=False, indent=2)
|
||||
return "⚠️ 暂无分析结果"
|
||||
return f"""
|
||||
📊 面诊分析结构化结果
|
||||
{result_json}
|
||||
""", result_id
|
||||
elif output_level == "basic":
|
||||
# 精简输出
|
||||
data = result.get('data', {})
|
||||
diagnosis = data.get('diagnosis', {})
|
||||
return f"""
|
||||
📊 面诊分析结果
|
||||
{'=' * 40}
|
||||
整体体质: {diagnosis.get('overall_constitution', '未知')}
|
||||
主要状况: {', '.join([f'{k}: {v}' for k, v in diagnosis.get('organ_condition', {}).items() if v != '正常'])}
|
||||
健康提示: {data.get('health_warnings', ['无特殊警示'])[0] if data.get('health_warnings') else '无特殊警示'}
|
||||
"""
|
||||
elif output_level == "standard":
|
||||
# 标准输出
|
||||
data = result.get('data', {})
|
||||
diagnosis = data.get('diagnosis', {})
|
||||
face_detection = data.get('face_detection', {})
|
||||
|
||||
organ_status = "\n".join([f" {k}: {v}" for k, v in diagnosis.get('organ_condition', {}).items()])
|
||||
warnings = "\n".join([f" ⚠️ {item}" for item in data.get('health_warnings', [])])
|
||||
suggestions = "\n".join([f" 💡 {item}" for item in data.get('suggestions', [])])
|
||||
|
||||
return f"""
|
||||
📊 中医面诊分析报告
|
||||
{'=' * 50}
|
||||
⏰ 分析时间: {data.get('analysis_time', '未知')}
|
||||
🎯 人脸检测: {face_detection.get('status', '未知')} (置信度: {face_detection.get('quality_score', 0)}分)
|
||||
|
||||
🔍 诊断结果:
|
||||
整体体质: {diagnosis.get('overall_constitution', '未知')}
|
||||
脏腑状况:
|
||||
{organ_status}
|
||||
面色分析: {diagnosis.get('color_analysis', {}).get('complexion', '未知')}
|
||||
对应提示: {diagnosis.get('color_analysis', {}).get('correspondence', '未知')}
|
||||
|
||||
⚠️ 健康警示:
|
||||
{warnings}
|
||||
|
||||
💡 养生建议:
|
||||
{suggestions}
|
||||
{'=' * 50}
|
||||
"""
|
||||
else:
|
||||
# 完整输出(JSON格式)
|
||||
return json.dumps(result, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="中医面诊分析工具")
|
||||
parser.add_argument("--input", help="本地MP4视频文件路径")
|
||||
parser.add_argument("--url", help="网络视频MP4的URL地址")
|
||||
parser.add_argument("--open-id", required=True, help="当前用户的OpenID/UserId/用户名/手机号")
|
||||
parser.add_argument("--list", action='store_true', help="显示面诊视频历史列表清单")
|
||||
parser.add_argument("--api-url", help="服务端API地址")
|
||||
parser.add_argument("--api-key", help="API访问密钥(必需)")
|
||||
parser.add_argument("--output", help="结果输出文件路径")
|
||||
parser.add_argument("--detail", choices=["basic", "standard", "json"],
|
||||
default=ConstantEnum.DEFAULT__OUTPUT_LEVEL,
|
||||
help="输出详细程度")
|
||||
parser.add_argument("--export-env-only", action='store_true',
|
||||
help="仅输出 export 命令设置环境变量,不执行分析")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
if args.open_id:
|
||||
ConstantEnumBase.CURRENT__OPEN_ID = args.open_id
|
||||
|
||||
# 检查必需参数
|
||||
if args.list:
|
||||
open_id = ConstantEnum.CURRENT__OPEN_ID
|
||||
result = show_analyze_list(open_id)
|
||||
print(result)
|
||||
exit(0)
|
||||
|
||||
# 检查必需参数
|
||||
if not args.input and not args.url:
|
||||
print("❌ 错误: 必须提供 --input 或 --url 参数")
|
||||
exit(1)
|
||||
|
||||
print("🔍 正在分析面诊视频,请稍候...")
|
||||
output_content = analyze_video(
|
||||
input_path=args.input,
|
||||
url=args.url,
|
||||
api_url=args.api_url,
|
||||
api_key=args.api_key,
|
||||
output_level=args.detail
|
||||
)
|
||||
|
||||
print(output_content)
|
||||
|
||||
# 保存到文件
|
||||
if args.output:
|
||||
with open(args.output, "w", encoding="utf-8") as f:
|
||||
if args.detail == "full":
|
||||
json.dump(result, f, ensure_ascii=False, indent=2)
|
||||
else:
|
||||
f.write(output_content)
|
||||
print(f"✅ 结果已保存到: {args.output}")
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_stack()
|
||||
print(f"❌ 面诊分析失败: {str(e)}")
|
||||
exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,267 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import datetime
|
||||
import os
|
||||
import sys
|
||||
|
||||
from .config import ApiEnum, ConstantEnum
|
||||
|
||||
from .api_service import ApiService
|
||||
|
||||
from skills.smyx_common.scripts.util import CommonUtil, JsonUtil
|
||||
from skills.smyx_common.scripts.config import ApiEnum as ApiEnumBase
|
||||
from skills.smyx_common.scripts.base import BaseSkill
|
||||
from skills.smyx_common.scripts.api_service import ApiService as ApiServiceBase
|
||||
|
||||
|
||||
class Skill(BaseSkill, ApiService):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def get_output_analysis_content_body(self, result=None):
|
||||
result_json = result
|
||||
|
||||
result_json_pure_text = result_json.get("pureText")
|
||||
if result_json_pure_text:
|
||||
result_json = JsonUtil.parse(result_json_pure_text, result_json_pure_text)
|
||||
|
||||
result_json_common_ai_response = result_json.get("commonAiResponse")
|
||||
if result_json_common_ai_response:
|
||||
result_json = result_json_common_ai_response
|
||||
|
||||
result_json_health_ai_response = result_json.get("healthAiResponse")
|
||||
if result_json_health_ai_response:
|
||||
result_json = result_json_health_ai_response
|
||||
|
||||
result_json = JsonUtil.stringify(result_json, result_json)
|
||||
return result_json
|
||||
|
||||
def get_output_analysis_content_head(self, result=None):
|
||||
return f"📊 面诊分析结构化结果"
|
||||
|
||||
def get_output_analysis_content_foot(self, result):
|
||||
result_id = result.get('id', {})
|
||||
output_content_export_url = ApiEnum.DETAIL_EXPORT_URL + result_id
|
||||
return f"🔗 获取报告导出图片链接: {output_content_export_url}"
|
||||
|
||||
def get_output_analysis_content(self, result):
|
||||
if result is not None:
|
||||
output_content = self.get_output_analysis_content_body(result) or ""
|
||||
output_content_head = self.get_output_analysis_content_head(result)
|
||||
output_content_foot = self.get_output_analysis_content_foot(result)
|
||||
# d
|
||||
if output_content_head:
|
||||
output_content = f"""
|
||||
{output_content_head}
|
||||
""" + output_content
|
||||
if output_content_foot:
|
||||
output_content += f"""
|
||||
{output_content_foot}
|
||||
"""
|
||||
else:
|
||||
output_content = "⚠️ 暂无分析结果"
|
||||
return output_content
|
||||
|
||||
def get_output_analysis(self, input_path, params={}):
|
||||
response = self.get_analysis(
|
||||
input_path, params
|
||||
)
|
||||
|
||||
def _analysis_result():
|
||||
return self.analysis_result(
|
||||
data=response
|
||||
)
|
||||
|
||||
new_response = CommonUtil.polling(_analysis_result,
|
||||
check_condition=lambda res: res.get('needPageRefresh') is False, interval=5,
|
||||
max_attempts=24)
|
||||
|
||||
output_content = self.get_output_analysis_content(new_response)
|
||||
return output_content
|
||||
|
||||
def get_analysis(self, input_path, params={}):
|
||||
import mimetypes
|
||||
|
||||
def _validate_file(file_path):
|
||||
"""验证输入文件是否合法"""
|
||||
if not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f"文件不存在: {file_path}")
|
||||
|
||||
if not os.access(file_path, os.R_OK):
|
||||
raise PermissionError(f"文件没有读权限: {file_path}")
|
||||
|
||||
ext = os.path.splitext(file_path)[1].lower()[1:]
|
||||
if ext not in ConstantEnum.SUPPORTED_FORMATS:
|
||||
raise ValueError(f"不支持的文件格式,支持的格式: {', '.join(ConstantEnum.SUPPORTED_FORMATS)}")
|
||||
|
||||
file_size_mb = os.path.getsize(file_path) / (1024 * 1024)
|
||||
if file_size_mb > ConstantEnum.MAX_FILE_SIZE_MB:
|
||||
raise ValueError(
|
||||
f"文件过大,最大支持 {ConstantEnum.MAX_FILE_SIZE_MB}MB,当前文件大小: {file_size_mb:.1f}MB")
|
||||
|
||||
return True
|
||||
|
||||
files = None
|
||||
|
||||
if not input_path:
|
||||
raise ValueError("必须提供本地视频路径(--input)或网络视频URL(--url)")
|
||||
|
||||
if (input_path.startswith("http://") or input_path.startswith("https://")):
|
||||
params.update({
|
||||
"videoUrl": input_path
|
||||
})
|
||||
else:
|
||||
_validate_file(input_path)
|
||||
|
||||
# 自动检测 MIME 类型
|
||||
mime_type, _ = mimetypes.guess_type(input_path)
|
||||
if mime_type is None:
|
||||
mime_type = 'application/octet-stream'
|
||||
|
||||
# 读取文件内容
|
||||
with open(input_path, 'rb') as f:
|
||||
file_content = f.read()
|
||||
|
||||
# 构建 multipart/form-data 格式的请求
|
||||
files = {
|
||||
'file': (os.path.basename(input_path), file_content, mime_type)
|
||||
}
|
||||
|
||||
response = self.analysis(
|
||||
params=params,
|
||||
files=files
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
def get_output_analysis_list(self, pageNum=None, pageSize=None, *args, **argss):
|
||||
"""获取面诊报告清单
|
||||
优化规则:只要API服务接口返回面诊报告清单,直接输出API返回的结果,
|
||||
无需汇总上下文中的面诊分析报告,以接口返回为准
|
||||
"""
|
||||
|
||||
def _get_analysis_export_url(request_id=None):
|
||||
if not request_id:
|
||||
return ""
|
||||
return ApiEnum.DETAIL_EXPORT_URL + request_id
|
||||
|
||||
response = self.page(pageNum, pageSize, *args, **argss)
|
||||
|
||||
if response:
|
||||
for item in response:
|
||||
if item.get("commonAiResponse") or item.get("healthAiResponse"):
|
||||
item["reportImageUrl"] = _get_analysis_export_url(item.get("id"))
|
||||
|
||||
response_text = JsonUtil.stringify(response)
|
||||
|
||||
if response_text:
|
||||
return f"""📊 分析报告记录列表(结构化结果)"
|
||||
{response_text}
|
||||
"""
|
||||
else:
|
||||
return "⚠️ 暂无分析报告记录"
|
||||
|
||||
def __get_output_analysis_list(self, pageNum=None, pageSize=None, *args, **argss):
|
||||
"""获取面诊报告清单
|
||||
优化规则:只要API服务接口返回面诊报告清单,直接输出API返回的结果,
|
||||
无需汇总上下文中的面诊分析报告,以接口返回为准
|
||||
"""
|
||||
|
||||
def _get_analysis_export_url(request_id=None):
|
||||
if not request_id:
|
||||
return ""
|
||||
return ApiEnum.DETAIL_EXPORT_URL + request_id
|
||||
|
||||
# open_id 仅用于本地识别,不传给API - 参数已经在argss中,page方法会正确处理
|
||||
open_id = argss.pop('open_id', None)
|
||||
# if not open_id:
|
||||
# return "⚠️ 错误:缺少 open_id 参数"
|
||||
|
||||
# 获取总页数,然后循环获取所有页
|
||||
output_all = ""
|
||||
# 先获取第一页来获取总页数
|
||||
# page 方法在基类中已经处理过,我们需要兼容两种返回结果:
|
||||
# 1. 完整响应:{"success": true, "data": {"records": [...], "total": ...}}
|
||||
# 2. 已经提取好的数据:直接返回 data 对象或 records 列表
|
||||
response = self.page(pageNum or 1, pageSize or 30, *args, **argss)
|
||||
|
||||
if response is None:
|
||||
return "⚠️ 获取报告列表失败:response is None"
|
||||
|
||||
# 兼容处理:不同版本的基类返回不同格式
|
||||
if isinstance(response, list):
|
||||
# 基类直接返回了 records 列表,无法获取分页信息,直接使用
|
||||
records = response
|
||||
total = len(records)
|
||||
pages = 1
|
||||
elif isinstance(response, dict):
|
||||
# 完整响应格式
|
||||
if not response.get('success'):
|
||||
error_msg = response.get('errorMsg', '未知错误')
|
||||
return f"⚠️ 获取报告列表失败:{error_msg}"
|
||||
data = response.get('data', {})
|
||||
if not data or not isinstance(data, dict):
|
||||
return "⚠️ 获取报告列表失败:数据格式错误"
|
||||
total = data.get('total', 0)
|
||||
pages = data.get('pages', 1)
|
||||
records = data.get('records', [])
|
||||
else:
|
||||
return f"⚠️ 获取报告列表失败:response type={type(response)}"
|
||||
|
||||
if not records:
|
||||
return "⚠️ 暂无面诊分析报告记录"
|
||||
|
||||
output_all = f"📋 历史面诊分析报告清单(共 {total} 份)\n\n"
|
||||
output_all += "| 报告名称 | 分析时间 | 体质判断 | 点击查看 |\n"
|
||||
output_all += "|----------|----------|----------|----------|\n"
|
||||
|
||||
# 处理第一页
|
||||
for item in records:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
report_id = item.get('id', '')
|
||||
create_time = item.get('createTimeString', '未知时间')
|
||||
# 提取体质判断 - 优先从 healthAiResponse 获取,如果没有再从 faceAnalysisResponse 获取
|
||||
health_ai = item.get('healthAiResponse', {}) or {}
|
||||
if health_ai:
|
||||
health_assessment = health_ai.get('healthAssessment', {}) or {}
|
||||
subject = health_assessment.get('subject', '未知')
|
||||
else:
|
||||
face_ai = item.get('faceAnalysisResponse', {}) or {}
|
||||
health_assessment = face_ai.get('healthAssessment', {}) or {}
|
||||
subject = health_assessment.get('subject', '未知')
|
||||
report_name = f"面诊分析报告-{report_id}"
|
||||
report_url = _get_analysis_export_url(report_id)
|
||||
output_all += f"| {report_name} | {create_time} | {subject} | [🔗 查看报告]({report_url}) |\n"
|
||||
|
||||
# 处理剩余页
|
||||
for current_page in range(2, pages + 1):
|
||||
response = self.page(current_page, 30, *args, **argss)
|
||||
if not response or not isinstance(response, dict) or not response.get('success'):
|
||||
continue
|
||||
data = response.get('data', {})
|
||||
if not data or not isinstance(data, dict):
|
||||
continue
|
||||
records = data.get('records', [])
|
||||
for item in records:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
report_id = item.get('id', '')
|
||||
create_time = item.get('createTimeString', '未知时间')
|
||||
# 提取体质判断 - 优先从 healthAiResponse 获取,如果没有再从 faceAnalysisResponse 获取
|
||||
health_ai = item.get('healthAiResponse', {}) or {}
|
||||
if health_ai:
|
||||
health_assessment = health_ai.get('healthAssessment', {}) or {}
|
||||
subject = health_assessment.get('subject', '未知')
|
||||
else:
|
||||
face_ai = item.get('faceAnalysisResponse', {}) or {}
|
||||
health_assessment = face_ai.get('healthAssessment', {}) or {}
|
||||
subject = health_assessment.get('subject', '未知')
|
||||
report_name = f"面诊分析报告-{report_id}"
|
||||
report_url = _get_analysis_export_url(report_id)
|
||||
output_all += f"| {report_name} | {create_time} | {subject} | [🔗 查看报告]({report_url}) |\n"
|
||||
|
||||
output_all += "\n> 注:面诊分析结果仅供健康参考,不能替代专业医疗诊断。"
|
||||
return output_all
|
||||
|
||||
|
||||
skill = Skill()
|
||||
@ -1,127 +0,0 @@
|
||||
altgraph==0.17.5
|
||||
annotated-doc==0.0.4
|
||||
annotated-types==0.7.0
|
||||
anyio==4.12.1
|
||||
APScheduler==3.11.2
|
||||
astroid==3.1.0
|
||||
Authlib==1.6.6
|
||||
blinker==1.4
|
||||
cachetools==6.2.6
|
||||
certifi==2026.1.4
|
||||
cffi==2.0.0
|
||||
chardet==5.2.0
|
||||
charset-normalizer==3.4.4
|
||||
click==8.3.1
|
||||
coverage==7.13.2
|
||||
coze-workload-identity==0.1.7
|
||||
cozeloop==0.1.19
|
||||
cryptography==3.4.8
|
||||
Cython==3.2.4
|
||||
dbus-python==1.2.18
|
||||
dill==0.4.1
|
||||
distro==1.7.0
|
||||
distro-info==1.1+ubuntu0.2
|
||||
et_xmlfile==2.0.0
|
||||
fastapi==0.121.2
|
||||
gitdb==4.0.12
|
||||
gitignore_parser==0.1.13
|
||||
GitPython==3.1.45
|
||||
greenlet==3.3.1
|
||||
h11==0.16.0
|
||||
httpcore==1.0.9
|
||||
httplib2==0.20.2
|
||||
httpx==0.28.1
|
||||
httpx-ws==0.8.2
|
||||
idna==3.11
|
||||
importlib-metadata==4.6.4
|
||||
inflect==7.5.0
|
||||
iniconfig==2.3.0
|
||||
isort==5.13.2
|
||||
jeepney==0.7.1
|
||||
Jinja2==3.1.6
|
||||
jiter==0.12.0
|
||||
jsonpatch==1.33
|
||||
jsonpointer==3.0.0
|
||||
keyring==23.5.0
|
||||
langchain==1.0.3
|
||||
langchain-core==1.0.2
|
||||
langchain-openai==1.0.1
|
||||
langgraph==1.0.2
|
||||
langgraph-checkpoint==3.0.0
|
||||
langgraph-prebuilt==1.0.2
|
||||
langgraph-sdk==0.2.9
|
||||
langsmith==0.4.39
|
||||
launchpadlib==1.10.16
|
||||
lazr.restfulclient==0.14.4
|
||||
lazr.uri==1.0.6
|
||||
markdown-it-py==4.0.0
|
||||
MarkupSafe==3.0.3
|
||||
mccabe==0.7.0
|
||||
mdurl==0.1.2
|
||||
more-itertools==8.10.0
|
||||
numpy==2.4.1
|
||||
oauthlib==3.2.0
|
||||
openai==2.16.0
|
||||
openpyxl==3.1.5
|
||||
orjson==3.11.5
|
||||
ormsgpack==1.12.2
|
||||
packaging==25.0
|
||||
pandas==2.3.3
|
||||
platformdirs==4.5.1
|
||||
pluggy==1.6.0
|
||||
psutil==7.1.3
|
||||
psycopg2-binary==2.9.11
|
||||
pycparser==3.0
|
||||
pydantic==2.12.4
|
||||
pydantic_core==2.41.5
|
||||
pydash==8.0.6
|
||||
Pygments==2.19.2
|
||||
PyGObject==3.42.1
|
||||
pyinstaller==6.18.0
|
||||
pyinstaller-hooks-contrib==2026.0
|
||||
PyJWT==2.10.1
|
||||
pylint==3.1.0
|
||||
PyMySQL==1.1.2
|
||||
pyparsing==2.4.7
|
||||
pytest==9.0.1
|
||||
pytest-asyncio==1.3.0
|
||||
pytest-cov==7.0.0
|
||||
pytest-mock==3.15.1
|
||||
python-apt==2.4.0+ubuntu4.1
|
||||
python-dateutil==2.9.0.post0
|
||||
pytz==2025.2
|
||||
PyYAML==6.0.3
|
||||
regex==2026.1.15
|
||||
requests==2.32.5
|
||||
requests-toolbelt==1.0.0
|
||||
rich==14.2.0
|
||||
SecretStorage==3.3.1
|
||||
setuptools==80.9.0
|
||||
six==1.16.0
|
||||
smmap==5.0.2
|
||||
sniffio==1.3.1
|
||||
sqlacodegen==3.2.0
|
||||
SQLAlchemy==2.0.46
|
||||
starlette==0.49.3
|
||||
supervisor==4.2.1
|
||||
tenacity==9.1.2
|
||||
tiktoken==0.12.0
|
||||
tomlkit==0.14.0
|
||||
tqdm==4.67.1
|
||||
typeguard==4.4.4
|
||||
typing-inspection==0.4.2
|
||||
typing_extensions==4.15.0
|
||||
tzdata==2025.3
|
||||
tzlocal==5.3.1
|
||||
unattended-upgrades==0.1
|
||||
urllib3==2.6.3
|
||||
uvicorn==0.38.0
|
||||
wadllib==1.3.6
|
||||
watchdog==6.0.0
|
||||
websockets==15.0.1
|
||||
wheel==0.45.1
|
||||
wsproto==1.3.2
|
||||
xlrd==2.0.2
|
||||
xxhash==3.6.0
|
||||
zipp==1.0.0
|
||||
zstandard==0.25.0
|
||||
@ -1,8 +0,0 @@
|
||||
from .util import RequestUtil, CommonUtil, DatetimeUtil
|
||||
from .base import *
|
||||
|
||||
__all__ = [
|
||||
'RequestUtil',
|
||||
'CommonUtil',
|
||||
'BaseUtil'
|
||||
]
|
||||
@ -1,96 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from .config import ApiEnum
|
||||
|
||||
from .base import BaseApiService
|
||||
from .util import RequestUtil, CommonUtil
|
||||
|
||||
|
||||
class ApiService(BaseApiService):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def get_download_url(self, tosKey, expireSeconds=3600):
|
||||
return RequestUtil.http_post(
|
||||
ApiEnum.GET_DOWNLOAD_URL__URL,
|
||||
params={
|
||||
"tosKey": tosKey,
|
||||
"expireSeconds": expireSeconds * 24
|
||||
}
|
||||
)
|
||||
|
||||
def page(self, url, pageNum=None, pageSize=None, *args, **argss):
|
||||
data = args[0] if len(args) > 0 else argss.get('data') if argss.get('data') is not None else {}
|
||||
if pageNum is None:
|
||||
pageNum = 1
|
||||
if pageSize is None:
|
||||
pageSize = ApiEnum.DEFAULT__PAGE_SIZE
|
||||
paramsPage = {
|
||||
'pageNum': int(pageNum),
|
||||
'pageSize': int(pageSize)
|
||||
}
|
||||
data.update({
|
||||
"page": paramsPage
|
||||
})
|
||||
if not CommonUtil.is_empty(data):
|
||||
if (len(args) == 0):
|
||||
argss.setdefault("data", data)
|
||||
response = RequestUtil.http_post(
|
||||
url,
|
||||
*args, **argss
|
||||
)
|
||||
return response
|
||||
|
||||
def list(self, url=None, *args, **argss):
|
||||
if url is not None:
|
||||
argss["url"] = url
|
||||
return self.page(1, ApiEnum.DEFAULT__PAGE_SIZE_MAX, *args, **argss)
|
||||
|
||||
def add(self, url=None, *args, **argss):
|
||||
response = RequestUtil.http_post(
|
||||
url,
|
||||
*args, **argss
|
||||
)
|
||||
return response
|
||||
|
||||
def edit(self, url=None, *args, **argss):
|
||||
response = RequestUtil.http_post(
|
||||
url,
|
||||
*args, **argss
|
||||
)
|
||||
return response
|
||||
|
||||
def delete(self, url=None, *args, **argss):
|
||||
response = RequestUtil.http_post(
|
||||
url,
|
||||
*args, **argss
|
||||
)
|
||||
return response
|
||||
|
||||
def http_post(self, url=None, *args, **argss):
|
||||
return RequestUtil.http_post(
|
||||
url,
|
||||
*args, **argss
|
||||
)
|
||||
|
||||
def http_put(self, url=None, *args, **argss):
|
||||
return RequestUtil.http_put(
|
||||
url,
|
||||
*args, **argss
|
||||
)
|
||||
|
||||
def http_get(self, url=None, *args, **argss):
|
||||
return RequestUtil.http_get(
|
||||
url,
|
||||
*args, **argss
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
def http_delete(self, url=None, *args, **argss):
|
||||
return RequestUtil.http_delete(
|
||||
url,
|
||||
*args, **argss
|
||||
)
|
||||
return response
|
||||
@ -1,33 +0,0 @@
|
||||
class BaseUtil:
|
||||
pass
|
||||
|
||||
|
||||
class BaseMixin:
|
||||
pass
|
||||
|
||||
|
||||
class BaseDao:
|
||||
pass
|
||||
|
||||
|
||||
class BaseService:
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
|
||||
class BaseApiService(BaseService):
|
||||
INSTANCE = None
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
if cls.INSTANCE is None:
|
||||
cls.INSTANCE = cls()
|
||||
return cls.INSTANCE
|
||||
|
||||
|
||||
class BaseSkill:
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@ -1,7 +0,0 @@
|
||||
ApiEnum:
|
||||
base-url-open-api: "http://192.168.1.234:9601/smyx-open-api"
|
||||
base-url-open-h5: "http://192.168.1.234:4100"
|
||||
base-url-health: "http://192.168.1.234:8080/jeecg-boot"
|
||||
|
||||
ConstantEnum:
|
||||
is-debug: true
|
||||
@ -1,7 +0,0 @@
|
||||
ApiEnum:
|
||||
base-url-open-api: "https://livemonitortest.lifeemergence.com/smyx-open-api"
|
||||
base-url-open-h5: "http://livemonitortest.lifeemergence.com"
|
||||
base-url-health: "https://healthtest.lifeemergence.com/jeecg-boot"
|
||||
|
||||
ConstantEnum:
|
||||
is-debug: true
|
||||
@ -1,235 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
from enum import Enum
|
||||
from typing import Dict
|
||||
import inspect
|
||||
|
||||
import yaml
|
||||
import platform
|
||||
|
||||
|
||||
class YamlUtil:
|
||||
|
||||
@staticmethod
|
||||
def load(path, config: Dict = {}) -> Dict:
|
||||
try:
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(config, f, default_flow_style=False, allow_unicode=True)
|
||||
return config
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
config = yaml.safe_load(f) or {}
|
||||
for key, value in config.items():
|
||||
if key not in config:
|
||||
config[key] = value
|
||||
return config
|
||||
except:
|
||||
pass
|
||||
return config
|
||||
|
||||
@staticmethod
|
||||
def save(path, config: Dict) -> Dict:
|
||||
try:
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(config, f, default_flow_style=False, allow_unicode=True)
|
||||
except:
|
||||
pass
|
||||
return config
|
||||
|
||||
|
||||
class BaseEnum:
|
||||
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
super().__init_subclass__(**kwargs)
|
||||
clsModule = cls.__module__
|
||||
cls_path = inspect.getfile(cls)
|
||||
clsFullName = f"{cls.__module__}.{cls.__name__}"
|
||||
cls_dirpath = os.path.dirname(cls_path) # .../src
|
||||
clsModulePath = clsModule.replace(".", "\\")
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__)) # .../src
|
||||
config_path = os.path.join(cls_dirpath, "config.yaml")
|
||||
config = YamlUtil.load(config_path)
|
||||
cls.init(config)
|
||||
env = config.get("env")
|
||||
if env:
|
||||
env_config_path = os.path.join(cls_dirpath, f"config-{env}.yaml")
|
||||
env_config = YamlUtil.load(env_config_path)
|
||||
cls.init(env_config)
|
||||
|
||||
@classmethod
|
||||
def init(cls, config=None):
|
||||
clsName = cls.__name__
|
||||
clsConfig = config and config.get(clsName)
|
||||
if clsConfig:
|
||||
for config_key, config_value in clsConfig.items():
|
||||
new_config_key = config_key = config_key.upper().replace("-", "_")
|
||||
if hasattr(cls, new_config_key):
|
||||
setattr(cls, new_config_key, config_value)
|
||||
|
||||
|
||||
class ApiEnum(BaseEnum):
|
||||
API_KEY = None
|
||||
|
||||
API_SECRET_KEY = None
|
||||
|
||||
DATABASE_URL = ""
|
||||
|
||||
BASE_URL_OPEN_API = ""
|
||||
|
||||
BASE_URL_OPEN_H5 = ""
|
||||
|
||||
BASE_URL_HEALTH = ""
|
||||
|
||||
OPEN_TOKEN = ""
|
||||
|
||||
TOKEN = ""
|
||||
|
||||
DEFAULT__REQUEST_TIMEOUT = 120
|
||||
|
||||
DEFAULT__PAGE_SIZE = 5
|
||||
|
||||
DEFAULT__PAGE_SIZE_MAX = 65536
|
||||
|
||||
GET_DOWNLOAD_URL__URL = BASE_URL_OPEN_API + "/api/tos/get-download-url"
|
||||
|
||||
|
||||
class ConstantEnum(BaseEnum):
|
||||
class SourceEnum(Enum):
|
||||
ARK_CLAW = "ARK_CLAW"
|
||||
JVS_CLAW = "JVS_CLAW"
|
||||
LIGHT_CLAW = "LIGHT_CLAW"
|
||||
WUHONG = "WUHONG"
|
||||
COZE = "COZE"
|
||||
SKILL_HUB = "SKILL_HUB"
|
||||
CLAW_HUB = "CLAW_HUB"
|
||||
FEISHU = "FEISHU"
|
||||
DINGTALK = "DINGTALK"
|
||||
WEIXIN = "WEIXIN"
|
||||
YUANBAO = "YUANBAO"
|
||||
WECOM = "WECOM"
|
||||
QQBOT = "QQBOT"
|
||||
|
||||
APP__ID = ""
|
||||
|
||||
APP__SOURCE = SourceEnum.CLAW_HUB.value
|
||||
|
||||
IS_DEBUG = False
|
||||
|
||||
CURRENT__OPEN_ID = ""
|
||||
|
||||
CURRENT__USER_NAME = ""
|
||||
|
||||
CURRENT__TENTANT_CODE = ""
|
||||
|
||||
FEISHU_APP__ID = ""
|
||||
|
||||
FEISHU_APP__SECRET = ""
|
||||
|
||||
FEISHU_APP__RECEIVE_ID = ""
|
||||
|
||||
DEFAULT__SCENE_CODE = ""
|
||||
|
||||
DEFAULT__SKILL_HUB_NAME = APP__SOURCE
|
||||
|
||||
DEFAULT__SKILL_PLATFORM_NAME = ""
|
||||
|
||||
DEFAULT__OUTPUT_LEVEL = "json"
|
||||
|
||||
SUPPORTED_FORMATS = ["mp4", "avi", "mov"]
|
||||
|
||||
MAX_FILE_SIZE_MB = 10
|
||||
|
||||
@staticmethod
|
||||
def is_debug():
|
||||
return platform.system() != 'Linux' and ConstantEnum.IS_DEBUG
|
||||
|
||||
@classmethod
|
||||
def init(cls, config=None):
|
||||
super().init(config)
|
||||
openclaw_sender_open_id = os.environ.get("OPENCLAW_SENDER_OPEN_ID")
|
||||
openclaw_sender_username = os.environ.get("OPENCLAW_SENDER_USERNAME")
|
||||
feishu_open_id = os.environ.get("FEISHU_OPEN_ID")
|
||||
if openclaw_sender_open_id:
|
||||
cls.CURRENT__OPEN_ID = openclaw_sender_open_id
|
||||
if openclaw_sender_username:
|
||||
cls.CURRENT__USER_NAME = openclaw_sender_username
|
||||
if feishu_open_id:
|
||||
cls.FEISHU_APP__RECEIVE_ID = feishu_open_id
|
||||
|
||||
class SceneCodeEnum(Enum):
|
||||
# 开放 #
|
||||
OPEN_HEALTH_AI_ANALYSIS = "OPEN_HEALTH_AI_ANALYSIS"
|
||||
OPEN_PERSON_RISK_ANALYSIS = "OPEN_PERSON_RISK_ANALYSIS"
|
||||
# 智眸 #
|
||||
PUBLIC_AREA_AI_ANALYSIS = "PUBLIC_AREA_AI_ANALYSIS"
|
||||
PERSONNEL_LEAVE_POST_MONITORING = "PERSONNEL_LEAVE_POST_MONITORING"
|
||||
CRAWL_MONITOR = "CRAWL_MONITOR"
|
||||
# 陪你安 #
|
||||
PEI_NI_AN_DEFAULT = "PEI_NI_AN_DEFAULT"
|
||||
PET_ANALYSIS = "PET_ANALYSIS"
|
||||
CRAWL_ANALYSIS = "CRAWL_ANALYSIS"
|
||||
AQUARIUM_ANALYSIS = "AQUARIUM_ANALYSIS"
|
||||
PSYCHOLOGY_ANALYSIS = "PSYCHOLOGY_ANALYSIS"
|
||||
AUTISM_ANALYSIS = "AUTISM_ANALYSIS"
|
||||
DIET_ANALYSIS = "DIET_ANALYSIS"
|
||||
DRIVE_ANALYSIS = "DRIVE_ANALYSIS"
|
||||
SPORT_ANALYSIS = "SPORT_ANALYSIS"
|
||||
EMOTION_ANALYSIS = "EMOTION_ANALYSIS"
|
||||
STUDY_ANALYSIS = "STUDY_ANALYSIS"
|
||||
INFANT_SAFETY_MONITORING_ANALYSIS = "INFANT_SAFETY_MONITORING"
|
||||
PHONE_USAGE_MONITORING_ANALYSIS = "PHONE_USAGE_MONITORING"
|
||||
INCONTINENCE_ALERT_ANALYSIS = "INCONTINENCE_ALERT"
|
||||
RESPIRATORY_SYMPTOM_RECOGNITION_ANALYSIS = "RESPIRATORY_SYMPTOM_RECOGNITION"
|
||||
ELECTRIC_VEHICLE_DETECTION_ANALYSIS = "ELECTRIC_VEHICLE_DETECTION"
|
||||
SMOKING_DETECTION_ANALYSIS = "SMOKING_DETECTION"
|
||||
PET_DETECTION_FEEDER_ANALYSIS = "PET_DETECTION_FEEDER"
|
||||
PET_HEALTH_MONITORING_ANALYSIS = "PET_HEALTH_MONITORING"
|
||||
STROKE_RISK_SCREENING_ANALYSIS = "STROKE_RISK_SCREENING"
|
||||
HUMAN_DETECTION_ANALYSIS = "HUMAN_DETECTION"
|
||||
STRANGER_RECOGNITION_ANALYSIS = "STRANGER_RECOGNITION"
|
||||
FOCUS_ANALYSIS_ANALYSIS = "FOCUS_ANALYSIS"
|
||||
HUMAN_POSTURE_RECOGNITION_ANALYSIS = "HUMAN_POSTURE_RECOGNITION"
|
||||
HUMAN_EMOTION_RECOGNITION_ANALYSIS = "HUMAN_EMOTION_RECOGNITION"
|
||||
FIRE_SMOKE_DETECTION_ANALYSIS = "FIRE_SMOKE_DETECTION"
|
||||
BASIC_OBJECT_DETECTION_ANALYSIS = "BASIC_OBJECT_DETECTION"
|
||||
CHILD_DANGEROUS_BEHAVIOR_RECOGNITION_ANALYSIS = "CHILD_DANGEROUS_BEHAVIOR_RECOGNITION"
|
||||
PET_RESTRICTED_AREA_WARNING_ANALYSIS = "PET_RESTRICTED_AREA_WARNING"
|
||||
SLEEP_QUALITY_ANALYSIS_ANALYSIS = "SLEEP_QUALITY_ANALYSIS"
|
||||
PET_DETECTION_ANALYSIS = "PET_DETECTION"
|
||||
PSYCHOLOGICAL_STRESS_ASSESSMENT_ANALYSIS = "PSYCHOLOGICAL_STRESS_ASSESSMENT"
|
||||
VISUAL_QA_ANALYSIS = "VISUAL_QA"
|
||||
PET_BODY_HEALTH_ANALYSIS = "PET_BODY_HEALTH_ANALYSIS"
|
||||
PET_BEHAVIOR_DETECTION_ANALYSIS = "PET_BEHAVIOR_DETECTION"
|
||||
INFANT_SUFFOCATION_WARNING_ANALYSIS = "INFANT_SUFFOCATION_WARNING"
|
||||
STRANGER_APPROACH_WARNING_ANALYSIS = "STRANGER_APPROACH_WARNING"
|
||||
IMAGE_QUALITY_DETECTION_ANALYSIS = "IMAGE_QUALITY_DETECTION"
|
||||
CHILD_EMOTION_RECOGNITION_ANALYSIS = "CHILD_EMOTION_RECOGNITION"
|
||||
OUTDOOR_MONITORING_ANALYSIS = "OUTDOOR_MONITORING"
|
||||
FALL_DETECTION_IMAGE_ANALYSIS = "FALL_DETECTION_IMAGE"
|
||||
CUSTOM_TIMELAPSE_ANALYSIS = "CUSTOM_TIMELAPSE"
|
||||
CONTACTLESS_VITAL_SIGNS_MONITORING_ANALYSIS = "CONTACTLESS_VITAL_SIGNS_MONITORING"
|
||||
VIDEO_SEARCH_ANALYSIS = "VIDEO_SEARCH"
|
||||
FAMILIAR_PERSON_RECOGNITION_ANALYSIS = "FAMILIAR_PERSON_RECOGNITION"
|
||||
TCM_CONSTITUTION_RECOGNITION_ANALYSIS = "TCM_CONSTITUTION_RECOGNITION"
|
||||
CONTACTLESS_HEALTH_RISK_DETECTION_ANALYSIS = "CONTACTLESS_HEALTH_RISK_DETECTION"
|
||||
UNACCOMPANIED_MONITORING_ANALYSIS = "UNACCOMPANIED_MONITORING"
|
||||
ELDERLY_FALL_DETECTION_ANALYSIS = "ELDERLY_FALL_DETECTION"
|
||||
PARKINSON_EPILEPSY_BEHAVIOR_RECOGNITION_ANALYSIS = "PARKINSON_EPILEPSY_BEHAVIOR_RECOGNITION"
|
||||
PET_BREED_INDIVIDUAL_RECOGNITION_ANALYSIS = "PET_BREED_INDIVIDUAL_RECOGNITION"
|
||||
ELDERLY_BED_EXIT_WANDERING_MONITORING_ANALYSIS = "ELDERLY_BED_EXIT_WANDERING_MONITORING"
|
||||
ARRHYTHMIA_EARLY_WARNING_ANALYSIS = "ARRHYTHMIA_EARLY_WARNING"
|
||||
FIRE_DETECTION_ANALYSIS = "FIRE_DETECTION"
|
||||
VISUAL_SUMMARY_ANALYSIS = "VISUAL_SUMMARY"
|
||||
PACKAGE_DETECTION_ANALYSIS = "PACKAGE_DETECTION"
|
||||
INFANT_BLANKET_KICK_MONITORING_ANALYSIS = "INFANT_BLANKET_KICK_MONITORING"
|
||||
PET_CALMING_TRIGGER_ANALYSIS = "PET_CALMING_TRIGGER"
|
||||
CAT_FACE_RECOGNITION_ANALYSIS = "CAT_FACE_RECOGNITION"
|
||||
INFANT_SLEEP_MONITORING_ANALYSIS = "INFANT_SLEEP_MONITORING"
|
||||
VIRTUAL_FENCE_INTRUSION_WARNING_ANALYSIS = "VIRTUAL_FENCE_INTRUSION_WARNING"
|
||||
FALL_DETECTION_VIDEO_ANALYSIS = "FALL_DETECTION_VIDEO"
|
||||
INFANT_CRY_ANALYSIS = "INFANT_CRY_ANALYSIS"
|
||||
PET_VOCAL_EMOTION_ANALYSIS = "PET_VOCAL_EMOTION_ANALYSIS"
|
||||
BIRD_RECOGNITION_ANALYSIS = "BIRD_RECOGNITION"
|
||||
FRAUD_CALL_IDENTIFICATION = "FRAUD_CALL_IDENTIFICATION"
|
||||
@ -1,18 +0,0 @@
|
||||
env: prod
|
||||
|
||||
ApiEnum:
|
||||
api-key:
|
||||
api-secret-key:
|
||||
database-url:
|
||||
base-url-open-api: "https://open.lifeemergence.com/smyx-open-api"
|
||||
base-url-open-h5: "http://livemonitor.lifeemergence.com"
|
||||
base-url-health: "https://lifeemergence.com/jeecg-boot"
|
||||
|
||||
ConstantEnum:
|
||||
is-debug: false
|
||||
app--id: x1a3s4nwy1s2r4se
|
||||
current--tentant-code: "PEI_NI_AN"
|
||||
feishu-app--id: cli_a93d769369badcb1
|
||||
feishu-app--secret:
|
||||
default--skill-platform-name: ARK_CLAW
|
||||
# default--scene-code: PEI_NI_AN_DEFAULT
|
||||
@ -1,348 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
本地化轻量级数据库封装
|
||||
使用SQLite + SQLAlchemy ORM
|
||||
支持基础CRUD操作,通过继承BaseDao快速实现各表的Dao层
|
||||
"""
|
||||
import datetime
|
||||
import sys
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional, Type, TypeVar
|
||||
from sqlalchemy import create_engine, Column, Integer, String, DateTime, func, Select, Table, MetaData, select, or_
|
||||
from sqlalchemy.orm import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker, Session
|
||||
from sqlalchemy.sql.expression import text
|
||||
|
||||
from skills.smyx_common.scripts.config import ConstantEnum, ApiEnum
|
||||
|
||||
from skills.smyx_common.scripts.util import StringUtil, DatetimeUtil, FileUtil
|
||||
|
||||
from skills.smyx_common.scripts.base import BaseMixin, BaseDao
|
||||
|
||||
# 基础模型类
|
||||
Base = declarative_base()
|
||||
|
||||
# 泛型类型,用于返回对应模型实例
|
||||
T = TypeVar('T', bound=Base)
|
||||
|
||||
meta = MetaData()
|
||||
|
||||
DATABASE_URL = ApiEnum.DATABASE_URL
|
||||
|
||||
|
||||
class BaseModelMixin(BaseMixin):
|
||||
|
||||
@classmethod
|
||||
def load(cls, source: dict):
|
||||
"""
|
||||
获取源枚举
|
||||
:param source: 源
|
||||
:return: User
|
||||
"""
|
||||
column_names = cls.__table__.columns.keys()
|
||||
user_dict = {k: source.get(StringUtil.snake_to_camel(k)) for k in column_names}
|
||||
user_dict["create_time"] = DatetimeUtil.parse(user_dict["create_time"])
|
||||
user_dict["update_time"] = DatetimeUtil.parse(user_dict["update_time"])
|
||||
model = cls(**user_dict)
|
||||
return model
|
||||
|
||||
|
||||
class Dao(BaseDao):
|
||||
"""
|
||||
基础Dao类,提供通用的CRUD操作
|
||||
子类只需配置__model__和__tablename__即可使用
|
||||
"""
|
||||
__model__: Type[T] = None # 对应的模型类,子类必须配置
|
||||
__tablename__: str = None # 表名,子类必须配置
|
||||
|
||||
def get_db_path(self, db_path):
|
||||
import os
|
||||
|
||||
cwd = os.getcwd()
|
||||
workspace = os.path.dirname(cwd)
|
||||
workspace = os.path.dirname(workspace)
|
||||
workspace = os.environ.get('OPENCLAW_WORKSPACE', workspace)
|
||||
parent_dir = os.path.join(workspace, "data")
|
||||
FileUtil.mkdir(parent_dir)
|
||||
db_path = os.path.join(parent_dir, db_path)
|
||||
|
||||
return db_path
|
||||
|
||||
def __init__(self, db_path: str = None):
|
||||
"""
|
||||
初始化Dao
|
||||
:param db_path: SQLite数据库文件路径
|
||||
"""
|
||||
|
||||
if not db_path:
|
||||
db_path = "smyx-common-claw.db"
|
||||
db_path = self.get_db_path(db_path)
|
||||
|
||||
self.engine = create_engine(f"sqlite:///{db_path}", echo=False)
|
||||
|
||||
# 创建会话工厂
|
||||
self.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=self.engine)
|
||||
# 初始化表结构
|
||||
self._create_tables()
|
||||
self._alter_tables()
|
||||
|
||||
def _create_tables(self) -> None:
|
||||
"""创建所有表结构"""
|
||||
Base.metadata.create_all(bind=self.engine)
|
||||
|
||||
def _alter_tables(self) -> None:
|
||||
"""创建所有表结构"""
|
||||
sql_statement = "ALTER TABLE sys_user ADD COLUMN source_id INT;"
|
||||
|
||||
# 3. 执行语句
|
||||
try:
|
||||
with self.engine.connect() as connection:
|
||||
connection.execute(text(sql_statement))
|
||||
connection.commit() # 对于数据定义语言(DDL),需要显式提交
|
||||
except Exception as e:
|
||||
connection.rollback()
|
||||
if len(e.args) and "duplicate column name" in e.args[0]:
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
|
||||
def get_session(self) -> Session:
|
||||
"""获取数据库会话"""
|
||||
return self.SessionLocal()
|
||||
|
||||
def save(self, model) -> T:
|
||||
"""
|
||||
创建新记录
|
||||
:param kwargs: 字段键值对
|
||||
:return: 创建的模型实例
|
||||
"""
|
||||
|
||||
try:
|
||||
return self.add(
|
||||
model
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return self.update(
|
||||
model
|
||||
)
|
||||
|
||||
def add(self, model) -> T:
|
||||
"""
|
||||
创建新记录
|
||||
:param kwargs: 字段键值对
|
||||
:return: 创建的模型实例
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
session.add(model)
|
||||
session.commit()
|
||||
session.refresh(model)
|
||||
return model
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def create(self, **kwargs) -> T:
|
||||
"""
|
||||
创建新记录
|
||||
:param kwargs: 字段键值对
|
||||
:return: 创建的模型实例
|
||||
"""
|
||||
instance = self.__model__(**kwargs)
|
||||
return self.add(instance)
|
||||
|
||||
def get_by_id(self, record_id: int) -> Optional[T]:
|
||||
"""
|
||||
根据ID查询记录
|
||||
:param record_id: 记录ID
|
||||
:return: 模型实例或None
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
return session.query(self.__model__).filter(self.__model__.id == record_id).first()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def get_by_username(self, username: str) -> Optional[T]:
|
||||
"""
|
||||
根据ID查询记录
|
||||
:param record_id: 记录ID
|
||||
:return: 模型实例或None
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
or_(
|
||||
self.__model__.del_flag == 0,
|
||||
self.__model__.del_flag.is_(None) # 关键:使用 .is_(None) 来判断 SQL 的 NULL
|
||||
)
|
||||
return session.query(self.__model__).filter(self.__model__.username == username,
|
||||
or_(
|
||||
self.__model__.del_flag == 0,
|
||||
self.__model__.del_flag.is_(None)
|
||||
# 关键:使用 .is_(None) 来判断 SQL 的 NULL
|
||||
)).first()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def list(self, filters: Optional[Dict[str, Any]] = None, limit: Optional[int] = None,
|
||||
offset: Optional[int] = None) -> List[T]:
|
||||
"""
|
||||
查询记录列表
|
||||
:param filters: 过滤条件字典,如{"name": "张三", "age": 18}
|
||||
:param limit: 最大返回数量
|
||||
:param offset: 偏移量
|
||||
:return: 模型实例列表
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
query = session.query(self.__model__)
|
||||
# .where(self.__model__.id != 2, self.__model__.id == 1))
|
||||
|
||||
if filters:
|
||||
for key, value in filters.items():
|
||||
query = query.filter(getattr(self.__model__, key) == value)
|
||||
|
||||
if offset:
|
||||
query = query.offset(offset)
|
||||
if limit:
|
||||
query = query.limit(limit)
|
||||
|
||||
return query.all()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def update(self, model) -> Optional[T]:
|
||||
"""
|
||||
更新记录
|
||||
:param record_id: 记录ID
|
||||
:param kwargs: 要更新的字段键值对
|
||||
:return: 更新后的模型实例或None
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
instance = session.query(self.__model__).filter(self.__model__.id == model.id).first()
|
||||
if not instance:
|
||||
return None
|
||||
|
||||
column_names = self.__model__.__table__.columns.keys()
|
||||
|
||||
for key in column_names:
|
||||
value = getattr(model, key)
|
||||
setattr(instance, key, value)
|
||||
|
||||
session.commit()
|
||||
session.refresh(instance)
|
||||
return instance
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def modify(self, record_id: int, **kwargs) -> Optional[T]:
|
||||
"""
|
||||
更新记录
|
||||
:param record_id: 记录ID
|
||||
:param kwargs: 要更新的字段键值对
|
||||
:return: 更新后的模型实例或None
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
instance = session.query(self.__model__).filter(self.__model__.id == record_id).first()
|
||||
if not instance:
|
||||
return None
|
||||
|
||||
for key, value in kwargs.items():
|
||||
setattr(instance, key, value)
|
||||
|
||||
session.commit()
|
||||
session.refresh(instance)
|
||||
return instance
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def update_by_username(self, username: str, **kwargs) -> Optional[T]:
|
||||
"""
|
||||
更新记录
|
||||
:param username: 记录ID
|
||||
:param kwargs: 要更新的字段键值对
|
||||
:return: 更新后的模型实例或None
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
instance = session.query(self.__model__).filter(self.__model__.username == username).first()
|
||||
if not instance:
|
||||
return None
|
||||
|
||||
for key, value in kwargs.items():
|
||||
setattr(instance, key, value)
|
||||
|
||||
session.commit()
|
||||
session.refresh(instance)
|
||||
return instance
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def delete(self, record_id: int) -> bool:
|
||||
"""
|
||||
删除记录
|
||||
:param record_id: 记录ID
|
||||
:return: 删除成功返回True,失败返回False
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
instance = session.query(self.__model__).filter(self.__model__.id == record_id).first()
|
||||
if not instance:
|
||||
return False
|
||||
|
||||
session.delete(instance)
|
||||
session.commit()
|
||||
return True
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def count(self, filters: Optional[Dict[str, Any]] = None) -> int:
|
||||
"""
|
||||
统计记录数量
|
||||
:param filters: 过滤条件字典
|
||||
:return: 记录数量
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
query = session.query(func.count(self.__model__.id))
|
||||
|
||||
if filters:
|
||||
for key, value in filters.items():
|
||||
query = query.filter(getattr(self.__model__, key) == value)
|
||||
|
||||
return query.scalar()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
class User(Base, BaseModelMixin):
|
||||
"""用户模型"""
|
||||
__tablename__ = "sys_user"
|
||||
|
||||
id = Column(String(32), primary_key=True, index=True)
|
||||
source_id = Column(String(32), comment="源头id")
|
||||
username = Column(String(100), unique=True, index=True, nullable=False, comment="用户名")
|
||||
email = Column(String(45), unique=True, index=True, comment="邮箱")
|
||||
birthday = Column(DateTime, unique=True, index=True, comment="邮箱")
|
||||
sex = Column(Integer, comment="性别")
|
||||
age = Column(Integer, comment="年龄")
|
||||
token = Column(String(500), comment="token")
|
||||
open_token = Column(String(1000), comment="开放token")
|
||||
source = Column(String(50), comment="token")
|
||||
del_flag = Column(Integer, comment="是否删除", default=0)
|
||||
create_time = Column(DateTime, default=func.now(), comment="创建时间")
|
||||
update_time = Column(DateTime, default=func.now(), onupdate=func.now(), comment="更新时间")
|
||||
|
||||
SourceEnum = ConstantEnum.SourceEnum
|
||||
|
||||
|
||||
class UserDao(Dao):
|
||||
"""用户Dao,继承BaseDao即可拥有所有基础CRUD功能"""
|
||||
__model__ = User
|
||||
__tablename__ = "users"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
@ -1,82 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
from .config import ApiEnum as ApiEnumBase, ConstantEnum
|
||||
from .base import BaseSkill
|
||||
from skills.smyx_common.scripts.api_service import ApiService as ApiServiceBase
|
||||
from .util import FileUtil
|
||||
|
||||
from .api_service import ApiService
|
||||
|
||||
class Skill(BaseSkill, ApiService):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
class AgentSkill(BaseSkill, ApiService):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def ai_chat(self, prompt: str, session_id: str = None, timeout: int = 120):
|
||||
"""
|
||||
通过 subprocess 调用 openclaw agent 命令
|
||||
|
||||
Args:
|
||||
prompt: 分析提示
|
||||
session_id: 会话 ID(可选)
|
||||
timeout: 超时时间(秒)
|
||||
|
||||
Returns:
|
||||
分析结果或会话 ID
|
||||
"""
|
||||
import uuid
|
||||
|
||||
# 生成唯一会话 ID
|
||||
if not session_id:
|
||||
entry_script = sys.argv[0]
|
||||
abs_entry_script = os.path.abspath(entry_script)
|
||||
main_name = FileUtil.get_name(abs_entry_script)
|
||||
session_id = f"{main_name}--{uuid.uuid4()}"
|
||||
|
||||
ConstantEnum.is_debug() and print(f"🤖 正在调用 openclaw agent (会话:{session_id})..., prompt:{prompt}")
|
||||
|
||||
# 构建命令
|
||||
cmd = [
|
||||
"openclaw",
|
||||
"agent",
|
||||
"-m", str(prompt),
|
||||
"--session-id", session_id,
|
||||
"--thinking", "minimal",
|
||||
"--timeout", str(timeout)
|
||||
]
|
||||
|
||||
ConstantEnum.is_debug() and print(f"🤖 正在调用 openclaw agent 执行命令{' '.join(cmd)}")
|
||||
|
||||
try:
|
||||
# 执行命令
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout + 10
|
||||
)
|
||||
|
||||
if result.stderr:
|
||||
ConstantEnum.is_debug() and print(f"🤖 正在调用 openclaw agent 执行错误:{result.stderr}")
|
||||
return
|
||||
|
||||
output = result.stdout
|
||||
ConstantEnum.is_debug() and print(f"🤖 正在调用 openclaw agent 执行成功:{output}")
|
||||
|
||||
return output
|
||||
|
||||
except subprocess.TimeoutExpired as e:
|
||||
ConstantEnum.is_debug() and print(f"🤖 正在调用 openclaw agent 超时({timeout}秒),任务可能仍在后台运行:{e}")
|
||||
except Exception as e:
|
||||
ConstantEnum.is_debug() and print(f"🤖 正在调用 openclaw agent 执行错误:{e}")
|
||||
|
||||
|
||||
skill = Skill()
|
||||
@ -1,413 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
import traceback
|
||||
|
||||
import requests
|
||||
from .config import ApiEnum, ConstantEnum, sys, YamlUtil
|
||||
|
||||
from .base import BaseUtil
|
||||
import time
|
||||
import logging
|
||||
from typing import Any, Callable, Optional, TypeVar, Dict
|
||||
import pydash as _
|
||||
|
||||
if ConstantEnum.is_debug():
|
||||
import http.client
|
||||
|
||||
# 【关键代码】开启调试模式
|
||||
http.client.HTTPConnection.debuglevel = 1
|
||||
# 可选:如果你希望日志更整洁,可以配合 logging 模块(否则打印会比较乱)
|
||||
import logging
|
||||
|
||||
logging.basicConfig()
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
requests_log = logging.getLogger("urllib3")
|
||||
requests_log.setLevel(logging.DEBUG)
|
||||
requests_log.propagate = True
|
||||
|
||||
|
||||
class StringUtil(BaseUtil):
|
||||
|
||||
@staticmethod
|
||||
def camel_to_snake(name):
|
||||
import re
|
||||
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
|
||||
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
||||
|
||||
@staticmethod
|
||||
def snake_to_pascal(name):
|
||||
import re
|
||||
name = re.sub(r'^([a-z])', lambda m: m.group(1).upper(), name)
|
||||
return re.sub(r'_([a-z])', lambda m: m.group(1).upper(), name)
|
||||
|
||||
@staticmethod
|
||||
def snake_to_camel(name):
|
||||
import re
|
||||
# 逻辑:匹配 '_[a-z]' (下划线+小写字母),将其替换为对应的大写字母(去掉下划线)
|
||||
return re.sub(r'_([a-z])', lambda m: m.group(1).upper(), name)
|
||||
|
||||
|
||||
class FileUtil(BaseUtil):
|
||||
|
||||
@staticmethod
|
||||
def get_fullname(path):
|
||||
try:
|
||||
return os.path.basename(path)
|
||||
except Exception as e:
|
||||
CommonUtil.trace_exception_stack(e)
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def get_name(path):
|
||||
try:
|
||||
return os.path.splitext(os.path.basename(path))[0]
|
||||
except Exception as e:
|
||||
CommonUtil.trace_exception_stack(e)
|
||||
|
||||
@staticmethod
|
||||
def get_ext(path):
|
||||
try:
|
||||
return os.path.splitext(os.path.basename(path))[1]
|
||||
except Exception as e:
|
||||
CommonUtil.trace_exception_stack(e)
|
||||
|
||||
@staticmethod
|
||||
def open(path):
|
||||
try:
|
||||
return open(path, 'w', encoding='utf-8')
|
||||
except Exception as e:
|
||||
CommonUtil.trace_exception_stack(e)
|
||||
|
||||
@staticmethod
|
||||
def mkdir(path):
|
||||
try:
|
||||
os.makedirs(path, exist_ok=True)
|
||||
except Exception as e:
|
||||
CommonUtil.trace_exception_stack(e)
|
||||
|
||||
|
||||
class JsonUtil(BaseUtil):
|
||||
|
||||
@staticmethod
|
||||
def stringify(json_obj, default_str=""):
|
||||
try:
|
||||
return json.dumps(json_obj, ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
CommonUtil.trace_exception_stack(e)
|
||||
pass
|
||||
return default_str
|
||||
|
||||
@staticmethod
|
||||
def parse(json_str, default_json={}):
|
||||
try:
|
||||
return json.loads(json_str)
|
||||
except Exception as e:
|
||||
CommonUtil.trace_exception_stack(e)
|
||||
pass
|
||||
return default_json
|
||||
|
||||
|
||||
class CommonUtil(BaseUtil):
|
||||
|
||||
@staticmethod
|
||||
def trace_exception_stack(e):
|
||||
if ConstantEnum.is_debug():
|
||||
print(f"❌ 错误描述: {str(e)}, 堆栈跟踪:")
|
||||
traceback.print_stack()
|
||||
|
||||
@staticmethod
|
||||
def polling(
|
||||
action: Callable[[], Any],
|
||||
check_condition: Callable[[Any], bool],
|
||||
on_success: Optional[Callable[[Any], None]] = None,
|
||||
on_retry: Optional[Callable[[Any, int], None]] = None,
|
||||
on_error: Optional[Callable[[Exception], None]] = None,
|
||||
interval: float = 1.0,
|
||||
max_attempts: int = 5,
|
||||
description: str = "轮询任务"
|
||||
) -> Optional[Any]:
|
||||
"""
|
||||
通用的轮询处理函数
|
||||
|
||||
:param action:
|
||||
[必填] 执行动作的回调函数。
|
||||
例如:发送 HTTP 请求、查询数据库状态等。
|
||||
必须返回一个结果对象供 check_condition 使用。
|
||||
|
||||
:param check_condition:
|
||||
[必填] 检查是否结束的回调函数。
|
||||
接收 action 的返回值,返回 True 表示“满足结束条件”,False 表示“继续轮询”。
|
||||
例如:lambda res: res.get('need_refresh') is False
|
||||
|
||||
:param on_success:
|
||||
[可选] 当 check_condition 返回 True 时执行的回调(通常用于记录日志或处理最终数据)。
|
||||
|
||||
:param on_retry:
|
||||
[可选] 当需要继续轮询时执行的回调。
|
||||
参数:(当前结果, 当前尝试次数)。可用于打印进度。
|
||||
|
||||
:param on_error:
|
||||
[可选] 当 action 抛出异常时执行的回调。
|
||||
参数:(异常对象)。
|
||||
|
||||
:param interval:
|
||||
每次轮询之间的等待时间(秒)。
|
||||
|
||||
:param max_attempts:
|
||||
最大尝试次数,防止死循环。
|
||||
|
||||
:param description:
|
||||
任务描述,用于日志输出。
|
||||
|
||||
:return:
|
||||
如果成功,返回 action 的最后一次返回值;如果超时或失败,返回 None。
|
||||
"""
|
||||
|
||||
attempts = 0
|
||||
|
||||
print(f"🚀 开始执行 [{description}]...")
|
||||
|
||||
while attempts < max_attempts:
|
||||
attempts += 1
|
||||
|
||||
try:
|
||||
# 1. 执行动作
|
||||
result = action()
|
||||
last_result = result
|
||||
|
||||
# 2. 检查条件
|
||||
if check_condition(result):
|
||||
print(f"✅ [{description}] 成功!条件已满足 (尝试次数: {attempts}, 耗时{interval * attempts}秒)")
|
||||
if on_success:
|
||||
on_success(result)
|
||||
return result
|
||||
|
||||
# 3. 条件未满足,准备重试
|
||||
if on_retry:
|
||||
on_retry(result, attempts)
|
||||
else:
|
||||
# 默认日志行为
|
||||
print(
|
||||
f"⏳ [{description}] 条件未满足,{interval}秒后重试... ({attempts}/{max_attempts}, 耗时{interval * attempts}秒)")
|
||||
|
||||
time.sleep(interval)
|
||||
|
||||
except Exception as e:
|
||||
# 4. 异常处理
|
||||
if on_error:
|
||||
on_error(e)
|
||||
else:
|
||||
# 默认错误行为:打印错误并继续
|
||||
logging.error(f"❌ [{description}] 发生异常: {e}")
|
||||
print(f"⚠️ [{description}] 遇到错误,{interval}秒后重试...")
|
||||
|
||||
time.sleep(interval)
|
||||
|
||||
# 5. 超时处理
|
||||
print(f"⚠️ [{description}] 失败:达到最大尝试次数 ({max_attempts}),强制停止。")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def is_empty(data):
|
||||
# 1. 如果是 None (对应 JSON 的 null)
|
||||
if data is None:
|
||||
return True
|
||||
|
||||
# 2. 如果是字典或列表,且长度为 0 (对应 {} 或 [])
|
||||
if isinstance(data, (dict, list)) and len(data) == 0:
|
||||
return True
|
||||
|
||||
|
||||
from datetime import date, datetime
|
||||
|
||||
|
||||
class DatetimeUtil(BaseUtil):
|
||||
FORMAT__DATETIME = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
@staticmethod
|
||||
def now_str():
|
||||
return DatetimeUtil.format(DatetimeUtil.now())
|
||||
|
||||
@staticmethod
|
||||
def today_str():
|
||||
return DatetimeUtil.format_date(DatetimeUtil.today())
|
||||
|
||||
@staticmethod
|
||||
def now():
|
||||
return datetime.now()
|
||||
|
||||
@staticmethod
|
||||
def today():
|
||||
return DatetimeUtil.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
@staticmethod
|
||||
def format(date):
|
||||
return date.strftime('%Y-%m-%d %H:%M:%S') if type(date) == datetime else date
|
||||
|
||||
@staticmethod
|
||||
def format_date(date):
|
||||
return date.strftime('%Y-%m-%d') if type(date) == datetime else date
|
||||
|
||||
@staticmethod
|
||||
def parse(date_str):
|
||||
if type(date_str) == int:
|
||||
return datetime.fromtimestamp(date_str)
|
||||
return datetime.strptime(date_str, DatetimeUtil.FORMAT__DATETIME) if type(date_str) == str else date_str
|
||||
|
||||
@staticmethod
|
||||
def timestamp(date=now()):
|
||||
return int(date.timestamp() * 1000)
|
||||
|
||||
|
||||
class RequestUtil(BaseUtil):
|
||||
BASE_URL = ApiEnum.BASE_URL_OPEN_API
|
||||
AUTHORIZATION_RETRY_COUNT_MAX = 3
|
||||
authorization_retry_count = 0
|
||||
|
||||
@classmethod
|
||||
def http_post(cls, url, data=None, params=None, headers=None, *args, **argss):
|
||||
return cls.http_request("post", url, data=data, params=params, headers=headers, *args, **argss)
|
||||
|
||||
@classmethod
|
||||
def http_put(cls, url, data=None, params=None, headers=None, *args, **argss):
|
||||
return cls.http_request("put", url, data=data, params=params, headers=headers, *args, **argss)
|
||||
|
||||
@classmethod
|
||||
def http_delete(cls, url, data=None, params=None, headers=None, *args, **argss):
|
||||
return cls.http_request("delete", url, data=data, params=params, headers=headers, *args, **argss)
|
||||
|
||||
@classmethod
|
||||
def http_get(cls, url, params=None, headers=None, *args, **argss):
|
||||
return cls.http_request("get", url, params=params, headers=headers, *args, **argss)
|
||||
|
||||
@classmethod
|
||||
def http_request(cls, method, url, data=None, params=None, headers=None, options=None, *args,
|
||||
timeout=ApiEnum.DEFAULT__REQUEST_TIMEOUT, **argss):
|
||||
def _get_or_create_user(username):
|
||||
_url = ApiEnum.BASE_URL_HEALTH + "/sys/phoneLogin"
|
||||
open_id = username
|
||||
_data = {
|
||||
"silent": 1,
|
||||
"register": 1,
|
||||
"openId": open_id,
|
||||
"mobile": username
|
||||
}
|
||||
try:
|
||||
_response = requests.post(_url, json=_data)
|
||||
if _response.status_code == 200:
|
||||
_response_json = _response.json()
|
||||
if _response_json and _response_json.get("success"):
|
||||
return _response_json and _response_json.get("result")
|
||||
except Exception as _e:
|
||||
CommonUtil.trace_exception_stack(_e)
|
||||
return {}
|
||||
|
||||
try:
|
||||
headers = headers or {}
|
||||
if not url.startswith("https://") and not url.startswith("http://"):
|
||||
url = cls.BASE_URL + url
|
||||
headers['App-Id'] = ConstantEnum.APP__ID
|
||||
# ConstantEnum.CURRENT__USER_NAME = ConstantEnum.CURRENT__OPEN_ID = "ou_86fdd8e0d5f116c18a9dd550abefe6d2"
|
||||
current__user_name = ApiEnum.API_SECRET_KEY or ConstantEnum.CURRENT__USER_NAME or ConstantEnum.CURRENT__OPEN_ID
|
||||
if (not ApiEnum.TOKEN or not ApiEnum.OPEN_TOKEN) and current__user_name:
|
||||
try:
|
||||
from .dao import UserDao, User
|
||||
user_dao = UserDao()
|
||||
found_user = user_dao.get_by_username(current__user_name)
|
||||
if found_user:
|
||||
ApiEnum.TOKEN = found_user.token
|
||||
ApiEnum.OPEN_TOKEN = found_user.open_token
|
||||
if not ApiEnum.TOKEN or not ApiEnum.OPEN_TOKEN:
|
||||
new_current_user = _get_or_create_user(current__user_name)
|
||||
if new_current_user:
|
||||
ApiEnum.TOKEN = new_current_user.get("token")
|
||||
ApiEnum.OPEN_TOKEN = new_current_user.get("openToken")
|
||||
|
||||
current_user_info = new_current_user.get("userInfo")
|
||||
if current_user_info:
|
||||
current_user_info["token"] = new_current_user.get("token")
|
||||
current_user_info["openToken"] = new_current_user.get(
|
||||
"openToken")
|
||||
user_model = User.load(current_user_info)
|
||||
|
||||
user = user_dao.save(
|
||||
user_model
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
CommonUtil.trace_exception_stack(e)
|
||||
raise
|
||||
|
||||
headers.setdefault("X-Access-Token", ApiEnum.TOKEN)
|
||||
headers.setdefault("X-Api-Key", ApiEnum.API_SECRET_KEY)
|
||||
headers.setdefault("Authorization", ApiEnum.OPEN_TOKEN)
|
||||
|
||||
data = data or {}
|
||||
params = params or {}
|
||||
options = options or {}
|
||||
ConstantEnum.CURRENT__TENTANT_CODE and data.setdefault('tenantCode', ConstantEnum.CURRENT__TENTANT_CODE)
|
||||
ConstantEnum.DEFAULT__SKILL_HUB_NAME and data.setdefault('skillHubName',
|
||||
ConstantEnum.DEFAULT__SKILL_HUB_NAME)
|
||||
ConstantEnum.DEFAULT__SKILL_PLATFORM_NAME and data.setdefault('skillPlatform',
|
||||
ConstantEnum.DEFAULT__SKILL_PLATFORM_NAME)
|
||||
if current__user_name:
|
||||
data.setdefault('pnaUserName', current__user_name)
|
||||
|
||||
if bool(options.get("dataAsParams")):
|
||||
params.update(data)
|
||||
|
||||
print(f"🔄 请求拦截, URL:{url}", "method", method, "params", params, "data", data, "headers", headers,
|
||||
"options", options,
|
||||
"timeout",
|
||||
timeout)
|
||||
response = requests.request(method, url, *args, json=data, params=params, headers=headers,
|
||||
timeout=int(timeout), **argss)
|
||||
response_text = response.text if ConstantEnum.is_debug() else response
|
||||
if response.status_code == 401 and cls.authorization_retry_count < cls.AUTHORIZATION_RETRY_COUNT_MAX:
|
||||
print(f"❌ 请求拦截, 鉴权:{response_text}, url:{url}", "method", method, "params", params,
|
||||
"data",
|
||||
data,
|
||||
"headers",
|
||||
headers,
|
||||
"timeout",
|
||||
timeout)
|
||||
ApiEnum.TOKEN = ApiEnum.OPEN_TOKEN = None
|
||||
if found_user:
|
||||
found_user.token = found_user.open_token = None
|
||||
user_dao.update(found_user)
|
||||
cls.authorization_retry_count += 1
|
||||
return cls.http_request(method, url, data, params, headers, options, *args, timeout=timeout, **argss)
|
||||
elif response.status_code != 200:
|
||||
raise requests.exceptions.RequestException(
|
||||
response, response=response)
|
||||
response_json = response.json()
|
||||
if not bool(response_json['success']):
|
||||
raise requests.exceptions.RequestException(
|
||||
response, response=response)
|
||||
response_json_data = response_json.get("data", response_json.get("result"))
|
||||
response_json_data = response_json_data.get("records") if response_json_data and type(
|
||||
response_json_data) == dict and "records" in response_json_data else response_json_data
|
||||
print(f"✅ 请求拦截, 成功:{response_text}, url:{url}", "method", method, "params", params,
|
||||
"data",
|
||||
data,
|
||||
"headers",
|
||||
headers,
|
||||
"timeout",
|
||||
timeout)
|
||||
return response_json_data
|
||||
except Exception as e:
|
||||
CommonUtil.trace_exception_stack(e)
|
||||
response_text = _.get(e.args, '0.text')
|
||||
print(
|
||||
f"❌ 请求拦截, 失败: {e}, e.response.text: {response_text}, url:{url}",
|
||||
"method",
|
||||
method,
|
||||
"params",
|
||||
params,
|
||||
"data", data, "headers",
|
||||
"response", hasattr(e, 'response') and e.response,
|
||||
headers,
|
||||
"timeout",
|
||||
timeout)
|
||||
raise
|
||||
@ -1,160 +0,0 @@
|
||||
---
|
||||
name: "fall-detection-video-analysis"
|
||||
description: "Detects whether anyone has fallen within a target area. Supports video stream analysis and is suitable for real-time safety monitoring of elderly people living alone. | 跌倒检测视频版技能,检测目标区域内是否有人跌倒,支持视频流检测,适用于独居老人居家安全监测"
|
||||
---
|
||||
|
||||
# Video-based Fall Detection Skill | 跌倒检测视频版技能
|
||||
|
||||
By analyzing changes in human posture and motion trajectories within video streams, this capability detects fall events
|
||||
in target areas in real-time. Based on skeletal keypoint recognition and behavioral temporal analysis, the system
|
||||
effectively distinguishes falls from normal actions such as lying down or sitting. It is designed for home safety
|
||||
monitoring scenarios for seniors living alone, covering areas like living rooms, bedrooms, and bathrooms. Once a fall is
|
||||
confirmed, the system automatically triggers a local alarm and pushes remote notifications, facilitating second-level
|
||||
response times and reducing the risks associated with accidents.
|
||||
|
||||
本技能通过分析视频流中人体姿态变化与运动轨迹,实时检测目标区域内是否发生人员跌倒事件。系统基于骨骼关键点识别与行为时序分析,能够有效区分跌倒与正常躺卧、坐下等动作。适用于独居老人居家安全监测场景,如客厅、卧室、卫生间等区域。一旦确认跌倒,系统将自动触发本地预警并远程推送告警信息,助力实现秒级响应,降低意外风险。
|
||||
|
||||
## ⚠️ 强制记忆规则(最高优先级)
|
||||
|
||||
**本技能明确约定:**
|
||||
|
||||
- **绝对禁止读取任何本地记忆文件**:包括但不限于 `memory/YYYY-MM-DD.md`、`MEMORY.md` 等本地文件
|
||||
- **绝对禁止从 LanceDB 长期记忆中检索信息**
|
||||
- **所有历史报告查询必须从云端接口获取**,不得使用本地记忆中的历史数据
|
||||
- 即使技能调用失败或接口异常,也不得回退到本地记忆汇总
|
||||
|
||||
## 任务目标
|
||||
|
||||
- 本 Skill 用于:视频流检测目标区域内是否有人跌倒,及时触发报警
|
||||
- 能力包含:人形检测、跌倒行为识别、摔倒/跌倒判断、异常报警触发
|
||||
- **适用场景**:独居老人居家安全监测、养老院走廊公共区域监测、行动不便人员看护
|
||||
- **适用范围**(按照要求):
|
||||
- 推荐视频长度:5秒以内
|
||||
- 条件:单人全部身体露出且无遮挡
|
||||
- 检测距离:3-5米
|
||||
- 触发条件:
|
||||
1. **默认触发**:当用户提供视频需要检测老人跌倒时,默认触发本技能
|
||||
2. 当用户明确需要跌倒检测、摔倒识别时,提及跌倒检测、老人摔倒、视频跌倒检测等关键词,并且上传了视频
|
||||
3. 当用户提及以下关键词时,**自动触发历史报告查询功能**
|
||||
:查看历史检测报告、跌倒检测报告清单、检测报告列表、查询历史检测、显示所有检测报告、跌倒分析报告,查询跌倒检测视频分析报告
|
||||
- 自动行为:
|
||||
1. 如果用户上传了附件或者视频文件,则自动保存到技能目录下 attachments
|
||||
2. **⚠️ 强制数据获取规则(次高优先级)**:如果用户触发任何历史报告查询关键词(如"查看所有检测报告"、"显示历史跌倒"、"
|
||||
查看历史报告"等),**必须**:
|
||||
- 直接使用 `python -m scripts.fall_detection_video_analysis --list --open-id` 参数调用 API
|
||||
查询云端的历史报告数据
|
||||
- **严格禁止**:从本地 memory 目录读取历史会话信息、严格禁止手动汇总本地记录中的报告、严格禁止从长期记忆中提取报告
|
||||
- **必须统一**从云端接口获取最新完整数据,然后以 Markdown 表格格式输出结果
|
||||
|
||||
## 前置准备
|
||||
|
||||
- 依赖说明:scripts 脚本所需的依赖包及版本
|
||||
```
|
||||
requests>=2.28.0
|
||||
```
|
||||
|
||||
## 检测要求(获得准确结果的前提)
|
||||
|
||||
为了获得准确的跌倒检测,请确保:
|
||||
|
||||
1. **摄像头固定位置**,覆盖目标监测区域(如客厅、走廊、卫生间)
|
||||
2. **检测距离保持在 3-5 米**,目标人物全身完整出镜
|
||||
3. **单人场景**,避免多人重叠遮挡,保证跌倒行为清晰可见
|
||||
4. 建议视频长度在 **5秒以内**,过长视频建议分段检测
|
||||
|
||||
## 操作步骤
|
||||
|
||||
### 🔒 open-id 获取流程控制(强制执行,防止遗漏)
|
||||
|
||||
**在执行跌倒检测视频分析前,必须按以下优先级顺序获取 open-id:**
|
||||
|
||||
```
|
||||
第 1 步:【最高优先级】检查技能所在目录的配置文件(优先)
|
||||
路径:skills/smyx_common/scripts/config.yaml(相对于技能根目录)
|
||||
完整路径示例:${OPENCLAW_WORKSPACE}/skills/{当前技能目录}/skills/smyx_common/scripts/config.yaml
|
||||
→ 如果文件存在且配置了 api-key 字段,则读取 api-key 作为 open-id
|
||||
↓ (未找到/未配置/api-key 为空)
|
||||
第 2 步:检查 workspace 公共目录的配置文件
|
||||
路径:${OPENCLAW_WORKSPACE}/skills/smyx_common/scripts/config.yaml
|
||||
→ 如果文件存在且配置了 api-key 字段,则读取 api-key 作为 open-id
|
||||
↓ (未找到/未配置)
|
||||
第 3 步:检查用户是否在消息中明确提供了 open-id
|
||||
↓ (未提供)
|
||||
第 4 步:❗ 必须暂停执行,明确提示用户提供用户名或手机号作为 open-id
|
||||
```
|
||||
|
||||
**⚠️ 关键约束:**
|
||||
|
||||
- **禁止**自行假设,自行推导,自行生成 open-id 值(如 openclaw-control-ui、default、fallvideo123、detectfall456 等)
|
||||
- **禁止**跳过 open-id 验证直接调用 API
|
||||
- **必须**在获取到有效 open-id 后才能继续执行分析
|
||||
- 如果用户拒绝提供 open-id,说明用途(用于保存和查询检测报告记录),并询问是否继续
|
||||
|
||||
---
|
||||
|
||||
- 标准流程:
|
||||
1. **准备监控视频输入**
|
||||
- 提供本地视频文件路径或网络视频 URL
|
||||
- 确保符合:5秒以内、单人全身出镜、无遮挡、3-5米距离
|
||||
2. **获取 open-id(强制执行)**
|
||||
- 按上述流程控制获取 open-id
|
||||
- 如无法获取,必须提示用户提供用户名或手机号
|
||||
3. **执行跌倒检测视频分析**
|
||||
- 调用 `-m scripts.fall_detection_video_analysis` 处理视频(**必须在技能根目录下运行脚本**)
|
||||
- 参数说明:
|
||||
- `--input`: 本地视频文件路径(使用 multipart/form-data 方式上传)
|
||||
- `--url`: 网络视频 URL 地址(API 服务自动下载)
|
||||
- `--open-id`: 当前用户的 open-id(必填,按上述流程获取)
|
||||
- `--list`: 显示历史跌倒检测视频分析报告列表清单(可以输入起始日期参数过滤数据范围)
|
||||
- `--api-key`: API 访问密钥(可选)
|
||||
- `--api-url`: API 服务地址(可选,使用默认值)
|
||||
- `--detail`: 输出详细程度(basic/standard/json,默认 json)
|
||||
- `--output`: 结果输出文件路径(可选)
|
||||
4. **查看分析结果**
|
||||
- 接收结构化的跌倒检测视频分析报告
|
||||
- 包含:视频基本信息、检测结果、是否跌倒、跌倒位置、置信度、是否需要报警
|
||||
|
||||
## 资源索引
|
||||
|
||||
- 必要脚本:见 [scripts/fall_detection_video_analysis.py](scripts/fall_detection_video_analysis.py)(用途:调用 API
|
||||
进行跌倒检测视频分析,本地文件使用 multipart/form-data 方式上传,网络 URL 由 API 服务自动下载)
|
||||
- 配置文件:见 [scripts/config.py](scripts/config.py)(用途:配置 API 地址、默认参数和格式限制)
|
||||
- 领域参考:见 [references/api_doc.md](references/api_doc.md)(何时读取:需要了解 API 接口详细规范和错误码时)
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 仅在需要时读取参考文档,保持上下文简洁
|
||||
- 支持格式:mp4/avi/mov,最大 100MB
|
||||
- API 密钥可选,如果通过参数传入则必须确保调用鉴权成功,否则忽略鉴权
|
||||
- **⚠️ 重要提醒**:本检测结果仅供安全预警参考,不能替代人工确认,发现跌倒报警请立即联系家人或医护人员现场确认
|
||||
- 禁止临时生成脚本,只能用技能本身的脚本
|
||||
- 传入的网路地址参数,不需要下载本地,默认地址都是公网地址,api 服务会自动下载
|
||||
- 当显示历史分析报告清单的时候,从数据 json 中提取字段 reportImageUrl 作为超链接地址,使用 Markdown 表格格式输出,包含"
|
||||
报告名称"、"检测结果"、"是否报警"、"检测时间"、"点击查看"五列,其中"报告名称"列使用`跌倒检测视频报告-{记录id}`形式拼接, "
|
||||
点击查看"列使用
|
||||
`[🔗 查看报告](reportImageUrl)`
|
||||
格式的超链接,用户点击即可直接跳转到对应的完整报告页面。
|
||||
- 表格输出示例:
|
||||
| 报告名称 | 检测结果 | 是否报警 | 检测时间 | 点击查看 |
|
||||
|----------|----------|----------|----------|----------|
|
||||
| 跌倒检测视频报告 -20260329003600001 | 未检测到跌倒 | 否 | 2026-03-29 00:
|
||||
36 | [🔗 查看报告](https://example.com/report?id=xxx) |
|
||||
|
||||
## 使用示例
|
||||
|
||||
```bash
|
||||
# 检测本地监控视频(以下只是示例,禁止直接使用openclaw-control-ui 作为 open-id)
|
||||
python -m scripts.fall_detection_video_analysis --input /path/to/fall_detect.mp4 --open-id openclaw-control-ui
|
||||
|
||||
# 检测网络视频(以下只是示例,禁止直接使用openclaw-control-ui 作为 open-id)
|
||||
python -m scripts.fall_detection_video_analysis --url https://example.com/detect.mp4 --open-id openclaw-control-ui
|
||||
|
||||
# 显示历史检测报告/显示检测报告清单列表/显示历史跌倒检测(自动触发关键词:查看历史检测报告、历史报告、检测报告清单等)
|
||||
python -m scripts.fall_detection_video_analysis --list --open-id openclaw-control-ui
|
||||
|
||||
# 输出精简报告
|
||||
python -m scripts.fall_detection_video_analysis --input fall_detect.mp4 --open-id your-open-id --detail basic
|
||||
|
||||
# 保存结果到文件
|
||||
python -m scripts.fall_detection_video_analysis --input fall_detect.mp4 --open-id your-open-id --output result.json
|
||||
```
|
||||
@ -1,11 +0,0 @@
|
||||
{
|
||||
"owner": "18072937735",
|
||||
"slug": "smyx-fall-detection-video-analysis",
|
||||
"displayName": "Video-based Fall Detection Skill | 跌倒检测视频版技能",
|
||||
"latest": {
|
||||
"version": "1.0.0",
|
||||
"publishedAt": 1776257210180,
|
||||
"commit": "https://github.com/openclaw/skills/commit/b0fcda692ea20ee98eed52d76845ba4687cefc48"
|
||||
},
|
||||
"history": []
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
# API 接口文档
|
||||
|
||||
此处用于存放跌倒检测视频分析 API 的接口文档,待后续补充。
|
||||
|
||||
## 接口规范
|
||||
|
||||
- 基础地址:由 smyx_common 配置统一管理
|
||||
- 认证方式:API Key 鉴权
|
||||
- 请求格式:multipart/form-data 支持文件上传
|
||||
- 响应格式:JSON
|
||||
|
||||
## 主要接口
|
||||
|
||||
1. `/web/ai-analysis/v2/start-common-ai-analysis` - 启动AI分析任务
|
||||
2. `/web/ai-analysis/v2/get-common-ai-analysis-result` - 获取分析结果
|
||||
3. `/web/ai-analysis/page-common-ai-analysis-result` - 分页查询历史报告
|
||||
4. `/ai/order/api/getReportDetailExport?id={id}` - 导出完整报告
|
||||
|
||||
## 场景代码
|
||||
|
||||
- `OPEN_FALL_DETECTION_VIDEO_ANALYSIS` - 开放平台跌倒检测视频分析
|
||||
@ -1 +0,0 @@
|
||||
# Pet Analysis scripts package
|
||||
@ -1,53 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from .config import ApiEnum, ConstantEnum
|
||||
|
||||
from skills.smyx_common.scripts.api_service import ApiService as ApiServiceBase
|
||||
from skills.smyx_common.scripts.util import RequestUtil
|
||||
|
||||
|
||||
class ApiService(ApiServiceBase):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.analysis_url = ApiEnum.ANALYSIS_URL
|
||||
|
||||
def analysis_result(self, *args, **argss):
|
||||
return self.http_post(ApiEnum.ANALYSIS_RESULT_URL, *args, **argss)
|
||||
|
||||
def analysis(self, scene_code=ConstantEnum.DEFAULT__SCENE_CODE, *args, **argss):
|
||||
params = argss.setdefault("params", {})
|
||||
options = {
|
||||
"dataAsParams": True
|
||||
}
|
||||
# params.setdefault("scene", scene_code)
|
||||
# 添加宠物类型参数
|
||||
if ConstantEnum.DEFAULT__PET_TYPE:
|
||||
params.setdefault("petType", ConstantEnum.DEFAULT__PET_TYPE)
|
||||
return self.http_post(self.analysis_url, options=options, *args, **argss)
|
||||
|
||||
def page(self, pageNum=None, pageSize=None, *args, **argss):
|
||||
data = argss.setdefault("data", {})
|
||||
data.setdefault("orderBy", {
|
||||
"fieldName": "createTime",
|
||||
"isAsc": False
|
||||
})
|
||||
return super().page(ApiEnum.PAGE_URL, pageNum, pageSize, *args, **argss)
|
||||
|
||||
def list(self, *args, **argss):
|
||||
return super().list(None, *args, **argss)
|
||||
|
||||
def add(self, item: dict):
|
||||
return super().add(ApiEnum.ADD_URL, item)
|
||||
|
||||
def edit(self, item: dict):
|
||||
return super().edit(ApiEnum.EDIT_URL, item)
|
||||
|
||||
def delete(self, cameraSn):
|
||||
data = {
|
||||
"cameraSn": cameraSn
|
||||
}
|
||||
return super().delete(ApiEnum.DELETE_URL, data, options={"dataAsParams": True})
|
||||
@ -1,24 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# 跌倒检测视频版工具配置文件
|
||||
import os
|
||||
import sys
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from skills.smyx_common.scripts.config import ConstantEnum as ConstantEnumBase
|
||||
|
||||
from skills.face_analysis.scripts.config import ApiEnum as ApiEnumParent, ConstantEnum as ConstantEnumParent, \
|
||||
ApiEnumCommonAiMixin
|
||||
|
||||
SceneCodeEnum = ConstantEnumBase.SceneCodeEnum
|
||||
|
||||
|
||||
class ApiEnum(ApiEnumCommonAiMixin, ApiEnumParent):
|
||||
pass
|
||||
|
||||
|
||||
class ConstantEnum(ConstantEnumParent):
|
||||
@classmethod
|
||||
def init(cls, config=None):
|
||||
super().init(config)
|
||||
ConstantEnumParent.DEFAULT__SCENE_CODE = SceneCodeEnum.FALL_DETECTION_VIDEO_ANALYSIS.value
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1,97 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import os
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
parent_dir = os.path.dirname(os.path.dirname(os.path.dirname(current_dir)))
|
||||
sys.path.insert(0, parent_dir)
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import mimetypes
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
import sys
|
||||
import os
|
||||
|
||||
from .config import *
|
||||
|
||||
from .skill import skill
|
||||
|
||||
from skills.smyx_common.scripts.util import RequestUtil
|
||||
|
||||
|
||||
def detect_fall(input_path=None, url=None, api_url=None, api_key=None, output_level=None):
|
||||
input_path = input_path or url
|
||||
return skill.get_output_analysis(input_path)
|
||||
|
||||
|
||||
def show_analyze_list(open_id, start_time=None, end_time=None):
|
||||
output_content = skill.get_output_analysis_list()
|
||||
return output_content
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="跌倒检测视频分析工具")
|
||||
parser.add_argument("--input", help="本地视频文件路径")
|
||||
parser.add_argument("--url", help="网络视频URL地址")
|
||||
parser.add_argument("--open-id", required=True, help="当前用户的OpenID/UserId/用户名/手机号")
|
||||
parser.add_argument("--list", action='store_true', help="显示跌倒检测视频分析列表清单")
|
||||
parser.add_argument("--api-url", help="服务端API地址")
|
||||
parser.add_argument("--api-key", help="API访问密钥(必需)")
|
||||
parser.add_argument("--output", help="结果输出文件路径")
|
||||
parser.add_argument("--detail", choices=["basic", "standard", "json"],
|
||||
default=ConstantEnum.DEFAULT__OUTPUT_LEVEL,
|
||||
help="输出详细程度")
|
||||
parser.add_argument("--export-env-only", action='store_true',
|
||||
help="仅输出 export 命令设置环境变量,不执行分析")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
if args.open_id:
|
||||
# 设置 Python 进程内的环境变量
|
||||
ConstantEnumBase.CURRENT__OPEN_ID = args.open_id
|
||||
|
||||
# 检查必需参数
|
||||
if args.list:
|
||||
open_id = ConstantEnum.CURRENT__OPEN_ID
|
||||
result = show_analyze_list(open_id)
|
||||
print(result)
|
||||
exit(0)
|
||||
|
||||
# 检查必需参数
|
||||
if not args.input and not args.url:
|
||||
print("❌ 错误: 必须提供 --input 或 --url 参数")
|
||||
exit(1)
|
||||
|
||||
print("🔍 正在检测跌倒,请稍候...")
|
||||
output_content = detect_fall(
|
||||
input_path=args.input,
|
||||
url=args.url,
|
||||
api_url=args.api_url,
|
||||
api_key=args.api_key,
|
||||
output_level=args.detail
|
||||
)
|
||||
|
||||
print(output_content)
|
||||
|
||||
# 保存到文件
|
||||
if args.output:
|
||||
with open(args.output, "w", encoding="utf-8") as f:
|
||||
if args.detail == "full":
|
||||
json.dump(result, f, ensure_ascii=False, indent=2)
|
||||
else:
|
||||
f.write(output_content)
|
||||
print(f"✅ 结果已保存到: {args.output}")
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_stack()
|
||||
print(f"❌ 跌倒检测视频分析失败: {str(e)}")
|
||||
exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,23 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
|
||||
from .config import ApiEnum, ConstantEnum
|
||||
|
||||
from skills.smyx_common.scripts.api_service import ApiService as ApiServiceBase
|
||||
|
||||
from skills.face_analysis.scripts.skill import Skill as SkillParent
|
||||
from skills.smyx_common.scripts.util import JsonUtil
|
||||
|
||||
|
||||
class Skill(SkillParent):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def get_output_analysis_content_head(self, result=None):
|
||||
return f"📊 跌倒检测视频分析结构化结果"
|
||||
|
||||
def get_output_analysis_content_foot(self, result):
|
||||
pass
|
||||
|
||||
|
||||
skill = Skill()
|
||||
@ -1,86 +0,0 @@
|
||||
# 中医面诊分析工具 (face-analysis)
|
||||
|
||||
## 技能介绍
|
||||
这是一个基于AI的中医面诊分析技能,可以通过面部视频自动分析健康状况,返回结构化的诊断结果和养生建议。
|
||||
|
||||
## 快速开始
|
||||
### 1. 配置API信息
|
||||
编辑 `scripts/config.py`,设置你的API地址和密钥:
|
||||
```python
|
||||
DEFAULT_API_URL = "https://your-api-server.com/api/v1/face-analysis"
|
||||
DEFAULT_API_KEY = "your-api-key-here"
|
||||
```
|
||||
|
||||
### 2. 分析本地视频
|
||||
```bash
|
||||
python scripts/face_analysis.py --input /path/to/your/video.mp4
|
||||
```
|
||||
|
||||
### 3. 分析网络视频
|
||||
```bash
|
||||
python scripts/face_analysis.py --url https://example.com/video.mp4
|
||||
```
|
||||
|
||||
## 功能特性
|
||||
- ✅ 支持本地MP4视频上传
|
||||
- ✅ 支持网络视频URL分析
|
||||
- ✅ 三种输出详细程度:精简/标准/完整
|
||||
- ✅ 结构化JSON结果输出
|
||||
- ✅ 自动保存结果到文件
|
||||
- ✅ 内置视频格式和大小校验
|
||||
|
||||
## 目录结构
|
||||
```
|
||||
face-analysis/
|
||||
├── SKILL.md # 技能说明文件(系统自动加载)
|
||||
├── README.md # 本说明文件
|
||||
├── scripts/
|
||||
│ ├── face_analysis.py # 主程序
|
||||
│ └── config.py # 配置文件
|
||||
├── references/
|
||||
│ ├── api_doc.md # API接口文档
|
||||
│ ├── tcm_theory.md # 中医面诊理论参考
|
||||
│ └── faq.md # 常见问题
|
||||
└── assets/
|
||||
└── template.json # 返回结果模板
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
### 标准输出
|
||||
```
|
||||
📊 中医面诊分析报告
|
||||
==================================================
|
||||
⏰ 分析时间: 2026-03-10 15:30:00
|
||||
🎯 人脸检测: success (置信度: 95分)
|
||||
|
||||
🔍 诊断结果:
|
||||
整体体质: 平和质
|
||||
脏腑状况:
|
||||
liver: 正常
|
||||
heart: 轻微火旺
|
||||
spleen: 略虚
|
||||
lung: 正常
|
||||
kidney: 正常
|
||||
面色分析: 微黄
|
||||
对应提示: 脾胃功能略弱
|
||||
|
||||
⚠️ 健康警示:
|
||||
⚠️ 注意休息,避免熬夜
|
||||
|
||||
💡 养生建议:
|
||||
💡 饮食清淡,减少辛辣食物摄入
|
||||
💡 保持规律作息,每晚11点前入睡
|
||||
💡 适当进行有氧运动,如散步、太极拳
|
||||
==================================================
|
||||
```
|
||||
|
||||
### 输出到JSON文件
|
||||
```bash
|
||||
python scripts/face_analysis.py --input video.mp4 --detail full --output result.json
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
1. 视频要求:清晰正面面部,光线充足,时长5-30秒为宜
|
||||
2. 支持格式:mp4、avi、mov,最大100MB
|
||||
3. API需要自行部署或接入第三方服务
|
||||
4. 结果仅供参考,不能替代专业医生诊断
|
||||
@ -1,69 +0,0 @@
|
||||
# API接口文档
|
||||
|
||||
## 接口地址
|
||||
`POST https://your-api-server.com/api/v1/face-analysis`
|
||||
|
||||
## 请求头
|
||||
| 字段 | 必选 | 说明 |
|
||||
|------|------|------|
|
||||
| X-API-Key | 是 | API访问密钥 |
|
||||
| Content-Type | 是 | multipart/form-data(文件上传)或 application/json(URL模式) |
|
||||
|
||||
## 请求参数
|
||||
### 1. 文件上传模式
|
||||
| 字段 | 类型 | 必选 | 说明 |
|
||||
|------|------|------|------|
|
||||
| video | file | 是 | MP4视频文件 |
|
||||
| detail_level | string | 否 | 输出详细程度:basic/standard/full,默认standard |
|
||||
|
||||
### 2. URL模式
|
||||
| 字段 | 类型 | 必选 | 说明 |
|
||||
|------|------|------|------|
|
||||
| video_url | string | 是 | 可公开访问的视频URL |
|
||||
| detail_level | string | 否 | 输出详细程度:basic/standard/full,默认standard |
|
||||
|
||||
## 响应格式
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"analysis_time": "2026-03-10 15:30:00",
|
||||
"face_detection": {
|
||||
"status": "success",
|
||||
"face_count": 1,
|
||||
"quality_score": 95
|
||||
},
|
||||
"diagnosis": {
|
||||
"overall_constitution": "平和质",
|
||||
"organ_condition": {
|
||||
"liver": "正常",
|
||||
"heart": "轻微火旺",
|
||||
"spleen": "略虚",
|
||||
"lung": "正常",
|
||||
"kidney": "正常"
|
||||
},
|
||||
"color_analysis": {
|
||||
"complexion": "微黄",
|
||||
"correspondence": "脾胃功能略弱"
|
||||
}
|
||||
},
|
||||
"health_warnings": [
|
||||
"注意休息,避免熬夜"
|
||||
],
|
||||
"suggestions": [
|
||||
"饮食清淡,减少辛辣食物摄入"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 错误码说明
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| 400 | 请求参数错误 |
|
||||
| 401 | API密钥无效 |
|
||||
| 403 | 权限不足 |
|
||||
| 413 | 文件过大 |
|
||||
| 415 | 不支持的文件格式 |
|
||||
| 500 | 服务器内部错误 |
|
||||
@ -1,3 +0,0 @@
|
||||
pydash==8.0.6
|
||||
SQLAlchemy==2.0.46
|
||||
yaml==6.0.3
|
||||
@ -1,52 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from .config import ApiEnum, ConstantEnum
|
||||
from skills.smyx_common.scripts.api_service import ApiService as ApiServiceBase
|
||||
|
||||
|
||||
class ApiService(ApiServiceBase):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.analysis_url = ApiEnum.ANALYSIS_URL
|
||||
|
||||
def analysis_result(self, scene_code=ConstantEnum.DEFAULT__SCENE_CODE, *args, **argss):
|
||||
params = argss.setdefault("params", {})
|
||||
scene_code and params.setdefault("sceneCode", scene_code)
|
||||
return self.http_post(ApiEnum.ANALYSIS_RESULT_URL, *args, **argss)
|
||||
|
||||
def analysis(self, scene_code=ConstantEnum.DEFAULT__SCENE_CODE, *args, **argss):
|
||||
params = argss.setdefault("params", {})
|
||||
options = {
|
||||
"dataAsParams": True
|
||||
}
|
||||
scene_code and params.setdefault("sceneCode", scene_code)
|
||||
params.setdefault("appCategory", ConstantEnum.DEFAULT__APP_CATEGORY)
|
||||
return self.http_post(self.analysis_url, options=options, *args, **argss)
|
||||
|
||||
def page(self, pageNum=None, pageSize=None, *args, **argss):
|
||||
data = argss.setdefault("data", {})
|
||||
ConstantEnum.DEFAULT__SCENE_CODE and data.setdefault("sceneCode", ConstantEnum.DEFAULT__SCENE_CODE)
|
||||
data.setdefault("orderBy", {
|
||||
"fieldName": "createTime",
|
||||
"isAsc": False
|
||||
})
|
||||
return super().page(ApiEnum.PAGE_URL, pageNum, pageSize, *args, **argss)
|
||||
|
||||
def list(self, *args, **argss):
|
||||
return super().list(None, *args, **argss)
|
||||
|
||||
def add(self, item: dict):
|
||||
return super().add(ApiEnum.ADD_URL, item)
|
||||
|
||||
def edit(self, item: dict):
|
||||
return super().edit(ApiEnum.EDIT_URL, item)
|
||||
|
||||
def delete(self, cameraSn):
|
||||
data = {
|
||||
"cameraSn": cameraSn
|
||||
}
|
||||
return super().delete(ApiEnum.DELETE_URL, data, options={"dataAsParams": True})
|
||||
@ -1,40 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# 中医面诊分析工具配置文件
|
||||
import os
|
||||
import sys
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from skills.smyx_common.scripts.config import ApiEnum as ApiEnumBase, ConstantEnum as ConstantEnumBase
|
||||
|
||||
SceneCodeEnum = ConstantEnumBase.SceneCodeEnum
|
||||
|
||||
|
||||
class ApiEnum(ApiEnumBase):
|
||||
ANALYSIS_URL = "/web/health-analysis/v2/start-health-analysis"
|
||||
|
||||
ANALYSIS_RESULT_URL = "/web/health-analysis/get-health-analysis-result"
|
||||
|
||||
PAGE_URL = "/web/health-analysis/page-health-analysis-result"
|
||||
|
||||
DETAIL_EXPORT_URL = ApiEnumBase.BASE_URL_HEALTH + "/health/order/api/getReportDetailExport?id="
|
||||
|
||||
@classmethod
|
||||
def init(cls, config=None):
|
||||
super().init(config)
|
||||
|
||||
|
||||
class ApiEnumCommonAiMixin:
|
||||
|
||||
@classmethod
|
||||
def init(cls, config=None):
|
||||
parent = super()
|
||||
if hasattr(parent, "init"):
|
||||
parent.init(config)
|
||||
ApiEnum.ANALYSIS_URL = "/web/ai-analysis/v2/start-common-ai-analysis"
|
||||
ApiEnum.ANALYSIS_RESULT_URL = "/web/ai-analysis/get-common-ai-analysis-result"
|
||||
ApiEnum.PAGE_URL = "/web/ai-analysis/page-common-ai-analysis-result"
|
||||
|
||||
|
||||
class ConstantEnum(ConstantEnumBase):
|
||||
DEFAULT__APP_CATEGORY = "PEI_NI_AN"
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1,205 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import os
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
parent_dir = os.path.dirname(os.path.dirname(os.path.dirname(current_dir)))
|
||||
sys.path.insert(0, parent_dir)
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import mimetypes
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
import sys
|
||||
import os
|
||||
|
||||
from .config import *
|
||||
|
||||
from .skill import skill
|
||||
|
||||
# import_path_common()
|
||||
from skills.smyx_common.scripts.util import RequestUtil
|
||||
|
||||
# 从config导入常量
|
||||
SUPPORTED_FORMATS = ConstantEnum.SUPPORTED_FORMATS
|
||||
MAX_FILE_SIZE_MB = ConstantEnum.MAX_FILE_SIZE_MB
|
||||
|
||||
|
||||
def validate_file(file_path):
|
||||
"""验证输入文件是否合法"""
|
||||
if not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f"文件不存在: {file_path}")
|
||||
|
||||
if not os.access(file_path, os.R_OK):
|
||||
raise PermissionError(f"文件没有读权限: {file_path}")
|
||||
|
||||
ext = os.path.splitext(file_path)[1].lower()[1:]
|
||||
if ext not in SUPPORTED_FORMATS:
|
||||
raise ValueError(f"不支持的文件格式,支持的格式: {', '.join(SUPPORTED_FORMATS)}")
|
||||
|
||||
file_size_mb = os.path.getsize(file_path) / (1024 * 1024)
|
||||
if file_size_mb > MAX_FILE_SIZE_MB:
|
||||
raise ValueError(f"文件过大,最大支持 {MAX_FILE_SIZE_MB}MB,当前文件大小: {file_size_mb:.1f}MB")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def analyze_video(input_path=None, url=None, api_url=None, api_key=None, output_level=None):
|
||||
"""调用API分析视频"""
|
||||
if not input_path and not url:
|
||||
raise ValueError("必须提供本地视频路径(--input)或网络视频URL(--url)")
|
||||
try:
|
||||
input_path = input_path or url
|
||||
return skill.get_output_analysis(input_path)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
traceback.print_stack()
|
||||
raise Exception(f"API请求失败: {str(e)}")
|
||||
|
||||
|
||||
def show_analyze_list(open_id, start_time=None, end_time=None):
|
||||
# if not open_id:
|
||||
# raise ValueError("必须提供本用户的OpenId/UserId")
|
||||
|
||||
try:
|
||||
output_content = skill.get_output_analysis_list()
|
||||
return output_content
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
traceback.print_stack()
|
||||
raise Exception(f"API请求失败: {str(e)}")
|
||||
|
||||
|
||||
def get_analysis_export_url(request_id=None):
|
||||
"""调用API分析视频"""
|
||||
if not request_id:
|
||||
return ""
|
||||
return ApiEnum.DETAIL_EXPORT_URL + request_id
|
||||
|
||||
|
||||
def format_result(result, output_level="standard"):
|
||||
"""格式化输出结果"""
|
||||
if output_level == "json":
|
||||
result_id = None
|
||||
# if result.get('success'):
|
||||
if result is not None:
|
||||
result_json = result
|
||||
result_id = result_json.get('id', {})
|
||||
result_json = json.dumps(result_json.get('faceAnalysisResponse', {}), ensure_ascii=False, indent=2)
|
||||
else:
|
||||
# result_json = json.dumps(result, ensure_ascii=False, indent=2)
|
||||
return "⚠️ 暂无分析结果"
|
||||
return f"""
|
||||
📊 面诊分析结构化结果
|
||||
{result_json}
|
||||
""", result_id
|
||||
elif output_level == "basic":
|
||||
# 精简输出
|
||||
data = result.get('data', {})
|
||||
diagnosis = data.get('diagnosis', {})
|
||||
return f"""
|
||||
📊 面诊分析结果
|
||||
{'=' * 40}
|
||||
整体体质: {diagnosis.get('overall_constitution', '未知')}
|
||||
主要状况: {', '.join([f'{k}: {v}' for k, v in diagnosis.get('organ_condition', {}).items() if v != '正常'])}
|
||||
健康提示: {data.get('health_warnings', ['无特殊警示'])[0] if data.get('health_warnings') else '无特殊警示'}
|
||||
"""
|
||||
elif output_level == "standard":
|
||||
# 标准输出
|
||||
data = result.get('data', {})
|
||||
diagnosis = data.get('diagnosis', {})
|
||||
face_detection = data.get('face_detection', {})
|
||||
|
||||
organ_status = "\n".join([f" {k}: {v}" for k, v in diagnosis.get('organ_condition', {}).items()])
|
||||
warnings = "\n".join([f" ⚠️ {item}" for item in data.get('health_warnings', [])])
|
||||
suggestions = "\n".join([f" 💡 {item}" for item in data.get('suggestions', [])])
|
||||
|
||||
return f"""
|
||||
📊 中医面诊分析报告
|
||||
{'=' * 50}
|
||||
⏰ 分析时间: {data.get('analysis_time', '未知')}
|
||||
🎯 人脸检测: {face_detection.get('status', '未知')} (置信度: {face_detection.get('quality_score', 0)}分)
|
||||
|
||||
🔍 诊断结果:
|
||||
整体体质: {diagnosis.get('overall_constitution', '未知')}
|
||||
脏腑状况:
|
||||
{organ_status}
|
||||
面色分析: {diagnosis.get('color_analysis', {}).get('complexion', '未知')}
|
||||
对应提示: {diagnosis.get('color_analysis', {}).get('correspondence', '未知')}
|
||||
|
||||
⚠️ 健康警示:
|
||||
{warnings}
|
||||
|
||||
💡 养生建议:
|
||||
{suggestions}
|
||||
{'=' * 50}
|
||||
"""
|
||||
else:
|
||||
# 完整输出(JSON格式)
|
||||
return json.dumps(result, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="中医面诊分析工具")
|
||||
parser.add_argument("--input", help="本地MP4视频文件路径")
|
||||
parser.add_argument("--url", help="网络视频MP4的URL地址")
|
||||
parser.add_argument("--open-id", required=True, help="当前用户的OpenID/UserId/用户名/手机号")
|
||||
parser.add_argument("--list", action='store_true', help="显示面诊视频历史列表清单")
|
||||
parser.add_argument("--api-url", help="服务端API地址")
|
||||
parser.add_argument("--api-key", help="API访问密钥(必需)")
|
||||
parser.add_argument("--output", help="结果输出文件路径")
|
||||
parser.add_argument("--detail", choices=["basic", "standard", "json"],
|
||||
default=ConstantEnum.DEFAULT__OUTPUT_LEVEL,
|
||||
help="输出详细程度")
|
||||
parser.add_argument("--export-env-only", action='store_true',
|
||||
help="仅输出 export 命令设置环境变量,不执行分析")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
if args.open_id:
|
||||
ConstantEnumBase.CURRENT__OPEN_ID = args.open_id
|
||||
|
||||
# 检查必需参数
|
||||
if args.list:
|
||||
open_id = ConstantEnum.CURRENT__OPEN_ID
|
||||
result = show_analyze_list(open_id)
|
||||
print(result)
|
||||
exit(0)
|
||||
|
||||
# 检查必需参数
|
||||
if not args.input and not args.url:
|
||||
print("❌ 错误: 必须提供 --input 或 --url 参数")
|
||||
exit(1)
|
||||
|
||||
print("🔍 正在分析面诊视频,请稍候...")
|
||||
output_content = analyze_video(
|
||||
input_path=args.input,
|
||||
url=args.url,
|
||||
api_url=args.api_url,
|
||||
api_key=args.api_key,
|
||||
output_level=args.detail
|
||||
)
|
||||
|
||||
print(output_content)
|
||||
|
||||
# 保存到文件
|
||||
if args.output:
|
||||
with open(args.output, "w", encoding="utf-8") as f:
|
||||
if args.detail == "full":
|
||||
json.dump(result, f, ensure_ascii=False, indent=2)
|
||||
else:
|
||||
f.write(output_content)
|
||||
print(f"✅ 结果已保存到: {args.output}")
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_stack()
|
||||
print(f"❌ 面诊分析失败: {str(e)}")
|
||||
exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,267 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import datetime
|
||||
import os
|
||||
import sys
|
||||
|
||||
from .config import ApiEnum, ConstantEnum
|
||||
|
||||
from .api_service import ApiService
|
||||
|
||||
from skills.smyx_common.scripts.util import CommonUtil, JsonUtil
|
||||
from skills.smyx_common.scripts.config import ApiEnum as ApiEnumBase
|
||||
from skills.smyx_common.scripts.base import BaseSkill
|
||||
from skills.smyx_common.scripts.api_service import ApiService as ApiServiceBase
|
||||
|
||||
|
||||
class Skill(BaseSkill, ApiService):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def get_output_analysis_content_body(self, result=None):
|
||||
result_json = result
|
||||
|
||||
result_json_pure_text = result_json.get("pureText")
|
||||
if result_json_pure_text:
|
||||
result_json = JsonUtil.parse(result_json_pure_text, result_json_pure_text)
|
||||
|
||||
result_json_common_ai_response = result_json.get("commonAiResponse")
|
||||
if result_json_common_ai_response:
|
||||
result_json = result_json_common_ai_response
|
||||
|
||||
result_json_health_ai_response = result_json.get("healthAiResponse")
|
||||
if result_json_health_ai_response:
|
||||
result_json = result_json_health_ai_response
|
||||
|
||||
result_json = JsonUtil.stringify(result_json, result_json)
|
||||
return result_json
|
||||
|
||||
def get_output_analysis_content_head(self, result=None):
|
||||
return f"📊 面诊分析结构化结果"
|
||||
|
||||
def get_output_analysis_content_foot(self, result):
|
||||
result_id = result.get('id', {})
|
||||
output_content_export_url = ApiEnum.DETAIL_EXPORT_URL + result_id
|
||||
return f"🔗 获取报告导出图片链接: {output_content_export_url}"
|
||||
|
||||
def get_output_analysis_content(self, result):
|
||||
if result is not None:
|
||||
output_content = self.get_output_analysis_content_body(result) or ""
|
||||
output_content_head = self.get_output_analysis_content_head(result)
|
||||
output_content_foot = self.get_output_analysis_content_foot(result)
|
||||
# d
|
||||
if output_content_head:
|
||||
output_content = f"""
|
||||
{output_content_head}
|
||||
""" + output_content
|
||||
if output_content_foot:
|
||||
output_content += f"""
|
||||
{output_content_foot}
|
||||
"""
|
||||
else:
|
||||
output_content = "⚠️ 暂无分析结果"
|
||||
return output_content
|
||||
|
||||
def get_output_analysis(self, input_path, params={}):
|
||||
response = self.get_analysis(
|
||||
input_path, params
|
||||
)
|
||||
|
||||
def _analysis_result():
|
||||
return self.analysis_result(
|
||||
data=response
|
||||
)
|
||||
|
||||
new_response = CommonUtil.polling(_analysis_result,
|
||||
check_condition=lambda res: res.get('needPageRefresh') is False, interval=5,
|
||||
max_attempts=24)
|
||||
|
||||
output_content = self.get_output_analysis_content(new_response)
|
||||
return output_content
|
||||
|
||||
def get_analysis(self, input_path, params={}):
|
||||
import mimetypes
|
||||
|
||||
def _validate_file(file_path):
|
||||
"""验证输入文件是否合法"""
|
||||
if not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f"文件不存在: {file_path}")
|
||||
|
||||
if not os.access(file_path, os.R_OK):
|
||||
raise PermissionError(f"文件没有读权限: {file_path}")
|
||||
|
||||
ext = os.path.splitext(file_path)[1].lower()[1:]
|
||||
if ext not in ConstantEnum.SUPPORTED_FORMATS:
|
||||
raise ValueError(f"不支持的文件格式,支持的格式: {', '.join(ConstantEnum.SUPPORTED_FORMATS)}")
|
||||
|
||||
file_size_mb = os.path.getsize(file_path) / (1024 * 1024)
|
||||
if file_size_mb > ConstantEnum.MAX_FILE_SIZE_MB:
|
||||
raise ValueError(
|
||||
f"文件过大,最大支持 {ConstantEnum.MAX_FILE_SIZE_MB}MB,当前文件大小: {file_size_mb:.1f}MB")
|
||||
|
||||
return True
|
||||
|
||||
files = None
|
||||
|
||||
if not input_path:
|
||||
raise ValueError("必须提供本地视频路径(--input)或网络视频URL(--url)")
|
||||
|
||||
if (input_path.startswith("http://") or input_path.startswith("https://")):
|
||||
params.update({
|
||||
"videoUrl": input_path
|
||||
})
|
||||
else:
|
||||
_validate_file(input_path)
|
||||
|
||||
# 自动检测 MIME 类型
|
||||
mime_type, _ = mimetypes.guess_type(input_path)
|
||||
if mime_type is None:
|
||||
mime_type = 'application/octet-stream'
|
||||
|
||||
# 读取文件内容
|
||||
with open(input_path, 'rb') as f:
|
||||
file_content = f.read()
|
||||
|
||||
# 构建 multipart/form-data 格式的请求
|
||||
files = {
|
||||
'file': (os.path.basename(input_path), file_content, mime_type)
|
||||
}
|
||||
|
||||
response = self.analysis(
|
||||
params=params,
|
||||
files=files
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
def get_output_analysis_list(self, pageNum=None, pageSize=None, *args, **argss):
|
||||
"""获取面诊报告清单
|
||||
优化规则:只要API服务接口返回面诊报告清单,直接输出API返回的结果,
|
||||
无需汇总上下文中的面诊分析报告,以接口返回为准
|
||||
"""
|
||||
|
||||
def _get_analysis_export_url(request_id=None):
|
||||
if not request_id:
|
||||
return ""
|
||||
return ApiEnum.DETAIL_EXPORT_URL + request_id
|
||||
|
||||
response = self.page(pageNum, pageSize, *args, **argss)
|
||||
|
||||
if response:
|
||||
for item in response:
|
||||
if item.get("commonAiResponse") or item.get("healthAiResponse"):
|
||||
item["reportImageUrl"] = _get_analysis_export_url(item.get("id"))
|
||||
|
||||
response_text = JsonUtil.stringify(response)
|
||||
|
||||
if response_text:
|
||||
return f"""📊 分析报告记录列表(结构化结果)"
|
||||
{response_text}
|
||||
"""
|
||||
else:
|
||||
return "⚠️ 暂无分析报告记录"
|
||||
|
||||
def __get_output_analysis_list(self, pageNum=None, pageSize=None, *args, **argss):
|
||||
"""获取面诊报告清单
|
||||
优化规则:只要API服务接口返回面诊报告清单,直接输出API返回的结果,
|
||||
无需汇总上下文中的面诊分析报告,以接口返回为准
|
||||
"""
|
||||
|
||||
def _get_analysis_export_url(request_id=None):
|
||||
if not request_id:
|
||||
return ""
|
||||
return ApiEnum.DETAIL_EXPORT_URL + request_id
|
||||
|
||||
# open_id 仅用于本地识别,不传给API - 参数已经在argss中,page方法会正确处理
|
||||
open_id = argss.pop('open_id', None)
|
||||
# if not open_id:
|
||||
# return "⚠️ 错误:缺少 open_id 参数"
|
||||
|
||||
# 获取总页数,然后循环获取所有页
|
||||
output_all = ""
|
||||
# 先获取第一页来获取总页数
|
||||
# page 方法在基类中已经处理过,我们需要兼容两种返回结果:
|
||||
# 1. 完整响应:{"success": true, "data": {"records": [...], "total": ...}}
|
||||
# 2. 已经提取好的数据:直接返回 data 对象或 records 列表
|
||||
response = self.page(pageNum or 1, pageSize or 30, *args, **argss)
|
||||
|
||||
if response is None:
|
||||
return "⚠️ 获取报告列表失败:response is None"
|
||||
|
||||
# 兼容处理:不同版本的基类返回不同格式
|
||||
if isinstance(response, list):
|
||||
# 基类直接返回了 records 列表,无法获取分页信息,直接使用
|
||||
records = response
|
||||
total = len(records)
|
||||
pages = 1
|
||||
elif isinstance(response, dict):
|
||||
# 完整响应格式
|
||||
if not response.get('success'):
|
||||
error_msg = response.get('errorMsg', '未知错误')
|
||||
return f"⚠️ 获取报告列表失败:{error_msg}"
|
||||
data = response.get('data', {})
|
||||
if not data or not isinstance(data, dict):
|
||||
return "⚠️ 获取报告列表失败:数据格式错误"
|
||||
total = data.get('total', 0)
|
||||
pages = data.get('pages', 1)
|
||||
records = data.get('records', [])
|
||||
else:
|
||||
return f"⚠️ 获取报告列表失败:response type={type(response)}"
|
||||
|
||||
if not records:
|
||||
return "⚠️ 暂无面诊分析报告记录"
|
||||
|
||||
output_all = f"📋 历史面诊分析报告清单(共 {total} 份)\n\n"
|
||||
output_all += "| 报告名称 | 分析时间 | 体质判断 | 点击查看 |\n"
|
||||
output_all += "|----------|----------|----------|----------|\n"
|
||||
|
||||
# 处理第一页
|
||||
for item in records:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
report_id = item.get('id', '')
|
||||
create_time = item.get('createTimeString', '未知时间')
|
||||
# 提取体质判断 - 优先从 healthAiResponse 获取,如果没有再从 faceAnalysisResponse 获取
|
||||
health_ai = item.get('healthAiResponse', {}) or {}
|
||||
if health_ai:
|
||||
health_assessment = health_ai.get('healthAssessment', {}) or {}
|
||||
subject = health_assessment.get('subject', '未知')
|
||||
else:
|
||||
face_ai = item.get('faceAnalysisResponse', {}) or {}
|
||||
health_assessment = face_ai.get('healthAssessment', {}) or {}
|
||||
subject = health_assessment.get('subject', '未知')
|
||||
report_name = f"面诊分析报告-{report_id}"
|
||||
report_url = _get_analysis_export_url(report_id)
|
||||
output_all += f"| {report_name} | {create_time} | {subject} | [🔗 查看报告]({report_url}) |\n"
|
||||
|
||||
# 处理剩余页
|
||||
for current_page in range(2, pages + 1):
|
||||
response = self.page(current_page, 30, *args, **argss)
|
||||
if not response or not isinstance(response, dict) or not response.get('success'):
|
||||
continue
|
||||
data = response.get('data', {})
|
||||
if not data or not isinstance(data, dict):
|
||||
continue
|
||||
records = data.get('records', [])
|
||||
for item in records:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
report_id = item.get('id', '')
|
||||
create_time = item.get('createTimeString', '未知时间')
|
||||
# 提取体质判断 - 优先从 healthAiResponse 获取,如果没有再从 faceAnalysisResponse 获取
|
||||
health_ai = item.get('healthAiResponse', {}) or {}
|
||||
if health_ai:
|
||||
health_assessment = health_ai.get('healthAssessment', {}) or {}
|
||||
subject = health_assessment.get('subject', '未知')
|
||||
else:
|
||||
face_ai = item.get('faceAnalysisResponse', {}) or {}
|
||||
health_assessment = face_ai.get('healthAssessment', {}) or {}
|
||||
subject = health_assessment.get('subject', '未知')
|
||||
report_name = f"面诊分析报告-{report_id}"
|
||||
report_url = _get_analysis_export_url(report_id)
|
||||
output_all += f"| {report_name} | {create_time} | {subject} | [🔗 查看报告]({report_url}) |\n"
|
||||
|
||||
output_all += "\n> 注:面诊分析结果仅供健康参考,不能替代专业医疗诊断。"
|
||||
return output_all
|
||||
|
||||
|
||||
skill = Skill()
|
||||
@ -1,127 +0,0 @@
|
||||
altgraph==0.17.5
|
||||
annotated-doc==0.0.4
|
||||
annotated-types==0.7.0
|
||||
anyio==4.12.1
|
||||
APScheduler==3.11.2
|
||||
astroid==3.1.0
|
||||
Authlib==1.6.6
|
||||
blinker==1.4
|
||||
cachetools==6.2.6
|
||||
certifi==2026.1.4
|
||||
cffi==2.0.0
|
||||
chardet==5.2.0
|
||||
charset-normalizer==3.4.4
|
||||
click==8.3.1
|
||||
coverage==7.13.2
|
||||
coze-workload-identity==0.1.7
|
||||
cozeloop==0.1.19
|
||||
cryptography==3.4.8
|
||||
Cython==3.2.4
|
||||
dbus-python==1.2.18
|
||||
dill==0.4.1
|
||||
distro==1.7.0
|
||||
distro-info==1.1+ubuntu0.2
|
||||
et_xmlfile==2.0.0
|
||||
fastapi==0.121.2
|
||||
gitdb==4.0.12
|
||||
gitignore_parser==0.1.13
|
||||
GitPython==3.1.45
|
||||
greenlet==3.3.1
|
||||
h11==0.16.0
|
||||
httpcore==1.0.9
|
||||
httplib2==0.20.2
|
||||
httpx==0.28.1
|
||||
httpx-ws==0.8.2
|
||||
idna==3.11
|
||||
importlib-metadata==4.6.4
|
||||
inflect==7.5.0
|
||||
iniconfig==2.3.0
|
||||
isort==5.13.2
|
||||
jeepney==0.7.1
|
||||
Jinja2==3.1.6
|
||||
jiter==0.12.0
|
||||
jsonpatch==1.33
|
||||
jsonpointer==3.0.0
|
||||
keyring==23.5.0
|
||||
langchain==1.0.3
|
||||
langchain-core==1.0.2
|
||||
langchain-openai==1.0.1
|
||||
langgraph==1.0.2
|
||||
langgraph-checkpoint==3.0.0
|
||||
langgraph-prebuilt==1.0.2
|
||||
langgraph-sdk==0.2.9
|
||||
langsmith==0.4.39
|
||||
launchpadlib==1.10.16
|
||||
lazr.restfulclient==0.14.4
|
||||
lazr.uri==1.0.6
|
||||
markdown-it-py==4.0.0
|
||||
MarkupSafe==3.0.3
|
||||
mccabe==0.7.0
|
||||
mdurl==0.1.2
|
||||
more-itertools==8.10.0
|
||||
numpy==2.4.1
|
||||
oauthlib==3.2.0
|
||||
openai==2.16.0
|
||||
openpyxl==3.1.5
|
||||
orjson==3.11.5
|
||||
ormsgpack==1.12.2
|
||||
packaging==25.0
|
||||
pandas==2.3.3
|
||||
platformdirs==4.5.1
|
||||
pluggy==1.6.0
|
||||
psutil==7.1.3
|
||||
psycopg2-binary==2.9.11
|
||||
pycparser==3.0
|
||||
pydantic==2.12.4
|
||||
pydantic_core==2.41.5
|
||||
pydash==8.0.6
|
||||
Pygments==2.19.2
|
||||
PyGObject==3.42.1
|
||||
pyinstaller==6.18.0
|
||||
pyinstaller-hooks-contrib==2026.0
|
||||
PyJWT==2.10.1
|
||||
pylint==3.1.0
|
||||
PyMySQL==1.1.2
|
||||
pyparsing==2.4.7
|
||||
pytest==9.0.1
|
||||
pytest-asyncio==1.3.0
|
||||
pytest-cov==7.0.0
|
||||
pytest-mock==3.15.1
|
||||
python-apt==2.4.0+ubuntu4.1
|
||||
python-dateutil==2.9.0.post0
|
||||
pytz==2025.2
|
||||
PyYAML==6.0.3
|
||||
regex==2026.1.15
|
||||
requests==2.32.5
|
||||
requests-toolbelt==1.0.0
|
||||
rich==14.2.0
|
||||
SecretStorage==3.3.1
|
||||
setuptools==80.9.0
|
||||
six==1.16.0
|
||||
smmap==5.0.2
|
||||
sniffio==1.3.1
|
||||
sqlacodegen==3.2.0
|
||||
SQLAlchemy==2.0.46
|
||||
starlette==0.49.3
|
||||
supervisor==4.2.1
|
||||
tenacity==9.1.2
|
||||
tiktoken==0.12.0
|
||||
tomlkit==0.14.0
|
||||
tqdm==4.67.1
|
||||
typeguard==4.4.4
|
||||
typing-inspection==0.4.2
|
||||
typing_extensions==4.15.0
|
||||
tzdata==2025.3
|
||||
tzlocal==5.3.1
|
||||
unattended-upgrades==0.1
|
||||
urllib3==2.6.3
|
||||
uvicorn==0.38.0
|
||||
wadllib==1.3.6
|
||||
watchdog==6.0.0
|
||||
websockets==15.0.1
|
||||
wheel==0.45.1
|
||||
wsproto==1.3.2
|
||||
xlrd==2.0.2
|
||||
xxhash==3.6.0
|
||||
zipp==1.0.0
|
||||
zstandard==0.25.0
|
||||
@ -1,8 +0,0 @@
|
||||
from .util import RequestUtil, CommonUtil, DatetimeUtil
|
||||
from .base import *
|
||||
|
||||
__all__ = [
|
||||
'RequestUtil',
|
||||
'CommonUtil',
|
||||
'BaseUtil'
|
||||
]
|
||||
@ -1,96 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from .config import ApiEnum
|
||||
|
||||
from .base import BaseApiService
|
||||
from .util import RequestUtil, CommonUtil
|
||||
|
||||
|
||||
class ApiService(BaseApiService):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def get_download_url(self, tosKey, expireSeconds=3600):
|
||||
return RequestUtil.http_post(
|
||||
ApiEnum.GET_DOWNLOAD_URL__URL,
|
||||
params={
|
||||
"tosKey": tosKey,
|
||||
"expireSeconds": expireSeconds * 24
|
||||
}
|
||||
)
|
||||
|
||||
def page(self, url, pageNum=None, pageSize=None, *args, **argss):
|
||||
data = args[0] if len(args) > 0 else argss.get('data') if argss.get('data') is not None else {}
|
||||
if pageNum is None:
|
||||
pageNum = 1
|
||||
if pageSize is None:
|
||||
pageSize = ApiEnum.DEFAULT__PAGE_SIZE
|
||||
paramsPage = {
|
||||
'pageNum': int(pageNum),
|
||||
'pageSize': int(pageSize)
|
||||
}
|
||||
data.update({
|
||||
"page": paramsPage
|
||||
})
|
||||
if not CommonUtil.is_empty(data):
|
||||
if (len(args) == 0):
|
||||
argss.setdefault("data", data)
|
||||
response = RequestUtil.http_post(
|
||||
url,
|
||||
*args, **argss
|
||||
)
|
||||
return response
|
||||
|
||||
def list(self, url=None, *args, **argss):
|
||||
if url is not None:
|
||||
argss["url"] = url
|
||||
return self.page(1, ApiEnum.DEFAULT__PAGE_SIZE_MAX, *args, **argss)
|
||||
|
||||
def add(self, url=None, *args, **argss):
|
||||
response = RequestUtil.http_post(
|
||||
url,
|
||||
*args, **argss
|
||||
)
|
||||
return response
|
||||
|
||||
def edit(self, url=None, *args, **argss):
|
||||
response = RequestUtil.http_post(
|
||||
url,
|
||||
*args, **argss
|
||||
)
|
||||
return response
|
||||
|
||||
def delete(self, url=None, *args, **argss):
|
||||
response = RequestUtil.http_post(
|
||||
url,
|
||||
*args, **argss
|
||||
)
|
||||
return response
|
||||
|
||||
def http_post(self, url=None, *args, **argss):
|
||||
return RequestUtil.http_post(
|
||||
url,
|
||||
*args, **argss
|
||||
)
|
||||
|
||||
def http_put(self, url=None, *args, **argss):
|
||||
return RequestUtil.http_put(
|
||||
url,
|
||||
*args, **argss
|
||||
)
|
||||
|
||||
def http_get(self, url=None, *args, **argss):
|
||||
return RequestUtil.http_get(
|
||||
url,
|
||||
*args, **argss
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
def http_delete(self, url=None, *args, **argss):
|
||||
return RequestUtil.http_delete(
|
||||
url,
|
||||
*args, **argss
|
||||
)
|
||||
return response
|
||||
@ -1,33 +0,0 @@
|
||||
class BaseUtil:
|
||||
pass
|
||||
|
||||
|
||||
class BaseMixin:
|
||||
pass
|
||||
|
||||
|
||||
class BaseDao:
|
||||
pass
|
||||
|
||||
|
||||
class BaseService:
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
|
||||
class BaseApiService(BaseService):
|
||||
INSTANCE = None
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
if cls.INSTANCE is None:
|
||||
cls.INSTANCE = cls()
|
||||
return cls.INSTANCE
|
||||
|
||||
|
||||
class BaseSkill:
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@ -1,7 +0,0 @@
|
||||
ApiEnum:
|
||||
base-url-open-api: "http://192.168.1.234:9601/smyx-open-api"
|
||||
base-url-open-h5: "http://192.168.1.234:4100"
|
||||
base-url-health: "http://192.168.1.234:8080/jeecg-boot"
|
||||
|
||||
ConstantEnum:
|
||||
is-debug: true
|
||||
@ -1,7 +0,0 @@
|
||||
ApiEnum:
|
||||
base-url-open-api: "https://livemonitortest.lifeemergence.com/smyx-open-api"
|
||||
base-url-open-h5: "http://livemonitortest.lifeemergence.com"
|
||||
base-url-health: "https://healthtest.lifeemergence.com/jeecg-boot"
|
||||
|
||||
ConstantEnum:
|
||||
is-debug: true
|
||||
@ -1,240 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
from enum import Enum
|
||||
from typing import Dict
|
||||
import inspect
|
||||
|
||||
import yaml
|
||||
import platform
|
||||
|
||||
|
||||
class YamlUtil:
|
||||
|
||||
@staticmethod
|
||||
def load(path, config: Dict = {}) -> Dict:
|
||||
try:
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(config, f, default_flow_style=False, allow_unicode=True)
|
||||
return config
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
config = yaml.safe_load(f) or {}
|
||||
for key, value in config.items():
|
||||
if key not in config:
|
||||
config[key] = value
|
||||
return config
|
||||
except:
|
||||
pass
|
||||
return config
|
||||
|
||||
@staticmethod
|
||||
def save(path, config: Dict) -> Dict:
|
||||
try:
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(config, f, default_flow_style=False, allow_unicode=True)
|
||||
except:
|
||||
pass
|
||||
return config
|
||||
|
||||
|
||||
class BaseEnum:
|
||||
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
super().__init_subclass__(**kwargs)
|
||||
clsModule = cls.__module__
|
||||
cls_path = inspect.getfile(cls)
|
||||
clsFullName = f"{cls.__module__}.{cls.__name__}"
|
||||
cls_dirpath = os.path.dirname(cls_path) # .../src
|
||||
clsModulePath = clsModule.replace(".", "\\")
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__)) # .../src
|
||||
config_path = os.path.join(cls_dirpath, "config.yaml")
|
||||
config = YamlUtil.load(config_path)
|
||||
cls.init(config)
|
||||
env = config.get("env")
|
||||
if env:
|
||||
env_config_path = os.path.join(cls_dirpath, f"config-{env}.yaml")
|
||||
env_config = YamlUtil.load(env_config_path)
|
||||
cls.init(env_config)
|
||||
|
||||
@classmethod
|
||||
def init(cls, config=None):
|
||||
clsName = cls.__name__
|
||||
clsConfig = config and config.get(clsName)
|
||||
if clsConfig:
|
||||
for config_key, config_value in clsConfig.items():
|
||||
new_config_key = config_key = config_key.upper().replace("-", "_")
|
||||
if hasattr(cls, new_config_key):
|
||||
setattr(cls, new_config_key, config_value)
|
||||
|
||||
|
||||
class ApiEnum(BaseEnum):
|
||||
API_KEY = None
|
||||
|
||||
API_SECRET_KEY = None
|
||||
|
||||
DATABASE_URL = ""
|
||||
|
||||
BASE_URL_OPEN_API = ""
|
||||
|
||||
BASE_URL_OPEN_H5 = ""
|
||||
|
||||
BASE_URL_HEALTH = ""
|
||||
|
||||
OPEN_TOKEN = ""
|
||||
|
||||
TOKEN = ""
|
||||
|
||||
DEFAULT__REQUEST_TIMEOUT = 120
|
||||
|
||||
DEFAULT__PAGE_SIZE = 5
|
||||
|
||||
DEFAULT__PAGE_SIZE_MAX = 65536
|
||||
|
||||
GET_DOWNLOAD_URL__URL = BASE_URL_OPEN_API + "/api/tos/get-download-url"
|
||||
|
||||
|
||||
class ConstantEnum(BaseEnum):
|
||||
class SourceEnum(Enum):
|
||||
ARK_CLAW = "ARK_CLAW"
|
||||
JVS_CLAW = "JVS_CLAW"
|
||||
LIGHT_CLAW = "LIGHT_CLAW"
|
||||
WUHONG = "WUHONG"
|
||||
COZE = "COZE"
|
||||
SKILL_HUB = "SKILL_HUB"
|
||||
CLAW_HUB = "CLAW_HUB"
|
||||
FEISHU = "FEISHU"
|
||||
DINGTALK = "DINGTALK"
|
||||
WEIXIN = "WEIXIN"
|
||||
YUANBAO = "YUANBAO"
|
||||
WECOM = "WECOM"
|
||||
QQBOT = "QQBOT"
|
||||
|
||||
APP__ID = ""
|
||||
|
||||
APP__SOURCE = SourceEnum.CLAW_HUB.value
|
||||
|
||||
IS_DEBUG = False
|
||||
|
||||
CURRENT__OPEN_ID = ""
|
||||
|
||||
CURRENT__USER_NAME = ""
|
||||
|
||||
CURRENT__TENTANT_CODE = ""
|
||||
|
||||
FEISHU_APP__ID = ""
|
||||
|
||||
FEISHU_APP__SECRET = ""
|
||||
|
||||
FEISHU_APP__RECEIVE_ID = ""
|
||||
|
||||
DEFAULT__SCENE_CODE = ""
|
||||
|
||||
DEFAULT__SKILL_HUB_NAME = APP__SOURCE
|
||||
|
||||
DEFAULT__SKILL_PLATFORM_NAME = ""
|
||||
|
||||
DEFAULT__OUTPUT_LEVEL = "json"
|
||||
|
||||
SUPPORTED_FORMATS = ["mp4", "avi", "mov"]
|
||||
|
||||
MAX_FILE_SIZE_MB = 10
|
||||
|
||||
@staticmethod
|
||||
def is_debug():
|
||||
return platform.system() != 'Linux' and ConstantEnum.IS_DEBUG
|
||||
|
||||
@classmethod
|
||||
def init(cls, config=None):
|
||||
super().init(config)
|
||||
openclaw_sender_open_id = os.environ.get("OPENCLAW_SENDER_OPEN_ID")
|
||||
openclaw_sender_username = os.environ.get("OPENCLAW_SENDER_USERNAME")
|
||||
feishu_open_id = os.environ.get("FEISHU_OPEN_ID")
|
||||
if openclaw_sender_open_id:
|
||||
cls.CURRENT__OPEN_ID = openclaw_sender_open_id
|
||||
if openclaw_sender_username:
|
||||
cls.CURRENT__USER_NAME = openclaw_sender_username
|
||||
if feishu_open_id:
|
||||
cls.FEISHU_APP__RECEIVE_ID = feishu_open_id
|
||||
|
||||
class SceneCodeEnum(Enum):
|
||||
# 开放 #
|
||||
OPEN_HEALTH_AI_ANALYSIS = "OPEN_HEALTH_AI_ANALYSIS"
|
||||
OPEN_PERSON_RISK_ANALYSIS = "OPEN_PERSON_RISK_ANALYSIS"
|
||||
# 智眸 #
|
||||
PUBLIC_AREA_AI_ANALYSIS = "PUBLIC_AREA_AI_ANALYSIS"
|
||||
PERSONNEL_LEAVE_POST_MONITORING = "PERSONNEL_LEAVE_POST_MONITORING"
|
||||
CRAWL_MONITOR = "CRAWL_MONITOR"
|
||||
# 陪你安 #
|
||||
PEI_NI_AN_DEFAULT = "PEI_NI_AN_DEFAULT"
|
||||
PET_ANALYSIS = "PET_ANALYSIS"
|
||||
CRAWL_ANALYSIS = "CRAWL_ANALYSIS"
|
||||
AQUARIUM_ANALYSIS = "AQUARIUM_ANALYSIS"
|
||||
PSYCHOLOGY_ANALYSIS = "PSYCHOLOGY_ANALYSIS"
|
||||
AUTISM_ANALYSIS = "AUTISM_ANALYSIS"
|
||||
DIET_ANALYSIS = "DIET_ANALYSIS"
|
||||
DRIVE_ANALYSIS = "DRIVE_ANALYSIS"
|
||||
SPORT_ANALYSIS = "SPORT_ANALYSIS"
|
||||
EMOTION_ANALYSIS = "EMOTION_ANALYSIS"
|
||||
STUDY_ANALYSIS = "STUDY_ANALYSIS"
|
||||
INFANT_SAFETY_MONITORING_ANALYSIS = "INFANT_SAFETY_MONITORING"
|
||||
PHONE_USAGE_MONITORING_ANALYSIS = "PHONE_USAGE_MONITORING"
|
||||
INCONTINENCE_ALERT_ANALYSIS = "INCONTINENCE_ALERT"
|
||||
RESPIRATORY_SYMPTOM_RECOGNITION_ANALYSIS = "RESPIRATORY_SYMPTOM_RECOGNITION"
|
||||
ELECTRIC_VEHICLE_DETECTION_ANALYSIS = "ELECTRIC_VEHICLE_DETECTION"
|
||||
SMOKING_DETECTION_ANALYSIS = "SMOKING_DETECTION"
|
||||
PET_DETECTION_FEEDER_ANALYSIS = "PET_DETECTION_FEEDER"
|
||||
PET_HEALTH_MONITORING_ANALYSIS = "PET_HEALTH_MONITORING"
|
||||
STROKE_RISK_SCREENING_ANALYSIS = "STROKE_RISK_SCREENING"
|
||||
HUMAN_DETECTION_ANALYSIS = "HUMAN_DETECTION"
|
||||
STRANGER_RECOGNITION_ANALYSIS = "STRANGER_RECOGNITION"
|
||||
FOCUS_ANALYSIS_ANALYSIS = "FOCUS_ANALYSIS"
|
||||
HUMAN_POSTURE_RECOGNITION_ANALYSIS = "HUMAN_POSTURE_RECOGNITION"
|
||||
HUMAN_EMOTION_RECOGNITION_ANALYSIS = "HUMAN_EMOTION_RECOGNITION"
|
||||
FIRE_SMOKE_DETECTION_ANALYSIS = "FIRE_SMOKE_DETECTION"
|
||||
BASIC_OBJECT_DETECTION_ANALYSIS = "BASIC_OBJECT_DETECTION"
|
||||
CHILD_DANGEROUS_BEHAVIOR_RECOGNITION_ANALYSIS = "CHILD_DANGEROUS_BEHAVIOR_RECOGNITION"
|
||||
PET_RESTRICTED_AREA_WARNING_ANALYSIS = "PET_RESTRICTED_AREA_WARNING"
|
||||
SLEEP_QUALITY_ANALYSIS_ANALYSIS = "SLEEP_QUALITY_ANALYSIS"
|
||||
PET_DETECTION_ANALYSIS = "PET_DETECTION"
|
||||
PSYCHOLOGICAL_STRESS_ASSESSMENT_ANALYSIS = "PSYCHOLOGICAL_STRESS_ASSESSMENT"
|
||||
VISUAL_QA_ANALYSIS = "VISUAL_QA"
|
||||
PET_BODY_HEALTH_ANALYSIS = "PET_BODY_HEALTH_ANALYSIS"
|
||||
PET_BEHAVIOR_DETECTION_ANALYSIS = "PET_BEHAVIOR_DETECTION"
|
||||
INFANT_SUFFOCATION_WARNING_ANALYSIS = "INFANT_SUFFOCATION_WARNING"
|
||||
STRANGER_APPROACH_WARNING_ANALYSIS = "STRANGER_APPROACH_WARNING"
|
||||
IMAGE_QUALITY_DETECTION_ANALYSIS = "IMAGE_QUALITY_DETECTION"
|
||||
CHILD_EMOTION_RECOGNITION_ANALYSIS = "CHILD_EMOTION_RECOGNITION"
|
||||
OUTDOOR_MONITORING_ANALYSIS = "OUTDOOR_MONITORING"
|
||||
FALL_DETECTION_IMAGE_ANALYSIS = "FALL_DETECTION_IMAGE"
|
||||
CUSTOM_TIMELAPSE_ANALYSIS = "CUSTOM_TIMELAPSE"
|
||||
CONTACTLESS_VITAL_SIGNS_MONITORING_ANALYSIS = "CONTACTLESS_VITAL_SIGNS_MONITORING"
|
||||
VIDEO_SEARCH_ANALYSIS = "VIDEO_SEARCH"
|
||||
FAMILIAR_PERSON_RECOGNITION_ANALYSIS = "FAMILIAR_PERSON_RECOGNITION"
|
||||
TCM_CONSTITUTION_RECOGNITION_ANALYSIS = "TCM_CONSTITUTION_RECOGNITION"
|
||||
CONTACTLESS_HEALTH_RISK_DETECTION_ANALYSIS = "CONTACTLESS_HEALTH_RISK_DETECTION"
|
||||
UNACCOMPANIED_MONITORING_ANALYSIS = "UNACCOMPANIED_MONITORING"
|
||||
ELDERLY_FALL_DETECTION_ANALYSIS = "ELDERLY_FALL_DETECTION"
|
||||
PARKINSON_EPILEPSY_BEHAVIOR_RECOGNITION_ANALYSIS = "PARKINSON_EPILEPSY_BEHAVIOR_RECOGNITION"
|
||||
PET_BREED_INDIVIDUAL_RECOGNITION_ANALYSIS = "PET_BREED_INDIVIDUAL_RECOGNITION"
|
||||
ELDERLY_BED_EXIT_WANDERING_MONITORING_ANALYSIS = "ELDERLY_BED_EXIT_WANDERING_MONITORING"
|
||||
ARRHYTHMIA_EARLY_WARNING_ANALYSIS = "ARRHYTHMIA_EARLY_WARNING"
|
||||
FIRE_DETECTION_ANALYSIS = "FIRE_DETECTION"
|
||||
VISUAL_SUMMARY_ANALYSIS = "VISUAL_SUMMARY"
|
||||
PACKAGE_DETECTION_ANALYSIS = "PACKAGE_DETECTION"
|
||||
INFANT_BLANKET_KICK_MONITORING_ANALYSIS = "INFANT_BLANKET_KICK_MONITORING"
|
||||
PET_CALMING_TRIGGER_ANALYSIS = "PET_CALMING_TRIGGER"
|
||||
CAT_FACE_RECOGNITION_ANALYSIS = "CAT_FACE_RECOGNITION"
|
||||
INFANT_SLEEP_MONITORING_ANALYSIS = "INFANT_SLEEP_MONITORING"
|
||||
VIRTUAL_FENCE_INTRUSION_WARNING_ANALYSIS = "VIRTUAL_FENCE_INTRUSION_WARNING"
|
||||
FALL_DETECTION_VIDEO_ANALYSIS = "FALL_DETECTION_VIDEO"
|
||||
INFANT_CRY_ANALYSIS = "INFANT_CRY_ANALYSIS"
|
||||
PET_VOCAL_EMOTION_ANALYSIS = "PET_VOCAL_EMOTION_ANALYSIS"
|
||||
BIRD_RECOGNITION_ANALYSIS = "BIRD_RECOGNITION"
|
||||
FRAUD_CALL_IDENTIFICATION = "FRAUD_CALL_IDENTIFICATION"
|
||||
PLANT_SPECIES_RECOGNITION = "PLANT_SPECIES_RECOGNITION"
|
||||
PLANT_GROWTH_STAGE_RECOGNITION = "PLANT_GROWTH_STAGE_RECOGNITION"
|
||||
PLANT_DISEASE_RECOGNITION = "PLANT_DISEASE_RECOGNITION"
|
||||
PLANT_NUTRITION_DIAGNOSIS = "PLANT_NUTRITION_DIAGNOSIS"
|
||||
PLANT_WILTING_MONITORING = "PLANT_WILTING_MONITORING"
|
||||
@ -1,18 +0,0 @@
|
||||
env: prod
|
||||
|
||||
ApiEnum:
|
||||
api-key:
|
||||
api-secret-key:
|
||||
database-url:
|
||||
base-url-open-api: "https://open.lifeemergence.com/smyx-open-api"
|
||||
base-url-open-h5: "http://livemonitor.lifeemergence.com"
|
||||
base-url-health: "https://lifeemergence.com/jeecg-boot"
|
||||
|
||||
ConstantEnum:
|
||||
is-debug: false
|
||||
app--id: x1a3s4nwy1s2r4se
|
||||
current--tentant-code: "PEI_NI_AN"
|
||||
feishu-app--id: cli_a93d769369badcb1
|
||||
feishu-app--secret:
|
||||
default--skill-platform-name: ARK_CLAW
|
||||
# default--scene-code: PEI_NI_AN_DEFAULT
|
||||
@ -1,348 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
本地化轻量级数据库封装
|
||||
使用SQLite + SQLAlchemy ORM
|
||||
支持基础CRUD操作,通过继承BaseDao快速实现各表的Dao层
|
||||
"""
|
||||
import datetime
|
||||
import sys
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional, Type, TypeVar
|
||||
from sqlalchemy import create_engine, Column, Integer, String, DateTime, func, Select, Table, MetaData, select, or_
|
||||
from sqlalchemy.orm import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker, Session
|
||||
from sqlalchemy.sql.expression import text
|
||||
|
||||
from skills.smyx_common.scripts.config import ConstantEnum, ApiEnum
|
||||
|
||||
from skills.smyx_common.scripts.util import StringUtil, DatetimeUtil, FileUtil
|
||||
|
||||
from skills.smyx_common.scripts.base import BaseMixin, BaseDao
|
||||
|
||||
# 基础模型类
|
||||
Base = declarative_base()
|
||||
|
||||
# 泛型类型,用于返回对应模型实例
|
||||
T = TypeVar('T', bound=Base)
|
||||
|
||||
meta = MetaData()
|
||||
|
||||
DATABASE_URL = ApiEnum.DATABASE_URL
|
||||
|
||||
|
||||
class BaseModelMixin(BaseMixin):
|
||||
|
||||
@classmethod
|
||||
def load(cls, source: dict):
|
||||
"""
|
||||
获取源枚举
|
||||
:param source: 源
|
||||
:return: User
|
||||
"""
|
||||
column_names = cls.__table__.columns.keys()
|
||||
user_dict = {k: source.get(StringUtil.snake_to_camel(k)) for k in column_names}
|
||||
user_dict["create_time"] = DatetimeUtil.parse(user_dict["create_time"])
|
||||
user_dict["update_time"] = DatetimeUtil.parse(user_dict["update_time"])
|
||||
model = cls(**user_dict)
|
||||
return model
|
||||
|
||||
|
||||
class Dao(BaseDao):
|
||||
"""
|
||||
基础Dao类,提供通用的CRUD操作
|
||||
子类只需配置__model__和__tablename__即可使用
|
||||
"""
|
||||
__model__: Type[T] = None # 对应的模型类,子类必须配置
|
||||
__tablename__: str = None # 表名,子类必须配置
|
||||
|
||||
def get_db_path(self, db_path):
|
||||
import os
|
||||
|
||||
cwd = os.getcwd()
|
||||
workspace = os.path.dirname(cwd)
|
||||
workspace = os.path.dirname(workspace)
|
||||
workspace = os.environ.get('OPENCLAW_WORKSPACE', workspace)
|
||||
parent_dir = os.path.join(workspace, "data")
|
||||
FileUtil.mkdir(parent_dir)
|
||||
db_path = os.path.join(parent_dir, db_path)
|
||||
|
||||
return db_path
|
||||
|
||||
def __init__(self, db_path: str = None):
|
||||
"""
|
||||
初始化Dao
|
||||
:param db_path: SQLite数据库文件路径
|
||||
"""
|
||||
|
||||
if not db_path:
|
||||
db_path = "smyx-common-claw.db"
|
||||
db_path = self.get_db_path(db_path)
|
||||
|
||||
self.engine = create_engine(f"sqlite:///{db_path}", echo=False)
|
||||
|
||||
# 创建会话工厂
|
||||
self.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=self.engine)
|
||||
# 初始化表结构
|
||||
self._create_tables()
|
||||
self._alter_tables()
|
||||
|
||||
def _create_tables(self) -> None:
|
||||
"""创建所有表结构"""
|
||||
Base.metadata.create_all(bind=self.engine)
|
||||
|
||||
def _alter_tables(self) -> None:
|
||||
"""创建所有表结构"""
|
||||
sql_statement = "ALTER TABLE sys_user ADD COLUMN source_id INT;"
|
||||
|
||||
# 3. 执行语句
|
||||
try:
|
||||
with self.engine.connect() as connection:
|
||||
connection.execute(text(sql_statement))
|
||||
connection.commit() # 对于数据定义语言(DDL),需要显式提交
|
||||
except Exception as e:
|
||||
connection.rollback()
|
||||
if len(e.args) and "duplicate column name" in e.args[0]:
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
|
||||
def get_session(self) -> Session:
|
||||
"""获取数据库会话"""
|
||||
return self.SessionLocal()
|
||||
|
||||
def save(self, model) -> T:
|
||||
"""
|
||||
创建新记录
|
||||
:param kwargs: 字段键值对
|
||||
:return: 创建的模型实例
|
||||
"""
|
||||
|
||||
try:
|
||||
return self.add(
|
||||
model
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return self.update(
|
||||
model
|
||||
)
|
||||
|
||||
def add(self, model) -> T:
|
||||
"""
|
||||
创建新记录
|
||||
:param kwargs: 字段键值对
|
||||
:return: 创建的模型实例
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
session.add(model)
|
||||
session.commit()
|
||||
session.refresh(model)
|
||||
return model
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def create(self, **kwargs) -> T:
|
||||
"""
|
||||
创建新记录
|
||||
:param kwargs: 字段键值对
|
||||
:return: 创建的模型实例
|
||||
"""
|
||||
instance = self.__model__(**kwargs)
|
||||
return self.add(instance)
|
||||
|
||||
def get_by_id(self, record_id: int) -> Optional[T]:
|
||||
"""
|
||||
根据ID查询记录
|
||||
:param record_id: 记录ID
|
||||
:return: 模型实例或None
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
return session.query(self.__model__).filter(self.__model__.id == record_id).first()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def get_by_username(self, username: str) -> Optional[T]:
|
||||
"""
|
||||
根据ID查询记录
|
||||
:param record_id: 记录ID
|
||||
:return: 模型实例或None
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
or_(
|
||||
self.__model__.del_flag == 0,
|
||||
self.__model__.del_flag.is_(None) # 关键:使用 .is_(None) 来判断 SQL 的 NULL
|
||||
)
|
||||
return session.query(self.__model__).filter(self.__model__.username == username,
|
||||
or_(
|
||||
self.__model__.del_flag == 0,
|
||||
self.__model__.del_flag.is_(None)
|
||||
# 关键:使用 .is_(None) 来判断 SQL 的 NULL
|
||||
)).first()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def list(self, filters: Optional[Dict[str, Any]] = None, limit: Optional[int] = None,
|
||||
offset: Optional[int] = None) -> List[T]:
|
||||
"""
|
||||
查询记录列表
|
||||
:param filters: 过滤条件字典,如{"name": "张三", "age": 18}
|
||||
:param limit: 最大返回数量
|
||||
:param offset: 偏移量
|
||||
:return: 模型实例列表
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
query = session.query(self.__model__)
|
||||
# .where(self.__model__.id != 2, self.__model__.id == 1))
|
||||
|
||||
if filters:
|
||||
for key, value in filters.items():
|
||||
query = query.filter(getattr(self.__model__, key) == value)
|
||||
|
||||
if offset:
|
||||
query = query.offset(offset)
|
||||
if limit:
|
||||
query = query.limit(limit)
|
||||
|
||||
return query.all()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def update(self, model) -> Optional[T]:
|
||||
"""
|
||||
更新记录
|
||||
:param record_id: 记录ID
|
||||
:param kwargs: 要更新的字段键值对
|
||||
:return: 更新后的模型实例或None
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
instance = session.query(self.__model__).filter(self.__model__.id == model.id).first()
|
||||
if not instance:
|
||||
return None
|
||||
|
||||
column_names = self.__model__.__table__.columns.keys()
|
||||
|
||||
for key in column_names:
|
||||
value = getattr(model, key)
|
||||
setattr(instance, key, value)
|
||||
|
||||
session.commit()
|
||||
session.refresh(instance)
|
||||
return instance
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def modify(self, record_id: int, **kwargs) -> Optional[T]:
|
||||
"""
|
||||
更新记录
|
||||
:param record_id: 记录ID
|
||||
:param kwargs: 要更新的字段键值对
|
||||
:return: 更新后的模型实例或None
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
instance = session.query(self.__model__).filter(self.__model__.id == record_id).first()
|
||||
if not instance:
|
||||
return None
|
||||
|
||||
for key, value in kwargs.items():
|
||||
setattr(instance, key, value)
|
||||
|
||||
session.commit()
|
||||
session.refresh(instance)
|
||||
return instance
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def update_by_username(self, username: str, **kwargs) -> Optional[T]:
|
||||
"""
|
||||
更新记录
|
||||
:param username: 记录ID
|
||||
:param kwargs: 要更新的字段键值对
|
||||
:return: 更新后的模型实例或None
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
instance = session.query(self.__model__).filter(self.__model__.username == username).first()
|
||||
if not instance:
|
||||
return None
|
||||
|
||||
for key, value in kwargs.items():
|
||||
setattr(instance, key, value)
|
||||
|
||||
session.commit()
|
||||
session.refresh(instance)
|
||||
return instance
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def delete(self, record_id: int) -> bool:
|
||||
"""
|
||||
删除记录
|
||||
:param record_id: 记录ID
|
||||
:return: 删除成功返回True,失败返回False
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
instance = session.query(self.__model__).filter(self.__model__.id == record_id).first()
|
||||
if not instance:
|
||||
return False
|
||||
|
||||
session.delete(instance)
|
||||
session.commit()
|
||||
return True
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def count(self, filters: Optional[Dict[str, Any]] = None) -> int:
|
||||
"""
|
||||
统计记录数量
|
||||
:param filters: 过滤条件字典
|
||||
:return: 记录数量
|
||||
"""
|
||||
session = self.get_session()
|
||||
try:
|
||||
query = session.query(func.count(self.__model__.id))
|
||||
|
||||
if filters:
|
||||
for key, value in filters.items():
|
||||
query = query.filter(getattr(self.__model__, key) == value)
|
||||
|
||||
return query.scalar()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
class User(Base, BaseModelMixin):
|
||||
"""用户模型"""
|
||||
__tablename__ = "sys_user"
|
||||
|
||||
id = Column(String(32), primary_key=True, index=True)
|
||||
source_id = Column(String(32), comment="源头id")
|
||||
username = Column(String(100), unique=True, index=True, nullable=False, comment="用户名")
|
||||
email = Column(String(45), unique=True, index=True, comment="邮箱")
|
||||
birthday = Column(DateTime, unique=True, index=True, comment="邮箱")
|
||||
sex = Column(Integer, comment="性别")
|
||||
age = Column(Integer, comment="年龄")
|
||||
token = Column(String(500), comment="token")
|
||||
open_token = Column(String(1000), comment="开放token")
|
||||
source = Column(String(50), comment="token")
|
||||
del_flag = Column(Integer, comment="是否删除", default=0)
|
||||
create_time = Column(DateTime, default=func.now(), comment="创建时间")
|
||||
update_time = Column(DateTime, default=func.now(), onupdate=func.now(), comment="更新时间")
|
||||
|
||||
SourceEnum = ConstantEnum.SourceEnum
|
||||
|
||||
|
||||
class UserDao(Dao):
|
||||
"""用户Dao,继承BaseDao即可拥有所有基础CRUD功能"""
|
||||
__model__ = User
|
||||
__tablename__ = "users"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
@ -1,82 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
from .config import ApiEnum as ApiEnumBase, ConstantEnum
|
||||
from .base import BaseSkill
|
||||
from skills.smyx_common.scripts.api_service import ApiService as ApiServiceBase
|
||||
from .util import FileUtil
|
||||
|
||||
from .api_service import ApiService
|
||||
|
||||
class Skill(BaseSkill, ApiService):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
class AgentSkill(BaseSkill, ApiService):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def ai_chat(self, prompt: str, session_id: str = None, timeout: int = 120):
|
||||
"""
|
||||
通过 subprocess 调用 openclaw agent 命令
|
||||
|
||||
Args:
|
||||
prompt: 分析提示
|
||||
session_id: 会话 ID(可选)
|
||||
timeout: 超时时间(秒)
|
||||
|
||||
Returns:
|
||||
分析结果或会话 ID
|
||||
"""
|
||||
import uuid
|
||||
|
||||
# 生成唯一会话 ID
|
||||
if not session_id:
|
||||
entry_script = sys.argv[0]
|
||||
abs_entry_script = os.path.abspath(entry_script)
|
||||
main_name = FileUtil.get_name(abs_entry_script)
|
||||
session_id = f"{main_name}--{uuid.uuid4()}"
|
||||
|
||||
ConstantEnum.is_debug() and print(f"🤖 正在调用 openclaw agent (会话:{session_id})..., prompt:{prompt}")
|
||||
|
||||
# 构建命令
|
||||
cmd = [
|
||||
"openclaw",
|
||||
"agent",
|
||||
"-m", str(prompt),
|
||||
"--session-id", session_id,
|
||||
"--thinking", "minimal",
|
||||
"--timeout", str(timeout)
|
||||
]
|
||||
|
||||
ConstantEnum.is_debug() and print(f"🤖 正在调用 openclaw agent 执行命令{' '.join(cmd)}")
|
||||
|
||||
try:
|
||||
# 执行命令
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout + 10
|
||||
)
|
||||
|
||||
if result.stderr:
|
||||
ConstantEnum.is_debug() and print(f"🤖 正在调用 openclaw agent 执行错误:{result.stderr}")
|
||||
return
|
||||
|
||||
output = result.stdout
|
||||
ConstantEnum.is_debug() and print(f"🤖 正在调用 openclaw agent 执行成功:{output}")
|
||||
|
||||
return output
|
||||
|
||||
except subprocess.TimeoutExpired as e:
|
||||
ConstantEnum.is_debug() and print(f"🤖 正在调用 openclaw agent 超时({timeout}秒),任务可能仍在后台运行:{e}")
|
||||
except Exception as e:
|
||||
ConstantEnum.is_debug() and print(f"🤖 正在调用 openclaw agent 执行错误:{e}")
|
||||
|
||||
|
||||
skill = Skill()
|
||||
@ -1,413 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
import traceback
|
||||
|
||||
import requests
|
||||
from .config import ApiEnum, ConstantEnum, sys, YamlUtil
|
||||
|
||||
from .base import BaseUtil
|
||||
import time
|
||||
import logging
|
||||
from typing import Any, Callable, Optional, TypeVar, Dict
|
||||
import pydash as _
|
||||
|
||||
if ConstantEnum.is_debug():
|
||||
import http.client
|
||||
|
||||
# 【关键代码】开启调试模式
|
||||
http.client.HTTPConnection.debuglevel = 1
|
||||
# 可选:如果你希望日志更整洁,可以配合 logging 模块(否则打印会比较乱)
|
||||
import logging
|
||||
|
||||
logging.basicConfig()
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
requests_log = logging.getLogger("urllib3")
|
||||
requests_log.setLevel(logging.DEBUG)
|
||||
requests_log.propagate = True
|
||||
|
||||
|
||||
class StringUtil(BaseUtil):
|
||||
|
||||
@staticmethod
|
||||
def camel_to_snake(name):
|
||||
import re
|
||||
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
|
||||
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
||||
|
||||
@staticmethod
|
||||
def snake_to_pascal(name):
|
||||
import re
|
||||
name = re.sub(r'^([a-z])', lambda m: m.group(1).upper(), name)
|
||||
return re.sub(r'_([a-z])', lambda m: m.group(1).upper(), name)
|
||||
|
||||
@staticmethod
|
||||
def snake_to_camel(name):
|
||||
import re
|
||||
# 逻辑:匹配 '_[a-z]' (下划线+小写字母),将其替换为对应的大写字母(去掉下划线)
|
||||
return re.sub(r'_([a-z])', lambda m: m.group(1).upper(), name)
|
||||
|
||||
|
||||
class FileUtil(BaseUtil):
|
||||
|
||||
@staticmethod
|
||||
def get_fullname(path):
|
||||
try:
|
||||
return os.path.basename(path)
|
||||
except Exception as e:
|
||||
CommonUtil.trace_exception_stack(e)
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def get_name(path):
|
||||
try:
|
||||
return os.path.splitext(os.path.basename(path))[0]
|
||||
except Exception as e:
|
||||
CommonUtil.trace_exception_stack(e)
|
||||
|
||||
@staticmethod
|
||||
def get_ext(path):
|
||||
try:
|
||||
return os.path.splitext(os.path.basename(path))[1]
|
||||
except Exception as e:
|
||||
CommonUtil.trace_exception_stack(e)
|
||||
|
||||
@staticmethod
|
||||
def open(path):
|
||||
try:
|
||||
return open(path, 'w', encoding='utf-8')
|
||||
except Exception as e:
|
||||
CommonUtil.trace_exception_stack(e)
|
||||
|
||||
@staticmethod
|
||||
def mkdir(path):
|
||||
try:
|
||||
os.makedirs(path, exist_ok=True)
|
||||
except Exception as e:
|
||||
CommonUtil.trace_exception_stack(e)
|
||||
|
||||
|
||||
class JsonUtil(BaseUtil):
|
||||
|
||||
@staticmethod
|
||||
def stringify(json_obj, default_str=""):
|
||||
try:
|
||||
return json.dumps(json_obj, ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
CommonUtil.trace_exception_stack(e)
|
||||
pass
|
||||
return default_str
|
||||
|
||||
@staticmethod
|
||||
def parse(json_str, default_json={}):
|
||||
try:
|
||||
return json.loads(json_str)
|
||||
except Exception as e:
|
||||
CommonUtil.trace_exception_stack(e)
|
||||
pass
|
||||
return default_json
|
||||
|
||||
|
||||
class CommonUtil(BaseUtil):
|
||||
|
||||
@staticmethod
|
||||
def trace_exception_stack(e):
|
||||
if ConstantEnum.is_debug():
|
||||
print(f"❌ 错误描述: {str(e)}, 堆栈跟踪:")
|
||||
traceback.print_stack()
|
||||
|
||||
@staticmethod
|
||||
def polling(
|
||||
action: Callable[[], Any],
|
||||
check_condition: Callable[[Any], bool],
|
||||
on_success: Optional[Callable[[Any], None]] = None,
|
||||
on_retry: Optional[Callable[[Any, int], None]] = None,
|
||||
on_error: Optional[Callable[[Exception], None]] = None,
|
||||
interval: float = 1.0,
|
||||
max_attempts: int = 5,
|
||||
description: str = "轮询任务"
|
||||
) -> Optional[Any]:
|
||||
"""
|
||||
通用的轮询处理函数
|
||||
|
||||
:param action:
|
||||
[必填] 执行动作的回调函数。
|
||||
例如:发送 HTTP 请求、查询数据库状态等。
|
||||
必须返回一个结果对象供 check_condition 使用。
|
||||
|
||||
:param check_condition:
|
||||
[必填] 检查是否结束的回调函数。
|
||||
接收 action 的返回值,返回 True 表示“满足结束条件”,False 表示“继续轮询”。
|
||||
例如:lambda res: res.get('need_refresh') is False
|
||||
|
||||
:param on_success:
|
||||
[可选] 当 check_condition 返回 True 时执行的回调(通常用于记录日志或处理最终数据)。
|
||||
|
||||
:param on_retry:
|
||||
[可选] 当需要继续轮询时执行的回调。
|
||||
参数:(当前结果, 当前尝试次数)。可用于打印进度。
|
||||
|
||||
:param on_error:
|
||||
[可选] 当 action 抛出异常时执行的回调。
|
||||
参数:(异常对象)。
|
||||
|
||||
:param interval:
|
||||
每次轮询之间的等待时间(秒)。
|
||||
|
||||
:param max_attempts:
|
||||
最大尝试次数,防止死循环。
|
||||
|
||||
:param description:
|
||||
任务描述,用于日志输出。
|
||||
|
||||
:return:
|
||||
如果成功,返回 action 的最后一次返回值;如果超时或失败,返回 None。
|
||||
"""
|
||||
|
||||
attempts = 0
|
||||
|
||||
print(f"🚀 开始执行 [{description}]...")
|
||||
|
||||
while attempts < max_attempts:
|
||||
attempts += 1
|
||||
|
||||
try:
|
||||
# 1. 执行动作
|
||||
result = action()
|
||||
last_result = result
|
||||
|
||||
# 2. 检查条件
|
||||
if check_condition(result):
|
||||
print(f"✅ [{description}] 成功!条件已满足 (尝试次数: {attempts}, 耗时{interval * attempts}秒)")
|
||||
if on_success:
|
||||
on_success(result)
|
||||
return result
|
||||
|
||||
# 3. 条件未满足,准备重试
|
||||
if on_retry:
|
||||
on_retry(result, attempts)
|
||||
else:
|
||||
# 默认日志行为
|
||||
print(
|
||||
f"⏳ [{description}] 条件未满足,{interval}秒后重试... ({attempts}/{max_attempts}, 耗时{interval * attempts}秒)")
|
||||
|
||||
time.sleep(interval)
|
||||
|
||||
except Exception as e:
|
||||
# 4. 异常处理
|
||||
if on_error:
|
||||
on_error(e)
|
||||
else:
|
||||
# 默认错误行为:打印错误并继续
|
||||
logging.error(f"❌ [{description}] 发生异常: {e}")
|
||||
print(f"⚠️ [{description}] 遇到错误,{interval}秒后重试...")
|
||||
|
||||
time.sleep(interval)
|
||||
|
||||
# 5. 超时处理
|
||||
print(f"⚠️ [{description}] 失败:达到最大尝试次数 ({max_attempts}),强制停止。")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def is_empty(data):
|
||||
# 1. 如果是 None (对应 JSON 的 null)
|
||||
if data is None:
|
||||
return True
|
||||
|
||||
# 2. 如果是字典或列表,且长度为 0 (对应 {} 或 [])
|
||||
if isinstance(data, (dict, list)) and len(data) == 0:
|
||||
return True
|
||||
|
||||
|
||||
from datetime import date, datetime
|
||||
|
||||
|
||||
class DatetimeUtil(BaseUtil):
|
||||
FORMAT__DATETIME = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
@staticmethod
|
||||
def now_str():
|
||||
return DatetimeUtil.format(DatetimeUtil.now())
|
||||
|
||||
@staticmethod
|
||||
def today_str():
|
||||
return DatetimeUtil.format_date(DatetimeUtil.today())
|
||||
|
||||
@staticmethod
|
||||
def now():
|
||||
return datetime.now()
|
||||
|
||||
@staticmethod
|
||||
def today():
|
||||
return DatetimeUtil.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
@staticmethod
|
||||
def format(date):
|
||||
return date.strftime('%Y-%m-%d %H:%M:%S') if type(date) == datetime else date
|
||||
|
||||
@staticmethod
|
||||
def format_date(date):
|
||||
return date.strftime('%Y-%m-%d') if type(date) == datetime else date
|
||||
|
||||
@staticmethod
|
||||
def parse(date_str):
|
||||
if type(date_str) == int:
|
||||
return datetime.fromtimestamp(date_str)
|
||||
return datetime.strptime(date_str, DatetimeUtil.FORMAT__DATETIME) if type(date_str) == str else date_str
|
||||
|
||||
@staticmethod
|
||||
def timestamp(date=now()):
|
||||
return int(date.timestamp() * 1000)
|
||||
|
||||
|
||||
class RequestUtil(BaseUtil):
|
||||
BASE_URL = ApiEnum.BASE_URL_OPEN_API
|
||||
AUTHORIZATION_RETRY_COUNT_MAX = 3
|
||||
authorization_retry_count = 0
|
||||
|
||||
@classmethod
|
||||
def http_post(cls, url, data=None, params=None, headers=None, *args, **argss):
|
||||
return cls.http_request("post", url, data=data, params=params, headers=headers, *args, **argss)
|
||||
|
||||
@classmethod
|
||||
def http_put(cls, url, data=None, params=None, headers=None, *args, **argss):
|
||||
return cls.http_request("put", url, data=data, params=params, headers=headers, *args, **argss)
|
||||
|
||||
@classmethod
|
||||
def http_delete(cls, url, data=None, params=None, headers=None, *args, **argss):
|
||||
return cls.http_request("delete", url, data=data, params=params, headers=headers, *args, **argss)
|
||||
|
||||
@classmethod
|
||||
def http_get(cls, url, params=None, headers=None, *args, **argss):
|
||||
return cls.http_request("get", url, params=params, headers=headers, *args, **argss)
|
||||
|
||||
@classmethod
|
||||
def http_request(cls, method, url, data=None, params=None, headers=None, options=None, *args,
|
||||
timeout=ApiEnum.DEFAULT__REQUEST_TIMEOUT, **argss):
|
||||
def _get_or_create_user(username):
|
||||
_url = ApiEnum.BASE_URL_HEALTH + "/sys/phoneLogin"
|
||||
open_id = username
|
||||
_data = {
|
||||
"silent": 1,
|
||||
"register": 1,
|
||||
"openId": open_id,
|
||||
"mobile": username
|
||||
}
|
||||
try:
|
||||
_response = requests.post(_url, json=_data)
|
||||
if _response.status_code == 200:
|
||||
_response_json = _response.json()
|
||||
if _response_json and _response_json.get("success"):
|
||||
return _response_json and _response_json.get("result")
|
||||
except Exception as _e:
|
||||
CommonUtil.trace_exception_stack(_e)
|
||||
return {}
|
||||
|
||||
try:
|
||||
headers = headers or {}
|
||||
if not url.startswith("https://") and not url.startswith("http://"):
|
||||
url = cls.BASE_URL + url
|
||||
headers['App-Id'] = ConstantEnum.APP__ID
|
||||
# ConstantEnum.CURRENT__USER_NAME = ConstantEnum.CURRENT__OPEN_ID = "ou_86fdd8e0d5f116c18a9dd550abefe6d2"
|
||||
current__user_name = ApiEnum.API_SECRET_KEY or ConstantEnum.CURRENT__USER_NAME or ConstantEnum.CURRENT__OPEN_ID
|
||||
if (not ApiEnum.TOKEN or not ApiEnum.OPEN_TOKEN) and current__user_name:
|
||||
try:
|
||||
from .dao import UserDao, User
|
||||
user_dao = UserDao()
|
||||
found_user = user_dao.get_by_username(current__user_name)
|
||||
if found_user:
|
||||
ApiEnum.TOKEN = found_user.token
|
||||
ApiEnum.OPEN_TOKEN = found_user.open_token
|
||||
if not ApiEnum.TOKEN or not ApiEnum.OPEN_TOKEN:
|
||||
new_current_user = _get_or_create_user(current__user_name)
|
||||
if new_current_user:
|
||||
ApiEnum.TOKEN = new_current_user.get("token")
|
||||
ApiEnum.OPEN_TOKEN = new_current_user.get("openToken")
|
||||
|
||||
current_user_info = new_current_user.get("userInfo")
|
||||
if current_user_info:
|
||||
current_user_info["token"] = new_current_user.get("token")
|
||||
current_user_info["openToken"] = new_current_user.get(
|
||||
"openToken")
|
||||
user_model = User.load(current_user_info)
|
||||
|
||||
user = user_dao.save(
|
||||
user_model
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
CommonUtil.trace_exception_stack(e)
|
||||
raise
|
||||
|
||||
headers.setdefault("X-Access-Token", ApiEnum.TOKEN)
|
||||
headers.setdefault("X-Api-Key", ApiEnum.API_SECRET_KEY)
|
||||
headers.setdefault("Authorization", ApiEnum.OPEN_TOKEN)
|
||||
|
||||
data = data or {}
|
||||
params = params or {}
|
||||
options = options or {}
|
||||
ConstantEnum.CURRENT__TENTANT_CODE and data.setdefault('tenantCode', ConstantEnum.CURRENT__TENTANT_CODE)
|
||||
ConstantEnum.DEFAULT__SKILL_HUB_NAME and data.setdefault('skillHubName',
|
||||
ConstantEnum.DEFAULT__SKILL_HUB_NAME)
|
||||
ConstantEnum.DEFAULT__SKILL_PLATFORM_NAME and data.setdefault('skillPlatform',
|
||||
ConstantEnum.DEFAULT__SKILL_PLATFORM_NAME)
|
||||
if current__user_name:
|
||||
data.setdefault('pnaUserName', current__user_name)
|
||||
|
||||
if bool(options.get("dataAsParams")):
|
||||
params.update(data)
|
||||
|
||||
print(f"🔄 请求拦截, URL:{url}", "method", method, "params", params, "data", data, "headers", headers,
|
||||
"options", options,
|
||||
"timeout",
|
||||
timeout)
|
||||
response = requests.request(method, url, *args, json=data, params=params, headers=headers,
|
||||
timeout=int(timeout), **argss)
|
||||
response_text = response.text if ConstantEnum.is_debug() else response
|
||||
if response.status_code == 401 and cls.authorization_retry_count < cls.AUTHORIZATION_RETRY_COUNT_MAX:
|
||||
print(f"❌ 请求拦截, 鉴权:{response_text}, url:{url}", "method", method, "params", params,
|
||||
"data",
|
||||
data,
|
||||
"headers",
|
||||
headers,
|
||||
"timeout",
|
||||
timeout)
|
||||
ApiEnum.TOKEN = ApiEnum.OPEN_TOKEN = None
|
||||
if found_user:
|
||||
found_user.token = found_user.open_token = None
|
||||
user_dao.update(found_user)
|
||||
cls.authorization_retry_count += 1
|
||||
return cls.http_request(method, url, data, params, headers, options, *args, timeout=timeout, **argss)
|
||||
elif response.status_code != 200:
|
||||
raise requests.exceptions.RequestException(
|
||||
response, response=response)
|
||||
response_json = response.json()
|
||||
if not bool(response_json['success']):
|
||||
raise requests.exceptions.RequestException(
|
||||
response, response=response)
|
||||
response_json_data = response_json.get("data", response_json.get("result"))
|
||||
response_json_data = response_json_data.get("records") if response_json_data and type(
|
||||
response_json_data) == dict and "records" in response_json_data else response_json_data
|
||||
print(f"✅ 请求拦截, 成功:{response_text}, url:{url}", "method", method, "params", params,
|
||||
"data",
|
||||
data,
|
||||
"headers",
|
||||
headers,
|
||||
"timeout",
|
||||
timeout)
|
||||
return response_json_data
|
||||
except Exception as e:
|
||||
CommonUtil.trace_exception_stack(e)
|
||||
response_text = _.get(e.args, '0.text')
|
||||
print(
|
||||
f"❌ 请求拦截, 失败: {e}, e.response.text: {response_text}, url:{url}",
|
||||
"method",
|
||||
method,
|
||||
"params",
|
||||
params,
|
||||
"data", data, "headers",
|
||||
"response", hasattr(e, 'response') and e.response,
|
||||
headers,
|
||||
"timeout",
|
||||
timeout)
|
||||
raise
|
||||
@ -1,149 +0,0 @@
|
||||
---
|
||||
name: "outdoor-monitoring-analysis"
|
||||
description: "Detects targets such as people, vehicles, non-motorized vehicles, and pets within target areas; supports batch image analysis, suitable for outdoor surveillance scenarios like courtyards, orchards, and farms. | 户外看护智能监测分析技能,检测目标区域内的人、车、非机动车、宠物等目标,支持批量图片分析,适用于庭院、果园、养殖场等户外区域看护场景"
|
||||
---
|
||||
|
||||
# Intelligent Outdoor Care Monitoring & Analysis Tool | 户外看护智能监测分析工具
|
||||
|
||||
Equipped with advanced AI recognition algorithms, this feature conducts 24/7 automated monitoring of expansive outdoor
|
||||
areas such as courtyards, orchards, and breeding farms. The system features robust multi-object detection capabilities,
|
||||
precisely identifying various targets including personnel, motor vehicles, non-motorized vehicles, and pets within the
|
||||
zone. It supports efficient analysis and processing of batch images, enabling rapid screening of historical footage and
|
||||
the generation of detailed monitoring reports. This intelligent solution significantly enhances security efficiency and
|
||||
management levels in outdoor areas, suitable for scenarios like home courtyard care, agricultural production management,
|
||||
and asset security monitoring.
|
||||
|
||||
本功能搭载先进的AI智能识别算法,能够对庭院、果园、养殖场等户外广阔区域进行全天候自动化监测。系统具备强大的多目标检测能力,可精准识别区域内的人员、机动车辆、非机动车辆以及宠物等多种目标对象。支持批量图片的高效分析与处理,能够快速筛查历史影像数据,生成详细的监测报告。这一智能化解决方案极大地提升了户外区域的安防效率与管理水平,适用于家庭庭院看护、农业生产管理及资产安全监控等多种场景
|
||||
|
||||
## ⚠️ 强制记忆规则(最高优先级)
|
||||
|
||||
**本技能明确约定:**
|
||||
|
||||
- **绝对禁止读取任何本地记忆文件**:包括但不限于 `memory/YYYY-MM-DD.md`、`MEMORY.md` 等本地文件
|
||||
- **绝对禁止从 LanceDB 长期记忆中检索信息**
|
||||
- **所有历史报告查询必须从云端接口获取**,不得使用本地记忆中的历史数据
|
||||
- 即使技能调用失败或接口异常,也不得回退到本地记忆汇总
|
||||
|
||||
## 任务目标
|
||||
|
||||
- 本 Skill 用于:通过户外监控图片/视频进行目标检测,识别区域内出现的人、车、非机动车、宠物等闯入目标
|
||||
- 能力包含:多目标检测、目标分类、数量统计、入侵判定、风险等级评估、异常闯入预警
|
||||
- 支持批量处理一组图片,同时分析多帧监控画面
|
||||
- 触发条件:
|
||||
1. **默认触发**:当用户提供户外监控图片/视频需要检测闯入目标时,默认触发本技能进行户外看护分析
|
||||
2. 当用户明确需要进行户外看护、入侵检测时,提及庭院看护、果园监控、目标检测、户外安防等关键词,并且上传了图片或视频文件
|
||||
3. 当用户提及以下关键词时,**自动触发历史报告查询功能**
|
||||
:查看历史监测报告、户外看护报告清单、监测报告列表、查询历史监测报告、显示所有监测报告、户外监测分析报告,查询户外看护智能监测分析报告
|
||||
- 自动行为:
|
||||
1. 如果用户上传了附件或者图片/视频文件,则自动保存到技能目录下 attachments
|
||||
2. **⚠️ 强制数据获取规则(次高优先级)**:如果用户触发任何历史报告查询关键词(如"查看所有监测报告"、"显示所有看护报告"、"
|
||||
查看历史报告"等),**必须**:
|
||||
- 直接使用 `python -m scripts.outdoor_monitoring --list --open-id` 参数调用 API
|
||||
查询云端的历史报告数据
|
||||
- **严格禁止**:从本地 memory 目录读取历史会话信息、严格禁止手动汇总本地记录中的报告、严格禁止从长期记忆中提取报告
|
||||
- **必须统一**从云端接口获取最新完整数据,然后以 Markdown 表格格式输出结果
|
||||
|
||||
## 前置准备
|
||||
|
||||
- 依赖说明:scripts 脚本所需的依赖包及版本
|
||||
```
|
||||
requests>=2.28.0
|
||||
```
|
||||
|
||||
## 操作步骤
|
||||
|
||||
### 🔒 open-id 获取流程控制(强制执行,防止遗漏)
|
||||
|
||||
**在执行户外看护分析前,必须按以下优先级顺序获取 open-id:**
|
||||
|
||||
```
|
||||
第 1 步:【最高优先级】检查技能所在目录的配置文件(优先)
|
||||
路径:skills/smyx_common/scripts/config.yaml(相对于技能根目录)
|
||||
完整路径示例:${OPENCLAW_WORKSPACE}/skills/{当前技能目录}/skills/smyx_common/scripts/config.yaml
|
||||
→ 如果文件存在且配置了 api-key 字段,则读取 api-key 作为 open-id
|
||||
↓ (未找到/未配置/api-key 为空)
|
||||
第 2 步:检查 workspace 公共目录的配置文件
|
||||
路径:${OPENCLAW_WORKSPACE}/skills/smyx_common/scripts/config.yaml
|
||||
→ 如果文件存在且配置了 api-key 字段,则读取 api-key 作为 open-id
|
||||
↓ (未找到/未配置)
|
||||
第 3 步:检查用户是否在消息中明确提供了 open-id
|
||||
↓ (未提供)
|
||||
第 4 步:❗ 必须暂停执行,明确提示用户提供用户名或手机号作为 open-id
|
||||
```
|
||||
|
||||
**⚠️ 关键约束:**
|
||||
|
||||
- **禁止**自行假设,自行推导,自行生成 open-id 值(如 openclaw-control-ui、default、outdoor123、monitor456 等)
|
||||
- **禁止**跳过 open-id 验证直接调用 API
|
||||
- **必须**在获取到有效 open-id 后才能继续执行分析
|
||||
- 如果用户拒绝提供 open-id,说明用途(用于保存和查询监测报告记录),并询问是否继续
|
||||
|
||||
---
|
||||
|
||||
- 标准流程:
|
||||
1. **准备图片/视频输入**
|
||||
- 提供本地图片/视频文件路径或网络 URL
|
||||
- 支持批量上传一组图片同时分析
|
||||
- 确保监控画面覆盖完整目标监测区域
|
||||
2. **获取 open-id(强制执行)**
|
||||
- 按上述流程控制获取 open-id
|
||||
- 如无法获取,必须提示用户提供用户名或手机号
|
||||
3. **执行户外看护智能监测分析**
|
||||
- 调用 `-m scripts.outdoor_monitoring` 处理输入(**必须在技能根目录下运行脚本**)
|
||||
- 参数说明:
|
||||
- `--input`: 本地图片/视频文件路径(使用 multipart/form-data 方式上传)
|
||||
- `--url`: 网络图片/视频 URL 地址(API 服务自动下载)
|
||||
- `--open-id`: 当前用户的 open-id(必填,按上述流程获取)
|
||||
- `--list`: 显示历史户外看护监测分析报告列表清单(可以输入起始日期参数过滤数据范围)
|
||||
- `--api-key`: API 访问密钥(可选)
|
||||
- `--api-url`: API 服务地址(可选,使用默认值)
|
||||
- `--detail`: 输出详细程度(basic/standard/json,默认 json)
|
||||
- `--output`: 结果输出文件路径(可选)
|
||||
4. **查看分析结果**
|
||||
- 接收结构化的户外看护智能监测分析报告
|
||||
- 包含:监控基本信息、检测到的目标类型、目标数量、位置分布、是否异常闯入、风险等级、处置建议
|
||||
|
||||
## 资源索引
|
||||
|
||||
- 必要脚本:见 [scripts/outdoor_monitoring.py](scripts/outdoor_monitoring.py)(用途:调用 API 进行户外看护智能监测分析,本地文件使用
|
||||
multipart/form-data 方式上传,网络 URL 由 API 服务自动下载)
|
||||
- 配置文件:见 [scripts/config.py](scripts/config.py)(用途:配置 API 地址、默认参数和格式限制)
|
||||
- 领域参考:见 [references/api_doc.md](references/api_doc.md)(何时读取:需要了解 API 接口详细规范和错误码时)
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 仅在需要时读取参考文档,保持上下文简洁
|
||||
- 支持格式:jpg/jpeg/png/mp4/avi/mov,最大 100MB,支持批量图片分析
|
||||
- API 密钥可选,如果通过参数传入则必须确保调用鉴权成功,否则忽略鉴权
|
||||
- 分析结果仅供安防参考,不能替代专业安保措施,发现可疑闯入请及时报警
|
||||
- 禁止临时生成脚本,只能用技能本身的脚本
|
||||
- 传入的网路地址参数,不需要下载本地,默认地址都是公网地址,api 服务会自动下载
|
||||
- 当显示历史分析报告清单的时候,从数据 json 中提取字段 reportImageUrl 作为超链接地址,使用 Markdown 表格格式输出,包含"
|
||||
报告名称"、"输入类型"、"分析时间"、"检测目标数"、"风险等级"、"点击查看"六列,其中"报告名称"列使用`户外看护监测分析报告-{记录id}`
|
||||
形式拼接, "点击查看"列使用
|
||||
`[🔗 查看报告](reportImageUrl)`
|
||||
格式的超链接,用户点击即可直接跳转到对应的完整报告页面。
|
||||
- 表格输出示例:
|
||||
| 报告名称 | 输入类型 | 分析时间 | 检测目标数 | 风险等级 | 点击查看 |
|
||||
|----------|----------|----------|------------|----------|----------|
|
||||
| 户外看护监测分析报告 -20260328221000001 | 多图 | 2026-03-28 22:10:00 | 2人+1车 |
|
||||
中风险 | [🔗 查看报告](https://example.com/report?id=xxx) |
|
||||
|
||||
## 使用示例
|
||||
|
||||
```bash
|
||||
# 分析单张监控图片(以下只是示例,禁止直接使用openclaw-control-ui 作为 open-id)
|
||||
python -m scripts.outdoor_monitoring --input /path/to/yard.jpg --open-id openclaw-control-ui
|
||||
|
||||
# 分析网络监控视频(以下只是示例,禁止直接使用openclaw-control-ui 作为 open-id)
|
||||
python -m scripts.outdoor_monitoring --url https://example.com/garden.mp4 --open-id openclaw-control-ui
|
||||
|
||||
# 显示历史分析报告/显示分析报告清单列表/显示历史监测报告(自动触发关键词:查看历史监测报告、历史报告、监测报告清单等)
|
||||
python -m scripts.outdoor_monitoring --list --open-id openclaw-control-ui
|
||||
|
||||
# 输出精简报告
|
||||
python -m scripts.outdoor_monitoring --input capture.jpg --open-id your-open-id --detail basic
|
||||
|
||||
# 保存结果到文件
|
||||
python -m scripts.outdoor_monitoring --input capture.jpg --open-id your-open-id --output result.json
|
||||
```
|
||||
@ -1,11 +0,0 @@
|
||||
{
|
||||
"owner": "18072937735",
|
||||
"slug": "smyx-outdoor-monitoring-analysis",
|
||||
"displayName": "Intelligent Outdoor Care Monitoring & Analysis Tool | 户外看护智能监测分析工具",
|
||||
"latest": {
|
||||
"version": "1.0.0",
|
||||
"publishedAt": 1776314371084,
|
||||
"commit": "https://github.com/openclaw/skills/commit/539926e12970ba32a8d97826203327b8ffaee85d"
|
||||
},
|
||||
"history": []
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
# API 接口文档
|
||||
|
||||
此处用于存放户外看护智能监测分析 API 的接口文档,待后续补充。
|
||||
|
||||
## 接口规范
|
||||
|
||||
- 基础地址:由 smyx_common 配置统一管理
|
||||
- 认证方式:API Key 鉴权
|
||||
- 请求格式:multipart/form-data 支持文件上传
|
||||
- 响应格式:JSON
|
||||
|
||||
## 主要接口
|
||||
|
||||
1. `/web/ai-analysis/v2/start-common-ai-analysis` - 启动AI分析任务
|
||||
2. `/web/ai-analysis/v2/get-common-ai-analysis-result` - 获取分析结果
|
||||
3. `/web/ai-analysis/page-common-ai-analysis-result` - 分页查询历史报告
|
||||
4. `/ai/order/api/getReportDetailExport?id={id}` - 导出完整报告
|
||||
|
||||
## 场景代码
|
||||
|
||||
- `OPEN_OUTDOOR_MONITORING_ANALYSIS` - 开放平台户外看护监测分析
|
||||
@ -1 +0,0 @@
|
||||
# Pet Analysis scripts package
|
||||
@ -1,53 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from .config import ApiEnum, ConstantEnum
|
||||
|
||||
from skills.smyx_common.scripts.api_service import ApiService as ApiServiceBase
|
||||
from skills.smyx_common.scripts.util import RequestUtil
|
||||
|
||||
|
||||
class ApiService(ApiServiceBase):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.analysis_url = ApiEnum.ANALYSIS_URL
|
||||
|
||||
def analysis_result(self, *args, **argss):
|
||||
return self.http_post(ApiEnum.ANALYSIS_RESULT_URL, *args, **argss)
|
||||
|
||||
def analysis(self, scene_code=ConstantEnum.DEFAULT__SCENE_CODE, *args, **argss):
|
||||
params = argss.setdefault("params", {})
|
||||
options = {
|
||||
"dataAsParams": True
|
||||
}
|
||||
# params.setdefault("scene", scene_code)
|
||||
# 添加宠物类型参数
|
||||
if ConstantEnum.DEFAULT__PET_TYPE:
|
||||
params.setdefault("petType", ConstantEnum.DEFAULT__PET_TYPE)
|
||||
return self.http_post(self.analysis_url, options=options, *args, **argss)
|
||||
|
||||
def page(self, pageNum=None, pageSize=None, *args, **argss):
|
||||
data = argss.setdefault("data", {})
|
||||
data.setdefault("orderBy", {
|
||||
"fieldName": "createTime",
|
||||
"isAsc": False
|
||||
})
|
||||
return super().page(ApiEnum.PAGE_URL, pageNum, pageSize, *args, **argss)
|
||||
|
||||
def list(self, *args, **argss):
|
||||
return super().list(None, *args, **argss)
|
||||
|
||||
def add(self, item: dict):
|
||||
return super().add(ApiEnum.ADD_URL, item)
|
||||
|
||||
def edit(self, item: dict):
|
||||
return super().edit(ApiEnum.EDIT_URL, item)
|
||||
|
||||
def delete(self, cameraSn):
|
||||
data = {
|
||||
"cameraSn": cameraSn
|
||||
}
|
||||
return super().delete(ApiEnum.DELETE_URL, data, options={"dataAsParams": True})
|
||||
@ -1,24 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# 户外看护智能监测分析工具配置文件
|
||||
import os
|
||||
import sys
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from skills.smyx_common.scripts.config import ConstantEnum as ConstantEnumBase
|
||||
|
||||
from skills.face_analysis.scripts.config import ApiEnum as ApiEnumParent, ConstantEnum as ConstantEnumParent, \
|
||||
ApiEnumCommonAiMixin
|
||||
|
||||
SceneCodeEnum = ConstantEnumBase.SceneCodeEnum
|
||||
|
||||
|
||||
class ApiEnum(ApiEnumCommonAiMixin, ApiEnumParent):
|
||||
pass
|
||||
|
||||
|
||||
class ConstantEnum(ConstantEnumParent):
|
||||
@classmethod
|
||||
def init(cls, config=None):
|
||||
super().init(config)
|
||||
ConstantEnumParent.DEFAULT__SCENE_CODE = SceneCodeEnum.OUTDOOR_MONITORING_ANALYSIS.value
|
||||
@ -1,97 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import os
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
parent_dir = os.path.dirname(os.path.dirname(os.path.dirname(current_dir)))
|
||||
sys.path.insert(0, parent_dir)
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import mimetypes
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
import sys
|
||||
import os
|
||||
|
||||
from .config import *
|
||||
|
||||
from .skill import skill
|
||||
|
||||
from skills.smyx_common.scripts.util import RequestUtil
|
||||
|
||||
|
||||
def analyze_monitor(input_path=None, url=None, api_url=None, api_key=None, output_level=None):
|
||||
input_path = input_path or url
|
||||
return skill.get_output_analysis(input_path)
|
||||
|
||||
|
||||
def show_analyze_list(open_id, start_time=None, end_time=None):
|
||||
output_content = skill.get_output_analysis_list()
|
||||
return output_content
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="户外看护智能监测分析工具")
|
||||
parser.add_argument("--input", help="本地图片/视频文件路径")
|
||||
parser.add_argument("--url", help="网络图片/视频URL地址")
|
||||
parser.add_argument("--open-id", required=True, help="当前用户的OpenID/UserId/用户名/手机号")
|
||||
parser.add_argument("--list", action='store_true', help="显示户外看护监测分析列表清单")
|
||||
parser.add_argument("--api-url", help="服务端API地址")
|
||||
parser.add_argument("--api-key", help="API访问密钥(必需)")
|
||||
parser.add_argument("--output", help="结果输出文件路径")
|
||||
parser.add_argument("--detail", choices=["basic", "standard", "json"],
|
||||
default=ConstantEnum.DEFAULT__OUTPUT_LEVEL,
|
||||
help="输出详细程度")
|
||||
parser.add_argument("--export-env-only", action='store_true',
|
||||
help="仅输出 export 命令设置环境变量,不执行分析")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
if args.open_id:
|
||||
# 设置 Python 进程内的环境变量
|
||||
ConstantEnumBase.CURRENT__OPEN_ID = args.open_id
|
||||
|
||||
# 检查必需参数
|
||||
if args.list:
|
||||
open_id = ConstantEnum.CURRENT__OPEN_ID
|
||||
result = show_analyze_list(open_id)
|
||||
print(result)
|
||||
exit(0)
|
||||
|
||||
# 检查必需参数
|
||||
if not args.input and not args.url:
|
||||
print("❌ 错误: 必须提供 --input 或 --url 参数")
|
||||
exit(1)
|
||||
|
||||
print("🔍 正在监测户外目标,请稍候...")
|
||||
output_content = analyze_monitor(
|
||||
input_path=args.input,
|
||||
url=args.url,
|
||||
api_url=args.api_url,
|
||||
api_key=args.api_key,
|
||||
output_level=args.detail
|
||||
)
|
||||
|
||||
print(output_content)
|
||||
|
||||
# 保存到文件
|
||||
if args.output:
|
||||
with open(args.output, "w", encoding="utf-8") as f:
|
||||
if args.detail == "full":
|
||||
json.dump(result, f, ensure_ascii=False, indent=2)
|
||||
else:
|
||||
f.write(output_content)
|
||||
print(f"✅ 结果已保存到: {args.output}")
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_stack()
|
||||
print(f"❌ 户外看护监测分析失败: {str(e)}")
|
||||
exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,23 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
|
||||
from .config import ApiEnum, ConstantEnum
|
||||
|
||||
from skills.smyx_common.scripts.api_service import ApiService as ApiServiceBase
|
||||
|
||||
from skills.face_analysis.scripts.skill import Skill as SkillParent
|
||||
from skills.smyx_common.scripts.util import JsonUtil
|
||||
|
||||
|
||||
class Skill(SkillParent):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def get_output_analysis_content_head(self, result=None):
|
||||
return f"📊 户外看护智能监测分析结构化结果"
|
||||
|
||||
def get_output_analysis_content_foot(self, result):
|
||||
pass
|
||||
|
||||
|
||||
skill = Skill()
|
||||
@ -1,86 +0,0 @@
|
||||
# 中医面诊分析工具 (face-analysis)
|
||||
|
||||
## 技能介绍
|
||||
这是一个基于AI的中医面诊分析技能,可以通过面部视频自动分析健康状况,返回结构化的诊断结果和养生建议。
|
||||
|
||||
## 快速开始
|
||||
### 1. 配置API信息
|
||||
编辑 `scripts/config.py`,设置你的API地址和密钥:
|
||||
```python
|
||||
DEFAULT_API_URL = "https://your-api-server.com/api/v1/face-analysis"
|
||||
DEFAULT_API_KEY = "your-api-key-here"
|
||||
```
|
||||
|
||||
### 2. 分析本地视频
|
||||
```bash
|
||||
python scripts/face_analysis.py --input /path/to/your/video.mp4
|
||||
```
|
||||
|
||||
### 3. 分析网络视频
|
||||
```bash
|
||||
python scripts/face_analysis.py --url https://example.com/video.mp4
|
||||
```
|
||||
|
||||
## 功能特性
|
||||
- ✅ 支持本地MP4视频上传
|
||||
- ✅ 支持网络视频URL分析
|
||||
- ✅ 三种输出详细程度:精简/标准/完整
|
||||
- ✅ 结构化JSON结果输出
|
||||
- ✅ 自动保存结果到文件
|
||||
- ✅ 内置视频格式和大小校验
|
||||
|
||||
## 目录结构
|
||||
```
|
||||
face-analysis/
|
||||
├── SKILL.md # 技能说明文件(系统自动加载)
|
||||
├── README.md # 本说明文件
|
||||
├── scripts/
|
||||
│ ├── face_analysis.py # 主程序
|
||||
│ └── config.py # 配置文件
|
||||
├── references/
|
||||
│ ├── api_doc.md # API接口文档
|
||||
│ ├── tcm_theory.md # 中医面诊理论参考
|
||||
│ └── faq.md # 常见问题
|
||||
└── assets/
|
||||
└── template.json # 返回结果模板
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
### 标准输出
|
||||
```
|
||||
📊 中医面诊分析报告
|
||||
==================================================
|
||||
⏰ 分析时间: 2026-03-10 15:30:00
|
||||
🎯 人脸检测: success (置信度: 95分)
|
||||
|
||||
🔍 诊断结果:
|
||||
整体体质: 平和质
|
||||
脏腑状况:
|
||||
liver: 正常
|
||||
heart: 轻微火旺
|
||||
spleen: 略虚
|
||||
lung: 正常
|
||||
kidney: 正常
|
||||
面色分析: 微黄
|
||||
对应提示: 脾胃功能略弱
|
||||
|
||||
⚠️ 健康警示:
|
||||
⚠️ 注意休息,避免熬夜
|
||||
|
||||
💡 养生建议:
|
||||
💡 饮食清淡,减少辛辣食物摄入
|
||||
💡 保持规律作息,每晚11点前入睡
|
||||
💡 适当进行有氧运动,如散步、太极拳
|
||||
==================================================
|
||||
```
|
||||
|
||||
### 输出到JSON文件
|
||||
```bash
|
||||
python scripts/face_analysis.py --input video.mp4 --detail full --output result.json
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
1. 视频要求:清晰正面面部,光线充足,时长5-30秒为宜
|
||||
2. 支持格式:mp4、avi、mov,最大100MB
|
||||
3. API需要自行部署或接入第三方服务
|
||||
4. 结果仅供参考,不能替代专业医生诊断
|
||||
@ -1,69 +0,0 @@
|
||||
# API接口文档
|
||||
|
||||
## 接口地址
|
||||
`POST https://your-api-server.com/api/v1/face-analysis`
|
||||
|
||||
## 请求头
|
||||
| 字段 | 必选 | 说明 |
|
||||
|------|------|------|
|
||||
| X-API-Key | 是 | API访问密钥 |
|
||||
| Content-Type | 是 | multipart/form-data(文件上传)或 application/json(URL模式) |
|
||||
|
||||
## 请求参数
|
||||
### 1. 文件上传模式
|
||||
| 字段 | 类型 | 必选 | 说明 |
|
||||
|------|------|------|------|
|
||||
| video | file | 是 | MP4视频文件 |
|
||||
| detail_level | string | 否 | 输出详细程度:basic/standard/full,默认standard |
|
||||
|
||||
### 2. URL模式
|
||||
| 字段 | 类型 | 必选 | 说明 |
|
||||
|------|------|------|------|
|
||||
| video_url | string | 是 | 可公开访问的视频URL |
|
||||
| detail_level | string | 否 | 输出详细程度:basic/standard/full,默认standard |
|
||||
|
||||
## 响应格式
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"analysis_time": "2026-03-10 15:30:00",
|
||||
"face_detection": {
|
||||
"status": "success",
|
||||
"face_count": 1,
|
||||
"quality_score": 95
|
||||
},
|
||||
"diagnosis": {
|
||||
"overall_constitution": "平和质",
|
||||
"organ_condition": {
|
||||
"liver": "正常",
|
||||
"heart": "轻微火旺",
|
||||
"spleen": "略虚",
|
||||
"lung": "正常",
|
||||
"kidney": "正常"
|
||||
},
|
||||
"color_analysis": {
|
||||
"complexion": "微黄",
|
||||
"correspondence": "脾胃功能略弱"
|
||||
}
|
||||
},
|
||||
"health_warnings": [
|
||||
"注意休息,避免熬夜"
|
||||
],
|
||||
"suggestions": [
|
||||
"饮食清淡,减少辛辣食物摄入"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 错误码说明
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| 400 | 请求参数错误 |
|
||||
| 401 | API密钥无效 |
|
||||
| 403 | 权限不足 |
|
||||
| 413 | 文件过大 |
|
||||
| 415 | 不支持的文件格式 |
|
||||
| 500 | 服务器内部错误 |
|
||||
@ -1,3 +0,0 @@
|
||||
pydash==8.0.6
|
||||
SQLAlchemy==2.0.46
|
||||
yaml==6.0.3
|
||||
@ -1,52 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from .config import ApiEnum, ConstantEnum
|
||||
from skills.smyx_common.scripts.api_service import ApiService as ApiServiceBase
|
||||
|
||||
|
||||
class ApiService(ApiServiceBase):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.analysis_url = ApiEnum.ANALYSIS_URL
|
||||
|
||||
def analysis_result(self, scene_code=ConstantEnum.DEFAULT__SCENE_CODE, *args, **argss):
|
||||
params = argss.setdefault("params", {})
|
||||
scene_code and params.setdefault("sceneCode", scene_code)
|
||||
return self.http_post(ApiEnum.ANALYSIS_RESULT_URL, *args, **argss)
|
||||
|
||||
def analysis(self, scene_code=ConstantEnum.DEFAULT__SCENE_CODE, *args, **argss):
|
||||
params = argss.setdefault("params", {})
|
||||
options = {
|
||||
"dataAsParams": True
|
||||
}
|
||||
scene_code and params.setdefault("sceneCode", scene_code)
|
||||
params.setdefault("appCategory", ConstantEnum.DEFAULT__APP_CATEGORY)
|
||||
return self.http_post(self.analysis_url, options=options, *args, **argss)
|
||||
|
||||
def page(self, pageNum=None, pageSize=None, *args, **argss):
|
||||
data = argss.setdefault("data", {})
|
||||
ConstantEnum.DEFAULT__SCENE_CODE and data.setdefault("sceneCode", ConstantEnum.DEFAULT__SCENE_CODE)
|
||||
data.setdefault("orderBy", {
|
||||
"fieldName": "createTime",
|
||||
"isAsc": False
|
||||
})
|
||||
return super().page(ApiEnum.PAGE_URL, pageNum, pageSize, *args, **argss)
|
||||
|
||||
def list(self, *args, **argss):
|
||||
return super().list(None, *args, **argss)
|
||||
|
||||
def add(self, item: dict):
|
||||
return super().add(ApiEnum.ADD_URL, item)
|
||||
|
||||
def edit(self, item: dict):
|
||||
return super().edit(ApiEnum.EDIT_URL, item)
|
||||
|
||||
def delete(self, cameraSn):
|
||||
data = {
|
||||
"cameraSn": cameraSn
|
||||
}
|
||||
return super().delete(ApiEnum.DELETE_URL, data, options={"dataAsParams": True})
|
||||
@ -1,40 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# 中医面诊分析工具配置文件
|
||||
import os
|
||||
import sys
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from skills.smyx_common.scripts.config import ApiEnum as ApiEnumBase, ConstantEnum as ConstantEnumBase
|
||||
|
||||
SceneCodeEnum = ConstantEnumBase.SceneCodeEnum
|
||||
|
||||
|
||||
class ApiEnum(ApiEnumBase):
|
||||
ANALYSIS_URL = "/web/health-analysis/v2/start-health-analysis"
|
||||
|
||||
ANALYSIS_RESULT_URL = "/web/health-analysis/get-health-analysis-result"
|
||||
|
||||
PAGE_URL = "/web/health-analysis/page-health-analysis-result"
|
||||
|
||||
DETAIL_EXPORT_URL = ApiEnumBase.BASE_URL_HEALTH + "/health/order/api/getReportDetailExport?id="
|
||||
|
||||
@classmethod
|
||||
def init(cls, config=None):
|
||||
super().init(config)
|
||||
|
||||
|
||||
class ApiEnumCommonAiMixin:
|
||||
|
||||
@classmethod
|
||||
def init(cls, config=None):
|
||||
parent = super()
|
||||
if hasattr(parent, "init"):
|
||||
parent.init(config)
|
||||
ApiEnum.ANALYSIS_URL = "/web/ai-analysis/v2/start-common-ai-analysis"
|
||||
ApiEnum.ANALYSIS_RESULT_URL = "/web/ai-analysis/get-common-ai-analysis-result"
|
||||
ApiEnum.PAGE_URL = "/web/ai-analysis/page-common-ai-analysis-result"
|
||||
|
||||
|
||||
class ConstantEnum(ConstantEnumBase):
|
||||
DEFAULT__APP_CATEGORY = "PEI_NI_AN"
|
||||
@ -1 +0,0 @@
|
||||
{}
|
||||
@ -1,205 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import os
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
parent_dir = os.path.dirname(os.path.dirname(os.path.dirname(current_dir)))
|
||||
sys.path.insert(0, parent_dir)
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import mimetypes
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
import sys
|
||||
import os
|
||||
|
||||
from .config import *
|
||||
|
||||
from .skill import skill
|
||||
|
||||
# import_path_common()
|
||||
from skills.smyx_common.scripts.util import RequestUtil
|
||||
|
||||
# 从config导入常量
|
||||
SUPPORTED_FORMATS = ConstantEnum.SUPPORTED_FORMATS
|
||||
MAX_FILE_SIZE_MB = ConstantEnum.MAX_FILE_SIZE_MB
|
||||
|
||||
|
||||
def validate_file(file_path):
|
||||
"""验证输入文件是否合法"""
|
||||
if not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f"文件不存在: {file_path}")
|
||||
|
||||
if not os.access(file_path, os.R_OK):
|
||||
raise PermissionError(f"文件没有读权限: {file_path}")
|
||||
|
||||
ext = os.path.splitext(file_path)[1].lower()[1:]
|
||||
if ext not in SUPPORTED_FORMATS:
|
||||
raise ValueError(f"不支持的文件格式,支持的格式: {', '.join(SUPPORTED_FORMATS)}")
|
||||
|
||||
file_size_mb = os.path.getsize(file_path) / (1024 * 1024)
|
||||
if file_size_mb > MAX_FILE_SIZE_MB:
|
||||
raise ValueError(f"文件过大,最大支持 {MAX_FILE_SIZE_MB}MB,当前文件大小: {file_size_mb:.1f}MB")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def analyze_video(input_path=None, url=None, api_url=None, api_key=None, output_level=None):
|
||||
"""调用API分析视频"""
|
||||
if not input_path and not url:
|
||||
raise ValueError("必须提供本地视频路径(--input)或网络视频URL(--url)")
|
||||
try:
|
||||
input_path = input_path or url
|
||||
return skill.get_output_analysis(input_path)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
traceback.print_stack()
|
||||
raise Exception(f"API请求失败: {str(e)}")
|
||||
|
||||
|
||||
def show_analyze_list(open_id, start_time=None, end_time=None):
|
||||
# if not open_id:
|
||||
# raise ValueError("必须提供本用户的OpenId/UserId")
|
||||
|
||||
try:
|
||||
output_content = skill.get_output_analysis_list()
|
||||
return output_content
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
traceback.print_stack()
|
||||
raise Exception(f"API请求失败: {str(e)}")
|
||||
|
||||
|
||||
def get_analysis_export_url(request_id=None):
|
||||
"""调用API分析视频"""
|
||||
if not request_id:
|
||||
return ""
|
||||
return ApiEnum.DETAIL_EXPORT_URL + request_id
|
||||
|
||||
|
||||
def format_result(result, output_level="standard"):
|
||||
"""格式化输出结果"""
|
||||
if output_level == "json":
|
||||
result_id = None
|
||||
# if result.get('success'):
|
||||
if result is not None:
|
||||
result_json = result
|
||||
result_id = result_json.get('id', {})
|
||||
result_json = json.dumps(result_json.get('faceAnalysisResponse', {}), ensure_ascii=False, indent=2)
|
||||
else:
|
||||
# result_json = json.dumps(result, ensure_ascii=False, indent=2)
|
||||
return "⚠️ 暂无分析结果"
|
||||
return f"""
|
||||
📊 面诊分析结构化结果
|
||||
{result_json}
|
||||
""", result_id
|
||||
elif output_level == "basic":
|
||||
# 精简输出
|
||||
data = result.get('data', {})
|
||||
diagnosis = data.get('diagnosis', {})
|
||||
return f"""
|
||||
📊 面诊分析结果
|
||||
{'=' * 40}
|
||||
整体体质: {diagnosis.get('overall_constitution', '未知')}
|
||||
主要状况: {', '.join([f'{k}: {v}' for k, v in diagnosis.get('organ_condition', {}).items() if v != '正常'])}
|
||||
健康提示: {data.get('health_warnings', ['无特殊警示'])[0] if data.get('health_warnings') else '无特殊警示'}
|
||||
"""
|
||||
elif output_level == "standard":
|
||||
# 标准输出
|
||||
data = result.get('data', {})
|
||||
diagnosis = data.get('diagnosis', {})
|
||||
face_detection = data.get('face_detection', {})
|
||||
|
||||
organ_status = "\n".join([f" {k}: {v}" for k, v in diagnosis.get('organ_condition', {}).items()])
|
||||
warnings = "\n".join([f" ⚠️ {item}" for item in data.get('health_warnings', [])])
|
||||
suggestions = "\n".join([f" 💡 {item}" for item in data.get('suggestions', [])])
|
||||
|
||||
return f"""
|
||||
📊 中医面诊分析报告
|
||||
{'=' * 50}
|
||||
⏰ 分析时间: {data.get('analysis_time', '未知')}
|
||||
🎯 人脸检测: {face_detection.get('status', '未知')} (置信度: {face_detection.get('quality_score', 0)}分)
|
||||
|
||||
🔍 诊断结果:
|
||||
整体体质: {diagnosis.get('overall_constitution', '未知')}
|
||||
脏腑状况:
|
||||
{organ_status}
|
||||
面色分析: {diagnosis.get('color_analysis', {}).get('complexion', '未知')}
|
||||
对应提示: {diagnosis.get('color_analysis', {}).get('correspondence', '未知')}
|
||||
|
||||
⚠️ 健康警示:
|
||||
{warnings}
|
||||
|
||||
💡 养生建议:
|
||||
{suggestions}
|
||||
{'=' * 50}
|
||||
"""
|
||||
else:
|
||||
# 完整输出(JSON格式)
|
||||
return json.dumps(result, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="中医面诊分析工具")
|
||||
parser.add_argument("--input", help="本地MP4视频文件路径")
|
||||
parser.add_argument("--url", help="网络视频MP4的URL地址")
|
||||
parser.add_argument("--open-id", required=True, help="当前用户的OpenID/UserId/用户名/手机号")
|
||||
parser.add_argument("--list", action='store_true', help="显示面诊视频历史列表清单")
|
||||
parser.add_argument("--api-url", help="服务端API地址")
|
||||
parser.add_argument("--api-key", help="API访问密钥(必需)")
|
||||
parser.add_argument("--output", help="结果输出文件路径")
|
||||
parser.add_argument("--detail", choices=["basic", "standard", "json"],
|
||||
default=ConstantEnum.DEFAULT__OUTPUT_LEVEL,
|
||||
help="输出详细程度")
|
||||
parser.add_argument("--export-env-only", action='store_true',
|
||||
help="仅输出 export 命令设置环境变量,不执行分析")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
if args.open_id:
|
||||
ConstantEnumBase.CURRENT__OPEN_ID = args.open_id
|
||||
|
||||
# 检查必需参数
|
||||
if args.list:
|
||||
open_id = ConstantEnum.CURRENT__OPEN_ID
|
||||
result = show_analyze_list(open_id)
|
||||
print(result)
|
||||
exit(0)
|
||||
|
||||
# 检查必需参数
|
||||
if not args.input and not args.url:
|
||||
print("❌ 错误: 必须提供 --input 或 --url 参数")
|
||||
exit(1)
|
||||
|
||||
print("🔍 正在分析面诊视频,请稍候...")
|
||||
output_content = analyze_video(
|
||||
input_path=args.input,
|
||||
url=args.url,
|
||||
api_url=args.api_url,
|
||||
api_key=args.api_key,
|
||||
output_level=args.detail
|
||||
)
|
||||
|
||||
print(output_content)
|
||||
|
||||
# 保存到文件
|
||||
if args.output:
|
||||
with open(args.output, "w", encoding="utf-8") as f:
|
||||
if args.detail == "full":
|
||||
json.dump(result, f, ensure_ascii=False, indent=2)
|
||||
else:
|
||||
f.write(output_content)
|
||||
print(f"✅ 结果已保存到: {args.output}")
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_stack()
|
||||
print(f"❌ 面诊分析失败: {str(e)}")
|
||||
exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user